参考网址:
之前写过关于车牌识别的项目,银行卡识别和车牌识别类似,也是先从待检测图片中找到银行卡号码的区域,再将号码提取出来。
银行卡识别的难点在于:
1.银行卡种类繁多,不能使用一类固定的算法识别所有的银行卡。
2.银行卡固定和银行卡号码区域的固定比较复杂,因为主要应用于手机拍摄识别,所以像素光线都会产生很大影响。
针对手机拍摄有两套方案:
1.拍摄时固定好银行卡的位置,这样我们就不需要先进行银行卡区域的提取,直接提取银行卡号码区域。
2.对于普通拍摄照片,先进行银行卡区域提取,再提取银行卡号码区域。
版本更新:
CardOCR1.0
——银行卡轮廓和数字轮廓均采用canny边缘检测算法。
CardOCR1.1
——银行卡轮廓定位利用sobel算法替代canny算法。获取数字区域过程中采用增强对比度再利用特定阈值获取数字区域替代了之前canny算法的边缘检测。识别效果较之前有比较大的提升,鲁棒性也更好了。下一阶段是关于银行卡轮廓的偏斜扭正,和利用人工神经网络识别数字。
这里我们先采用第二种方案,如果后续识别效果不理想我们再采用第一种。
大概流程:
1.提取银行卡轮廓
2.偏斜扭正
3.获取数字区域
4.数字识别
我这里刚开始采用农业银行的银行卡做为研究,后期再推及其他银行卡。
原图:(来自百度)
(1)提取银行卡轮廓
高斯模糊-》灰度化-》Canny边缘检测-》二值化-》找轮廓-》轮廓判断
高斯模糊:去除小的噪声影响,平滑图像。
GaussianBlur(inimg, inimg, Size(GAUSSIANBLUR, GAUSSIANBLUR),0, 0, BORDER_DEFAULT);
View Code
灰度化:后续部分操作仅支持灰度图像
cvtColor(inimg, inimg, CV_BGR2GRAY);
View Code
Canny边缘检测:检测整个银行卡的边缘
Canny(inimg, inimg, CARDCANNY_1, CARDCANNY_2);
View Code
当然像这样背景较简单的图片比较容易判断。
二值化:将图像转化为黑白图像,便于轮廓提取
threshold(inimg, inimg, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
View Code
找轮廓:画出检测的轮廓
vector<vector<Point> > contours;
cv::findContours(inimg, contours, CV_RETR_EXTERNAL, CV_CHAIN_APPROX_NONE); //找轮廓
View Code
轮廓判断:筛选出满足条件的轮廓
vector<Rect> rect;
for (int i = 0; i < contours.size(); ++i)
{
//cout << contours[i].size() << endl;
if (contours[i].size() > CARDCONTOURS)//将比较小的轮廓剔除掉
{
rect.push_back( boundingRect(contours[i]) );
}
}
//cout << rect.size() << endl;
for (int j = 0; j < rect.size(); j++)
{
float r = (float)rect[j].width / (float)rect[j].height;
if (r < 1) {
r = (float)rect[j].height / (float)rect[j].width;
}
//cout << r << ",,," << rect[j].width << ",,," << rect[j].height << endl;
if (r <= CARDRATIO_MAX && r >= CARDRATIO_MIN){
original(rect[j]).copyTo(outimg); // copy the region rect1 from the image to roi1
//imshow("re", outimg);
}
}
View Code
这个过程我们可能获取到多个满足条件的银行卡区域,这里我仅提取获取的第一张图片。如果想要更精准的话,需要我们像车牌识别过程中一样用机器学习进行分类。
1.1版本中采用xy轴的Sobel算法进行轮廓提取,由于我们仅需要提取出银行卡区域,不需要十分精确的边缘,我们采用Sobel算法更满足我们的要求。
canny边缘检测采用双阈值值法,高阈值用来检测图像中重要的、显著的线条、轮廓等,而低阈值用来保证不丢失细节部分,低阈值检测出来的边缘更丰富,但是很多边缘并不是我们关心的。最后采用一种查找算法,将低阈值中与高阈值的边缘有重叠的线条保留,其他的线条都删除。canny可以获取很精确的边缘。
(2)偏斜扭正
此部分待后续开发
(3)获取数字区域
提取银行卡号码区域-》消除多余像素-》画轮廓-》提取轮廓
提取银行卡号码区域:对获取的银行卡区域我们进行号码区域提取时,由于银行卡号码位于整个银行卡的区域的固定的,我们可以直接按固定区域提取银行卡号码所在的位置。
Rect rect(CHARRECT_X, CHARRECT_Y, CHARRECT_WIDTH, CHARRECT_HEIGHT);
original(rect).copyTo(inimg);
View Code
消除多余像素:灰度化-》Canny检测-》消除多余行和列(按非零像素的数量进行判断)
cvtColor(inimg, inimg, CV_BGR2GRAY);
//边缘检测
Canny(inimg, inimg, CHARCANNY_1, CHARCANNY_2);
threshold(inimg, inimg, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
imshow(" threshold",inimg);
//inimg.copyTo(outimg);
resize(inimg, outimg, Size(CHARBIGSIZE_WIDTH, CHARBIGSIZE_HEIGHT), 0, 0, INTER_LINEAR);
for (int row = 0; row < inimg.rows; row++)
{
//消除多余行
Rect rectNum(0, row, inimg.cols, 1);
Mat temp;
inimg(rectNum).copyTo(temp);
int n = countNonZero(temp);
//cout << "Zerorow" << n << ",,," << inimg.rows << endl;
if (n < COUNTNONZERO_ROW)
{
inimg.row(row).setTo(Scalar(0));
}
}
for (int col = 0; col < inimg.cols; col++)
{
//消除多余列
Rect rectNum(col, 0, 1, inimg.rows);
Mat temp;
inimg(rectNum).copyTo(temp);
int n = countNonZero(temp);
//cout << "Zerocol: " << n << endl;
if (n < COUNTNONZERO_COL)
{
inimg.col(col).setTo(Scalar(0));
}
}
View Code
画轮廓:将银行卡号码区域按一定比例放大,这样可以保证完整的号码被选取出来。但是放大之后也会有弊端,就是获取的区域但是多个数字连在一起的区域,需要我们进一步进行判断。
cvtColor(inimg, inimg, CV_BGR2GRAY);
//边缘检测
Canny(inimg, inimg, CHARCANNY_1, CHARCANNY_2);
threshold(inimg, inimg, 0, 255, CV_THRESH_OTSU + CV_THRESH_BINARY);
imshow(" threshold",inimg);
//inimg.copyTo(outimg);
resize(inimg, outimg, Size(CHARBIGSIZE_WIDTH, CHARBIGSIZE_HEIGHT), 0, 0, INTER_LINEAR);
for (int row = 0; row < inimg.rows; row++)
{
//消除多余行
Rect rectNum(0, row, inimg.cols, 1);
Mat temp;
inimg(rectNum).copyTo(temp);
int n = countNonZero(temp);
//cout << "Zerorow" << n << ",,," << inimg.rows << endl;
if (n < COUNTNONZERO_ROW)
{
inimg.row(row).setTo(Scalar(0));
}
}
for (int col = 0; col < inimg.cols; col++)
{
//消除多余列
Rect rectNum(col, 0, 1, inimg.rows);
Mat temp;
inimg(rectNum).copyTo(temp);
int n = countNonZero(temp);
//cout << "Zerocol: " << n << endl;
if (n < COUNTNONZERO_COL)
{
inimg.col(col).setTo(Scalar(0));
}
}
View Code
提取轮廓:上一步说到多数字连接的问题,这里我们进行多数字分割处理。
for (int j = 0; j < charRect.size(); j++)
{
if (charRect[j].height > ONECHARRECT_HEIGTH)
{
cout << charRect[j].height << ",,," << j << endl;
if (charRect[j].width <= ONECHARRECT_WIDTH) {
rectangle(outTemp, charRect[j], Scalar(100), 1);
Mat charTempImg;
outimg(charRect[j]).copyTo(charTempImg);
//cout << "width" << charRect[j] << endl;
stringstream ss(stringstream::in | stringstream::out);
ss << "source/char/char" << "_" << j << ".jpg";
imwrite(ss.str(), charTempImg);
}else {
int n = charRect[j].width / ONECHARRECT_WIDTH_NUM;
for (int k = 0; k < n; k++) {
float nt = charRect[j].width / n;
Rect recttemp(charRect[j].x + n*nt - (k+1)*nt, charRect[j].y, nt, charRect[j].height);
Mat charTempImg;
outimg(recttemp).copyTo(charTempImg);
//cout << "width" << recttemp << endl;
stringstream ss(stringstream::in | stringstream::out);
ss << "source/char/char" << "_" << j << "_" << k << ".jpg";
imwrite(ss.str(), charTempImg);
}
}
}
}
View Code
1.1版本
灰度化-》直方图增强对比度-》特定阈值二值化
去掉了canny边缘检测。
增强对比度:
vector< Mat > splitBGR(inimg.channels());
//分割通道,存储到splitBGR中
split(inimg, splitBGR);
//对各个通道分别进行直方图均衡化
for (int i = 0; i<inimg.channels(); i++)
equalizeHist(splitBGR[i], splitBGR[i]);
//合并通道
merge(splitBGR, inimg);
//imshow("imageAdjust", inimg);
View Code
threshold(inimg, inimg, 25, 255, THRESH_BINARY_INV);
View Code
(4)数字识别
这部分拟采用深度神经网络实现,现阶段还未实现。待后续更新。