0%

OpenCV 身份证离线识别技术实战 (二)

OpenCV 身份证离线识别技术实战的终章,首先是集成 tess-two 到 Android 完成离线 OCR,然后再移植从图像预处理的代码移植到 Android 上, 这两件事情完成便搞定了身份证号码离线识别的功能了。最后思考一点:如果身份证图像不是正放的应该怎么处理呢?

总体思路

tess-two 实现离线 OCR

引入依赖:

1
implementation 'com.rmtheis:tess-two:9.1.0'

MainActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import com.googlecode.tesseract.android.TessBaseAPI;

public class MainActivity extends AppCompatActivity {

private static final String PATH = "/storage/emulated/0/";
// 中文识别的训练结果
private static final String MODEL_NAME = "chi_sim.traineddata";

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate (savedInstanceState);
setContentView (R.layout.activity_main);
TextView tv = findViewById (R.id.tv);

new Thread(()-> {
copyTrainedDataToSDCard ();
TessBaseAPI baseAPI = new TessBaseAPI();
baseAPI.setDebug (true);
// PATH 下应该包含 tessdata 文件夹,设置语言简体中文
baseAPI.init (PATH, "chi_sim");
baseAPI.setImage (xxxx);
String utf8Text = baseAPI.getUTF8Text ();
runOnUiThread (()->{
Log.i (TAG, "onCreate: utf8Text = " + utf8Text);
tv.setText (utf8Text);
});
}).start ();
}
}

其实通过 tess-two 实现离线 OCR 的过程,只需要注意以下几点:

1、经过训练的模型需要放在本地,而且处于 tessdata 文件夹下(只要模型的文件夹是 tessdata 就行),如下:

1
2
3
4
5
6
7
8
9
> adb shell
HWGLK:/ $ cd /storage/emulated/0/tessdata/
HWGLK:/storage/emulated/0/tessdata $ ls -ahl
total 21M
drwxrwx--x 2 root sdcard_rw 3.4K 2021-12-28 00:21 .
drwxrwx--x 142 root sdcard_rw 20K 2021-12-29 19:53 ..
-rw-rw---- 1 root sdcard_rw 42M 2021-12-28 20:26 chi_sim.traineddata
HWGLK:/storage/emulated/0/tessdata $ pwd
/storage/emulated/0/tessdata

无论是从网络下载,还是从 assets 文件夹 copy 到 sdcard,只要模型文件所处的文件夹符合上述规则即可。

2、https://github.com/tesseract-ocr/tessdata ,可以从这里下载到已经训练好的模型文件,如果要做中文文字识别,那么下载 chi_sim.traineddata 即可,并且在代码中指定要识别的文字是简体中文(chi_sim)。

3、识别过程最好放在子线程执行,不要阻塞 UI 线程。

集成 OpenCV 到 Android

我这里直接用 OpenCV 官网编译好的 so 库,然后集成到项目即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├─assets
├─cpp
│ ├─include
│ │ ├─opencv
│ │ └─opencv2
│ └─libs
│ └─armeabi-v7a
├─java
│ └─cn
│ └─tim
│ └─idcard
└─res
├─drawable
├─layout

CMakeLists.txt,由于需要创建 Bitmap,所以需要引入 jnigraphics 这个库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmake_minimum_required(VERSION 3.10.2)
project('tess_two_ocr')

find_library(jnigraphics-lib jnigraphics)

find_library(log-lib log)

include_directories(include)

link_directories(libs/${ANDROID_ABI})

add_library(native-lib SHARED native-lib.cpp)

target_link_libraries(native-lib ${log-lib} opencv_java3 jnigraphics)

build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
plugins {
id 'com.android.application'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.0"

defaultConfig {
applicationId "cn.tim.tess_two_ocr"
minSdkVersion 21
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {
cppFlags "-std=c++11"
}
}

ndk {
// 设置支持的架构平台
abiFilters "armeabi-v7a"
}
}

buildTypes {
release {
minifyEnabled false
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
ndkVersion '21.1.6352462'

sourceSets {
main {
// 配置一下库的位置
jniLibs.srcDirs = ['src/main/cpp/libs']
}
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
//tess-two 的依赖
implementation 'com.rmtheis:tess-two:9.1.0'
}

native-lib.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
#include <jni.h>
#include <android/bitmap.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>
#include <opencv2/opencv.hpp>
#include <android/log.h>


#define LOG_TAG "native"

#define LOGI (...) __android_log_print (ANDROID_LOG_INFO,LOG_TAG,__VA_ARGS__)

#define DEFAULT_CARD_WIDTH 640
#define DEFAULT_CARD_HEIGHT 400
#define FIX_IDCARD_SIZE Size (DEFAULT_CARD_WIDTH, DEFAULT_CARD_HEIGHT)

extern "C" JNIEXPORT jstring JNICALL
Java_cn_tim_idcard_MainActivity_stringFromJNI(JNIEnv* env, jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}

extern "C" {
using namespace cv;
using namespace std;

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);

jobject createBitmap(JNIEnv *env, Mat srcData, jobject config) {
// Image Details
int imgWidth = srcData.cols;
int imgHeight = srcData.rows;

LOGI("imgWidth=% d, imgHeight=% d\n", imgWidth, imgHeight);
jclass bmpCls = env->FindClass("android/graphics/Bitmap");
jmethodID createBitmapMid = env->GetStaticMethodID(bmpCls, "createBitmap", "(IILandroid/graphics/Bitmap$Config;) Landroid/graphics/Bitmap;");
jobject jBmpObj = env->CallStaticObjectMethod(bmpCls, createBitmapMid, imgWidth, imgHeight, config);
Java_org_opencv_android_Utils_nMatToBitmap(env, nullptr, (jlong) &srcData, jBmpObj);
return jBmpObj;
}


JNIEXPORT jobject JNICALL
Java_cn_tim_idcard_MainActivity_getIdNumber(JNIEnv *env, jclass type, jobject src, jobject config) {
Mat src_img;
Mat dst_img;
//bitmap 转换为 Mat 型格式数据
Java_org_opencv_android_Utils_nBitmapToMat2(env, type, src, (jlong) &src_img, 0);

Mat dst;
// 无损压缩 640*400
resize(src_img, src_img,FIX_IDCARD_SIZE);
// 灰度化
cvtColor(src_img, dst, COLOR_BGR2GRAY);
// 二值化
threshold(dst, dst, 100, 255, CV_THRESH_BINARY);
// 膨胀处理
Mat erodeElement = getStructuringElement(MORPH_RECT, Size(20, 10));
erode(dst, dst, erodeElement);
// 轮廓检测
vector< vector<Point> > contours;
vector<Rect> rects;

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

// 轮廓检测
for (auto & contour : contours) {
Rect rect = boundingRect(contour);
if (rect.width > rect.height * 9) {
rects.push_back(rect);
rectangle(dst, rect, Scalar(0,255,255));
dst_img = src_img(rect);
}
}

if (rects.size() == 1) {
Rect rect = rects.at(0);
dst_img = src_img(rect);
}else {
int lowPoint = 0;
Rect finalRect;
for (const auto& rect : rects) {
if (rect.tl().y > lowPoint) {
lowPoint = rect.tl().y;
finalRect = rect;
}
}
rectangle(dst, finalRect, Scalar(255, 255, 0));
dst_img = src_img(finalRect);
}

jobject bitmap = createBitmap(env, dst_img, config);

end:
src_img.release();
dst_img.release();
dst.release();

return bitmap;
}
}

完整工程可见, https://github.com/zouchanglin/AndroidProjects/tree/main/tesstwoocr

开始提出的问题?

其实要想解决身份证图像不是正放的最简单的处理方式就是:引导用户正确放置身份证的角度,如下图:

这样在识别不到身份证的情况下提示用户按照框的形式摆放就可以帮我们最大限度的提升处理的效率,根据这样的思路,我将实现一个完整的通过拍照识别 + 提示框辅助的方式来完成身份证识别的 Demo。

欢迎关注我的其它发布渠道