上海建设摩托车官方网站,如何做好网络推广销售,如何做外文网站,o2o网站开发OpenCV 入门系列#xff1a; OpenCV 入门#xff08;一#xff09;—— OpenCV 基础 OpenCV 入门#xff08;二#xff09;—— 车牌定位 OpenCV 入门#xff08;三#xff09;—— 车牌筛选 OpenCV 入门#xff08;四#xff09;—— 车牌号识别 OpenCV 入门#xf…OpenCV 入门系列 OpenCV 入门一—— OpenCV 基础 OpenCV 入门二—— 车牌定位 OpenCV 入门三—— 车牌筛选 OpenCV 入门四—— 车牌号识别 OpenCV 入门五—— 人脸识别模型训练与 Windows 下的人脸识别 OpenCV 入门六—— Android 下的人脸识别 OpenCV 入门七—— 身份证识别 利用 OpenCV 实现身份证识别 Demo 效果 主要步骤分为两大步
利用 OpenCV 从完整的身份证图片中识别出身份证号码区域并返回身份证号码的图片利用 OCR 识别工具将身份证号码图片识别成文字
实际上身份证识别、银行卡识别都是相同的思路。
1、OpenCV 图像识别
1.1 上层代码过程
在 Activity 中点击“从相册中查找”按钮从相册中选择一张图片转换为一个 640 * 480 的 Bitmap 设置到 ImageView 中
class MainActivity : AppCompatActivity() {private lateinit var mBinding: ActivityMainBindingprivate var mFullImage: Bitmap? nulloverride 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? nullif (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 nullmBinding.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 trueBitmapFactory.decodeFile(imagePath, tempOptions)// 计算出缩放倍数以及缩放后的宽高var tempWidth tempOptions.outWidthvar tempHeight tempOptions.outHeightvar scale 1while (true) {if (tempWidth STANDARD_ID_CARD_WIDTH tempHeight STANDARD_ID_CARD_HEIGHT) {break}tempWidth / 2tempHeight / 2scale * 2}// 利用计算好的宽高与缩放倍数解析出一个 Bitmapval options BitmapFactory.Options()options.outWidth tempWidthoptions.outHeight tempHeightoptions.inSampleSize scalereturn BitmapFactory.decodeFile(imagePath, options)}companion object {private val TAG MainActivity::class.java.simpleNameprivate const val REQUEST_CODE 100private const val STANDARD_ID_CARD_WIDTH 640private const val STANDARD_ID_CARD_HEIGHT 480}
}然后点击“查找 ID”按钮时将完整的身份证 Bitmap 传给 ImageProcessor 交由 Native 层的 OpenCV 进行识别 private var mResultImage: Bitmap? null/*** 从整张图片中截取出身份证号码区域*/fun searchIdImage(view: View) {mBinding.tvIdNumber.text nullmResultImage 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)}JvmStaticexternal 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.hppusing 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 转换成 bitmapJava_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 * 400resize(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.轮廓检测vectorvectorPoint contours;vectorRect 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 分支它具有如下特征
简单易用开源且支持离线使用为 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: Intwhile (inputStream.read(buffer).also { len it } ! -1) {fos.write(buffer, 0, len)}fos.close()}inputStream.close()// init 传入的 datapath 必须是包含 tessdata 的目录returnasync mTessBaseAPI?.init(/sdcard/tess, DEFAULT_LANGUAGE) ?: false} catch (e: IOException) {e.printStackTrace()}returnasync false}// 3.处理异步任务结果dismissProgress()if (!result.await()) {Toast.makeText(thisMainActivity, 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?.utF8TextmTessBaseAPI?.clear()}当然从最终的识别结果来看并没有达到百分百的准确率这与训练样本的数量不够有关。Tesseract-OCR 的样本训练方法可参考超级详细的Tesseract-OCR样本训练方法。