博主渣渣本科一枚,毕业设计选了一个基于OpenCV的车牌识别的题目,在此记下其中用到的一些关键技术备忘,也希望可以给后来人些许启发。
车牌识别的第一步自然是想办法把车牌从一张图片中提取出来,也就是所谓的车牌定位。目前方法有很多,我采用的是基于边缘检测的车牌定位方案。
一般来说由于车牌区域有车牌字符的存在,所以会有相当丰富的边缘信息,所以可以求取车牌的边缘图像,然后把所有分布密集的边缘聚合在一起就可以得到一些候选区域,而这些候选区域中就应当包含有我们要找的车牌区域,这时候只要再通过候选区域的长宽比,颜色等信息就可以找到车牌了。
思路讲完了,现在开始正题:
首先要把RGB彩色图像转为灰度图像,这一步无需多讲,OpenCV自带库函数void cvCvtColor( const CvArr* src, CvArr* dst, int code );一行直接搞定,其中src表示输入的源彩色图像,dst存放输出的灰度图像,code选CV_RGB2GRAY得到灰度图像
对比度增强这一步是为了让图像中的边缘更加明显。这里采用基于顶帽变换和底帽变换的方法来增强对比度。即:
Mat tophat,blackhat; //分别用于保存顶帽变换和底帽变换后的图像
Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
morphologyEx(temp, tophat, MORPH_TOPHAT, element, Point(-1, -1)); //这里的temp是源图像
morphologyEx(temp, blackhat, MORPH_BLACKHAT, element, Point(-1, -1));
add(temp, tophat, temp);
subtract(temp, blackhat,temp);
边缘检测有很多办法,比如Sobel,Canny,Laplace等等,我采用的是Sobel算子对图像作水平差分,以求取垂直边缘。因为由于车牌区域的边缘是以垂直边缘为主,一般的背景区域不会有密集的垂直边缘,如果求取全方向上的边缘的话,在背景比较复杂的时候,背景区域也会存在更多边缘,带来干扰。以Canny算子为例,虽然检测效果看起来更好,但是非车牌的背景区域同样检测出了太多无用的边缘,不利于后续处理。
GaussianBlur(temp, temp, Size(9, 3), 0, 0); //高斯平滑,这里temp代表待处理图像
Sobel(temp, temp, CV_8U, 2, 0, 3); //利用Sobel算子进行二阶水平差分,求取垂直边缘
//Scharr(temp, temp, CV_8U, 1, 0, 1, 0, BORDER_DEFAULT);
threshold(temp, temp, 0, 255, CV_THRESH_BINARY + CV_THRESH_OTSU);//二值化
在得到边缘图像之后只要再经过一些形态学操作就可以得到候选区域。这里我们可以先进行开运算,开运算是对图像先腐蚀后膨胀,在腐蚀过程中,可以去除背景中一些细小的杂点,以及比较细的边缘,而车牌区域由于边缘密集所以不容易被完全腐蚀掉,再经过膨胀操作就可以恢复出车牌区域。开运算也可以通过OpenCV库函数morphologyEx实现,这里不再赘述。
反复执行以上操作至图像不再变化。不过这不操作略微费时,实际操作时可以先对图像求取水平和垂直投影,然后只对投影值大于一定阈值的区域进行矩形化,这样可以省下部分时间开销。附上结果和部分代码:
//参数src为待处理源图像
//参数dst用于存储处理后的图像
//x,y两个数组存放水平和垂直投影值
void rectanglize(IplImage *src, IplImage * dst,int * x,int * y) //矩形化
{
double temp = 0;
CvScalar white, black;
white.val[0] = 255;
black.val[0] = 0;
for (int i = 1; i < src->height - 1; i++)
{
for (int j = 1; j < src->width - 1; j++)
{
if (y[i]>15 && x[j]>5)
{
temp = cvGet2D(src, i, j - 1).val[0] + cvGet2D
(src, i, j + 1).val[0] + cvGet2D(src, i - 1, j).val[0] + cvGet2D(src, i + 1, j).val[0];
if (255 == cvGet2D(src, i, j).val[0])
{
if (0 == temp)
cvSet2D(dst, i, j, black); //如果该白点周围都是黑点,则设置改点为黑点
else
cvSet2D(dst, i, j, white); //反之则为白点
}
else
{
if (temp >= 255 * 2)
cvSet2D(dst, i, j, white); //如果该黑点周围存在两个及以上的白点,则设置改点为白点
else
cvSet2D(dst, i, j, black); //反之则为黑点
}
}
}
}
//将图像边缘设置为黑色
for (int i = 0; i < src->height; i++)
{
cvSet2D(dst, i, 0, black);
cvSet2D(dst, i, src->width - 1, black);
}
for (int j = 0; j < src->width; j++)
{
cvSet2D(dst, 0, j, black);
cvSet2D(dst, src->height-1, j, black);
}
}
最后一步,区域筛选,从所有候选区域选出车牌区域,这一步就比较简单啦,比较明显的判据就是车牌的长宽比,过长或者过宽的区域都不会车牌。直接上代码,这一步需要说明下,src是要判断的图像,用cvFindContours函数可以找出图像中所有连通域,用CvSeq类型的双向链表表示,接着只要遍历链表节点,移除不合适的节点就可以完成筛选了。但是不知道怎么回事,用OpenCV自带的cvSeqRemove函数貌似无法删除节点,自己写了一个删除节点的函数还是不行,移植别人删除节点的代码还是不行,当时快把我搞疯了(╯°Д°)╯︵┻━┻,所以我自己建了一个结构体plateinfo构成链表来存放车牌在图像中的位置。
//plateinfo定义
typedef struct plateinfo //车牌信息
{
CvRect rect; //车牌位置
IplImage * plateregion; //车牌区域
//IplImage * plateregion = cvCreateImage(cvSize(132, 42), IPL_DEPTH_8U, 1); //车牌区域
PlateChar * plate_num = NULL; //车牌号
struct plateinfo * next = NULL;
}Plate;
void screen(IplImage * src)
{
//IplImage* dst = cvCreateImage(cvGetSize(src), 8, 3);
CvMemStorage * storage = cvCreateMemStorage(0);
CvSeq * temp = 0;
int totals = cvFindContours(src, storage, &temp, sizeof(CvContour), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE, cvPoint(0, 0));
cout << "find " << totals << " contours" << endl;
int total = totals;
//plate = contours;
double minarea = 100;
double templarea;
int flag = 1;
/*int * flag=new int [totals];
memset(flag, 0,totals * 4);*/
//temp = plate;
for (int i = 0; temp != NULL; temp = temp->h_next,i++)
{
templarea = fabs(cvContourArea(temp));
//cout << templarea << endl;
if (templarea < minarea) //除去面积过小的区域
{
cvSeqRemove(temp, 0);
//mySeqRemove(&temp);
totals--;
continue;
}
CvRect aRect = cvBoundingRect(temp, 0);
if (float(aRect.width / aRect.height)<2 || float(aRect.width / aRect.height)>4) //除去高比例过大和过小的区域
{
cvSeqRemove(temp, 0);
//mySeqRemove(&temp);
totals--;
continue;
}
if (0 == colorScreen(temp)) //除去颜色不符合的区域
{
cvSeqRemove(temp, 0);
totals--;
continue;
}
//flag[i] = 1;
if (1 == flag)
{
aRect.x -= int(aRect.width*0.1);
//aRect.y -= int(aRect.height*0.1);
aRect.width = int(aRect.width*1.2);
aRect.height = int(aRect.height*1.2);
firstplate->rect = aRect;
flag = 0;
}
else
{
aRect.x -= int(aRect.width*0.1);
//aRect.y -= int(aRect.height*0.1);
aRect.width = int(aRect.width*1.2);
aRect.height = int(aRect.height*1.2);
Plate * newplate = new Plate;
newplate->rect = aRect;
constructPlateList(firstplate, newplate);
}
/*CvScalar red = CV_RGB(255, 0, 0);
cvDrawContours(source, contours, red, red, 0, 1, 8);*/
}
cout << "find " << totals << " plates" << endl;
if (0 == totals)
{
delete(firstplate);
firstplate = NULL;
cout << "find no plate" << endl;
exit(0);
}
//int count = 0;
//for (int i = 0; i < total; i++)
//{
// cout << flag[i]<
// if (0 == flag[i])
// {
//
// }
// //mySeqRemove(&plate, count);
// else
// {
// CvScalar red = CV_RGB(255, 0, 0);
// cvDrawContours(source, plate, red, red, 0, 1, 8);
// count++;
// }
//}
}
另外我在筛选过程中加上了颜色判断步骤colorScreen,一般来说长宽比已经足够判断出车牌了,而且后期字符分割时可以再做一次判断:如果分不出7个字符,那么同样不是车牌。所以这一步本身不是特别重要,而且由于不同图像光照明暗的变化有所不同,难以选取出统一的阈值。所以我把判断条件放的特别宽,这一步只是单纯为了保险,不能作为唯一判据。送上代码:
bool colorScreen(CvSeq * contour) //根据颜色判断待选区域是否为车牌
{
CvRect rect = cvBoundingRect(contour, 0);
double area = fabs(cvContourArea(contour));
CvMat * temp = cvCreateMatHeader(rect.height, rect.width,CV_8UC3);
cvGetSubRect(source, temp, rect);
IplImage * _tempimg = cvGetImage(temp, cvCreateImageHeader(cvSize(rect.width, rect.height), IPL_DEPTH_8U,3));
IplImage * tempimg = cvCreateImage(cvSize(rect.width, rect.height), IPL_DEPTH_8U, 3);
cvCopy(_tempimg, tempimg); //需将_tempimg复制到tempimg中,对_tempimg操作会改变原图
//cvCvtColor(tempimg, tempimg, CV_BGR2HSV); //颜色空间由RGB转到HSV
rgb2hsi(tempimg, tempimg); //颜色空间由RGB转到HSI
CvScalar color;
CvScalar black = CV_RGB(0, 0, 0);
CvScalar red = CV_RGB(255, 0, 0);
double blue = 0;
double white = 0;
for (int i = 0; i < tempimg->height; i++)
{
for (int j = 0; j < tempimg->width; j++)
{
color = cvGet2D(tempimg, i, j);//color.val[0]为H,val.[1]为S,val.[2]为V
//if (color.val[0]>90 && color.val[0]<120 && color.val[1]>130)//HSV方案
if (color.val[0]>255 * 190 / 360 && color.val[0]<255 * 260 / 360 && color.val[1]>255 * 30 / 100)//HSI方案
{
blue++;
//cvSet2D(source, i, j, black);
}
//if (color.val[1] < 100 && color.val[2]>180)//HSV方案
if (color.val[1] < 255 * 30 / 100 && color.val[2]>255 * 60 / 100)//HSI方案
{
white++;
//cvSet2D(source, i, j, red);
}
}
}
//cout << "area= " << area << " blue: "<height<<","<width<
/*cvReleaseImage(&tempimg);
cvReleaseMat(&temp);*/
/*showpic("source", source);
cvWaitKey(0);*/
if ((blue + white) > (tempimg->width*temp->height*0.2) && blue / (white + 1) > 1 && blue / (white + 1) < 12)
return 1;
else
return 0;
}