基于 OpenCV 的信用卡数字识别:从原理到实现

发布于:2025-09-09 ⋅ 阅读:(21) ⋅ 点赞:(0)

在计算机视觉领域,模板匹配是一种简单高效的目标识别技术,尤其适用于固定模板的物体检测。本文将通过一个完整的信用卡数字识别项目,详细讲解如何使用 OpenCV 实现图像处理、轮廓检测和模板匹配,最终完成信用卡数字的自动识别与卡号类型判断。

项目概述

本项目的核心目标是从信用卡图像中自动提取数字区域,识别出具体数字,并根据卡号的第一位数字判断信用卡类型(如 Visa、MasterCard 等)。整个流程分为四个关键步骤:

  1. 模板图像处理:创建 0-9 数字的模板库
  2. 信用卡图像预处理:增强数字区域特征
  3. 数字区域提取:定位信用卡上的 4 组数字
  4. 模板匹配识别:对比模板库识别具体数字并判断卡类型

准备工作

环境配置

首先需要安装必要的 Python 库,本项目依赖以下工具包:

pip install opencv-python numpy argparse
  • OpenCV (cv2):核心图像处理库,提供图像读取、滤波、轮廓检测等功能
  • NumPy:数值计算库,用于数组操作和数学计算
  • argparse:命令行参数解析库,方便灵活传入输入图像路径

数据集准备

项目需要两类图像文件:

  1. 模板图像:包含 0-9 数字的 OCR-A 字体图像(推荐使用白底黑字或黑底白字的清晰图像)
  2. 信用卡图像:需要识别的信用卡照片,建议选择光线充足、角度正的图像

核心代码解析

1. 工具函数定义

首先定义三个通用工具函数,用于图像显示、轮廓排序和图像缩放:

def cv_show(name, img):
    """图像显示函数,显示后按任意键关闭窗口"""
    cv2.imshow(name, img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

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]
    (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

2. 信用卡类型映射

根据国际标准,信用卡号的第一位数字代表卡的类型,我们定义一个映射字典:

FIRST_NUMBER = {
    "3": "American Express",  # 美国运通卡
    "4": "Visa",              # 维萨卡
    "5": "MasterCard",        # 万事达卡
    "6": "Discover Card"      # 发现卡
}

3. 模板图像处理

模板处理是整个识别系统的基础,需要从模板图像中提取 0-9 的数字特征:

# 读取模板图像
img = cv2.imread(args["template"])
if img is None:
    print(f"错误: 无法加载模板图像 {args['template']}")
    exit()

# 转换为灰度图并进行二值化(反色处理,使数字为白色)
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]

# 查找轮廓(只保留外部轮廓)
contours_result = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 兼容不同OpenCV版本的轮廓返回格式
refCnts = contours_result[0] if len(contours_result) == 2 else contours_result[1]

# 绘制轮廓并显示
img_copy = img.copy()
cv2.drawContours(img_copy, refCnts, -1, (0, 0, 255), 2)
cv_show('模板轮廓', img_copy)

# 从左到右排序轮廓(确保数字顺序正确)
refCnts = sort_contours(refCnts, method="left-to-right")[0]
digits = {}

# 提取每个数字的ROI(感兴趣区域)并标准化大小
for (i, c) in enumerate(refCnts):
    (x, y, w, h) = cv2.boundingRect(c)
    roi = ref[y:y + h, x:x + w]
    # 标准化为统一大小(57x88),便于后续匹配
    roi = cv2.resize(roi, (57, 88))
    digits[i] = roi
    cv_show(f'数字 {i}', roi)

print(f"成功提取 {len(digits)} 个数字模板")

关键技术点

  • 二值化反色:将数字变为白色,背景变为黑色,突出数字特征
  • 轮廓检测:使用cv2.RETR_EXTERNAL只检测最外层轮廓,避免内部细节干扰
  • 轮廓排序:确保数字按 0-9 的顺序排列,为后续匹配建立正确映射
  • ROI 标准化:将所有数字调整为相同大小,消除尺寸差异对匹配的影响

4. 信用卡图像预处理

信用卡图像通常存在光照不均、背景复杂等问题,需要一系列预处理操作增强数字区域:

# 读取信用卡图像并调整大小(固定宽度300,保持比例)
image = cv2.imread(args["image"])
if image is None:
    print(f"错误: 无法加载信用卡图像 {args['image']}")
    exit()

cv_show('原始图像', image)
image = resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('灰度图', gray)

# 顶帽操作(突出亮区域,抑制暗背景)
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('顶帽操作', tophat)

# 闭操作(填充数字内部的小空隙,使数字更完整)
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show('闭操作', closeX)

# 二值化(使用OTSU自动阈值,适应不同光照条件)
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('二值化', thresh)

# 再次闭操作(进一步强化数字区域,连接断裂部分)
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show('最终二值化', thresh)

预处理流程解析

  1. 灰度化:简化图像维度,减少计算量
  2. 顶帽操作:突出图像中的亮区域(数字),消除暗背景干扰
  3. 闭操作:先膨胀后腐蚀,填充数字内部的小空洞,使数字轮廓更完整
  4. 自适应二值化:使用 OTSU 算法自动计算最佳阈值,处理不同光照条件下的图像
  5. 二次闭操作:进一步优化数字区域,确保数字连续性

5. 数字区域提取

从预处理后的图像中定位信用卡上的 4 组数字区域:

# 查找所有轮廓
contours_result = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
threshCnts = contours_result[0] if len(contours_result) == 2 else contours_result[1]

# 绘制所有轮廓查看效果
cur_img = image.copy()
cv2.drawContours(cur_img, threshCnts, -1, (0, 0, 255), 2)
cv_show('所有轮廓', cur_img)

# 筛选数字区域(根据宽高比和尺寸范围)
locs = []
for (i, c) in enumerate(threshCnts):
    (x, y, w, h) = cv2.boundingRect(c)
    # 计算宽高比(数字组通常为宽大于高的矩形)
    ar = w / float(h)
    
    # 筛选条件:宽高比2.5-4.0,宽度40-55,高度10-20(根据实际图像调整)
    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])
print(f"找到 {len(locs)} 个数字区域")

区域筛选原理

  • 宽高比:信用卡数字组通常是宽大于高的矩形,宽高比约为 3:1
  • 尺寸范围:根据调整后的图像宽度(300),数字组的宽度通常在 40-55 像素之间
  • 顺序排序:信用卡卡号从左到右排列,因此需要按 x 坐标排序

6. 数字识别与结果输出

使用模板匹配技术识别每个数字,并输出最终结果:

output = []

# 遍历每一个数字区域
for (i, (gX, gY, gW, gH)) in enumerate(locs):
    groupOutput = []

    # 提取数字组区域(适当扩展边界,确保包含完整数字)
    y_start = max(0, gY - 5)
    y_end = min(gray.shape[0], gY + gH + 5)
    x_start = max(0, gX - 5)
    x_end = min(gray.shape[1], gX + gW + 5)

    group = gray[y_start:y_end, x_start:x_end]
    cv_show(f'数字组 {i}', group)

    # 数字组二值化
    group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
    cv_show(f'二值化组 {i}', group)

    # 查找数字组中的单个数字轮廓
    contours_result = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    digitCnts = contours_result[0] if len(contours_result) == 2 else contours_result[1]
    # 从左到右排序单个数字
    digitCnts = sort_contours(digitCnts, method="left-to-right")[0]

    # 识别每个数字
    for c in digitCnts:
        (x, y, w, h) = cv2.boundingRect(c)
        roi = group[y:y + h, x:x + w]
        # 标准化为与模板相同的大小(57x88)
        roi = cv2.resize(roi, (57, 88))
        cv_show('单个数字', roi)

        # 模板匹配(计算与每个模板的相似度)
        scores = []
        for (digit, digitROI) in digits.items():
            # 使用相关系数匹配法(TM_CCOEFF_NORMED),结果越接近1相似度越高
            result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF_NORMED)
            (_, score, _, _) = cv2.minMaxLoc(result)
            scores.append(score)

        # 选择相似度最高的模板作为识别结果
        groupOutput.append(str(np.argmax(scores)))

    # 在原图像上绘制识别结果
    cv2.rectangle(image, (gX, gY), (gX + gW, gY + gH), (0, 0, 255), 1)
    cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
                cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

    # 将当前组的结果添加到总结果中
    output.extend(groupOutput)

# 输出最终识别结果
print("\n=== 识别结果 ===")
if output:
    card_number = "".join(output)
    print(f"识别到的数字: {card_number}")

    # 格式化输出(信用卡通常为4组4位数字)
    if len(card_number) >= 16:
        formatted = " ".join([card_number[i:i + 4] for i in range(0, 16, 4)])
        print(f"格式化卡号: {formatted}")

        # 判断信用卡类型
        first_digit = card_number[0]
        if first_digit in FIRST_NUMBER:
            print(f"信用卡类型: {FIRST_NUMBER[first_digit]}")
        else:
            print("信用卡类型: 未知")
    else:
        print(f"识别到的数字长度: {len(card_number)}")
else:
    print("错误: 未能识别出任何数字")

# 显示最终结果图像
cv_show("最终识别结果", image)
cv2.destroyAllWindows()

模板匹配原理

  • 匹配方法:使用cv2.TM_CCOEFF_NORMED(归一化相关系数匹配),返回值范围为 [-1, 1],1 表示完全匹配
  • 相似度计算:将每个待识别数字与 0-9 的模板逐一对比,计算相似度得分
  • 结果选择:选择得分最高的模板对应的数字作为识别结果

运行方法

将代码保存为credit_card_ocr.py,准备好模板图像(如ocr_a_reference.png)和信用卡图像(如credit_card_01.png),然后在命令行中运行:

python credit_card_ocr.py --image credit_card_01.png --template ocr_a_reference.png

运行过程中会依次显示各步骤的处理结果,最终显示带有识别结果的信用卡图像,并在命令行输出识别到的卡号和信用卡类型。


网站公告

今日签到

点亮在社区的每一天
去签到