安卓端上传校园卡照片,经识别之后返回识别结果(识别姓名、学号、学院)。
校园卡样例
识别结果
【&&&是为了后续处理而设立的分隔符】
在这里只说明图像处理的过程
最后附上全部代码
- 总流程和框架
二值化处理图片,形态学处理得到文字区域,对满足条件的区域进行分割,将分割出的图进行OCR识别出字符串,最后JAVA利用JNA调用dll [ vs里创建的项目类型要选桌面-dll ]
使用到的框架 openCV, tesseract, JNA
头文件
#include //Include file for every supported OpenCV function
#include
#include
#include
#ifndef CARD_RECOGNIZER
#define CARD_RECOGNIZER
cv::Mat mat, grayMat;
std::vector 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 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(i, j) = c * pow(mat.at(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);
}
- 文字定位
大致分五步
【反色】
【腐蚀】去掉证件上可能存在的纹路图案信息
【膨胀】获得文字区域
【获取轮廓】
【轮廓筛选】
这一步的效果是这样的
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 > contours;
findContours(inversed, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //CV_RETR_EXTERNAL:Retrieves exterior contours only
// Erase bad contours
std::vector>::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;
}
}
=========================================================
validArea方法应该结合具体实例来写【拿尺子量比例】,比如我现在要处理的卡片信息是这种情况的
那我就应该结合这些参数来判断【先假设一个合理的参数变化范围,然后解个不等式即可】
要识别的部分是圈起来的部分【在证件规格一致、用户按要求拍摄的情况下】
经过筛选之后应该将其排序
// 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());
- 切割
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);
}
- 识别
利用训练好的库对子图进行识别即可
std::vector 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
}