基于SVM和神经网络的车牌识别
本文将介绍创建自动车牌识别(Automatic Number Plate Recognition, ANPR)所需的步骤。对于不同的情形,实现自动车牌识别会用不同的方法和技术,例如,IR摄像机、固定汽车位置、光照条件等。本文着手构造一个用来检测汽车车牌ANPR的应用,该应用处理的图像使从汽车2-3米处拍摄的,拍摄环境的光线昏暗模糊,并且与地面不平行、车牌在图像中有轻微的扭曲。
本文的主要目标是介绍图像分割、特征提取、模式识别基础以及两个重要的模式识别算法:支持向量机(Suport Vector Machine, SVM)和人工神经网络(Artificial Neural Network, ANN)。
本文主要内容:
1) ANPR
2) 车牌检测
3) 车牌识别
一、 ANPR简介
自动车牌识别也称为自动车牌照识别(Automatic Vehicle Identification, AVI)、洗车车牌识别(Car Plate Recognition, CPR),它是一种使用光学字符识别(Optical Character Recognition, OCR)和其他方法(如,用图像分割与检测)来获取车辆牌照的监控方法。
对于一个ANPR系统,其最好结果可用一个红外(IR)摄像机来获取数据,因为在分割这一步中,对检测和OCR分割很简单、干净。并且误差最小。这是由光学的一些基本原理决定的,例如入射角等于反射角,当人看到光滑表面(如平面镜)时就会有这样的反映。粗糙表面(如纸)的反射会导致漫射或散射。多数车牌有一个称为回射的特性,车牌表面覆盖着一种材料,它由许多微小半球颗粒构成,会导致光线沿路反射回去。
如果使用结合了结构性红外光学投影器的摄像机,就可只获取红外光,这样就能得到很高品质的图像,对这种图像进行分割,然后检测和识别车牌。这种情况下的车牌独立于任意光照环境。
二、 ANPR算法
在解析ANPR算法代码之前,需要明白注意步骤和使用ANPR算法的任务。ANPR有两个主要步骤:车牌检测和车牌识别。车牌检测的目的是在整个视频帧中检测到车牌位置。当在图像中检测到车牌时,分割的车牌被传到第二个步骤,即车牌识别,它用OCR算法来识别车牌上的字母和数字。
下面将定义模式识别算法常用的三个步骤:
1)分割:这一步会检测并裁剪图像中每个感兴趣的块或区域;
2)特征提取:这一步对字符图像集的每个部分进行提取;
3)分类:这一步会从车牌识别那一步的结果中得到每个字符,或从车牌检测(plate detection)那一步中将夺得图像块分为“是车牌”或“不是车牌”;
除了这个主要的应用以外,模式识别算法的主要目的是检测和识别汽车车牌,下面简单介绍一下两个任务,这两个任务通常都不会解释。
第一是:如何训练模式识别系统;
第二是:如何评估模式识别系统。
三、 车牌检测
这一步要检测当前帧中所有的车牌。为了实现此功能,该步骤又分为两个主要步骤:图像分割和对分割的图像进行分类。这一步的功能不会解释因为将图像块作为一个向量特征。
在第一步(图像分割)中,将使用各种滤波器、形态学算子,以及轮廓算法来验证所获取图像中所有车牌的部分。
在第二步(分类)中,对每个图像块(即特征)将采用支持向量机(Support Vector Machine, SVM)作为分类器进行分类。在创建主要的应用之前,需要训练两个不同的类:车牌和非车牌号。这步所使用的图像使在汽车前面2-4米拍摄平行的正面视角彩色图像,这些图像有800像素宽。这些要求对确保正确的图像分割很重要。可创建一个多尺度图像算法来进行检测。
下面包括了车牌检测的所有过程:
1) Sobel滤波器;
2) 阀值算子;
3) 闭形态学算子;
4) 一个填充区域掩码;
5) 用红色标记(特征图像中)可能检测到的车牌;
6) 在执行SVM分类器后检测车牌。
四、 图像分割
图像分割是将图像分成多个区域的过程。该过程是为了分析而简化图像,同时也使特征提取更容易。
车牌分割有一个重要特征:假定从汽车前面拍摄图像,会在车牌上有大量竖直边(vertical edge),并且车牌不会被旋转,也没有透视扭曲(perspectivedistortion)。这一性质在分割图像时可采用来删除没有任何竖直边的那些区域。
在找到竖直边之前,需要将彩色图像转换为灰度图像(因为彩色对本任务没有任何用),删除可能由摄像机产生的噪声或其他环节噪声。利用5x5的高斯模糊来去噪。如果不用去噪方法,可能得到很多竖直边,从而造成检测失败。
Matimage = imread("car1.jpg");
Matimg_gray;
cvtColor(image,img_gray,CV_BGR2GRAY);//转Áa化¡¥为a灰¨°度¨¨图ª?
blur(img_gray,img_gray,Size(5,5));//5x5高?斯1模¡ê糊y去¨£¤噪?
为了找到竖直边,将采用sobel滤波器来找到第一个水平导数(horizontal derivative)。导数是数学函数,它可用来在图像中查找竖直边。根据情况,使用x方向一阶导数,y方向0阶。
Matimg_sobel;
//水?平?x方¤?向¨°一°?阶¡Á导Ì?数ºy,ê?查¨¦找¨°竖º¨²直¡À边À?
Sobel(img_gray,img_sobel,CV_8U,1,0,3,1,0);
在执行完sobel滤波器之后,将采用阀值滤波器来得到二值图像,所采用的阀值由otsu算法得到。Otsu算法的输入是一个8位图像,它将自动得到优化的阀值:
Mat img_threshold;
//阀¤¡ì值¦Ì滤?波¡§,ê?得Ì?到Ì?二t值¦Ì图ª?像?
threshold(img_sobel,img_threshold,0,255,CV_THRESH_OTSU+CV_THRESH_BINARY);
通过采用一个闭形态学算子,可删除在每个竖直边缘线之间的空白区域,并连接有大量边的所有区域的。在这一部中,有可能包含车牌区域。
首先,需要定义在闭形态学算子中所使用的结构元素。可使用getStructuringElement函数来定义一个结构元素,它的维度大小为17x3,这可能与其他图像尺寸有所不同:
//结¨¢构1矩?阵¨®元a素?
Matelement=getStructuringElement(MORPH_RECT,Size(17,3));
在闭形态学算子中使用morphologyEx函数就会得到结构元素:
//闭À?形?态¬?学¡ì算?子Á¨®
morphologyEx(img_threshold,img_threshold,CV_MOP_CLOSE,element);
在使用这些函数后,就会得到包含车牌的区域,但多数区域都不包含车牌号。这些区域可用连通分量分析(connected-component analysis)或用findContours函数将其分开。
//迭̨¹代䨲器¡Â,ê?轮?廓¤a检¨¬测a
vector< vector < Point > >contours;
findContours(img_threshold,contours,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_NONE);
//使º1用®?向¨°量¢?迭̨¹代䨲器¡Â来¤¡ä得Ì?到Ì?被À?旋y转Áa的Ì?矩?形?
vector<vector<Point>>::iterator itc=contours.begin();
vector<RotatedRect> rects;
while(itc!=contours.end())
{
RotatedRectmr=minAreaRect(Mat(*itc));
if(!verifySizes(mr))
{
itc=contours.erase(itc);
}
else
{
++itc;
rects.push_back(mr);
}
}
五、 分类
在预处理和分割完图像的所有部分后,需要决定每部分是否为车牌号。可使用支持向量机(Support Vector Machine, SVM)算法来完成该功能。
支持向量机是一种模式识别算法,它源于二分类的监督学习(supervised-learning)算法。监督学习是通过标签数据进行学习的机器学习算法。用户需要用一些标签数据来训练算法,标签数据是指每个样本都应该属于某个具体的类。
SVM会创建一个或多个超平面,这些超平面可用来判断数据属于哪个类。一个经典的SVM实例是,对一个只有两个类的二维平面的点集合,SVM搜索的最优直线以将不同类的点分开。
在开始分类之前,需要训练分类器,该工作主要在应用开始之前完成,这称为离线训练。离线训练并不是一件容易的事,因为它需要充足的数据来训练系统,但不是数据集越大就能得到最好的结果。本项目并没有充足的数据,因为并没有公开的车牌数据库。因此,需要拍摄数百张汽车照片,然后预处理并分割它们。
为了简单理解机器学习是如何工作的,可对分类器算法使用图像像素特征(注意:有更好的方法和特征用于训练svm, 比如,主成分分析(PrincipalComponents Analysis, PCV)、傅里叶变换、纹理分析等)。
六、 OCR分割
首先,对获取的车牌图像用直方图均衡进行处理,将其作为OCR函数的输入,然后采用阀值滤波器对图像进行处理,并将处理后的图像作为查找轮廓(find contour)算法的输入。
这个分割的过程的代码如下:
Mat img_threshold;
threshold(image,img_threshold,60,255,CV_THRESH_BINARY_INV);
if(DEBUG)
{
imshow(“Threshold plate”,img_threshold);
}
Mat img_contours;
img_threshold.copyTo(img_contours);
vector<vector<Point>> contours;
findContours(img_contours,contours,CV_RETR_EXTERNAL,CV_CHAIN_APPROX_NONE);
使用CV_THRESH_BINARY_INV可将白色输入值变为黑色,将黑色输入值变为白色,从而反转阀值化的输出结果。为了得到每个字符的轮廓,这是有必要的,因为轮廓算法会查找白色像素。
对每个检测到的轮廓,需要验证其大小并删除所有尺寸较小或宽高比不正确的区域。在本项目中,正确的车牌字符的宽高比约为45/77,但由于字符会有旋转或扭曲,允许车牌字符的宽高比有35%的误差。如果一块区域的这个比率超过标准比率的80%,可认为这个区域为黑色块,而不是一个字符。可用countNonZero函数来计算像素值大于0的像素个数:
Bool OCR::verifySizes(Mat r)
{Float aspect=45.0f/77.0f;
Float charAspect=(float)r.cols/(float)r.rows;
Float error=0.35;
Float minHeight=15;
Float maxHeight=28;
Float minAspect=0.2;
Float maxAspect=aspect+aspect+aspect*error;
Float area=countNonZero(r);
Float bbArea=r.cols*r.rows;
Float percPixels=area/bbArea;
If(percPixels<0.8 && charAspect>minAspect &&charAspect<maxAspect&&r.rows>=minHeight&&r.rows<maxHeight)
Return true;
Else
Return false;}
如果一个分割的区域是字符,则必须要对其预处理,使它所有字符有一样的大小和位置,然后用辅助类charsegment将其保存到一个向量中。该类保存分割后的字符图像和用于调整字符所需的位置,因为查找轮廓算法不会按所需顺序返回轮廓。
七、 特征提取
为了用人工神经网络进行训练和分类,下面将对每个分割出来的字符进行特征提取。
与用SVM进行车牌检测时的特征提取不同,这里不会使用所有图像像素作为特征,而是采用光学字符识别中更常用的特征,这些特征包含了水平和竖直累积直方图,以及低分辨的图像样本。
对每个字符,通过使用countNonZero函数来按列或按行统计非零像素个数,并将其保存到新的数据矩阵mhist中。对mhist进行归一化处理,其过程为:通过minMaxLoc函数找到该矩阵的最大值,将它的每个元素都除以这个最大值,并通过convertTo函数来最终实现。创建名为ProjectedHistogram的函数,用它来实现累积直方图,这个函数将二值图像和直方图类型(水平或竖直)作为输入:
Mat OCR::ProgetedHistogram(Mat img,int t)
{
Int sz=(t)?img.rows:img.cols;
Matmhist=Mat::zeros(1,sz,CV_32F);
For(int j=0;j<sz;j++)
{
Matdata=(t)?img.row(j):img.col(j);
Mhist.at<float>(j)=countNonZero(data);
}
Double min,max;
minMaxLoc(mhist,&min,&max);
if(max>0)
{
Mhist.covertTo(mhist,-1,1.0f/max,0);
}
Return mhist;
}
八、 OCR分类
在分类这一步,将使用机器学习算法中的人工神经网络。更具体一点,使用多层感知器(Multi-Layer Perception,MLP),它是常见的ANN算法。
MLP由包含一个输入层、包含一个输出层和一个或多个隐藏层的神经网络组成。每一层由一个或多个神经元同前一层和后一层相连。
在MLP中的所有神经元都差不多,每个神经元都有几个输入(连接前一层)神经元和输出(连接后一层)神经元,该神经元会将相同值传递给与之相连的多个输出神经元。每个神经元通过输入权重加上一个偏移项来计算输出值,并由所选择的激励函数(activation function)来进行转换。
有三种广泛使用的激励函数:恒等函数、Sigmoid函数和高斯函数,最常用的默认激励函数为Sigmoid函数。
一个ANN训练网络将一个特征向量作为输入,将该向量传递到隐藏层,然后通过权重和激励函数来计算结果,并将结果传递到下一层,直到最后传递给输出层才结束,输出层是指其神经元类别编号为神经网络的层数。
Opencv为ANN定义了一个CvANN_MLP类。通过create函数来初始化该类,初始化时需要指定以下参数的值:神经网络的层数、神经元数、激励数、alpha和beta。
Void OCR::train(Mat TrainData,Mat classes,int nlayers)
{
MatlayerSizes(1,3,CV_32SC1);
layerSizes.at<int>(0)=TrainData.cols;
layerSizes.at<int>(1)=nlayers;
layerSizes.at<int>(2)=numCharaters;
ann.create(layerSizes,CvANN_MLP::SIGMOID_SYM,1,1);
Mat trainClasses;
trainClasses.create(TrainData.rows,numCharacters,CV_32FC1);
for(int i=0;i<trainClasses.rows;k++)
{
For(intk=0;k<trainClasses.cols;k++)
{
If(k==classes.at<int>(i))
trainClasses.at<float>(I,k)=1;
else
trainClasses.at<float>(I,k)=0;
}
}
Mat weights(1,TrainData.rows,CV_32FC1,Scalar::all(1));
Ann.train(TrainData,trainClasses,weights);
Trained=true;
}
九、 评价
本项目到此已经完成,但当训练像OCR这样的机器学习算法时,需要知道所使用的最佳特征和参数,以及如何修正项目中出现的分类、识别和检测错误。
需要在不同情形和参数下评价当前开发的这个系统,评价错误的产生,获取让错误最小的参数。
本文用下面这些变量来评价这个OCR应用:低分辨率图像特征的大小和隐藏层的隐藏神经元数。
评价程序会获取每个下采样特征矩阵,然后取100行用作训练,而其他行用作测试ANN算法,然后给出其误差。
在训练前,要测试每个随机样本并检测其输出是否正确。如果输出不正确,将增加错误计算变量的值,然后通过将错误计算的值除以样本数来进行评价。这意味着用随机数据训练,其错误率会在0和1之间。
Float test(Mat samples, Mat classes)
{
Float errors=0;
For(int i=0;i<samples.rows;i++)
{
Intresult=ocr.classify(samples.row(i));
If(result!=classes.at<int>(i))
Errors++;
}
Return errors/samples.rows;
}