项目实战—Credit Card 数字识别续
现在我们需要进行第二阶段的工作。
准备工作(opencv)
cv2.getStructuringElement()
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(11,11))
这个函数的第一个参数表示内核的形状,有三种形状可以选择。
矩形:MORPH_RECT;
交叉形:MORPH_CROSS;
椭圆形:MORPH_ELLIPSE;
第二和第三个参数分别是内核的尺寸以及锚点的位置。一般在调用erode以及dilate函数之前,先定义一个Mat类型的变量来获得
getStructuringElement函数的返回值:
对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心点。element形状唯一依赖锚点位置,其他情况下,锚点只是影响了形态学运算结果的偏移。
cv2.resize()
cv2.resize(InputArray src, OutputArray dst, Size, fx, fy, interpolation)
InputArray src 输入图片
OutputArray dst 输出图片 (可省)
Size 输出图片尺寸 (w, h)宽和高
fx, fy 沿x轴,y轴的缩放系数 interpolation 插入方式
interpolation 选项所用的5种插值方法:
cv2.INTER_NEAREST 最近邻插值
cv2.INTER_LINEAR 双线性插值(默认设置)
cv2.INTER_AREA 使用像素区域关系进行重采样。
cv2.INTER_CUBIC 4x4像素邻域的双三次插值
cv2.INTER_LANCZOS4 8x8像素邻域的Lanczos插值
cv2.findContours() & cv2.drawContours()
binary, contours, hierarchy = cv2.findContours(src_bi, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
NONE输出所有点,SIMPLE只保留他们的终点。
binary是返回的二值图
contours返回一个list,list中每个元素都是图像中的一个轮廓,用numpy中的ndarray表示。
hierarchy结果,这是一个ndarray,其中的元素个数和轮廓个数相同,每个轮廓contours[i]对应4个hierarchy元素hierarchy[i][0] ~hierarchy[i][3],分别表示后一个轮廓、前一个轮廓、父轮廓、内嵌轮廓的索引编号,如果没有对应项,则该值为负数
cv2.drawContours(image, contours, contourIdx, color[, thickness[,
lineType[, hierarchy[, maxLevel[, offset ]]]]])
第一个参数是指明在哪幅图像上绘制轮廓; 第二个参数是轮廓本身,在Python中是一个list。
第三个参数指定绘制轮廓list中的哪条轮廓,如果是-1,则绘制其中的所有轮廓。后面的参数很简单。其中thickness表明轮廓线的宽度,如果是-1(cv2.FILLED),则为填充模式。绘制参数将在以后独立详细介绍。
现在我们即将进行最终的代码实现,具体过程不讲述,代码种有详细注释,一步一步的调试
代码实现
我们将使用实验图片:
模板图片:
来看代码:
ocr_template_match.py
# 导入工具包 from imutils import contours import numpy as np import argparse import cv2 import myutils # 设置参数 # 在pycharm中,Edit Configurations中配置下面参数 ap = argparse.ArgumentParser() ap.add_argument("-i", "--image", required=True, help="path to input image") ap.add_argument("-t", "--template", required=True, help="path to template OCR-A image") ar = ap.parse_args() args = vars(ap.parse_args()) # print(args["image"]) # "第一种方式:" # args = vars(ap.parse_args()) # args为字典{'image': 'images/credit_card_01.png', 'template': 'images/ocr_a_reference.png'} # 调用方式:print(args["image"]) # "第二种方式:" # args = ap.parse_args() # Namespace(image='images/credit_card_01.png', template='images/ocr_a_reference.png') # 调用方式:print(args.image) # 指定****类型 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() # 读取一个模板图像 img = cv2.imread(args["template"]) cv_show('img', img) # 灰度图 ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv_show('ref', ref) # 二值图像 # ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1] # 法1c ret, ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV) # 法2, 10值越小,保留的黑越黑,INV后变成白色 # ref要么是0要么是255 # ret返回的是阈值;ref返回的是二值化后的图像矩阵,是二维矩阵。 cv_show('ref', ref) # 计算轮廓 #cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点坐标 #返回的list中每个元素都是图像中的一个轮廓 # ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # ref_二值图, refCnts是边缘轮廓,返回值形状为(10,),表示10个形状,每个里面是一个列表,列表里是具体每一个轮廓的值。 # cv2.RETR_EXTERNAL获取外轮廓, cv2.RETR_TREE获取所有轮廓 cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3) # -1表示画出所有轮廓 cv_show('img', img) print(np.array(refCnts).shape) refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] # 排序,指定 从左到右 还是 从上到下 # refCnts找到的轮廓不一定就是,从左往右123,现在找到的10个轮廓顺序乱序,需要将轮廓从左往右对应拍成0,1,2..等等 digits = {} # 遍历每一个轮廓 for (i, c) in enumerate(refCnts): # (i, c)加不加括号都是一样的 # 计算外接矩形并且resize成合适大小 (x, y, w, h) = cv2.boundingRect(c) roi = ref[y:y + h, x:x + w] roi = cv2.resize(roi, (57, 88), cv2.INTER_LINEAR) cv_show("roi", roi) # 每一个数字对应每一个模板 digits[i] = roi print(digits) # 初始化卷积核 rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) # (9, 3)是指的矩阵的宽w和高h, 9是因为想让横向的信息多保留? # rectKernel为: # [[1 1 1 1 1 1 1 1 1] # [1 1 1 1 1 1 1 1 1] # [1 1 1 1 1 1 1 1 1]] # 形状(3, 9) sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 读取输入图像,预处理 image = cv2.imread(args["image"]) cv_show('image', image) image = myutils.resize(image, width=300) # resize前(368, 583, 3),resize后(189, 300, 3) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # cv_show('gray', gray) cv_show('gray', gray) # gradX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel) # # 此处的卷积核为(9, 3),全1 # cv_show('gradX', gradX) # 此处做闭操作也行 # 闭操作就是把字连接起来 # 顶帽操作,突出更明亮的区域;为了突出字体,所以核(9,3)和字体部分差不多比例 tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) # 可以对灰度图进行操作 cv_show('tophat', tophat) # gradX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel) # # 此处的卷积核为(9, 3),全1 # cv_show('gradX', gradX) # 此处做闭操作也行 gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1) # ksize=-1相当于用3*3的 cv_show('gradX1', gradX) gradX = np.absolute(gradX) # 这个是(0,3040)之间的浮点数 (minVal, maxVal) = (np.min(gradX), np.max(gradX)) gradX = (255 * ((gradX - minVal) / (maxVal - minVal))) # 这个是(0,255)之间的浮点数 gradX = gradX.astype("uint8") cv_show('gradX', gradX) # 通过闭操作(先膨胀,再腐蚀)将数字连在一起 gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel) # 此处的卷积核为(9, 3),全1 cv_show('gradX', gradX) # THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0 thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY+cv2.THRESH_OTSU)[1] # cv_show('thresh', thresh) cv2.imshow('thresh2', thresh) thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) # 再来一个闭操作 cv_show('thresh', thresh) # 计算轮廓 thresh_, 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('img', cur_img) cv2.imwrite("1.jpg", 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)) # 将符合的轮廓从左到右排序 locs = sorted(locs, key=lambda x: x[0]) output = [] # 遍历每一个轮廓中的数字 for (i, (gX, gY, gW, gH)) in enumerate(locs): # initialize the list of group digits groupOutput = [] # 根据坐标提取每一个组 group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5] cv_show('group', group) # 预处理 group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1] cv_show('group', group) # 计算每一组的轮廓 group_, digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) digitCnts = contours.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) (_, score, _, _) = cv2.minMaxLoc(result) # 由于使用的是相关性,所以是去最大值即可 # min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result) # # 返回的res 中min_loc代表每一个滑动窗口左上角坐标的值,min_val代表损失 # print(score) scores.append(score) # 得到最合适的数字 groupOutput.append(str(np.argmax(scores))) # 画出来 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) # cv2.FONT_HERSHEY_SIMPLEX为字体,常用还有cv2.FONT_HERSHEY_COMPLEX # 得到结果 print(groupOutput) output.extend(groupOutput) print(output) # 打印结果 print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]])) print("Credit Card #: {}".format("".join(output))) cv2.imshow("Image", image) cv2.waitKey(0)
myutils.py
import cv2 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 = [cv2.boundingRect(c) for c in cnts] # 用一个最小的矩形,把找到的形状包起来x,y,h,w # print(cv2.boundingRect(cnts[0])) # (730, 20, 54, 85) print(list(zip(cnts, boundingBoxes))) print(zip(cnts, boundingBoxes)) (cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda b: b[1][i], reverse=reverse)) return cnts, boundingBoxes def resize(image, width=None, height=None, inter=cv2.INTER_AREA): dim = None (h, w) = image.shape[:2] if width is None and height is None: return image if width is None: r = height / float(h) dim = (int(w * r), height) else: r = width / float(w) dim = (width, int(h * r)) resized = cv2.resize(image, dim, interpolation=inter) return resized
最终效果:
可以看到,效果非常不错。
当然了,我们本次所用的数字识别,其本质来讲属于模板匹配,并非真正意义上的数字识别,不过接下来我们将使用更加强大的OCR识别。