基于 OpenCV 的信用卡数字识别:从原理到实现
在计算机视觉领域,模板匹配是一种简单高效的目标识别技术,尤其适用于固定模板的物体检测。本文将通过一个完整的信用卡数字识别项目,详细讲解如何使用 OpenCV 实现图像处理、轮廓检测和模板匹配,最终完成信用卡数字的自动识别与卡号类型判断。
项目概述
本项目的核心目标是从信用卡图像中自动提取数字区域,识别出具体数字,并根据卡号的第一位数字判断信用卡类型(如 Visa、MasterCard 等)。整个流程分为四个关键步骤:
- 模板图像处理:创建 0-9 数字的模板库
- 信用卡图像预处理:增强数字区域特征
- 数字区域提取:定位信用卡上的 4 组数字
- 模板匹配识别:对比模板库识别具体数字并判断卡类型
准备工作
环境配置
首先需要安装必要的 Python 库,本项目依赖以下工具包:
pip install opencv-python numpy argparse
- OpenCV (cv2):核心图像处理库,提供图像读取、滤波、轮廓检测等功能
- NumPy:数值计算库,用于数组操作和数学计算
- argparse:命令行参数解析库,方便灵活传入输入图像路径
数据集准备
项目需要两类图像文件:
- 模板图像:包含 0-9 数字的 OCR-A 字体图像(推荐使用白底黑字或黑底白字的清晰图像)
- 信用卡图像:需要识别的信用卡照片,建议选择光线充足、角度正的图像
核心代码解析
1. 工具函数定义
首先定义三个通用工具函数,用于图像显示、轮廓排序和图像缩放:
def cv_show(name, img):"""图像显示函数,显示后按任意键关闭窗口"""cv2.imshow(name, img)cv2.waitKey(0)cv2.destroyAllWindows()def sort_contours(cnts, method='left-to-right'):"""轮廓排序函数,支持四种排序方式"""reverse = Falsei = 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, boundingBoxesdef 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] = roicv_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)
预处理流程解析:
- 灰度化:简化图像维度,减少计算量
- 顶帽操作:突出图像中的亮区域(数字),消除暗背景干扰
- 闭操作:先膨胀后腐蚀,填充数字内部的小空洞,使数字轮廓更完整
- 自适应二值化:使用 OTSU 算法自动计算最佳阈值,处理不同光照条件下的图像
- 二次闭操作:进一步优化数字区域,确保数字连续性
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
运行过程中会依次显示各步骤的处理结果,最终显示带有识别结果的信用卡图像,并在命令行输出识别到的卡号和信用卡类型。