Android 端基于 OpenCV 的边框识别功能

  • 2017-09-11
  • 92,529
  • 65

之前写了一个智能图片裁剪库:SmartCropper,选择照片之后会自动识别出边框的位置,适用于身份证,名片,文档等照片的裁剪。

你也可以关注我新开源的另外一个库 SmartCamera,也是基于 OpenCV 实现的, 能够实时采集并且识别相机内物体边框是否吻合指定区域, 由于是实时采集安卓相机视频流并且识别, 对于性能的要求会更高。

本篇文章主要就边框识别部分说一下开发过程及实现原理,通过阅读本篇文章,你将具备以下技能:

  1. 了解 NDK 开发的基本步骤,能使用 Java、C++/C 混合开发简单的应用
  2. 了解 OpenCV 库的作用及其用法,能使用 OpenCV 做图像处理
  3. 了解基于 OpenCV 的边框识别实现

OpenCV 的全称是 Open Source Computer Vision Library,是一个使用 C++ 编写的跨平台的计算机视觉库,能对输入的图片进行处理,包括常见的高斯模糊,提取灰度图片,提取轮廓等等,可以应用于增强现实,人脸识别,运动跟踪,物体识别,图像分区等。

在 Android 平台需要使用 JNI 技术来调用 C++ 的库,事实上,OpenCV 的官网已经提供了编写好的 Android 库:OpenCv4Android,我们可以按照提示导入该库,就可用以使用 Java 代码来调用了。但是该库包含了 OpenCV 所有的模块,造成了该库体积非常大,其中很多并不是我们需要的。所以我的做法是只使用该库提供的编译好的 C++ 库,挑选自己需要用到的模块,引入其动态或者静态库,编写 C++ 代码调用 OpenCV 的这些模块完成主要功能,最后使用 JNI 技术编写 Java 接口供 Android 程序调用。

导入 OpenCV 库

下载好 OpenCv4Android 后解压目录如下所示:

OpenCV-2.4.13-android-sdk
|_ doc
|_ samples
|_ sdk
|    |_ etc
|    |_ java
|    |_ native
|          |_ 3rdparty
|          |_ jni
|          |_ libs
|               |_ armeabi
|               |_ armeabi-v7a
|               |_ x86
|
|_ LICENSE
|_ README.android

sdk/java 目录提供了 OpenCv  的 Java API,导入到项目中,并且将 native/libs 下面的 native 库导入之后就可以使用 OpenCV 的 Java API 了。native/jni 目录下提供了编译用的 cmake 文件以及头文件。

在 SmartCropper 中只使用到了 opencv_core 与 opencv_imgproc 模块,  所以只需要导入这两个模块的头文件与动态库/静态库就行了。
目录如下所示:

smartcropperlib
├── opencv
│   ├── include
│   │      └── opencv2
│   │      ├── core
│   │      ├── imgproc
│   │      ├── opencv.hpp
│   │      └── opencv_modules.hpp
│   └── lib
│        ├── armeabi
│        │   ├── libopencv_core.a
│        │   └── libopencv_imgproc.a
│        ├── armeabi-v7a
│        ├── mips
│        └── x86
├── CMakeLists.txt
└── build.gradle
└── src
     └── main
          ├── cpp
          │    ├── Scanner.cpp
          │    ├── android_utils.cpp
          │    ├── include
          │    │     ├── Scanner.h
          │    │     └── android_utils.h
          │    └── smart_cropper.cpp
          ├── java
          └── res

编写 cmake 文件:


include_directories(opencv/include
                    src/main/cpp/include)

add_library(opencv_imgproc STATIC IMPORTED)
add_library(opencv_core STATIC IMPORTED)

set_target_properties(opencv_imgproc PROPERTIES IMPORTED_LOCATION${PROJECT_SOURCE_DIR}/opencv/lib/${ANDROID_ABI}/libopencv_imgproc.a)
set_target_properties(opencv_core PROPERTIES IMPORTED_LOCATION${PROJECT_SOURCE_DIR}/opencv/lib/${ANDROID_ABI}/libopencv_core.a)

add_library( smart_cropper
             SHARED
             src/main/cpp/Scanner.cpp
             src/main/cpp/smart_cropper.cpp
             src/main/cpp/android_utils.cpp)
find_library( log-lib
              log)
find_library(jnigraphics-lib
             jnigraphics)
target_link_libraries( smart_cropper
                       opencv_imgproc
                       opencv_core
                       ${log-lib}
                       ${jnigraphics-lib})

主要注意点如下:

  1. include_directories添加头文件查找路径,包括引入库的和自己写的
  2. add_library添加动态库或静态库,其中本地的动态库名称,位置可以由set_target_properties设置
  3. find_library通过名称查找并引入库,可以引入 NDK 中的库,比如日志模块
  4. target_link_libraries添加参加编译的库名称,也可以是绝对路径,注意被依赖的模块写在后面

修改 build.gradle 文件:


android {
    //...
    defaultConfig {
        //...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11 -frtti -fexceptions -lz"
                abiFilters 'armeabi'
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
    //...
}

这里指定了 C++ 的版本为11,开启 RTTI,启用异常处理,这样就完成了导入 OpenCV 代码库的配置。

边框识别

SmartCropper 类中提供了图片的边框识别与裁剪:


public class SmartCropper {

    /**
     *  输入图片扫描边框顶点
     * @param srcBmp 扫描图片
     * @return 返回顶点数组,以 左上,右上,右下,左下排序
     */
    public static Point[] scan(Bitmap srcBmp) {
        //...
    }

    /**
     * 裁剪图片
     * @param srcBmp 待裁剪图片
     * @param cropPoints 裁剪区域顶点,顶点坐标以图片大小为准
     * @return 返回裁剪后的图片
     */
    public static Bitmap crop(Bitmap srcBmp, Point[] cropPoints) {
        //...
    }

    private static native void nativeScan(Bitmap srcBitmap, Point[] outPoints);

    private static native void nativeCrop(Bitmap srcBitmap, Point[] points, Bitmap outBitmap);

    static {
        System.loadLibrary("smart_cropper");
    }

}

主要逻辑位于 native 层,先看 nativeScan 方法对应的 C++ 代码:


static void native_scan(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray outPoint_) {
    if (env -> GetArrayLength(outPoint_) != 4) {
        return;
    }
    Mat srcBitmapMat;
    bitmap_to_mat(env, srcBitmap, srcBitmapMat);
    Mat bgrData(srcBitmapMat.rows, srcBitmapMat.cols, CV_8UC3);
    cvtColor(srcBitmapMat, bgrData, CV_RGBA2BGR);
    scanner::Scanner docScanner(bgrData);
    std::vector scanPoints = docScanner.scanPoint();
    if (scanPoints.size() == 4) {
        for (int i = 0; i < 4; ++i) { env -> SetObjectArrayElement(outPoint_, i, createJavaPoint(env, scanPoints[i]));
        }
    }
}

先将传入的 Bitmap 对象转化成 OpenCV 提供的 Mat 对象,你可以理解成一个多维矩阵,保存了位图的信息,在 OpenCV 中 Mat 即为图片 ,所有对图片的操作即为操作 Mat 对象,然后将 RGBA 格式的图片转化成 BGR 格式,这种转化对于 OpenCV 来说十分方便,cvtColor(srcBitmapMat, bgrData, CV_RGBA2BGR); 当然还提供了其他的色彩空间转化。接着调用 scanner::Scanner 对象的 scanPoint 函数获取识别好的四个顶点。
可以看到主要逻辑位于 docScanner.scanPoint() 中,我们先看一下 bitmap_to_ma 是如何将 bitmap 转化成 Mat 对象的:


void bitmap_to_mat(JNIEnv *env, jobject &srcBitmap, Mat &srcMat) {
    void *srcPixels = 0;
    AndroidBitmapInfo srcBitmapInfo;
    try {
        AndroidBitmap_getInfo(env, srcBitmap, &srcBitmapInfo);
        AndroidBitmap_lockPixels(env, srcBitmap, &srcPixels);
        uint32_t srcHeight = srcBitmapInfo.height;
        uint32_t srcWidth = srcBitmapInfo.width;
        srcMat.create(srcHeight, srcWidth, CV_8UC4);
        if (srcBitmapInfo.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            Mat tmp(srcHeight, srcWidth, CV_8UC4, srcPixels);
            tmp.copyTo(srcMat);
        } else {
            Mat tmp = Mat(srcHeight, srcWidth, CV_8UC2, srcPixels);
            cvtColor(tmp, srcMat, COLOR_BGR5652RGBA);
        }
        AndroidBitmap_unlockPixels(env, srcBitmap);
        return;
    } catch (cv::Exception &e) {
        AndroidBitmap_unlockPixels(env, srcBitmap);
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, e.what());
        return;
    } catch (...) {
        AndroidBitmap_unlockPixels(env, srcBitmap);
        jclass je = env->FindClass("java/lang/Exception");
        env -> ThrowNew(je, "unknown");
        return;
    }
}

AndroidBitmapInfo 类,AndroidBitmap_getInfo 方法等位于 NDK 中,使得我们可以在 native 层方便的操作 Java 层的 Bitmap 对象,该库是我们在 CMakeLists 文件中通过 jnigraphics 引入的。
AndroidBitmap_getInfo 获取了 Bitmap 的信息,包括图片宽高,图片格式,然后通过 AndroidBitmap_lockPixels 获取像素数组,接着通过不同的图片格式创建不同的 Mat 容器存放像素数组。最后统一转换成 RGBA 格式返回。

回到之前说的主要函数:docScanner.scanPoint()


vector Scanner::scanPoint() {
    //缩小图片尺寸
    Mat image = resizeImage();
    //预处理图片
    Mat scanImage = preprocessImage(image);
    vector<vector> contours;
    //提取边框
    findContours(scanImage, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
    //按面积排序
    std::sort(contours.begin(), contours.end(), sortByArea);
    vector result;
    if (contours.size() > 0) {
        vector contour = contours[0];
        double arc = arcLength(contour, true);
        vector outDP;
        //多变形逼近
        approxPolyDP(Mat(contour), outDP, 0.02*arc, true);
        //筛选去除相近的点
        vector selectedPoints = selectPoints(outDP, 1);
        if (selectedPoints.size() != 4) {
            //如果筛选出来之后不是四边形,那么使用最小矩形包裹
            RotatedRect rect = minAreaRect(contour);
            Point2f p[4];
            rect.points(p);
            result.push_back(p[0]);
            result.push_back(p[1]);
            result.push_back(p[2]);
            result.push_back(p[3]);
        } else {
            result = selectedPoints;
        }
        for(Point &p : result) {
            p.x *= resizeScale;
            p.y *= resizeScale;
        }
    }
    // 按左上,右上,右下,左下排序
    return sortPointClockwise(result);
}

1. 缩小图片尺寸:


Mat Scanner::resizeImage() {
    int width = srcBitmap.cols;
    int height = srcBitmap.rows;
    int maxSize = width > height? width : height;
    if (maxSize > resizeThreshold) {
        resizeScale = 1.0f * maxSize / resizeThreshold;
        width = static_cast(width / resizeScale);
        height = static_cast(height / resizeScale);
        Size size(width, height);
        Mat resizedBitmap(size, CV_8UC3);
        resize(srcBitmap, resizedBitmap, size);
        return resizedBitmap;
    }
    return srcBitmap;
}

缩小图片尺寸对 OpenCV 来说非常简单,创建一个目标大小的 Size 对象, 创建一个目标大小的 Mat 对象,最后调用 resize 就 OK 了。

2. 预处理图片


Mat Scanner::preprocessImage(Mat& image) {
    Mat grayMat;
    cvtColor(image, grayMat, CV_BGR2GRAY);
    Mat blurMat;
    GaussianBlur(grayMat, blurMat, Size(5,5), 0);
    Mat cannyMat;
    Canny(blurMat, cannyMat, 0, 5);
    return cannyMat;
}

使用 cvtColor 将图片转换成灰度图片;使用 GaussianBlur 对图片做高斯模糊,减少噪点;使用 Canny 做边缘检测,此时图片会变成黑底,白色细线
描图片内容边界的图片,像下面这样:

后面的处理就是基于这种图片的。

3. 提取图片边框:


    vector<vector> contours;
    //提取边框
    findContours(scanImage, contours, RETR_EXTERNAL, CHAIN_APPROX_NONE);
    //按面积排序
    std::sort(contours.begin(), contours.end(), sortByArea);
    vector result;
    if (contours.size() > 0) {
        vector contour = contours[0];
        double arc = arcLength(contour, true);
        vector outDP;
        //多变形逼近
        approxPolyDP(Mat(contour), outDP, 0.02*arc, true);
        //筛选去除相近的点
        vector selectedPoints = selectPoints(outDP, 1);
        if (selectedPoints.size() != 4) {
            //如果筛选出来之后不是四边形,那么使用最小矩形包裹
            RotatedRect rect = minAreaRect(contour);
            Point2f p[4];
            rect.points(p);
            result.push_back(p[0]);
            result.push_back(p[1]);
            result.push_back(p[2]);
            result.push_back(p[3]);
        } else {
            result = selectedPoints;
        }
        for(Point &p : result) {
            p.x *= resizeScale;
            p.y *= resizeScale;
        }
    }

OpenCV 的 findContours 方法能提取出所有线段,以数组的方式返回。然后调用 std::sort 按面积排序,注意最后一个参数 sortByArea 是一个函数指针,用于指定排序的规则:


static bool sortByArea(const vector &v1, const vector &v2) {
    double v1Area = fabs(contourArea(Mat(v1)));
    double v2Area = fabs(contourArea(Mat(v2)));
    return v1Area > v2Area;
}

使用 contourArea 可以很方便的计算闭合图像的面积。
找出最大面积的边界之后使用 approxPolyDP 多边形逼近来减少线段数量,期望是四边形,也就是 4 条线段。然后调用 selectPoints 去除一些误判的相近的点:


vector Scanner::selectPoints(vector points, int selectTimes) {
    if (points.size() > 4) {
        double arc = arcLength(points, true);
        vector::iterator itor = points.begin();
        while (itor != points.end()) {
            if (points.size() == 4) {
                return points;
            }
            Point& p = *itor;
            if (itor != points.begin()) {
                Point& lastP = *(itor - 1);
                double pointLength = sqrt(pow((p.x-lastP.x),2) + pow((p.y-lastP.y),2));
                if(pointLength < arc * 0.01 * selectTimes && points.size() > 4) {
                    itor = points.erase(itor);
                    continue;
                }
            }
            itor++;
        }
        if (points.size() > 4) {
            return selectPoints(points, selectTimes + 1);
        }
    }
    return points;
}

这里使用了递归,返回值预期是大小为4的数组。
如果筛选出来的数组大小不是 4,就使用 OpenCV 的 minAreaRect 获取最小外接局限作为妥协值。

4. 将顶点按左上,右上,右下,左下排序


vector Scanner::sortPointClockwise(vector points) {
    if (points.size() != 4) {
        return points;
    }
    Point unFoundPoint;
    vector result = {unFoundPoint, unFoundPoint, unFoundPoint, unFoundPoint};
    long minDistance = -1;
    for(Point &point : points) {
        long distance = point.x * point.x + point.y * point.y;
        if(minDistance == -1 || distance < minDistance) {
            result[0] = point;
            minDistance = distance;
        }
    }
    if (result[0] != unFoundPoint) {
        Point &leftTop = result[0];
        points.erase(std::remove(points.begin(), points.end(), leftTop));
        if ((pointSideLine(leftTop, points[0], points[1]) * pointSideLine(leftTop, points[0], points[2])) < 0) {
            result[2] = points[0];
        } else if ((pointSideLine(leftTop, points[1], points[0]) * pointSideLine(leftTop, points[1], points[2])) < 0) {
            result[2] = points[1];
        } else if ((pointSideLine(leftTop, points[2], points[0]) * pointSideLine(leftTop, points[2], points[1])) < 0) { result[2] = points[2]; } } if (result[0] != unFoundPoint && result[2] != unFoundPoint) { Point &leftTop = result[0]; Point &rightBottom = result[2]; points.erase(std::remove(points.begin(), points.end(), rightBottom)); if (pointSideLine(leftTop, rightBottom, points[0]) > 0) {
            result[1] = points[0];
            result[3] = points[1];
        } else {
            result[1] = points[1];
            result[3] = points[0];
        }
    }
    if (result[0] != unFoundPoint && result[1] != unFoundPoint && result[2] != unFoundPoint && result[3] != unFoundPoint) {
        return result;
    }
    return points;
}

已知四个顶点形成的四边形为凸四边形(入参做判断),默认以距离顶点(0,0)最近的点作为左上,然后找一个点与该点相连,如果此时另外两个点分别位于这条线的两侧,那么这条线就位对角线,该点为右下。位于这条线上方的为右上,下发的为左下。
以上就是边框识别的所有内容,

边框裁剪

裁剪相对比较简单一些,下面是通过4个顶点作透视变换裁剪出想要的图片:


static void native_crop(JNIEnv *env, jclass type, jobject srcBitmap, jobjectArray points_, jobject outBitmap) {
    std::vector points = pointsToNative(env, points_);
    if (points.size() != 4) {
        return;
    }
    Point leftTop = points[0];
    Point rightTop = points[1];
    Point rightBottom = points[2];
    Point leftBottom = points[3];

    Mat srcBitmapMat;
    bitmap_to_mat(env, srcBitmap, srcBitmapMat);

    AndroidBitmapInfo outBitmapInfo;
    AndroidBitmap_getInfo(env, outBitmap, &outBitmapInfo);
    Mat dstBitmapMat;
    int newHeight = outBitmapInfo.height;
    int newWidth = outBitmapInfo.width;
    dstBitmapMat = Mat::zeros(newHeight, newWidth, srcBitmapMat.type());

    vector srcTriangle;
    vector dstTriangle;

    srcTriangle.push_back(Point2f(leftTop.x, leftTop.y));
    srcTriangle.push_back(Point2f(rightTop.x, rightTop.y));
    srcTriangle.push_back(Point2f(leftBottom.x, leftBottom.y));
    srcTriangle.push_back(Point2f(rightBottom.x, rightBottom.y));

    dstTriangle.push_back(Point2f(0, 0));
    dstTriangle.push_back(Point2f(newWidth, 0));
    dstTriangle.push_back(Point2f(0, newHeight));
    dstTriangle.push_back(Point2f(newWidth, newHeight));

    Mat transform = getPerspectiveTransform(srcTriangle, dstTriangle);
    warpPerspective(srcBitmapMat, dstBitmapMat, transform, dstBitmapMat.size());

    mat_to_bitmap(env, dstBitmapMat, outBitmap);
}

还是使用 bitmap_to_mat 读出图片信息,分别使用 srcTriangle、dstTriangle 保存待裁剪的区域顶点与裁剪顶点,可以看到裁剪的4个顶点分别对应图片的4个顶点,通过 getPerspectiveTransform 获得变换矩阵,然后运用变换 warpPerspective 得到裁剪好的 Mat 对象, 最后将 Mat 对象转换回 Bitmap 对象。

关于 UI 实现部分就不详细介绍了,全部内容位于 CropImageView 中。最后再说几句,使用 OpenCV 做边框识别还是有很多局限性,容易受背景颜色,其他边框的干扰,如果待识别物体与背景的颜色很相似,那么可能就识别不出来,或者背景有复杂的线也会干扰识别。之前看过一篇文章使用 TensorFlow 结合 OpenCV 来识别边框的方式能很好的弥补单纯用 OpenCV 来做识别的局限性,可以参考:手机端运行卷积神经网络的一次实践 — 基于 TensorFlow 和 OpenCV 实现文档检测功能

>> 转载请注明来源:Android 端基于 OpenCV 的边框识别功能

评论

  • 开发者头条回复

    感谢分享!已推荐到《开发者头条》:https://toutiao.io/posts/9bddvd 欢迎点赞支持!
    欢迎订阅《pqpo》https://toutiao.io/subjects/55960

  • drogo回复

    弱问这是不是叫边缘检测

    • pqpo回复

      是的,边缘检测出来之后筛选出预期的边框部分

  • log2cc回复

    如果遇到圆角的身份证之类的,边框检测就不准了。

    • pqpo回复

      小的圆角会过滤掉的,检测不准的原因很大程度上在于背景和检测物体颜色太相近,或者背景线条比较复杂

  • lennon回复

    你好为什么找不到couldn’t find “libsmart_cropper.so” 我在项目根目录有添加 而且我是 maven { url ‘https://jitpack.io’ }添加依赖的

    • pqpo回复

      compile ‘com.github.pqpo:SmartCropper:V1.0.1@aar’ 这样试试

      • lennon回复

        还是不可以 我按照接入的文档实现的 7.0系统

  • Freddie Wang回复

    相當好的一篇文章, 學習了!
    之後會考慮參考你的做法來加強我們最近剛open source的一個library
    謝謝分享!

    • pqpo回复

      你开源的库地址能分享一下吗?互相学习!

  • bhfo回复

    哥,请问一下,libopencv_core.a 这些库怎么生成的?
    是不是要用ndk来编的?

    • pqpo回复

      opencv 官网下载的,也可以使用源代码自行编译

  • last bus回复

    怎么样才可以跑模拟器

    • pqpo回复

      推荐用真机,模拟器要编译一个x86版本的so

  • Vwxy回复

    开发者您好!我们是大四的学生,正在修读软件测试课程,大作业需要测试一个项目,我们希望能测试您的SmartCropper项目。想跟您咨询一下具体的开发环境配置,我们是完全的学习目的,测试结果可与您分享,希望能得到您的支持,期待您的回复!

    • pqpo回复

      自己编译的话使用 Android Studio 即可,如果只是运行已经提供了 apk 文件。有任何问题可以继续留言或者邮件。

      • Vwxy回复

        好的,谢谢您,我们先去试试,后面有问题再向您请教。

  • 傲雪秋思回复

    依赖错误啊 !大神,怎么添加都是不成功啊!

    • pqpo回复

      compile ‘com.github.pqpo:SmartCropper:V1.1.1@aar’ 这样不行?报什么错

      • 傲雪秋思回复

        Failed to resolve
        Error:(33, 13) Failed to resolve: com.github.pqpo:SmartCropper:V1.1.1

        • pqpo回复

          这个加了没? maven { url ‘https://jitpack.io’ }

          • 傲雪

            都加了

          • pqpo

            我这边试了是可以的,要不你直接去 aar 目录或者 github release 页面下载 aar 包再引入吧。

  • aazzl回复

    主题很赞,可以分享下嘛

  • android菜鸟回复

    请问怎么固定住四个点啊,因为有时候裁剪的图会旋转还有镜像。。。。

  • zhang回复

    😮 确实经常出现翻转和镜像问题 ,还有当四个点固定为不规则的四边形时,裁切图片出来会被拉伸。

    • android菜鸟回复

      ivCrop.setFullImgCrop();用这个方法,默认覆盖全部,四个点默认出现的位置就是对的

  • Sira回复

    我发现我看不懂你的code,一开始在想是不是都是C++,但我看你的库98%都是用Java写的… 可是看不懂「::」丶「->」;然後「vector」居然是一个内建符号…?

    • pqpo回复

      确实 98{936b63963a8c9f2b24063da536a495a32039ff9ed9d82cacc18dd4741407c351} 的代码都是 Java 写的,主要也就 UI 层的实现, Cpp 代码位于 smartcropperlib/src/main/cpp 中,由于是基于 OpenCV 的,所以代码量也不算多。另外 cpp 版本是 cpp11,你说的「::」丶「->」、「vector」都是 c++ 里面的。

  • zhouhang回复

    请问native层的nativeScan代码在哪里能找到?怎样修改?谢谢!

    • pqpo回复

      smartcropperlib/src/main/cpp

  • zhouhang回复

    用了nativeScan方法返回的四个点很多时候是不对的,请问如何修改内部的方法,需求是检测身份证的边框

    • pqpo回复

      入口代码位于:SmartCropper/smartcropperlib/src/main/cpp/smart_cropper.cpp
      单纯使用 OpenCV 识别边框本身是有局限性的,识别率在背景复杂的情况下不是很高。如果有能力的话可以考虑加入卷积神经网络来改善识别准确率。

      • zhouhang回复

        哈哈,我刚刚看到代码,谢谢您,卷积神经网络这个没听过呀,我发现在背景和身份证颜色比较像的情况或者是白色背景就不行,请问有什么好的办法解决吗?还是要用卷积神经网络这个东东呀,就解决身份证,我看有些app可以实时检测卡片边框,然后自动拍照的,这个是什么技术呀?

          • zhouhang

            好的,非常感谢您!

          • zhouhang

            额额,大神请问下,在网上找了关于卷积神经网络很多例子,但都是运行不了的,没有现成的直接从头学习需要很长的时间,想请问你这个例子有没有cv2.Canny() 和 cv2.findContours()的方法呀,我看这两个方法可以检测身份证的边框,而现在这个例子还有没有改善的方法呀,因为检测效果实在一般,还有实时检测边框这个是什么思路?实在头疼,这个需求实在难搞,在打开相机需要实时检测卡片,我看名片全能王app做的效果最好了!

    • pqpo回复

      SmartCropper 裁剪算法里面用到了cv2.Canny() 和 cv2.findContours(),并且已经做过一些优化了。文档识别是名片全能王的核心技术了,现成的是没有的,要实现名片全能王那样的效果肯定是要花费很多精力的,建议你先看一遍代码,然后再想是否能优化,但是不使用机器学习的情况下是达不到名片全能王的效果。

  • 溪溪回复

    您好,我导入arr包也显示Could not find method compile() for arguments [com.github.pqpo:SmartCropper:V1.0.1@aar]错误

  • 不懂回复

    开发者你好,感谢您的开源,但是在使用您开源的项目的时候遇到一些麻烦,比如四个可拖动的点贴在一堆变得不可拖动了。

    • pqpo回复

      可以考虑设置:setDragLimit(false);

  • 回复

    你好 你这个裁剪 不能识别边缘 也不能矫正 在Android 7.0版本上 请教一下 如何解决该问题呢?上次看到你的回复 是受坏境影响 ,如何避免呢?

  • 广恒回复

    您好,这个对于我们新手看的太吃了了。难道不能直接把opencv都引入进去,使用java调用,是不是会简单一些

    • pqpo回复

      OpenCV 官网已经提供了 Android SDK,直接使用 c 库会更灵活,打包出来的包会更小。

  • kazike回复

    您好

  • 卡兹克回复

    可否联系下我 我是某企业的开发 想用您的项目 qq: 2522837359 有问题请教您,感谢感谢

    • pqpo回复

      加你了,也可以通过邮箱(pqponet@gmail.com)联系到我。

  • xinixng回复

    请问一下,摄像头获取到的byte[]怎么样直接转mat

    • pqpo回复

      查看这里:https://github.com/pqpo/SmartCamera/blob/master/smartcameralib/src/main/cpp/smart_camera.cpp#L56

  • 广恒回复

    您好,demo下载试了一下,发现剪裁出来的图片变得很模糊,虚的很厉害。这个在哪里调整呀?

  • Wuzee回复

    您的项目结合使用 TensorFlow替换边缘识别部分,再使用 GPUImage制作滤镜,整体效果很接近全能扫描王了!!超棒!!感谢!!

    • pqpo回复

      💡 之前正有计划使用 TensorFlow 替换边缘检测,有机会希望能和你交流交流。

      • Wuzee回复

        期待!!!我是基于您文章结尾给的链接部署的,虽然成功了,但是低端机上效率较低。另外尝试部署 TensorFlow Lite时,模型转换成.tflite 一直失败。期待您的调研结果!!

  • 回复

    Error:Expected NDK STL shared object file at D:\sdk\ndk-bundle\sources\cxx-stl\gnu-libstdc++\4.9\libs\x86\libgnustl_shared.so
    你好,请问报上面的错,怎么解决?

    • saltwater回复

      把as默认的ndk降级到r17就可以了

      • 哈哈回复

        你好,这个方法可以解决吗?

  • ths-虾米回复

    楼主,我用python写了一个检测名片脚本,原理和你是一样的,都是用opencv的canny算子边缘检测方形。感觉opencv的边缘检测是有问题的,在单一背景下运行是可以成功检测到目标,但是遇到复杂背景比如说我把名片放在手上就很难检测到了。我看了一下名片全能王app的检测过程,是很顺畅的,想问楼主有没有什么好的建议吗

    • pqpo回复

      使用机器学习-卷积神经网络来识别边框准确性会更高。

  • ws回复

    ERROR: Failed to resolve: com.github.pqpo:SmartCropper:v1.2.4
    Show in Project Structure dialog
    Affected Modules: app

  • lc回复

    大神您好,我需要实时的拍摄识别边框,用了您的这个库,我的思路是异步任务获取Camera的每一帧图像转出Bitmap,然后用你的SmartCropper.scan获取四个定点,然后自己绘制边框在相机上。但是效果不尽人意。有更好的办法吗,万分感谢

    • pqpo回复

      是实时帧数不够吗?

      • lc回复

        我是相机对焦后,获取帧数

  • QQ827681776回复

    如果我前端比如小程序H5传来的图片信息,可以使用哪个现有的轮子使用

回复给 pqpo 点击这里取消回复。