目录
引言
在数字化时代,银行、金融机构以及各类支付平台每天都需要处理大量的信用卡和银行卡信息。传统的手工录入卡号方式不仅效率低下,而且容易出错,严重影响了业务处理速度和用户体验。因此,开发一套能够自动识别银行卡号的智能系统具有重要的现实意义和商业价值。
本文将详细介绍如何使用Python和OpenCV库构建一个基于模板匹配技术的银行智能卡号识别系统。该系统能够接收一张信用卡图片作为输入,自动定位卡号区域,分割单个数字,并通过与标准模板进行匹配,最终输出识别出的卡号和信用卡类型。
项目背景与应用场景
银行卡号识别技术广泛应用于以下场景:
- 移动支付:用户拍照上传银行卡,自动填充卡号信息
- 银行APP:开户、绑卡等业务中的卡号自动识别
- 电商平台:支付环节的银行卡信息快速录入
- 金融机构:票据处理、客户信息管理等后台业务
技术选型
本项目采用以下技术栈:
- Python:简洁易用的编程语言,拥有丰富的第三方库
- OpenCV:强大的计算机视觉库,提供了丰富的图像处理函数
- NumPy:用于数值计算和数组操作
- argparse:Python标准库,用于解析命令行参数
为什么选择模板匹配而不是深度学习?
很多人可能会问,现在深度学习OCR技术(如Tesseract、PaddleOCR)已经非常成熟,为什么还要使用传统的模板匹配方法呢?原因主要有以下几点:
- 实现简单:模板匹配无需大量标注数据和复杂的模型训练过程
- 运行速度快:在CPU上即可实时运行,适合资源受限的环境
- 准确率高:对于标准化字体(如信用卡常用的OCR-A字体),识别准确率可达100%
- 易于理解和维护:代码逻辑清晰,便于调试和修改
当然,模板匹配也有其局限性,比如对光照变化、图像倾斜、模糊等情况比较敏感。在文章的最后,我们会讨论如何优化系统以及如何过渡到深度学习方法。
Python命令行参数解析神器argparse详解
在正式开始讲解图像处理之前,我们先来学习一个非常重要的Python标准库------argparse。它能够帮助我们轻松构建用户友好的命令行接口,让程序更加灵活和易用。
1.1 argparse模块概述
argparse是Python标准库中用于解析命令行参数的模块,它能够自动生成帮助信息、处理参数类型转换、支持位置参数和可选参数,并且在用户输入错误参数时能够给出清晰的错误提示。
与手动解析sys.argv相比,argparse具有以下优势:
- 自动生成帮助和使用说明
- 支持多种参数类型(字符串、整数、浮点数等)
- 支持默认值和必填参数
- 支持短选项和长选项(如-h和--help)
- 能够进行参数校验
1.2 argparse核心工作流程
使用argparse构建命令行接口通常分为三个步骤:
- 创建ArgumentParser对象:这个对象是整个参数解析系统的核心,用于定义和管理所有的命令行参数
- 添加参数:使用add_argument()方法向解析器中添加参数规则
- 解析参数:使用parse_args()方法解析命令行输入的参数,返回一个Namespace对象
下面我们通过一个简单的示例来演示这个流程:
python
# 导入argparse模块
import argparse
# 步骤1:创建ArgumentParser对象
parser = argparse.ArgumentParser(description="这是一个简单的加法计算器")
# 步骤2:添加参数
parser.add_argument("a", type=int, help="第一个数字")
parser.add_argument("b", type=int, help="第二个数字")
# 步骤3:解析参数
args = parser.parse_args()
# 使用参数
print(f"{args.a} + {args.b} = {args.a + args.b}")
运行这个脚本:
bash
python calculator.py 3 5
输出:
3 + 5 = 8
查看帮助信息:
bash
python calculator.py -h
输出:
usage: calculator.py [-h] a b
这是一个简单的加法计算器
positional arguments:
a 第一个数字
b 第二个数字
options:
-h, --help show this help message and exit
1.3 add_argument方法常用参数详解
add_argument()方法是argparse中最重要的方法,它有很多参数可以配置。下面我们来详细介绍一些最常用的参数:
(1) name or flags
指定参数的名称。如果是位置参数,只需传入一个名称;如果是可选参数,可以传入多个标志(如短选项和长选项)。
python
# 位置参数
parser.add_argument("filename", help="要处理的文件名")
# 可选参数(短选项+长选项)
parser.add_argument("-v", "--verbose", action="store_true", help="启用详细输出")
(2) type
指定参数的类型。argparse会自动将命令行输入的字符串转换为指定的类型。常用的类型有str、int、float等。
python
parser.add_argument("--age", type=int, help="年龄")
parser.add_argument("--score", type=float, help="分数")
(3) default
指定参数的默认值。如果用户没有在命令行中指定该参数,就会使用默认值。
python
parser.add_argument("--port", type=str, default='COM5', help='串口号')
parser.add_argument("--threshold", type=int, default=1500, help='阈值')
(4) required
指定该参数是否为必填参数。如果设置为True,用户必须在命令行中提供该参数,否则会报错。
python
parser.add_argument("-i", "--image", required=True, help="输入图像路径")
parser.add_argument("-t", "--template", required=True, help="模板图像路径")
(5) help
指定参数的帮助信息。当用户使用-h或--help选项时,会显示这些信息。
python
parser.add_argument("--confid_level", type=float, default=0.8, help='识别的置信度')
(6) action
指定参数的行为。默认值是'store',表示存储参数值。其他常用的action值有:
- 'store_true'/'store_false':存储布尔值True或False
- 'append':将参数值追加到列表中
- 'count':统计参数出现的次数
python
# 布尔开关
parser.add_argument("--debug", action="store_true", help="启用调试模式")
# 追加到列表
parser.add_argument("--tag", action="append", help="标签(可多次使用)")
# 统计次数
parser.add_argument("-v", "--verbose", action="count", default=0, help="详细程度(-v, -vv, -vvv)")
1.4 实战示例:从简单到复杂
现在我们来看一个更复杂的示例,综合运用上面介绍的各种参数:
python
import argparse
# 创建解析器
parser = argparse.ArgumentParser(
description="这是一个功能丰富的图像处理程序",
epilog="示例:python image_processor.py input.jpg -o output.jpg --resize 800 600 --gray"
)
# 位置参数:输入文件
parser.add_argument("input_file", help="输入图像文件路径")
# 可选参数:输出文件
parser.add_argument("-o", "--output", help="输出图像文件路径")
# 可选参数:调整大小
parser.add_argument("--resize", type=int, nargs=2, metavar=("WIDTH", "HEIGHT"), help="调整图像大小")
# 可选参数:转换为灰度图
parser.add_argument("--gray", action="store_true", help="转换为灰度图")
# 可选参数:质量
parser.add_argument("--quality", type=int, default=90, choices=range(1, 101), help="输出图像质量(1-100)")
# 可选参数:详细程度
parser.add_argument("-v", "--verbose", action="count", default=0, help="详细输出(-v显示基本信息,-vv显示详细信息)")
# 解析参数
args = parser.parse_args()
# 使用参数
if args.verbose >= 1:
print(f"输入文件:{args.input_file}")
if args.output:
print(f"输出文件:{args.output}")
if args.resize:
print(f"调整大小:{args.resize[0]}x{args.resize[1]}")
if args.gray:
print("转换为灰度图")
print(f"输出质量:{args.quality}")
# 这里可以添加实际的图像处理代码
print("图像处理完成!")
运行示例:
bash
python image_processor.py photo.jpg -o result.jpg --resize 1024 768 --gray -v
输出:
输入文件:photo.jpg
输出文件:result.jpg
调整大小:1024x768
转换为灰度图
输出质量:90
图像处理完成!
1.5 本文项目中的argparse应用
回到我们的信用卡卡号识别项目,我们使用argparse来接收两个必填参数:输入图像路径和模板图像路径。
python
import argparse
# 设置参数
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()) # 将Namespace对象转换为字典
这里我们使用了vars()函数,它将parse_args()返回的Namespace对象转换为一个字典,这样我们就可以通过args"image"和args"template"来访问参数值了。
OpenCV图像处理基础
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉库,它提供了大量的函数和算法,用于图像处理、特征提取、目标检测、机器学习等领域。OpenCV支持多种编程语言,包括Python、C++、Java等,其中Python接口因其简洁易用而广受欢迎。
2.1 OpenCV简介与环境配置
OpenCV最初由英特尔公司于1999年发起,现在由Willow Garage和Itseez等公司维护。它具有以下特点:
- 跨平台:支持Windows、Linux、macOS等操作系统
- 高性能:核心算法使用C++编写,运行速度快
- 功能丰富:包含超过2500个优化算法
- 开源免费:基于BSD许可证,可以免费用于商业和非商业用途
在Python中安装OpenCV非常简单,只需使用pip命令:
bash
pip install opencv-python
如果需要安装包含额外模块的完整版本,可以使用:
bash
pip install opencv-contrib-python
安装完成后,在Python中导入cv2模块即可使用:
python
import cv2
print(cv2.__version__) # 查看OpenCV版本
2.2 图像的基本操作:读取、显示、保存
(1) 读取图像
使用cv2.imread()函数读取图像。它接受两个参数:图像文件路径和读取模式。
常用的读取模式有:
- cv2.IMREAD_COLOR:以彩色模式读取图像(默认)
- cv2.IMREAD_GRAYSCALE:以灰度模式读取图像
- cv2.IMREAD_UNCHANGED:以原始模式读取图像(包括alpha通道)
python
# 读取彩色图像
img = cv2.imread('card1.png')
# 读取灰度图像
gray = cv2.imread('card1.png', cv2.IMREAD_GRAYSCALE)
注意:OpenCV读取的彩色图像通道顺序是BGR,而不是我们通常使用的RGB。这一点在显示和处理图像时需要特别注意。
(2) 显示图像
使用cv2.imshow()函数显示图像。它接受两个参数:窗口名称和图像数组。
python
cv2.imshow('Image', img)
cv2.waitKey(0) # 等待按键输入
cv2.destroyAllWindows() # 关闭所有窗口
cv2.waitKey()是一个键盘绑定函数,它接受一个参数,表示等待的毫秒数。如果传入0,则会无限等待直到有按键输入。
为了方便使用,我们可以定义一个显示图像的函数:
python
def cv_show(name, img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
(3) 保存图像
使用cv2.imwrite()函数保存图像。它接受两个参数:保存路径和图像数组。
python
cv2.imwrite('result.png', img)
2.3 图像颜色空间转换:BGR转灰度
在计算机视觉中,我们经常需要将彩色图像转换为灰度图像。这是因为灰度图像只包含一个通道,计算量更小,处理速度更快,而且很多算法(如轮廓检测)只能处理灰度图像。
使用cv2.cvtColor()函数进行颜色空间转换。将BGR图像转换为灰度图像的代码如下:
python
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
其他常用的颜色空间转换还有:
- cv2.COLOR_BGR2RGB:转换为RGB格式
- cv2.COLOR_BGR2HSV:转换为HSV格式
- cv2.COLOR_BGR2YCrCb:转换为YCrCb格式
2.4 图像二值化:阈值处理
二值化是将灰度图像转换为黑白图像的过程。它通过设定一个阈值,将像素值大于阈值的设为白色(255),小于阈值的设为黑色(0),或者相反。
OpenCV提供了cv2.threshold()函数进行阈值处理。它的基本用法如下:
python
ret, dst = cv2.threshold(src, thresh, maxval, type)
参数说明:
- src:输入图像(必须是灰度图像)
- thresh:阈值
- maxval:当像素值超过阈值时赋予的值
- type:阈值处理类型
常用的阈值处理类型有:
- cv2.THRESH_BINARY:大于阈值的为maxval,否则为0
- cv2.THRESH_BINARY_INV:大于阈值的为0,否则为maxval
- cv2.THRESH_TRUNC:大于阈值的为thresh,否则不变
- cv2.THRESH_TOZERO:大于阈值的不变,否则为0
- cv2.THRESH_TOZERO_INV:大于阈值的为0,否则不变
在我们的项目中,我们使用反向二值化(THRESH_BINARY_INV),将模板图像转换为黑底白字,这样更便于后续的轮廓检测:
python
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
2.5 形态学操作
形态学操作是基于形状的图像处理技术,主要用于消除噪声、分割图像、连接相邻的元素等。常用的形态学操作有腐蚀、膨胀、开运算、闭运算、顶帽、黑帽等。
(1) 腐蚀和膨胀
腐蚀和膨胀是最基本的形态学操作。腐蚀会缩小图像中的白色区域,而膨胀会扩大白色区域。
python
# 定义结构元素
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
# 腐蚀
erosion = cv2.erode(img, kernel, iterations=1)
# 膨胀
dilation = cv2.dilate(img, kernel, iterations=1)
(2) 开运算和闭运算
- 开运算:先腐蚀后膨胀,用于消除小的亮区域,分离物体
- 闭运算:先膨胀后腐蚀,用于消除小的暗区域,连接相邻的物体
python
# 开运算
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
# 闭运算
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
(3) 顶帽和黑帽
- 顶帽:原图像减去开运算结果,用于突出图像中的亮细节
- 黑帽:闭运算结果减去原图像,用于突出图像中的暗细节
在我们的项目中,我们使用顶帽操作来突出信用卡上的数字区域:
python
# 定义结构元素
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
# 顶帽操作
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
2.6 轮廓检测与绘制
轮廓检测是计算机视觉中非常重要的一项技术,它可以帮助我们找到图像中物体的边界。OpenCV提供了cv2.findContours()函数来检测轮廓。
(1) 轮廓检测
cv2.findContours()函数的基本用法如下:
python
contours, hierarchy = cv2.findContours(image, mode, method)
参数说明:
- image:输入图像(必须是二值图像)
- mode:轮廓检索模式
- method:轮廓近似方法
常用的轮廓检索模式有:
- cv2.RETR_EXTERNAL:只检测最外层轮廓
- cv2.RETR_LIST:检测所有轮廓,但不建立层次关系
- cv2.RETR_CCOMP:检测所有轮廓,建立两级层次结构
- cv2.RETR_TREE:检测所有轮廓,建立完整的层次结构
常用的轮廓近似方法有:
- cv2.CHAIN_APPROX_NONE:存储所有的轮廓点
- cv2.CHAIN_APPROX_SIMPLE:压缩轮廓点,只保留终点坐标
注意:cv2.findContours()函数会原地修改输入图像,因此最好传入图像的副本。
在我们的项目中,我们使用RETR_EXTERNAL模式和CHAIN_APPROX_SIMPLE方法来检测模板图像中的数字轮廓:
python
refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
这里我们使用-2来获取轮廓列表,这是因为在不同版本的OpenCV中,findContours()函数的返回值个数不同。使用-2可以兼容OpenCV 3和OpenCV 4。
(2) 绘制轮廓
使用cv2.drawContours()函数在图像上绘制轮廓:
python
cv2.drawContours(img, contours, contourIdx, color, thickness)
参数说明:
- img:要绘制轮廓的图像
- contours:轮廓列表
- contourIdx:要绘制的轮廓索引(-1表示绘制所有轮廓)
- color:轮廓颜色(BGR格式)
- thickness:轮廓线条宽度
python
# 绘制所有轮廓,颜色为红色,线条宽度为3
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
(3) 轮廓的外接矩形
对于每个轮廓,我们可以使用cv2.boundingRect()函数计算它的外接矩形,这有助于我们定位和裁剪目标区域:
python
(x, y, w, h) = cv2.boundingRect(contour)
返回值:
- x:矩形左上角的x坐标
- y:矩形左上角的y坐标
- w:矩形的宽度
- h:矩形的高度
python
# 绘制外接矩形
cv2.rectangle(img, (x, y), (x + w, y + h), (0, 255, 0), 2)
信用卡卡号识别系统整体设计
3.1 系统架构与核心流程
我们的信用卡卡号识别系统采用"先定位、后识别"的策略,整体流程可以分为以下四个核心环节:
- 模板准备阶段:预处理标准OCR-A字体模板图像,提取每个数字的轮廓,构建数字模板库
- 目标定位阶段:对输入的信用卡图像进行预处理,定位卡号所在的区域
- 特征提取阶段:在卡号区域中提取单个数字的轮廓,分割出每个数字
- 匹配识别阶段:将分割出的单个数字与模板库中的数字进行匹配,识别出具体的数字值
- 结果输出阶段:整合识别结果,判断信用卡类型,输出最终结果
整个系统的工作流程可以用下图表示:
输入信用卡图像
↓
图像预处理(灰度化、形态学操作、边缘检测)
↓
卡号区域定位与提取
↓
数字组分割
↓
单个数字分割
↓
模板匹配识别
↓
结果整合与输出
3.2 OCR-A字体介绍
OCR-A是一种专门为光学字符识别(OCR)设计的字体,由美国国家标准协会(ANSI)于1968年发布。它的特点是字符形状简单、差异明显,易于被计算机识别。
尽管现代OCR系统已经能够识别各种复杂的字体,但OCR-A字体仍然被广泛应用于信用卡、身份证、银行票据等需要高精度识别的领域。信用卡上的卡号通常都采用OCR-A字体印刷,这也是我们选择模板匹配方法的重要原因之一。
OCR-A字体的数字0-9如下图所示:
3.3 模板匹配技术原理
模板匹配是一种最简单、最直观的图像识别技术。它的核心思想是:用一个小的模板图像在目标图像上滑动,逐像素计算两者的相似度,找到匹配度最高的区域。
OpenCV提供了cv2.matchTemplate()函数来实现模板匹配。它的基本用法如下:
python
result = cv2.matchTemplate(image, templ, method)
参数说明:
- image:目标图像
- templ:模板图像
- method:匹配方法
常用的匹配方法有:
- cv2.TM_SQDIFF:平方差匹配,值越小匹配度越高
- cv2.TM_SQDIFF_NORMED:归一化平方差匹配
- cv2.TM_CCORR:相关匹配,值越大匹配度越高
- cv2.TM_CCORR_NORMED:归一化相关匹配
- cv2.TM_CCOEFF:相关系数匹配,值越大匹配度越高
- cv2.TM_CCOEFF_NORMED:归一化相关系数匹配
在我们的项目中,我们使用归一化相关系数匹配方法(TM_CCOEFF_NORMED),因为它对光照变化和对比度变化具有较好的鲁棒性。
模板匹配的结果是一个二维数组,其中每个元素表示模板在对应位置的匹配得分。我们可以使用cv2.minMaxLoc()函数找到得分最高的位置:
python
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
返回值:
- min_val:最小得分
- max_val:最大得分
- min_loc:最小得分的位置
- max_loc:最大得分的位置
对于归一化相关系数匹配方法,max_val越接近1,表示匹配度越高。
3.4 卡号识别的关键挑战与解决方案
在实际应用中,信用卡卡号识别面临着诸多挑战,主要包括:
- 背景干扰:信用卡上通常有复杂的图案、logo和文字,会干扰数字的识别
- 光照变化:不同拍摄条件下的光照差异会影响图像的亮度和对比度
- 图像倾斜:拍摄时信用卡可能会有一定角度的倾斜
- 图像模糊:拍摄时手抖或对焦不准会导致图像模糊
- 反光:信用卡表面的反光会使数字区域过曝
针对这些挑战,我们采取了以下解决方案:
- 形态学操作:使用顶帽操作突出数字区域,抑制背景干扰
- 边缘检测:使用Sobel算子检测数字的边缘,增强数字的轮廓
- 轮廓筛选:根据数字的宽高比、面积等特征筛选出真正的数字轮廓
- 归一化处理:将分割出的数字缩放到与模板相同的大小,确保匹配的准确性
模板图像预处理与数字模板库构建
模板图像预处理是整个系统的基础。我们需要从标准的OCR-A字体模板图像中提取出每个数字的轮廓,并构建一个数字模板库,以便后续进行匹配识别。
4.1 模板图像加载与显示
首先,我们加载模板图像并显示出来:
python
# 读取模板图像
img = cv2.imread(args["template"])
cv_show('Template Image', img)
模板图像应该是一张包含OCR-A字体数字0-9的图像,数字从左到右依次排列。
4.2 灰度化与反向二值化处理
接下来,我们将彩色模板图像转换为灰度图像,然后进行反向二值化处理,将图像转换为黑底白字:
python
# 转换为灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('Gray Template', ref)
# 反向二值化(黑底白字)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('Binary Template', ref)
为什么要进行反向二值化呢?因为cv2.findContours()函数默认检测白色物体的轮廓。将图像转换为黑底白字后,数字变成了白色,背景变成了黑色,这样我们就可以准确地检测到数字的轮廓了。
4.3 数字轮廓检测与提取
现在,我们使用cv2.findContours()函数检测模板图像中的数字轮廓:
python
# 检测轮廓
refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 绘制轮廓
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
cv_show('Template Contours', img)
这里我们使用了RETR_EXTERNAL模式,只检测最外层轮廓,这样可以避免检测到数字内部的轮廓(如数字0和8内部的孔洞)。
4.4 轮廓排序:从左到右
检测到的轮廓列表是无序的,我们需要将它们按照从左到右的顺序进行排序,这样才能正确地对应数字0-9。
为了实现轮廓排序,我们需要编写一个工具函数sort_contours()。这个函数接受一个轮廓列表和排序方法("left-to-right"或"top-to-bottom"),返回排序后的轮廓列表和对应的边界框。
python
def sort_contours(cnts, method="left-to-right"):
# 初始化反向标志和排序索引
reverse = False
i = 0
# 如果是从右到左或从下到上排序,设置反向标志为True
if method == "right-to-left" or method == "bottom-to-top":
reverse = True
# 如果是从上到下或从下到上排序,排序索引为1(y坐标)
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)
使用这个函数对轮廓进行排序:
python
# 从左到右排序轮廓
refCnts = myutils.sort_contours(refCnts, "left-to-right")[0]
4.5 数字模板库的构建
现在,我们遍历排序后的每个轮廓,计算它的外接矩形,裁剪出对应的数字区域,缩放到固定大小,然后将其存储到数字模板库中:
python
# 初始化数字模板库
digits = {}
# 遍历每个轮廓
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))
# 显示裁剪出的数字
cv_show(f'Digit {i}', roi)
# 将数字模板添加到库中
digits[i] = roi
# 打印数字模板库的信息
print(f"成功构建数字模板库,包含 {len(digits)} 个数字模板")
这里我们将每个数字模板都缩放到57x88的大小。这个尺寸是根据OCR-A字体的比例选择的,确保数字的形状不会失真。
现在,我们的数字模板库就构建完成了。digits字典的键是数字0-9,值是对应的数字模板图像。
信用卡图像预处理与卡号区域定位
现在我们开始处理输入的信用卡图像。我们的目标是从复杂的信用卡背景中定位并提取出卡号区域。
5.1 信用卡图像加载与尺寸调整
首先,我们加载信用卡图像并调整其大小,以便后续处理:
python
# 读取信用卡图像
image = cv2.imread(args["image"])
cv_show('Original Image', image)
# 调整图像大小,宽度为300,保持宽高比
(h, w) = image.shape[:2]
r = 300.0 / w
image = cv2.resize(image, (300, int(h * r)))
cv_show('Resized Image', image)
调整图像大小有两个好处:一是可以加快处理速度,二是可以统一不同输入图像的尺寸,使后续的参数更加稳定。
5.2 灰度化与形态学顶帽操作
接下来,我们将彩色图像转换为灰度图像,然后使用形态学顶帽操作来突出数字区域:
python
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('Gray Image', gray)
# 定义矩形结构元素
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
# 顶帽操作,突出亮区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('Tophat Image', tophat)
顶帽操作的作用是原图像减去开运算结果。开运算会消除图像中的小的亮区域,因此顶帽操作可以突出图像中比周围更亮的区域,也就是我们想要的数字区域。
5.3 Sobel边缘检测
现在,我们使用Sobel算子来检测图像的边缘,增强数字的轮廓:
python
# Sobel边缘检测,x方向
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
gradX = np.absolute(gradX)
# 归一化到0-255
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")
cv_show('Sobel Edge', gradX)
我们只在x方向进行边缘检测,因为数字是垂直排列的,x方向的梯度变化更明显。ksize=-1表示使用3x3的Sobel核。
5.4 闭运算与二值化
接下来,我们使用闭运算将相邻的数字连接成一个整体,然后进行二值化处理:
python
# 闭运算,先膨胀后腐蚀,将数字连接成块
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('Closed Image', gradX)
# 二值化,使用OTSU自动阈值
thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('Binary Image', thresh)
# 再次进行闭运算,消除小的孔洞
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show('Closed Binary Image', thresh)
这里我们使用了OTSU自动阈值方法,它可以根据图像的灰度分布自动计算最佳阈值,不需要我们手动指定。
5.5 卡号区域轮廓检测与筛选
现在,我们检测二值图像中的轮廓,并根据数字组的特征筛选出真正的卡号区域:
python
# 检测轮廓
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 复制原图像用于绘制轮廓
img_copy = image.copy()
# 初始化卡号区域列表
locs = []
# 遍历每个轮廓
for (i, c) in enumerate(cnts):
# 计算外接矩形
(x, y, w, h) = cv2.boundingRect(c)
# 计算宽高比
ar = w / float(h)
# 根据宽高比和尺寸筛选轮廓
# 信用卡上的数字组通常是4个数字,宽高比大约在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))
# 在图像上绘制轮廓
cv2.rectangle(img_copy, (x, y), (x + w, y + h), (0, 255, 0), 2)
# 显示筛选后的轮廓
cv_show('Card Number Regions', img_copy)
# 将卡号区域从左到右排序
locs = sorted(locs, key=lambda x: x[0])
print(f"检测到 {len(locs)} 个数字组")
信用卡卡号通常由4组数字组成,每组4个数字。每组数字的宽高比大约在2.5到4.0之间。我们利用这个特征来筛选出真正的卡号区域,排除其他无关的轮廓。
数字分割与模板匹配识别
现在我们已经定位了卡号区域,接下来需要将每个数字组中的单个数字分割出来,然后与我们之前构建的数字模板库进行匹配,识别出具体的数字。
6.1 数字组轮廓提取与排序
我们已经将卡号区域从左到右排序好了,现在遍历每个数字组:
python
# 初始化输出列表
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(f'Group {i+1}', group)
这里我们在裁剪数字组区域时,在四周各扩展了5个像素,以避免裁剪到数字的边缘。
6.2 单个数字的分割
接下来,我们对每个数字组进行预处理,然后分割出单个数字:
python
# 预处理:二值化
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show(f'Binary Group {i+1}', group)
# 检测数字组中的每个数字轮廓
digitCnts = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 从左到右排序数字轮廓
digitCnts = myutils.sort_contours(digitCnts, "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('Digit', roi)
6.3 模板匹配算法实现
现在,我们将分割出的单个数字与模板库中的每个数字模板进行匹配,找到匹配度最高的数字:
python
# 初始化匹配得分列表
scores = []
# 遍历每个数字模板
for (digit, digitROI) in digits.items():
# 模板匹配
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF_NORMED)
# 获取最大得分
(_, max_val, _, _) = cv2.minMaxLoc(result)
# 将得分添加到列表中
scores.append(max_val)
# 找到得分最高的数字
best_digit = str(np.argmax(scores))
best_score = np.max(scores)
# 将识别出的数字添加到组输出中
groupOutput.append(best_digit)
# 打印识别结果
print(f"识别出数字:{best_digit},匹配得分:{best_score:.4f}")
我们使用归一化相关系数匹配方法(TM_CCOEFF_NORMED),得分越接近1,表示匹配度越高。我们选择得分最高的数字作为识别结果。
6.4 信用卡类型判断
根据信用卡卡号的第一位数字,我们可以判断出信用卡的类型:
python
# 将组输出添加到最终输出中
output.append("".join(groupOutput))
# 在原图像上绘制识别结果
cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 2)
cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 整合最终的卡号
card_number = " ".join(output)
print(f"\n识别出的卡号:{card_number}")
# 判断信用卡类型
first_digit = card_number[0]
card_type = FIRST_NUMBER.get(first_digit, "Unknown")
print(f"信用卡类型:{card_type}")
我们定义了一个字典FIRST_NUMBER,用于映射卡号首位数字和信用卡类型:
python
# 指定信用卡类型
FIRST_NUMBER = {
"3": "American Express",
"4": "Visa",
"5": "MasterCard",
"6": "Discover Card"
}
6.5 结果可视化与输出
最后,我们在原图像上绘制识别结果并显示:
python
# 在图像上显示信用卡类型
cv2.putText(image, f"Type: {card_type}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
# 显示最终结果
cv_show('Final Result', image)
# 保存结果图像
cv2.imwrite('result.png', image)
完整代码实现与测试
7.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())
# 指定信用卡类型
FIRST_NUMBER = {
"3": "American Express",
"4": "Visa",
"5": "MasterCard",
"6": "Discover Card"
}
def cv_show(name, img):
"""显示图像"""
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# ========== 模板图像预处理 ==========
print("正在处理模板图像...")
# 读取模板图像
img = cv2.imread(args["template"])
cv_show('Template Image', img)
# 转换为灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('Gray Template', ref)
# 反向二值化(黑底白字)
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1]
cv_show('Binary Template', ref)
# 检测轮廓
refCnts = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 绘制轮廓
cv2.drawContours(img, refCnts, -1, (0, 0, 255), 3)
cv_show('Template Contours', img)
# 从左到右排序轮廓
refCnts = myutils.sort_contours(refCnts, "left-to-right")[0]
# 构建数字模板库
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
print("数字模板库构建完成!")
# ========== 信用卡图像预处理与识别 ==========
print("\n正在处理信用卡图像...")
# 读取信用卡图像
image = cv2.imread(args["image"])
cv_show('Original Image', image)
# 调整图像大小
(h, w) = image.shape[:2]
r = 300.0 / w
image = cv2.resize(image, (300, int(h * r)))
cv_show('Resized Image', image)
# 转换为灰度图
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
cv_show('Gray Image', 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 Image', tophat)
# Sobel边缘检测
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1)
gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal)))
gradX = gradX.astype("uint8")
cv_show('Sobel Edge', gradX)
# 闭运算
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('Closed Image', gradX)
# 二值化
thresh = cv2.threshold(gradX, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('Binary Image', thresh)
# 再次闭运算
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel)
cv_show('Closed Binary Image', thresh)
# 检测轮廓
cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)[-2]
# 筛选卡号区域
locs = []
img_copy = image.copy()
for (i, c) in enumerate(cnts):
(x, y, w, h) = cv2.boundingRect(c)
ar = w / float(h)
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))
cv2.rectangle(img_copy, (x, y), (x + w, y + h), (0, 255, 0), 2)
cv_show('Card Number Regions', img_copy)
locs = sorted(locs, key=lambda x: x[0])
print(f"检测到 {len(locs)} 个数字组")
# 识别每个数字
output = []
for (i, (gX, gY, gW, gH)) in enumerate(locs):
groupOutput = []
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, "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_NORMED)
(_, max_val, _, _) = cv2.minMaxLoc(result)
scores.append(max_val)
best_digit = str(np.argmax(scores))
groupOutput.append(best_digit)
output.append("".join(groupOutput))
cv2.rectangle(image, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 2)
cv2.putText(image, "".join(groupOutput), (gX, gY - 15),
cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 输出结果
card_number = " ".join(output)
print(f"\n识别出的卡号:{card_number}")
first_digit = card_number[0]
card_type = FIRST_NUMBER.get(first_digit, "Unknown")
print(f"信用卡类型:{card_type}")
# 显示最终结果
cv2.putText(image, f"Type: {card_type}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv_show('Final Result', image)
cv2.imwrite('result.png', image)
print("\n识别完成!结果已保存为 result.png")
7.2 myutils工具函数实现
我们需要创建一个名为myutils.py的文件,包含sort_contours函数:
python
import cv2
def sort_contours(cnts, method="left-to-right"):
"""
对轮廓进行排序
参数:
cnts: 轮廓列表
method: 排序方法,可选值:"left-to-right", "right-to-left", "top-to-bottom", "bottom-to-top"
返回:
排序后的轮廓列表和对应的边界框
"""
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)
7.3 系统运行与测试
现在,我们可以运行这个系统来测试信用卡卡号识别效果。首先,准备好以下文件:
- credit_card_ocr.py:上面的完整代码
- myutils.py:工具函数
- ocr_a_reference.png:OCR-A字体模板图像
- card1.png:测试用的信用卡图像
然后,在命令行中运行:
bash
python credit_card_ocr.py -i card1.png -t ocr_a_reference.png
7.4 测试结果分析
运行成功后,你会看到一系列的中间处理图像,最后会显示最终的识别结果图像。同时,控制台会输出识别出的卡号和信用卡类型。
对于清晰、正面拍摄的信用卡图像,本系统的识别准确率可以达到100%。以下是一些测试结果示例:
输入图像:
输出结果:
识别出的卡号:4000 1234 5678 9010
信用卡类型:Visa
结果图像: