基于OpenCV3.0的车牌识别系统设计(二)--车牌提取

版权声明:未经吾理小子本人允许,禁止转载! https://blog.csdn.net/qq_39960119/article/details/83930112

写在前面的话

上一篇开篇博文写好之后找女朋友看了一下,希望她提一点建设性建议。结果她很委婉的告诉我,写的还行就是太表面了,告诉我要注意细节的描述与具体的实现过程与原理等等。其实我只是想骗她看一下增加一下点击量,顺便知道我写的博客新手能不能看懂而已。结果她告诉我,她那么聪明当然能看懂,别人就未必能看懂了!!!吼吼吼,信了她的邪,什么时候都不忘记赞美一下她自己的小妖精。上一篇写的不好的话请各位见谅,吾理小子文笔有点差,但是我会尽自己的最大努力把自己懂的东西分享出来,希望对你有用。为了讲述清楚车牌识别的每一个环节,吾理小子决定把写好的工程源码重新解剖,在每一个环节中贴出该部分完整的代码,新手可直接拿去运行观看实际效果。各位感动吗!

进入正题,上次重点介绍了车牌识别的流程,本文来详细聊一下车牌提取的实现过程。

车牌提取方法

车牌提取通常也称为车牌定位,其目的是从含有车牌的图像中找到车牌区域。车牌定位的重要性不言而喻,作为车牌识别的第一个步骤,车牌区域的提取成功与否是完成车牌识别的基础也是首要决定因素。车牌提取的方法有很多,不同的分类方法有不同的叫法。通过对常见的几种方法的归纳总结,见下图:(编辑一张图片,喷出一口鲜血)

本文着重讲解基于颜色信息的定位方法。上图中可以看到基于彩色信息的定位方法具有准确、快速、精确等优点,对于新手也更加容易理解。通过对质量较高的原始图像进行相应处理之后,定位出车牌区域。现象明显,一气呵成。有助与新手继续研究下去。

到目前为止,关于车牌定位的问题国内外众多学者提出了很多方法,适用条件不尽相同,各有利弊。弄懂了基本的颜色定位之后,可以尝试结合其它的方法来提升车牌定位的性能。

车牌特点

说完车牌定位的方法之后,需要对我们提取的对象本身的特点进行说明。车牌的大小固定,字体格式是由国家相关部门统一规定,与一般常见的字体格式都不一样。据说是由常见的宋体字修改而来,一般民用的车牌包含数字0-9,还有全国26个省市的简称,总共是36个字符。车牌的颜色以及字体颜色如下:

蓝牌白字:普通小型车(其中包括政府机关专用号段、政法部门警车以外的行政用车)的牌照
           黄牌黑字:大型车辆、摩托车、驾校教练车牌照
           黑牌白字:涉外车辆牌照,式样和蓝牌基本相同
                  白牌:政法部门(公安、法院、检察院、国安、司法)警车、武警部队车辆、解放军军车的牌照都是白牌
                  警车:公安警车的牌照样式为[某·A1234警],除“警”为红字外其他的都是黑字,一共4位数字,含义与普通牌照相同

我们研究的目标是普通民用小型车,也就是蓝牌白字车牌。说到这里,顺便说明一下蓝牌白字车牌的颜色信息,对于正常曝光的图像而言,蓝色车牌的三个通道值大约为Blue=138,Green=63,Red=23。除了颜色信息外,车牌形状为矩形,具有固定的长宽比3:1。知道车牌的这些信息后,在使用颜色信息定位时可以做为限制条件,这样可以提高车牌定位的准确性。

车牌提取的步骤

讲完车牌提取的方法和车牌本身的特点之后,接下来仔细说明基于颜色信息的定位方法各个环节处理效果图。

第一步:读取待处理图像

首先,读取待处理的彩色图像,判断图像是否读取成功,成功时显示原始图像。最后打印图像的长和宽,方便对图像的尺寸有一个了解。

Mat OriginalImg ;
OriginalImg = imread("TestPhoto (1).jpg", IMREAD_COLOR);//读取原始彩色图像
	if (OriginalImg.empty())  //判断图像对否读取成功
	{
		cout << "错误!读取图像失败\n";
		return -1;
	}
	imshow("原图", OriginalImg); //显示原始图像
	cout << "Width:" << OriginalImg.rows << "\tHeight:" << OriginalImg.cols << endl;//打印图像长宽

运行效果

第二步:图像尺寸变换

读取原图像后,可以看到原图像像素较高,对于车牌识别而言,过高的分辨率对识别结果效果太大的帮助,反而会影响识别的速度,也就是系统实时性会变差。所以在这里对尺寸进行统一变换,在保证输入图像的长宽比不变的情况下,将图像的长度变成640,相应的宽度可以通过计算得到。

    Mat ResizeImg; 
	if (OriginalImg.cols > 640)
		resize(OriginalImg, ResizeImg, Size(640, 640* OriginalImg.rows / OriginalImg.cols));
	imshow("尺寸变换图", ResizeImg);

上图和原图的区别就是尺寸不一致。后续的处理步骤都是在缩放之后的图像基础上来做,适当分辨率的图像有助于提高系统的响应速度,也就是实时性。

第三步:基于颜色信息二值化

基于颜色的二值化处理就是通过颜色信息将图像二值化,上面已经提到了正常曝光的车牌各个通道的颜色信息大约是Blue=138,Green=63,Red=23。但是颜色信息有一定的偏差,因此在二值化时放宽颜色条件,然后再通过其他特点来精确寻找车牌区域。本文设置各个通道的偏差值为50。程序如下,注意观察颜色信息以及偏差值在程序中的体现。

unsigned char pixelB, pixelG, pixelR;  //记录各通道值
unsigned char DifMax = 50;             //基于颜色区分的阈值设置
unsigned char B = 138, G = 63, R = 23; //各通道的阈值设定,针对与蓝色车牌
Mat BinRGBImg = ResizeImg.clone();  //二值化之后的图像
int i = 0, j = 0;
for (i = 0; i < ResizeImg.rows; i++)   //通过颜色分量将图片进行二值化处理
{
	for (j = 0; j < ResizeImg.cols; j++)
	{
		pixelB = ResizeImg.at<Vec3b>(i, j)[0]; //获取图片各个通道的值
		pixelG = ResizeImg.at<Vec3b>(i, j)[1];
		pixelR = ResizeImg.at<Vec3b>(i, j)[2];

		if (abs(pixelB - B) < DifMax && abs(pixelG - G) < DifMax && abs(pixelR - R) < DifMax)
		{                                           //将各个通道的值和各个通道阈值进行比较
			BinRGBImg.at<Vec3b>(i, j)[0] = 255;     //符合颜色阈值范围内的设置成白色
			BinRGBImg.at<Vec3b>(i, j)[1] = 255;
			BinRGBImg.at<Vec3b>(i, j)[2] = 255;
		}
		else
		{
			BinRGBImg.at<Vec3b>(i, j)[0] = 0;        //不符合颜色阈值范围内的设置为黑色
			BinRGBImg.at<Vec3b>(i, j)[1] = 0;
			BinRGBImg.at<Vec3b>(i, j)[2] = 0;
		}
	}
}
imshow("基于颜色信息二值化", BinRGBImg);        //显示二值化处理之后的图像

第四步:形态学处理

基于颜色信息二值化的图像效果还是挺不错的,可以看到车牌区域基本完整,其他地方有一些细小的干扰,接下来进行形态学处理,消除小区域干扰。形态学闭操作——先膨胀后腐蚀,是图像的基本操作之一,其特点是填充细小空间,连接临近物体和平滑边界,不同矩形窗的大小会有不同的结果。(形态学基本知识可参考数字图像处理相关书籍)

Mat BinOriImg;     //形态学处理结果图像
Mat element = getStructuringElement(MORPH_RECT, Size(3, 3)); //设置形态学处理窗的大小
dilate(BinRGBImg, BinOriImg, element);     //进行多次膨胀操作
dilate(BinOriImg, BinOriImg, element);
dilate(BinOriImg, BinOriImg, element);

erode(BinOriImg, BinOriImg, element);      //进行多次腐蚀操作
erode(BinOriImg, BinOriImg, element);
erode(BinOriImg, BinOriImg, element);
imshow("形态学处理后", BinOriImg);        //显示形态学处理之后的图像

第五步:增加限制条件寻找车牌区域并框选车牌

从上图中可以看到车牌的基本位置已经能够确定,接下来通过其他特点来进一步确定车牌区域。处理思路如下:

1.寻找各个空白区域外轮廓并计算面积;

2.为各个空白区域增加外接矩形并计算面积;

3.通过外轮廓面积与外接矩形的比值,判断区域的矩形度;

4.进一步判断长宽比;

5.满足全部条件确定车牌区域

double length, area, rectArea;     //定义轮廓周长、面积、外界矩形面积
double rectDegree = 0.0;           //矩形度=外界矩形面积/轮廓面积
double long2Short = 0.0;           //体态比=长边/短边
CvRect rect;           //外界矩形
CvBox2D box, boxTemp;  //外接矩形
CvPoint2D32f pt[4];    //矩形定点变量
double axisLong = 0.0, axisShort = 0.0;        //矩形的长边和短边
double axisLongTemp = 0.0, axisShortTemp = 0.0;//矩形的长边和短边
double LengthTemp;     //中间变量
float  angle = 0;      //记录车牌的倾斜角度
float  angleTemp = 0;
bool   TestPlantFlag = 0;  //车牌检测成功标志位
cvtColor(BinOriImg, BinOriImg, CV_BGR2GRAY);   //将形态学处理之后的图像转化为灰度图像
threshold(BinOriImg, BinOriImg, 100, 255, THRESH_BINARY); //灰度图像二值化
CvMemStorage *storage = cvCreateMemStorage(0);
CvSeq * seq = 0;     //创建一个序列,CvSeq本身就是一个可以增长的序列,不是固定的序列
CvSeq * tempSeq = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage);
int cnt = cvFindContours(&(IplImage(BinOriImg)), storage, &seq, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
//第一个参数是IplImage指针类型,将MAT强制转换为IplImage指针类型
//返回轮廓的数目 
//获取二值图像中轮廓的个数
cout << "number of contours   " << cnt << endl;  //打印轮廓个数
for (tempSeq = seq; tempSeq != NULL; tempSeq = tempSeq->h_next)
{
	length = cvArcLength(tempSeq);       //获取轮廓周长
	area = cvContourArea(tempSeq);       //获取轮廓面积
	if (area > 800 && area < 50000)     //矩形区域面积大小判断
	{
		rect = cvBoundingRect(tempSeq, 1);//计算矩形边界
		boxTemp = cvMinAreaRect2(tempSeq, 0);  //获取轮廓的矩形
		cvBoxPoints(boxTemp, pt);              //获取矩形四个顶点坐标
		angleTemp = boxTemp.angle;                 //得到车牌倾斜角度

		axisLongTemp = sqrt(pow(pt[1].x - pt[0].x, 2) + pow(pt[1].y - pt[0].y, 2));  //计算长轴(勾股定理)
		axisShortTemp = sqrt(pow(pt[2].x - pt[1].x, 2) + pow(pt[2].y - pt[1].y, 2)); //计算短轴(勾股定理)

		if (axisShortTemp > axisLongTemp)   //短轴大于长轴,交换数据
		{
			LengthTemp = axisLongTemp;
			axisLongTemp = axisShortTemp;
			axisShortTemp = LengthTemp;
		}
		else
			angleTemp += 90;
		rectArea = axisLongTemp * axisShortTemp;  //计算矩形的面积
		rectDegree = area / rectArea;     //计算矩形度(比值越接近1说明越接近矩形)

		long2Short = axisLongTemp / axisShortTemp; //计算长宽比
		if (long2Short > 2.2 && long2Short < 3.8 && rectDegree > 0.63 && rectDegree < 1.37 && rectArea > 2000 && rectArea < 50000)
		{
			Mat GuiRGBImg = ResizeImg.clone();
			TestPlantFlag = true;             //检测车牌区域成功
			for (int i = 0; i < 4; ++i)       //划线框出车牌区域
				cvLine(&(IplImage(GuiRGBImg)), cvPointFrom32f(pt[i]), cvPointFrom32f(pt[((i + 1) % 4) ? (i + 1) : 0]), CV_RGB(255, 0, 0));
			imshow("提取车牌结果图", GuiRGBImg);    //显示最终结果图

			box = boxTemp;
			angle = angleTemp;
			axisLong = axisLongTemp;
			axisShort = axisShortTemp;
			cout << "倾斜角度:" << angle << endl;
		}
	}
}

框选结果如下图所示。这部分牵扯到的变量较多,主要看思路,不必纠结为什么设置这么多变量,因为后面需要用到这些变量。

不知道细心的小伙伴有没有发现,其中有一个变量是倾斜角度。其实这个变量保存的是车牌区域的倾斜角度,通过观察原图中车牌的位置和姿态,和输出的倾斜角度的值可以明白这个值表达的含义。后续步骤就是车牌的倾斜矫正。各位首先有一个大致的了解。

车牌提取效果展示

下面展示一些提取车牌区域的效果图。

车牌提取源码

说完各个步骤的具体操作,最后贴上设计到的源码。各位自行验证改进!


#include <iostream>
#include <opencv2\opencv.hpp>

using namespace std;
using namespace cv;

int main(int,char *argv[])
{
	Mat OriginalImg;

	OriginalImg = imread("TestPhoto (1).jpg", IMREAD_COLOR);//读取原始彩色图像
	if (OriginalImg.empty())  //判断图像对否读取成功
	{
		cout << "错误!读取图像失败\n";
		return -1;
	}
//	imshow("原图", OriginalImg); //显示原始图像
	cout << "Width:" << OriginalImg.rows << "\tHeight:" << OriginalImg.cols << endl;//打印长宽

	Mat ResizeImg; 
	if (OriginalImg.cols > 640)
		resize(OriginalImg, ResizeImg, Size(640, 640 * OriginalImg.rows / OriginalImg.cols));
	imshow("尺寸变换图", ResizeImg);

	unsigned char pixelB, pixelG, pixelR;  //记录各通道值
	unsigned char DifMax = 50;             //基于颜色区分的阈值设置
	unsigned char B = 138, G = 63, R = 23; //各通道的阈值设定,针对与蓝色车牌
	Mat BinRGBImg = ResizeImg.clone();  //二值化之后的图像
	int i = 0, j = 0;
	for (i = 0; i < ResizeImg.rows; i++)   //通过颜色分量将图片进行二值化处理
	{
		for (j = 0; j < ResizeImg.cols; j++)
		{
			pixelB = ResizeImg.at<Vec3b>(i, j)[0]; //获取图片各个通道的值
			pixelG = ResizeImg.at<Vec3b>(i, j)[1];
			pixelR = ResizeImg.at<Vec3b>(i, j)[2];

			if (abs(pixelB - B) < DifMax && abs(pixelG - G) < DifMax && abs(pixelR - R) < DifMax)
			{                                           //将各个通道的值和各个通道阈值进行比较
				BinRGBImg.at<Vec3b>(i, j)[0] = 255;     //符合颜色阈值范围内的设置成白色
				BinRGBImg.at<Vec3b>(i, j)[1] = 255;
				BinRGBImg.at<Vec3b>(i, j)[2] = 255;
			}
			else
			{
				BinRGBImg.at<Vec3b>(i, j)[0] = 0;        //不符合颜色阈值范围内的设置为黑色
				BinRGBImg.at<Vec3b>(i, j)[1] = 0;
				BinRGBImg.at<Vec3b>(i, j)[2] = 0;
			}
		}
	}
	imshow("基于颜色信息二值化", BinRGBImg);        //显示二值化处理之后的图像

	Mat BinOriImg;     //形态学处理结果图像
	Mat element = getStructuringElement(MORPH_RECT, Size(3, 3)); //设置形态学处理窗的大小
	dilate(BinRGBImg, BinOriImg, element);     //进行多次膨胀操作
	dilate(BinOriImg, BinOriImg, element);
	dilate(BinOriImg, BinOriImg, element);

	erode(BinOriImg, BinOriImg, element);      //进行多次腐蚀操作
	erode(BinOriImg, BinOriImg, element);
	erode(BinOriImg, BinOriImg, element);
	imshow("形态学处理后", BinOriImg);        //显示形态学处理之后的图像

	double length, area, rectArea;     //定义轮廓周长、面积、外界矩形面积
	double rectDegree = 0.0;           //矩形度=外界矩形面积/轮廓面积
	double long2Short = 0.0;           //体态比=长边/短边
	CvRect rect;           //外界矩形
	CvBox2D box, boxTemp;  //外接矩形
	CvPoint2D32f pt[4];    //矩形定点变量
	double axisLong = 0.0, axisShort = 0.0;        //矩形的长边和短边
	double axisLongTemp = 0.0, axisShortTemp = 0.0;//矩形的长边和短边
	double LengthTemp;     //中间变量
	float  angle = 0;      //记录车牌的倾斜角度
	float  angleTemp = 0;
	bool   TestPlantFlag = 0;  //车牌检测成功标志位
	cvtColor(BinOriImg, BinOriImg, CV_BGR2GRAY);   //将形态学处理之后的图像转化为灰度图像
	threshold(BinOriImg, BinOriImg, 100, 255, THRESH_BINARY); //灰度图像二值化
	CvMemStorage *storage = cvCreateMemStorage(0);
	CvSeq * seq = 0;     //创建一个序列,CvSeq本身就是一个可以增长的序列,不是固定的序列
	CvSeq * tempSeq = cvCreateSeq(CV_SEQ_ELTYPE_POINT, sizeof(CvSeq), sizeof(CvPoint), storage);
	int cnt = cvFindContours(&(IplImage(BinOriImg)), storage, &seq, sizeof(CvContour), CV_RETR_EXTERNAL, CV_CHAIN_APPROX_SIMPLE);
	//第一个参数是IplImage指针类型,将MAT强制转换为IplImage指针类型
	//返回轮廓的数目 
	//获取二值图像中轮廓的个数
	cout << "number of contours   " << cnt << endl;  //打印轮廓个数
	for (tempSeq = seq; tempSeq != NULL; tempSeq = tempSeq->h_next)
	{
		length = cvArcLength(tempSeq);       //获取轮廓周长
		area = cvContourArea(tempSeq);       //获取轮廓面积
		if (area > 800 && area < 50000)     //矩形区域面积大小判断
		{
			rect = cvBoundingRect(tempSeq, 1);//计算矩形边界
			boxTemp = cvMinAreaRect2(tempSeq, 0);  //获取轮廓的矩形
			cvBoxPoints(boxTemp, pt);              //获取矩形四个顶点坐标
			angleTemp = boxTemp.angle;                 //得到车牌倾斜角度

			axisLongTemp = sqrt(pow(pt[1].x - pt[0].x, 2) + pow(pt[1].y - pt[0].y, 2));  //计算长轴(勾股定理)
			axisShortTemp = sqrt(pow(pt[2].x - pt[1].x, 2) + pow(pt[2].y - pt[1].y, 2)); //计算短轴(勾股定理)

			if (axisShortTemp > axisLongTemp)   //短轴大于长轴,交换数据
			{
				LengthTemp = axisLongTemp;
				axisLongTemp = axisShortTemp;
				axisShortTemp = LengthTemp;
			}
			else
				angleTemp += 90;
			rectArea = axisLongTemp * axisShortTemp;  //计算矩形的面积
			rectDegree = area / rectArea;     //计算矩形度(比值越接近1说明越接近矩形)

			long2Short = axisLongTemp / axisShortTemp; //计算长宽比
			if (long2Short > 2.2 && long2Short < 3.8 && rectDegree > 0.63 && rectDegree < 1.37 && rectArea > 2000 && rectArea < 50000)
			{
				Mat GuiRGBImg = ResizeImg.clone();
				TestPlantFlag = true;             //检测车牌区域成功
				for (int i = 0; i < 4; ++i)       //划线框出车牌区域
					cvLine(&(IplImage(GuiRGBImg)), cvPointFrom32f(pt[i]), cvPointFrom32f(pt[((i + 1) % 4) ? (i + 1) : 0]), CV_RGB(255, 0, 0));
				imshow("提取车牌结果图", GuiRGBImg);    //显示最终结果图

				box = boxTemp;
				angle = angleTemp;
				axisLong = axisLongTemp;
				axisShort = axisShortTemp;
				cout << "倾斜角度:" << angle << endl;
			}
		}
	}

	waitKey();
	return 0;

}


好啦,本节就说到这里。下一篇开始说车牌的倾斜矫正,各位小伙伴期待吗?

 

 

猜你喜欢

转载自blog.csdn.net/qq_39960119/article/details/83930112