写在前面的话
上一篇开篇博文写好之后找女朋友看了一下,希望她提一点建设性建议。结果她很委婉的告诉我,写的还行就是太表面了,告诉我要注意细节的描述与具体的实现过程与原理等等。其实我只是想骗她看一下增加一下点击量,顺便知道我写的博客新手能不能看懂而已。结果她告诉我,她那么聪明当然能看懂,别人就未必能看懂了!!!吼吼吼,信了她的邪,什么时候都不忘记赞美一下她自己的小妖精。上一篇写的不好的话请各位见谅,吾理小子文笔有点差,但是我会尽自己的最大努力把自己懂的东西分享出来,希望对你有用。为了讲述清楚车牌识别的每一个环节,吾理小子决定把写好的工程源码重新解剖,在每一个环节中贴出该部分完整的代码,新手可直接拿去运行观看实际效果。各位感动吗!
进入正题,上次重点介绍了车牌识别的流程,本文来详细聊一下车牌提取的实现过程。
车牌提取方法
车牌提取通常也称为车牌定位,其目的是从含有车牌的图像中找到车牌区域。车牌定位的重要性不言而喻,作为车牌识别的第一个步骤,车牌区域的提取成功与否是完成车牌识别的基础也是首要决定因素。车牌提取的方法有很多,不同的分类方法有不同的叫法。通过对常见的几种方法的归纳总结,见下图:(编辑一张图片,喷出一口鲜血)
本文着重讲解基于颜色信息的定位方法。上图中可以看到基于彩色信息的定位方法具有准确、快速、精确等优点,对于新手也更加容易理解。通过对质量较高的原始图像进行相应处理之后,定位出车牌区域。现象明显,一气呵成。有助与新手继续研究下去。
到目前为止,关于车牌定位的问题国内外众多学者提出了很多方法,适用条件不尽相同,各有利弊。弄懂了基本的颜色定位之后,可以尝试结合其它的方法来提升车牌定位的性能。
车牌特点
说完车牌定位的方法之后,需要对我们提取的对象本身的特点进行说明。车牌的大小固定,字体格式是由国家相关部门统一规定,与一般常见的字体格式都不一样。据说是由常见的宋体字修改而来,一般民用的车牌包含数字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;}elseangleTemp += 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;}elseangleTemp += 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;}
好啦,本节就说到这里。下一篇开始说车牌的倾斜矫正,各位小伙伴期待吗?