第一章 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.(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
#include
#include
#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> contours;
vector 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++图像计算?
【本文源码下载】