文章目录
- 一、案例概述
- 二、环境与依赖
- [三、 `argparse` 简单教学](#三、
argparse简单教学) - 四、工具函数模块(myutils.py)
-
- [4.1 轮廓排序函数 sort_contours](#4.1 轮廓排序函数 sort_contours)
- [4.2 等比例缩放函数 resize](#4.2 等比例缩放函数 resize)
- 五、主程序完整流程详解
-
- [5.1 命令行参数解析](#5.1 命令行参数解析)
- [5.2 卡组织映射字典](#5.2 卡组织映射字典)
- [5.3 模板图像预处理](#5.3 模板图像预处理)
- [5.4 银行卡图像预处理](#5.4 银行卡图像预处理)
- [5.5 数字组区域筛选](#5.5 数字组区域筛选)
- [5.6 模板匹配识别数字](#5.6 模板匹配识别数字)
- [5.7 结果绘制与输出](#5.7 结果绘制与输出)
- 六、完整代码实现
- 七、总结
一、案例概述
本文将详细讲解一个基于 OpenCV 的银行卡号识别案例,采用模板匹配的经典方案实现。案例通过形态学处理定位银行卡数字区域,再利用模板匹配算法逐个识别数字,最终输出完整卡号与卡组织类型。
案例包含三个核心文件:
myutils.py:封装轮廓排序、图像缩放等通用工具函数银行卡识别.py:主程序,包含完整的识别流程args.py:argparse 命令行参数解析基础示例
二、环境与依赖
| 依赖库 | 作用 |
|---|---|
| OpenCV (cv2) | 图像处理、形态学操作、轮廓检测、模板匹配 |
| NumPy | 数值计算与数组操作 |
| argparse | Python 内置库,解析命令行参数 |
三、 argparse 简单教学
python
import argparse
# 1. 创建解析器对象
parser = argparse.ArgumentParser()
# 2. 定义我们期望接收的参数
# --a 和 --b 是参数的名字,type=str 表示期望接收字符串类型,default 是默认值
parser.add_argument('--a', type=str, default='hello', help='a的值')
parser.add_argument('--b', type=str, default='python', help='b的值')
# 3. 解析参数
# 这一步会去检查你运行脚本时输入的命令,并把参数值提取出来
opt = parser.parse_args()
# 4. 使用参数
# 通过 opt.参数名 的方式来获取值
c = opt.a
d = opt.b
print(c, d)
运行效果演示:
假设我们将上面的代码保存为 test.py,那么会有以下几种运行方式:(终端中输入)
- 不传任何参数
- 命令:
python test.py - 结果:
hello python - 解释:因为没有提供
--a和--b,程序会使用我们在default中设置的默认值。
- 传入部分或全部参数
- 命令:
python test.py --a "你好" --b "世界" - 结果:
你好 世界 - 解释:程序接收到了我们传入的值,并用它们覆盖了默认值。
为什么要用它?
在银行卡识别的案例中,使用 argparse 的好处非常明显:
- 灵活性:我们不需要为了测试不同的银行卡图片而去反复修改代码里的
image_path = 'card1.png'。 - 便捷性:只需在命令行中运行
python 银行卡识别.py -i 新的卡号图片.png -t 模板图片.png即可,非常方便。
四、工具函数模块(myutils.py)
该文件封装了两个高频使用的图像处理工具函数,是案例的基础组件。
4.1 轮廓排序函数 sort_contours
python
import cv2
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
功能说明:
- 支持四种排序方式:left-to-right(从左到右)、right-to-left、top-to-bottom、bottom-to-top
- 核心原理:先计算每个轮廓的外接矩形(x, y, w, h),再按 x 坐标(横向排序)或 y 坐标(纵向排序)进行排序
zip(*...)解包语法:将排序后的 (轮廓,外接矩形) 元组列表重新拆分为两个独立列表
4.2 等比例缩放函数 resize
python
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
功能说明:
- 传入
width或height其中一个参数,即可保持宽高比等比例缩放 - 默认插值方法为
cv2.INTER_AREA(面积插值),适合图像缩小场景
注意:
cv2.resize的dsize参数格式为(宽,高),与shape返回的(高,宽)顺序相反。
五、主程序完整流程详解
5.1 命令行参数解析
python
import numpy as np
import argparse
import cv2
import myutils
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())
-
使用
argparse接收两个必填参数:输入银行卡图片名称、数字模板图片名称。 -
vars()将解析结果转为字典,通过args['image']和args['template']取值。
5.2 卡组织映射字典
python
FIRST_NUMBER = {
'3': 'American Express',
'4': 'Visa',
'5': 'MasterCard',
'6': 'Discover Card'
}
根据银行卡号首位数字判断卡组织。
5.3 模板图像预处理
模板图像是包含 0-9 十个数字的图片,预处理目标是提取每个数字的轮廓并建立模板库。
步骤 1:灰度化 + 二值化
python
img = cv2.imread(args['template'])
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
- 转为灰度图减少计算量
THRESH_BINARY_INV反向二值化,得到黑底白字,便于后续轮廓检测
步骤 2:轮廓检测与排序
python
refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
refCnts = myutils.sort_contours(refCnts, method='left-to-right')[0]
cv2.RETR_EXTERNAL只检测最外层轮廓cv2.CHAIN_APPROX_SIMPLE压缩轮廓点,只保留端点[-2]是兼容写法:OpenCV3 返回 3 个值,OpenCV4 返回 2 个值,取倒数第二个始终能得到轮廓列表
步骤 3:建立数字模板字典
python
digits = {}
for (i, c) in enumerate(refCnts):
(x, y, w, h) = cv2.boundingRect(c)
roi = ref[y:y + h, x:x + w]
roi = cv2.resize(roi, (57, 88))
digits[i] = roi
遍历每个数字轮廓,裁剪出数字区域并统一缩放到 57×88 尺寸,存入 digits 字典,键为数字 0-9,值为对应数字的二值图像。
5.4 银行卡图像预处理
目标:从复杂的卡面背景中定位出数字区域。
步骤 1:读取与基础预处理
python
image = cv2.imread(args['image'])
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
步骤 2:顶帽运算(Top Hat)
python
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
- 顶帽 = 原图 - 开运算(先腐蚀后膨胀)
- 作用:突出亮区域细节,抑制大面积暗背景,消除卡面背景纹理,保留数字等亮细节
- 使用 9×3 的长方形卷积核,适配数字横向长条特征
步骤 3:闭运算 + 二值化
python
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
- 闭运算(先膨胀后腐蚀):将同组内分散的数字像素连接成整体块
THRESH_OTSU:大津法自动计算最优阈值,适合双峰直方图,阈值参数设为 0 即可
步骤 4:二次闭运算
python
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
使用正方形核再次闭运算,填充数字块内部的小孔洞,让轮廓更完整。
5.5 数字组区域筛选
python
cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
locs = []
for c in cnts:
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
if 2.5 < ar < 4.0:
if (40 < w < 55) and (10 < h < 20):
locs.append((x, y, w, h))
locs = sorted(locs, key=lambda x: x[0])
核心筛选逻辑:
- 计算每个轮廓外接矩形的宽高比
ar,银行卡每组 4 位数字宽高比通常在 2.5~4.0 之间 - 再通过宽、高的像素范围进一步过滤噪声轮廓
- 最后按 x 坐标从左到右排序,保证数字组顺序正确
注意:此处的宽高范围是基于
width=300缩放后的图像设定的,如果修改了缩放尺寸,需要对应调整阈值。
5.6 模板匹配识别数字
遍历每一组数字区域,逐个识别单数字。
python
output = []
for (gX, gY, gW, gH) in locs:
groupOutput = []
# 裁剪数字组区域,留5像素边距避免切到数字
group = gray[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
# 二值化
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 检测组内单个数字轮廓
digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
digitCnts = myutils.sort_contours(digitCnts, method="left-to-right")[0]
对每个数字轮廓进行模板匹配:
python
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.TM_CCOEFF(相关系数匹配法),值越大匹配度越高 cv2.minMaxLoc返回匹配结果的最小值、最大值及对应坐标- 取 10 个模板中得分最高的对应数字作为识别结果
5.7 结果绘制与输出
python
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)
print(f'Credit Card Type:{FIRST_NUMBER[output[0]]}')
print(f'Credit Card #:{"".join(output)}')
cv2.imshow('Image', image)
cv2.waitKey(0)
最终在原图上绘制识别框与识别结果,控制台输出卡组织类型和完整卡号。
六、完整代码实现
python
import numpy as np
import argparse # python内置库
import cv2
import myutils
"""
-i card1.png
-t kahao.png
"""
# 设置参数
ap = argparse.ArgumentParser() # 创建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()) # vars()是Python中的一个内置函数,用于返回对象的属性和值的字典。
# 指定信用卡类型
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(args['template'])
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 = cv2.findContours(ref.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cv2.drawContours(img,refCnts,-1,(0,0,255),3)
cv_show('refCnts',img)
refCnts = myutils.sort_contours(refCnts,method='left-to-right')[0] # 排序,从左到右,从上到下
digits = {} # 保存模板中每个数字对应的像素值
# i in (0:) c in refCnts
for (i,c) in enumerate(refCnts): # 遍历每一个轮廓
(x,y,w,h) = cv2.boundingRect(c) # 计算外接矩形并且resize成合适大小
roi = ref[y:y + h, x:x + w]
roi = cv2.resize(roi,(57,88)) # 缩放到指定的大小
cv_show('roi',roi)
digits[i] = roi # 每一个数字对应每一个模板
print(digits)
'''---------信用卡的图像处理-------------'''
# 读取输入图像,预处理
image = cv2.imread(args['image'])
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)
# --------找到数字边框--------
# 1、通过闭操作(先膨胀,再腐蚀)将数字连在一起
closeX = cv2.morphologyEx(tophat,cv2.MORPH_CLOSE,rectKernel)
cv_show('closeX',closeX)
# 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('close2',thresh)
# 计算轮廓
cnts = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cnts_img = image.copy()
cv2.drawContours(cnts_img,cnts,-1,(0,0,255),3)
cv_show('cnts_img',cnts_img)
# 遍历轮廓,找到数字部分像素区域
locs = []
for c in cnts:
(x,y,w,h) = cv2.boundingRect(c) # 计算外接矩形
ar = w / float(h)
# 选择合适的区域,根据实际任务来。
if 2.5 < ar < 4.0:
if (40 < w < 55) and (10 < h < 20):
locs.append((x,y,w,h))
# 将符合的轮廓从左到右排序
locs = sorted(locs,key=lambda x:x[0])
print(locs)
output = []
# 遍历每一个轮廓中的数字
for (gX,gY,gW,gH) in 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)
# 计算每一组的轮廓
digitCnts = cv2.findContours(group.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
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(f'Credit Card Type:{FIRST_NUMBER[output[0]]}')
print(f'Credit Card #:{"".join(output)}')
cv2.imshow('Image',image)
cv2.waitKey(0)
注意:
- 运行前右键修改运行配置,在脚本形参中输入如:
-i card1.png -t kahao.pngtemplate.png为模板图,card.png为银行卡图,都在同级目录下
运行效果:

七、总结
本文完整拆解了基于模板匹配的银行卡号识别案例,从工具函数到主流程,涵盖了形态学处理、轮廓检测、筛选定位、模板匹配全链路。这是一个非常经典的 OpenCV 入门案例,能帮助理解传统图像处理的基本思路。