------韦访 20181012
1、概述
上一讲学习了opencv的一些基本的知识,但是,不玩几个实例是学不会的,所以就从opencv比较热门的车牌识别开始,继续学习。
2、车牌识别的步骤
一般车牌识别分为4步:图像获取、车牌定位、车牌字符分割和车牌字符识别。
图像获取:你要识别车牌,至少得有包含车牌的图片吧?
车牌定位:一般图像获取的图片不可能只有一张完整的车牌,而没有其他背景的,如下图,
我们的把车牌单独准确的分割出来,如下图,
这样才可以进行下一步操作。
车牌字符分割:上面的步骤是将车牌单独提取出来了,但是让机器一下子就识别出来还是比较有难度的,一般做法是将字符单独分割出来,再单独识别。像MNIST那样识别。
车牌字符识别:车牌字符分割成单独的字符以后,最后一步就是识别这个字符了。
3、车牌定位
图像的获取就不是我们要讲的内容了,这里先讲车牌的定位。车牌定位有很多中方法,我们这里用边缘检测分割的方法。
Candy边缘检测
上一讲中有讲过边缘检测,直接用就好,
import cv2
img = cv2.imread('car.jpg', flags=cv2.IMREAD_GRAYSCALE)
GBlur = cv2.GaussianBlur(img, (3, 3), 0)
canny = cv2.Canny(GBlur, 50, 150)
cv2.imshow('canny', canny)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
形态学操作----膨胀腐蚀处理
膨胀腐蚀处理,就是通过膨胀(变胖)连接相近的区域,腐蚀(变瘦)则去除孤立细小的色块。希望将车牌的所有字符连接起来,这样我们就可以通过轮廓识别来分割车牌区域。先介绍腐蚀和膨胀的原理。
腐蚀:腐蚀会把图片“变瘦”,其原理是在原图的小区域内取局部最小值,对于二值化图,只有0和255(非黑即白),所以该小区域内,只要有一个像素是0,则该像素点就是0,这样原图中边缘的地方就变成0,示意图如下,
Opencv中用erode函数进行腐蚀,需要我们指定核的大小。核也称卷积矩阵,它对一个区域的像素做mix up或卷积运算,它决定如何通过临近像素点来计算新的像素点。核的结构可以是矩形、椭圆、十字形。可以用cv2.getStructuringElement来生成不同形状的结构元素。比如,
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 矩形结构 kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 椭圆结构 kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 十字形结构
膨胀:膨胀跟腐蚀刚好相反,取局部最大值,把图片“变胖”,使用cv2.dilate函数。
(以上知识点参考博客:https://www.jianshu.com/p/05ef50ac89ac)
来看膨胀腐蚀后,图像变成什么样?上代码,
#encoding:utf-8
import cv2
#将图片转为灰度图像
img = cv2.imread('car.jpg', cv2.COLOR_RGB2GRAY)
#将原图做个备份
sourceImage = img.copy()
#高斯模糊滤波器对图像进行模糊处理
img = cv2.GaussianBlur(img, (3, 3), 0)
#canny边缘检测
img = cv2.Canny(img, 500, 200, 3)
cv2.imshow('Canny', img)
#指定核大小,如果效果不佳,可以试着将核调大
kernelX = cv2.getStructuringElement(cv2.MORPH_RECT, (29, 1))
kernelY = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 19))
#对图像进行膨胀腐蚀处理
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelX, anchor=(-1, -1), iterations=4)
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelY, anchor=(-1, -1), iterations=1)
img = cv2.dilate(img, kernelY, anchor=(-1, -1), iterations=2)
#再对图像进行模糊处理
img = cv2.medianBlur(img, 15)
img = cv2.medianBlur(img, 15)
cv2.imshow('dilate&erode', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
可以看到,经过处理后,只剩下两个白色区域,那么怎么区分哪个是车牌?
百度了一下,小型汽车号牌尺寸为440x220,那么,我们就可以根据宽高比和轮廓检测找出车牌的位置,代码如下,
#encoding:utf-8
import cv2
#将图片转为灰度图像
img = cv2.imread('car.jpg', cv2.COLOR_RGB2GRAY)
#将原图做个备份
sourceImage = img.copy()
#高斯模糊滤波器对图像进行模糊处理
img = cv2.GaussianBlur(img, (3, 3), 0)
#canny边缘检测
img = cv2.Canny(img, 500, 200, 3)
cv2.imshow('Canny', img)
#指定核大小,如果效果不佳,可以试着将核调大
kernelX = cv2.getStructuringElement(cv2.MORPH_RECT, (29, 1))
kernelY = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 19))
#对图像进行膨胀腐蚀处理
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelX, anchor=(-1, -1), iterations=4)
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelY, anchor=(-1, -1), iterations=1)
img = cv2.dilate(img, kernelY, anchor=(-1, -1), iterations=2)
#再对图像进行模糊处理
img = cv2.medianBlur(img, 15)
img = cv2.medianBlur(img, 15)
cv2.imshow('dilate&erode', img)
#检测轮廓,
#输入的三个参数分别为:输入图像、层次类型、轮廓逼近方法
#因为这个函数会修改输入图像,所以上面的步骤使用copy函数将原图像做一份拷贝,再处理
#返回的三个返回值分别为:修改后的图像、图轮廓、层次
image, contours, hier = cv2.findContours(img.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# 边界框
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
print('w' + str(w))
print('h' + str(h))
print(float(w)/h)
print('------')
#由于国内普通小车车牌的宽高比为3.14,所以,近似的认为,只要宽高比大于2.2且小于4的则认为是车牌
if float(w)/h >= 2.2 and float(w)/h <= 4.0:
#将车牌从原图中切割出来
lpImage = sourceImage[y:y+h, x:x+w]
if 'lpImage' not in dir():
print('未检测到车牌!')
cv2.waitKey(0)
cv2.destroyAllWindows()
exit()
cv2.imshow('img', lpImage)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果,
刚好把车牌分割出来,多试几张图片看看,
可以看到,这么模型并没有百分百的定位到车牌,有些膨胀腐蚀后,车牌没了,有些则是车牌跟车体连成一体了导致无法识别。其实还可以结合识别车牌背景颜色来优化模型的,车牌颜色就那几个,这里就先不涉及优化的内容了。
4、字符分割
上面只是将车牌定位出来了,我们的目的是识别,但目前还不能直接识别出车牌里面的字符,要先对字符进行分割,然后再一个字符一个字符的识别。字符分割的方法有很多种,比如,
按比例分割:车牌样式是固定的,字符的大小也是固定的,就可以直接根据比例来分割出字符来,但是这样对上一步的要求就比较高,要求切割出来的车牌必须的方方正正的,要不然效果可能不如意。
轮廓检测分割:这个方法比较简单,如果车牌比较清晰的话,这个方法还是可以的,但是,在识别一些中文字符时,有可能一个中文字符被识别成多个轮廓,如下图,这个就需要处理一下,对于不是很清晰的车牌,它识别的效果也不太如意。
像素值分割:这个方法简单暴力,根据水平和垂直映射像素来分割。先将车牌进行边缘检测、二值化,然后判断每一列的像素值大于0(白色)的个数超过一个阈值(比如5)时,就认为该列有字符。
下面,我们就使用轮廓检测分割和像素值分割来讲解。
轮廓检测分割
前面说过,我们之所以能识别物体,是因为“识别”的是物体的边缘。图像识别不需要彩色的信息,所以,为了简单化,先将分割出来的车牌图像进行边缘检测和二值化处理,得到的图像是二值化的。代码如下,
#边缘检测
lpImage = cv2.Canny(lpImage, 500, 200, 3)
#对图像进行二值化操作
ret, thresh = cv2.threshold(lpImage.copy(), 127, 255, cv2.THRESH_BINARY)
cv2.imshow('img', thresh)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
完整代码如下:
#encoding:utf-8
import cv2
import numpy as np
#将图片转为灰度图像
img = cv2.imread('11.jpg', cv2.COLOR_RGB2GRAY)
#将原图做个备份
sourceImage = img.copy()
cv2.imshow('sourceImage', img)
#高斯模糊滤波器对图像进行模糊处理
img = cv2.GaussianBlur(img, (3, 3), 0)
#canny边缘检测
img = cv2.Canny(img, 500, 200, 3)
#指定核大小,如果效果不佳,可以试着将核调大
kernelX = cv2.getStructuringElement(cv2.MORPH_RECT, (29, 1))
kernelY = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 19))
#对图像进行膨胀腐蚀处理
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelX, anchor=(-1, -1), iterations=4)
img = cv2.dilate(img, kernelX, anchor=(-1, -1), iterations=2)
img = cv2.erode(img, kernelY, anchor=(-1, -1), iterations=1)
img = cv2.dilate(img, kernelY, anchor=(-1, -1), iterations=2)
#再对图像进行模糊处理
img = cv2.medianBlur(img, 15)
img = cv2.medianBlur(img, 15)
# cv2.imshow('dilate&erode', img)
#检测轮廓,
#输入的三个参数分别为:输入图像、层次类型、轮廓逼近方法
#因为这个函数会修改输入图像,所以上面的步骤使用copy函数将原图像做一份拷贝,再处理
#返回的三个返回值分别为:修改后的图像、图轮廓、层次
image, contours, hier = cv2.findContours(img.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
for c in contours:
# 边界框
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(img, (x, y), (x + w, y + h), (255, 0, 0), 2)
print('w' + str(w))
print('h' + str(h))
print(float(w)/h)
print('------')
#由于国内普通小车车牌的宽高比为3.14,所以,近似的认为,只要宽高比大于2.2且小于4的则认为是车牌
if float(w)/h >= 2.2 and float(w)/h <= 4.0:
#将车牌从原图中切割出来
lpImage = sourceImage[y:y+h, x:x+w]
if 'lpImage' not in dir():
print('未检测到车牌!')
cv2.waitKey(0)
cv2.destroyAllWindows()
exit()
cv2.imshow('chepai', lpImage)
#边缘检测
lpImage = cv2.Canny(lpImage, 500, 200, 3)
#对图像进行二值化操作
ret, thresh = cv2.threshold(lpImage.copy(), 127, 255, cv2.THRESH_BINARY)
#轮廓检测
image, contours, hier = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
i = 0
lpchars = []
for c in contours:
# 边界框
x, y, w, h = cv2.boundingRect(c)
cv2.rectangle(thresh, (x, y), (x + w, y + h), (255, 0, 0), 2)
print('w' + str(w))
print('h' + str(h))
print(float(w)/h)
print(str(0.8 * thresh.shape[0]))
print('------')
#根据比例和高判断轮廓是否字符
if float(w)/h >= 0.3 and float(w)/h <= 0.8 and h >= 0.6 * thresh.shape[0]:
#将车牌从原图中切割出来
lpImage2 = lpImage[y:y+h, x:x+w]
cv2.imshow(str(i), lpImage2)
i += 1
lpchars.append([x, y, w, h])
cv2.imshow('sdd', thresh)
if len(lpchars) < 1:
print('未检测到字符!')
cv2.waitKey(0)
cv2.destroyAllWindows()
exit()
lpchars = np.array(lpchars)
#对x坐标升序,这样,字符顺序就是对的了
lpchars = lpchars[lpchars[:,0].argsort()]
print(lpchars)
#如果识别的字符小于7,说明汉字没识别出来,要单独识别汉字
if len(lpchars) < 7:
aveWidth = 0
aveHeight = 0
aveY = 0
for index in lpchars:
aveY += index[1]
aveWidth += index[2]
aveHeight += index[3]
aveY = aveY/len(lpchars)
aveWidth = aveWidth/len(lpchars)
aveHeight = aveHeight/len(lpchars)
zhCharX = lpchars[0][0] - (lpchars[len(lpchars) - 1][0] - lpchars[0][0]) / (len(lpchars) - 1)
if zhCharX < 0:
zhCharX = 0
print(aveWidth)
print(aveHeight)
print(zhCharX)
print(aveY)
cv2.imshow('img', lpImage[aveY:aveY + aveHeight, zhCharX:zhCharX + aveWidth])
cv2.waitKey(0)
cv2.destroyAllWindows()
运行结果:
对于这个车牌的检测效果还是可以的,我们再试试其他的车牌,
像上面两张车牌,同一张照片的不同角度,第一张车牌笔记大,容易识别,在车牌定位这一步做的很好了,所以在字符分割这一步也很好。而第二张车牌,在车牌定位这一步做的不是很好导致有很多背景噪音的,就无法识别出里面的字符了。由此可见,车牌图像获取这一步也是非常关键的,直接影响到后面的处理结果。
5、字符识别
字符识别?我们Tensorflow入门教程的第一讲的识别MNIST不就是字符识别?直接用Tensorflow训练一下车牌的字符应该是可以拿来用的,但是我们现在既然学的是OpenCV,那么就用OpenCV常用的方法好了,我百度了一下,大概常用的方法有两种,一种是OCR识别,另一张也是神经网络的方法ANN。我还没学,估计篇幅不小,那么,这里就先不做字符识别的功能,后续再补上一讲。
总结:
在用OpenCV做车牌识别的时候,我在想,如果车牌定位和字符识别这步用Tensorflow的方法来做会不会准确率更高呢?如果以后有机会我再试试,主要是车牌的数据不好找,人工标注也需要大量的精力,看着办了。
---------------------------------------------20210103---------------------------------------------
深度学习版车牌识别请看下面的博客:
---------------------------------------------20181020---------------------------------------------
补充说明一下,今天看到一本书说到,对于车牌识别系统,最好是使用红外(IR)摄像头来获取数据,并附一张普通摄像头拍摄到的图像和使用红外摄像头拍摄的图像的对比,