图像车牌识别关键就是要从图像中准确地定位出车牌的位置,并将车牌部分的图像分割出来。
本文设计了一个简易的车牌识别识别系统,现介绍其车牌定位和分割部分。
在设计中我采用了颜色特征和形态特征相结合的方法对车牌进行定位。车牌区域有很丰富的特征信息,如颜色,不同性质的车牌会有不同的颜色,大多数普通的车牌就是字符为白色,背景为蓝色。
<1>HSV颜色模型
HSV是根据颜色的直观特性创建的一种颜色空间,其中H表示色调,S表示饱和度,V表示亮度。
<2>车牌的形态特征
根据HSV模型,对图像进行针对颜色的图像处理及腐蚀去噪之后,图像除少量噪声之外,主要的区域就是车牌区域。车牌区域由于有多个字符,所以每行都存在多个灰度跳变,由这一特征可以确定车牌在两行区域之间,在已确定的两行之间,在采取同样的方法确定区域边界的两列。这样就可以确定车牌的大致区域了,可以修改相关的参数,如跳变的次数等,在大致区域内进行二次定位,本实验在定位操作过程中就进行了两次定位。
<3>车牌分割
找出车牌区域之后,必须将车牌上的字符分割出来,这过程包括两个步骤,第一首先得将车牌从背景中分割出来,第二再将车牌的各个字符分别分割出来。
对于第一步,在对图像进行相关颜色处理之后,已经将字符从背景之中分割出来了,即本实验的分割1操作。本实验的分割2则使用了阈值分割的方法,通过将图像转换为灰度图像,选取适当的阈值,对其进行二值化。
第二步将字符分别分割出来,字符分割通常可以选择连通域的方法,本实验对字符的分割则是根据车牌字符排列位置特征:车牌有七个字符,第一个为汉字,第二个为字母,其余五个为数字和字母,第二个字符和第三个字符之间有一个点(算半个字符)。字符的宽度是一样,所以依据字符的宽度就可以较为准确将单个字符分别分割出来。
<4>利用颜色特征和形态特征对车牌进行定位的具体实现
根据车牌底色为蓝色,字符为白色的特征,首先将图像由BGR色彩空间转换到HSV色彩空间,这里注意由于调用opencv函数()将图像由BGR色彩空间转换为HSV色彩空间时,HSV空间的H取值为0到180(HSV色彩空间规定H取值的一半),S的取值为0到255,V的取值为0到255。转换为HSV空间之后,调用opencv库中的cvSplit函数将HSV色彩空间的H、S、V三个通道的图像分别复制到imgH、imgS、imgV三个单通道图像中。在H取值范围为0到180的情况下蓝色的H取值大概在94到115之间,这样就可以使用opencv库中cvInRangeS函数,将imgH属于蓝色区域部分的像素灰度置1,其余部分置0。同理,对imgS和imgV同样选取适当的取值范围,根据像素值范围进行二值化。最后将三个二值化后的图像按位求与,得到的图像就是同时满足蓝色的H、S、V三个通道的值范围,进行一些去噪操作后,可以得到效果较为好的车牌图像。
<5>字符分割
在车牌定位的过程中,得到两种处理后的图像,一是基于颜色处理得到的imgHsvBinnary,另外一个就是基于阈值分割处理得到的imgRgbBinnary。将字符单个分割出来的算法就是简单地根据字符之间的间距和字符个数特点,七个字符加一圆点,以7.5个字符进行计算,在分割出来的车牌的基础上,将车牌的宽度分成7.5份,每份的宽度就是7.5分之一的车牌宽度,通过对车牌区域设定相应位置的ROI,可以将字符比较准确地分割出来。效果还不错,如图:
<6>代码的实现
编程环境选择VS2010+opencv,以VS2010作为开发平台,opencv作为图像处理的一个函数库,通过配置VS2010,使opencv成为VS2010开发平台的一个函数库,并基于MFC开发了一个简易的GUI。
(1)车牌定位代码段(车牌定位按钮相应代码,将定位好的车牌用彩色线条在原图中标记出来)
//图像预处理(根据颜色特征)
IplImage *imgH=NULL,*imgS=NULL,*imgV=NULL,*imgHSV=NULL,*imgGray=NULL;
imgHSV=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,3);
imgH=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
imgS=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
imgV=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
cvCvtColor(SrcImage,imgHSV,CV_BGR2HSV); // 转为HSV图像
cvSplit(imgHSV,imgH,imgS,imgV,NULL); //将多个通道分别复制到各个单通道图像中
cvInRangeS(imgH,cvScalar(94,0,0,0),cvScalar(115,0,0,0),imgH); //选取各个通道的像素值范围,在范围内的则
cvInRangeS(imgS,cvScalar(90,0,0,0),cvScalar(255,0,0,0),imgS); //置1,否则置0(按颜色进行二值化)
cvInRangeS(imgV,cvScalar(36,0,0,0),cvScalar(255,0,0,0),imgV);
IplImage *imgTemp=NULL,*imgHsvBinnary=NULL;
imgTemp=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
imgHsvBinnary=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
cvAnd(imgH,imgS,imgTemp);
cvAnd(imgTemp,imgV,imgHsvBinnary); //H,S,V三个通道分别按位求与,将所得的单通道图像保存到imgHsvBinnary中
//形态学去噪
//定义结构元素
IplConvKernel *element=0; //自定义核
int values[2]={255,255};
int rows=2,cols=1,anchor_x=0,anchor_y=1;
element = cvCreateStructuringElementEx(cols,rows,anchor_x,anchor_y,CV_SHAPE_CUSTOM,values);
cvDilate(imgHsvBinnary,imgHsvBinnary,element,1); //膨胀腐蚀
cvErode(imgHsvBinnary,imgHsvBinnary,element,2); //多次腐蚀(2次),消除噪声
cvNamedWindow("imgh1");
cvShowImage("imgh1",imgHsvBinnary); //效果不错(但对于其他颜色貌似就没什么作用)
//阈值分割
imgGray=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
cvCvtColor(SrcImage,imgGray,CV_RGB2GRAY); //将原图转为单通道的灰色图像
IplImage *imgRgbBinnary;
imgRgbBinnary=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,1);
int Thresold=myOtsu(imgGray); //利用大津法求灰色图像阈值
cvThreshold(imgGray,imgRgbBinnary,Thresold,255,CV_THRESH_OTSU); //利用大津阈值进行二值化
cvNamedWindow("imgh2");
cvShowImage("imgh2",imgRgbBinnary); //显示二值化图像
//车牌定位
//行定位(根据车牌的区域的图像特征进行定位)
int hop_num=10; //字符连续跳变次数的阈值
int num=0; //计算跳变的次数
int begin=0; //跳变是否开始
int mark_Row[2]={0},k=0;//第一次标记车牌的开始行与结束行
int mark_Row1[2]={0}; //第二次标记
//第一次定位
for(int i=SrcImage->height-1;i>=0;i--)
{
num=0;
for(int j=0;jwidth-1;j++)
{
if(cvGet2D(imgHsvBinnary,i,j).val[0]!=cvGet2D(imgHsvBinnary,i,j+1).val[0]) //左右两列的值不等则视为一次跳变
{
num++;
}
}
if(num>hop_num)
{
mark_Row[k]=i;
k=1;
}
}
//cvLine(SrcImage,cvPoint(0,mark_Row[0]),cvPoint(SrcImage->width,mark_Row[0]),CV_RGB(255,255,0)); //在原图中画出所标记的两行
//cvLine(SrcImage,cvPoint(0,mark_Row[1]),cvPoint(SrcImage->width,mark_Row[1]),CV_RGB(255,255,0));
//列定位
int mark_col[2]={0},mark_col1[2]={0},num_col=0,k_col=0;
int a[100]={0},Thresold_col=7;
for(int j=0;jwidth;j++)
{
num_col=0;
for(int i=mark_Row[1];i0];i++) //只扫描已经标记的两行之间的图像
if(cvGet2D(imgHsvBinnary,i,j).val[0]>0)
num_col++;
if(num_col>Thresold_col)
{
mark_col[k_col]=j;
k_col=1;
}
}
int i=0;
//cvLine(SrcImage,cvPoint(mark_col[0],0),cvPoint(mark_col[0],SrcImage->height),CV_RGB(255,0,0));
//cvLine(SrcImage,cvPoint(mark_col[1],0),cvPoint(mark_col[1],SrcImage->height),CV_RGB(255,0,0));
IplImage *imgLicense;
int license_Width1=(mark_col[1]-mark_col[0]);
int license_Height1 =mark_Row[0]-mark_Row[1];
if(license_Width1/license_Height1<3) //根据车牌的宽度和高度比对车牌区域进行修正
{
int real_height1 = license_Width1/3; //车牌的宽度和高度比大概为3:1
mark_Row[1] = mark_Row[0]-real_height1;
license_Height1 = real_height1;
}
//第二次定位(在第一次定位的基础之上)
k=0;
for(int i=mark_Row[0];i>mark_Row[1];i--)
{
num=0;
for(int j=mark_col[0];j1];j++)
{
if(cvGet2D(imgHsvBinnary,i,j).val[0]!=cvGet2D(imgHsvBinnary,i,j+1).val[0]) //左右两列的值不等则视为一次跳变
{
num++;
}
}
if(num>8)
{
mark_Row1[k]=i;
k=1;
}
}
k_col=0;
for(int j=mark_col[0];j1];j++)
{
num_col=0;
for(int i=mark_Row1[1];i0];i++) //只扫描已经标记的两行之间的图像
if(cvGet2D(imgHsvBinnary,i,j).val[0]>0)
num_col++;
if(num_col>6)
{
mark_col1[k_col]=j;
k_col=1;
}
}
int license_Width=(mark_col1[1]-mark_col1[0]);
int license_Height =mark_Row1[0]-mark_Row1[1];
if(license_Width/license_Height<3) //根据宽度和高度比再次修正
{
int real_height = license_Width/3; //车牌的宽度和高度比大概为3:1
mark_Row1[1] = mark_Row1[0]-real_height;
license_Height = real_height;
}
cvSetImageROI(SrcImage,cvRect(mark_col1[0],mark_Row1[1],license_Width,license_Height)); //将车牌区域设置为ROI区域
cvSetImageROI(imgRgbBinnary,cvRect(mark_col1[0],mark_Row1[1],license_Width,license_Height));
cvSetImageROI(imgHsvBinnary,cvRect(mark_col1[0],mark_Row1[1],license_Width,license_Height));
imgLicense=cvCreateImage(cvGetSize(SrcImage),SrcImage->depth,SrcImage->nChannels); //用于显示的车牌图片
SrcLicenseimg1=cvCreateImage(cvGetSize(imgRgbBinnary),imgRgbBinnary->depth,imgRgbBinnary->nChannels);
SrcLicenseimg2=cvCreateImage(cvGetSize(imgHsvBinnary),imgHsvBinnary->depth,imgHsvBinnary->nChannels);
cvCopy(SrcImage,imgLicense,0);
cvCopy(imgRgbBinnary,SrcLicenseimg1,0); //将车牌区域拷贝到相应的图像中
cvCopy(imgHsvBinnary,SrcLicenseimg2,0);
//cvNamedWindow("SrcLicenseimg1"); //显示车牌的二值化化图片
//cvShowImage("SrcLicenseimg1",SrcLicenseimg1);
//cvNamedWindow("SrcLicenseimg2");
//cvShowImage("SrcLicenseimg2",SrcLicenseimg2);
cvResetImageROI(SrcImage); //取消ROI设置
cvResetImageROI(imgRgbBinnary);
cvResetImageROI(imgHsvBinnary);
//cvNamedWindow("license");
//cvShowImage("license",imgLicense);
cvResize(imgLicense,TheImage_plate);
ShowImage(TheImage_plate,IDC_LICENSE_AREA); //显示车牌
cvLine(SrcImage,cvPoint(mark_col1[0],mark_Row1[0]),cvPoint(mark_col1[1],mark_Row1[0]),CV_RGB(255,0,0)); //在原图像中画出车牌区域
cvLine(SrcImage,cvPoint(mark_col1[1],mark_Row1[0]),cvPoint(mark_col1[1],mark_Row1[1]),CV_RGB(255,0,0));
cvLine(SrcImage,cvPoint(mark_col1[1],mark_Row1[1]),cvPoint(mark_col1[0],mark_Row1[1]),CV_RGB(255,0,0));
cvLine(SrcImage,cvPoint(mark_col1[0],mark_Row1[1]),cvPoint(mark_col1[0],mark_Row1[0]),CV_RGB(255,0,0));
//cvNamedWindow("src");
//cvShowImage("src",SrcImage);
cvResize( SrcImage, TheImage ); //显示原图(已经画出了车牌区域)
ShowImage( TheImage, IDC_VIDEO_SHOW );
}
//求取二值化的阈值
int CLICENSE_PLATE_RECONGNITIONDlg::myOtsu(IplImage *frame)
{
int width = frame->width;
int height = frame->height;
int pixelCount[GrayScale]={0};
float pixelPro[GrayScale]={0};
int i, j, pixelSum = width * height, threshold = 0;
uchar* data = (uchar*)frame->imageData;
//统计每个灰度级中像素的个数
for(i = 0; i < height; i++)
{
for(j = 0;j < width;j++)
{
pixelCount[(int)data[i * width + j]]++;
}
}
//计算每个灰度级的像素数目占整幅图像的比例
for(i = 0; i < GrayScale; i++)
{
pixelPro[i] = (float)pixelCount[i] / pixelSum;
}
//遍历灰度级[0,255],寻找合适的threshold
float w0, w1, u0tmp, u1tmp, u0, u1, deltaTmp, deltaMax = 0;
for(i = 0; i < GrayScale; i++)
{
w0 = w1 = u0tmp = u1tmp = u0 = u1 = deltaTmp = 0;
for(j = 0; j < GrayScale; j++)
{
if(j <= i) //背景部分
{
w0 += pixelPro[j];
u0tmp += j * pixelPro[j];
}
else //前景部分
{
w1 += pixelPro[j];
u1tmp += j * pixelPro[j];
}
}
u0 = u0tmp / w0;
u1 = u1tmp / w1;
deltaTmp = (float)(w0 *w1* pow((u0 - u1), 2)) ;
if(deltaTmp > deltaMax)
{
deltaMax = deltaTmp;
threshold = i;
}
}
return threshold;