实战1-银行卡号识别
项目来源:opencv入门
项目目的:识别传入的银行卡照片中的卡号
难点:银行卡上会有一些干扰项,如何排除这些干扰项,并且打印正确的号码是一个问题
最终效果如上图
实现这样的功能需要以下几个步骤:
- 首先必须有与银行卡中卡号数字基本一样的数字模板,将模板中的数字提取出来并存储起来(0-9)
- 将需要检测的银行卡图片中的数字提取出来
- 将银行卡的数字与模板数字一一对比,最终找到一个匹配度最高的数字,并把数字标注上
整个思路很简单,但是难点就在于如何将图片处理得更加容易让计算机识别数字,所以整个项目要围绕着图片得的处理来做
第一步-提取数字模板
这是事先准备好的数字模板
接下来要将图片中的数字都找到,也就是找到各个数字在整个图片上的像素点坐标(轮廓)
首先得到图片的灰度图,再进行二值化处理(这一切都是为了让图片中的数字更易于识别)
# 灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值图像
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]#超过阈值部分取maxval ( 最大值 ),否则取0
然后会得到这样的图像
好了,现在图片已经很清晰了,不需要再进行其它的处理了,直接将其提取
那怎么提取呢?
可以通过cv2.findContours()找到数字的轮廓
函数 cv2.findContours() 有三个参数,第一个是输入图像,第二个是 轮廓检索模式,第三个是轮廓近似方法。返回值有三个,第一个是图像,第二个 是轮廓,第三个是(轮廓的)层析结构。轮廓(第二个返回值)是一个 Python 列表,其中存储这图像中的所有轮廓。每一个轮廓都是一个 Numpy 数组,包 含对象边界点(x,y)的坐标。
注意新版本中这个api的返回值有变化
返回两个参数contours和 hierarchy,contours就是每个数字的轮廓数组,包含边界点的坐标
其中cv2.RETR_EXTERNAL是获取外轮廓
contours, hierarchy = cv2.findContours(ref.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
接下来可以将轮廓画出来看看
会用到cv2.drawContours()函数
函数 cv2.drawContours() 可以被用来绘制轮廓。它可以根据你提供 的边界点绘制任何形状。它的第一个参数是原始图像,第二个参数是轮廓,一 个 Python 列表。第三个参数是轮廓的索引(在绘制独立轮廓是很有用,当设置 -1时绘制所有轮廓)。接下来的参数是轮廓的颜色和厚度等。
cv2.drawContours(img,contours,-1, (0, 0, 255), 3)
看下效果
好,现在轮廓都找到了,并且我们也有了轮廓的坐标,这个时候我们应该将每个数字的像素点位置都存起来(并不是将图片分割!,整个图片仍然没有任何变化)
好,现在有一个要注意的点,那就是我们在上面得到的contours数组并不是按图片中各个数字从左到右排列的,也就是说数组中第一个坐标可能是图片中8的坐标,那这个时候我们就必须对数组进行排序,排序顺序就是从左到右存
那排序怎么实现呢,其实就是根据x坐标从小到大排序就行了
排完序之后,contours中0下标存的就是数字0的模板,这里很好的利用了数组下标的优点
好的,排序完之后,我们就可以来存这个数字的模板了
思路是遍历contours数组,得到每个模板的坐标以及宽高,利用x+w就能得到图片的x轴范围,y+h就能得到y轴的范围,把他们存起来就得到一个数字的模板了
digits = {}
#遍历每一个轮廓
for (i,c) in enumerate(contours):
#计算外接矩形并resize合适的大小
(x, y, w, h) = cv2.boundingRect(c)
# cv2.rectangle(img,(x,y),(x + w, y + h),(0, 0, 255), 2)
roi = ref[y:y + h, x:x + w]
# 第二个参数是输出图像的宽高
roi = cv2.resize(roi, (57, 88))
# 每一个数字对应每一个模板
digits[i] = roi
至此,我们项目的第一步就完成了
接下来就是将要检测的图像中的数字提取出来,其实整个提取思路都是一样的,但是银行卡的图像比我们的模板往往更加复杂,所以我们要对图片增加一些处理的步骤
跟着上面的来说,我们对复杂图片的处理需要引入卷积核,这里我们定义两个卷积核
# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
一个是9x3的矩阵,一个是5x5
下面对图像进行处理,老规矩,取灰度图
然后进行礼帽处理,目的是为了突出更明亮的区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
接下来再用 Sobel核子对图片进行卷积,目的的为了得到图像梯度,也就是边缘检测
我们现在要做的是把可能为数字的区域都找出来
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")
上图看上去更加模糊了,但是数字和非数字区域的明亮度变了
好的,接下来可以通过闭操作(先膨胀,再腐蚀),将数字连起来(是为了最后找到数字区域,因为卡号是4个数字连在一起的,我们把4个数字的区域找出来)
变成这样
再来一次阈值操作
thresh = cv2.threshold(gradX, 0, 255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
变成这样
矩形区域好像白色没有填满,再来一次闭操作
ok了,现在疑似数字的区域都很明显了吧,那下一步就是将这个区域进行排除,找到真正为银行卡号的区域,其他的区域就不要了
那怎么做呢?我们先把他们的轮廓都找出来,然后判断这些轮廓的宽度,符合银行卡号区域宽的的留下,不符合的去掉就可以了
# 计算轮廓
contours_, hierarchy_ = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cnts = contours_
cur_img = image.copy()
cv2.drawContours(cur_img, cnts, -1, (0, 0, 255), 3)
cv_show('img', cur_img)
locs = []
# 遍历轮廓
for (i, c) in enumerate(cnts):
# 计算矩形
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
# 选择合适的区域,根据实际任务来,这里的基本都是四个数字一组
if ar > 2.5 and ar < 4.0:
if (w > 40 and w < 55) and (h > 10 and h < 20):
# 符合的留下来
locs.append((x, y, w, h))
得到卡号轮廓后,同样对其从左至右排序
好了,那接下来干嘛呢,我们刚刚得到的是四个数字组成的区域的轮廓,这个时候我们应该遍历这些区域,得到里面的四个数字的轮廓
同样也是个遍历操作
for (i, (gX, gY, gW, gH)) in enumerate(locs):
groupOutput = []
# 根据坐标提取每一个组
group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
cv_show('group', group)
会得到四个这样的组
然后就获取这个组的轮廓,就像第一步骤一样,将数字提取出来就可以了
#计算每一组的轮廓
digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
# 从左到右排序
digitCnts = myutils.sort_contours(digitCnts, method="left-to-right")[0]
好的,接下来就是最重要的第三部操作了,将模板与上面得到的数字匹配,找到匹配度最高的那个模板数字就是我们要找的数字了
#计算每一组的每一个数值
for c in digitCnts:
# 找到当前数值的轮廓,resize成合适的的大小
(x, y, w, h) = cv2.boundingRect(c)
roi = group[y:y + h, x:x + w]
roi = cv2.resize(roi, (57, 88))
# cv_show('roi', roi)
# 计算匹配得分
scores = []
for (digit, digitROI) in digits.items():
# 模板匹配
result = cv2.matchTemplate(roi, digitROI,cv2.TM_CCOEFF)
# print('result',result)
# 获取匹配度最高的数值
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
print("scores",scores)
# 得到最合适的数字
groupOutput.append(str(np.argmax(scores)))
print('groupOutput',groupOutput)
完成上述步骤之后,我们的groupOutput就存放了我们识别出来的银行卡号了,我们只需要在图片上将卡号绘制出来就可以了
# 画出来
cv2.rectangle(image, (gX - 5, gY - 5),(gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(image, "".join(groupOutput), (gX, gY - 15),cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 得到结果
output.extend(groupOutput)
最终效果如下
好了,以上就是此小项目的实现过程
总结:这是我学cv的第一个小实战项目,确实感觉蛮有意思的,学之前觉得这个东西很神奇,学习之后会发现其实一切都是按照逻辑一步步来的,没有那么"高大上",继续努力吧