基于模板匹配的车牌识别系统实例

       这几天其实是准备做课题的,无奈车牌识别系统(界面是VS2017+Qt5.9.2做的)一直没有做完,所以一直在修正这个系统,前三天其实已经完成了,最后一天是改进识别方案,虽然个别字符识别不准确(尤其是汉字),Anyway,效果比着第三天的结果有明显的提升,先看一下识别效果:

       

                                 

     我努力的克制自己不要讲废话,下面我就尽量简洁的讲一下实现方法吧

大致流程如下:

                                      

1.图像预处理:

 这里的主要思想是从张图片中提取车牌,那么怎么确定车牌呢?就是利用图像从RGB空间向HSV空间的转换,H是色相(hue),S是饱和度(saturation),V是色调(value),由于车牌是蓝色底,白色字体,如果将所有除蓝色之外的颜色都设置成黑色,那么不就可以把车牌与背景明显区分开了吗。

2.车牌定位:

  经过1的步骤之后,再把图像灰度化,就变成更容易区分车牌的图片了,想想,如果一副背景都是黑色的,中间有一个小矩形区域是白色的图片,怎么才能把白色区域分割出来,很简单,由于黑色区域是灰度值为零的区域,白色的灰度值不为0,所以只要统计每行灰度值不为0的像素点个数,然后在竖直方向上投影,再然后计算出连续不为0的宽度(下图y1-y2的宽度),就是车牌的高度了。如下图所示:


同理,在水平方向上投影,也就计算出车牌的宽度了,如下图:


void PlateRecognition::on_actionPlateExtraction_P_triggered()
{
	//读入原始图像
	IplImage *pSrc_Image = NULL;
	pSrc_Image = cvCreateImage(cvGetSize(src_image),src_image->depth,src_image->nChannels);
	pSrc_Image = cvCloneImage(src_image);

	//hsv:存放hsv颜色空间图像
	IplImage* hsv = cvCreateImage(cvGetSize(pSrc_Image), IPL_DEPTH_8U, 3);
	cvCvtColor(pSrc_Image, hsv, CV_BGR2HSV);//转换RGB2HSV

											//blue:存放蓝色范围内图像
	IplImage* blue = cvCreateImage(cvGetSize(pSrc_Image), IPL_DEPTH_8U, 1);
	cvCvtColor(pSrc_Image, blue, CV_BGR2GRAY);

	getBlueMask(hsv, blue);//提取符合颜色要求的像素点位置

	IplImage* bin = cvCreateImage(cvGetSize(pSrc_Image), IPL_DEPTH_8U, 1);//bin:单通道二值图像
	cvThreshold(blue, bin, 0, 255, CV_THRESH_BINARY);

	IplImage* image = cvCreateImage(cvGetSize(pSrc_Image), IPL_DEPTH_8U, 1);
	cvCopy(bin, image);

	//imageShow(ui.label_3,image);
	//对车辆图像做水平投影
	//定义像素累加和数组,图像高度不应大于2048
	int level_shadow[2048];
	int height = pSrc_Image->height;
	int width = pSrc_Image->width;
	CvScalar s_shadow;

	//清空并初始化数组
	memset(level_shadow, 0, sizeof(level_shadow));

	//对图像做水平投影
	for (int y = height - 1; y >= 0; y--)
	{
		for (int x = 0; x<width; x++)
		{
			s_shadow = cvGet2D(image, y, x);
			if (s_shadow.val[0] != 0)
				level_shadow[y]++; //统计每行不为零的像素的个数				
		}
	}

	//对图像水平投影的值做聚类,如果小行数值除以大行数值的商大于小于0.6,
	//则认为这两行不是一类,将小行数值置为0
	for (int y = height - 1; y >= 1; y--)
	{
		if (level_shadow[y - 1] != 0)
		{
			if ((float(level_shadow[y])) / (float(level_shadow[y - 1]))<0.6)
				level_shadow[y] = 0;
		}
	}

	//统计水平投影中不为零的区间
	for (int y = height - 1; y >= 0; y--)
	{
		if (level_shadow[y] != 0)
			level_shadow[y] = level_shadow[y + 1] + 1;//有图像的区域level_shadow[y]从1开始递增,1,2,3...直到有图像结束
	}

	//求出水平投影数组中连续不为零的最大区间,此即认为是车牌大致高度
	int Y_min = 0;//车牌高度小坐标
	int Y_max = 0;//车牌高度大坐标
	int M_max_value = 0;
	M_max_value = level_shadow[0]; //把level_shadow的第一个值赋给M_max_value
	for (int y = 0; y<height; y++)
	{
		if (level_shadow[y]>M_max_value)
		{
			M_max_value = level_shadow[y];
			Y_min = y;
			Y_max = Y_min + M_max_value;
		}
	}

	if (M_max_value<10)
		QMessageBox::information(this,tr("Information!"),tr("Failed to extract license plate height"));

	//定义ROI区域,切割出车牌的高度
	CvRect ROI_Plate_Height;
	ROI_Plate_Height.x = 0;
	ROI_Plate_Height.y = Y_min;
	ROI_Plate_Height.width = pSrc_Image->width;
	ROI_Plate_Height.height = M_max_value;
	cvSetImageROI(image, ROI_Plate_Height);

	//将此区域复制一份,以便后续处理
	IplImage * pROI_Height_Image = NULL;
	pROI_Height_Image = cvCreateImage(cvSize(ROI_Plate_Height.width, ROI_Plate_Height.height), 8, 1);
	cvCopy(image, pROI_Height_Image);


	//对车牌高度区域做闭运算,得出车牌的矩形区域,以切割出车牌
	//运算核大小采用(车牌的高度*0.6))
	int Copy_M_max_value = M_max_value; //复制此值,
	int Close_width = 0;
	int Close_height = 0;

	//核大小规定为奇数
	while ((Copy_M_max_value % 3) != 0)
	{
		Copy_M_max_value--;
	}
	Close_width = int(Copy_M_max_value*0.6);
	Close_height = Copy_M_max_value;
	IplConvKernel * pKernel_Close = NULL;
	pKernel_Close = cvCreateStructuringElementEx(Close_width, Close_height, Close_width / 2, Close_height / 2, CV_SHAPE_RECT, NULL);//得到结构元素
	cvMorphologyEx(pROI_Height_Image, pROI_Height_Image, NULL, pKernel_Close, CV_MOP_CLOSE, 1);//对图像进行闭运算


	//求联通区域的最大宽度,定位车牌的横坐标
	int X_min = 0;//车牌宽度小坐标
	int X_max = 0;//车牌宽度大坐标
	int M_row_max_value = 0;
	int count_row[2048];//图像宽度不应大于2048
	memset(count_row, 0, sizeof(count_row));

	//取车牌中间的一条直线进行检测,求此直线上连续不为0的像素的最大宽度,此即为车牌宽度
	int mid_height = M_max_value / 2;
	uchar *ptr_mid = (uchar*)(pROI_Height_Image->imageData + mid_height * pROI_Height_Image->widthStep);
	for (int x = width - 1; x >= 0; x--)
	{
		if (ptr_mid[x] != 0)
			count_row[x] = count_row[x + 1] + 1;
	}

	//求出count_row数组中的最大值
	int Max_value_count_row = 0;
	Max_value_count_row = count_row[0];
	for (int x = 0; x<width; x++)
	{
		if (count_row[x]>Max_value_count_row)
		{
			Max_value_count_row = count_row[x];
			X_min = x;
			X_max = X_min + Max_value_count_row;
		}
	}

	//车牌的宽度应大于高度的三倍,对切割出的车牌进行验证
	if (float(Max_value_count_row) / float(M_max_value)<3 || float(Max_value_count_row) / float(M_max_value)>6)
		QMessageBox::information(this, tr("Information!"), tr("Failed to extract license plate height"));

	//切割出车牌
	CvRect ROI_Plate;
	ROI_Plate.x = X_min;  //对车牌区域做宽度为3放大定位
	ROI_Plate.y = Y_min;
	ROI_Plate.width = Max_value_count_row;
	ROI_Plate.height = M_max_value;

	//判断车牌定位区域是否合法
	if (ROI_Plate.x<0 || ROI_Plate.x>width)
		QMessageBox::information(this, tr("Information!"), tr("Failed to extract license plate height"));
	if (ROI_Plate.y<0 || ROI_Plate.y>height)
		QMessageBox::information(this, tr("Information!"), tr("Failed to extract license plate height"));
	if ((ROI_Plate.x + ROI_Plate.width)>width)
		QMessageBox::information(this, tr("Information!"), tr("Failed to extract license plate height"));
	if ((ROI_Plate.y + ROI_Plate.height)>height)
		QMessageBox::information(this, tr("Information!"), tr("Failed to extract license plate height"));

	cvSetImageROI(bin, ROI_Plate);

	plate_image = cvCreateImage(cvSize(ROI_Plate.width, ROI_Plate.height), 8, 1);
	cvCopy(bin, plate_image);

	//将车牌图像颜色反转
	cvNot(plate_image, plate_image);

	//图像显示
	imageShowGray(ui.label_3, plate_image);
	//m_Cimage.mSetImg(plate_image);
	//UpdateAllViews(NULL);
}

3,倾斜校正:

   车牌分割出来之后不一定是一个矩形,因为由于拍摄角度问题,可能倾斜,所以可能是个平行四边形,这个时候就要用到校正了,这里用的方法是,统计车牌两端的字符像素的平均加权高度,如果两边的平均高度一样,那么就证明是不倾斜,否则是,如果倾斜,就根据斜率重新组织图像,如下图,最后再用膨胀操作,细化字符

                                  

void PlateRecognition::on_actionTiltCorrection_T_triggered()
{
	IplImage *pRotation = cvCreateImage(cvGetSize(plate_image), 8, 1);

	CvScalar s, s_new;
	double num = 0;
	double leftaverage = 0;
	double rightaverage = 0;
	int iHeight = plate_image->height;
	int iWidth = plate_image->width;

	double slope = 0;
	int pix_new;


	//计算前半部分斜率
	for (int ht = 0; ht<iHeight; ht++)
	{
		for (int wt = 0; wt<iWidth / 6; wt++)
		{
			s = cvGet2D(plate_image, ht, wt);
			if (0 == s.val[0])
			{
				num += iWidth / 2 - wt;
				leftaverage += ht * (iWidth / 2 - wt);
			}
		}
	}
	leftaverage /= num;
	num = 0;

	//计算后半部分斜率
	for (int ht = 0; ht<iHeight; ht++)
	{
		for (int wt = (iWidth / 6) * 5; wt<iWidth; wt++)
		{
			s = cvGet2D(plate_image, ht, wt);
			if (0 == s.val[0])
			{
				num += iWidth / 2 - wt;
				rightaverage += ht * (iWidth / 2 - wt);
			}
		}
	}

	rightaverage /= num;

	//求出斜率,并对斜率做适当缩放避免校正过量
	slope = (leftaverage - rightaverage) / (iWidth / 2)*0.6;//原来是1.2


	//图像映射
	for (int ht = 0; ht<iHeight; ht++)
	{
		for (int wt = 0; wt<iWidth / 2; wt++)
		{
			pix_new = int((ht - (wt - iWidth / 2)*slope));
			if (pix_new<0 || pix_new >= iHeight)
			{
				s.val[0] = 255;
				cvSet2D(pRotation, ht, wt, s);
			}
			else
			{
				s = cvGet2D(plate_image, pix_new, wt);
				s_new.val[0] = s.val[0];
				cvSet2D(pRotation, ht, wt, s_new);
			}
		}
	}

	for (int ht = 0; ht<iHeight; ht++)
	{
		for (int wt = iWidth / 2; wt<iWidth; wt++)
		{
			pix_new = int((ht - (wt - iWidth / 2)*(slope)));
			if (pix_new<0 || pix_new >= iHeight)
			{
				s.val[0] = 255;
				cvSet2D(pRotation, ht, wt, s);
			}
			else
			{
				s = cvGet2D(plate_image, pix_new, wt);
				s_new.val[0] = s.val[0];
				cvSet2D(pRotation, ht, wt, s);
			}
		}
	}

	cvNot(pRotation, pRotation);
	
	cvDilate(pRotation, pRotation, 0, 1);
	
	//m_Cimage.mSetImg(pRotation);
	//图像显示
	plate_image = cvCloneImage(pRotation);
	imageShowGray(ui.label_4, pRotation);
}

4.字符分割:

     统计车牌中每列的白色像素点个数之和,个数最多的列证明是字符之间的空隙,也就是要分割的位置,同理,水平方向也这样做,可以去除原车牌头上的两个螺丝钉的痕迹,如下图:

                                            

5.归一化细化:

       就是先将分割出的每个字符都变换到20*40的大小,这样可以统一,便于识别,细化就是让字符笔画变细,每个笔画宽度为一个像素,这两步的结果如下图所示:

                                                     

void PlateRecognition::on_actionCharacterSegmentation_triggered()
{
	// TODO: 在此添加命令处理程序代码
	CvMat  *ColSum = cvCreateMat(1, plate_image->width, CV_64FC1);
	cvReduce(plate_image, ColSum, 0, CV_REDUCE_SUM);

	CvMat  *RoSum = cvCreateMat(plate_image->height, 1, CV_64FC1);
	cvReduce(plate_image, RoSum, 1, CV_REDUCE_SUM);

	int SplitPoint[8]; //存储切割点数组
	int TopBotPt[2];

	MSplit(ColSum, 8, SplitPoint);//车牌分割
	MSplit(RoSum, 2, TopBotPt);

	BubbleSort(SplitPoint);
	if (TopBotPt[0]>TopBotPt[1])
	{
		int temp = TopBotPt[0];
		TopBotPt[0] = TopBotPt[1];
		TopBotPt[1] = TopBotPt[0];
	}
	if (plate_image->height - TopBotPt[1]>plate_image->height*0.15)
		TopBotPt[1] = plate_image->height - 1;

	CvSize PSize = cvSize(20, 40);

	cvNot(plate_image, plate_image);

	for (int i = 0; i<7; i++)
	{

		dst_image[i] = cvCreateImage(PSize, IPL_DEPTH_8U, 1);


		CvRect rect = cvRect(SplitPoint[i], TopBotPt[0], SplitPoint[i + 1] - SplitPoint[i], TopBotPt[1] - TopBotPt[0]);

		cvSetImageROI(plate_image, rect);

		cvResize(plate_image, dst_image[i], CV_INTER_NN);


		m_thin.Thin(dst_image[i], dst_image[i]);//图像细化

												//cvCopy(pRotation, dst_image[i]);

		cvResetImageROI(plate_image);
	}
	for (int i = 0; i < 7; i++)
	{
		Split(dst_image[i]);
		CvSize Size = cvSize(Rwidth, Rheight);
		change_dst_image[i] = cvCreateImage(Size, IPL_DEPTH_8U, 1);
		change_dst_image[i]=Split(dst_image[i]); 
		CvSize PSize = cvSize(20, 40);
		IplImage * imgr = cvCreateImage(PSize, IPL_DEPTH_8U, 1);
		cvResize(change_dst_image[i], imgr, CV_INTER_NN);
		cvCopy(imgr, dst_image[i]);
		m_thin.Thin(dst_image[i], dst_image[i]);
	}


	QString filename;
	IplImage * plate_char;
	//将分割出来的字符整齐排列到图像plate_char上,
	//为便于显示分割效果,将字符间的间距设为12个像素
	plate_char = cvCreateImage(cvSize(g_width * 7 + (7 - 1) * 12, g_height), 8, 1);
	cvZero(plate_char);

	for (int i = 0; i<7; i++)
	{
		int x = 0 + i * g_width + i * 12;
		int y = 0;
		int width = g_width;
		int height = g_height;
		cvSetImageROI(plate_char, cvRect(x, y, width, height));
		cvCopy(dst_image[i], plate_char);
		filename = QString::number(i) + ".jpg";
		cvSaveImage(filename.toLatin1().data(), dst_image[i]);
		cvResetImageROI(plate_char);
	}

	//cvShowImage("分割出的字符", plate_char);
	imageShowGray(ui.label_2, plate_char);
	cvSaveImage("pt.jpg", plate_char);

	//UpdateAllViews(NULL);
}

6.特征提取:

   特征提取包括提取对样本字符的特征提取和对分割出来的字符的特征提取,通过比对分割出来的字符的特征与样本字符的特征来判断字符属于哪一个字符,然后输出结果,其中这个特征怎么提取是关键,这里采用两种统计特征,一种是网格特征,由于模板图像都是20像素*40像素大小的,所以将带识别的字符也归一化为模板图像大小,然后把图像分成5*5=25个小格子,统计每一个格子里白色像素的个数,形成一个25维的矢量,另外一种特征是交叉点,在水平方向以及垂直方向五等分的地方,做水平或者垂直线穿过数字,看其与数字相交的次数,这样又获得了8个数值,加起一共33维。

                                                                 


7,字符识别:

字符识别就是我在步骤6中讲的 通过比对分割出来的字符的特征与样本字符的特征来判断字符属于哪一个字符,然后输出结果,

但是问题来了,这样识别出来的并不精确,很多字符都能识别错,然后我就在找一些漏洞,发现分割出来的字符和样本的字符都能用眼睛看出来是什么,但是它们的位置有偏差,比如,一个“F”可能在样本中,它是在图片的正中间,但是分割出来的字符“F”可能就是在一张图片的左边,或者左上边,有时候是分割出来的字符在图片正中间,而样本字符是在图片的一边,

                                                                                

就像上面所说的那样,所有我要重新做一下规范,也就是把样本图片和分割出来的图片都让字符的长和宽“充满”20*40的图片,you konw that,就像上图左边的图片一样。

             那接下来应该怎么弄呢,想必你已经清楚了,也就是模仿第2步的“车牌定位”这一步,对第5步之后的图像进行“字符定位”我姑且这样说,就是把多余的黑背景去掉,也就达到目的了。事实证明,正确率着实提高了不少。

案例全部代码可以在这里下载

                                                                  谢谢大家,我的表演结束!

  • 15
    点赞
  • 124
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值