OpenCV 入门系列:
OpenCV 入门(一)------ OpenCV 基础
OpenCV 入门(二)------ 车牌定位
OpenCV 入门(三)------ 车牌筛选
OpenCV 入门(四)------ 车牌号识别
OpenCV 入门(五)------ 人脸识别模型训练与 Windows 下的人脸识别
OpenCV 入门(六)------ Android 下的人脸识别
OpenCV 入门(七)------ 身份证识别
利用 OpenCV 实现身份证识别 Demo 效果:
主要步骤分为两大步:
- 利用 OpenCV 从完整的身份证图片中识别出身份证号码区域,并返回身份证号码的图片
- 利用 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 分支,它具有如下特征:
- 简单易用
- 开源且支持离线使用
- 为 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样本训练方法。