车牌识别源代码部份流程供参考一.docx
《车牌识别源代码部份流程供参考一.docx》由会员分享,可在线阅读,更多相关《车牌识别源代码部份流程供参考一.docx(44页珍藏版)》请在冰点文库上搜索。
车牌识别源代码部份流程供参考一
车牌识别源代码部份流程(供参考一)
实际上车牌识别代码量并不是很大,如果不停的手工输入;用不到一天的时间,即可大功告成。
但是程序需要反复调试,才可以走向成熟。
所以工作量是很大的。
另外车牌识别还有其本身的特点——理论并不成熟。
这就给车牌识别带来了更多的工作量。
一般来说,开发车牌识别程序,先按照最原始最朴素的思想编码,然后是不断地优化。
这将贯彻到整个车牌识别的开发过程中去。
车牌识别最基本的流程是:
将采集后的图像二值化,然后依次经过车牌定位、字符分割、去除干扰,最后是字符识别。
有时还会加入本节前面部分所叙述的思想(比如回溯)。
下面将分五章具体介绍每一个模块。
一、二值化
二值化是车牌识别的第一步。
二值化前后的对比如下图:
二值化的算法很简单,首先有一个亮度的阈值(threshold),对每一个像素的亮度和这个阈值做比较,根据比较结果得出车牌的前景和背景。
用c/c++描述如下:
voidCLPR:
:
Binary(intthreshold)
{
inty;
for(y=0;y{
intx;
for(x=0;x{
unsignedcharred,green,blue;
GetPixel(red,green,blue,x,y);
intbright;
bright=red+green;
if(m_search_blue_plate)
{
if(bright<=threshold)
SetBinary(x,y,BACKGROUND);
else
SetBinary(x,y,FOREGOUND);
}
else//wearesearchingyellowplate
{
if(bright>=threshold)
SetBinary(x,y,FOREGOUND);
else
SetBinary(x,y,BACKGROUND);
}
}
}
}
二值化算法虽然简单,但是阈值却不容易寻找。
本章后面的部分,将重点介绍各种求解阈值的算法。
1、OTSU算法
OTSU算法的思想是:
把输入图像首先转换成灰度图象,然后对图像进行直方图分析。
如果直方图呈双峰分布。
那么双峰之间的“谷”就是阈值。
从统计学角度讲,阈值两边的距离最大。
由于车牌识别的特殊性,图象象素点的亮度为该象素点的红色分量和绿色分量的和,并且忽略蓝色分量。
这一点对蓝色车牌和黄色车牌都是适用的。
OTSU算法仅对直方图呈双峰分布的图像有效。
全部代码如下:
voidLPR:
:
OTSU()
{
//直方图统计
{
intindex;
for(index=0;indexm_pixel_number[index]=0;
}
{
inty;
for(y=0;y<=m_height;y++)
{
intx;
for(x=0;x<=m_width;x++)
{
intbright;
bright=Bright(x,y);
m_pixel_number[bright]++;
}
}
}
//真正求阈值
doublesum;
sum=0;
intn;
n=0;
intk;
for(k=0;k<=(m_bright_level_count-1);k++)
{
sum+=k*m_pixel_number[k];
n+=m_pixel_number[k];
}
doublec_sum;
c_sum=0.0;
doublef_max;
f_max=-1.0;
intn1;
n1=0;
for(k=0;k<(m_bright_level_count-1);k++)
{
n1+=m_pixel_number[k];
if(n1==0)
continue;
intn2;
n2=n-n1;
if(n2==0)
break;
c_sum+=(double)k*m_pixel_number[k];
doublem_1,m_2;
m_1=c_sum/n1;
m_2=(sum-c_sum)/n2;
doublesb;
sb=(m_1-m_2)*(m_1-m_2)*(double)n1*(double)n2;
if(f_max{
f_max=sb;
m_prepare_threhold=(int)(k+0.5);
}
}
}
2、Matlab算法
使用Matlab进行车牌识别,也是一个比较好的选择。
在Matlab的环境中首先把输入的彩色图像使用命令rgb2gray转换成灰度图像。
有了灰度图像就可以使用命令graythresh获得阈值了。
最后使用命令im2bw对图像进行二值化。
十分方便!
代码如下:
I=imread('blood1.tif');
imhist(I);
%人工观察灰度直方图,发现灰度120处有谷,确定阈值T=120
I1=im2bw(I,120/255);
%im2bw函数需要将灰度值转换到[0,1]范围内
figure,imshow(I1);
改进为
I=imread('blood1.tif');
imhist(I);
I1=graythresh(I);
%im2bw函数需要将灰度值转换到[0,1]范围内
figure,imshow(I1);
二、车牌定位
图像二值化,占用车牌识别中的大部分时间,也是最难的。
在二值化之后,车牌识别将变得比较难。
车牌定位为二值化后的第一步。
下面分小节分别介绍各种车牌定位算法。
车牌识别过程中,角点定位的基本思想是。
在所有的边界点中,如果某些点的曲率半径比较小,那么这些点叫做“角点”:
如下图所示(角点用红点表示):
图中字符上和车牌的四角都有角点。
但是这并不影响车牌的定位。
根据距离最大的四个角点,得到了车牌的四个角,从而定了车牌。
从角点定位的原理看出,如果经过旋转后车牌并不会影响角点定位的成功率和速度。
该算法的实现可以采取遍历匹配的算法,实现如下:
voidLPR:
:
GetConere()
{
inty;
for(y=0;y{
intx;
for(x=0;x{
if(Line(x,y,x+4,y)>=3)
{
if(Line(x,y,x,y+4)>=3)
{
if(Line(x+1,y+1,x+4,y+4)<=1)
Add(x,y,LEFT_TOP_CONNER);
}
if(Line(x,y,x,y-4)>=3)
{
if(Line(x+1,y-1,x+4,y-4)<=1)
Add(x,y,LEFT_DOWN_CONNER);
}
}
if(Line(x,y,x-4,y)>=3)
{
if(Line(x,y,x,y+4)>=3)
{
if(Line(x-1,y+1,x-4,y+4)<=1)
Add(x,y,RIGHT_TOP_CONNER);
}
if(Line(x,y,x,y-4)>=3)
{
if(Line(x-1,y-1,x-4,y-4)<=1)
Add(x,y,RIGHT_DOWN_CONNER);
}
}
}
}
}
函数Line(x1,y1,x2,y2)返回过两点(x1,y1),(x2,y2)的直线,前景的象素个数。
注意这里的4,是检验角点的区域范围,如果区域过大,图像旋转时就会影响车牌定位的成功率。
1、上下定位方法
仔细观察二值化后的图像,在车牌的上边和下边各有一条较长的背景线(上图用红线表示)。
根据这两条背景线可以准确的定位车牌。
定位算法如下:
intLPR:
:
HorizontalLine(intx,inty,intcount)
{
intret;
ret=0;
intx_loop;
for(x_loop=0;x_loopif(!
IsForegournd(x+x_loop,y))
ret++;
returnret;
}
IsForegournd(x,y)为询问点(x,y)是不是前景点的函数。
该算法比角点定位算法要快,但是不适合经过旋转后的车牌。
虽然经过改进后也可以识别出旋转后的车牌,但是速度很慢,不能出现在成熟的产品中。
2、变化率定位法
根据尺寸分割
从理论上讲,图片和实物相比,尺寸上有了很大的变化。
并不一定图片和实物几何意义上的相似。
也就是说未必图像和实物成比例。
但是实验证实,在一到两个像素范围内,在水平方向上,实物和图像基本上成比例。
请观察下图:
这个是来自于《GA36-2007中华人民共和国机动车号牌》的车牌尺寸说明。
根据这幅图片,我们可以在一定位的车牌上,找到各个字符的坐标。
请看下面的代码:
voidGetCharacterPosition(intcharacter_position[7],intplate_left,intplate_right)
{
staticconstintmm[]=
{
3+45/2,
character_position_mm[0]+12+45,
character_position_mm[1]+12+10+12+45,
character_position_mm[2]+12+45,
character_position_mm[3]+12+45,
character_position_mm[4]+12+45,
character_position_mm[5]+12+45,
character_position_mm[6]+45/2+1,
};
intindex;
for(index=0;indexcharacter_position[index]=plate_left+(-plate_left+plate_right)*mm[index]/mm[sizeof(mm)/sizeof(mm[0])-1];
}
上面的代码技巧性很大,还需大家认真揣摩。
去除干扰
干扰车牌识别的因素很多。
比如车牌旋转、污染、固定螺丝和车牌边框等等。
下面分小节分别介绍去除各种干扰的方法。
去除噪音
去除噪音的原理是:
每一个字符,都是很大的一个连续块,但是噪音确是比较小的多个连续块。
通过递归算法得到每个字符中各个块的大小,保留最大的块,其余块当作噪音删掉。
算法很简单,这里不再列出代码。
去除螺丝干扰
去除螺丝的工作要和字符本身的特征联系在一起。
对不同的字符、相同字符不同部位的螺丝,都要分别编码。
工作量很大。
下面分小节举几个例子:
10986等左上方螺丝
这种情况如下图所示:
在比较圆的9的上部出现了一个螺丝,比较明显。
可以删掉,代码如下:
voidCLPR:
:
DeleteLeftUpSmallScrewCharacterRound()
{
boolpossible_screw;
intpossible_screw_start;
intpossible_screw_end;
if(m_character_index==5)
{
possible_screw=false;
{
intseek;
for(seek=0;seek<=1;seek++)
{
possible_screw_start=m_min_y+seek;
intscrew_right;
if(ExpandHorizontal(screw_right,possible_screw_start,possible_screw_start,m_max_x,SUB))
{
if(screw_right>=m_delta_x/2)
{
possible_screw=true;
break;
}
}
}
}
intdelta_x;
if(possible_screw)
{
possible_screw=false;
for(possible_screw_end=possible_screw_start+1;possible_screw_end{
delta_x=RecognizeCharacterLocateVeritcalSmallSideScrewGetDeltaX(true,possible_screw_end,true);
intleft;
if(!
ExpandHorizontal(left,possible_screw_end,possible_screw_end,m_min_x,ADD))
continue;
if(left>m_delta_x/2)
continue;
intright;
if(!
ExpandHorizontal(right,possible_screw_end,possible_screw_end,m_max_x,SUB))
continue;
if(right>delta_x/2)
continue;
if(possible_screw_end+4>m_center_y)
continue;
intdistance1;
distance1=HorizontalDistance(possible_screw_end,possible_screw_end);
intdistance2;
distance2=HorizontalDistance(possible_screw_end+2,possible_screw_end+2);
intdistance3;
distance3=HorizontalDistance(possible_screw_end+4,possible_screw_end+4);
if(!
(distance1<=distance2&&distance2<=distance3&&distance10))
continue;
if(distance3continue;
intseek;
for(seek=2;seek<=4;seek++)
{
if(ChangeTimeHorizontal(possible_screw_end+seek,possible_screw_end+seek)==4)
{
possible_screw=true;
break;
}
}
if(possible_screw)
break;
}
}
if(possible_screw)
{
intleft;
if(!
ExpandHorizontal(left,possible_screw_end-1,possible_screw_end-1,m_min_x,ADD))
possible_screw=false;
else
{
if(left>=m_delta_x/2)
possible_screw=false;
}
}
if(possible_screw)
{
boolknown_letter;
known_letter=false;
if(!
known_letter)
{
m_min_y=possible_screw_end;
RecognizeCharacterAdjustVertical();
}
}
}
}
以上代码拷贝自深职院张教授车牌识别工程(索威尔),实际上是2003年的版本。
读者可以进行修改,并放入自己工程中。
2EFT5等右上方螺丝
这种情况如下图所示:
图中有三条直线,如果沿着这三条直线扫描的话,会发现:
黑色的扫描线前景占很大的比例,绿色的扫描线背景占很大的比例;红色的扫描线前景背景比例都不突出,但是前景背景交替较为频繁。
这说明根据前景背景变化率可以定位车牌。
这种算法可以有效防止车牌旋转的干扰。
获得变化率的代码如下:
doubleCLPR:
:
ScanLine(intx,inty,intcount)
{
boolcurrent_foreground;
current_foreground=IsForeground(x,y);
intchange_times;
change_times=0;
intloop;
for(loop=1;loop{
if(current_foreground)
{
if(!
IsForeground(x+loop,y))
{
change_times++;
current_foreground=false;
}
}
else
{
if(IsForeground(x+loop,y))
{
change_times++;
current_foreground=true;
}
}
}
doubleret;
ret=change_times;
ret/=(count+1);//donotdiv0
returnret;
}
三、字符分割
在车牌识别过程中,车牌定位后的工作便是分割字符。
本章将分若干节介绍字符分割的算法。
连续点分割法
对于一个数字或者字母,前景的点是连续的。
用填充算法对种子点填充即可得到整个字符。
当然汉字就不是了,所以这种分割算法仅仅适合数字或者字母。
但是当整个车牌的数字和字母都得到之后,剩下的那个必定是汉字。
这个思想用c/c++描述如下:
voidLPR:
:
Scan()
{
inty;
for(y=m_plate_top;y<=m_plate_bottom;y++)
{
intx;
for(x=m_plate_left;x<=m_plate_right;x++)
{
if(IsForeGround(x,y)&&!
IsVisited(x,y))
{
Fill(x,y);
}
}
}
}
Fill是种子填充,算法有多种。
下面分别介绍。
1、递归填充算法
递归填充算法的基本思想是,首先访问当前点,然后访问当前点的四个邻居。
每次访问时,要做一个记号,否则递归过程无法结束。
具体到车牌识别,每访问一个点,还要记录该点的坐标,从而得到当前字符点的集合。
voidLPR:
:
Fill(intx,inty)
{
if(!
IsForground(x,y))
return;
if(IsVisited(x,y))
return;
AddPixelToCharacter(x,y);
MarkPixelVisited(x,y);
Fill(x+1,y);
Fill(x,y+1);
Fill(x-1,y);
Fill(x,y-1);
}
可以看出用c/c++描述的填充算法十分简单。
2、递归扫描线算法
基本的递归填充算法,函数递归调用较为频繁,引起系统资源消耗巨大。
人们后来又提出了改进了的“扫描线种子填充算法”。
其基本思想是:
在种子的左右两边水平扫描暂时不需要递归,以减少不必要的函数调用。
水平扫描结束后,仅仅考虑扫描线两个端点即可。
算法如下:
voidLPR:
:
Fill(intx,inty)
{
int(*stack)[2];
stack=(int(*)[2])newint[m_width*2];
intstack_length;
stack_length=0;
stack[stack_length][0]=x;
stack[stack_length][1]=y;
stack_length++;
while(true)
{
if(stack_length==0)
break;
x=stack[stack_length-1][0];
y=stack[stack_length-1][1];
stack_length--;
intleft_x;
for(left_x=x-1;left_x>=0;left_x--)
if(!
IsForeGround(left_x,y)||IsVisited(left_x,y))
break;
left_x++;
for(x=left_x;x{
if(!
IsForeGround(x,y)||IsVisited(left_x,y))
break;
Visit(x,y);
}
intright_x;
right_x=x;
if(right_x>=m_width)
right_x=m_width-1;
left_x--;
if(left_x<0)
left_x=0;
intdown_y;
down_y=y+1;
if(down_y{
for(x=left_x;x<=right_x;x++)
{
if(IsForeGround(x,down_y)&&!
IsVisited(left_x,y))
{
stack[stack_length][0]=x;
stack[stack_length][1]=down_y;
stack_length++;
}
}
}
intup_y;
up_y=y-1;
if(up_y>=0)
{
for(x=left_x;x<=right_x;x++)
{
if(IsForeGround(x,up_y)&&!
IsVisited(left_x,y))
{
stack[stack_length][0]=x;
stack[stack_length][1]=up_y;
stack_length++;
}
}
}
}
delete[](int*)stack;
}
虽然扫描线种子填充算法,比基本的种子填充算法复杂一些,但是在车牌识别测试后发现,扫描线种子填充算法比种子填充算法要快三分之一左右。
边界法
边界法和连续点分割法很相似,只不过连续的边界点,而不是所有的前景点。
边界法需要事先得到前景像素中的边界点,
这可能会花费一些时间。
但是这样会加速得到连续点的递归操作。
边界点如下图:
得到边界点的算法如下:
voidCLPR:
:
GetEdge()
{
inty;
for(y=0;y{
intx;
for(x=0;x{
if(IsForeg