哈喽,大家好呀,这里是是滑稽研究所。这是我之前学习的一个opencv的小项目,感觉通过它学习到了很多,这里与大家分享一下。除了之前已经学习过的基础的图像处理方法之外,本次我学到一个新的方法——cv2.matchTemplate()函数,它的作用是模板匹配。我们把它放在最后讲解。
本文非常详细,希望对小伙伴们有所帮助。
其中一个素材如下:
我们的数字模板是这样的:
模板的数字是等距的,我们需要把每个数字单独的裁出来,然后人工加上编号。使用enumerate()方法即可实现。
简单粗暴一点,上代码,我们跟着代码每个阶段的运行结果来理一遍思路。
#滑稽研究所出品
import cv2 as cv
from imutils import contours
import matplotlib as plt
import numpy as np
#对模板图像做预处理
img=cv.imread("images/ocr_a_reference.png")
gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
ref=cv.threshold(gray,10,255,cv.THRESH_BINARY_INV)[1]#二值化
refCnts=cv.findContours(ref.copy(),cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)[0]
#后面还要继续使用ref,因此需使用ref.copy(),否则会对原图做出改变;第二个参数为指定检测外轮廓;第三个参数为轮廓逼近的一种方法
cv.drawContours(img,refCnts,-1,(0,0,255),3)#-1表示绘制所有轮廓,当指定为其他值时,只在图像中选择一个绘制单个轮廓
cv.imshow("m", img)
refCnts=contours.sort_contours(refCnts,method="left-to-right")[0]#返回排序完的轮廓
digits={}#建立一个字典类型,i是轮廓索引,c是轮廓----字典类型:每个索引号对应一个索引值
for(i,c)in enumerate(refCnts):#i是轮廓索引,c是对应轮廓,则完成了对检测出来的轮廓进行了排序
(x,y,w,h)=cv.boundingRect(c)#得到每一个外接矩形的左上坐标点以及长度、宽度
roi=ref[y:y+h,x:x+w]#每个数字的外接矩形的尺寸
roi=cv.resize(roi,(57,88))#重置外接矩形的尺寸至合适大小
digits[i]=roi#每个数字对应一个模板
#展示模板
cv.imshow("7", digits[7])
#对待检测图像做预处理
recKernel=cv.getStructuringElement(cv.MORPH_RECT,(10,3))#为保证检测信息准确,需去除银行卡页面杂乱信
sqKernel=cv.getStructuringElement(cv.MORPH_RECT,(2,2))#因此需要对图像做形态学操作,故在此设立卷积核
image=cv.imread("images/credit_card_01.png")
image=cv.resize(image,(250,200))
gray=cv.cvtColor(image,cv.COLOR_BGR2GRAY)
tophat=cv.morphologyEx(gray,cv.MORPH_TOPHAT,recKernel)#根据字体的大小来选定合适的核;顶帽操作来突出明亮的区域
#此处为x方向
gradx=cv.Sobel(tophat,ddepth=cv.CV_32F,dx=1,dy=0,ksize=3)#对X还是对Y需要或者同时需要根据实际需要来设定,图像梯度
gradx=np.absolute(gradx)#取绝对值
(minVal,maxVal)=(np.min(gradx),np.max(gradx))#归一化
#8位无符号数,取值范围为0~255,超出范围则会被截断(截断指的是,当数值大于255保留为255,当数值小于0保留为0,其余不变)
gradx=(255*((gradx-minVal)/(maxVal-minVal)))
gradx=gradx.astype("uint8")
cv.imshow("2", gradx)
gradx=cv.morphologyEx(gradx,cv.MORPH_CLOSE,recKernel)#执行闭操作,使图像上的内容成块出现
cv.imshow("8", gradx)
ret,thresh=cv.threshold(gradx,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU)#低阈值之所以设为0,是因为后面的方法选用了OTSU自动设定阈值,适合双峰的图像操作
thresh=cv.morphologyEx(thresh,cv.MORPH_CLOSE,sqKernel)#本次闭操作是为了填补二值化图像中块中的不完整小块
cv.imshow("9", thresh)
cv.imshow("4", thresh)
Cnts=cv.findContours(thresh.copy(),cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)[0]
cnts=Cnts
curImage=image.copy()
cv.drawContours(curImage,cnts,-1,(0,0,255),3)#此处轮廓不是原图像的轮廓,而是经历了一些列运算之后的图像的轮廓
cv.imshow("5", curImage)
locs=[]
for (i,c)in enumerate(cnts):
(x,y,w,h)=cv.boundingRect(c)#做出每个轮廓的外接矩形
ar=w/float(h)#根据外接矩形的长宽比来筛选有用的矩形,并将其添加到元组中
print("rate",ar)
print("w",w,"h",h)
if ar>2.4 and ar<4.0:
if(w>35 and w<55)and(h>10 and h<20):
locs.append((x,y,w,h))
locs=sorted(locs,key=lambda x:x[0])#经筛选之后的轮廓
print("this:",locs)
output=[]
#我们之前筛选出了4个轮廓,每个轮廓又等分成4份,分别与模板对比
for (i,(gx,gy,gw,gh))in enumerate(locs):#遍历每一块中的每一个数字
groupOutput=[]
group=gray[gy-2:gy+gh+2,gx-2:gx+gw+2]#取轮廓及其周围的区域
cv.imshow("group",group)
group=cv.threshold(group,0,255,cv.THRESH_BINARY|cv.THRESH_OTSU)[1]#后面的[]要加,否则会报错元组类型不能copy,下面再对每个块进行轮廓检测、绘制
digitCnts,hierarchy=cv.findContours(group.copy(),cv.RETR_EXTERNAL,cv.CHAIN_APPROX_SIMPLE)#每一个group再进行轮廓检测、绘制
digitCnts=contours.sort_contours(digitCnts,method="left-to-right")[0]
for c in digitCnts:#计算每一组数中的每一个数值
(x,y,w,h)=cv.boundingRect(c)
roi=group[y:y+h,x:x+w]
roi=cv.resize(roi,(57,88))#尺寸需与模板的尺寸对应,得到每一个数字所在的区域
scores=[]#新建一个空列表,用来存储检测到的数字
for (digit,digitROI)in digits.items():#在模板预处理中建立了数值的字典类型,一个为索引、一个为值
result=cv.matchTemplate(roi,digitROI,cv.TM_CCOEFF)#匹配,返回与之匹配度最高的数值
#cv.TM_CCOEFF方法越匹配,返回数值越高,数值越低则代表匹配结果越不理想。所以我们下面需要取最大值
(_,score,_,_)=cv.minMaxLoc(result)#做10次匹配,取最大值(注意:取最大值还是最小值跟选取的模板匹配方法有关)
#4个值分别为 min_val,max_val,min_indx,max_indx 最大值和索引
scores.append(score)
groupOutput.append(str(np.argmax(scores)))
cv.rectangle(image,(gx-5,gy-5),(gx+gw+5,gy+gh+5),(0,0,255),1)#第一组的矩形框
cv.putText(image,"".join(groupOutput),(gx-8,gy-15),cv.FONT_HERSHEY_SIMPLEX,0.65,(0,0,255),2)
output.extend(groupOutput)
print("Credit Card #: {}".format("".join(output)))
cv.imshow("Image",image)
cv.waitKey(0)
cv.destroyAllWindows()
处理过的数字模板:
裁剪的7的区域:
如上图,我们遍历数字的轮廓,这意味着我们可以得到它的最小正外接矩形,坐标也就得到了。那么等值裁切就很简单了。带入左上角坐标加上宽高遍历即可。
顶帽操作:
上图一经过二值化之后,又进行了一次顶帽操作(原图像-开运算)和cv2.Sobel()的横向边缘计算,即x方向的计算。第二张图为y方向的计算。很显然我们应该选择第一张。
容易粘连的地方:
就我个人理解上,这样的好处除了使我们数字轮廓更加明显,另一点就是防止后续进行闭操作时,卡号和左下方的数字距离太近导致粘连。这会使我们的模板无法匹配。同理,若干扰项与我们需要的区域左右相邻,是否可以采用y方向上的运算?
两次闭操作之后:
此时已经具备了获取轮廓的条件。我们画出轮廓。红色区域即是我们捕捉到的轮廓,之前我们一直使用面积去排除干扰项,很显然对本次的情况来说不适用。卡号区域均为四个数字一组,那么我们可以通过计算长宽比的方法来筛选我们的卡号区域。方法还是通过添加轮廓的最小外接正矩形。阈值应根据实际情况来调整。
在获取坐标之后,我们使用sort()对x坐标来排序,完成轮廓从左到右的排序。这是为了确保我们与模板对比时顺序正确。
最终结果:
在我们筛选出区域之后,4个数字为一组,同样是遍历裁剪出单独的区域,前文我们说过,在获取轮廓的最小外接正矩形之后,得到坐标和宽高,等值裁剪就是一件十分容易的事情。之后就是顺序遍历模板进行对比。对比的具体方法,写在了注释里。对比方法有3种,我们按需选择,调整后续的筛选方法。
最后enumerate()方法可能不常用但不复杂,我们对模板进行处理的时候,经过从左到右的排序之后,还需要人工赋值。这个方法可以很方便的帮我们完成。
我们需要注意的就是对如何让轮廓按照我们的需要排序。从左到右?按照坐标从大到小?之前我们对轮廓的顺序排列没有要求,这次我们用到了应认真学习理解。我认为在以后会经常用到。不要觉得知识点零碎,修改参数重复实验繁琐。只有深入理解它们才能面对各自需求灵活使用。
refCnts = [“x”,“y”,“z”,“m”,“n”]
for(i,c)in enumerate(refCnts):
print(i,c)
#其实就是加上了索引
0 x
1 y
2 z
3 m
4 n
我们通过这个方法完成了对轮廓从左到右的排序。
当然还要其他排序的方法,大家百度自行了解哈。
digitCnts=contours.sort_contours(digitCnts,method=“left-to-right”)[0]
嗨呀,来评论区找我玩儿。
好,那么本期文章就到这里结束了,我们下期再见呀。
转载自:穗麦子
阅读原文