Opencv图像处理之基于模板匹配信用卡数字识别任务
0 前言
在现代社会中,我们可能会面临需要手动输入信用卡号码的情况,这不仅费时费力,还存在输入错误的风险。因此,自动识别信用卡上的数字成为一个重要且有趣的任务(如下图,如果能自动识别出来该卡号为4000 1234 5678 9010,则会大大提高我们的效率)。
本文将探讨如何利用OpenCV库中的图像处理函数来预处理信用卡图像,以准备进行数字提取。介绍了包括图像去噪、边缘检测、轮廓提取等技术以求在图像中准确地定位信用卡数字。此外,还介绍了数字识别的关键步骤,即基于模板匹配的方法来识别数字。
ps:转载请标明原文出处!
1 思路概述
整个任务可以分为三个步骤,模板的预处理和数字定位、信用卡图片预处理和数字定位、模板匹配。核心方法为模板匹配,它的基本思想是通过将待识别的数字与预先准备好的数字模板进行比较,从而找到最佳匹配的数字。
1.1 模板的预处理和数字定位
首先,我们需要准备一套数字模板,当然本文中已经有准备好的模板,如下图。(关注博主私聊获取全部资料!!)
接着进行一些图像预处理步骤,以确保匹配的准确性,包括图像去噪、二值化、边缘检测等。这些都可以用OpenCV中的内置函数来实现。
最后使用边缘检测和轮廓提取来找到各个数字的大致位置。
1.2 信用卡图片预处理和数字定位
首先,对于信用卡图像,同样进行一些预处理步骤,包括图像去噪、尺寸调整、灰度化等。这些步骤有助于提高数字识别的准确性。
接着,可以通过使用边缘检测、轮廓提取、形态学操作等技术来找到数字的位置。
1.3 模板匹配
当数字的位置被定位后,将数字与事先准备好的数字模板进行比较。通过计算数字与模板之间的相似度,可以找到最佳匹配的数字。
2 详细步骤
本文编译环境为Pycharm(IDE) + python 3.6.3 + opencv3.4.1.15 + win10。
2.1 导入工具包、参数设置与绘图函数设定
- 工具包导入:导入opencv、numpy和argparse三个包即可,代码如下。
import cv2
import numpy as np
import argparse
- 参数设置:代码如下。其中,args = vars(ag.parse_args())表示解析命令行中传递的参数,并返回一个命名空间对象,其中包含了解析后的参数及其对应的值,并生成一个字典。
# parameter setting
ag = argparse.ArgumentParser()
ag.add_argument('-i', '--image', required=True, help='Path to input image')
ag.add_argument('-j', '--template', required=True, help='Path to template')
args = vars(ag.parse_args())
- 因参数均设置required=True,运行或debug前需在“Edit Configuration Settings”中输入参数。请将下图中红线部分替换为所要识别的信用卡图片地址和模板所在地址,其他非红线部分无需修改。
绘图函数设定:
def cv_show(img):
cv2.imshow('img',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
2.2 模板处理
2.2.1 模板读入与预处理
- 首先,利用opencv中内置函数imread读入模板图片。
img = cv2.imread(args['template'])
cv_show(img)
- 接着,为了增强分割轮廓的准确性,需要对图像进行预处理。具体图像具体分析(这个主要是看最终效果,无标准答案),本文选用预处理操作为灰度化、二值化。
- 灰度化代码:
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
- 二值化代码,threshold函数可:
ref = cv2.threshold(img_gray, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show(ref)
处理后的图像如下:
2.2.2 分割轮廓—模板
- 首先利用findContours函数计算每个数字的轮廓。其中,RETR_EXTERNAL只计算外轮廓, CHAIN_APPROX_SIMPLE只保留终点坐标。coutours为轮廓的坐标,binary和hierarchy 在本文中无需使用。
- 注:cv_show函数非必要,仅为每一步输出图像。
binary, coutours, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, coutours, -1, (0, 0, 255), thickness=3)
cv_show(img)
- 接着,本任务中利用索引来进行数字识别,因此需将所得轮廓进行排序。
- 定义一个索引排序函数,代码如下:
# 对边界进行排序
def sort_coutours(conts, method='left-to-right'):
Reversed = False
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
i = 0
boundingbox = [cv2.boundingRect(cont) for cont in conts]
(conts, boundingbox) = zip(*sorted(zip(conts, boundingbox), reverse=Reversed, key=lambda x: x[1][i]))
return conts, boundingbox
- 调用代码:
ref_coutours = sort_coutours(coutours)[0]
- 接下来,创建一个名为digits的空字典,遍历ref_contours中的每个轮廓,使用boundingRect获取轮廓的边界框坐标(x, y, w, h)。根据边界框坐标在ref图像上提取感兴趣区域(roi)。再使用resize函数将roi调整为指定的尺寸(57x88像素,所识别图像也需转换为相同尺寸)。最后,将调整后的roi存储到digits字典中,键为数字的索引。
# 创建一个字典
digits = {}
for (i, c) in enumerate(ref_coutours):
(x, y, w, h) = cv2.boundingRect(c)
roi = ref[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
digits[i] = roi
2.2.3 图像读入与预处理
- 首先,利用opencv中内置函数imread读入所要识别信用卡图片。
# 读入图片,进行预处理
image = cv2.imread(args['image'])
cv_show(image)
image = myutils.resize(image, width=300)
- 接着,为了增强分割轮廓的准确性,需要对图像进行预处理。本文选用预处理操作包括灰度化、礼帽(突出亮的区域),代码如下。
# 转换为灰度图
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show(image_gray)
# 礼帽(突出亮的区域)
rectkernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
tophat_image = cv2.morphologyEx(image_gray, cv2.MORPH_TOPHAT, rectkernel)
cv_show(tophat_image)
2.2.3 分割轮廓—图像
- 获取银行卡数字的轮廓可分为两个步骤:首先,获取红色方框中的整体轮廓(eg:4000),然后在得到的整体轮廓中提取每个数字的单独轮廓。
- 获取整体轮廓。
# sobel算子
gradx = cv2.Sobel(tophat_image, 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")
cv_show(gradx)
- 利用sobel算子计算所有轮廓信息(仅计算x方向,也可单独计算y,不建议x和y均计算,效果不好)。
- 使用np.absolute函数获取梯度的绝对值,以确保梯度值都为正数。
- 将梯度值归一化到0-255的范围内,方便后续显示。
cv_show结果如下:
- 此时需要相邻数字间的轮廓连城一块儿(eg:4000),方便一起获得前文中红色方框中轮廓,文本中采用两次闭运算即可实现。
# 闭运算(先膨胀,再腐蚀)
sqkernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
closed_gradx = cv2.morphologyEx(gradx, cv2.MORPH_CLOSE, rectkernel)
cv_show(closed_gradx)
#
thresh = cv2.threshold(closed_gradx, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show(thresh)
# 闭运算(先膨胀,再腐蚀)d
closed2_gradx = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqkernel)
cv_show(closed2_gradx)
得到下图,红线圈出部分即为所需要轮廓,可见处理后的图像更容易获得所需轮廓。
- 接着用findContours函数计算轮廓。
binary_image, coutours_image, hierarchy_image = cv2.findContours(closed2_gradx.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
image_copy = image.copy()
cv2.drawContours(image_copy, coutours_image, -1, (0, 0, 255), thickness=2)
cv_show(image_copy)
- 所得coutours_image包含图中所有轮廓,接下来需要筛选出对任务目标有用的轮廓(数字组轮廓,4000、1234、5678、9010)。筛选出所用轮廓后,再排序(eg:为了保证4000在1234前面)。
# 定义所需要的轮廓
locs = []
for (i, cout) in enumerate(coutours_image):
groupOutput = []
(x, y, w, h) = cv2.boundingRect(cout)
ar = float(w/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])
- 得到数字组的轮廓后(eg:4000),再对数字组中每个数字轮廓进行提取。
- 最后记得将每个数字的轮廓尺寸resize至57x88像素(与前文2.2.2相同)。因代码中该部分与模板匹配写在同一个for循环中,故代码在2.3节中给出。
2.3 模板匹配
对每个数字模板进行模板匹配,计算匹配得分。再将最高得分对应的数字添加到groupOutput列表中。将groupOutput列表中的数字合并到output列表中,最后输出结果。代码如下:
# 区域内部
output = []
for (i, (gx, gy, gw, gh)) in enumerate(locs):
groupOutput = []
group = image_gray[gy-5:gy+gh+5, gx-5:gx+gw+5]
thresh_group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show(thresh_group)
bin, cou, hie = cv2.findContours(thresh_group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cous = sort_coutours(cou, method="left-to-right")[0]
for c in cous:
(x, y, w, h) = cv2.boundingRect(c)
roi = thresh_group[y:y+h, x:x+w]
roi = cv2.resize(roi, (57, 88))
cv_show(roi)
scores = []
for (digit, digitroi) in digits.items():
result = cv2.matchTemplate(roi, digitroi, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
groupOutput.append(str(np.argmax(scores)))
output.extend(groupOutput)
print("Credit Card #: {}".format("".join(output)))
3 结语
该任务为学习时练手任务,适合小白上手,欢迎各位大佬交流!!关注博主私聊即可获得全套源码!
ps:转载请标明原文出处!