在计算机视觉领域,模板匹配与轮廓检测是基础且实用的技术组合,广泛应用于字符识别、目标定位等场景。本文将带大家从零搭建一个银行卡号识别项目,通过OpenCV实现模板创建、图像预处理、轮廓提取、模板匹配等核心步骤,精准识别银行卡上的数字信息。
一、项目概述
本项目核心目标是自动识别银行卡表面的卡号,整体流程分为两大模块:一是创建数字模板库,通过对标准OCR-A(一种便于机器识别的字体)数字模板图像的处理,提取0-9每个数字的轮廓特征并保存;二是对银行卡图像进行预处理、轮廓检测定位卡号区域,再通过模板匹配识别每个数字,最终输出卡号及银行卡类型。
技术栈:Python + OpenCV + NumPy,无需复杂的深度学习模型,仅通过传统计算机视觉技术即可实现,适合入门者学习和拓展。
实现结果:
二、核心技术原理
1. 模板匹配
模板匹配是通过滑动模板图像在目标图像上的每个位置,计算两者的相似度,找到最匹配区域的技术。本项目使用OpenCV的cv2.matchTemplate()函数,采用cv2.TM_CCOEFF方法计算相关系数,系数越高表示匹配度越好,以此确定目标数字对应的模板。
2. 轮廓检测
轮廓是图像中连续的像素集合,代表物体的边界特征。通过cv2.findContours()函数可提取图像轮廓,结合轮廓的外接矩形(cv2.boundingRect())可定位目标区域的位置和尺寸,实现卡号区域及单个数字的分割。
3. 形态学操作
形态学操作(顶帽、闭运算)用于图像预处理,消除背景干扰、连接断裂的字符。顶帽操作(cv2.MORPH_TOPHAT)可突出图像中的亮细节,抑制暗背景;闭运算(cv2.MORPH_CLOSE)先膨胀后腐蚀,能填充字符间的空隙,将分散的数字连接成完整区域。
4. 图像缩放与阈值处理
图像缩放(cv2.resize())用于统一模板与目标数字的尺寸,确保匹配的准确性;阈值处理(cv2.threshold())结合cv2.THRESH_OTSU自动阈值法,将灰度图转化为二值图,突出字符与背景的对比,为轮廓检测奠定基础。需要注意的是,我们需要使得二值化后的图片背景为黑色,图中的主体为白色,因为背景为白色,主体为黑色在轮廓检测的时候可能会把整个图片的边框作为轮廓,这算是opencv库的一个小bug。
三、完整代码解析
1. 工具函数定义
首先定义两个核心工具函数:轮廓排序和图像缩放,为后续处理提供支撑。
python
import cv2
import numpy as np
import argparse
def sort_contours(cnts, method='left-to-right'):
"""
对轮廓进行排序
:param cnts: 轮廓列表
:param method: 排序方式,可选left-to-right/right-to-left/top-to-bottom/bottom-to-top
:return: 排序后的轮廓及对应边界框
"""
reverse = False
i = 0 # 排序依据的坐标索引(0为x轴,1为y轴)
if method in ['right-to-left', 'bottom-to-top']:
reverse = True
if method in ['top-to-bottom', 'bottom-to-top']:
i = 1 # 按y轴排序
# 计算每个轮廓的外接矩形
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):
"""
保持比例缩放图像
:param image: 输入图像
:param width: 目标宽度(None则按高度比例计算)
:param height: 目标高度(None则按宽度比例计算)
:param inter: 插值方法,默认INTER_AREA适合缩放
:return: 缩放后的图像
"""
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))
return cv2.resize(image, dim, interpolation=inter)
def cv_show(name, img):
"""图像显示辅助函数"""
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows() # 避免窗口残留
2. 模板库创建
通过处理标准OCR-A数字模板,提取每个数字的轮廓特征,构建数字模板字典。
python
# 解析命令行参数(指定输入图像和模板路径)
ap = argparse.ArgumentParser()
ap.add_argument("-i", "--image", required=True, help="path to input image")
ap.add_argument("-t", "--template", required=True, help="path to template OCR-A image")
args = vars(ap.parse_args())
# 银行卡类型映射(根据卡号第一位判断)
FIRST_NUMBER = {"3": "American Express", "4": "Visa", "5": "MasterCard", "6": "Discover Card"}
# 1. 处理模板图像,创建数字模板库
template = cv2.imread(args["template"])
cv_show("Template Original", template)
# 模板图像预处理:灰度化 -> 二值化(反色,让数字为白色,背景为黑色)
ref = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
cv_show("Template Gray", ref)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show("Template Binary Inv", ref)
# 提取模板轮廓(仅保留外部轮廓)
_, refCnts, _ = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓查看效果
cv2.drawContours(template, refCnts, -1, (255, 0, 0), 3)
cv_show("Template Contours", template)
# 按从左到右排序轮廓(对应0-9数字顺序)
refCnts, _ = sort_contours(refCnts, method="left-to-right")
digits = {} # 存储数字模板:键为数字(0-9),值为对应的轮廓图像
# 遍历轮廓,提取每个数字的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 # 数字i对应模板roi
print("数字模板库构建完成,包含数字0-9")
3. 银行卡图像预处理与卡号定位
对银行卡图像进行一系列预处理,定位出卡号所在的区域,为后续数字识别做准备。
python
# 2. 银行卡图像处理与卡号区域定位
image = cv2.imread(args["image"])
cv_show("Card Original", image)
# 缩放图像(宽度设为300,保持比例),便于处理
image = resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show("Card Gray", gray)
# 顶帽操作:突出亮细节(卡号),抑制暗背景
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) # 矩形结构元素
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show("Card TopHat", tophat)
# 闭运算1:连接断裂的数字(先膨胀后腐蚀)
closex = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show("Card Close1", closex)
# 二值化:自动阈值(OTSU),适合双峰图像
thresh = cv2.threshold(closex, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show("Card Threshold", thresh)
# 闭运算2:进一步填充空隙,强化卡号区域
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 正方形结构元素
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show("Card Close2", thresh)
# 提取轮廓,定位卡号区域
_, cnts, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts_img = image.copy()
cv2.drawContours(cnts_img, cnts, -1, (0, 0, 255), 3)
cv_show("Card All Contours", cnts_img)
# 筛选卡号区域轮廓(根据宽高比和尺寸过滤)
locs = []
for c in cnts:
x, y, w, h = cv2.boundingRect(c)
ar = w / float(h) # 宽高比
# 卡号区域特征:宽高比2.5-4.0,尺寸在合理范围
if 2.5 < ar < 4.0 and 40 < w < 55 and 10 < h < 20:
locs.append((x, y, w, h))
# 按x轴排序卡号区域(从左到右)
locs = sorted(locs, key=lambda x: x[0])
print(f"定位到{len(locs)}个卡号区域")
这里筛选卡号需要根据银行卡的特点来设计代码,如果换成身份证号码的识别,我们代码只需要更改这段从轮廓中筛选出卡号位置的代码。
4. 模板匹配识别数字
对每个卡号区域进行分割,提取单个数字,通过模板匹配识别数字,最终输出结果。
python
# 3. 模板匹配识别数字并输出结果
output = []
for (gX, gY, gW, gH) in locs:
groupOutput = []
# 提取卡号区域ROI,适当扩大边界(避免裁剪到数字边缘)
group = gray[gY-5:gY+gH+5, gX-5:gX+gW+5]
cv_show("Card Number Group", group)
# 对数字组进行二值化
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show("Group Threshold", group)
# 提取数字组内单个数字的轮廓
_, digitCnts, _ = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 按从左到右排序数字轮廓
digitCnts, _ = sort_contours(digitCnts, method="left-to-right")
# 遍历每个数字轮廓,进行模板匹配
for c in digitCnts:
x, y, w, h = cv2.boundingRect(c)
roi = group[y:y+h, x:x+w]
# 标准化尺寸(与模板一致)
roi = cv2.resize(roi, (57, 88))
# 模板匹配:计算与每个数字模板的相似度
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(image, "".join(groupOutput), (gX, gY-15),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 汇总识别结果
output.extend(groupOutput)
# 输出最终结果
card_type = FIRST_NUMBER.get(output[0], "Unknown Card")
card_number = "".join(output)
print(f"银行卡类型:{card_type}")
print(f"银行卡号:{card_number}")
# 显示最终识别效果
cv2.imshow("Final Recognition Result", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
四、项目运行与效果优化
1. 运行方式与结果
将代码保存为credit_card_recognition.py,准备一张银行卡图像和OCR-A数字模板图像,在命令行执行以下命令:
python credit_card_recognition.py -i card.jpg -t template.png
其中card.jpg为银行卡图像路径,template.png为数字模板图像路径。
也可以直接在pycharm中修改运行配置里面来填写形参-i card.jpg -t template.png。
运行结果:
2. 常见问题与优化方案
-
识别准确率低:可能是模板图像质量差、银行卡图像有倾斜或阴影。优化方案:选用高清无噪点的模板;对银行卡图像进行倾斜校正(霍夫变换)、阴影去除(灰度归一化)。
-
卡号区域定位失败 :结构元素尺寸不合适。优化方案:根据银行卡图像尺寸调整
rectKernel和sqKernel的大小,确保能准确提取卡号区域轮廓。 -
数字分割不完整:二值化阈值不当或轮廓筛选条件过严。优化方案:调整阈值参数,或放宽轮廓筛选的宽高比、尺寸范围,结合实际图像微调。
五、总结与展望
本项目通过传统计算机视觉技术,实现了银行卡号的自动识别,核心在于模板匹配与轮廓检测的结合,以及合理的图像预处理流程。该方案无需训练复杂模型,部署简单、运行高效,适合对实时性要求不高的场景。
未来可进一步优化方向:1. 加入图像倾斜校正和阴影去除模块,提升对复杂场景图像的适配性;2. 结合机器学习算法(如SVM、KNN)替换模板匹配,提高识别准确率和泛化能力;3. 拓展支持多种字体、多种银行卡类型的识别,实现更通用的字符识别系统。
通过本项目的实践,能深入理解OpenCV中形态学操作、轮廓处理、模板匹配的核心用法,为后续更复杂的计算机视觉项目打下基础。
作业:实现身份证好识别。