Android:基于OpenCV实现身份证识别(C++)——移植图像算法

系列文章目录

第一章 Android:基于OpenCV实现身份证识别(C++)——图像处理
第二章 Android:基于OpenCV实现身份证识别(C++)——移植图像算法



前言

我们要做一个Android上的身份证号码识别功能,在上一篇用OpenCV做了图像处理,本文目标是将我们的C++程序移植到Android程序中。
【本文源码下载】

软件环境:

  • Android Studio Chipmunk | 2021.2.1
  • opencv-4.5.5-android-sdk

一、Android Studio中配置OpenCV

1.创建项目

新建项目——选择Native C++——点击Next;
在这里插入图片描述
修改名称和包名——点击Next;
在这里插入图片描述
这里可以选择C++的标准库版本,我这里保持默认,点击Finish。
在这里插入图片描述

2.下载并解压opencv-android-sdk

官网下载opencv-4.5.5-android-sdk.zip 并解压至磁盘,后面我们要把此SDK目录配置到项目中。
在这里插入图片描述
解压后效果如下:
在这里插入图片描述

3.配置OpenCV 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)

4.配置CMakeLists.txt文件

修改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 来代替。

1.自定义协定

关于如何自定义协定?可参考官方文档:自定义协定
选择手机图片的协定如下:

/**
 * 选择照片的协定
 */
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;

2.调用registerForActivityResult

然后在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是不支持这样初始化的。

三、移植C++图像处理程序

1.定义原生方法

定义原生方法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")
        }
    }
}

2.Mat与Bitmap数据格式转换

在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;
}

3.移植图像处理逻辑

现在将上一篇的图像处理逻辑复制到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++图像计算?

本文源码下载

  • 2
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

两块三刀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值