简介
关于计算机视觉的基础内容我们之前已经说完了,今天我们就来用一个小案例来回顾并丰富一下我们的知识。
一、整体流程
- 预处理模板图像,提取数字模板
- 预处理信用卡图像,定位数字区域
- 对每个数字区域进行处理,提取单个数字
- 使用模板匹配识别每个数字
- 输出识别结果
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 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))
# zip(*...) 使用星号操作符解包排序后的元组列表,并将其重新组合成两个列表:一个包含所有轮廓,另一个包含所有边界框。
return cnts, boundingBoxes
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) #默认是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() # 释放所有窗口资源