身份证识别!听起来好难,不会做。。。,又要实现这个功能,跟领导说使用收费的吧,又不好意思!自己动手实现才是王道。摸索+百度,暂时实现了一个身份证号码的识别。记录一下。
首先准备工作:都知道要使用开源库OpenCV,可以去官网下载或者git上都有,OpenCV很友好,已经为我们Android编译好了动态库和include供我们使用,我自己的环境:AndroidStudio3.6+NDK_r15,还需要文字识别库OCR,不想去下载可以使用我分享的网盘,里面包含了需要的所有文件:链接: https://pan.baidu.com/s/1LdF2Zgl4FChNiMCLlNWjSg 提取码: z73h
接下来开始具体的实现过程:
一、先自己实现一个ocr的语言训练库
1、将下载下来的tesseract-ocr-w64-setup-v5.0.0.20190623.exe安转到电脑上,很简单一直next就行了,安装好只有需要配置两个环境变量,给Path里面添加tesseract的安装路径,如下:
这是一个检测工具,它已经自带的给我们生成了一个eng.traineddata训练数据,一会可以检测识别一张图片,就是网盘里面的图片id7.tif .
2、还需要新建一个TESSDATA_PREFIX路径,就是训练data的路径,例如我的是这样的:
配置成功后可以检测一下,打开cmd命令输入:tesseract -v
接着我们来检测识别一下图片:输入命令:tesseract F:\study\Tesseract-OCR\tessdata\id7.tif 22 回车,我们得到一个22.txt的文件 就是识别的结果文件 如下图
3ok了,加测工具配置好了,我们需要训练工具,网盘下载下来的jTessBoxEditor.zip我们解压后有一个.jar文件
通过这个工具我们合并图片以及识别校验图片内容,在cmd命令中输入java -jar jTessBoxEditor.jar 就启动了这个工具
4、点击 Tools->Merge TIFF 找到训练样本(样本自己找哦,这里提供一张),全选中,打开,出现界面
这是要保存合并的图片,名字命名一定要规范,命名的格式如下(很重要):[语言].[字体].exp[num].tif
强调一下,使用jTessBoxEditor工具合并图片的时候,我遇到了一个问题,20张图片合并的时候报错了,提示“couldn't seek!” 莫慌! 使用网盘提供的TiffToy工具去合成,是一样的~~~ 工具打开就会用。
5、将zh.song.exp1.tif 文件复制到Tesseract-OCR安装目录。
在Tesseract-OCR安装目录下执行命令:
tesseract zh.song.exp1.tif zh.song.exp1 batch.nochop makebox
生成box文件
6、将生成的这两个文件放入同一个目录下,可以自己新建一个目录
7、运行jTessBoxEditor工具,点击Box Editor->open,打开合并的tif文件。
8、开始校验
将不对数据改回来就ok了。
9、定义字体特征文件。创建一个名称为font_properties的字体特征文件。内容如下:
解释一下:song就是我们合并图片时起的名字,后面5个0要有空格,分别代表的意思是 <italic> 、<bold> 、<fixed> 、<serif>、 <fraktur>的取值为1或0,表示字体是否具有这些属性。
10、生成语言文件。在样本图片所在目录下创建一个批处理文件,输入如下内容:
rem 执行改批处理前先要目录下创建font_properties文件
echo Run Tesseract for Training..
tesseract zh.song.exp1.tif zh.song.exp1 nobatch box.train
echo Compute the Character Set..
unicharset_extractor zh.song.exp1.box
mftraining -F font_properties -U unicharset -O zh.unicharset zh.song.exp1.tr
echo Clustering..
cntraining zh.song.exp1.tr
echo Rename Files..
ren normproto zh.normproto
ren inttemp zh.inttemp
ren pffmtable zh.pffmtable
ren shapetable zh.shapetable
echo Create Tessdata..
combine_tessdata zh.
执行完之后就得到了一个zh.traineddata的文件了,这就是库文件了、、、
二、第一步完成了,接下来就是我们熟悉的Android开发了。
1、新建一个C++项目,不用多说,都懂得。直接把训练的文件放入main中
2、将网盘下载下来的opencv的zip解压,将include文件夹拷贝到项目的cpp文件夹下,我们需要的so拷贝到对应的jniLibs
3、编辑CMakeLists.txt文件
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.10.2)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
add_library( # Sets the name of the library.
shensuCid
SHARED
CidOcr.cpp)
find_library( # Sets the name of the path variable.
log-lib
log)
# -L 指定库的查找路径
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/../jniLibs/${CMAKE_ANDROID_ARCH_ABI}")
#设置头文件查找路径
include_directories(include)
target_link_libraries( # Specifies the target library.
shensuCid
opencv_java4
# jnigraphics
${log-lib})
一切准备就绪了,编写我们的C++文件,就是将图片数据传给jni,经过一系列处理得到一个bitmap。基本思路就是将相机的yuv格式的data数据传给opencv。
主函数内容如下:
#define DEFAULT_CARD_WIDTH 640
#define DEFAULT_CARD_HEIGHT 400
#define FIX_IDCARD_SIZE Size(DEFAULT_CARD_WIDTH,DEFAULT_CARD_HEIGHT)
using namespace cv;
using namespace std;
extern "C" JNIEXPORT void JNICALL Java_org_opencv_android_Utils_nBitmapToMat2
(JNIEnv *env, jclass, jobject bitmap, jlong m_addr, jboolean needUnPremultiplyAlpha);
extern "C" 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) {
int imgWidth = srcData.cols;
int imgHeight = srcData.rows;
int numPix = 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, 0, (jlong) &srcData, jBmpObj);
return jBmpObj;
}
extern "C"
JNIEXPORT jobject JNICALL
Java_com_XXX_getIdBitmapWithYUVData(JNIEnv *env, jclass type,
jbyteArray yuvData_,
jint width, jint height,
jintArray picRect_,jobject config) {
jbyte *yuvData = env->GetByteArrayElements(yuvData_, NULL);
jint *picRect = env->GetIntArrayElements(picRect_, NULL);
// TODO 先将yuvdata转化为opencv的mat格式数据
Mat src_img;
Mat image(height + height/2,width,CV_8UC1,(unsigned char *)yuvData);
Mat mBgr;
cvtColor(image, mBgr, CV_YUV2BGR_NV21);
if(mBgr.empty()){
return NULL;
}
//picRect 就是手机屏幕绘制的方框的区域
if(picRect == NULL){
return NULL;
}
int left = picRect[0];
int top = picRect[1];
int right = picRect[2];
int bottom = picRect[3];
Rect finalRect(left,top,right - left,bottom);
src_img = mBgr(finalRect);
if(src_img.empty()){
return NULL;
}
Mat dst_img;
Mat dst;
//无损压缩//640*400
resize(src_img, src_img,FIX_IDCARD_SIZE);
//灰度化
cvtColor(src_img, dst, COLOR_BGR2GRAY);
//二值化
threshold(dst, dst, 115, 255, CV_THRESH_BINARY);
//膨胀,发酵,可以使黑色区域连接到一起
Mat erodeElement = getStructuringElement(MORPH_RECT, Size(20, 10));
erode(dst, dst, erodeElement);
//轮廓检测
vector< vector<Point> > contoursList;
vector<Rect> rects;
findContours(dst, contoursList, RETR_TREE, CHAIN_APPROX_SIMPLE, Point(0, 0));
for (int i = 0; i < contoursList.size(); i++){
Rect rect = boundingRect(contoursList.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);
}
}
if (rects.size() == 1) {
Rect rect = rects.at(0);
dst_img = src_img(rect);
}else {
int lowPoint = 0;
Rect finalContourRect;
for (int i = 0; i < rects.size(); i++){
Rect rect = rects.at(i);
Point point = rect.tl();
if (rect.tl().y > lowPoint) {
lowPoint = point.y;
finalContourRect = rect;
}
}
rectangle(dst, finalContourRect, Scalar(255, 255, 0));
dst_img = src_img(finalContourRect);
}
if(dst_img.empty()){
return NULL;
}
jobject bitmap = createBitmap(env,dst_img,config);
src_img.release();
dst_img.release();
dst.release();
env->ReleaseByteArrayElements(yuvData_, yuvData, 0);
env->ReleaseIntArrayElements(picRect_, picRect, 0);
return bitmap;
}
在java层:项目进入后需要有两件事情:将我们训练的文件拷贝到手机本地并且初始化TessBaseAPI:
private class TessAsyncTask extends AsyncTask<Void, Void, Boolean> {
private String language = "zh";
@Override
protected Boolean doInBackground(Void... voids) {
String tessPath = IdCardCaptureActivity.this.getApplicationContext().getCacheDir().getAbsolutePath() + "/tess/";
baseApi = new TessBaseAPI();
try {
InputStream is = null;
is = getAssets().open(language + ".traineddata");
File file = new File(tessPath + "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();
boolean result = baseApi.init(tessPath, language);
return result;
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
Log.e("init ocr:","init tess success!");
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(IdCardCaptureActivity.this);
builder.setTitle("初始化身份证识别库失败!\n");
builder.setMessage("找不到库文件!");
builder.setCancelable(true);
builder.create().show();
}
}
}
项目需要依赖tess库
implementation 'com.rmtheis:tess-two:9.1.0'
将得到的bitmap传给TessBaseAPI
tessBaseAPI.setImage(resultBitmap);
String cid = tessBaseAPI.getUTF8Text();