今天整理一个OpenCV实践的小项目, 前几天整理了一篇OpenCV处理图像的知识笔记,后面,就通过一些小项目把这些知识运用到实践中去,一个是加深理解,另一个是融会贯通,连成整体,因为我发现,如果这些东西不用的话,其实很快就会忘掉。 另外,就是我发现这些实践小项目非常使用,有些代码或者图像的处理技巧可以为以后所用,所以这也是我想整理下来的原因。
第一个实践项目是信用卡数字识别,就是给定一张信用卡, 做出下面的这种效果:
这个项目用到的知识其实在很多其他场景也会遇到,比如像车牌号识别检测,数字识别等,所以感觉还是比较实用的。 但其实, 本质上用到的知识并不复杂,完全是前面整理的OpenCV基本图像操作,那么究竟是如何做到的那?
下面首先分析这个项目的宏观实现逻辑,也就是拿到这样的一个小任务应该大致上怎么思考,然后给出具体的做法以及代码解释。
给定一个信用卡,最终要输出上面的卡号,且需要在原图中把卡号的位置圈出来。 本质上,这是一个模板匹配任务,如果想让计算机认识数字,我们需要给定一个模板,比如下面这个:
这样, 我们只要找到信用卡上的数字区域,然后拿着数字区域的数字一一与模板进行匹配,看看到底是啥数字,就能识别出来了。 但是,对于信用卡来说我们需要找到它的数字区域呀,对于给定的模板,我们虽然有它的数字区域,但是也得分割成一个个的数字,才能进行匹配工作呀,所以该任务,就转成了处理信用卡, 处理模板以及模板匹配三个子问题。 、
想起了小学学过的一篇课文《走一步,再走一步》。
如何处理信用卡,找到数字区域呢? 大致上思路如下:
如果处理模板呢?这个很简单。轮廓检测一次,就能找到这10个对象,然后给每个对象赋予值,然后建立成一个字典即可。
下面就一步一步的进行代码解释。
模板图像先进行三步操作: 读入 -> 转成灰度图 -> 二值化
, 因为轮廓检测函数接收的是二值图。
# 读取模板图像
img = cv2.imread("images/ocr_a_reference.png") # 读取的时候转灰度 cv2.imread("images/ocr_a_reference.png", 0)
# 转成灰度图
template = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 二值图像
template = cv2.threshold(template, 10, 255, cv2.THRESH_BINARY_INV)[1]
接下来,用cv2的轮廓检测函数拿到10个数字的轮廓
cv2.findContours()
函数接受的参数为二值图, 即黑白图像(不是灰度图),cv2.RETR_EXTERNAL
只检测外轮廓,cv2.CHAIN_APPROX_SIMPLE
只保留终点坐标
# 最新版opencv只返回两个值了 3.2之后, 不会返回原来的二值图像了,直接返回轮廓信息和层级信息
contourss, hierarchy = cv2.findContours(template.copy(), cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
len(contourss) # 10个轮廓
效果如下:
这样就找到了每个数字的外轮廓,一个10个,但是要注意下,这10个轮廓的排列顺序并不一定是按照上面这个0-9的轮廓对应着来的,所以为了保险起见,我们需要根据每个轮廓左上角的坐标值,先从小到大排序。
# 下面将轮廓进行排序,这是因为必须保证轮廓的顺序是0-9的顺序排列着
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
# 根据每个轮廓左上角的点进行排序, 这样能保证轮廓的顺序就是0-9的数字排列顺序
(cnts, boundingBoxes) = zip(*sorted(zip(cnts, boundingBoxes), key=lambda x:x[1][i], reverse=reverse))
return cnts, boundingBoxes
refCnts = sort_contours(contourss, method='left-to-right')[0]
这样每个轮廓就按照0-9排列好了, 那下面思路就很清晰了,遍历每个轮廓对象,给他附上真正的数字即可,即建立数字->轮廓
的关联映射。
# 每个轮廓进行数字编号
digits2Cnt = {}
# 遍历每个轮廓
for i, c in enumerate(refCnts):
# 计算外接矩形,并且resize成合适大小
(x, y, w, h) = cv2.boundingRect(c)
# 单独把每个数字框拿出来 坐标系竖着的是y, 横着的是x
roi = template[y:y+h, x:x+w]
# 重新改变大小
roi = cv2.resize(roi, (57, 88))
# 框与字典对应
digits2Cnt[i] = roi
# 把处理好的模板进行保存
pickle.dump(digits2Cnt, open('digits2Cnt.pkl', 'wb'))
这里有两个点,首先对于每个轮廓,先计算它的外接矩形,也就是先框起来,然后从原始的模板图像中,拿出这个框,这才是每个数字。 然后为了能和后面信用卡上的数字进行匹配,这里还需要resize下。
这样模板图像处理完毕,拿到了ditits2Cnt
字典,字典的键就是数字值,而值就是模板中的轮廓对象。
信用卡这部分要稍微复杂一些,因为我们还得先定位到信用卡上的数字区域,然后通过一些操作对这块区域增强等。
第一步,读取图像,改变大小,并转成灰度图。
# 读取图像
base_path = 'images'
file_name = 'credit_card_01.png'
credit_card = cv2.imread(os.path.join(base_path, file_name))
credit_card = resize(credit_card, width=300)
credit_gray = cv2.cvtColor(credit_card, cv2.COLOR_BGR2GRAY)
效果如下:
接下来,进行顶帽操作, 这个操作可以突出更加明亮的区域,而黑帽操作可以突出更加黑暗的区域。
# 顶帽操作,突出更明亮的区域
# 初始化卷积核
rectKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 3)) # 自定义卷积核的大小了
sqKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5))
tophat = cv2.morphologyEx(credit_gray, cv2.MORPH_TOPHAT, rectKernel)
效果如下:
接下来,要进行边缘检测, 把上面的各个对象的边缘给他突出出来。边缘检测那里我们学习了水平边缘检测,垂直边缘检测,以及两者的合并操作,往往效果较好。 但这里发现单独水平边缘检测就可以。
# 水平边缘检测
gradX = cv2.Sobel(tophat, ddepth=cv2.CV_32F, dx=1, dy=0, ksize=-1) # 水平边缘检测
# gradX = cv2.convertScaleAbs(gradX) 这个操作会把一些背景边缘也给检测出来,加了一些噪声
# 所以下面手动归一化操作
gradX = np.absolute(gradX)
(minVal, maxVal) = (np.min(gradX), np.max(gradX))
gradX = (255 * ((gradX-minVal) / (maxVal-minVal)))
gradX = gradX.astype('uint8')
# 这里也可以按照之前的常规, 先水平,后垂直,然后合并,但是效果可能不如单独x的效果好
效果如下:
目前确实能找到边缘了,但是想把数字挨着近的连接成片,就需要用到形态学相关操作了。
# 闭操作: 先膨胀, 后腐蚀 膨胀就能连成一块了
gradX = cv2.morphologyEx(gradX, cv2.MORPH_CLOSE, rectKernel)
效果如下:
然后会发现,数字虽然大部分连成一块一块的了,但是有些地方有些黑洞,且颜色还不是特别命令和明显,所以下面转成二值图片,突出对象,阈值+闭操作增强。
#THRESH_OTSU会自动寻找合适的阈值,适合双峰,需把阈值参数设置为0 让opencv自动的去做判断,找合适的阈值,这样就能自动找出哪些有用,哪些没用
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) #再来一个闭操作
效果如下:
接下来的话,就能很容易的通过轮廓检测算法找到轮廓,但是如果想拿到数字的轮廓,这里还需要根据长宽比例进行筛选。
threshCnts, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = threshCnts
cur_img = credit_card.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])
这里的操作依然是先用外接矩形包起对象,然后再进行选择。这样就拿到了四个大轮廓。
接下来就非常简单了:
遍历每个大轮廓
outputs = []
# 遍历每一个轮廓中的的数字
for (i, (gX, gY, gW, gH)) in enumerate(locs):
# 初始化组
groupOutput = []
# 根据坐标提取每一组
group = credit_gray[gY-5:gY+gH+5, gX-5:gX+gW+5] # 有5的一个容错长度
# 对于这每一组,先预处理
# 二值化,自动寻找合适阈值,增强对比,更突出有用的部分,即数字
group = cv2.threshold(group, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
# 计算每一组的轮廓
digitCnts, hierarchy = cv2.findContours(group.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
digitCnts = 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))
# 模板匹配
scores = []
for (digit, digitROI) in digits2Cnt.items():
result = cv2.matchTemplate(roi, digitROI, cv2.TM_CCOEFF)
(_, score, _, _) = cv2.minMaxLoc(result)
scores.append(score)
# 得到合适的数字
# 这是个列表,存储的每个小组里面的数字识别结果
groupOutput.append(str(np.argmax(scores)))
# 画出来
cv2.rectangle(credit_card, (gX - 5, gY - 5), (gX + gW + 5, gY + gH + 5), (0, 0, 255), 1)
cv2.putText(credit_card, "".join(groupOutput), (gX, gY - 15), cv2.FONT_HERSHEY_SIMPLEX, 0.65, (0, 0, 255), 2)
# 合并到最后的结果里面
outputs.extend(groupOutput)
输出结果
# 打印结果
print("Credit Card Type: {}".format(FIRST_NUMBER[outputs[0]]))
print("Credit Card #: {}".format("".join(outputs)))
cv2.imshow("Image", credit_card)
这个项目到这里就结束了,整体比较简单,但是这里面涉及到的很多知识点都比较常用。总结如下:
图像的读取 ->转灰度->二值化
操作cv2.findContours
)当然,并没有涉及到很复杂的逻辑,全是Opencv的基础函数以及python基础操作,算是图像处理的一个小入门项目吧。
本次项目代码地址https://github.com/zhongqiangwu960812/OpenCVLearning, 感兴趣的可以玩一下啦。