信用卡数字识别-流程预览
使用模板匹配做数字检测
1、图像转灰度图
2、对模板进行轮廓检测,得到的多个轮廓(内轮廓和外轮廓),计算外轮廓的外接矩形
3、对信用可图像做同样的操作后,由于有其他字体或数字的干扰,需要做形态学操作,礼帽+闭操作可以突出明亮区域(为了过滤背景)。x方向的Sobel算子,实验表明,加y的效果的并不好,x方向取绝对值 -> 归一化。通过闭操作(先膨胀,再腐蚀)将数字连在一起. 将本是4个数字的4个框膨胀成1个框,就腐蚀不掉了。。。。等操作(得到字体区域)。
4、车牌识别,银行卡数字识别,数字 长宽比例固定。根据长宽比过滤掉不相关的轮廓。信用卡识别首先要得到4组数字轮廓,一组由4位数组成,后续再分割成单字符
5、再进行匹配,得到结果。
一、基础配置
# 导入工具包
from imutils import contours
import numpy as np
import argparse
import cv2
import myutils
# 设置参数
ap = argparse.ArgumentParser()
ap.add_argument("-i","--image",default='./images/credit_card_01.png',help="path to input image")
ap.add_argument("-t","--template",default='./ocr_a_reference.png',help="path to template OCR image")
args = vars(ap.parse_args())
# 指定信用卡类型
FIRST_NUMBER = {"3": "American Express","4": "Visa","5": "MasterCard","6": "Discover Card"}
# 绘图展示
def cv_show(name,img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
二、模板处理
模板处理流程: 轮廓检测, 外接矩形, 抠出模板, 让模板对应每个数值
字典digits = {} # 存模板的单个数字
# 读取一个模板图像
img = cv2.imread(args["template"])
cv_show('template',img)
# 灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('template_gray',ref)
# 二值图像
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('template_bi',ref)
# 1.计算轮廓
# cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点坐标
# 返回的list中每个元素都是图像中的一个轮廓
refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE) # cv版本大于3.8的,只有两个返回值
cv2.drawContours(img,refCnts,-1,(0,0,255),3) # 轮廓在二值图上得到, 画要画在原图上
cv_show('template_Contours',img)
print (np.array(refCnts).shape)
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] #排序,从左到右,从上到下
digits = {} # 存模板的单个数字
# 2.遍历每一个轮廓,外接矩形
for (i, c) in enumerate(refCnts): # c是每个轮廓的终点坐标
# 计算外接矩形并且resize成合适大小
(x, y, w, h) = cv2.boundingRect(c)
# 3.抠出模板
roi = ref[y:y + h, x:x + w] # 每个roi对应一个数字
roi = cv2.resize(roi, (57, 88)) # 太小,调大点
# 4.每一个数字对应每一个模板
digits[i] = roi
注:在自己动手实践过程中,发现有以下几个点需要注意(顺一遍没有大问题的同学, 可先跳过此处)
- Line 8:因findCoutours 检测黑底白字的物体,所以要选择反转的二值化THRESH_BINARY_INV
- Line 14:我们需要外轮廓,画外接矩形,所以用cv2.RETR_EXTERNAL
- Line 23:enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列
seq = [‘one’, ‘two’, ‘three’]
for i, element in enumerate(seq):
… print i, element
…
0 one
1 two
2 three
- Line 27:侧重于抠出单个数值的模板,而不是画个rectangle
- Line 20,31:digits设置为字典,第i个健对应的第i个模板数值roi
- Line 19:对轮廓进行排序,并且返回两个值,只需要轮廓[0]
refCnts = myutils.sort_contours(refCnts, method=“left-to-right”)[0]
排序前
排序后
也可以不排序,在后面数值与模板数值匹配时需要做相应改动,感兴趣的同学自行研究
对轮廓进行排序的代码如下:
def sort_contours(cnts, method="left-to-right"):
reverse = False
i = 0
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
if method == "top-to-bottom" or method == "bottom-to-top":
i = 1
# 计算外接矩形 boundingBoxes是一个元组
boundingBoxes = [cv2.boundingRect(c) for c in cnts] #用一个最小的矩形,把找到的形状包起来x,y,h,w
# sorted排序
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cnts, boundingBoxes # 轮廓和boundingBoxess
三、输入图像处理
# 1.初始化卷积核,根据实际任务指定大小,不一定非要3x3
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 2.读取输入图像,预处理
image = cv2.imread(args["image"])
cv_show('Input_img',image)
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('Input_gray',gray)
# 3.礼帽操作,突出更明亮的区域
# 形态学操作,礼帽+闭操作可以突出明亮区域,但并不是非得礼帽+闭操作
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('Input_tophat',tophat)
# 4.x方向的Sobel算子,实验表明,加y的效果的并不好
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0,ksize=-1) #ksize=-1相当于用3*3的
# x方向取绝对值 -> 归一化
gradX = np.absolute(gradX) # absolute: 计算绝对值
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")
print (np.array(gradX).shape)
cv_show('Input_Sobel_gradX',gradX)
# 5.通过闭操作(先膨胀,再腐蚀)将数字连在一起. 将本是4个数字的4个框膨胀成1个框,就腐蚀不掉了
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('Input_CLOSE_gradX',gradX)
# 6.THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('Input_thresh',thresh)
# 7.再来一个闭操作
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) # 填补空洞
cv_show('Input_thresh_CLOSE',thresh)
# 8.计算轮廓
threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE)
cnts = threshCnts
cur_img = image.copy()
cv2.drawContours(cur_img,cnts,-1,(0,0,255),3)
cv_show('Input_Contours',cur_img)
注:
- Line 20-23:绝对值+归一化
归一化 x’ = (x-min)/(max-min) - Line 36:第二个闭操作换个9x9的核
onekernel = np.ones((9,9), np.uint8)
thresh = cv2.morphologyEx(thresh,cv2.MORPH_CLOSE,onekernel)
四、遍历轮廓
# 1.遍历轮廓
locs = [] # 存符合条件的轮廓
for i,c in enumerate(threshCnts):
# 计算矩形
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))
# 将符合的轮廓从左到右排序
locs = sorted(locs,key=lambda x:x[0])
注:
- Line 6-11:根据w和h的比例,选出包括4个数字的区域,视实际情况判定
- Line 14:
key=lambda 元素: 元素[字段索引]
C = (sorted(C, key=lambda x: x[0]))
x:x[]字母可以随意修改,排序方式按照中括号[]里面的维度,[0]按照第一维,[1]按照第二维。
五、遍历数字
# 2.遍历每一个轮廓中的数字
output = [] # 存正确的数字
for (i,(gx,gy,gw,gh)) in enumerate(locs): # 遍历每一组大轮廓(包含4个数字)
# initialize the list of group digits
groupOutput = []
# 根据坐标提取每一个组(4个值)
group = gray[gy-5:gy+gh+5, gx-5:gx+gw+5] # 往外扩一点
cv_show('group_'+str(i),group)
# 2.1 预处理
group = cv2.threshold(group,0,255,cv2.THRESH_BINARY|cv2.THRESH_OTSU)[1] # 二值化的group
# cv_show('group_'+str(i),group)
# 计算每一组的轮廓 这样就分成4个小轮廓了
digitCnts = cv2.findContours(group.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[0]
# 排序
digitCnts = myutils.sort_contours(digitCnts,method="left-to-right")[0]
# 2.2 计算并匹配每一组中的每一个数值
for c in digitCnts: # c表示每个小轮廓的终点坐标
z = 0
# 找到当前数值的轮廓,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_"+str(z),roi)
# 计算匹配得分: 0得分多少,1得分多少...
scores = [] # 单次循环中,scores存的是一个数值 匹配 10个模板数值的最大得分
# 在模板中计算每一个得分
# digits的digit正好是数值0,1,...,9;digitROI是每个数值的特征表示
for (digit,digitROI) in digits.items():
# 进行模板匹配, res是结果矩阵
res = cv2.matchTemplate(roi,digitROI,cv2.TM_CCOEFF) # 此时roi是X digitROI是0 依次是1,2.. 匹配10次,看模板最高得分多少
Max_score = cv2.minMaxLoc(res)[1] # 返回4个,取第二个最大值Maxscore
scores.append(Max_score) # 10个最大值
print("scores:",scores)
# 得到最合适的数字
groupOutput.append(str(np.argmax(scores))) # 返回的是输入列表中最大值的位置
z = z+1
# 2.3 画出来
cv2.rectangle(image,(gx-5,gy-5),(gx+gw+5,gy+gh+5),(0,0,255),1) # 左上角,右下角
# 2.4 putText参数:图片,添加的文字,左上角坐标,字体,字体大小,颜色,字体粗细
cv2.putText(image,"".join(groupOutput),(gx,gy-15),
cv2.FONT_HERSHEY_SIMPLEX,0.65,(0,0,255),2)
# 2.5 得到结果
output.extend(groupOutput)
print("groupOutput:",groupOutput)
注:
- 二的digits[i] 是 模板中的单个数值(即五中的digitROI)
- 五的digitCnts -> group -> roi 是 输入图中的单个数值
- 输入图中的单个数值 和 模板中的单个数值 需进行相同的resize,否则无法进行匹配
roi = cv2.resize(roi, (57, 88)) - Line 11:对group的二值化预处理时(即 将一组轮廓group(4数字) 分为 4个小轮廓digitCnts前),必须加上cv2.THRESH_OTSU,否则检测不出4个小轮廓
- Line 44:groupOutput中存的是每一组(4个)数值 如4000,因此在putText时,数值中间就不用空格或其他内容了,双引号""中间没有任何东西。
putText参数:图片,添加的文字,左上角坐标,字体,字体大小,颜色,字体粗细
putText的第3个参数是左上角的坐标,因此在打印下一组groupOutput,它会重新以这组的(gx,gy-15)为新左上角坐标
六、识别结果
# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Output_image", image)
cv2.waitKey(0)
输出:
(10,)
(189, 300)
groupOutput: ['4', '0', '0', '0']
groupOutput: ['1', '2', '3', '4']
groupOutput: ['5', '6', '7', '8']
groupOutput: ['9', '0', '1', '0']
Credit Card Type: Visa
Credit Card #: 4000123456789010
附:测试另外几张信用卡数字识别的效果