证件文字信息识别[JAVA调用C++]

安卓端上传校园卡照片,经识别之后返回识别结果(识别姓名、学号、学院)。
校园卡样例


9.jpg

识别结果


捕获.JPG

【&&&是为了后续处理而设立的分隔符】
在这里只说明图像处理的过程
最后附上全部代码

  1. 总流程和框架
    二值化处理图片,形态学处理得到文字区域,对满足条件的区域进行分割,将分割出的图进行OCR识别出字符串,最后JAVA利用JNA调用dll [ vs里创建的项目类型要选桌面-dll ]

使用到的框架 openCV, tesseract, JNA
头文件

#include <opencv2/opencv.hpp> //Include file for every supported OpenCV function
#include <algorithm> 
#include <tesseract/baseapi.h>
#include <cstring>
#ifndef CARD_RECOGNIZER
#define CARD_RECOGNIZER

cv::Mat mat, grayMat;
std::vector<cv::RotatedRect> textAreaRaw;
cv::Rect recognizeArea;

const int IMAGE_SIZE_WIDTH = 1400;      //horizontal direction
const int IMAGE_SIZE_HEIGHT = 900;      //vertical direction
const int INFO_COUNT = 3;               //the count of the text information that needs to be recognized
const int SAMPLE_SIZE_WIDTH = 100;
const int SAMPLE_SIZE_HEIGHT = 30;
const int RECOGNITION_SUCCESS = 1;
const int RECOGNITION_FAIL = 2;

const char* RECOGNIZATION_FAIL_STRING = "FAIL";

tesseract::TessBaseAPI tess;

extern "C" __declspec( dllexport ) const char* recognize(const char* filePath);
void gamma(cv::Mat& src, double c, double gamma);
void binaryProcess();
void segmentBinary(cv::Mat & mat);
int locateText(cv::Mat& inversed);
bool validArea(const cv::RotatedRect& rect);
void normalizeSingleArea(const cv::Mat& src, cv::RotatedRect& rawArea, cv::Mat& result);
char* UTF8ToANSI(const char * uft8);
wchar_t * Utf_8ToUnicode(const char * szU8);
char * UnicodeToAnsi(const wchar_t * szStr);
#endif // CARD_RECOGNIZER

注意要暴露给java的函数前要加上extern "C" __declspec( dllexport )

识别函数

const char * recognize(const char * filePath) {
    using namespace cv;
    //initialize the mat and gray mat according to filePath
    mat = imread(filePath, CV_LOAD_IMAGE_GRAYSCALE);
    resize(mat, mat, cv::Size(IMAGE_SIZE_WIDTH, IMAGE_SIZE_HEIGHT));
    grayMat = mat.clone();

    tess.Init(NULL, "chi_sim", tesseract::OEM_DEFAULT);
    tess.SetPageSegMode(tesseract::PSM_SINGLE_BLOCK);
    recognizeArea = cv::Rect(0.45*IMAGE_SIZE_WIDTH, 0.25*IMAGE_SIZE_HEIGHT, 0.4*IMAGE_SIZE_WIDTH, 0.455*IMAGE_SIZE_HEIGHT);

    binaryProcess();                // Binary Process

    // Get the inversed binary image.
    Mat inversed;
    bitwise_not(mat, inversed);

    // Get the valid contours and raw text areas
    if ( locateText(inversed) == RECOGNITION_FAIL ) {
        return RECOGNIZATION_FAIL_STRING;
    }

    // Sort the text areas by their position
    struct sortByY {
        bool operator () (const RotatedRect & a, const RotatedRect & b) {
            return a.center.y < b.center.y;
        }
    };
    std::sort(textAreaRaw.begin(), textAreaRaw.end(), sortByY());

    std::vector<Mat> segments(INFO_COUNT);
    std::string result;
    for ( int i = 0; i < INFO_COUNT; ++i ) {
        normalizeSingleArea(grayMat, textAreaRaw[ i ], segments[ i ]);
        segmentBinary(segments[ i ]);
        tess.SetImage(( uchar* ) segments[ i ].data, segments[ i ].cols, segments[ i ].rows, 1, segments[ i ].cols);
        char* resultUTF8 = tess.GetUTF8Text();
        result.append(resultUTF8);
        result.append("&&&");
        delete[] resultUTF8;
    }
    return UTF8ToANSI(result.c_str());
}

2.二值化处理
卡片拍摄受光照影响很大,加上卡片纹路,使用gamma变化之后加以自适应能够得到较好的效果

void gamma(cv::Mat & mat, double c, double gamma) {
    using namespace cv;
    Mat matFloat(mat.size(), CV_32FC1);
    for ( int i = 0; i < mat.rows; ++i ) {
        for ( int j = 0; j < mat.cols; ++j ) {
            matFloat.at<float>(i, j) = c * pow(mat.at<uchar>(i, j), gamma);
        }
    }
    normalize(matFloat, matFloat, 0, 255, CV_MINMAX);
    convertScaleAbs(matFloat, mat);
}

/*Process of a whole mat.*/
/*Works*/
void binaryProcess() {
    cv::Mat tempSRC = mat.clone();
    gamma(tempSRC, 1, 0.001);

    int blockSize = 31;
    int C = 50;
    int maxVal = 255;

    cv::adaptiveThreshold(tempSRC, mat, maxVal, CV_ADAPTIVE_THRESH_GAUSSIAN_C, CV_THRESH_BINARY, blockSize, C);
}
  1. 文字定位
    大致分五步
    【反色】
    【腐蚀】去掉证件上可能存在的纹路图案信息
    【膨胀】获得文字区域
    【获取轮廓】
    【轮廓筛选】

这一步的效果是这样的

int locateText(cv::Mat& inversedMat) {

    cv::Mat inversed = inversedMat.clone();

    // Get the element for erosion
    cv::Mat lineEliminator = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(2, 2));

    // Perform erosion to get rid of the subtle lines
    cv::erode(inversed, inversed, lineEliminator);

    // Perform dilation to make the contours more prominent
    cv::Mat dilator = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(8, 6));
    cv::dilate(inversed, inversed, dilator, cv::Point(-1, -1), 4);

    // Get the contours
    std::vector< std::vector <cv::Point> > contours;
    findContours(inversed, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE);           //CV_RETR_EXTERNAL:Retrieves exterior contours only
    
    // Erase bad contours
    std::vector<std::vector <cv::Point>>::iterator iterator = contours.begin();
    while ( iterator != contours.end() ) {
        cv::RotatedRect minRec = cv::minAreaRect(cv::Mat(*iterator));
        if ( validArea(minRec) ) {                                                      //The valid method should be ajusted with different situation.
            textAreaRaw.push_back(minRec);
            ++iterator;
        } else {
            iterator = contours.erase(iterator);
        }
    }

    if ( textAreaRaw.size() != INFO_COUNT ) {
        return RECOGNITION_FAIL;
    }

    return RECOGNITION_SUCCESS;
}

bool validArea(const cv::RotatedRect & rect) {
    if ( recognizeArea.contains(rect.center) ) {
        return true;
    } else {
        return false;
    }
}
文字定位.PNG

=========================================================
validArea方法应该结合具体实例来写【拿尺子量比例】,比如我现在要处理的卡片信息是这种情况的


卡片参数.jpg

那我就应该结合这些参数来判断【先假设一个合理的参数变化范围,然后解个不等式即可】

对于这个校园卡来说
文字定位2.PNG

要识别的部分是圈起来的部分【在证件规格一致、用户按要求拍摄的情况下】

经过筛选之后应该将其排序

    // Sort the text areas by their position
    struct sortByY {
        bool operator () (const RotatedRect & a, const RotatedRect & b) {
            return a.center.y < b.center.y;
        }
    };
    std::sort(textAreaRaw.begin(), textAreaRaw.end(), sortByY());
  1. 切割
void normalizeSingleArea(const cv::Mat & src, cv::RotatedRect & rawArea, cv::Mat & result) {
    float r, angle;
    angle = rawArea.angle;
    r = ( float ) rawArea.size.width / ( float ) ( float ) rawArea.size.height;

    if ( r < 1 ) {
        angle = 90 + angle;
    }

    cv::Mat rotmat = cv::getRotationMatrix2D(rawArea.center, angle, 1);
    cv::Mat img_rotated;
    warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);

    //Crop the image
    cv::Size rect_size = rawArea.size;

    if ( r<1 )
        std::swap(rect_size.width, rect_size.height);

    cv::getRectSubPix(img_rotated, rect_size, rawArea.center, result);
}
  1. 识别
    利用训练好的库对子图进行识别即可
    std::vector<Mat> segments(INFO_COUNT);
    std::string result;
    for ( int i = 0; i < INFO_COUNT; ++i ) {
        normalizeSingleArea(grayMat, textAreaRaw[ i ], segments[ i ]);
        segmentBinary(segments[ i ]);
        tess.SetImage(( uchar* ) segments[ i ].data, segments[ i ].cols, segments[ i ].rows, 1, segments[ i ].cols);
        char* resultUTF8 = tess.GetUTF8Text();
        result.append(resultUTF8);
        result.append("&&&");
        delete[] resultUTF8;
    }
    return UTF8ToANSI(result.c_str());

char* UTF8ToANSI(const char * uft8) {
    wchar_t* temp = Utf_8ToUnicode(uft8);       //unicode sequence
    char* result = UnicodeToAnsi(temp);
    delete[] temp;
    return result;
}

wchar_t * Utf_8ToUnicode(const char * szU8) {
    //UTF8 to Unicode
    //由于中文直接复制过来会成乱码,编译器有时会报错,故采用16进制形式

    //预转换,得到所需空间的大小
    int wcsLen = ::MultiByteToWideChar(CP_UTF8, NULL, szU8, strlen(szU8), NULL, 0);
    //分配空间要给'\0'留个空间,MultiByteToWideChar不会给'\0'空间
    wchar_t* wszString = new wchar_t[ wcsLen + 1 ];
    //转换
    ::MultiByteToWideChar(CP_UTF8, NULL, szU8, strlen(szU8), wszString, wcsLen);
    //最后加上'\0'
    wszString[ wcsLen ] = '\0';
    return wszString;
}

char * UnicodeToAnsi(const wchar_t * szStr) {

    int nLen = WideCharToMultiByte(CP_ACP, 0, szStr, -1, NULL, 0, NULL, NULL);
    if ( nLen == 0 ) {
        return NULL;
    }
    char* pResult = new char[ nLen ];

    WideCharToMultiByte(CP_ACP, 0, szStr, -1, pResult, nLen, NULL, NULL);

    return pResult;
}

UTF8ToANSI(result.c_str())是为了消除乱码【这里略麻烦】
加上“&&&”是方便java里调用split得到三个string
生成一下,生成DLL之后就可以用了

[附java使用例子]

    public interface CppLibrary extends Library {
        //加载链接库
        CppLibrary INSTANTCE = (CppLibrary) Native.loadLibrary("C:\\Coding\\cpp\\CardRecognizerDll\\CardRecognizer\\x64\\Debug\\CardRecognizer.dll", CppLibrary.class);
        //此方法为链接库中的方法
        String recognize(String filePath);
    }

    public static void main(String[] args) {
        System.setProperty("jna.encoding", "GB2312");               //necessary
        String filePath = "C:\\Users\\10068\\Desktop\\9.jpg";       //路径里不要有中文
        String result = CppLibrary.INSTANTCE.recognize(filePath);   //encoding: gb2312 到这一步OK
        System.out.println("GB2312:\n " + result);                                  //OK
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,635评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,628评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,971评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,986评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,006评论 6 394
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,784评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,475评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,364评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,860评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,008评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,152评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,829评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,490评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,035评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,156评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,428评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,127评论 2 356

推荐阅读更多精彩内容