文章目录
- 前言
- 信用卡图像处理
- 矩形内核
- 顶帽运算(TopHat)提取数字特征
- 闭运算降噪与轮廓闭合处理
- 银行卡数字区域轮廓筛选与定位
- 基于长宽比、尺寸阈值筛选数字区域
- 单组数字拆分与逐字模板匹配识别
- 数字尺寸统一归一化处理
- 识别结果绘制
- 卡号拼接与卡种匹配输出
前言
在上篇教程中,我们已经完成了项目环境搭建、工具脚本封装、数字模板图预处理、0-9数字模板提取存储的全部核心操作,成功将标准模板图中的十个数字拆解为独立、统一尺寸的数字样本字典,为后续真实银行卡数字识别搭建了核心模板库。
本篇作为项目下篇核心实战内容,将重点讲解真实银行卡图像的全套图像处理流程、数字区域精准定位、单数字拆分、模板匹配识别、结果可视化输出全链路逻辑。全程延续零基础友好的讲解风格,逐行拆解代码原理、图像处理底层逻辑、参数设置依据,让大家不仅能跑通代码,更能理解每一步图像变换的意义,彻底掌握OpenCV模板匹配数字识别的核心思路。
信用卡图像处理
不同于干净的标准模板图,真实银行卡图片存在背景复杂、光线不均、底色干扰、纹理杂乱等问题,直接轮廓检测无法精准提取数字,因此需要通过缩放、灰度转换、形态学运算、二值化层层优化图像质量,凸显数字特征,弱化背景干扰。
完整代码:
c
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)
首先读取图片,从命令行参数中读取我们传入的待识别银行卡图片,此时读取的图像包含完整的色彩、背景、文字、图案所有信息,是原始未处理的图像。
c
image = cv2.imread(args['image'])
cv_show('image',image)
然后进行图像预处理,真实银行卡图片像素尺寸大小不一,有的原图分辨率过高、像素冗余,有的尺寸过小特征模糊,统一将图像宽度缩放至300像素,高度按比例自适应缩放。
然后再进行灰度图展示
c
image = myutils.resize(image,width=300)
gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
cv_show('gray',gray)
我们调用的是先前封装好的脚本myutils.py中的resize函数,它可以统一输入图像尺寸,避免因图像大小差异导致轮廓检测阈值失效;压缩冗余像素,减少代码运算量,提升程序运行速度;标准化数字像素大小,匹配我们提前设定的数字模板尺寸,保证后续模板匹配精度。
myutils.py中的resize函数:
c
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
运行结果图:


矩形内核
形态学运算是OpenCV处理二值图像、提取目标特征、降噪的核心手段,而结构核(Kernel)是形态学运算的核心载体,所有的膨胀、腐蚀、开运算、闭运算、顶帽运算都需要依靠自定义结构核完成。
c
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT,(9,3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
rectKernel = (9,3)矩形核,长宽比接近3:1的横向矩形结构核,专门适配银行卡数字的形态------银行卡数字为横向排列、细长矩形形态。
sqKernel = (5,5)正方形核,标准正方形结构核,尺寸更小、更规整,主要用于后期精细化降噪、闭合微小轮廓缺口,填补数字边缘的细微断裂,同时不会破坏数字整体形态。
顶帽运算(TopHat)提取数字特征
顶帽运算是形态学高级运算。公式:顶帽图像 = 原图 - 开运算图像。
c
tophat = cv2.morphologyEx(gray,cv2.MORPH_TOPHAT,rectKernel)
cv_show('tophat',tophat)

顶帽运算可以剔除大面积均匀背景,只保留数字、文字、细小纹理等高亮细节特征。
闭运算降噪与轮廓闭合处理
我们要知道原理:
膨胀操作:填补数字边缘的细微缺口、断裂,让数字轮廓完整连续;
腐蚀操作:还原数字整体尺寸,避免膨胀导致数字轮廓过度放大、粘连;
闭运算(MORPH_CLOSE)的运算逻辑:先膨胀、后腐蚀。
c
closeX = cv2.morphologyEx(tophat,cv2.MORPH_CLOSE,rectKernel)
cv_show('closeX',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('close2',thresh)
先对顶帽运算的结果图进行闭运算,然后对结果图进行二值化处理,在对二值化处理后的图片在进行一次闭运算,将图片中的空缺补齐。

银行卡数字区域轮廓筛选与定位
经过全套预处理后,图像中仅保留卡号、少量文字轮廓,接下来我们需要通过轮廓检测+特征筛选,精准定位出银行卡四组卡号数字区域,过滤掉有效期、银行名称、logo等无效轮廓。
c
cnts = cv2.findContours(thresh,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
cnts_img = img.copy()
cv2.drawContours(cnts_img,cnts,-1,(0,0,255),3)
cv_show('cnts_img',cnts_img)
cv2.RETR_EXTERNAL只检测图像最外层轮廓,忽略所有嵌套子轮廓。银行卡数字是实心区域,内部无嵌套轮廓,该参数可以过滤掉无效内层轮廓,减少运算量;同时cv2.CHAIN_APPROX_SIMPLE压缩轮廓点,只保留轮廓拐点坐标,去除冗余像素点,精简轮廓数据,提升运算速度;

基于长宽比、尺寸阈值筛选数字区域
c
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)
为每一个轮廓绘制最小外接矩形,返回矩形左上角坐标(x,y)、宽度w、高度h,通过这四个参数定义轮廓的几何尺寸。
银行卡的卡号是4位数字为一组,整体呈现细长矩形形态。经过大量实测,合法卡号组的长宽比固定在2.5~4.0之间。
结合缩放后图像的像素标准,限定宽度40-55像素、高度10-20像素,精准匹配四位数字组的像素尺寸,彻底过滤所有不符合尺寸的无效轮廓。
最后进行排序,关键字为x坐标,我们筛选出来的四个数字区域可以观察到,他的y,w,h变化都不是很大,只有x是有变化趋势的,所以选取x作为关键字。


单组数字拆分与逐字模板匹配识别
我们已经定位到四组卡号区域,每组包含4个数字,接下来需要拆分每组数字、提取单个数字、与模板库数字匹配、判定最优结果,完成最终的数字识别。
c
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]
通过坐标裁剪提取每组数字区域时,我们对坐标进行了上下左右5像素扩边。原因是:轮廓检测的外接矩形会紧贴数字边缘,容易裁剪丢失数字边缘像素,扩边5像素可以完整保留数字全貌,避免因裁剪缺失导致识别失败,同时不会引入多余干扰。



c
digitCnts = cv2.findContours(group.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)[-2]
digitCnts = myutils.sort_contours(digitCnts,method='left-to-right')[0]
对每组4位数字的局部图像,再次进行外层轮廓检测,提取单个数字的轮廓。此时检测到的轮廓即为单个0-9数字的独立轮廓。
再次调用我们封装的sort_contours排序函数,对单数字轮廓从左到右排序,严格保证每组内4个数字的顺序与银行卡显示顺序一致。
数字尺寸统一归一化处理
c
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])
cv_show('roi',roi)
上篇中我们将所有标准模板数字统一缩放为57*88像素,因此这里必须将检测到的真实银行卡单个数字,同样resize为宽57、高88的统一尺寸。
尺寸归一化是模板匹配识别成功的关键,如果尺寸不一致,匹配分数会完全失效,识别准确率直接归零。缩放后每个单数字roi图像,与模板库数字尺寸、维度完全对齐,可直接用于匹配计算。
c
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)))
我们遍历上篇构建的digits模板字典,依次取出0-9的标准数字模板;执行模板匹配,得到匹配结果矩阵;然后提取匹配结果中的最小值、最大值、最小值坐标、最大值坐标,我们只需要最大匹配分数score。
找出分数列表中最大值对应的索引,该索引即为匹配度最高的数字(0-9);将识别出的数字转为字符串,存入当前组的输出列表groupOutput,完成单个数字识别。
识别结果绘制
在原始银行卡图像上,为每一组识别完成的数字区域绘制红色矩形框,标记识别的目标位置,实现结果可视化。扩边绘制与前期图像裁剪扩边逻辑一致,完整包裹数字区域,标注清晰美观。线宽设置为1,不遮挡原始数字,同时标记明显。
同时将每组识别完成的4位数字拼接为字符串,绘制在对应数字区域的上方位置,方便直观查看识别结果。
c
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)
绘制坐标:区域上方15像素,字体大小0.65、颜色红色、线宽2。
卡号拼接与卡种匹配输出
c
print("Credit Card Type:{}".format(FIRST_NUMBER[output[0]]))
print("Credit Card #: {}".format("".join(output)))
cv2.imshow( "Image", image)
cv2.waitKey(0)
将四组数字的识别结果全部存入output列表,拼接为完整的16位银行卡卡号;根据开篇定义的FIRST_NUMBER字典,通过卡号第一位数字匹配银行卡类型,支持Visa、MasterCard、American Express、Discover Card四种主流卡种识别。
输出结果:

c
Credit Card Type:Visa
Credit Card #: 4000123456789010