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();
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容