Git地址为:中文车牌识别系统。 EasyPR,也就是Easy to do Plate Recognition的意思。相比于其他的车牌识别系统,EasyPR有如下特点:
系统还提供全套的训练数据提供(包括车牌检测的近500个车牌和字符识别的4000多个字符)。所有全部都可以在Github的项目地址上直接下载到。
EasyPR是如何产生的呢:
我在CSDN的博客上找了一篇文章。文章的作者taotao1233在这两篇博客中以半学习笔记半开发讲解的方式说明了一个车牌识别系统的全部开发过程。非常感谢他的这些博客,借助于这些资料,我着手开始了开发。令人高兴的是,系统确实能够工作,但是让人沮丧的,似乎也就“仅仅”能够工作而已。在车牌检测这个环节中正确性已经惨不忍睹。
于是我决定沉下心来,仔细研究他的系统实现的每一个过程,结合OpenCV的官网教程与API资料,我发现他的实现系统中有很多并不适合我目前在做的场景。我手里的数据大部分是高速上的图像抓拍数据,其中每个车牌都偏小,而且模糊度较差。直接使用他们的方法,正确率低到了可怕的地步。于是我开始尝试利用openCv中的一些函数与功能,替代,增加,调优等等方法,不断的优化。在最终实现的代码中,我的代码已经跟他的原始代码有很多的不一样了,但是成功率大幅度上升,而且车牌的正确检测率不断被优化。
最终我实现的系统与他的系统有以下几点不同:
尽管我和他的系统有这么多的不同,但是我们在根本的系统结构上是一致的。应该说,我们都是参照了“Mastering OpenCV”这本数的处理结构。事实上,结果也证明了“Mastering OpenCV”上的车牌识别的处理逻辑,是一个实际有效的最佳处理流程。
“Mastering OpenCV”,包括我们的系统,都是把车牌识别划分为了两个过程:即车牌检测(Plate Detection)和字符识别(Chars Recognition)两个过程。
图1 完整的EasyPR的处理流程:
EasyPR的原型源于CSDN的taotao1233的一个博客,博主以读书笔记的形式记述了通过阅读“Mastering OpenCV”这本书完成的一个车牌系统的雏形。
这个雏形有几个特点:1.将车牌系统划分为了两个过程,即车牌检测和字符识别。2.整个系统是针对西班牙的车牌开发的,与中文车牌不同。3.系统的训练模型来自于原书。作者基于这个系统,诞生了开发一个适用于中文的,且适合与协作开发的开源车牌系统的想法,也就是EasyPR。
众所皆知,现在是大数据的时代。那么,什么是大数据?可能有些人认为这个只是一个概念或着炒作。但是大数据确是实实在在有着基础理论与科学研究背景的一门技术,其中包含着分布式计算、内存计算、机器学习、计算机视觉、语音识别、自然语言处理等众多计算机界崭新的技术,而且是这些技术综合的产物。事实上,大数据的“大”包含着4个特征,即4V理念,包括Volume(体量)、Varity(多样性)、Velocity(速度)、Value(价值)。
综上,大数据技术不仅包含数据量的大,也包含处理数据的复杂,和处理数据的速度,以及数据中蕴含的价值。而车牌识别这个系统,虽然传统,古老,却是包含了所有这四个特侦的一个大数据技术的缩影。
在车牌识别中,你需要处理的数据是图像中海量的像素单元;你处理的数据不再是传统的结构化数据,而是图像这种复杂的数据;如果不能在很短的时间内识别出车牌,那么系统就缺少意义;虽然一副图像中有很多的信息,但可能仅仅只有那一小块的信息(车牌)以及车身的颜色是你关心,而且这些信息都蕴含着巨大的价值。也就是说,车牌识别系统事实上就是现在火热的大数据技术在某个领域的一个聚焦,通过了解车牌识别系统,可以很好的帮助你理解大数据技术的内涵,也能清楚的认识到大数据的价值。
除了帮忙我们更好的理解大数据技术,使我们跟的上时代,开发一个车牌系统还有其他原因。
那就是,现在的车牌系统,仍然还有许多待解决的挑战。事实上,车牌识别系统业界一直都没有一个成熟的百分百适用的方案。一些90%以上的车牌识别系统都是跟高清摄像机做了集成,由摄像头传入的高分辨率图片进入识别系统,可以达到较高的识别率。但是如果图像分辨率一旦下来,或者图里的车牌脏了的话,那么很遗憾,识别率远远不如我们的肉眼。那么,就没有办法进一步优化了么。答案是有的,这个就需要谈到目前火热的深度学习与计算机视觉技术,使用多隐层的深度神经网络也许能够解决这个问题。但是目前EasyPR并没有采用这种技术,或许以后会采用。但是这个方向是有的。
在上一篇文档中,我们了解到EasyPR包括两个部分,但实际上为了更好进行模块化开发,EasyPR被划分成了六个模块,其中每个模块的准确率与速度都影响着整个系统。具体说来,EasyPR中PlateDetect与CharsRecognize各包括三个模块。
PlateDetect包括的是车牌定位,SVM训练,车牌判断三个过程,见下图。
图3 PlateDetect过程详解
通过PlateDetect过程我们获得了许多可能是车牌的图块,将这些图块进行手工分类,聚集一定数量后,放入SVM模型中训练,得到SVM的一个判断模型,在实际的车牌过程中,我们再把所有可能是车牌的图块输入SVM判断模型,通过SVM模型自动的选择出实际上真正是车牌的图块。
PlateDetect过程结束后,我们获得一个图片中我们真正关心的部分--车牌。下一步就是根据这个车牌图片,生成一个车牌号字符串的过程,也就是CharsRecognisze的过程。CharsRecognise包括的是字符分割,ANN训练,字符识别三个过程,具体见下图。
图4 CharsRecognise过程详解
在CharsRecognise过程中,一副车牌图块首先会进行灰度化,二值化,然后使用一系列算法获取到车牌的每个字符的分割图块。获得海量的这些字符图块后,进行手工分类(这个步骤非常耗时间,后面会介绍如何加速这个处理的方法),然后喂入神经网络(ANN)的MLP模型中,进行训练。在实际的车牌识别过程中,将得到7个字符图块放入训练好的神经网络模型,通过模型来预测每个图块所表示的具体字符,例如图片中就输出了“苏EUK722”。
至此一个完整的车牌识别过程就结束了,但是在每一步的处理过程中,有许多的优化方法和处理策略。尤其是车牌定位和字符分割这两块,非常重要,它们不仅生成实际数据,还生成训练数据,因此会直接影响到模型的准确性,以及模型判断的最终结果。这两部分会是作者重点介绍的模块,至于SVM模型与ANN模型,由于使用的是OpenCV提供的类,因此可以直接看openCV的源码或者机器学习介绍的书,来了解训练与判断过程。
根据EasyPR的结构,我们把它分为六个部分,前三个部分统称为“Plate Detect”过程。主要目的是在一副图片中发现仅包含车牌的图块,以此提高整体识别的准确率与速度。这个过程非常重要,如果这步失败了,后面的字符识别过程就别想了。而“Plate Detect”过程中的三个部分又分别称之为“Plate Locate” ,“SVM train”,“Plate judge”,其中最重要的部分是第一步“Plate Locate”过程。本节主要介绍“Plate Locate”过程,并且回答以下三个问题:
1.此过程的作用是什么,为什么重要?
2.此过程是如何实现车牌定位这个功能的?
3.此过程中的细节是什么,如何进行调优?
1.“Plate Locate”的作用与重要性
在说明“Plate Locate”的作用与重要性之前,请看下面这两幅图片。
图5 两幅包含车牌的不同形式图片
左边的图片是作者训练的图片(作者大部分的训练与测试都是基于此类交通抓拍图片),右边的图片则是在百度图片中“车牌”获得(这个图片也可以称之为生活照片)。右边图片的问题是一个网友评论时问的。他说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& );
//! 车牌的尺寸验证
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型,0代表成功,其他代表失败。plateLocate内部是如何实现的,让我们再深入下看看。
2.“Plate Locate”的实现过程
plateLocate过程基本参考了taotao1233的博客的处理流程,但略有不同。plateLocate的总体识别思路是:如果我们的车牌没有大的旋转或变形,那么其中必然包括很多垂直边缘(这些垂直边缘往往缘由车牌中的字符),如果能够找到一个包含很多垂直边缘的矩形块,那么有很大的可能性它就是车牌。
依照这个思路我们可以设计一个车牌定位的流程。设计好后,再根据实际效果进行调优。下面的流程是经过多次调整与尝试后得出的,包含了数月来作者针对测试图片集的一个最佳过程(不一定适用所有情况)。plateLocate主要处理流程图如下:
图6 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。这个标准是对千个测试车牌平均后得出的通用值。下图为最终的三个候选”车牌“图块。
这些“车牌”有两个作用:
3.“Plate Locate”的深入讨论与调优策略
说了这么多,读者想必对整个“Plate Locate”过程已经有了一个完整的认识。那么让我们一步步审核一下处理流程中的每一个步骤。在本节中我们对前3个步骤,分别是高斯模糊、灰度化和Sobel算子进行分析。
3.1、高斯模糊
对图像去噪,为边缘检测算法做准备。
图7 高斯模糊效果
理论: 阮一峰讲高斯模糊。
正态分布的密度函数叫做"高斯函数"(Gaussian function)。它的二维形式是:
其中,μ是x的均值,σ是x的方差。因为计算平均值的时候,中心点就是原点,所以μ等于0。有了这个函数 ,就可以计算每个点的权重了。
1、假定中心点的坐标是(0,0),那么距离它最近的8个点的坐标如下:
2、为了计算权重矩阵,需要设定σ的值。假定σ=1.5,则模糊半径为1的权重矩阵如左下:
这9个点的权重总和等于0.4787147,如果只计算这9个点的加权平均,还必须让它们的权重之和等于1,因此上面9个值还要分别除以0.4787147,得到最终的权重矩阵,如右上。
3、有了权重矩阵,就可以计算高斯模糊的值了。假设现有9个像素点,灰度值(0-255)如下,每个点乘以自己的权重值:
将这9个值加起来,就是中心点的高斯模糊的值。对所有点重复这个过程,就得到了高斯模糊后的图像。
高斯模糊这个过程一定是必要的么?
笔者的回答是必要的,倘若我们将这句代码注释并稍作修改,重新运行一下。你会发现plateLocate过程在闭操作时就和原来发生了变化。最后结果如下。
图8 不采用高斯模糊后的结果
可以看出,车牌所在的矩形产生了偏斜。最后得到的候选“车牌”图块如下:
图9 不采用高斯模糊后的“车牌”图块
如果不使用高斯模糊而直接用边缘检测算法,我们得到的候选“车牌”达到了8个!这样不仅会增加车牌判断的处理时间,还增加了判断出错的概率。由于得到的车牌图块中车牌是斜着的,如果我们的字符识别算法需要一个水平的车牌图块,那么几乎肯定我们会无法得到正确的字符识别效果。
高斯模糊中的半径也会给结果带来明显的变化。有的图片,高斯模糊半径过高了,车牌就定位不出来。有的图片,高斯模糊半径偏低了,车牌也定位不出来。因此、高斯模糊的半径既不宜过高,也不能过低。CPlateLocate类中的值为5的静态常量DEFAULT_GAUSSIANBLUR_SIZE,标示着推荐的高斯模糊的半径。这个值是对于近千张图片经过测试后得出的综合定位率最高的一个值。在CPlateLocate类的构造函数中,m_GaussianBlurSize被赋予了DEFAULT_GAUSSIANBLUR_SIZE的值,因此,默认的高斯模糊的半径就是5。如果不是特殊情况,不需要修改它。
在数次的实验以后,必须承认,保留高斯模糊过程与半径值为5是最佳的实践。为应对特殊需求,在CPlateLocate类中也应该提供了方法修改高斯半径的值,调用代码(假设需要一个为3的高斯模糊半径)如下:
CPlateLocate plate;
plate.setGaussianBlurSize(3);
目前EasyPR的处理步骤是先进行高斯模糊,再进行灰度化。从目前的实验结果来看,基于色彩的高斯模糊过程比灰度后的高斯模糊过程更容易检测到边缘点。
3.2、灰度化处理
为边缘检测算法准备灰度化环境。灰度化的效果如下。
在灰度化处理步骤中,争议最大的就是信息的损失。无疑的,原先plateLocate过程面对的图片是彩色图片,而从这一步以后,就会面对的是灰度图片。在前面,已经说过这步骤是利是弊是需要讨论的。
无疑,对于计算机而言,色彩图像相对于灰度图像难处理多了,很多图像处理算法仅仅只适用于灰度图像,例如后面提到的Sobel算子。在这种情况下,你除了把图片转成灰度图像再进行处理别无它法,除非重新设计算法。但另一方面,转化成灰度图像后恰恰失去了最丰富的细节。要知道,真实世界是彩色的,人类对于 事物的辨别是基于彩色的框架。甚至可以这样说,因为我们的肉眼能够区别彩色,所以我们对于事物的区分,辨别,记忆的能力就非常的强。
车牌定位环节中去掉彩色的利弊也是同理。转换成灰度图像虽然利于使用各种专用的算法,但失去了真实世界中辨别的最重要工具---色彩的区分。举个简单的例子,人怎么在一张图片中找到车牌?非常简单,一眼望去,一个合适大小的矩形,蓝色的、或者黄色的、或者其他颜色的在另一个黑色,或者白色的大的跟车形类似的矩形中。这个过程非常直观,明显,而且可以排除模糊,色泽,不清楚等很多影响。如果使用灰度图像,就必须借助水平,垂直求导等方法。
未来如果PlateLocate过程可以使用颜色来判断,可能会比现在的定位更清楚、准确。但这需要研究与实验过程,在EasyPR的未来版本中可能会实现。但无疑,使用色彩判断是一种趋势,因为它不仅符合人眼识别的规律,更趋近于人工智能的本质,而且它更准确,速度更快。
3.3、Sobel算子
检测图像中的垂直边缘,便于区分车牌。下图是Sobel算子的效果。
如果要说哪个步骤是plateLocate中的核心与灵魂,毫无疑问是Sobel算子。没有Sobel算子,也就没有垂直边缘的检测,也就无法得到车牌的可能位置,也就没有后面的一系列的车牌判断、字符识别过程。通过Sobel算子,可以很方便的得到车牌的一个相对准确的位置,为我们的后续处理打好坚实的基础。在上面的plateLocate的执行过程中可以看到,正是通过Sobel算子,将车牌中的字符与车的背景明显区分开来,为后面的二值化与闭操作打下了基础。那么Sobel算子是如何运作的呢?
Soble算子原理是对图像求一阶的水平与垂直方向导数,根据导数值的大小来判断是否是边缘。请详见CSDN小魏的博客(小心她博客里把Gx和Gy弄反了)。为了计算方便,Soble算子并没有真正去求导,而是使用了周边值的加权和的方法,学术上称作“卷积”。权值称为“卷积模板”。例如下图左边就是Sobel的Gx卷积模板(计算垂直边缘),中间是原图像,右边是经过卷积模板后的新图像。
图10 Sobel算子Gx示意图
在这里演示了通过卷积模板,原始图像红色的像素点原本是5的值,经过卷积计算(- 1 * 3 - 2 * 3 - 1 * 4 + 1 * 5 + 2 * 7 + 1 * 6 = 12)后红色像素的值变成了12。在代码中调用Soble算子需要较多的步骤:
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
// Gradient X
Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_x, abs_grad_x );
// Gradient Y
Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_y, abs_grad_y ); //取绝对值,并转为uchar类型
/// Total Gradient (approximate)
addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad );
函数原型: void Sobel( InputArray src, OutputArray dst, int ddepth,
int dx, int dy, int ksize=3,
double scale=1, double delta=0,
int borderType=BORDER_DEFAULT );
在调用参数中有两个常量SOBEL_X_WEIGHT与SOBEL_Y_WEIGHT代表水平方向和垂直方向的权值,默认前者是1,后者是0,代表仅仅做水平方向求导,而不做垂直方向求导。这样做的意义是,如果我们做了垂直方向求导,会检测出很多水平边缘。水平边缘多也许有利于生成更精确的轮廓,但是由于有些车子前端太多的水平边缘了,例如车头排气孔,标志等等,很多的水平边缘会误导我们的连接结果,导致我们得不到一个恰好的车牌位置。
由于Sobel算子如此重要,可以将车牌与其他区域明显区分出来,那么问题就来了,有没有与Sobel功能类似的算子可以达到一致的效果,或者有没有比Sobel效果更好的算子?
Sobel算子求图像的一阶导数,Laplace算子则是求图像的二阶导数,在通常情况下,也能检测出边缘,不过Laplace算子的检测不分水平和垂直。下图是Laplace算子与Sobel算子的一个对比。
可以看出,通过Laplace算子的图像包含了水平边缘和垂直边缘,根据我们刚才的描述。水平边缘对于车牌的检测一般无利反而有害。经过对近百幅图像的测试,Sobel算子的效果优于Laplace算子,因此不适宜采用Laplace算子替代Sobel算子。
除了Sobel算子,还有一个算子,Shcarr算子。但这个算子其实只是Sobel算子的一个变种,由于Sobel算子在3*3的卷积模板上计算往往不太精确,因此有一个特殊的Sobel算子,其权值按照下图来表达,称之为Scharr算子。下图是Sobel算子与Scharr算子的一个对比。
一般来说,Scharr算子能够比Sobel算子检测边缘的效果更好,从上图也可以看出。但是,这个“更好”是一把双刃剑。我们的目的并不是画出图像的边缘,而是确定车牌的一个区域,越精细的边缘越会干扰后面的闭运算。因此,针对大量的图片的测试,Sobel算子一般都优于Scharr 算子。
关于Sobel算子更详细的解释和Scharr算子与Sobel算子的同异,可以参看官网的介绍:Sobel与Scharr。
综上所述,在求图像边缘的过程中,Sobel算子是一个最佳的契合车牌定位需求的算子,Laplace算子与Scharr算子的效果都不如它。有一点要说明的:Sobel算子仅能对灰度图像有效果,不能将色彩图像作为输入。因此在进行Soble算子前必须进行前面的灰度化工作。
根据前文的内容,车牌定位的功能还剩下如下的步骤,见下图中未涂灰的部分。
图11 车牌定位步骤
我们首先从Soble算子分析出来的边缘来看。通过下图可见,Sobel算子有很强的区分性,车牌中的字符被清晰的描绘出来,那么如何根据这些信息定位出车牌的位置呢?
图12 Sobel后效果
我们的车牌定位功能做了个假设,即车牌是包含字符图块的一个最小的外接矩形。在大部分车牌处理中,这个假设都能工作的很好。我们来看下这个假设是如何工作的。
1、二值化
首先,我们通过二值化处理将Sobel生成的灰度图像转变为二值图像。二值化算法非常简单,就是对图像的每个像素做一个阈值处理。为后续的形态学算子Morph等准备二值化的图像。 经过二值化处理后的图像效果为下图,与灰度图像仔细区分下,二值化图像中的白色是没有颜色强与暗的区别的。
图13 二值化后效果
在灰度图像中,每个像素的值是0-255之间的数字,代表灰暗的程度。如果设定一个阈值T,规定像素的值x满足如下条件时则:if x < t then x = 0; if x >= t then x = 1。如此一来,每个像素的值仅有{0,1}两种取值,0代表黑、1代表白,图像就被转换成了二值化的图像。在上面的公式中,阈值T应该取多少?由于不同图像的光造程度不同,导致作为二值化区分的阈值T也不一样。因此一个简单的做法是直接使用opencv的二值化函数时加上自适应阈值参数。如下:
threshold(src, dest, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
通过这种方法,我们不需要计算阈值的取值,直接使用即可。threshold函数是二值化函数,参数src代表源图像,dest代表目标图像,两者的类型都是cv::Mat型,最后的参数代表二值化时的选项,CV_THRESH_OTSU代表自适应阈值,CV_THRESH_BINARY代表正二值化。正二值化意味着像素的值越接近0,越可能被赋值为0,反之则为1。而另外一种二值化方法表示反二值化,其含义是像素的值越接近0,越可能被赋值1,如果想使用反二值化,可以使用参数CV_THRESH_BINARY_INV代替CV_THRESH_BINARY即可。在后面的字符识别中我们会同时使用到正二值化与反二值化两种例子。因为中国的车牌有很多类型,最常见的是蓝牌和黄牌。其中蓝牌字符浅,背景深,黄牌则是字符深,背景浅,因此需要正二值化方法与反二值化两种方法来处理,其中正二值化处理蓝牌,反二值化处理黄牌。
2、闭操作
闭操作用于将车牌字母连接成为一个连通域,便于取轮廓。我们这里看下经过闭操作后图像连接的效果。
图14 闭操作后效果
2.1、理论 OpenCV3学习(4.3)——图像形态学(膨胀,腐蚀)
闭操作就是对图像先膨胀,再腐蚀。闭操作的结果一般是可以将许多靠近的图块相连称为一个无突起的连通域。在我们的图像定位中,使用了闭操作去连接所有的字符小图块,然后形成一个车牌的大致轮廓。从结果上看就是,两个分开的图块组成的图。最后变成一个无突起的连通域(纯白的部分)。
图15 闭操作原理
在opencv中,调用闭操作的方法是首先建立矩形模板,矩形的大小是可以设置的,由于矩形是用来覆盖以中心像素的所有其他像素,因此矩形的宽和高最好是奇数。通过以下代码设置矩形的宽和高:
Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );
morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);
在这里,我们使用了类成员变量,这两个类成员变量在构造函数中被赋予了初始值。宽是17,高是3。设置完矩形的宽和高以后,就可以调用形态学操作了。opencv中所有形态学操作有一个统一的函数,通过参数来区分不同的具体操作。例如MOP_CLOSE代表闭操作,MOP_OPEN代表开操作。
图16 闭操作
可以看出,使用闭操作以后,车牌字符的图块被连接成了一个较为规则的矩形,通过闭操作,将车牌中的字符连成了一个图块,同时将突出的部分进行裁剪,图块成为了一个类似于矩形的不规则图块。我们知道,车牌应该是一个规则的矩形,因此获取规则矩形的办法就是先取轮廓,再接着求最小外接矩形。
2.2、矩形模板的宽度问题
需要注意的是,矩形模板的宽度,17是个推荐值,低于17都不推荐。为什么这么说,因为有一个”断节“的问题。中国车牌有一个特点,就是表示城市的字母与右边相邻的字符距离远大于其他相邻字符之间的距离。如果你设置的不够大,结果导致左边的字符与右边的字符中间断开了,这种情况我称之为“断节”如果你不想字符从中间被分成"苏A"和"7EUK22"的话,那么就必须把它设置大点。如下图:
图17 “断节”效果
另外还有一种讨厌的情况,就是右边的字符第一个为1的情况,例如苏B13GH7。在这种情况下,由于1的字符的形态原因,导致跟左边的B的字符的距离更远,在这种情况下,低于17都有很大的可能性会断节。下图说明了矩形模板宽度过小时(例如设置为7)面对不同车牌情况下的效果。其中第二个例子选取了苏E开头的车牌,由于E在Sobel算子运算过后仅存有左边的竖杠,因此也会导致跟右边的字符相距过远的情况!
宽度过大也是不好的,因为它会导致闭操作连接不该连接的部分,例如下图的情况。这种情况下,你取轮廓获得矩形肯定会大于你设置的校验规则,即便通过校验了,由于图块中有不少不是车牌的部分,会给字符识别带来麻烦
图18 矩形模板宽度过大
因此,矩形的宽度是一个需要非常细心权衡的值,过大过小都不好,取决于你的环境。至于矩形的高度,3是一个较好的值,一般来说都能工作的很好,不需要改变。记得我在前一篇文章中提到,工业用图片与生活场景下图片的区别么。笔者做了一个实验,下载了30多张左右的百度车牌图片。用plateLocate过程去识别他们。如果按照下面的方式设置参数,可以保证90%以上的定位成功率。
CPlateLocate plate;
plate.setDebug(1);
plate.setGaussianBlurSize(5);
plate.setMorphSizeWidth(7);
plate.setMorphSizeHeight(3);
plate.setVerifyError(0.9);
plate.setVerifyAspect(4);
plate.setVerifyMin(1);
plate.setVerifyMax(30);
但是,仍然有一些图片是EasyPR不好处理的。或者可以说,按照目前的边缘检测算法,难以处理的。请看下面一张图片:
这张图片最麻烦的地方在于车牌左右两侧凹下去的边侧,这个边缘在Sobel算子中非常明显,如果矩形模板过长,很容易跟它们连接起来。更麻烦的是这个车牌属于上面说的“断节”很容易发生的类型,因为车牌右侧字符的第一个字母是“1”,这个导致如果矩形模板过短,则很容易车牌断成两截。结果最后导致了如下的情况。
如果我设置矩形模板宽度为12,则会发生下面的情况:
如果我增加矩形模板宽度到13,则又会发生下面的情况:
因此矩形模板的宽度是个整数值,在12和13中间没有中间值。这个导致几乎没有办法处理这幅车牌图像。
上面的情况属于车尾车牌的一种没办法解决的情况。下面所说的情况属于车头的情况,相比前者,错误检测的几率高的多!为什么,因为是一类型车牌无法处理。要问我这家车是哪家,我只能说:碰到开奥迪Q5及其系列的,早点嫁了吧。伤不起。
图19 奥迪Q5前部垂直边缘太多
这么多的垂直边缘,极为容易检错。已经试过了,几乎没有办法处理这种车牌。只能替换边缘检测这种思路,采用颜色区分等方法。奥体Q系列前脸太多垂直边缘了,给跪
3、取轮廓
取轮廓操作是个相对简单的操作,将连通域的外围勾画出来,便于形成外接矩形。看下经过取轮廓操作的效果。
图20 取轮廓操作
在图中,红色的线条就是轮廓,可以看到,有非常多的轮廓。取轮廓操作就是将图像中的所有独立的不与外界有交接的图块取出来。然后根据这些轮廓,求这些轮廓的最小外接矩形。这里面需要注意的是这里用的矩形是RotatedRect,意思是可旋转的。因此我们得到的矩形不是水平的,这样就为处理倾斜的车牌打下了基础。取轮廓操作的代码如下:
vector< vector< Point> > contours;
findContours(img_threshold, contours, // a vector of contours
CV_RETR_EXTERNAL, // 提取外部轮廓
CV_CHAIN_APPROX_NONE); // all pixels of each contours
4、尺寸判断
尺寸判断操作是对外接矩形进行判断,以判断它们是否是可能的候选车牌的操作。排除不可能是车牌的矩形。经过尺寸判断,会排除大量由轮廓生成的不合适尺寸的最小外接矩形。效果如下图:
图21 尺寸判断操作
通过对图像中所有的轮廓的外接矩形进行遍历,我们调用CplateLocate的另一个成员方法verifySizes,在原先的verifySizes方法中,使用的是针对西班牙车牌的检测。而我们的系统需要检测的是中国的车牌。因此需要对中国的车牌大小有一个认识。中国车牌的一般大小是440mm*140mm,面积为440*140,宽高比为3.14。
verifySizes使用如下方法判断矩形是否是车牌:
1.设立一个偏差率error,根据这个偏差率计算最大和最小的宽高比rmax、rmin。判断矩形的r是否满足在rmax、rmin之间。 2.设定一个面积最大值max与面积最小值min。判断矩形的面积area是否满足在max与min之间。 ???
以上两个条件必须同时满足,任何一个不满足都代表这不是车牌。偏差率和面积最大值、最小值都可以通过参数设置进行修改,且他们都有一个默认值。如果发现verifySizes方法无法发现你图中的车牌,试着修改这些参数。另外,verifySizes方法是可选的。你也可以不进行verifySizes直接处理,但是这会大大加重后面的车牌判断的压力。一般来说,合理的verifySizes能够去除90%不合适的矩形。
//! 对minAreaRect获得的最小外接矩形,用纵横比进行判断
bool CPlateLocate::verifySizes(RotatedRect mr)
{
float error = m_error;
//Spain car plate size: 52x11 aspect 4,7272
//China car plate size: 440mm*140mm,aspect 3.142857
float aspect = m_aspect;
//Set a min and max area. All other patchs are discarded
//int min= 1*aspect*1; // minimum area
//int max= 2000*aspect*2000; // maximum area
int min= 44*14*m_verifyMin; // minimum area
int max= 44*14*m_verifyMax; // maximum area
//Get only patchs that match to a respect ratio.
float rmin= aspect-aspect*error;
float rmax= aspect+aspect*error;
int area= mr.size.height * mr.size.width;
float r = (float)mr.size.width / (float)mr.size.height;
if(r < 1)
{
r= (float)mr.size.height / (float)mr.size.width;
}
if(( area < min || area > max ) || ( r < rmin || r > rmax ))
{
return false;
}
else
{
return true;
}
}
5、角度判断
角度判断操作通过角度进一步排除一部分车牌。通过verifySizes的矩形,还必须进行一个筛选,即角度判断。一般来说,在一副图片中,车牌不太会有非常大的倾斜,我们做如下规定:如果一个矩形的偏斜角度大于某个角度(例如30度),则认为不是车牌并舍弃。
对上面的尺寸判断结果的六个黄色矩形应用角度判断后结果如下图:
图22 角度判断后的候选车牌
可以看出,原先的6个候选矩形只剩3个。车牌两侧的车灯的矩形被成功筛选出来。角度判断会去除verifySizes筛选余下的7%矩形,使得最终进入车牌判断环节的矩形只有原先的全部矩形的3%。
int k = 1;
for(int i=0; i< rects.size(); i++)
{
RotatedRect minRect = rects[i];
if(verifySizes(minRect))
{
// rotated rectangle drawing
// Get rotation matrix
// 旋转这部分代码确实可以将某些倾斜的车牌调整正,
// 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。
// 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试
// 这段代码。
if(m_debug)
{
Point2f rect_points[4];
minRect.points( rect_points );
for( int j = 0; j < 4; j++ )
line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );
}
float r = (float)minRect.size.width / (float)minRect.size.height;
float angle = minRect.angle;
Size rect_size = minRect.size;
if (r < 1)
{
angle = 90 + angle;
swap(rect_size.width, rect_size.height);
}
//如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理
if (angle - m_angle < 0 && angle + m_angle > 0)
{
//Create and rotate image
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
Mat img_rotated;
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
Mat resultMat;
resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);
resultVec.push_back(resultMat);
}
}
6、旋转
旋转操作是为后面的车牌判断与字符识别提高成功率的关键环节。旋转操作将偏斜的车牌调整为水平。使用旋转与不适用旋转的效果区别如下图:
可以看出,没有旋转操作的车牌是倾斜,加大了后续车牌判断与字符识别的难度。因此最好需要对车牌进行旋转。在角度判定阈值内的车牌矩形,我们会根据它偏转的角度进行一个旋转,保证最后得到的矩形是水平的。调用的opencv函数如下:
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
Mat img_rotated;
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
7、大小调整
结束了么?不,还没有,至少在我们把这些候选车牌导入机器学习模型之前,需要确保他们的尺寸一致。机器学习模型在预测的时候,是通过模型输入的特征来判断的。我们的车牌判断模型的特征是所有的像素的值组成的矩阵。因此,如果候选车牌的尺寸不一致,就无法被机器学习模型处理。因此需要用resize方法进行调整。
我们将车牌resize为宽度136,高度36的矩形。为什么用这个值?这个值一开始也不是确定的,我试过许多值。最后我将近千张候选车牌做了一个统计,取它们的平均宽度与高度,因此就有了136和36这个值。所以,这个是一个统计值,平均来说,这个值的效果最好。
大小调整调用了CplateLocate的最后一个成员方法showResultMat,代码很简单,贴下,不做细讲了。
//! 显示最终生成的车牌图像,便于判断是否成功进行了旋转。
Mat CPlateLocate::showResultMat(Mat src, Size rect_size, Point2f center, int index)
{
Mat img_crop;
getRectSubPix(src, rect_size, center, img_crop);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_crop_" << index << ".jpg";
imwrite(ss.str(), img_crop);
}
Mat resultResized;
resultResized.create(HEIGHT, WIDTH, TYPE);
resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_resize_" << index << ".jpg";
imwrite(ss.str(), resultResized);
}
return resultResized;
}
总结
通过接近10多个步骤的处理,我们才有了最终的候选车牌。这些过程是一环套一环的,前步骤的输出是后步骤的输入,而且顺序也是有规则的。目前针对我的测试图片来说,它们工作的很好,但不一定适用于你的情况。车牌定位以及图像处理算法的一个大的问题就是他的弱鲁棒性,换一个场景可能就得换一套工作方式。因此结合你的使用场景来做调整吧,
讨论:车牌定位全部步骤了解后,我们来讨论下。这个过程是否是一个最优的解?
毫无疑问,一个算法的好坏除了取决于它的设计思路,还取决于它是否充分利用了已知的信息。如果一个算法没有充分利用提供的信息,那么它就有进一步优化的空间。EasyPR的 plateLocate过程就是如此,在实施过程中它相继抛弃掉了色彩信息,没有利用纹理信息,因此车牌定位的过程应该还有优化的空间。如果 plateLocate过程无法良好的解决你的定位问题,那么尝试下能够利用其他信息的方法,也许你会大幅度提高你的定位成功率。
车牌定位讲完后,下面就是机器学习的过程。不同于前者,我不会重点说明其中的细节,而是会概括性的说明每个步骤的用途以及训练的最佳实践。
文章转载自:非常详细的讲解车牌识别easypr