Android开发之OpenCV实战:开发环境的搭建(身份证号码识别为例)
声明
本文的主要内容是:利用NDK开发技术调用OpenCV库,所以身份证号码识别并不是重点,开发环境的搭建才是重点!!!! 事实上,本人C++入门级别,要是让我用python调OpenCV写一个比较完善的身份证号码识别算法或许还有戏,用C++,写不动。。。本文的身份证识别只能识别我准备好的身份证(尴尬),如果想真的实现算法,建议系统学学C++和OpenCV。
项目地址:https://github.com/CAM1113/IDReg
OpenCV环境:链接:https://pan.baidu.com/s/1TSOFxte1DvK7UXlAHDyZQQ 提取码:j3y0
开发环境
- Android Studio Arctic Fox | 2020.3.1(官网下载安装)
- OpenCV-android-sdk 版本4.5.5(下载后解压到一个文件夹里,不用进行配置)
- NDK(Android Studio自己安装)
环境配置步骤
-
新建项目
IDReg
-
新建
IDRecon
模块,我们将在这个模块中进行OpenCV
相关的开发。
-
在
IDRecon
模块的src/main
目录下新建文件夹cpp
,用于存放NDK开发相关的代码。在cpp
文件夹下新建文件夹libs
,用于存放OpenCV
的动态链接库。将 解压路径/OpenCV-android-sdk/sdk/native/libs 文件夹下的四个文件夹拷贝到libs
中。因为打包成时需要把OpenCV
中的.so
库一起打包。在CPP
文件夹下新建CMakeLists.txt
文件,用来编写编译的配置信息;新建OpenCVHelper.cpp
文件,用来编写C++
的代码。
-
最重要的第一步: 在
IDRecon
模块的build.gradle
文件中添加NDK
相关的配置。
首先在defaultConfig
闭包中添加externalNativeBuild
闭包,增加编译d的参数配置。
然后在android
闭包中添加externalNativeBuild
闭包,指出CMakeLists.txt
文件的路径和CMake的版本
最后在android
闭包中添加sourceSets
闭包,指明OpenCV
的动态链接库的存放位置。//****// android{ //****// defaultConfig{ //****// externalNativeBuild { cmake { cppFlags '' arguments "-DANDROID_STL=c++_shared" } } } //****// externalNativeBuild { cmake { path file('src/main/cpp/CMakeLists.txt') version '3.10.2' } } sourceSets{ main{ jniLibs.srcDirs = ["src/main/cpp/opencv/libs"] } } } //****//
-
最重要的二步: 编写
src/main/cpp/CMakeLists.txt
,代码如下(说明见注释):# cmake版本 cmake_minimum_required(VERSION 3.10.2) #替换,相当于宏定义 set(PROJECT_NAME opencv) #项目名称,非必选 project(${PROJECT_NAME}) # 很重要,设置OpenCV_DIR,让项目能找到OpenCV的库 set(OpenCV_DIR opencv的解压地址\\OpenCV-android-sdk\\sdk\\native\\jni) find_package(OpenCV REQUIRED) if(OpenCV_FOUND) # 把OpenCV的库引入项目 include_directories(${OpenCV_INCLUDE_DIRS}) message(STATUS "OpenCV library status:") message(STATUS " version: ${OpenCV_VERSION}") message(STATUS " libraries: ${OpenCV_LIBS}") message(STATUS " include path: ${OpenCV_INCLUDE_DIRS}") else(OpenCV_FOUND) message(FATAL_ERROR "OpenCV library not found") endif(OpenCV_FOUND) add_library( #库的名称 ${PROJECT_NAME} # 将库设置为.so动态连接库 SHARED # 添加动态库里包含的所有cpp文件,包括引用的lib里的cpp OpenCVHelper.cpp ) find_library( #库位置对应的名称,给其他地方使用 log-lib #库生成的中间文件的位置 log) #链接对应的文件,很重要,要连接jni图像库,OpenCV的库和自己编写的文件(${log-lib}) target_link_libraries( ${PROJECT_NAME} jnigraphics ${OpenCV_LIBRARIES} ${log-lib})
-
令
app
模块依赖IDRecon
模块,在app
模块的build.gradle
文件中的dependencies
闭包下添加依赖//****// dependencies{ implementation project(":IDRecong") }
-
环境配置完毕,剩下的按
NDK
开发的流程编写代码即可
代码编写
-
在
IDRecon
模块下的/src/main
文件夹下新建asserts
文件夹,文件夹下存放身份证数字匹配的模板(0-9十个数字的图片,资源见源码) -
在
IDRecon
模块下的/src/main/java/包名
文件夹下新建IDRecong.kt
文件,代码如下:package com.cam.idrecong import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory class OpenCVUtils { companion object { init { System.loadLibrary("opencv") } private external fun idRecognise(bitmap: Bitmap, list:List<Bitmap>): String fun idRecognise(bitmap: Bitmap,context: Context): String{ val list = ArrayList<Bitmap>() for(i in 0..9){ list.add(BitmapFactory.decodeStream(context.assets.open("$i.jpg"))) } return idRecognise(bitmap,list); } } }
-
编写
OpenCVHelper.cpp
代码,代码如下:#include <jni.h> #include <iostream> #include <opencv2/core/core.hpp> #include <opencv2/imgcodecs/imgcodecs.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include <string> #include <opencv2/opencv.hpp> #include <android/bitmap.h> using namespace std; using namespace cv; Size numSize = Size(16, 24); void BitmapToMat2(JNIEnv *env, jobject &bitmap, Mat &mat, jboolean needUnPremultiplyAlpha) { AndroidBitmapInfo info; void *pixels = 0; Mat &dst = mat; try { CV_Assert(AndroidBitmap_getInfo(env, bitmap, &info) >= 0); CV_Assert(info.format == ANDROID_BITMAP_FORMAT_RGBA_8888 || info.format == ANDROID_BITMAP_FORMAT_RGB_565); CV_Assert(AndroidBitmap_lockPixels(env, bitmap, &pixels) >= 0); CV_Assert(pixels); dst.create(info.height, info.width, CV_8UC4); if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) { Mat tmp(info.height, info.width, CV_8UC4, pixels); if (needUnPremultiplyAlpha) cvtColor(tmp, dst, COLOR_mRGBA2RGBA); else tmp.copyTo(dst); } else { // info.format == ANDROID_BITMAP_FORMAT_RGB_565 Mat tmp(info.height, info.width, CV_8UC2, pixels); cvtColor(tmp, dst, COLOR_BGR5652RGBA); } AndroidBitmap_unlockPixels(env, bitmap); return; } catch (const cv::Exception &e) { AndroidBitmap_unlockPixels(env, bitmap); jclass je = env->FindClass("java/lang/Exception"); env->ThrowNew(je, e.what()); return; } catch (...) { AndroidBitmap_unlockPixels(env, bitmap); jclass je = env->FindClass("java/lang/Exception"); env->ThrowNew(je, "Unknown exception in JNI code {nBitmapToMat}"); return; } } bool compareFacesByHist(const Mat &srcImage, const Mat &tempalteImage) { int sums = 0; for (int i = 0; i < srcImage.rows; i++) { for (int j = 0; j < srcImage.cols; j++) { sums += abs(srcImage.at<uchar>(i, j) - tempalteImage.at<uchar>(i, j)); } } float s = sums / (srcImage.rows * srcImage.cols + 0.0); return s < 20; } string getNums(Mat src, vector<Mat> oriMats) { if (src.empty()) { return ""; } resize(src, src, Size(640, 400)); // 灰度图 cvtColor(src, src, COLOR_BGR2GRAY); Mat dst; // 二值化 threshold(src, dst, 150, 255, THRESH_BINARY); Mat erodeElement = getStructuringElement(MORPH_RECT, Size(20, 10)); erode(dst, dst, erodeElement); vector<vector<Point>> contours; findContours(dst, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); for (auto &contour: contours) { Rect rect = boundingRect(contour); if (rect.width > rect.height * 9) { dst = src(rect); } } threshold(dst, dst, 150, 255, THRESH_BINARY); Mat erods; erodeElement = getStructuringElement(MORPH_RECT, Size(2, 2)); dilate(dst, erods, erodeElement); erode(erods, erods, erodeElement); erode(erods, erods, erodeElement); erode(erods, erods, erodeElement); erode(erods, erods, erodeElement); contours.clear(); findContours(erods, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); vector<Mat> mats; for (auto &contour: contours) { Rect rect = boundingRect(contour); if (rect.width > 5 && rect.height > 5 && rect.height >= rect.width && rect.width * rect.height >= 200) { mats.push_back(dst(rect)); } } vector<int> result; for (int i = 0; i < mats.size(); i++) { Mat m = mats.at(i); resize(m, m, numSize); for (int j = 0; j < oriMats.size(); j++) { if (compareFacesByHist(m, oriMats.at(j))) { result.push_back(j); } } } string s; for (int i = result.size() - 1; i >= 0; i--) { s.append(format("%d", result[i])); } return s; } extern "C" JNIEXPORT jstring JNICALL Java_com_cam_idrecong_OpenCVUtils_00024Companion_idRecognise(JNIEnv *env, jobject thiz, jobject bitmap, jobject list) { Mat m; BitmapToMat2(env, bitmap, m, false); vector<Mat> mats; jclass clazz = env->GetObjectClass(list); // get(int index) jmethodID methodId = env->GetMethodID(clazz, "get", "(I)Ljava/lang/Object;"); for (int i = 0; i < 10; i++) { Mat x; jobject oriBitmap = (env->CallObjectMethod(list, methodId, i)); BitmapToMat2(env, oriBitmap, x, false); cvtColor(x, x, COLOR_BGR2GRAY); resize(x, x, numSize); mats.push_back(x); } try { string s = getNums(m, mats); return env->NewStringUTF(s.c_str()); } catch (Exception e) { return env->NewStringUTF("error"); } }
-
身份证照片
id_test.jpg
放在app模块下的下的drawable
下。
-
编写
app
模块下的MainActivity
代码,代码如下:class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val bitmap = BitmapFactory.decodeResource(resources,R.drawable.id_test); val s = OpenCVUtils.idRecognise(bitmap,this) Log.e("CAM",s) } }
7.运行!