OpenCV 入门(七)—— 身份证识别

OpenCV 入门系列:

OpenCV 入门(一)------ OpenCV 基础
OpenCV 入门(二)------ 车牌定位
OpenCV 入门(三)------ 车牌筛选
OpenCV 入门(四)------ 车牌号识别
OpenCV 入门(五)------ 人脸识别模型训练与 Windows 下的人脸识别
OpenCV 入门(六)------ Android 下的人脸识别
OpenCV 入门(七)------ 身份证识别

利用 OpenCV 实现身份证识别 Demo 效果:

主要步骤分为两大步:

  1. 利用 OpenCV 从完整的身份证图片中识别出身份证号码区域,并返回身份证号码的图片
  2. 利用 OCR 识别工具将身份证号码图片识别成文字

实际上身份证识别、银行卡识别都是相同的思路。

1、OpenCV 图像识别

1.1 上层代码过程

在 Activity 中,点击"从相册中查找"按钮从相册中选择一张图片转换为一个 640 * 480 的 Bitmap 设置到 ImageView 中:

kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityMainBinding
    private var mFullImage: Bitmap? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)
    }

    /**
     * 从相册中选择一张图片
     */
    fun search(view: View) {
        val intent = Intent(Intent.ACTION_PICK)
        intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
        startActivityForResult(Intent.createChooser(intent, "选择待识别图片"), REQUEST_CODE)
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_CODE && resultCode == RESULT_OK && data != null) {
            getResult(data.data)
        }
    }

    private fun getResult(data: Uri?) {
        // 获取图片路径
        var imagePath: String? = null
        if ("file" == data?.scheme) {
            Log.i(TAG, "path uri 获得图片")
            imagePath = data.path
        } else if ("content" == data?.scheme) {
            Log.i(TAG, "content uri 获得图片")
            val filePathColumns = arrayOf(MediaStore.Images.Media.DATA)
            val cursor = contentResolver.query(data, filePathColumns, null, null, null)
            if (null != cursor) {
                if (cursor.moveToFirst()) {
                    val columnIndex = cursor.getColumnIndex(filePathColumns[0])
                    imagePath = cursor.getString(columnIndex)
                }
                cursor.close()
            }
        }

        // 根据图片路径生成 Bitmap 并显示
        if (!TextUtils.isEmpty(imagePath)) {
            mFullImage?.recycle()
            mFullImage = toBitmap(imagePath)
            mBinding.tvIdNumber.text = null
            mBinding.ivIdCard.setImageBitmap(mFullImage)
        }
    }

    /**
     * 根据图片路径生成 Bitmap,宽高要缩放到 STANDARD_ID_CARD_WIDTH
     * 与 STANDARD_ID_CARD_HEIGHT 的范围内
     */
    private fun toBitmap(imagePath: String?): Bitmap? {
        if (imagePath == null) {
            return null
        }

        val tempOptions = BitmapFactory.Options()
        tempOptions.inJustDecodeBounds = true

        BitmapFactory.decodeFile(imagePath, tempOptions)
        // 计算出缩放倍数以及缩放后的宽高
        var tempWidth = tempOptions.outWidth
        var tempHeight = tempOptions.outHeight
        var scale = 1
        while (true) {
            if (tempWidth <= STANDARD_ID_CARD_WIDTH && tempHeight <= STANDARD_ID_CARD_HEIGHT) {
                break
            }
            tempWidth /= 2
            tempHeight /= 2
            scale *= 2
        }

        // 利用计算好的宽高与缩放倍数解析出一个 Bitmap
        val options = BitmapFactory.Options()
        options.outWidth = tempWidth
        options.outHeight = tempHeight
        options.inSampleSize = scale
        return BitmapFactory.decodeFile(imagePath, options)
    }

    companion object {
        private val TAG = MainActivity::class.java.simpleName

        private const val REQUEST_CODE = 100

        private const val STANDARD_ID_CARD_WIDTH = 640
        private const val STANDARD_ID_CARD_HEIGHT = 480
    }
}

然后点击"查找 ID"按钮时,将完整的身份证 Bitmap 传给 ImageProcessor 交由 Native 层的 OpenCV 进行识别:

kotlin 复制代码
	private var mResultImage: Bitmap? = null

	/**
     * 从整张图片中截取出身份证号码区域
     */
    fun searchIdImage(view: View) {
        mBinding.tvIdNumber.text = null
        mResultImage = ImageProcessor.getIdNumberArea(mFullImage, Bitmap.Config.ARGB_8888)
        mFullImage?.recycle()
        mBinding.ivIdCard.setImageBitmap(mResultImage)
    }

ImageProcessor 的内容很简单,就定义了一个 JVM 静态的 Native 方法 getIdNumberArea():

kotlin 复制代码
class ImageProcessor {

    companion object {

        init {
            System.loadLibrary("ID-Recognition")
        }

        @JvmStatic
        external fun getIdNumberArea(fullImage: Bitmap?, config: Bitmap.Config): Bitmap
    }
}

该方法需要得到识别后身份证号区域的 Bitmap。

1.2 Native 识别过程

Native 层首先要解决 Bitmap 与 Mat 之间相互转换的问题。因为我们从上层传到 Native 的待识别图片是 Bitmap,但是 OpenCV 中是没有 Bitmap 对象的,类似的可以被认为是一张图片的结构是 Mat。那么在给 OpenCV 识别前,就要将 Bitmap 转化成 Mat,识别后再将 Mat 转换成 Bitmap 返回给上层。

OpenCV 提供了转换函数 nBitmapToMat2() 和 nMatToBitmap(),我们还需自己实现一个创建 Bitmap 对象的函数 createBitmap():

cpp 复制代码
#include <jni.h>
#include <opencv2/opencv.hpp>

using namespace std;
using namespace cv;

extern "C" {

extern JNIEXPORT void JNICALL Java_org_opencv_android_Utils_nBitmapToMat2
        (JNIEnv *env, jclass, jobject bitmap, jlong m_addr, jboolean needUnPremultiplyAlpha);
extern JNIEXPORT void JNICALL Java_org_opencv_android_Utils_nMatToBitmap
        (JNIEnv *env, jclass, jlong m_addr, jobject bitmap);

/**
 * 反射调用上层的 Bitmap 的 createBitmap() 创建一个 Bitmap 对象,并且
 * 将 srcData 的内容填充到 Bitmap 中
 */
jobject createBitmap(JNIEnv *env, Mat &srcData, jobject config) {
    int width = srcData.cols;
    int height = srcData.rows;

    // 反射 Bitmap.createBitmap() 并调用以创建 Bitmap 对象
    jclass bitmapClass = env->FindClass("android/graphics/Bitmap");
    jmethodID createBitmapMethod = env->GetStaticMethodID(
            bitmapClass,
            "createBitmap",
            "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");

    jobject bitmap = env->CallStaticObjectMethod(bitmapClass, createBitmapMethod, width, height,
                                                 config);
    // 将 srcData 转换成 bitmap
    Java_org_opencv_android_Utils_nMatToBitmap(env, bitmapClass, (jlong) &srcData, bitmap);

    return bitmap;
}
}

接下来再实现 OpenCV 的识别函数:

cpp 复制代码
extern "C"
JNIEXPORT jobject JNICALL
Java_com_opencv_id_recognition_ImageProcessor_getIdNumberArea(JNIEnv *env, jclass clazz,
                                                              jobject full_image, jobject config) {
    Mat src_img;
    Mat dst_img;
    Mat temp_img;

    // 1.通过 OpenCV 提供的函数,将上层传来的 Bitmap 转换为 Mat 对象
    Java_org_opencv_android_Utils_nBitmapToMat2(env, clazz, full_image, (jlong) &src_img, false);

    // 2.将图片无损压缩至 640 * 400
    resize(src_img, src_img, FIXED_ID_CARD_SIZE);

    // 3.灰度化
    cvtColor(src_img, temp_img, COLOR_BGR2GRAY);

    // 4.二值化
    threshold(temp_img, temp_img, 100, 255, THRESH_BINARY | THRESH_OTSU);

    // 5.膨胀操作
    Mat eroded_img = getStructuringElement(MORPH_RECT, Size(20, 10));
    erode(temp_img, temp_img, eroded_img);

    // 6.轮廓检测
    vector<vector<Point>> contours;
    vector<Rect> rects;

    findContours(temp_img, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));

    for (int i = 0; i < contours.size(); i++) {
        Rect rect = boundingRect(contours[i]);
        if (rect.width > rect.height * 9) {
            rects.push_back(rect);
            rectangle(dst_img, rect, Scalar(0, 255, 255));
            dst_img = src_img(rect);
        }
    }

    // 7.筛选结果,如果 rects 有多个元素,则挑选纵坐标靠下的
    if (rects.size() == 1) {
        dst_img = src_img(rects[0]);
    } else if (rects.size() > 1) {
        int lowPoint = 0;
        Rect finalRect;
        for (auto &rect: rects) {
            if (rect.tl().y > lowPoint) {
                lowPoint = rect.tl().y;
                finalRect = rect;
            }
        }
        rectangle(temp_img, finalRect, Scalar(255, 255, 0));
        dst_img = src_img(finalRect);
    }

    // 8. 根据最终的 Mat 创建 Bitmap 作为返回值
    jobject bitmap = createBitmap(env, dst_img, config);

    // 9. 释放资源
    src_img.release();
    dst_img.release();
    temp_img.release();

    return bitmap;
}

2、OCR 识别

上一步我们能得到一个包含身份证号码的 Bitmap,接下来需要使用 OCR 识别技术将图片中的身份证号码识别成文字。OCR 全称 Optical Character Recognition,是一个对文本资料的图像文件进行分析识别处理,获取文字及版面信息的过程。

我们使用的是 Tess-two。Tess-two 是 TesseraToolForAndroid 的一个 git 分支,它具有如下特征:

  1. 简单易用
  2. 开源且支持离线使用
  3. 为 Android 平台定制的 Java API

首先我们将识别模型文件 cn.traineddata 拷贝到 /src/main/assets 目录下,在 Activity 的 onCreate() 中启动协程,将该模型文件拷贝到手机中,并初始化 Tess:

kotlin 复制代码
	override fun onCreate(savedInstanceState: Bundle?) {
        ...
        lifecycleScope.launch {
            initTess()
        }
    }

	private suspend fun initTess() {
        coroutineScope {
            // 1.显示进度
            showProgress()
            val result = async {
                mTessBaseAPI = TessBaseAPI()
                // 2.通过流将识别模型拷贝到手机中
                try {
                    val inputStream = assets.open("$DEFAULT_LANGUAGE.traineddata")
                    val assetFile = File("/sdcard/tess/tessdata/$DEFAULT_LANGUAGE.traineddata")
                    if (!assetFile.exists()) {
                        assetFile.parentFile?.mkdirs()
                        val fos = FileOutputStream(assetFile)
                        val buffer = ByteArray(2048)
                        var len: Int
                        while (inputStream.read(buffer).also { len = it } != -1) {
                            fos.write(buffer, 0, len)
                        }
                        fos.close()
                    }
                    inputStream.close()
                    // init 传入的 datapath 必须是包含 tessdata 的目录
                    return@async mTessBaseAPI?.init("/sdcard/tess", DEFAULT_LANGUAGE) ?: false
                } catch (e: IOException) {
                    e.printStackTrace()
                }
                return@async false
            }
            // 3.处理异步任务结果
            dismissProgress()
            if (!result.await()) {
                Toast.makeText(this@MainActivity, "load trainedData failed", Toast.LENGTH_SHORT)
                    .show()
            }
        }
    }

	companion object {
        private const val DEFAULT_LANGUAGE = "cn"
    }

注意 TessBaseAPI.init() 的第一个参数,路径必须是包含了 tessdata 目录的父目录,否则初始化会抛异常。

最后,点击"识别文字"按钮时,将被识别的 Bitmap 设置给 Tess 然后获取文字结果即可:

kotlin 复制代码
	fun recognition(view: View) {
        mTessBaseAPI?.setImage(mResultImage)
        mBinding.tvIdNumber.text = mTessBaseAPI?.utF8Text
        mTessBaseAPI?.clear()
    }

当然,从最终的识别结果来看,并没有达到百分百的准确率,这与训练样本的数量不够有关。Tesseract-OCR 的样本训练方法,可参考超级详细的Tesseract-OCR样本训练方法

相关推荐
命运之手40 分钟前
【Android】自定义换肤框架01之皮肤包制作
android·skin·skinner·换肤框架·不重启换肤
练习本1 小时前
android perfetto使用技巧梳理
android
GitLqr2 小时前
Android - 云游戏本地悬浮输入框实现
android·开源·jitpack
周周的Unity小屋2 小时前
Unity实现安卓App预览图片、Pdf文件和视频的一种解决方案
android·unity·pdf·游戏引擎·webview·3dwebview
单丽尔4 小时前
Gemini for China 大更新,现已上架 Android APP!
android
JerryHe5 小时前
Android Camera API发展历程
android·数码相机·camera·camera api
Synaric6 小时前
Android与Java后端联调RSA加密的注意事项
android·java·开发语言
程序员老刘·7 小时前
如何评价Flutter?
android·flutter·ios
JoyceMill9 小时前
Android 图像效果的奥秘
android
取名真难.10 小时前
人脸检测(Python)
python·opencv·计算机视觉