赞
踩
第一章 Android:基于OpenCV实现身份证识别(C++)——图像处理
第二章 Android:基于OpenCV实现身份证识别(C++)——移植图像算法
我们要做一个Android上的身份证号码识别功能,在上一篇用OpenCV做了图像处理,本文目标是将我们的C++程序移植到Android程序中。
【本文源码下载】
软件环境:
- Android Studio Chipmunk | 2021.2.1
- opencv-4.5.5-android-sdk
新建项目——选择Native C++——点击Next;
修改名称和包名——点击Next;
这里可以选择C++的标准库版本,我这里保持默认,点击Finish。
在官网下载opencv-4.5.5-android-sdk.zip 并解压至磁盘,后面我们要把此SDK目录配置到项目中。
解压后效果如下:
修改gradle.properties文件,加入opencvsdk路径;
opencvsdk=D\:\\Soft\\Dev\\OpenCV-android-sdk
修改build.gradle(:app)文件 ,加入cmake配置;
android {
defaultConfig {
externalNativeBuild {
cmake {
cppFlags "-frtti -fexceptions"
abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
arguments "-DOpenCV_DIR=" + opencvsdk + "/sdk/native"
arguments "-DANDROID_STL=c++_shared"
}
}
}
}
如果没有配置 arguments “-DANDROID_STL=c++_shared”,可能会在运行时闪退,并报以下错误。
E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.example.idrec, PID: 28414
java.lang.UnsatisfiedLinkError: dlopen failed: library "libc++_shared.so" not found
at java.lang.Runtime.loadLibrary0(Runtime.java:1071)
at java.lang.Runtime.loadLibrary0(Runtime.java:1007)
at java.lang.System.loadLibrary(System.java:1667)
at com.example.idrec.MainActivity.<clinit>(MainActivity.kt:69)
at java.lang.Class.newInstance(Native Method)
修改CMakeLists.txt文件,加入以下配置;
include_directories(${OpenCV_DIR}/jni/include)
add_library( lib_opencv SHARED IMPORTED )
set_target_properties(lib_opencv PROPERTIES IMPORTED_LOCATION ${OpenCV_DIR}/libs/${ANDROID_ABI}/libopencv_java4.so)
并在target_link_libraries内加入lib_opencv,效果如下;
target_link_libraries( # Specifies the target library.
idrec
lib_opencv
# Links the target library to the log library
# included in the NDK.
${log-lib}
)
要实现从手机选择图片,传统的startActivityForResult 官方已经废弃了,如果想启动一个 Activity 并获取返回的结果,推荐使用 registerForActivityResult 来代替。
关于如何自定义协定?可参考官方文档:自定义协定
选择手机图片的协定如下:
/** * 选择照片的协定 */ class SelectPhotoContract : ActivityResultContract<Unit, Bitmap?>() { private lateinit var context: Context override fun createIntent(context: Context, input: Unit?): Intent { this.context = context return Intent(Intent.ACTION_PICK).setType("image/*") } override fun parseResult(resultCode: Int, intent: Intent?): Bitmap? { if (intent == null || resultCode != Activity.RESULT_OK) return null // Uri转-》Bitmap intent.data?.let { val input = context.contentResolver.openInputStream(it) val bitmap = BitmapFactory.decodeStream(input) input?.close() return bitmap } return null } }
这是一个输出Bitmap的协定,我们需要在parseResult方法中通过结果Uri获取Bitmap;
然后在Activity中注册registerForActivityResult 并显示图片结果,代码如下:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val mSelectPhoto = selectPhoto() /** 选择的图片 */ private var mSrcBitmap: Bitmap? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) binding.btChooseImage.setOnClickListener { mSelectPhoto.launch(Unit) } binding.btReadId.setOnClickListener { readID() } } /** * 选择图片 */ private fun selectPhoto() = registerForActivityResult(SelectPhotoContract()) { it?.let { mSrcBitmap = it binding.ivImage.setImageBitmap(it) return@registerForActivityResult } Toast.makeText(this, "没有选择图片", Toast.LENGTH_SHORT).show() } }
注意:private val mSelectPhoto = selectPhoto() 不要写成private val mSelectPhoto1 by lazy { selectPhoto() }懒加载的形式,registerForActivityResult是不支持这样初始化的。
定义原生方法getIdNumber(),点击“识别ID”按钮时,通过readID()调用getIdNumber()来获取识别后的Bitmap图像,并显示出来;
class MainActivity : AppCompatActivity() { ... /** * 读取身份证号码 */ private fun readID(){ if(mSrcBitmap == null){ Toast.makeText(this,"请先选择图片", Toast.LENGTH_SHORT).show() }else{ mSrcBitmap?.let { val idBitmap = getIdNumber(it, Bitmap.Config.ARGB_8888) binding.ivImage.setImageBitmap(idBitmap) } } } external fun getIdNumber(src: Bitmap, config: Bitmap.Config): Bitmap companion object { // Used to load the 'idrec' library on application startup. init { System.loadLibrary("idrec") } } }
在C++中我们使用的Mat格式,其实它就相当于Android中的Bitmap格式;
如果我们要在C++中做图像处理,就面临两个问题:
(1)Bitmap如何转为Mat格式?
(2)Mat又如何转为Bitmap格式?
这个在OpenCV-android-sdk\sdk\java\src\org\opencv\android\Utils.java中已经帮我们提供了格式转换方法,现在需要在cpp文件中声明一下,就可以使用了。
修改main/cpp/native-lib.cpp文件,添加以下代码:
#include <jni.h> #include <string> #include <opencv2/opencv.hpp> #define CARD_SIZE Size(640, 400) using namespace cv; using namespace std; extern "C" JNIEXPORT void JNICALL Java_org_opencv_android_Utils_nBitmapToMat2(JNIEnv *env, jclass clazz, jobject bitmap, jlong m_addr, jboolean un_premultiply_alpha); extern "C" JNIEXPORT void JNICALL Java_org_opencv_android_Utils_nMatToBitmap(JNIEnv *env, jclass, jlong m_addr, jobject bitmap); /** * Mat格式转——》Bitmap * @param env * @param srcData * @param config * @return */ jobject createBitmap(JNIEnv *env, Mat srcData, jobject config){ int imgWidth = srcData.cols; int imgHeight = srcData.rows; // 利用反射创建Bitmap对象 jclass bmpCls = env->FindClass("android/graphics/Bitmap"); jmethodID createBitmapMid = env->GetStaticMethodID(bmpCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); jobject jBmpObj = env->CallStaticObjectMethod(bmpCls, createBitmapMid, imgWidth, imgHeight, config); Java_org_opencv_android_Utils_nMatToBitmap(env, nullptr, (jlong) &srcData, jBmpObj); return jBmpObj; } /** * 获取身份证号码 */ extern "C" JNIEXPORT jobject JNICALL Java_com_example_idrec_MainActivity_getIdNumber(JNIEnv *env, jobject thiz, jobject src, jobject config) { Mat src_img; Mat dst_img; // bitmap转——》Mat Java_org_opencv_android_Utils_nBitmapToMat2(env, (jclass)thiz, src, (jlong)&src_img, 0); Mat dst; ... 图像处理逻辑 ... // 创建Bitmap对象 jobject bitmap = createBitmap(env, dst_img, config); // 释放资源 src_img.release(); dst_img.release(); dst.release(); return bitmap; }
现在将上一篇的图像处理逻辑复制到getIdNumber方法中,代码如下:
/** * 获取身份证号码 */ extern "C" JNIEXPORT jobject JNICALL Java_com_example_idrec_MainActivity_getIdNumber(JNIEnv *env, jobject thiz, jobject src, jobject config) { Mat src_img; Mat dst_img; // bitmap转——》Mat Java_org_opencv_android_Utils_nBitmapToMat2(env, (jclass)thiz, src, (jlong)&src_img, 0); Mat dst; // 无损压缩 640*400 resize(src_img, src_img, CARD_SIZE); // 灰度化 cvtColor(src_img, dst, COLOR_BGR2GRAY); // 二值化 threshold(dst, dst, 100, 255, THRESH_BINARY); // 膨胀 Mat erodeElement = getStructuringElement(MORPH_RECT, Size(20, 10)); erode(dst, dst, erodeElement); // 轮廓检测 vector<vector<Point>> contours; vector<Rect> rects; findContours(dst, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0)); for (auto & contour : contours) { Rect rect = boundingRect(contour); rectangle(dst, rect, Scalar(0, 255)); // 筛选轮廓图片 if (rect.width > rect.height * 9) { rects.push_back(rect); rectangle(dst, rect, Scalar(0, 0, 255)); } } if (rects.size() == 1) { dst_img = src_img(rects.at(0)); } else { Rect rectTmp = rects.at(0); // 遍历查找Y最大的轮廓 for (auto rect : rects) { if (rect.tl().y > rectTmp.tl().y) { rectTmp = rect; } } rectangle(dst, rectTmp, Scalar(255, 255, 0)); dst_img = src_img(rectTmp); } // 创建Bitmap对象 jobject bitmap = createBitmap(env, dst_img, config); // 释放资源 src_img.release(); dst_img.release(); dst.release(); return bitmap; }
先从相册选择图片后,点击“识别ID”按钮,开始识别身份证号码。
以上就是本文要讲的内容,从中我们学到了如何在Android 引入OpenCV?如何Mat与Bitmap格式转换?如何在Android中调用C++图像计算?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。