计算机视觉案例分析之银行卡号识别
简介
计算机视觉第一课opencv(三)保姆级教学
计算机视觉第一课opencv(一)保姆级教学
计算机视觉第一课opencv(二)保姆级教学
计算机视觉第一课opencv(四)保姆级教学
关于计算机视觉的基础内容我们之前已经说完了,今天我们就来用一个小案例来回顾并丰富一下我们的知识。
一、整体流程
- 预处理模板图像,提取数字模板
- 预处理信用卡图像,定位数字区域
- 对每个数字区域进行处理,提取单个数字
- 使用模板匹配识别每个数字
- 输出识别结果
card.png
template.png
这是数字模板,就是把银行卡号与这里面每一个数字对比实现模糊匹配
二、代码分析
1. 导入工具包和参数设置
# 导入工具包
import numpy as np # 用于数值计算和数组操作
import argparse # 用于解析命令行参数
import cv2 # OpenCV库,用于图像处理
import myutils # 自定义工具函数,包含图像 resize、轮廓排序等功能
argparse
用于解析命令行参数,允许用户通过命令行指定输入图像和模板图像:
# 设置命令行参数
# 创建 ArgumentParser 对象,用于定义和解析命令行参数
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"}
辅助函数cv_show
用于显示图像:
def cv_show(name, img):cv2.imshow(name, img)cv2.waitKey(0) # 等待按键,0表示无限等待
myutils.py
import cv2def sort_contours(cnts, method='left-to-right'):reverse = Falsei = 0if method == 'right-to-left' or method == 'bottom-to-top':reverse = Trueif method == 'top-to-bottom' or method == 'bottom-to-top':i = 1boundingBoxes = [cv2.boundingRect(c) for c in cnts](cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),key=lambda b: b[1][i], reverse=reverse))# zip(*...) 使用星号操作符解包排序后的元组列表,并将其重新组合成两个列表:一个包含所有轮廓,另一个包含所有边界框。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 imageif 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) #默认是cv2.INTER_AREA,即面积插值,适用于缩放图像return resized
这里定义了两个函数方法:
1. sort_contours
函数:轮廓排序
这个函数的作用是按照指定的方式对图像中的轮廓进行排序,方便后续的图像处理和分析。
参数说明:
cnts
:需要排序的轮廓列表(通常来自cv2.findContours
的返回值)method
:排序方法,可选值包括:- 'left-to-right'(默认):从左到右排序
- 'right-to-left':从右到左排序
- 'top-to-bottom':从上到下排序
- 'bottom-to-top':从下到上排序
工作原理:
-
首先根据排序方法设置
reverse
(是否反转排序结果)和i
(排序依据的坐标索引):- 对于水平方向排序(左右),使用 x 坐标(索引 0)
- 对于垂直方向排序(上下),使用 y 坐标(索引 1)
- 反向排序(如 right-to-left)时设置
reverse=True
-
boundingBoxes = [cv2.boundingRect(c) for c in cnts]
:- 为每个轮廓计算边界框(外接矩形)
cv2.boundingRect(c)
返回一个元组(x, y, w, h)
,其中 (x,y) 是矩形左上角坐标,w 和 h 是宽和高
-
排序部分:
zip(cnts, boundingBoxes)
将轮廓与其对应的边界框组合成元组sorted(..., key=lambda b: b[1][i], reverse=reverse)
根据边界框的指定坐标(x 或 y)进行排序zip(*...)
解包排序后的结果,重新组合成轮廓列表和边界框列表
-
返回排序后的轮廓和对应的边界框
2. resize
函数:图像缩放
这个函数用于按比例调整图像的尺寸,可以指定宽度或高度,保持原图的宽高比。
参数说明:
image
:输入图像width
:目标宽度(可选)height
:目标高度(可选)inter
:插值方法,默认为cv2.INTER_AREA
(面积插值)
工作原理:
-
首先获取原图的高度和宽度:
(h, w) = image.shape[:2]
-
处理特殊情况:
- 如果宽度和高度都未指定,直接返回原图
- 如果只指定高度,则计算高度的缩放比例
r = height / float(h)
,再计算对应的宽度 - 如果只指定宽度,则计算宽度的缩放比例
r = width / float(w)
,再计算对应的高度
-
执行缩放:
cv2.resize(image, dim, interpolation=inter)
使用计算出的目标尺寸dim
进行缩放- 默认使用
cv2.INTER_AREA
插值方法,这种方法在缩小图像时效果较好,能保持图像质量
-
返回缩放后的图像
使用场景
sort_contours
常用于需要按顺序处理轮廓的场景,如识别数字、字符时按阅读顺序排列resize
是预处理的常用步骤,用于统一图像尺寸,方便后续处理或显示
2. 模板图像处理(创建数字模板库)
这部分的目的是从模板图像中提取 0-9 的数字特征,建立模板库:
# 读取模板图像
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)
查找模板中的数字轮廓:
_, refCnts, hierarchy = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
# 绘制轮廓查看效果
cv2.drawContours(img, refCnts, -1, color=(0, 0, 255), thickness=3)
cv_show('img', img)
对轮廓进行排序(从左到右):
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]
提取每个数字的 ROI(感兴趣区域)并保存到字典中:
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, dsize=(57, 88)) # 统一大小digits[i] = roi # 存储模板
3. 信用卡图像处理
读取并预处理信用卡图像:
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)) # 定义矩形卷积核
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('tophat', tophat)
闭操作(连接数字,形成完整区域):
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX', closeX)# 二值化处理
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('thresh1', thresh)
4. 定位数字区域
查找图像中的轮廓,定位数字区域:
_, threshCnts, h = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = threshCnts# 绘制所有轮廓查看效果
cur_img = image.copy()
cv2.drawContours(cur_img, cnts, -1, (0, 0, 255), thickness=3)
cv_show('img', cur_img)
筛选出符合信用卡数字区域特征的轮廓:
locs = []
for (i, c) in enumerate(cnts):(x, y, w, h) = cv2.boundingRect(c)ar = w / float(h) # 宽高比# 信用卡数字区域通常宽高比在2.5-4.0之间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))# 按x坐标排序(从左到右)
locs = sorted(locs, key=lambda x: x[0])
5. 识别每个数字
output = []
for (i, (gx, gy, gw, gh)) in enumerate(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)# 查找每个数字的轮廓group_, digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)digitCnts = myutils.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]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)))
6. 显示和输出结果
# 在图像上绘制识别结果
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("Credit Card Type: {}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv2.imshow("Image", image)
cv2.waitKey(0)
三.总体代码
myutils.py在上面
# coding: utf-8
'''
任务书:要求给一家银行设计一套信用卡识别系统。
功能:传入一张信用卡图片,自动识别并输出信用卡中的数字
'''# 导入工具包
import numpy as np # 用于数值计算和数组操作
import argparse # 用于解析命令行参数
import cv2 # OpenCV库,用于图像处理
import myutils # 自定义工具函数,包含图像 resize、轮廓排序等功能'''
命令行参数示例:
-i card1.png # 指定输入的信用卡图片路径
-t template.png # 指定包含数字模板的图片路径
'''# 设置命令行参数
# 创建 ArgumentParser 对象,用于定义和解析命令行参数
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"} # 发现卡def cv_show(name, img): # 图像展示函数cv2.imshow(name, img) # 显示图像,name为窗口名称,img为图像数据cv2.waitKey(0) # 等待用户按键,0表示无限等待# 注意:原代码缺少cv2.destroyAllWindows(),实际使用时建议添加以释放窗口资源'''--------模板图像中数字的定位与提取--------'''
# 读取模板图像
img = cv2.imread(args["template"])
cv_show('Template Image', img) # 显示原始模板图像# 将模板图像转换为灰度图(简化图像处理,减少计算量)
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('Grayscale Template', ref) # 显示灰度模板图像# 对灰度图进行二值化处理(黑白图像),采用反相阈值(THRESH_BINARY_INV)
# 反相处理后变为黑底白字,更便于后续轮廓检测
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('Binary Inverted Template', ref) # 显示二值化后的模板图像# 查找模板图像中的轮廓
# cv2.findContours()函数返回三个值:处理后的图像、轮廓列表、轮廓层次结构
# cv2.RETR_EXTERNAL:只检测外轮廓
# cv2.CHAIN_APPROX_SIMPLE:只保留轮廓的端点坐标,减少存储
_, refCnts, hierarchy = cv2.findContours(ref, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在原始模板图像上绘制找到的轮廓(红色),便于可视化
cv2.drawContours(img, refCnts, -1, color=(0, 0, 255), thickness=3)
cv_show('Template with Contours', img) # 显示带有轮廓的模板图像# 对轮廓进行排序(从左到右),确保数字顺序正确
refCnts = myutils.sort_contours(refCnts, method="left-to-right")[0]# 创建字典存储每个数字对应的模板图像
digits = {}# 遍历每一个轮廓,提取并处理数字模板
for (i, c) in enumerate(refCnts):# 计算轮廓的外接矩形(x,y为左上角坐标,w,h为宽高)(x, y, w, h) = cv2.boundingRect(c)# 提取数字区域(ROI:Region of Interest)roi = ref[y:y + h, x:x + w]# 将数字区域调整为统一大小(57x88像素),便于后续模板匹配roi = cv2.resize(roi, dsize=(57, 88))cv_show(f'Digit Template {i}', roi) # 显示每个数字的模板digits[i] = roi # 将数字模板存入字典,键为数字值,值为对应的图像'''--------信用卡图像处理与数字识别--------'''
# 读取输入的信用卡图像
image = cv2.imread(args["image"])
cv_show('Original Credit Card Image', image) # 显示原始信用卡图像# 调整信用卡图像大小(宽度设为300像素,高度按比例缩放)
image = myutils.resize(image, width=300)
# 将信用卡图像转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('Grayscale Credit Card', gray) # 显示灰度信用卡图像# 顶帽操作(Top Hat):突出图像中的亮区域,抑制暗区域
# 作用是消除背景干扰,突出信用卡上的数字(通常数字是亮色的)
# 创建矩形结构元素(卷积核),用于形态学操作
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('Top Hat Result', tophat) # 显示顶帽操作结果# -------定位数字区域的边框--------
# 1、通过闭操作(先膨胀后腐蚀)将数字连接成一个整体区域
closeX = cv2.morphologyEx(tophat, cv2.MORPH_CLOSE, rectKernel)
cv_show('Close Operation Result', closeX) # 显示闭操作结果# 对闭操作结果进行二值化处理
# 使用OTSU自动阈值法(适合双峰分布的图像),需将阈值参数设为0
thresh = cv2.threshold(closeX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('Binary Threshold Result', thresh) # 显示二值化结果# 再次执行闭操作,进一步连接可能断开的数字区域
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show('Second Close Operation Result', thresh) # 显示第二次闭操作结果# 查找二值化图像中的轮廓
_, threshCnts, h = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 在原始信用卡图像上绘制所有找到的轮廓(红色)
cur_img = image.copy()
cv2.drawContours(cur_img, threshCnts, -1, (0, 0, 255), thickness=3)
cv_show('Credit Card with All Contours', cur_img) # 显示带有所有轮廓的信用卡图像# 筛选出可能包含数字的轮廓区域
locs = [] # 存储符合条件的轮廓位置信息# 遍历所有轮廓
for (i, c) in enumerate(threshCnts):# 计算轮廓的外接矩形(x, y, w, h) = cv2.boundingRect(c)# 计算宽高比(aspect ratio)ar = w / float(h)# 根据信用卡数字区域的特征筛选轮廓:# 1. 宽高比通常在2.5到4.0之间(数字区域是横向的矩形)# 2. 宽度在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)) # 将符合条件的轮廓位置加入列表# 将符合条件的轮廓按x坐标排序(从左到右),确保数字顺序正确
locs = sorted(locs, key=lambda x: x[0])# 识别每个数字区域中的具体数字
output = [] # 存储最终识别的信用卡数字# 遍历每一个数字区域
for (i, (gx, gy, gw, gh)) in enumerate(locs):groupOutput = [] # 存储当前数字区域识别的数字# 提取数字区域(适当扩大边界5像素,确保包含完整数字)group = gray[gy - 5:gy + gh + 5, gx - 5:gx + gw + 5]cv_show(f'Digit Group {i}', group) # 显示当前数字区域# 对数字区域进行二值化处理group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]cv_show(f'Binarized Digit Group {i}', group) # 显示二值化后的数字区域# 查找当前数字区域内每个数字的轮廓group_, digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)# 对数字轮廓进行排序(从左到右)digitCnts = myutils.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('Single Digit ROI', roi) # 显示单个数字区域'''-------使用模板匹配识别数字---------'''scores = [] # 存储与每个模板的匹配得分# 与每个数字模板进行匹配for (digit, digitROI) in digits.items():# 使用相关系数匹配方法(TM_CCOEFF)进行模板匹配result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)# 提取匹配结果中的最大值(最佳匹配得分)(_, score, _, _) = cv2.minMaxLoc(result)scores.append(score)# 找到得分最高的模板对应的数字,即为识别结果groupOutput.append(str(np.argmax(scores)))# 在信用卡图像上绘制识别结果# 绘制矩形框住数字区域cv2.rectangle(image, pt1=(gx - 5, gy - 5), # 左上角坐标pt2=(gx + gw + 5, gy + gh + 5), # 右下角坐标color=(0, 0, 255), # 红色thickness=1) # 线宽# 在矩形上方绘制识别的数字cv2.putText(image, "".join(groupOutput), # 要显示的文本(gx, gy - 15), # 文本位置(左下角)cv2.FONT_HERSHEY_SIMPLEX, # 字体fontScale=0.65, # 字体大小color=(0, 0, 255), # 红色thickness=2) # 字重# 将当前数字区域的识别结果添加到总结果中output.extend(groupOutput)# 输出最终识别结果
print("信用卡类型: {}".format(FIRST_NUMBER[output[0]])) # 根据第一位数字判断卡类型
print("信用卡号码: {}".format("".join(output))) # 拼接所有数字并显示# 显示带有识别结果的信用卡图像
cv2.imshow("Final Result", image)
cv2.waitKey(0)
cv2.destroyAllWindows() # 释放所有窗口资源