这篇文章是一个系列中的第三篇。前两篇的地址贴下:介绍、详解1。我撰写这系列文章的目的是:1、普及车牌识别中相关的技术与知识点;2、帮助开发者了解EasyPR的实现细节;3、增进沟通。
EasyPR的项目地址在这:GitHub。要想运行EasyPR的程序,首先必须配置好openCV,具体可以参照这篇文章。
在前两篇文章中,我们已经初步了解了EasyPR的大概内容,在本篇内容中我们开始深入EasyRP的程序细节。了解EasyPR是如何一步一步实现一个车牌的识别过程的。
根据EasyPR的结构,我们把它分为六个部分,前三个部分统称为“Plate Detect”过程。主要目的是在一副图片中发现仅包含车牌的图块,以此提高整体识别的准确率与速度。这个过程非常重要,如果这步失败了,后面的字符识别过程就别想了。而“Plate Detect”过程中的三个部分又分别称之为“Plate Locate” ,“SVM train”,“Plate judge”,其中最重要的部分是第一步“Plate Locate”过程。本篇文章中就是主要介绍“Plate Locate”过程,并且回答以下三个问题:
1.此过程的作用是什么,为什么重要?
2.此过程是如何实现车牌定位这个功能的?
3.此过程中的细节是什么,如何进行调优?
1.“Plate Locate”的作用与重要性
在说明“Plate Locate”的作用与重要性之前,请看下面这两幅图片。
图1 两幅包含车牌的不同形式图片
左边的图片是作者训练的图片(作者大部分的训练与测试都是基于此类交通抓拍图片),右边的图片则是在百度图片中“车牌”获得(这个图片也可以称之为生活照片)。右边图片的问题是一个网友评论时问的。他说EasyPR在处理百度图片时的识别率不高。确实如此,由于工业与生活应用目的不同,拍摄的车牌的大小,角度,色泽,清晰度不一样。而对图像处理技术而言,一些算法对于图像的形式以及结构都有一定的要求或者假设。因此在一个场景下适应的算法并不适用其他场景。目前EasyPR所有的功能都是基于交通抓拍场景的图片制作的,因此也就导致了其无法处理生活场景中这些车牌照片。
那么是否可以用一致的“Plate Locate”过程中去处理它?答案是也许可以,但是很难,而且最后即便处理成功,效率也许也不尽如人意。我的推荐是:对于不同的场景要做不同的适配。尽管“Plate Locate”过程无法处理生活照片的定位,但是在后面的字符识别过程中两者是通用的。可以对EasyPR的“Plate Locate”做改造,同时仍然使用整体架构,这样或许可以处理。
有一点事实值得了解到是,在生产环境中,你所面对的图片形式是固定的,例如左边的图片。你可以根据特定的图片形式来调优你的车牌程序,使你的程序对这类图片足够健壮,效率也够高。在上线以后,也有很好的效果。但当图片形式调整时,就必须要调整你的算法了。在“Plate Locate”过程中,有一些参数可以调整。如果通过调整这些参数就可以使程序良好工作,那最好不过。当这些参数也不能够满足需求时,就需要完全修改EasyPR的实现代码,因此需要开发者了解EasyPR是如何实现plateLocate这一过程的。
在EasyPR中,“Plate Locate”过程被封装成了一个“CPlateLocate”类,通过“plate_locate.h”声明,在“plate_locate.cpp”中实现。
CPlateLocate包含三个方法以及数个变量。方法提供了车牌定位的主要功能,变量则提供了可定制的参数,有些参数对于车牌定位的效果有非常明显的影响,例如高斯模糊半径、Sobel算子的水平与垂直方向权值、闭操作的矩形宽度。CPlateLocate类的声明如下:
class CPlateLocate { public: CPlateLocate(); //! 车牌定位 int plateLocate(Mat, vector<Mat>& ); //! 车牌的尺寸验证 bool verifySizes(RotatedRect mr); //! 结果车牌显示 Mat showResultMat(Mat src, Size rect_size, Point2f center); //! 设置与读取变量 //... protected: //! 高斯模糊所用变量 int m_GaussianBlurSize; //! 连接操作所用变量 int m_MorphSizeWidth; int m_MorphSizeHeight; //! verifySize所用变量 float m_error; float m_aspect; int m_verifyMin; int m_verifyMax; //! 角度判断所用变量 int m_angle; //! 是否开启调试模式,0关闭,非0开启 int m_debug; };
注意,所有EasyPR中的类都声明在命名空间easypr内,这里没有列出。CPlateLocate中最核心的方法是plateLocate方法。它的声明如下:
//! 车牌定位 int plateLocate(Mat, vector<Mat>& );
方法有两个参数,第一个参数代表输入的源图像,第二个参数是输出数组,代表所有检索到的车牌图块。返回值为int型,0代表成功,其他代表失败。plateLocate内部是如何实现的,让我们再深入下看看。
2.“Plate Locate”的实现过程
plateLocate过程基本参考了taotao1233的博客的处理流程,但略有不同。
plateLocate的总体识别思路是:如果我们的车牌没有大的旋转或变形,那么其中必然包括很多垂直边缘(这些垂直边缘往往缘由车牌中的字符),如果能够找到一个包含很多垂直边缘的矩形块,那么有很大的可能性它就是车牌。
依照这个思路我们可以设计一个车牌定位的流程。设计好后,再根据实际效果进行调优。下面的流程是经过多次调整与尝试后得出的,包含了数月来作者针对测试图片集的一个最佳过程(这个流程并不一定适用所有情况)。plateLocate的实现代码在这里不贴了,Git上有所有源码。plateLocate主要处理流程图如下:
图2 plateLocate流程图
下面会一步一步参照上面的流程图,给出每个步骤的中间临时图片。这些图片可以在1.01版的CPlateLocate中设置如下代码开启调试模式。
CPlateLocate plate; plate.setDebug(1);
临时图片会生成在tmp文件夹下。对多个车牌图片处理的结果仅会保留最后一个车牌图片的临时图片。
1、原始图片。
2、经过高斯模糊后的图片。经过这步处理,可以看出图像变的模糊了。这步的作用是为接下来的Sobel算子去除干扰的噪声。
3、将图像进行灰度化。这个步骤是一个分水岭,意味着后面的所有操作都不能基于色彩信息了。此步骤是利是弊,后面再做分析。
4、对图像进行Sobel运算,得到的是图像的一阶水平方向导数。这步过后,车牌被明显的区分出来。
5、对图像进行二值化。将灰度图像(每个像素点有256个取值可能)转化为二值图像(每个像素点仅有1和0两个取值可能)。
6、使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域。
7、求轮廓。求出图中所有的轮廓。这个算法会把全图的轮廓都计算出来,因此要进行筛选。
8、筛选。对轮廓求最小外接矩形,然后验证,不满足条件的淘汰。经过这步,仅仅只有六个黄色边框的矩形通过了筛选。
8、角度判断与旋转。把倾斜角度大于阈值(如正负30度)的矩形舍弃。左边第一、二、四个矩形被舍弃了。余下的矩形进行微小的旋转,使其水平。
10、统一尺寸。上步得到的图块尺寸是不一样的。为了进入机器学习模型,需要统一尺寸。统一尺寸的标准宽度是136,长度是36。这个标准是对千个测试车牌平均后得出的通用值。下图为最终的三个候选”车牌“图块。
这些“车牌”有两个作用:一、积累下来作为支持向量机(SVM)模型的训练集,以此训练出一个车牌判断模型;二、在实际的车牌检测过程中,将这些候选“车牌”交由训练好的车牌判断模型进行判断。如果车牌判断模型认为这是车牌的话就进入下一步即字符识别过程,如果不是,则舍弃。
3.“Plate Locate”的深入讨论与调优策略
好了,说了这么多,读者想必对整个“Plate Locate”过程已经有了一个完整的认识。那么让我们一步步审核一下处理流程中的每一个步骤。回答下面三个问题:这个步骤的作用是什么?省略这步或者替换这步可不可以?这个步骤中是否有参数可以调优的?通过这几个问题可以帮助我们更好的理解车牌定位功能,并且便于自己做修改、定制。
由于篇幅关系,下面的深入讨论仅有一半,剩余的一半放在下期。
1、高斯模糊
在我们的车牌定位中的第一步就是高斯模糊处理。详细说明可以看这篇文章。
高斯模糊是非常有名的一种图像处理技术。顾名思义,其一般应用是将图像变得模糊,但同时高斯模糊也应用在图像的预处理阶段。理解高斯模糊前,先看一下平均模糊算法。平均模糊的算法非常简单。见下图,每一个像素的值都取周围所有像素(共8个)的平均值。
图3 平均模糊示意图
在上图中,左边红色点的像素值本来是2,经过模糊后,就成了1(取周围所有像素的均值)。在平均模糊中,周围像素的权值都是一样的,都是1。如果周围像素的权值不一样,并且与二维的高斯分布的值一样,那么就叫做高斯模糊。
在上面的模糊过程中,每个像素取的是周围一圈的平均值,也称为模糊半径为1。如果取周围三圈,则称之为半径为3。半径增大的话,会更加深模糊的效果。
在PlateLocate中是这样调用高斯模糊的。
//高斯模糊。Size中的数字影响车牌定位的效果。 GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize), 0, 0, BORDER_DEFAULT );
其中Size字段的参数指定了高斯模糊的半径。值是CPlateLocate类的m_GaussianBlurSize变量。由于opencv的高斯模糊仅接收奇数的半径,因此变量为偶数值会抛出异常。
这里给出了opencv的高斯模糊的API(英文,2.48以上版本)。
高斯模糊这个过程一定是必要的么。笔者的回答是必要的,倘若我们将这句代码注释并稍作修改,重新运行一下。你会发现plateLocate过程在闭操作时就和原来发生了变化。最后结果如下。
图4 不采用高斯模糊后的结果
可以看出,车牌所在的矩形产生了偏斜。最后得到的候选“车牌”图块如下:
图5 不采用高斯模糊后的“车牌”图块
由于去除了高斯模糊过程,我们得到的候选“车牌”达到了8个!这样不仅会增加车牌判断的处理时间,还增加了判断出错的概率。由于得到的车牌图块中车牌是斜着的,如果我们的字符识别算法需要一个水平的车牌图块,那么几乎肯定我们会无法得到正确的字符识别效果。
高斯模糊中的半径也会给结果带来明显的变化。有的图片,高斯模糊半径过高了,车牌就定位不出来。有的图片,高斯模糊半径偏低了,车牌也定位不出来。因此、高斯模糊的半径既不宜过高,也不能过低。CPlateLocate类中的值为5的静态常量DEFAULT_GAUSSIANBLUR_SIZE,标示着推荐的高斯模糊的半径。这个值是对于近千张图片经过测试后得出的综合定位率最高的一个值。在CPlateLocate类的构造函数中,m_GaussianBlurSize被赋予了DEFAULT_GAUSSIANBLUR_SIZE的值,因此,默认的高斯模糊的半径就是5。如果不是特殊情况,不需要修改它。
在数次的实验以后,必须承认,保留高斯模糊过程与半径值为5是最佳的实践。为应对特殊需求,在CPlateLocate类中也应该提供了方法修改高斯半径的值,调用代码(假设需要一个为3的高斯模糊半径)如下:
CPlateLocate plate; plate.setGaussianBlurSize(3);
目前EasyPR的处理步骤是先进行高斯模糊,再进行灰度化。从目前的实验结果来看,基于色彩的高斯模糊过程比灰度后的高斯模糊过程更容易检测到边缘点。
2、灰度化处理
在灰度化处理步骤中,争议最大的就是信息的损失。无疑的,原先plateLocate过程面对的图片是彩色图片,而从这一步以后,就会面对的是灰度图片。在前面,已经说过这步骤是利是弊是需要讨论的。
无疑,对于计算机而言,色彩图像相对于灰度图像难处理多了,很多图像处理算法仅仅只适用于灰度图像,例如后面提到的Sobel算子。在这种情况下,你除了把图片转成灰度图像再进行处理别无它法,除非重新设计算法。但另一方面,转化成灰度图像后恰恰失去了最丰富的细节。要知道,真实世界是彩色的,人类对于事物的辨别是基于彩色的框架。甚至可以这样说,因为我们的肉眼能够区别彩色,所以我们对于事物的区分,辨别,记忆的能力就非常的强。
车牌定位环节中去掉彩色的利弊也是同理。转换成灰度图像虽然利于使用各种专用的算法,但失去了真实世界中辨别的最重要工具---色彩的区分。举个简单的例子,人怎么在一张图片中找到车牌?非常简单,一眼望去,一个合适大小的矩形,蓝色的、或者黄色的、或者其他颜色的在另一个黑色,或者白色的大的跟车形类似的矩形中。这个过程非常直观,明显,而且可以排除模糊,色泽,不清楚等很多影响。如果使用灰度图像,就必须借助水平,垂直求导等方法。
未来如果PlateLocate过程可以使用颜色来判断,可能会比现在的定位更清楚、准确。但这需要研究与实验过程,在EasyPR的未来版本中可能会实现。但无疑,使用色彩判断是一种趋势,因为它不仅符合人眼识别的规律,更趋近于人工智能的本质,而且它更准确,速度更快。
在PlateLocate过程中是这样调用灰度化的。
cvtColor( src_blur, src_gray, CV_RGB2GRAY );
这里给出了opencv的灰度化的API(英文,2.48以上版本)。
3.Sobel算子
如果要说哪个步骤是plateLocate中的核心与灵魂,毫无疑问是Sobel算子。没有Sobel算子,也就没有垂直边缘的检测,也就无法得到车牌的可能位置,也就没有后面的一系列的车牌判断、字符识别过程。通过Sobel算子,可以很方便的得到车牌的一个相对准确的位置,为我们的后续处理打好坚实的基础。在上面的plateLocate的执行过程中可以看到,正是通过Sobel算子,将车牌中的字符与车的背景明显区分开来,为后面的二值化与闭操作打下了基础。那么Sobel算子是如何运作的呢?
Soble算子原理是对图像求一阶的水平与垂直方向导数,根据导数值的大小来判断是否是边缘。请详见优快云小魏的博客(小心她博客里把Gx和Gy弄反了)。
为了计算方便,Soble算子并没有真正去求导,而是使用了周边值的加权和的方法,学术上称作“卷积”。权值称为“卷积模板”。例如下图左边就是Sobel的Gx卷积模板(计算垂直边缘),中间是原图像,右边是经过卷积模板后的新图像。
图6 Sobel算子Gx示意图
在这里演示了通过卷积模板,原始图像红色的像素点原本是5的值,经过卷积计算(- 1 * 3 - 2 * 3 - 1 * 4 + 1 * 5 + 2 * 7 + 1 * 6 = 12)后红色像素的值变成了12。
在代码中调用Soble算子需要较多的步骤。
/// Generate grad_x and grad_y Mat grad_x, grad_y; Mat abs_grad_x, abs_grad_y; /// Gradient X //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_x, abs_grad_x ); /// Gradient Y //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT ); Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT ); convertScaleAbs( grad_y, abs_grad_y ); /// Total Gradient (approximate) addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad );
这里给出了opencv的Sobel的API(英文,2.48以上版本)
在调用参数中有两个常量SOBEL_X_WEIGHT与SOBEL_Y_WEIGHT代表水平方向和垂直方向的权值,默认前者是1,后者是0,代表仅仅做水平方向求导,而不做垂直方向求导。这样做的意义是,如果我们做了垂直方向求导,会检测出很多水平边缘。车身前端太多的水平边缘了,例如车头排气孔,标志等等,会干扰后面的闭操作。因此在一般的实践中,仅仅做水平方向的求导。
plateLocate过程的深入分析后面还有闭操作,取轮廓和旋转等复杂操作。在本期由于篇幅的限制,放到下期再进行说明。
(待续....)
备注说明:
EasyPR目前仍然存在许多问题,需要不断完善,大致如下:
1.精确度以及鲁棒性仍然不够理想,目前针对工业图片,大致在90%左右。
2.中文汉字训练集不够,对江浙一带省份汉字识别率还行,其他省份较弱。
3.无法适用于生活图片(例如上面讨论的问题)。
4.缺乏多种不同语言的接口与示例。
5.没有移动(Android)版本。
因为问题较多,因此开发者在使用的时候,直接拿来用碰到的问题较多,可能需要不少调整。但我认为EasyPR会越来越完善,开源软件的优化与维护是一个长期的工作,需要众人齐力的不断完善才能变好。如果有志同道合者,原意助力EasyPR的完善,请跟我联系。我希望能够组建一个EasyPR的改进团队(2-4人左右)。如果有数据提供者,非常感谢,你们对EasyPR的精确与鲁棒性的提升起着至关重要的作用,你们的贡献将会被列入EasyPR的重要贡献者名单里。Thank you。
版权说明:
本文中的所有文字,图片,代码的版权都是属于作者和博客园共同所有。欢迎转载,但是务必注明作者与引用来源。任何未经允许的剽窃以及爬虫抓取都属于侵权,作者和博客园保留所有权利。
参考文献:
1.http://my.phirobot.com/blog/2014-02-opencv_configuration_in_vs.html
2.http://blog.youkuaiyun.com/jinshengtao/article/details/17883075
3.http://www.ruanyifeng.com/blog/2012/11/gaussian_blur.html
4.http://blog.youkuaiyun.com/xiaowei_cqu/article/details/7829481
原文地址:http://www.cnblogs.com/subconscious/p/4013591.html