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

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

总体思路

tess-two实现离线OCR

引入依赖:

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

MainActivity.java

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就行),如下:

> 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库,然后集成到项目即可:

├─assets
├─cpp
│  ├─include
│  │  ├─opencv
│  │  └─opencv2
│  └─libs
│      └─armeabi-v7a
├─java
│  └─cn
│      └─tim
│          └─idcard
└─res
├─drawable
├─layout

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

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

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

#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。