NDK系列之OpenCV 身份证信息离线识别技术实战,本节主要是通过OpenCV C++库,实现身份证信息识别,如身份证号码识别,本节使用到的技术点同样适用于车牌号识别、银行卡号码识别等。
实现效果:
本节主要内容:
1.OpenCV库导入;
2.Java层代码实现;
3.Native层身份证识别;
4.OCR识别图片上面的文字。
源码:
NdkIdCard: NDK OpenCV 身份证信息离线识别
一、OpenCV库导入
1)复制OpenCV源文件到cpp目录下,动态库文件复制到jniLibs目录下:
2)在CMakeLists文件中,导入源文件和库文件
二、Java层代码实现
MainActivity的从相册中选择身份证图片、查找身份证图片中的身份证号码区域、识别身份证号码区域的文字(身份证号码):
1)从手机相册中选择身份证图片,这个大家基本都会,不具体说明,最终获取到身份证图片的Bitmap对象赋值给fullImage变量;
public void search(View view) {
if (!initPermission()) return;
Intent intent;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
intent = new Intent();
intent.setAction(Intent.ACTION_GET_CONTENT);
} else {
intent = new Intent(Intent.ACTION_PICK,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
}
intent.setType("image/*");
//使用选取器并自定义标题
startActivityForResult(Intent.createChooser(intent, "选择待识别图片"), 100);
}
2)查找身份证图片中的身份证号码区域
将Java层获取到的选择的身份证图片的Bitmap,即fullImage变量,传给Native层,通过OpenCV库,将身份证图片中的身份证号码区域找到,并裁剪出来;
public void searchId(View view) {
tesstext.setText(null);
ResultImage = null;
Bitmap bitmapResult = ImageProcess.getIdNumber(fullImage, Bitmap.Config.ARGB_8888);
fullImage.recycle();
ResultImage = bitmapResult;
//tesseract-ocr
id.setImageBitmap(bitmapResult);
}
ImageProcess.java:
public class ImageProcess {
static {
System.loadLibrary("native-lib");
}
public static native Bitmap getIdNumber(Bitmap src, Bitmap.Config config);
}
3)识别身份证号码区域的文字
将获取到的身份证号码区域图片,使用OCR识别成文字。
public void recognition(View view) {
// 识别Bitmap中的图片
baseApi.setImage(ResultImage);
tesstext.setText(baseApi.getUTF8Text());
baseApi.clear();
}
三、Native层身份证识别
Native层工作主要是:获取Java层传递过来的身份证原图,使用OpenCV,找到身份证号码区域,裁剪该区域图片,并转化为Bitmap格式,返回Java层。
总体思路:
实现逻辑:
1)将Java层传递过来的bitmap转成的Mat,因为OpenCV支持的是Mat格式。
Mat src_img; // 身份证原图
Mat dst_img; // 身份证号码区域图
//将bitmap转换为Mat型格式数据
Java_org_opencv_android_Utils_nBitmapToMat2(env, type, src, (jlong) &src_img, 0);
2)无损压缩,压缩图片至640*400,减少后续查找身份证号码区域时间,提高效率。
#define DEFAULT_CARD_WIDTH 640
#define DEFAULT_CARD_HEIGHT 400
#define FIX_IDCARD_SIZE Size(DEFAULT_CARD_WIDTH,DEFAULT_CARD_HEIGHT)
resize(src_img, src_img, FIX_IDCARD_SIZE);
压缩后身份证图:
3)灰度化,查找身份证号码区域跟图片颜色无关,将图片颜色值RBG 转化为 Gray,RBG: 256 *256 *256 = 2^24,Gray:256,转化后理论上效率提升提高2^16倍。转化公式:Gray f(i,j)=0.30R(i,j)+0.59G(i,j)+0.11B(i,j),这里直接使用OpenCV API即可:
cvtColor(src_img, dst, COLOR_BGR2GRAY);
灰度化后身份证图:
4)二值化,再一步过滤,降噪;将图片颜色设置成只有黑白两种颜色,过滤掉灰色部分,如身份证背景波纹。
threshold(dst, dst, 100, 255, CV_THRESH_BINARY);
二值化后身份证图:黑白色
5)膨胀,发酵,Native层目的是查找身份证号码区域,从身份证图中可以看出,身份证号码区域是一块有间隔连续的号码区域;我们是不是可以将像素点膨胀,使得身份证号码区域成为一块连续的号码区域;
Mat dst; // 膨胀身份证图
Mat erodeElement = getStructuringElement(MORPH_RECT, Size(20, 10));
erode(dst, dst, erodeElement);
膨胀后身份证图:
6)轮廓检测,将连续像素点识别出来,并筛选出符合身份证号码区域的结果存放到集合中;身份证号码区域宽高比例 > 1:9,即width > height * 9的加入集合。
vector<vector<Point> > contours;
vector<Rect> rects;
findContours(dst, contours, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (int i = 0; i < contours.size(); i++) {
Rect rect = boundingRect(contours.at(i));
//rectangle(dst, rect, Scalar(0, 0, 255)); // 在dst 图片上显示 rect 矩形
if (rect.width > rect.height * 9) {
rects.push_back(rect);
rectangle(dst, rect, Scalar(0, 255, 255));
dst_img = src_img(rect);
}
}
轮廓检测后的身份证图:
7)如果筛选后集合中只有1条数据,那么该数据将是身份证号码区域;否则遍历集合,取区域y坐标值最大的区域为身份证号码区域,因为身份证号码区域在身份证图中位于最底部。
注:真实场景可能存在身份证原图旋转,或者倒着等不规范的图片,我们要在拍照的时候提示用户将身份证放到识别框中拍照,尽可能从上层避免上面所说的不规范图片。
if (rects.size() == 1) {
Rect rect = rects.at(0);
dst_img = src_img(rect);
} else {
int lowPoint = 0;
Rect finalRect;
for (int i = 0; i < rects.size(); i++) {
Rect rect = rects.at(i);
Point p = rect.tl();
if (rect.tl().y > lowPoint) {
lowPoint = rect.tl().y;
finalRect = rect;
}
}
rectangle(dst, finalRect, Scalar(255, 255, 0));
dst_img = src_img(finalRect);
}
身份证识别框拍照:
8)将裁剪的身份证号码图dst_img(Mat格式)转化为bitmap格式,返回java层
jobject bitmap = createBitmap(env, dst_img, config);
src_img.release();
dst_img.release();
dst.release();
return bitmap;
裁剪的身份证号码图 :
四、OCR识别图片上面的文字
1)依赖OCR 库 tess-two
implementation 'com.rmtheis:tess-two:7.0.0'
2)导入身份证号码识别训练学习的样本,训练学习的样本越多,OCR识别到的身份证号码越准确。
身份证号码识别训练学习的样本是一个二进制文件:cn.traineddata,这里可以直接使用我源码里面的身份证号码识别训练学习的样本。区别于收费的OCR识别公司,只要在于训练学习的样本数量,个人开发者很难做到使用大量的样本数完成训练学习工作;所以由于我的训练学样本很少,会存在一些号码数字识别不准确,如6可能识别到的是4等。
3)初始化tess-two库
private TessBaseAPI baseApi;
private void initTess() {
new AsyncTask<Void, Void, Boolean>() {
@Override
protected void onPreExecute() {
showProgress();
}
@Override
protected void onPostExecute(Boolean result) {
dismissProgress();
if (!result) {
Toast.makeText(MainActivity.this, "load trainedData failed", Toast.LENGTH_SHORT).show();
}
}
@RequiresApi(api = Build.VERSION_CODES.N)
@Override
protected Boolean doInBackground(Void... params) {
baseApi = new TessBaseAPI();
try {
InputStream is = null;
is = getAssets().open(language + ".traineddata");
String path = mContext.getDataDir().getAbsolutePath();
LogUtil.i(TAG, "initTess path " + path);
File file = new File(path + "/tessdata/" + language + ".traineddata");
if (!file.exists()) {
file.getParentFile().mkdirs();
FileOutputStream fos = new FileOutputStream(file);
byte[] buffer = new byte[2048];
int len;
while ((len = is.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
fos.close();
}
is.close();
return baseApi.init(path, language);
} catch (IOException e) {
e.printStackTrace();
LogUtil.i(TAG, "initTess err " + e.getMessage());
}
return false;
}
}.execute();
}
4)调用API识别身份证号码区域的文字
public void recognition(View view) {
// 识别Bitmap中的图片
baseApi.setImage(ResultImage);
tesstext.setText(baseApi.getUTF8Text());
baseApi.clear();
}
至此,OpenCV 身份证信息离线识别技术项目已完成;车牌号识别、银行卡号码识别等也是同样的实现思想。
源码: