通过前两篇我们已经对Opencv有所了解了,接下来就要真正的来处理我们的图像,然后把卡号给提取出来。首先我们先简单分析以下银行卡然后把处理流程列出来:
由上图我们很容易知道既然我们要找到卡号,银行卡的外边轮廓,然后根据比例找到卡号的位置,处理流程:
- 把采集到的图片根据银行卡边缘进行剪切,得到银行卡的区域
- 根据比例把卡号区域剪切出来,得到卡号的区域(具体怎么截取自己可以想不同的算法识别卡号的位置)
- 处理得到的卡号区域(降噪/二值化/膨胀/腐蚀等等)
- 分割每一个卡号(用于后期识别和训练模型使用)
基本流程就是这些,接下来就让我们从查找银行卡边缘开始:
1.对银行卡边缘进行裁剪
图像处理:
// 首先降噪
Mat blur;
GaussianBlur(mat, blur, Size(5, 5), BORDER_DEFAULT, BORDER_DEFAULT);
// 梯度增强 , x 轴和 y 轴
Mat grad_x, grad_y;
Scharr(blur, grad_x, CV_32F, 1, 0);
Scharr(blur, grad_y, CV_32F, 0, 1);
Mat grad_abs_x, grad_abs_y;
convertScaleAbs(grad_x, grad_abs_x);
convertScaleAbs(grad_y, grad_abs_y);
Mat grad;
addWeighted(grad_abs_x, 0.5, grad_abs_y, 0.5, 0, grad);
// 把图片转成灰度图,减少图片的信息
Mat gray;
cvtColor(grad, gray, COLOR_BGRA2GRAY);
// 二值化,待进行轮廓查找
Mat binary;
threshold(gray, binary, 40, 255, THRESH_BINARY);
效果图:
通过梯度增强把银行卡的边缘更加突出,为我们查找更准确的边缘轮廓矩形。这里二值化主要是去除一些背影信息,减少不必要的轮廓,方便查找。图片处理好之后接下来就是查找边缘轮廓了。
轮廓查找:
轮廓查找我们要筛选出符合要求的论然后返回
// 轮廓查找
vector<vector<Point> > contours;
//查找轮廓
findContours(binary, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
for (int i = 0; i < contours.size(); ++i) {
Rect rect = boundingRect(contours[i]);
//绘制轮廓
drawContours(mat,contours,i,Scalar(0, 0, 255),1);
// 是不是符合规则
if (rect.width > mat.cols / 2 && rect.width != mat.cols && rect.height > mat.rows / 2) {
card_rect = rect;
break;
}
//防止轮廓都是不合尺寸的
if (i == contours.size() - 1) {
card_rect.x = 0;
card_rect.y = 0;
card_rect.width = binary.cols;
card_rect.height = binary.rows;
}
}
剪切后的效果图红色是轮廓:
可以看到边缘已经根据外接矩形给剪切了。然后就是寻找卡号区域
2.剪切卡号区域
我这里是根据比例来剪切卡号区域的,你可以想其他办法去实现,那样剪切的区域跟准确,这里可以做持续优化,方法方式不限,只要能找到合适的卡号区域就行。
首先对剪切后的银行卡图片进行缩放,因为边框位置的不同剪切后银行卡区域可能图片大小不一致,所以先缩放到一定大小之后,按照比例剪切卡号区域
//缩放
int srcRows = 1388;
int srcCols = 2201;
Mat srcScale(srcRows, srcCols, srcImg.type());
resize(srcImg, srcScale, srcScale.size(), 0, 0, INTER_LINEAR);
//剪切矩形
Rect cR;
cR.x = srcScale.cols / 20;
cR.y = srcScale.rows / 2;
cR.width = srcScale.cols * 18 / 20;
cR.height = srcScale.rows / 6;
//剪切
Mat rectMat(srcScale, cR);
可能不同的银行卡,卡号位置不一样,其实银行卡识别为了能达到更高的识别效率,需要写多套不同的图像处理代码,单单写一套代码识别率还是有限的,比较我们拍摄的角度/拍摄的光照/银行卡的样式多种多样,银行卡卡号有印刷的有突出的,银行卡背景又有很多不一样,所以在处理中其实还是要多写几套代码去适配。而对对于身份证的识别就非常简单了,因为身份证都是国家统一的。
3.处理卡号区域图像
得到卡号区域之后我们需要做的就是把卡号和背景分开,避免背景对识别产生影响,导致识别失败。所以我们要做一些图像处理:
// 首先降噪 噪点可以选大一点
Mat blur;
GaussianBlur(rectMat, blur, Size(9, 9), BORDER_DEFAULT, BORDER_DEFAULT);
//转灰度图
Mat g;
cvtColor(blur, g, COLOR_BGRA2GRAY); //转为灰度图
//获取自定义核
//第一个参数MORPH_RECT表示矩形的卷积核,当然还可以选择椭圆形的、交叉型的
Mat element = getStructuringElement(MORPH_RECT, Size(5, 5));
Mat erodeOut;
//腐蚀操作
erode(g, erodeOut, element);
//二值化 THRESH_OTSU自动阀值
Mat th;
threshold(erodeOut, th, 39, 255, THRESH_OTSU);
//膨胀
Mat dilate_element = getStructuringElement(MORPH_RECT, Size(8, 8));
Mat dilate_erodeOut;
dilate(th, dilate_erodeOut, dilate_element);
从图像可以看出,还有些是干扰区域(注意这里可能你处理后的数字直接有粘练的问题,这个在这里就不说了,可以自己去研究),然后就是通过获取这个灰度图的所有矩形。
vector<vector<Point> > contours;
vector<Vec4i> hierarcy;
findContours(dilate_erodeOut, contours, hierarcy, RETR_TREE, CHAIN_APPROX_NONE); //查找所有轮廓
vector<Rect> boundRect(contours.size()); //定义外接矩形集合
int x0 = 0, y0 = 0, w0 = 0, h0 = 0;
int dilate_area = dilate_erodeOut.cols * dilate_erodeOut.rows;
for (int i = 0; i < contours.size(); i++) {
boundRect[i] = boundingRect(contours[i]); //查找每个轮廓的外接矩形
x0 = boundRect[i].x;
y0 = boundRect[i].y;
w0 = boundRect[i].width;
h0 = boundRect[i].height;
//绘制第i个外接矩形
rectangle(dilate_erodeOut, Point(x0, y0), Point(x0 + w0, y0 + h0), Scalar(0, 255, 0), 2, 8);
int area = boundRect[i].area();
if (h0 < dilate_erodeOut.rows * 2 / 5) {
drawContours(dilate_erodeOut, contours, i, Scalar(0), 1);
contours.erase(contours.begin() + i);
} else if (area < dilate_area / 200) {
drawContours(dilate_erodeOut, contours, i, Scalar(0), 1);
contours.erase(contours.begin() + i);
}
// TODO : 根据高度和面积 处理腐蚀,伐值化,膨胀 不掉的干扰点。大面积的干扰点。
}
但是我们会发现有很多的干扰矩形,就是除了我们数字区域之外的其他噪点区域,所以我们对得到的全部矩形做了“面积”,“高度”,大小的过滤。当然这里仅仅做了高度,面积的过滤其实还不够,还应该根据x,y坐标的特点进行再次过滤排除等等,这里可以自由发挥,根据自己的算法,可以做个更加优化,识别率更高。
过滤之后:
把我们的区域都找到了,把干扰区域都过滤掉了,生下来的就是我们的数字了。接下来就是把数字提取,分割出来,为识别和训练作数据采集。
4.分割每一个卡号
上面我们已经通过过滤得到了所有的卡号区域,下面就让我们把卡号区域给截取出来,然后保存到本地,为我们下一章节的训练使用:注意我们得到的矩形集合,他并不是有序的,就是如果我们直接按照集合內矩形的顺序截取,卡号是乱的,所有在截取之前需要根据x坐标排序。
//排序
for (int i = 0; i < split_mat.size() - 1; ++i) {
for (int j = 0; j < split_mat.size() - 1 - i; ++j) {
if (split_mat[j].x > split_mat[j + 1].x) {
swapRect = split_mat[j];
split_mat[j] = split_mat[j + 1];
split_mat[j + 1] = swapRect;
}
}
}
//分割
for (int i = 0; i < split_mat.size(); ++i) {
//分割
Mat sp(dilate_erodeOut, split_mat[i]);
//缩放
int nRows = 11;
int nCols = 7;
Mat dst(nRows, nCols, sp.type());
resize(sp, dst, dst.size(), 0, 0, INTER_LINEAR);
Mat bit;
bitwise_not(dst, bit);
//输出------训练数据提取样本时候使用
char name[50];
//坑 保存的时候图片尺寸不要太大(由于刚开始图片保存的是110:70的导致怎么训练都不成功),最好格式是png.
sprintf(name,"/storage/emulated/0/dstImg_%d.png",i);
imwrite(name, bit);
}
需要注意这里对分割的结果进行了缩放,起初我是直接保存的原始尺寸是110:70,但是在训练的时候始终训练失败,通过查找原因,一步一步的排除错误,最终发现图片太大。
下一篇就是拿到我们分割后的数字进行训练,这里你可以多分割几个卡号,多采集一些数据。