数字识别
一、总体方法和流程讲解
将银行卡中的四组数字读取出来并识别为对应的数字。
-
如何将银行卡中的数字识别出来?
利用模板匹配。在模板中准备 0- 9 的 10 个数字,将每一个数字与银行卡中的单个数字利用模板匹配方式进行匹配,找出匹配最佳的选项,即为对应结果。
- 要准备一个和银行卡中所存数字字体完全一致的模板
-
如何将模板中的各个数字分类预处理?
利用轮廓检测。利用轮廓检测中的构建外接形操作,
- 首先对模板进行轮廓检测,得到各个数字的外轮廓与内轮廓
- 基于外轮廓绘制外接矩形
- 在进行模板匹配之前,若模板中的各个数字和银行卡中的各个数字大小不一,则应在匹配之前进行 resize 操作
-
轮廓检测
-
转换为灰度图
-
再将灰度图转换为二值图
-
轮廓检测
-
过滤出轮廓比和数字对不一致的轮廓
-
再对数字对内部进行轮廓检测、模板匹配
-
二、PyCharm配置
在pycharm运行的配置步骤:
- 右键该py文件,找到More run/Debug->Modify Run Configuration
- 在Configuration选项卡里找到Parameters项,
- 输入-i images/credit_card_03.png -t images/ocr_a_reference.png,然后点击apply
- 然后再按ctrl+alt+L(官方的)格式化代码
- 然后在46,105,144行将第一个参数去掉(可能你们的不一定是这几行,但只要找到.findContours就清楚了,最新版才需要做,非最新版请忽略) 然后就可以运行了,运行途中会控制台出一个警告,无伤大雅
三、具体代码
1. 自定义模块
myutils.py:
- sort_contours:自定义轮廓排序函数,将模板中的每个数字按照自左向右、从上而下的方向进行排序
- resize:自定义resize函数用于将模板中的数字大小和银行卡中的数字大小相匹配
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
# 用一个最小的矩形,把找到的形状包起来x,y,h,w,使用列表生成式
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
# zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。zip(*)是解压操作
# sorted 函数传入可迭代对象、排序依据key、reverse默认从小到大
# 下面操作的含义就是,先将各轮廓和对应的边界矩形绑定为元组,再利用sorted函数,按照之前分析的顺序,依据边界框的值进行排序
# 下面的匿名函数是为了遍历到zip压缩的各个元组内部的boundingBoxes
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))
return cnts, boundingBoxes
# 自定义resize函数用于将模板中的数字大小和银行卡中的数字大小相匹配
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
# 若width为空,则利用所需长宽比修改原图像的宽度,height为空同理
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
2. 主要功能模块
ocr_template_match.py:
-
必要操作
# 导入工具包 from imutils import contours import numpy as np import argparse import cv2 import myutils # 设置参数 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") 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() -
模板预处理 + 拆分模板各数字
################################################### 图像预处理 ################################################### # 读取一个模板图像 img = cv2.imread(args["template"]) cv_show('img', img) # 灰度图 ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) cv_show('ref', ref) # 二值图像,各个参数的含义:来源、阈值、赋值、赋值方式,这里为什么是列表下标的[1]? # 因为 threshold参数会返回两个值,第一个值我们通常省略掉了,下面同理 ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1] cv_show('ref', ref) ################################################### 计算模板数字轮廓 ################################################### # cv2.findContours()函数接受的参数为二值图,即黑白的(不是灰度图),cv2.RETR_EXTERNAL只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE只保留终点坐标 # 上面两个参数是最常用的两个轮廓检测参数 # 返回的refCnts(list)中每个元素都是图像中的一个轮廓 # 鉴于上面的threshold返回值的方式,这里也可以写作: # refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[0] refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) # 只能在BGR图上绘制彩线,当然,把三层灰度图利用merge方法合并之后也可以绘制彩线 cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3) cv_show('img', img) print(np.array(refCnts).shape) # 输出为 10,表示模板图中有 10 个轮廓,分别代表了 0 - 9 refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] # 排序,从左到右,从上到下 digits = {} # 遍历每一个轮廓,使用enumrate是为了遍历到下标值 for (i, c) in enumerate(refCnts): # 计算外接矩形并且resize成合适大小 (x, y, w, h) = cv2.boundingRect(c) roi = ref[y:y + h, x:x + w] # (57, 88) 自定义匹配数字的大小 roi = cv2.resize(roi, (57, 88)) # 每一个数字对应每一个模板,digits是一个字典,内容应该是{0:模板0, 1:模板1, ......, 9:模板9} digits[i] = roi
输出图片为:
原始模板图片:
模板灰度图:
二值化模板图(翻转过后):
轮廓检测图:
-
预处理银行卡面
################################################### 预处理银行卡面 ################################################### # 初始化卷积核,自定义,形成一个全为 1 的二维阵列 # 相当于 rectKernel = np.ones((9, 3), np.uint8) rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 读取输入图像,预处理 image = cv2.imread(args["image"]) cv_show('image', image) # 按照长宽比resize图像 image = myutils.resize(image, width=300) gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) cv_show('gray', gray) # 礼帽操作,突出更明亮的区域 # 形态学操作不用二值化吗?不用,只有边缘检测才需要 tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) cv_show('tophat', tophat) # 利用Sobel算子只计算 x 方向的梯度 gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, # ksize=-1相当于用3*3的,默认 ksize=-1) # 绝对值化,避免梯度操作中的某些负值影响 gradX = np.absolute(gradX) (minVal, maxVal) = (np.min(gradX), np.max(gradX)) # 特征缩放,避免某些值超过了 255 ,注意 np.array 的操作流程 gradX = (255 * ((gradX - minVal) / (maxVal - minVal))) gradX = gradX.astype("uint8") print(np.array(gradX).shape) cv_show('gradX', gradX) # 通过闭操作(先膨胀,再腐蚀)将数字连在一起,也就是将数字对轮廓中连在一起顺便填充一些空白 gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel) cv_show('gradX', gradX) # 下面就是进行二值化操作了,便于轮廓检测 # THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0 thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1] cv_show('thresh', thresh) # 再来一个闭操作,填空 thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) # 再来一个闭操作 cv_show('thresh', thresh)
银行卡原始图像(BGR):
银行卡变换大小与灰度化:
对灰度图进行形态学操作(礼帽):
利用Sobel算子计算 X 方向上的梯度:
闭操作(用于将数字连接在一起成组)
二值化:
闭操作(填充组内空白)
-
计算轮廓并打印结果
################################################### 计算轮廓 ################################################### 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) locs = [] # 用于存储ROI,也就是银行中的 四个数字对区域 # 遍历轮廓 for (i, c) in enumerate(cnts): # 计算矩形 (x, y, w, h) = cv2.boundingRect(c) # 计算每个边界框的宽高比,用于过滤出ROI 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): # 用于装载最后匹配的序列,每次换组时都会将 groupOutput 清空 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) # 计算组中数字轮廓,每组分割出来以后不用进行形态学操作,内部可以直接使用轮廓检测 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] # (57, 88)自定义的数字大小用于匹配 roi = cv2.resize(roi, (57, 88)) cv_show('roi', roi) # 计算匹配得分 scores = [] # 将模板中的每个数字与ROI(也就是每组中的单个数字)进行模板匹配 for (digit, digitROI) in digits.items(): # 模板匹配 result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF) (_, score, _, _) = cv2.minMaxLoc(result) scores.append(score) # 得到最合适的数字,下标即为对应的数字,np模块的argmax函数就是返回最大值所在的下标值 # 为何此处要将下标转化为字符串后再加入 groupOutput 呢? groupOutput.append(str(np.argmax(scores))) # 下面的 gX gY gW gH 都是遍历 数字组 得到的结果 # 将组框画出来,每次都是直接在原BGR图上作画 cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1) # 在组框上方写出对应组的所有数字, # 怪不得要转换成字符串,因为要使用字符串拼接, 0.65 指的是 fontScale 表示字体大小对于默认值的缩放系数 1.0f 表示默认大小 cv2.putText(image, "".join(groupOutput), (gX - 5, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2) # 将检测出来的数字组插入最终结果集 output.extend(groupOutput) # 打印结果 print("Credit Card Type: {}".format(FIRST_NUMBER[output[0]])) print("Credit Card #: {}".format("".join(output))) cv2.imshow("Image", image) cv2.waitKey(0)在原始BGR图像上绘制轮廓:
读出第一组区域:
二值化:
针对第一组进行边缘检测与划分,然后将划分出来的四个数字分别于模板中的每个数字进行比对得出结果
后面情况以此类推
最终效果演示:
其实,如果银行卡并不是正对着用户,则可以使用图像校准预处理后再进行上述操作。