OpenCV之信用卡卡号识别

1.OpenCV概述

OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉和图像处理库,主要用于 图像处理、视频分析、机器视觉、深度学习 等领域。它最初由 Intel 研发,现在由 OpenCV 组织 维护,目前支持 C++、Python、Java 等多种语言。擅长领域有:

  1. 图像处理
    • 去噪:去掉照片上的噪点,让图像更清晰
    • 平滑 & 锐化:模糊处理(比如美颜相机的磨皮)、增强边缘(让模糊的字变得更清楚)
    • 颜色调整:可以把彩色图转换成黑白图(灰度化),或者增强对比度
    • 图像分割:把图片中的不同区域分开,比如把人的头像从背景中抠出来(绿幕抠像)
  2. 物体检测与识别
    • 人脸检测:找到照片或视频里的人脸,并画出边框(Haar 级联分类器、DNN)
    • 车牌识别:用于交通监控,自动读取车牌号码
    • 目标检测:找出图片中的特定物体,比如识别商店里的商品、扫描条形码等
  1.  运动跟踪
    • 光流法(Optical Flow):计算物体在连续帧中的运动轨迹
    • 目标跟踪算法(KCF, CSRT, Meanshift 等):给定一个目标,让电脑自动跟踪它
    • 背景建模(Background Subtraction):检测哪些像素在变动,适用于监控场景
  2. 视频处理
    • 视频稳定:去除视频抖动,让画面更流畅
    • 背景去除:提取前景人物,换掉背景,比如绿幕抠像
    • 帧间差分:检测视频中的移动物体,比如监控摄像头检测入侵者
  3. 深度学习
    • 人脸识别(Face Recognition): 结合 DNN 深度神经网络,实现人脸对比、身份认证
    • 目标检测(Object Detection):结合YOLO、SSD、Faster R-CNN 等深度学习模型
    • 图像分割(Image Segmentation):分割出物体的精确轮廓,而不仅仅是简单画个边框

2.概述

1.目标

识别信用卡数字,在控制台输出,同时在信用卡上标注出来。

2.流程

先解析模板数字,获取数字以及对应的数字轮廓特征,再解析信用卡指定位置数字轮廓特征,与模板数字做匹配,获取对应识别结果并标注出来。

3.参数设置

对应信用卡图片路径和模板图片路径配置参数
--image imgs/credit_card_02.png --template ocr_a_reference.png

1
2
3
4
5
# 设置参数  
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())

2.数字模板处理

解析图像模板,提取 数字字符的轮廓,并将其存储为模板数据,方便后续匹配识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 绘图展示(任意按键消失)
def cv_show(name, _img):
cv2.imshow(name, _img)
cv2.waitKey(0)
cv2.destroyAllWindows()

def sort_contours(contours, method="left-to-right"):
# 1️⃣ 确定排序方式,默认升序,从左到右从上到下
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

# 2️⃣ 计算轮廓的外接矩形,bounding_boxes轮廓位置大小
bounding_boxes = [cv2.boundingRect(c) for c in contours]

# 3️⃣ 按指定方向排序,将轮廓和对应的边界框绑定在一起
(_contours, bounding_boxes) = zip(*sorted(zip(contours, bounding_boxes), key=lambda b: b[1][i], reverse=reverse))
return _contours, bounding_boxes

def resize(image, width=None, height=None, inter=cv2.INTER_AREA):
"""
按比例缩放图片,支持指定宽度或高度 进行等比例缩放,避免图片失真
:param image: 输入图像(numpy 数组)
:param width: 目标 宽度,如果设为 None,则按 高度等比例缩放
:param height: 目标 高度,如果设为 None,则按 宽度等比例缩放
:param inter: 插值方法,默认使用 cv2.INTER_AREA(适用于缩小图片)
:return: 图像(numpy 数组)
"""
# 1️⃣ 获取图像的原始宽高
dim = None
(h, w) = image.shape[:2]
# 2️⃣ 如果width和height 都为空,返回原图
if width is None and height is None:
return image
# 3️⃣ 计算新的尺寸
if width is None:
r = height / float(h)
dim = (int(w * r), height)
else:
r = width / float(w)
dim = (width, int(h * r))
# 4️⃣ 执行缩放
resized = cv2.resize(image, dim, interpolation=inter)
return resized
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def template_parsing(_template_img):  
# 1️⃣ 颜色转换:灰度化,将原始图片转换为灰度图像(去掉颜色信息)
# 作用:减少计算量,提高后续处理速度
ref = cv2.cvtColor(_template_img, cv2.COLOR_BGR2GRAY)
cv_show('COLOR_BGR2GRAY', ref)

# 2️⃣ 二值化处理,像素值≤10变为白色(255),像素值>10变为黑色(0)
# 作用:突出白色数字/字符,便于后续轮廓检测
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('THRESH_BINARY_INV', ref)

# 3️⃣ 轮廓检测,提取二值图中的轮廓
# RETR_EXTERNAL 只提取外部轮廓,避免嵌套干扰
# CHAIN_APPROX_SIMPLE 只存储必要的边界点,提高计算效率
ref_contours, _ = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 在原图上绘制轮廓红色,线宽1px
# ref_contours:轮廓列表 -1:绘制所有轮廓 (0, 0, 255):红色 1:轮廓的线宽
cv2.drawContours(_template_img, ref_contours, -1, (0, 0, 255), 1)
cv_show('drawContours', _template_img)

# 4️⃣ 轮廓排序
# 作用:按从左到右的顺序排列轮廓,确保数字顺序正确
ref_contours = utils.sort_contours(ref_contours, method="left-to-right")[0]

# 5️⃣ 提取单个数字区域
_digits_dict = {}
for (i, c) in enumerate(ref_contours):
(x, y, w, h) = cv2.boundingRect(c) # 获取最小外接矩形
roi = ref[y:y + h, x:x + w] # 截取对应区域,提取数字
roi = cv2.resize(roi, (57, 88)) # 归一化尺寸,保证所有数字大小一致
_digits_dict[i] = roi # 以索引i作为key,以数字模板roi作为value

# 6️⃣ 显示提取出的数字
for key, value in _digits_dict.items():
cv_show(f"Digit {key}", value) # value是0~255的二维像素矩阵

return _digits_dict # 用于后续模板匹配

3.信用卡解析

解析信用卡图像,提取卡号区域,并返回卡号的位置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
def credit_card_parsing(_card_img):  
# 1️⃣ 将图像转换为灰度图,减少计算复杂度
gray = cv2.cvtColor(_card_img, cv2.COLOR_BGR2GRAY)
cv_show('gray', gray)

# 创建了一个宽度为9,高度为3的矩形结构元素,适用于细长的信用卡号
rect_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
# 2️⃣ 进行顶帽变换(Top-Hat)(先腐蚀再膨胀),增强比背景亮的细小区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rect_kernel)
cv_show('tophat', tophat)

# 3️⃣ 计算水平梯度(Sobel 算子),检测水平方向的变化,适用于信用卡号的横向排列
grad_x = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
grad_x = np.absolute(grad_x) # 取绝对值
(minVal, maxVal) = (np.min(grad_x), np.max(grad_x)) # gradX的最大和最小值
grad_x = (255 * ((grad_x - minVal) / (maxVal - minVal))) # 将梯度值转换为图像像素值(0 表示黑色,255 表示白色)
grad_x = grad_x.astype("uint8") # 类型转8位无符号整型,[0, 255]
cv_show('gradX1', grad_x)

# 4️⃣ 通过闭操作(先膨胀,再腐蚀)将数字连在一起,减少破碎字符
grad_x = cv2.morphologyEx(grad_x, cv2.MORPH_CLOSE, rect_kernel)
cv_show('gradX2', grad_x)

# 5️⃣ 将图像转换为二值图像(即只有黑白两种像素值),大于阈值的像素设置为 255(白色),小于或等于阈值的像素设置为 0(黑色)
thresh = cv2.threshold(grad_x, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('thresh1', thresh)

# 6️⃣ 再次闭操作 使用更大的核(5x5)进行闭操作,填补字符内部的小孔洞,确保字符区域完整
sq_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sq_kernel)
cv_show('thresh2', thresh)

# 7️⃣ 计算轮廓:找到所有白色区域的轮廓
thresh_contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cur_img = _card_img.copy()
cv2.drawContours(cur_img, thresh_contours, -1, (0, 0, 255), 1)
cv_show('img', cur_img)

# 8️⃣ 过滤&选择可能的卡号区域
_locations = []
for (i, c) in enumerate(thresh_contours):
(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): # 宽 高
_locations.append((x, y, w, h))
# 将符合的轮廓从左到右排序
return gray, sorted(_locations, key=lambda x1: x1[0])
# [(34, 111, 47, 14), (95, 111, 48, 14), (157, 111, 47, 14), (219, 111, 48, 14)]

4.卡号分割与识别

流程:

  1. 遍历卡号区域,对每个区域进行预处理
  2. 提取数字轮廓,逐个数字进行匹配
  3. 使用模板匹配识别每个数字,并标注到原图上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def detect_parsing_digits(_locations, _gray_img, _card_img, _digits_dict):  
_output = [] # 存储完整卡号的识别结果
"""
:param _locations: 信用卡号的坐标列表,格式为[(x, y, w, h), ...]
:param _gray_img: 处理后的灰度图
:param _card_img: 原始信用卡彩色图,用于绘制识别结果
:param _digits_dict: 数字模板字典 {0:模板0,1:模板1, ...,9:模板9},用于模板匹配
:return: 识别到的数字
"""
# 1️⃣ 遍历每个卡号区域
for (i, (gX, gY, gW, gH)) in enumerate(_locations):
group_out_put = [] # 存储当前4位数字的识别结果

# 2️⃣ 提取卡号区域
group = _gray_img[gY - 5:gY + gH + 5, gX - 5:gX + gW + 5]
cv_show('group1', group)

# 3️⃣ 二值化处理,将卡号部分变白,背景变黑,提高对比度
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('group2', group)

# 4️⃣ 提取每个数字轮廓,确保从左到右排序
digit_contours, _ = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
digit_contours = utils.sort_contours(digit_contours, method="left-to-right")[0]

# 5️⃣ 遍历每个数字轮廓
for c in digit_contours:
# 裁剪出当前数字roi
(x, y, w, h) = cv2.boundingRect(c)
roi = group[y:y + h, x:x + w]
# 调整大小 (57, 88),以匹配模板字典中的尺寸
roi = cv2.resize(roi, (57, 88))
cv_show('roi', roi)

# 6️⃣ 计算每个数字的匹配得分
scores = []
# 在模板中计算每一个得分
for (digit, digitROI) in _digits_dict.items():
# 计算ROI与每个模板的匹配程度
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
# 提取最佳匹配分数
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)

# 7️⃣ 选择最佳匹配的数字
group_out_put.append(str(np.argmax(scores)))

# 8️⃣ 在原图上绘制识别结果
cv2.rectangle(_card_img, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(_card_img, "".join(group_out_put), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)

# 9️⃣ 组合最终的信用卡号码
_output.extend(group_out_put)
return _output

5.整合调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
if __name__ == '__main__':  
image_path = args["image"]
template_path = args["template"]
print(f"image:{image_path},template:{template_path}")

# 模板图片
template_img = cv2.imread(template_path)
cv_show('template_img', template_img)
digits_dict = template_parsing(template_img) # 每一个数字对应每一个模板

# 信用卡图片
card_img = cv2.imread(image_path)
card_img = utils.resize(card_img, width=300)
cv_show('card_img', card_img)
gray_img, locations = credit_card_parsing(card_img) # 灰度图 轮廓 从左到右排序

# 检测解析 数字
output = detect_parsing_digits(locations, gray_img, card_img, digits_dict)

# 打印结果
print("Credit Card #: {}".format("".join(output)))
# Credit Card #: 4020340002345678
cv2.imshow("Image", card_img)
cv2.waitKey(0)

6.总结

  1. 模板处理要点
    • 转换为灰度图 ➝ cv2.cvtColor
    • 二值化 ➝ cv2.threshold
    • 轮廓检测 ➝ cv2.findContours
    • 提取单个数字并归一化 ➝ cv2.boundingRect + cv2.resize
  2. 信用卡号区域检测要点
    • 转换为灰度图 ➝ cv2.cvtColor
    • 顶帽变换(增强数字区域)➝ cv2.morphologyEx
    • Sobel 梯度计算(检测横向边缘)➝ cv2.Sobel
    • 闭操作(膨胀+腐蚀)(让数字连接在一起)➝ cv2.morphologyEx
    • 二值化 ➝ cv2.threshold
    • 轮廓检测(定位可能的卡号区域)➝ cv2.findContours
    • 过滤候选区域(宽高比筛选)➝ sorted
  3. 数字识别要点
    • 遍历卡号区域,提取感兴趣区域
    • 二值化(提高对比度)
    • 轮廓检测(提取单个数字)
    • 逐个数字与模板匹配 ➝ cv2.matchTemplate
    • 输出最佳匹配的数字

7.备注

环境:

  • mac: 15.2
  • python: 3.12.4
  • pytorch: 2.5.1
  • numpy: 1.26.4
  • opencv-python: 4.11.0.86

资源和代码:
https://github.com/keychankc/dl_code_for_blog/tree/main/006_opencv_credit_card_ocr