0.背景介绍
接下来我们正式进入项目实战部分,这一章要介绍的是一个信用卡号识别的项目。首先,我们来明确一下研究的问题,假设我们有一张信用卡如下所示,我们要做的就是识别出这上面卡号信息,然后会输出一个序列,第一个序列就是4020,第二序列是3400,第三个序列0234,第四个序列5678,也就是说此时我们不光是把这个数输出来,我们还要知道对应的位置。
之前我们已经介绍了Opencv
的各种图像基本操作,例如形态学操作、模板匹配,我们现在要做的就是把这些方法全部应用到一起,相当于把我们以前所学的知识点全部穿插到咱们这个项目当中了。
我们先来看一下要完成这个项目的基本思想。
思考一: 首先最核心的问题是我们如何判断一个数字是几呢?这里我们要用到模板匹配
假设我们有一个数字模板如下:
现在我们要做的就是将信用卡上每一个数字和模板上的数字进行匹配,看一下它与模板上的哪一个数字最接近,我们就把这个数字输出。因此我们第一步需要得到一个与目标信用卡数字字体非常接近的一个模板。
*思考二: *如何每一个数字单独拿出来?
我们之前介绍轮廓检测,但是直接得到的轮廓各个数字之间非常不规则,我们可以利用轮廓的外接矩形或者外接圆来进行操作。
总体就是分为以上两个步骤,具体过程我们还需要对图像进行各种预处理操作,我们在后面在代码中细致介绍。
以下是项目的主要框架,想要源码的可以私信我获取。
1.模板处理
1.1模板读取
首先我们将目标模板读取
# 导入工具包
from imutils import contours
import numpy as np
import argparse
import cv2
import myutils
# 指定模板和目标图像位置
target = 'images/credit_card_02.png'
template = 'images/ocr_a_reference.png'
# 定义图像展示函数
def cv_show(name,img):
cv2.imshow(name, img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 读取模板图像
img = cv2.imread(template)
cv_show(im)
1.2预处理
接下来对模板进行预处理,转换为二值图,因为我们后续轮廓检测时只接受二值图输入。
# 灰度图
ref = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
cv_show('ref',ref)
# 二值图像
ref = cv2.threshold(ref, 10, 255, cv2.THRESH_BINARY_INV)[1] #阈值设为10
cv_show('ref',ref)
现在得到了二值图像之后我们就可以进行图像轮廓检测了。
1.3轮廓计算
在这里我们使用cv2.findContours()
函数,其只接收一个二值图像,cv2.RETR_EXTERNAL
只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE
只保留终点坐标。返回参数我们只需要用refCnts
即可,它返回的是我们的轮廓信息
# 计算轮廓
#返回的list中每个元素都是图像中的一个轮廓
ref_, refCnts, hierarchy = cv2.findContours(ref.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
cv2.drawContours(img,refCnts,-1,(0,0,255),3) # -1表示绘制所有轮廓
cv_show('img',img) #展示轮廓
现在我们得到了0-9每个数字的外轮廓信息,但是直接返回的轮廓顺序不一定是按照我们模板从左到右排序的,接下来我们需要对轮廓进行排序,让它按照从左到右0,1,2,3,4…9的顺序排列,这里我们定义了函数,sort_contours
,我们直接根据我们的坐标排序,就可以得到按照0-9排列的轮廓
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] #计算外接矩形,用一个最小的矩形,返回x,y,h,w,分别表示坐标和高宽
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes),
key=lambda b: b[1][i], reverse=reverse))#排序
return cnts, boundingBoxes
现在得到了排序好后的轮廓,接下来我们需要把模板中每个数字轮廓单独拿出来放到一个字典中方便我们后续进行匹配,通过cv.boundingRect()
得到轮廓坐标和长宽信息,然后利用我们之前的ROI读取方法即可,最后我们更改一下轮廓的大小。
refCnts = sort_contours(refCnts, method="left-to-right")[0] #排序,从左到右
digits = {}
# 遍历每一个轮廓
for (i, c) in enumerate(refCnts):
# 计算外接矩形并且resize成合适大小
(x, y, w, h) = cv2.boundingRect(c)
roi = ref[y:y + h, x:x + w]
roi = cv2.resize(roi, (57, 88))
# 每一个数字对应每一个模板
digits[i] = roi
接下来我们就得到了每个数字模板的轮廓信息,并保留在digits字典中,接下来我们需要对输入图像进行处理。
2.输入图像处理
2.1图形读取
这里我们初始化了两个卷积核,分别为9×3和5×5的,这里大家可以根据自己的任务更换别的卷积核大小。然后我们把目标图像读取进来并转换为灰度图
# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3))
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
#读取输入图像,预处理
image = cv2.imread(target)
cv_show('image',image)
image = myutils.resize(image, width=300)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 转换为灰度图
cv_show('gray',gray)
2.2预处理
得到灰度图之后,我们需要进行更细节的预处理,因为我们想要检测的是银行卡号,我们关注的是这样的数字部分,也就是更亮的区域,因此我们在这里进行了礼帽操作,来突出我们想要研究的信息,在实际应用中,可以根据具体想要研究的任务来选择其他的处理方式
#礼帽操作,突出更明亮的区域
tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, rectKernel)
cv_show('tophat',tophat)
接下来我们进一步的利用sobel算子来进行边缘检测
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, #ksize=-1相当于用3*3的
ksize=-1) # 使用Sobel算子处理
gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX - minVal) / (maxVal - minVal))) # 归一化处理
gradX = gradX.astype("uint8")
print (np.array(gradX).shape)
cv_show('gradX',gradX)
得到边缘之后,我们希望将这些数字分块放到一起,每四个数字为一个小方块。我们可以利用之前介绍过的先膨胀,再腐蚀的操作。
#通过闭操作(先膨胀,再腐蚀)将数字连在一起
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
cv_show('gradX',gradX)
#二值化处理:THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0
thresh = cv2.threshold(gradX, 0, 255,
cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
cv_show('thresh',thresh)
#再来一个闭操作
thresh = cv2.morphologyEx(thresh, cv2.MORPH_CLOSE, sqKernel) #再来一个闭操作
cv_show('thresh',thresh)
首先经过一个闭操作后,得到下列结果
然后我们进一步使用二值化处理,将图片转换为二值图像
现在得到的结果中,还有一部分空隙,我们再进行一次闭操作,得到结果如下:
现在得到的结果是一个完全闭合的状态了,此时我们再检测它的外轮廓,会更准确一些。
2.3轮廓计算
接下来我们计算轮廓,和之前在处理模板一样,我们调用cv2.findContours()
函数计算轮廓信息。
# 计算轮廓
thresh_, threshCnts, hierarchy = 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),3)#在原始图像上绘制轮廓
cv_show('img',cur_img)
这里需要说明的是,我们这里是用经过一切预处理后得到的轮廓信息,然后绘制在原始图像中。但是我们得到的轮廓有点多,且有一些不太规则的形状,有些可能不是我们想要轮廓,我们需要把这些轮廓过滤,我们只要四组数字轮廓。我们可以根据他们的坐标位置进行筛选,具体的筛选范围大家要根据自己的实际任务选择.
locs = []
# 遍历轮廓
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))
# 将符合的轮廓从左到右排序
locs = sorted(locs, key=lambda x:x[0])
locs
[(34, 111, 47, 14), (95, 111, 48, 14), (157, 111, 47, 14), (219, 111, 48, 14)]
可以看到我们现在得到了四个轮廓,并且进行了排序,接下来我们怎么进行模板匹配呢?我们不是拿这四个大轮廓去匹配,而是在每一个大轮廓中,再去分隔成小轮廓,然后去和我们之前保存的10个数的模板进行匹配。
2.4计算匹配得分
接下来我们要做的是去遍历每一个轮廓当中的数字,然后将其与模板中的10个数字计算匹配得分,从而识别出对于的数字,我们先来看第一个轮廓:
有了这个之后,就和我们最开始处理模板一样,先进行二值化处理,得到下图
然后计算每一组的轮廓,并按照从左到右的顺序排列,以第一个数字为例,得到结果如下
然后我们就是将这每一个数字与模板上的10个数字进行对比,看一下和哪一个最相似。具体做法跟之前都是一样的吧,先去找到外接矩形,然后对外接矩形进行resize。然后我们就要计算得分了,我们利用模板匹配中的方法,使用cv2.TM_CCOEFF
计算得分,然后找到最匹配的数字,这样就完成了我们所有的步骤了
output = []
# 遍历每一个轮廓中的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):
# initialize the list of group digits
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 = contours.sort_contours(digitCnts,
method="left-to-right")[0]
# 计算每一组中的每一个数值
for c in digitCnts:
# 找到当前数值的轮廓,resize成合适的的大小
(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)
# 计算匹配得分
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)))
# 绘制结果
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: {}".format("".join(output)))
cv2.imshow("Image", image)
cv2.waitKey(0)
Credit Card: 4020340002345678
3.小结
我们这个项目主要分两步。第一步,定位到目标数字在什么位置。第二步,基于定位好的区域,在模板当中去匹配它到底是一个什么样的值。中间利用了我们之前介绍过的各种图像处理方法。今天我们这个项目是做一个信用卡号识别,如果说大家想做车牌识别、学生卡、身份证识别,都是一个类似的做法,我们主需要更改一下对于的照片模板以及其中的一些参数即可。