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

OpenCV 入门系列:

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

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

2024-4-24.身份证识别Demo效果

主要步骤分为两大步:

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

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

1、OpenCV 图像识别

1.1 上层代码过程

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

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 进行识别:

	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():

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():

#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 的识别函数:

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:

	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 然后获取文字结果即可:

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/595335.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

德国韦纳WENAROLL滚压刀,液压缸,滚光刀,挤压刀,滚轧刀

德国韦纳WENAROLL滚压刀,液压缸&#xff0c;滚光刀,挤压刀&#xff0c;滚轧刀&#xff08;百度一下&#xff0c;西安尚融&#xff09; 德国韦纳&#xff08;WENAROLL&#xff09;的滚压刀、液压缸、滚光刀、挤压刀和滚轧刀在工业领域享有很高的声誉&#xff0c;这些产品因其高…

SM618卡件SM480模块和利时

SM618卡件❗电:183-6998-1851❗SM480模块和利时。自动化程度的提高&#xff0c;I/O点数大幅增 加&#xff0c;传统单一配线的方式已经无法满足发展的需 要SM618卡件SM480模块和利时。&#xff0e;对简单、可靠的配线方式的需求日益强烈&#xff0e; 传统接线 - 以并联方式连 接…

C# WinForm —— 12 ListBox绑定数据

ListBox加载大量数据时&#xff0c;避免窗体闪烁的方法&#xff1a; 在加载语句的前后分别加上 BeginUpdate()方法 和 EndUpdate()方法 指定一个集合为绑定的数据源 1. 首先&#xff0c;右键项目&#xff0c;添加类 2. 在新建的类文件中添加属性值信息 3. 构建初始化的对象…

访问学者在外访学期间,是否可以中途回国?

在全球化的今天&#xff0c;访问学者制度已成为促进国际学术交流与合作的重要桥梁。然而&#xff0c;对于许多国外访问学者来说&#xff0c;一个常见的问题是&#xff1a;在访学期间&#xff0c;我是否可以中途回国&#xff1f;这个问题涉及到多个方面&#xff0c;包括政策法规…

7步教程从零开始搭建跨境电商平台开发

跨境电商平台开发一直是创业者们追逐的热门领域之一。本文将为您提供一个7步教程&#xff0c;帮助您从零开始搭建跨境电商平台&#xff0c;让您在这个充满机遇的领域中抢占先机。 步骤一&#xff1a;市场调研和定位 在开始搭建跨境电商平台之前&#xff0c;第一步是进行充分的…

大数据与会计专业主要学什么课程

大数据与会计专业是一个结合了传统会计知识与现代大数据技术的交叉学科&#xff0c;旨在培养既懂会计又熟悉大数据分析的复合型人才。该专业的学生将会学习以下主要课程内容&#xff1a; 会计基础课程&#xff1a;包括基础会计、财务会计、成本会计、管理会计等&#xff0c;这些…

我独自升级崛起下载教程 我独自升级崛起怎么一键下载

定于5月8日全球盛大发布的动作RPG力作《我独自升级崛起》&#xff0c;基于备受追捧的同名动画及网络漫画&#xff0c;誓为热情洋溢的游戏爱好者们呈献一场深度与广度兼具的冒险盛宴。这款游戏巧妙融合网络武侠元素&#xff0c;其创意十足的设计框架下&#xff0c;核心叙述聚焦于…

OSPF综合实验(超详细易懂)(HCIP)

1、拓扑信息 2、需求分析 3、IP规划 4、配置 5、测试 1、拓扑信息 2、需求分析 R4为ISP&#xff0c;其上只能配置I地址&#xff1b; R4与其他所有直连设备间均使用公有IP 公网中使用的是点到…

外贸大客户开发的三大困境

外贸大客户开发的三大困境&#xff0c;第一个是进不来&#xff0c;什么叫进不来呢&#xff1f;就是客户&#xff0c;大客户他不仅能够为企业带来大额的业绩&#xff0c;而且利润也高&#xff0c;那么也对于这种品牌也有一定的关联&#xff0c;还能为企业带来更多的一些资源&…

Python测试框架Pytest的参数化详解

上篇博文介绍过&#xff0c;Pytest是目前比较成熟功能齐全的测试框架&#xff0c;使用率肯定也不断攀升。 在实际工作中&#xff0c;许多测试用例都是类似的重复&#xff0c;一个个写最后代码会显得很冗余。这里&#xff0c;我们来了解一下pytest.mark.parametrize装饰器&…

karateclub,一个超酷的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个超酷的 Python 库 - karateclub。 Github地址&#xff1a;https://github.com/benedekrozemberczki/karateclub Python karateclub是一个用于图嵌入和图聚类的库&#xff…

git commit 提交报错pre-commit hook failed (add --no-verify to bypass) 解决方法,亲测有效

问题截图 今天在执行 git commit 命令时报错&#xff1a;pre-commit hook failed (add --no-verify to bypass) 解决 参考文章&#xff1a;git commit报错&#xff1a;pre-commit hook failed的解决方法 具体原理什么的就不解释了&#xff0c;可以看看上面的参考文章 解决方…

如何使用高德地图的 Loca 展示 gpx 文件的 3D 路径,Loca.LineLayer

如何使用高德地图的 Loca 展示 gpx 文件的 3D 路径&#xff0c;Loca.LineLayer 找寻了好久&#xff0c;终于将这个展示 3D 路径的功能实现了。 在线实例&#xff1a; http://kylebing.cn/tools/map/#/gpx/gpx-viewer-3d 这里是用于展示 gpx 路径&#xff0c;关于 gpx 的相关知…

Linux migrate_type进一步探索

文章接着上回Linux migrate_type初步探索 1、物理页面添加到buddy系统 我们都知道物理内存一开始是由memblock进行分配管理&#xff0c;后面会切换到buddy系统管理。那么接下来我们看一下&#xff0c;memblock管理的物理页面是怎么添加到buddy系统中的。 start_kernel() -&g…

液晶数显式液压万能试验机WES-300B

一、简介 主机为两立柱、两丝杠、油缸下置式&#xff0c;拉伸空间位于主机的上方&#xff0c;压缩、弯曲试验空间位于主机下横梁和工作台之间。测力仪表采用高清液晶显示屏&#xff0c;实验数据方便直观。 主要性能技术指标 最大试验力&#xff08;kN&#xff09; 300 试…

文件删了,回收站清空了怎么恢复?文件恢复软件一览

在日常生活和工作中&#xff0c;我们常常会遇到误删除文件的情况&#xff0c;有时甚至会因为清空了回收站而无法找回这些文件。这些文件可能包含重要的工作数据、个人照片或其他珍贵的回忆。那么&#xff0c;在这种情况下&#xff0c;我们该如何恢复这些被删除且清空回收站的文…

外婆传(封家香传)

余乃民国三十载&#xff08;公元一千九百四十一&#xff09;九月初九重阳佳节日出生于衡阳县长塘村封谷里。父封盖梅&#xff0c;在民国二十九年&#xff08;公元一千九百四十&#xff09;驾鹤西归&#xff0c;遗世独立&#xff0c;吾未能见其颜。母氏&#xff0c;因丧夫之痛&a…

C++ | Leetcode C++题解之第59题螺旋矩阵II

题目&#xff1a; 题解&#xff1a; class Solution { public:vector<vector<int>> generateMatrix(int n) {int num 1;vector<vector<int>> matrix(n, vector<int>(n));int left 0, right n - 1, top 0, bottom n - 1;while (left < r…

微信公众号排名 SEO的5个策略

随着微信公众号在社交媒体领域的持续发展和普及&#xff0c;如何提升公众号的搜索排名&#xff0c;成为许多运营者关注的焦点。公众号排名SEO&#xff0c;即针对微信公众号进行搜索引擎优化&#xff0c;旨在提高公众号在搜索结果中的曝光率和点击率。下面&#xff0c;我们将深入…

python学习笔记-01

python 在学习之前要了解的事项&#xff1a; 1.python缩进语法要求较为严格 2.是解释型语言 3.python2版本和python3版本不兼容 本系列笔记全部基于python3 1.hello world 安装好python之后&#xff0c;可以直接打开python&#xff0c;也可以通过cmd进入python。 print(&qu…
最新文章