之前学习了一个GitHub开源的框架,GitHub地址为:
https://github.com/liuruoze/EasyPR
希望通过此篇博客详细阐述如何一步步实现车牌的识别过程。
车牌识别分成了两个部分,首先是车牌的定位,然后则是车牌的文字识别。
Plate Detect过程中包含了三个部分,“Plate location”,“SVM train”,”Plate judge”。其中最重要的是“Plate location”部分。
有人提出了这样的疑问:
EasyPR在处理百度图片时的识别率不高。确实如此,由于工业与生活应用目的不同,拍摄的车牌的大小,角度,色泽,清晰度不一样。而对图像处理技术而言,一些算法对于图像的形式以及结构都有一定的要求或者假设。因此在一个场景下适应的算法并不适用其他场景。目前EasyPR所有的功能都是基于交通抓拍场景的图片制作的,因此也就导致了其无法处理生活场景中这些车牌照片。
因此对于不同的场景,算法要做不同的适配。对于“Plate Location”过程无法处理生活照片的定位,我们可以对这一部分进行适配改造,但是后面的字符识别过程是相同的,整体架构不变。
在EasyPR中,“Plate Locate”过程被封装成了一个“CPlateLocate” 类,通过“plate_locate.h”声明,在“plate_locate.cpp”中实现。
CPlateLocate包含三个方法以及数个变量。方法提供了车牌定位的主要功能,变量则提供了可定制的参数,有些参数对于车牌定位的效果有非常明显的影响,例如高斯模糊半径、Sobel算子的水平与垂直方向权值、闭操作的矩形宽度。
CPlateLocate中最核心的方法是plateLocate方法。
//! 车牌定位
int plateLocate(Mat, vector & );
方法有两个参数,第一个参数代表输入的源图像,第二个参数是输出数组,代表所有检索到的车牌图块。返回值为int型,0代表成功,其他代表失败。plateLocate内部是如何实现的,让我们再深入下看看。
如果我们的车牌没有大的旋转或变形,那么其中必然包括很多垂直边缘(这些垂直边缘往往缘由车牌中的字符),如果能够找到一个包含很多垂直边缘的矩形块,那么有很大的可能性它就是车牌。
识别流程如下:
1、对原始图像高斯模糊,这步的作用是平滑图像,去除干扰的噪声。
2、对灰度图像进行Sobel运算,得到图像的一阶水平方向导数。其实是提取出了图像的边缘信息。
3、对图像使用闭操作,可以看到车牌闭操作后被连接成一个矩形的区域。
4、求出上面图像的所有轮廓。对所有的轮廓要进行筛选。
5、对所有轮廓求最小外接矩形,然后验证,不满足条件的被淘汰。验证一般是按照外接矩形的长宽比例范围设置一定的阈值。
6、角度判断,我们要把倾斜角度大于阈值(如正负30度)的矩形舍弃。余下的矩形进行微小的旋转,使其水平。
7、上面的是 候选的车牌识别矩形。 我们将上面的候选区域的图像统一尺寸。因为机器学习模型,需要统一尺寸。统一尺寸的长度为136,高度为36。
这些候选车牌有两个作用:积累下来作为支持向量机模型的训练集,以此训练出一个车牌判断模型;二、在实际的车牌检测过程中,将这些候选车牌交给训练好的判断模型进行判断。如果车牌判断模型认为是车牌的话就进入下一步的字符识别过程。
高斯模糊这个过程一定是必要,倘若我们将这句代码注释并稍作修改,重新运行一下。你会发现plateLocate过程在闭操作时就和原来发生了变化。
有的时候你会发现得到的车牌图块中的车牌是斜着的,如果我们的字符识别算法需要一个水平的车牌图块,那么几乎肯定我们无法得到正确的字符识别效果。
高斯模糊中的半径也会给结果带来明显的变化。有的图片,高斯模糊半径过高了,车牌就定位不出来。有的图片,高斯模糊半径偏低了,车牌也定位不出来。因此、高斯模糊的半径既不宜过高,也不能过低。CPlateLocate类中的值为5的静态常量DEFAULT_GAUSSIANBLUR_SIZE,是推荐的高斯模糊的半径。
车牌定位模块的输出是一些候选车牌的图片,但如何从这些候选车牌中选出真正的车牌,就是通过SVM模型判断/预测得到的。
int num = inVec.size();
for (int j = 0; j < num; j++)
{
Mat inMat = inVec[j];
Mat p = histeq(inMat).reshape(1, 1);
//reshape第一个参数是通道数,第二个参数是rows行数,这里为1行
p.convertTo(p, CV_32FC1);
int response = (int)svm.predict(p);
if (response == 1)
{
resultVec.push_back(inMat);
}
}
return 0;
从预测的源码可以看出,他是将整个候选区域的Mat 变形为一个行向量作为特征向量。然后对这个特征向量进行预测。 (这个地方也可以使用其他的特征,一方面可以降低维数,一方面也能深层刻画特征)。
SVM贴标签加数据格式准备之前在项目中也用到过。最考验能力的是配置SVM模型的训练参数。SVM模型的训练需要一个CvSVMParams的对象,这个类是SVM模型中训练对象的参数的组合,如何给这些参数赋值,是很有研究的一个工作。机器学习最后模型的效果差异有很大因素取决于模型训练时的参数。
CvSVMParams SVM_params;
SVM_params.svm_type = CvSVM::C_SVC;
SVM_params.kernel_type = CvSVM::LINEAR; //CvSVM::LINEAR;
SVM_params.degree = 0;
SVM_params.gamma = 1;
SVM_params.coef0 = 0;
SVM_params.C = 1;
SVM_params.nu = 0;
SVM_params.p = 0;
SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);
一般使用查准率和查全率进行刻画。
precision指标的期望含义就是要“查的准”,recall的期望含义就是“不要漏”。查全率还有一个翻译叫做“召回率”。
如果precision和recall这两个值越高越好,但是如果一个高一个地则怎么刻画呢?为了能够数字化这些情况,有一种FScore计算方法
SVM调优
SVM调优部分,是通过对SVM的原理进行了解,并运用机器学习的一些调优策略进行优化的步骤。
在这个部分里,最好要懂一点机器学习的知识。同时,本部分也会讲的尽量通俗易懂,让人不会有理解上的负担。在EasyPR1.0版本中,SVM模型的代码完全参考了mastering opencv书里的实现思路。从1.1版本开始,EasyPR对车牌判断模块进行了优化,使得模型最后的效果有了较大的改善。
具体说来,本部分主要包括如下几个子部分:1.RBF核;2.参数调优;3.特征提取;4.接口函数;5.自动化。
下面分别对这几个子部分展开介绍。
1.RBF核
SVM中最关键的技巧是核技巧。“核”其实是一个函数,通过一些转换规则把低维的数据映射为高维的数据。在机器学习里,数据跟向量是等同的意思。例如,一个 [174, 72]表示人的身高与体重的数据就是一个两维的向量。在这里,维度代表的是向量的长度。(务必要区分“维度”这个词在不同语境下的含义,有的时候我们会说向量是一维的,矩阵是二维的,这种说法针对的是数据展开的层次。机器学习里讲的维度代表的是向量的长度,与前者不同)
简单来说,低维空间到高维空间映射带来的好处就是可以利用高维空间的线型切割模拟低维空间的非线性分类效果。也就是说,SVM模型其实只能做线型分类,但是在线型分类前,它可以通过核技巧把数据映射到高维,然后在高维空间进行线型切割。高维空间的线型切割完后在低维空间中最后看到的效果就是划出了一条复杂的分线型分类界限。从这点来看,SVM并没有完成真正的非线性分类,而是通过其它方式达到了类似目的,可谓“曲径通幽”。
SVM模型总共可以支持多少种核呢。根据官方文档,支持的核类型有以下几种:
liner核,也就是无核。
rbf核,使用的是高斯函数作为核函数。
poly核,使用多项式函数作为核函数。
sigmoid核,使用sigmoid函数作为核函数。
liner核和rbf核是所有核中应用最广泛的。
liner核,虽然名称带核,但它其实是无核模型,也就是没有使用核函数对数据进行转换。因此,它的分类效果仅仅比逻辑回归好一点。在EasyPR1.0版中,我们的SVM模型应用的是liner核。我们用的是图像的全部像素作为特征。
rbf核,会将输入数据的特征维数进行一个维度转换,具体会转换为多少维?这个等于你输入的训练量。假设你有500张图片,rbf核会把每张图片的数据转 换为500维的。如果你有1000张图片,rbf核会把每幅图片的特征转到1000维。这么说来,随着你输入训练数据量的增长,数据的维数越多。更方便在高维空间下的分类效果,因此最后模型效果表现较好。
既然选择SVM作为模型,而且SVM中核心的关键技巧是核函数,那么理应使用带核的函数模型,充分利用数据高维化的好处,利用高维的线型分类带来低维空间下的非线性分类效果。但是,rbf核的使用是需要条件的。
当你的数据量很大,但是每个数据量的维度一般时,才适合用rbf核。相反,当你的数据量不多,但是每个数据量的维数都很大时,适合用线型核。
在EasyPR1.0版中,我们用的是图像的全部像素作为特征,那么根据车牌图像的136×36的大小来看的话,就是4896维的数据,再加上我们输入的 是彩色图像,也就是说有R,G,B三个通道,那么数量还要乘以3,也就是14688个维度。这是一个非常庞大的数据量,你可以把每幅图片的数据理解为长度 为14688的向量。这个时候,每个数据的维度很大,而数据的总数很少,如果用rbf核的话,相反效果反而不如无核。
在EasyPR1.1版本时,输入训练的数据有3000张图片,每个数据的特征改用直方统计,共有172个维度。这个场景下,如果用rbf核的话,就会将每个数据的维度转化为与数据总数一样的数量,也就是3000的维度,可以充分利用数据高维化后的好处。
因此可以看出,为了让EasyPR新版使用rbf核技巧,我们给训练数据做了增加,扩充了两倍的数据,同时,减小了每个数据的维度。以此满足了rbf核的使用条件。通过使用rbf核来训练,充分发挥了非线性模型分类的优势,因此带来了较好的分类效果。
但是,使用rbf核也有一个问题,那就是参数设置的问题。在rbf训练的过程中,参数的选择会显著的影响最后rbf核训练出模型的效果。因此必须对参数进行最优选择。
2.参数调优
传统的参数调优方法是人手完成的。机器学习工程师观察训练出的模型与参数的对应关系,不断调整,寻找最优的参数。由于机器学习工程师大部分时间在调整模型的参数,也有了“机器学习就是调参”这个说法。
幸好,opencv的svm方法中提供了一个自动训练的方法。也就是由opencv帮你,不断改变参数,训练模型,测试模型,最后选择模型效果最好的那些参数。整个过程是全自动的,完全不需要你参与,你只需要输入你需要调整参数的参数类型,以及每次参数调整的步长即可。
现在有个问题,如何验证svm参数的效果?你可能会说,使用训练集以外的那30%测试集啊。但事实上,机器学习模型中专门有一个数据集,是用来验证参数效果的。也就是交叉验证集(cross validation set,简称validate data) 这个概念。
validate data就是专门从train data中取出一部分数据,用这部分数据来验证参数调整的效果。比方说现在有70%的训练数据,从中取出20%的数据,剩下50%数据用来训练,再用训练出来的模型在20%数据上进行测试。这20%的数据就叫做validate data。真正拿来训练的数据仅仅只是50%的数据。
正如上面把数据划分为test data和train data的理由一样。为了验证参数在新数据上的推广性,我们不能用一个训练数据集,所以我们要把训练数据集再细分为train data和validate data。在train data上训练,然后在validate data上测试参数的效果。所以说,在一个更一般的机器学习场景中,机器学习工程师会把数据分为train data,validate data,以及test data。在train data上训练模型,用validate data测试参数,最后用test data测试模型和参数的整体表现。
说了这么多,那么,大家可能要问,是不是还缺少一个数据集,需要再划分出来一个validate data吧。但是答案是No。opencv的train_auto函数帮你完成了所有工作,你只需要告诉它,你需要划分多少个子分组,以及validate data所占的比例。然后train_auto函数会自动帮你从你输入的train data中划分出一部分的validate data,然后自动测试,选择表现效果最好的参数。
感谢train_auto函数!既帮我们划分了参数验证的数据集,还帮我们一步步调整参数,最后选择效果最好的那个参数,可谓是节省了调优过程中80%的工作。
” 在rbf核介绍时提到过,输入数据的特征的维度现在是172,那么这个数字是如何计算出来的?现在的特征用的是直方统计函数,也就是先把图像二值化,然后统计图像中一行元素中1的数目,由于输入图像有36行,因此有36个值,再统计图像中每一列中1的数目,图像有136列,因此有136个值,两者相加正好等于172。”我们输入数据的特征不再是全部的三原色的像素值了,而是抽取过的一些特征。从原始的图像到抽取后的特征的过程就被称为特征提取的过程。在1.0版中没有特征提取的概念,是直接把图像中全部像素作为特征的。这要感谢群里的“如果有一天”同学,他坚持认为全部像素的输入是最低级的做法,认为用特征提取后的效果会好多。我问大概能到多少准确率,当时的准确率有92%,我以为已经很高了,结果他说能到99%。在半信半疑中我尝试了,果真如他所说,结合了rbf核与新特征训练的模型达到的precise在99%左右,而且recall也有98%,这真是个令人咋舌并且非常惊喜的成绩。
其实这是一个很显然的突破点,但是如何提取特征却需要自己采用不同的方法进行建模。