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

您所在的位置:网站首页 opencv识别颜色方块并框出 Android 端基于 OpenCV 的边框识别功能

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

2022-05-27 23:36| 来源: 网络整理| 查看: 265

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

了解 NDK 开发的基本步骤,能使用 Java、C++/C 混合开发简单的应用

了解 OpenCV 库的作用及其用法,能使用 OpenCV 做图像处理

了解基于 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})

主要注意点如下:

include_directories添加头文件查找路径,包括引入库的和自己写的

add_library添加动态库或静态库,其中本地的动态库名称,位置可以由set_target_properties设置

find_library通过名称查找并引入库,可以引入 NDK 中的库,比如日志模块

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  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 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 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  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 


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3