本文主要介绍通过OpenCV- python实现简单的银行卡卡号识别的思路和具体实现过程。
目录
知识准备
项目概述
实现过程
代码讲解
1.自定义函数
2.模版读入与预处理
3.银行卡读入与形态学操作
4. 卡号筛选与ROI切割
5.模版匹配,得出结果
结语
该过程需要用到以下知识:
1.OpenCV图像基础操作,如读取,灰度转换,改变大小等
2.阈值操作,如二值化
3.轮廓检测以及boundingBox构建
4.卷积核构建
5.Sobel算子求梯度,梯度融合
6.形态学操作,如tophat,close操作
7.machTemplate模版匹配
8.用pyplot查看图片,便于debug
本项目旨在综合运用OpenCV的各种方法实现对银行卡卡号的自动识别。
主要原理基于模版匹配,故需要提供与银行卡数字字体相同的数字模版作为参照。
1.读入模版图片,识别边框,取出各个数字作为参考项。
2.读入银行卡图片,进行形态学操作,识别边框,筛选数字区域。
3.将银行卡的卡号数字割出来,与模版比较,选出相似度最高的答案。
import cv2
import matplotlib.pyplot as plt
def read(img, thresh=127, inv=False):
origin = cv2.imread(img)
gray = cv2.imread(img, 0)
binary = cv2.threshold(gray, thresh, 255, cv2.THRESH_BINARY_INV if inv else cv2.THRESH_BINARY)[1]
return origin, gray, binary
def unify_size(img_list):
outputlist = []
xsize, ysize = img_list[0].shape
for img in img_list:
outputlist.append(cv2.resize(img, (ysize, xsize)))
return outputlist
在本项目中,笔者自定义了两个函数。read()实现了传入一个图片文件名,返回其原图,灰度图以及二值化处理后的结果。unify_size()实现了传入一个由图片构成的列表,对列表中所有元素按照列表第一个图片的大小进行大小统一。
template_bgr, template_gray, template_bin = read('template.png', inv=True)
contours, hier = cv2.findContours(template_bin, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
rectangles = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
rectangles.append([x, y, w, h])
rectangles.sort(key=lambda rect: rect[0])
num_template = []
for [x, y, w, h] in rectangles:
num_template.append(template_bin[y:y + h, x:x + w])
num_template = unify_size(num_template)
在本段代码中,首先调用自定义的read()方法返回了模版的bgr格式,灰度和二值化(反向)后的结果。然后调用findContours方法识别了模版图片中的所有外边界,储存在contours列表中。在循环中遍历所有外边框,将每个边框的x,y,w,h存储在rectangles列表中,并对其按照从左到右排序,使得下标和数字一一对应。最后截取每个ROI,储存在num_template列表中,并进行大小的统一。
# 读入
idcard_bgr, idcard_gray, idcard_bin = read('1.png', 127)
# 构建三个将来会用到的卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 5))
rectKernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (18, 7))
basicKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
# 改变大小
idcard_bgr, idcard_gray, idcard_bin = cv2.resize(idcard_bgr, (583, 368)), cv2.resize(idcard_gray,
(583, 368)), cv2.resize(idcard_bin,
(583, 368))
# 顶帽操作
idcard_gray_tophat = cv2.morphologyEx(idcard_gray, cv2.MORPH_TOPHAT, rectKernel)
plt.imshow(idcard_gray_tophat)
plt.show()
# 梯度计算
gradX = cv2.convertScaleAbs(cv2.Sobel(idcard_gray_tophat, cv2.CV_64F, 1, 0, ksize=3))
gradY = cv2.convertScaleAbs(cv2.Sobel(idcard_gray_tophat, cv2.CV_64F, 0, 1, ksize=3))
grad = cv2.addWeighted(gradX, 0.5, gradY, 0.5, 0)
plt.imshow(grad)
plt.show()
# 闭操作
grad_close = cv2.morphologyEx(grad, cv2.MORPH_CLOSE, rectKernel)
plt.imshow(grad_close)
plt.show()
# 自适应阈值二值化
close_autobin = cv2.threshold(grad_close, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
plt.imshow(close_autobin)
plt.show()
# 闭操作
close2 = cv2.morphologyEx(close_autobin, cv2.MORPH_CLOSE, rectKernel2)
plt.imshow(close2)
plt.show()
这段代码对读入的银行卡图片进行了多次形态学操作,其输出如下:
tophat
tophat操作将图片中的反差较大的信息提取了出来。
grad
梯度操作在tophat的基础上描出了梯度变化较大的部分。
第一次闭操作
闭操作将梯度轮廓进行了粘连。
二值化
第二次闭操作
进一步粘连二值化后的结果,使四个号码成块出现。
需要注意的是,实现上述效果的具体途径不是唯一的,各种操作综合运用是关键所在。在上面给出的例子中,经过反复调整,笔者发现,卷积核大小的设置是至关重要的,卷积核过小的话,进行闭操作无法将数字连在一起,分离的数字不利于下一步的筛选;而卷积核设置过大的话,数字可能会和其他色块粘连在一起,无法通过下一步的像素宽高比和色块大小进行筛选出来,导致特征丢失。
# 找边界
contours, hier = cv2.findContours(close2, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
boundingBoxes = []
# 按照宽高比和像素范围筛选数字组
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
ar = w / float(h)
if 2.6 < ar < 3.8 and 70 < w < 115 and 15 < h < 45:
boundingBoxes.append([x, y, w, h])
boundingBoxes.sort(key=lambda box: box[0])
group = []
# ROI
for [x, y, w, h] in boundingBoxes:
group.append(idcard_gray[y - 5:y + h + 5, x - 5:x + w + 5])
plt.imshow(idcard_gray[y - 5:y + h + 5, x - 5:x + w + 5])
plt.show()
group_binary = []
# 对数字组进行形态学操作
for nominee in group:
nominee_bin = cv2.threshold(nominee, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
nominee_close = cv2.morphologyEx(nominee_bin, cv2.MORPH_CLOSE, basicKernel)
group_binary.append(nominee_close)
plt.imshow(cv2.threshold(nominee, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1])
plt.show()
num_nominee = []
# 对数字组找边界
for nominee in group_binary:
contours, hier = cv2.findContours(nominee, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)
rectangles = []
for cnt in contours:
x, y, w, h = cv2.boundingRect(cnt)
rectangles.append([x, y, w, h])
rectangles.sort(key=lambda rect: rect[0])
# ROI切割出单个数字
for [x, y, w, h] in rectangles:
if w > 8 and h > 20:
num_nominee.append(nominee[y:y + h, x:x + w])
plt.imshow(nominee[y:y + h, x:x + w])
plt.show()
输出如下:
数字组ROI
形态学操作后
单个数字ROI
本节代码中要注意几点:
1.按照二次闭操作后的结果找到原图的边界后,需要对边界进行筛选,这里按照边界外接矩形的宽高比和像素大小进行筛选,这里的范围自行确定,只要避免错选和漏选。
2.第一次ROI切割要保留几个像素的切割余量。
3.对数字组ROI二值化后又进行闭操作是为了防止单个数字的笔画没有连起来。
4.对单个数字的筛选同1。
5.要注意对轮廓外矩形排序后提取ROI。
num_nominee = unify_size(num_nominee)
num_standard = []
(x, y) = num_nominee[0].shape
for i in range(len(num_template)):
num_standard.append(cv2.resize(num_template[i], (y, x)))
ans = []
for nominee in num_nominee:
score = []
for standard in num_standard:
res = cv2.matchTemplate(nominee, standard, cv2.TM_SQDIFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
score.append(min_val)
ans.append(score.index(min(score)))
print(ans)
首先对模版数字和待匹配数字大小统一化。
外循环遍历待匹配数字,内循环将其与10个模版匹配并打分,这里笔者采用了TM_SQDIFF_NORMED模式,分数越低,相似度越高。然后获取最低分对应的数组下标作为结果输出。
实现这样一个OCR的效果并不难,主要是对OpenCV基础函数的综合运用。本文提到的方法还有大量的改进空间,还存在很大的局限性。