计算机视觉(opencv)——基于模板匹配的信用卡号识别系统
实战:基于模板匹配的信用卡号识别系统
任务书:为某家银行设计一套智能卡号识别系统。要求:传入一张图片,就自动输出信用卡图片中的数字。
1. 概览(整套系统做了什么)
这套系统采用**模板匹配(template matching)**思想进行光学数字识别(OCR-like),总体流程:
用一张包含 0–9 数字的小模板图片(
kahao.png
)构建数字模板(每个数字对应一个二值模板)。对待识别的信用卡图像(
card1.png
)做一系列图像预处理(缩放、灰度、形态学操作)以突出数字区域。找到可能包含每组 4 位数字的外接矩形(locs)。
对每个数字位置进一步二值化、分割出单个数字轮廓,并将其 resize 到模板大小后,用
cv2.matchTemplate
与模板集合逐一匹配,取分数最高的模板作为识别结果。在原图上标注识别出的每组数字、输出卡号与卡种,保存结果图
card_result.jpg
并在屏幕上显示。
2. 运行前准备(依赖与输入)
必备环境与文件:
Python(建议 3.7+)
OpenCV(
opencv-python
),Numpy你代码中引用的
myutils
:需包含至少两个函数:sort_contours(cnts, method="left-to-right")
—— 对轮廓排序并返回(sorted_cnts, boundingBoxes)
(代码中只用到第 0 个返回值)resize(image, width=...)
—— 将图片按宽度等比例缩放(或等效实现)
输入文件(需存在于脚本工作目录):
kahao.png
—— 包含 0–9 的模板图片(要求黑底白字或类似,代码中通过二值反转确保黑底白字)card1.png
—— 待识别的信用卡照片
运行方式:把脚本保存为
card_recog.py
(或任意名字),运行:python card_recog.py
执行后会弹出若干
cv2.imshow()
的窗口用于中间调试(模板图、二值图、每组数字的分割图等),最终会弹出result
窗口并在控制台打印识别结果,同时生成card_result.jpg
。
注意:cv2.imshow
在无 GUI 的服务器(比如纯终端)上无法显示;在这种环境下可注释/移除这些显示调用,或把中间结果保存到文件以便检查。
图片准备:
kahao.png
card1.png
card2.png
card3.png
card4.png
card5.png
3. 代码核心模块详解(一行行读懂流程)
运行结果:
自建模块:myutils.py
import cv2""" myutils.py - 自定义工具函数模块 包含轮廓排序和图像缩放两个常用功能, 用于银行卡号识别系统中的图像处理 """ def sort_contours(cnts, method='left-to-right'):"""对轮廓进行排序(按指定方向)参数:cnts: 轮廓列表,由cv2.findContours()返回method: 排序方法,可选值包括:'left-to-right' (默认) - 从左到右'right-to-left' - 从右到左'top-to-bottom' - 从上到下'bottom-to-top' - 从下到上返回:排序后的轮廓列表和对应的边界框列表"""# 初始化排序方向标志和排序依据索引reverse = False # 是否反向排序i = 0 # 排序依据的维度索引(0表示x轴,1表示y轴)# 确定是否需要反向排序if method == 'right-to-left' or method == 'bottom-to-top':reverse = True# 确定排序依据是x轴还是y轴# 垂直方向排序(上下)用y坐标,水平方向排序(左右)用x坐标if method == 'top-to-bottom' or method == 'bottom-to-top':i = 1 # 使用y坐标排序# 为每个轮廓计算边界框(x, y, w, h)boundingBoxes = [cv2.boundingRect(c) for c in cnts]# 将轮廓与对应的边界框组合,按指定维度排序后再拆分# sorted()的key参数指定按边界框的第i个值(x或y)排序(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i], # b[1]是边界框,b[1][i]是x或y坐标reverse=reverse))return cnts, boundingBoxesdef resize(image, width=None, height=None, inter=cv2.INTER_AREA):"""按比例缩放图像(保持原图宽高比)参数:image: 输入图像width: 目标宽度(若为None则按height计算)height: 目标高度(若为None则按width计算)inter: 插值方法,默认cv2.INTER_AREA(适合缩小图像)返回:缩放后的图像"""# 初始化目标尺寸dim = None# 获取原图高度和宽度(h, w) = image.shape[:2] # 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
下面按逻辑模块逐步解释代码里每块做的事与设计动机。
3.1 模板图像中数字定位(构建数字模板字典)
import numpy as np
import cv2
import myutils# 指定信用卡类型
FIRST_NUMBER = {"3": "American Express","4": "Visa","5": "MasterCard","6": "Discover Card"}
def cv_show(name, img): # 绘图展示cv2.imshow(name, img)cv2.waitKey(0)"""----------模板图像中数字的定位处理----------"""
img = cv2.imread("kahao.png")
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] # 二值图像 黑底白字,方便找轮廓
cv_show('ref', ref)
# 计算轮廓。cv2.findContours()函数最重要的参数为 原图,即黑白的(不是灰度图),
# 然后是轮廓检索模式,cv2.RETR_EXTERNAL表示只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE压缩水平垂直
_, refCnts, hierarchy = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
cv_show('img', img)refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0] # 排序,从左到右,从上到下
digits = {} # 保存每一个数字对应的模板
for (i, c) in enumerate(refCnts): # 遍历每一个轮廓(x, y, w, h) = cv2.boundingRect(c) # 计算外接矩形并且resize成合适大小roi = cv2.resize(ref[y:y + h, x:x + w], (57, 88)) # 缩放成指定的大小# cv_show('roi', roi)digits[i] = roi # 每一个数字对应每一个模板
print(digits)
把
kahao.png
变灰度、用阈值并进行了 反转(THRESH_BINARY_INV
)使得模板是白字黑底或黑字白底得到统一输入(代码注释写的是“黑底白字,方便找轮廓”)。cv2.findContours
找外轮廓(RETR_EXTERNAL
),这样能得到 10 个数字的轮廓(假设模板图整齐排列)。通过
myutils.sort_contours(..., "left-to-right")
保证 0–9 的序列顺序正确(很重要)。将每个轮廓的 ROI resize 为固定大小
(57, 88)
—— 以后对检测到的数字也会统一缩放到相同尺寸,方便 template matching 得分比较。最终
digits
是一个字典,digits[i]
存放数字 i 的模板图像(i
的顺序依赖kahao.png
中数字的排列,通常从左到右对应 0,1,2...)。
关键点:模板图的排布必须清晰、无重叠,并且
sort_contours
的返回顺序要和数字索引映射一致,否则识别会混乱。
3.2 信用卡图像预处理(突出数字区域)
"""----------信用卡的图像处理----------"""
image = cv2.imread("card1.png")
cv_show('image', image)
image = myutils.resize(image, width=300) # 设置图像的大小
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('gray', gray)
# 顶帽操作,突出更明亮的区域,消除背景元素,原因是谱系图下变化小,不被腐蚀掉。
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) # 初始化卷积核
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel) # 顶帽 = 原始图像 - 开运算结果(来增强亮部区域)
cv_show('tophat', tophat)
将输入图像缩放到宽度 300(保持比例),减少计算量并统一尺度。
顶帽变换(tophat)用于突出图中比周围更亮的细节(卡号通常是凸起或亮于背景,通过顶帽可以增强这些亮区),
rectKernel
为长条形结构元素,适合横向数字排布。
3.3 找到数字组(闭操作 + 阈值 + 轮廓)
"""-----------找到数字边框-----------"""
# 1、通过闭操作(先膨胀,再腐蚀)将数字连在一起
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX', closeX)
# 2、THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('thresh', thresh)
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) # 再来一个闭操作
cv_show('thresh1', thresh)
# 3、计算轮廓
_, threshCnts, h = 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)
# 4、遍历轮廓,找到数字部分轮廓区域
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])
先用 闭操作(膨胀后腐蚀)把相邻数字连成一块,便于一次性找到一组 4 位数字的边框。
使用
THRESH_OTSU
自动分割(适合双峰直方图)。通过经验阈值筛选轮廓:长宽比在
2.5 ~ 4.0
、宽度在40~55
、高度在10~20
(这些阈值是基于缩放后宽 300 的图像得到的经验值)。最后按 x(横向)排序,保证识别顺序正确(从左到右)。
提示:如果你换了输入图片尺寸或缩放参数(比如缩放到 600 宽),这些经验阈值需要重新调参。
3.4 逐组分割并模板匹配识别单个数字
output = []
# 遍历每一个轮廓中的数字
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)# 预处理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 = 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)(_, score, _, _) = cv2.minMaxLoc(result)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()是OpenCV库中的一个函数,用于在图像上添加文本。cv2.putText(image, "".join(groupOutput), (gX, 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.imwrite('card_result.jpg', image)
cv2.imshow("result", image)
cv2.waitKey(0)
对每组区域单独二值化并计算内部轮廓(每个轮廓对应一个数字)。
再次用
myutils.sort_contours(..., "left-to-right")
确保单组内数字顺序。将每个单数字 ROI resize 到模板大小
(57,88)
后,用cv2.matchTemplate
和每个模板比对,取最大得分对应的索引作为预测数字。在原图画框并写上识别出的字符串,结果追加到
output
列表。脚本最后用
FIRST_NUMBER[output[0]]
判断卡种(依据卡号首位数字定义在字典FIRST_NUMBER
中)。
模板匹配选择了
cv2.TM_CCOEFF
,这是一个基于相关系数的方法,对亮度对比比较敏感。你也可以尝试其它方法(如TM_CCOEFF_NORMED
)以提升对亮度/缩放差异的鲁棒性。
4. 输出与保存
控制台会打印:
Credit Card Type: <卡种> Credit Card #: <识别出的卡号>
识别结果图保存为
card_result.jpg
,并弹出窗口显示最终带标注的结果图。
5. 常见问题与调试建议
模板不匹配 / 识别全部错乱
确认
kahao.png
中数字顺序与myutils.sort_contours
排序方式一致(一般左到右)。如果kahao.png
中字体或尺寸和目标图差异大,template match 容易误判。检查
digits
内容:在构建模板阶段使用cv2.imshow
查看roi
是否正确对应 0–9。
找不到任何 locs(没有检测到数字组)
经验阈值(长宽比和 w/h)是对缩放后图片的经验设定,若图片尺寸或文字大小不同需调节以下参数:
myutils.resize
的width
、ar
范围和w,h
范围。检查顶帽参数
rectKernel
的大小。不同的卡号刻印/印刷样式对核大小敏感。
中间窗口太多或在服务器上无法显示
将所有
cv_show(...)
(即cv2.imshow
)调用注释掉,或改为保存中间结果到文件夹供离线查看。
光照/倾斜/遮挡导致识别失败
可先做透视矫正(当信用卡图像是从角度拍摄时),或者使用更鲁棒的二值化(自适应阈值)和更强的形态学处理。
对于复杂场景,建议换用基于 CNN 的数字识别模型,能更好处理变形、噪声与字体差异(见改进建议)。
6. 性能与改进建议(可选,但很实用)
数据增强与深度学习替代
若需要高鲁棒性(不同字体、不同光照、部分遮挡),考虑训练一个小型 CNN(例如基于 LeNet 或轻量级 MobileNet)做单字符分类,替代模板匹配。模板匹配对字体/噪声敏感。
更鲁棒的候选区域筛选
使用 MSER、或结合边缘检测 + Hough 线(检测卡号的直线排布)来定位数字行,降低对固定阈值的依赖。
引入字符校验/格式化
卡号遵循 Luhn 校验算法,可以用 Luhn 校验判定识别结果的合理性并对错误位进行重识别或提示。
多模板或尺度金字塔匹配
对于不同大小的数字,准备多组模板(多尺度),或对检测到的单字符先做尺度归一化(或在模板匹配时用归一化相关系数
TM_CCOEFF_NORMED
)。
速度优化
将模板全部转换为浮点并预处理,使用
cv2.matchTemplate
的归一化方法以降低计算量,或只在检测到的 ROI 上进行匹配以减少无效匹配次数。
7. 运行示例(重申,便于复制)
目录应该包含:
card_recog.py # 你的脚本(包含上述代码) kahao.png # 模板数字图 card1.png # 待识别卡片图 myutils.py # 包含 sort_contours 与 resize 等函数
运行:
python card_recog.py
成功后控制台会看到卡种和卡号,并生成
card_result.jpg
。
8. 小结(为什么这套方案实用)
优点:实现简单、直观;不依赖训练数据,适合样式固定(银行内部模版、印刷一致)的场景;可以快速原型验证。
局限:对字体、光照、尺度、背景噪声比较敏感;对拍摄角度、遮挡、印刷变化的鲁棒性较差,若目标场景复杂建议过渡到训练的字符识别模型。