android下使用OpenCV实现身份证号码的识别

身份证识别!听起来好难,不会做。。。,又要实现这个功能,跟领导说使用收费的吧,又不好意思!自己动手实现才是王道。摸索+百度,暂时实现了一个身份证号码的识别。记录一下。
首先准备工作:都知道要使用开源库OpenCV,可以去官网下载或者git上都有,OpenCV很友好,已经为我们Android编译好了动态库和include供我们使用,我自己的环境:AndroidStudio3.6+NDK_r15,还需要文字识别库OCR,不想去下载可以使用我分享的网盘,里面包含了需要的所有文件:链接: https://pan.baidu.com/s/1LdF2Zgl4FChNiMCLlNWjSg 提取码: z73h

image.png

接下来开始具体的实现过程:
一、先自己实现一个ocr的语言训练库
1、将下载下来的tesseract-ocr-w64-setup-v5.0.0.20190623.exe安转到电脑上,很简单一直next就行了,安装好只有需要配置两个环境变量,给Path里面添加tesseract的安装路径,如下:
image.png

这是一个检测工具,它已经自带的给我们生成了一个eng.traineddata训练数据,一会可以检测识别一张图片,就是网盘里面的图片id7.tif .
image.png

2、还需要新建一个TESSDATA_PREFIX路径,就是训练data的路径,例如我的是这样的:


image.png

配置成功后可以检测一下,打开cmd命令输入:tesseract -v


image.png

接着我们来检测识别一下图片:输入命令:tesseract F:\study\Tesseract-OCR\tessdata\id7.tif 22 回车,我们得到一个22.txt的文件 就是识别的结果文件 如下图


image.png

3ok了,加测工具配置好了,我们需要训练工具,网盘下载下来的jTessBoxEditor.zip我们解压后有一个.jar文件
image.png

通过这个工具我们合并图片以及识别校验图片内容,在cmd命令中输入java -jar jTessBoxEditor.jar 就启动了这个工具


image.png

4、点击 Tools->Merge TIFF 找到训练样本(样本自己找哦,这里提供一张),全选中,打开,出现界面


image.png

这是要保存合并的图片,名字命名一定要规范,命名的格式如下(很重要):[语言].[字体].exp[num].tif

例如我定义的名字:zh.song.exp1.tif 其中exp一定不能随便写,其他可以自己定义,然后点击保存。我们就会看到有一个文件生成了
image.png

强调一下,使用jTessBoxEditor工具合并图片的时候,我遇到了一个问题,20张图片合并的时候报错了,提示“couldn't seek!” 莫慌! 使用网盘提供的TiffToy工具去合成,是一样的~~~ 工具打开就会用。

5、将zh.song.exp1.tif 文件复制到Tesseract-OCR安装目录。


image.png

在Tesseract-OCR安装目录下执行命令:
tesseract zh.song.exp1.tif zh.song.exp1 batch.nochop makebox
生成box文件


image.png

6、将生成的这两个文件放入同一个目录下,可以自己新建一个目录
image.png

7、运行jTessBoxEditor工具,点击Box Editor->open,打开合并的tif文件。


image.png

8、开始校验
image.png

将不对数据改回来就ok了。
9、定义字体特征文件。创建一个名称为font_properties的字体特征文件。内容如下:
image.png

解释一下: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.

image.png

执行完之后就得到了一个zh.traineddata的文件了,这就是库文件了、、、

二、第一步完成了,接下来就是我们熟悉的Android开发了。
1、新建一个C++项目,不用多说,都懂得。直接把训练的文件放入main中


image.png

2、将网盘下载下来的opencv的zip解压,将include文件夹拷贝到项目的cpp文件夹下,我们需要的so拷贝到对应的jniLibs


image.png

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();
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,723评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,080评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,604评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,440评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,431评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,499评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,893评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,541评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,751评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,547评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,619评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,320评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,890评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,896评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,137评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,796评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,335评论 2 342

推荐阅读更多精彩内容