计算机视觉(opencv)——基于模板匹配的信用卡号识别系统

实战:基于模板匹配的信用卡号识别系统

任务书:为某家银行设计一套智能卡号识别系统。要求:传入一张图片,就自动输出信用卡图片中的数字。


1. 概览(整套系统做了什么)

这套系统采用**模板匹配(template matching)**思想进行光学数字识别(OCR-like),总体流程:

  1. 用一张包含 0--9 数字的小模板图片(kahao.png)构建数字模板(每个数字对应一个二值模板)。

  2. 对待识别的信用卡图像(card1.png)做一系列图像预处理(缩放、灰度、形态学操作)以突出数字区域。

  3. 找到可能包含每组 4 位数字的外接矩形(locs)。

  4. 对每个数字位置进一步二值化、分割出单个数字轮廓,并将其 resize 到模板大小后,用 cv2.matchTemplate 与模板集合逐一匹配,取分数最高的模板作为识别结果。

  5. 在原图上标注识别出的每组数字、输出卡号与卡种,保存结果图 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, boundingBoxes


def 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. 常见问题与调试建议

  1. 模板不匹配 / 识别全部错乱

    • 确认 kahao.png 中数字顺序与 myutils.sort_contours 排序方式一致(一般左到右)。如果 kahao.png 中字体或尺寸和目标图差异大,template match 容易误判。

    • 检查 digits 内容:在构建模板阶段使用 cv2.imshow 查看 roi 是否正确对应 0--9。

  2. 找不到任何 locs(没有检测到数字组)

    • 经验阈值(长宽比和 w/h)是对缩放后图片的经验设定,若图片尺寸或文字大小不同需调节以下参数:myutils.resizewidthar 范围和 w,h 范围。

    • 检查顶帽参数 rectKernel 的大小。不同的卡号刻印/印刷样式对核大小敏感。

  3. 中间窗口太多或在服务器上无法显示

    • 将所有 cv_show(...)(即 cv2.imshow)调用注释掉,或改为保存中间结果到文件夹供离线查看。
  4. 光照/倾斜/遮挡导致识别失败

    • 可先做透视矫正(当信用卡图像是从角度拍摄时),或者使用更鲁棒的二值化(自适应阈值)和更强的形态学处理。

    • 对于复杂场景,建议换用基于 CNN 的数字识别模型,能更好处理变形、噪声与字体差异(见改进建议)。


6. 性能与改进建议(可选,但很实用)

  1. 数据增强与深度学习替代

    • 若需要高鲁棒性(不同字体、不同光照、部分遮挡),考虑训练一个小型 CNN(例如基于 LeNet 或轻量级 MobileNet)做单字符分类,替代模板匹配。模板匹配对字体/噪声敏感。
  2. 更鲁棒的候选区域筛选

    • 使用 MSER、或结合边缘检测 + Hough 线(检测卡号的直线排布)来定位数字行,降低对固定阈值的依赖。
  3. 引入字符校验/格式化

    • 卡号遵循 Luhn 校验算法,可以用 Luhn 校验判定识别结果的合理性并对错误位进行重识别或提示。
  4. 多模板或尺度金字塔匹配

    • 对于不同大小的数字,准备多组模板(多尺度),或对检测到的单字符先做尺度归一化(或在模板匹配时用归一化相关系数 TM_CCOEFF_NORMED)。
  5. 速度优化

    • 将模板全部转换为浮点并预处理,使用 cv2.matchTemplate 的归一化方法以降低计算量,或只在检测到的 ROI 上进行匹配以减少无效匹配次数。

7. 运行示例(重申,便于复制)

  • 目录应该包含:

    复制代码
    card_recog.py       # 你的脚本(包含上述代码)
    kahao.png           # 模板数字图
    card1.png           # 待识别卡片图
    myutils.py          # 包含 sort_contours 与 resize 等函数
  • 运行:

    复制代码
    python card_recog.py
  • 成功后控制台会看到卡种和卡号,并生成 card_result.jpg


8. 小结(为什么这套方案实用)

  • 优点:实现简单、直观;不依赖训练数据,适合样式固定(银行内部模版、印刷一致)的场景;可以快速原型验证。

  • 局限:对字体、光照、尺度、背景噪声比较敏感;对拍摄角度、遮挡、印刷变化的鲁棒性较差,若目标场景复杂建议过渡到训练的字符识别模型。

相关推荐
荼蘼4 小时前
OpenCV 高阶 图像金字塔 用法解析及案例实现
人工智能·opencv·计算机视觉
CVer儿5 小时前
【天文】星光超分辨图像增强
计算机视觉
星期天要睡觉6 小时前
计算机视觉(opencv)——基于模板匹配的身份证号识别系统
人工智能·opencv·计算机视觉
Francek Chen6 小时前
【深度学习计算机视觉】03:目标检测和边界框
人工智能·pytorch·深度学习·目标检测·计算机视觉·边界框
CoovallyAIHub7 小时前
基于YOLO集成模型的无人机多光谱风电部件缺陷检测
深度学习·算法·计算机视觉
CoovallyAIHub7 小时前
几十个像素的小目标,为何难倒无人机?LCW-YOLO让无人机小目标检测不再卡顿
深度学习·算法·计算机视觉
Cathyqiii11 小时前
生成对抗网络(GAN)
人工智能·深度学习·计算机视觉
湫兮之风16 小时前
Opencv: cv::LUT()深入解析图像块快速查表变换
人工智能·opencv·计算机视觉
学弟17 小时前
快捷:常见ocr学术数据集预处理版本汇总(适配mmocr)
计算机视觉