转自https://www.cnblogs.com/subconscious/p/4047960.html
根据前文的内容,车牌定位的功能还剩下如下的步骤,见下图中未涂灰的部分。
图1 车牌定位步骤
我们首先从Soble算子分析出来的边缘来看。通过下图可见,Sobel算子有很强的区分性,车牌中的字符被清晰的描绘出来,那么如何根据这些信息定位出车牌的位置呢?
图2 Sobel后效果
我们的车牌定位功能做了个假设,即车牌是包含字符图块的一个最小的外接矩形。在大部分车牌处理中,这个假设都能工作的很好。我们来看下这个假设是如何工作的。
车牌定位过程的全部代码如下:
//! 定位车牌图像
//! src 原始图像
//! resultVec 一个Mat的向量,存储所有抓取到的图像
//! 成功返回0,否则返回-1
int CPlateLocate::plateLocate(Mat src, vector<Mat>& resultVec)
{
Mat src_blur, src_gray;
Mat grad;
int scale = SOBEL_SCALE;
int delta = SOBEL_DELTA;
int ddepth = SOBEL_DDEPTH;
if( !src.data )
{ return -1; }
//高斯模糊。Size中的数字影响车牌定位的效果。
GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize),
0, 0, BORDER_DEFAULT );
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_GaussianBlur" << ".jpg";
imwrite(ss.str(), src_blur);
}
/// Convert it to gray
cvtColor( src_blur, src_gray, CV_RGB2GRAY );
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_gray" << ".jpg";
imwrite(ss.str(), src_gray);
}
/// Generate grad_x and grad_y
Mat grad_x, grad_y;
Mat abs_grad_x, abs_grad_y;
/// Gradient X
//Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_x, abs_grad_x );
/// Gradient Y
//Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
convertScaleAbs( grad_y, abs_grad_y );
/// Total Gradient (approximate)
addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad );
//Laplacian( src_gray, grad_x, ddepth, 3, scale, delta, BORDER_DEFAULT );
//convertScaleAbs( grad_x, grad );
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_Sobel" << ".jpg";
imwrite(ss.str(), grad);
}
Mat img_threshold;
threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
//threshold(grad, img_threshold, 75, 255, CV_THRESH_BINARY);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_threshold" << ".jpg";
imwrite(ss.str(), img_threshold);
}
Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );
morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_morphology" << ".jpg";
imwrite(ss.str(), img_threshold);
}
//Find 轮廓 of possibles plates
vector< vector< Point> > contours;
findContours(img_threshold,
contours, // a vector of contours
CV_RETR_EXTERNAL, // 提取外部轮廓
CV_CHAIN_APPROX_NONE); // all pixels of each contours
Mat result;
if(m_debug)
{
//// Draw blue contours on a white image
src.copyTo(result);
drawContours(result, contours,
-1, // draw all contours
Scalar(0,0,255), // in blue
1); // with a thickness of 1
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_Contours" << ".jpg";
imwrite(ss.str(), result);
}
//Start to iterate to each contour founded
vector<vector<Point> >::iterator itc = contours.begin();
vector<RotatedRect> rects;
//Remove patch that are no inside limits of aspect ratio and area.
int t = 0;
while (itc != contours.end())
{
//Create bounding rect of object
RotatedRect mr = minAreaRect(Mat(*itc));
//large the rect for more
if( !verifySizes(mr))
{
itc = contours.erase(itc);
}
else
{
++itc;
rects.push_back(mr);
}
}
int k = 1;
for(int i=0; i< rects.size(); i++)
{
RotatedRect minRect = rects[i];
if(verifySizes(minRect))
{
// rotated rectangle drawing
// Get rotation matrix
// 旋转这部分代码确实可以将某些倾斜的车牌调整正,
// 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。
// 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试
// 这段代码。
if(m_debug)
{
Point2f rect_points[4];
minRect.points( rect_points );
for( int j = 0; j < 4; j++ )
line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );
}
float r = (float)minRect.size.width / (float)minRect.size.height;
float angle = minRect.angle;
Size rect_size = minRect.size;
if (r < 1)
{
angle = 90 + angle;
swap(rect_size.width, rect_size.height);
}
//如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理
if (angle - m_angle < 0 && angle + m_angle > 0)
{
//Create and rotate image
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
Mat img_rotated;
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
Mat resultMat;
resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);
resultVec.push_back(resultMat);
}
}
}
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_result" << ".jpg";
imwrite(ss.str(), result);
}
return 0;
}
1 //! 定位车牌图像 2 //! src 原始图像 3 //! resultVec 一个Mat的向量,存储所有抓取到的图像 4 //! 成功返回0,否则返回-1 5 int CPlateLocate::plateLocate(Mat src, vector<Mat>& resultVec) 6 { 7 Mat src_blur, src_gray; 8 Mat grad; 9 10 int scale = SOBEL_SCALE; 11 int delta = SOBEL_DELTA; 12 int ddepth = SOBEL_DDEPTH; 13 14 if( !src.data ) 15 { return -1; } 16 17 //高斯模糊。Size中的数字影响车牌定位的效果。 18 GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize), 19 0, 0, BORDER_DEFAULT ); 20 21 if(m_debug) 22 { 23 stringstream ss(stringstream::in | stringstream::out); 24 ss << "tmp/debug_GaussianBlur" << ".jpg"; 25 imwrite(ss.str(), src_blur); 26 } 27 28 /// Convert it to gray 29 cvtColor( src_blur, src_gray, CV_RGB2GRAY ); 30 31 if(m_debug) 32 { 33 stringstream ss(stringstream::in | stringstream::out); 34 ss << "tmp/debug_gray" << ".jpg"; 35 imwrite(ss.str(), src_gray); 36 } 37 38 /// Generate grad_x and grad_y 39 Mat grad_x, grad_y; 40 Mat abs_grad_x, abs_grad_y; 41 42 /// Gradient X 43 //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT ); 44 Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT ); 45 convertScaleAbs( grad_x, abs_grad_x ); 46 47 /// Gradient Y 48 //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT ); 49 Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT ); 50 convertScaleAbs( grad_y, abs_grad_y ); 51 52 /// Total Gradient (approximate) 53 addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad ); 54 55 //Laplacian( src_gray, grad_x, ddepth, 3, scale, delta, BORDER_DEFAULT ); 56 //convertScaleAbs( grad_x, grad ); 57 58 59 if(m_debug) 60 { 61 stringstream ss(stringstream::in | stringstream::out); 62 ss << "tmp/debug_Sobel" << ".jpg"; 63 imwrite(ss.str(), grad); 64 } 65 66 Mat img_threshold; 67 threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY); 68 //threshold(grad, img_threshold, 75, 255, CV_THRESH_BINARY); 69 70 if(m_debug) 71 { 72 stringstream ss(stringstream::in | stringstream::out); 73 ss << "tmp/debug_threshold" << ".jpg"; 74 imwrite(ss.str(), img_threshold); 75 } 76 77 Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) ); 78 morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element); 79 80 if(m_debug) 81 { 82 stringstream ss(stringstream::in | stringstream::out); 83 ss << "tmp/debug_morphology" << ".jpg"; 84 imwrite(ss.str(), img_threshold); 85 } 86 87 //Find 轮廓 of possibles plates 88 vector< vector< Point> > contours; 89 findContours(img_threshold, 90 contours, // a vector of contours 91 CV_RETR_EXTERNAL, // 提取外部轮廓 92 CV_CHAIN_APPROX_NONE); // all pixels of each contours 93 94 Mat result; 95 if(m_debug) 96 { 97 //// Draw blue contours on a white image 98 src.copyTo(result); 99 drawContours(result, contours, 100 -1, // draw all contours 101 Scalar(0,0,255), // in blue 102 1); // with a thickness of 1 103 stringstream ss(stringstream::in | stringstream::out); 104 ss << "tmp/debug_Contours" << ".jpg"; 105 imwrite(ss.str(), result); 106 } 107 108 109 //Start to iterate to each contour founded 110 vector<vector<Point> >::iterator itc = contours.begin(); 111 112 vector<RotatedRect> rects; 113 //Remove patch that are no inside limits of aspect ratio and area. 114 int t = 0; 115 while (itc != contours.end()) 116 { 117 //Create bounding rect of object 118 RotatedRect mr = minAreaRect(Mat(*itc)); 119 120 //large the rect for more 121 if( !verifySizes(mr)) 122 { 123 itc = contours.erase(itc); 124 } 125 else 126 { 127 ++itc; 128 rects.push_back(mr); 129 } 130 } 131 132 int k = 1; 133 for(int i=0; i< rects.size(); i++) 134 { 135 RotatedRect minRect = rects[i]; 136 if(verifySizes(minRect)) 137 { 138 // rotated rectangle drawing 139 // Get rotation matrix 140 // 旋转这部分代码确实可以将某些倾斜的车牌调整正, 141 // 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。 142 // 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试 143 // 这段代码。 144 if(m_debug) 145 { 146 Point2f rect_points[4]; 147 minRect.points( rect_points ); 148 for( int j = 0; j < 4; j++ ) 149 line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 ); 150 } 151 152 float r = (float)minRect.size.width / (float)minRect.size.height; 153 float angle = minRect.angle; 154 Size rect_size = minRect.size; 155 if (r < 1) 156 { 157 angle = 90 + angle; 158 swap(rect_size.width, rect_size.height); 159 } 160 //如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理 161 if (angle - m_angle < 0 && angle + m_angle > 0) 162 { 163 //Create and rotate image 164 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1); 165 Mat img_rotated; 166 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC); 167 168 Mat resultMat; 169 resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++); 170 171 resultVec.push_back(resultMat); 172 } 173 } 174 } 175 176 if(m_debug) 177 { 178 stringstream ss(stringstream::in | stringstream::out); 179 ss << "tmp/debug_result" << ".jpg"; 180 imwrite(ss.str(), result); 181 } 182 183 return 0; 184 }
首先,我们通过二值化处理将Sobel生成的灰度图像转变为二值图像。
四.二值化
二值化算法非常简单,就是对图像的每个像素做一个阈值处理。
1.目标
为后续的形态学算子Morph等准备二值化的图像。
2.效果
经过二值化处理后的图像效果为下图,与灰度图像仔细区分下,二值化图像中的白色是没有颜色强与暗的区别的。
图3 二值化后效果
3.理论
在灰度图像中,每个像素的值是0-255之间的数字,代表灰暗的程度。如果设定一个阈值T,规定像素的值x满足如下条件时则:
if x < t then x = 0; if x >= t then x = 1。
如此一来,每个像素的值仅有{0,1}两种取值,0代表黑、1代表白,图像就被转换成了二值化的图像。在上面的公式中,阈值T应该取多少?由于不同图像的光造程度不同,导致作为二值化区分的阈值T也不一样。因此一个简单的做法是直接使用opencv的二值化函数时加上自适应阈值参数。如下:
threshold(src, dest, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
通过这种方法,我们不需要计算阈值的取值,直接使用即可。
threshold函数是二值化函数,参数src代表源图像,dest代表目标图像,两者的类型都是cv::Mat型,最后的参数代表二值化时的选项,
CV_THRESH_OTSU代表自适应阈值,CV_THRESH_BINARY代表正二值化。正二值化意味着像素的值越接近0,越可能被赋值为0,反之则为1。而另外一种二值化方法表示反二值化,其含义是像素的值越接近0,越可能被赋值1,,计算公式如下:
if x < t then x = 1; if x >= t then x = 0,
如果想使用反二值化,可以使用参数CV_THRESH_BINARY_INV代替CV_THRESH_BINARY即可。在后面的字符识别中我们会同时使用到正二值化与反二值化两种例子。因为中国的车牌有很多类型,最常见的是蓝牌和黄牌。其中蓝牌字符浅,背景深,黄牌则是字符深,背景浅,因此需要正二值化方法与反二值化两种方法来处理,其中正二值化处理蓝牌,反二值化处理黄牌。
五.闭操作
闭操作是个非常重要的操作,我会花很多的字数与图片介绍它。
1.目标
将车牌字母连接成为一个连通域,便于取轮廓。
2.效果
我们这里看下经过闭操作后图像连接的效果。
图4 闭操作后效果
3.理论
在做闭操作的说明前,必须简单介绍一下腐蚀和膨胀两个操作。
在图像处理技术中,有一些的操作会对图像的形态发生改变,这些操作一般称之为形态学操作。形态学操作的对象是二值化图像。
有名的形态学操作中包括腐蚀,膨胀,开操作,闭操作等。其中腐蚀,膨胀是许多形态学操作的基础。
腐蚀操作:
顾名思义,是将物体的边缘加以腐蚀。具体的操作方法是拿一个宽m,高n的矩形作为模板,对图像中的每一个像素x做如下处理:像素x至于模板的中心,根据模版的大小,遍历所有被模板覆盖的其他像素,修改像素x的值为所有像素中最小的值。这样操作的结果是会将图像外围的突出点加以腐蚀。如下图的操作过程:
图5 腐蚀操作原理
上图演示的过程是背景为黑色,物体为白色的情况。腐蚀将白色物体的表面加以“腐蚀”。在opencv的官方教程中,是以如下的图示说明腐蚀过程的,与我上面图的区别在于:背景是白色,而物体为黑色(这个不太符合一般的情况,所以我没有拿这张图作为通用的例子)。读者只需要了解背景为不同颜色时腐蚀也是不同的效果就可以了。
图6 腐蚀操作原理2
膨胀操作:
膨胀操作与腐蚀操作相反,是将图像的轮廓加以膨胀。操作方法与腐蚀操作类似,也是拿一个矩形模板,对图像的每个像素做遍历处理。不同之处在于修改像素的值不是所有像素中最小的值,而是最大的值。这样操作的结果会将图像外围的突出点连接并向外延伸。如下图的操作过程:
图7 膨胀操作原理
下面是在opencv的官方教程中,膨胀过程的图示:
图8 膨胀操作原理2
开操作:
开操作就是对图像先腐蚀,再膨胀。其中腐蚀与膨胀使用的模板是一样大小的。为了说明开操作的效果,请看下图的操作过程:
图9 开操作原理
由于开操作是先腐蚀,再膨胀。因此可以结合图5和图7得出图9,其中图5的输出是图7的输入,所以开操作的结果也就是图7的结果。
闭操作:
闭操作就是对图像先膨胀,再腐蚀。闭操作的结果一般是可以将许多靠近的图块相连称为一个无突起的连通域。在我们的图像定位中,使用了闭操作去连接所有的字符小图块,然后形成一个车牌的大致轮廓。闭操作的过程我会讲的细致一点。为了说明字符图块连接的过程。在这里选取的原图跟上面三个操作的原图不大一样,是一个由两个分开的图块组成的图。原图首先经过膨胀操作,将两个分开的图块结合起来(注意我用偏白的灰色图块表示由于膨胀操作而产生的新的白色)。接着通过腐蚀操作,将连通域的边缘和突起进行削平(注意我用偏黑的灰色图块表示由于腐蚀被侵蚀成黑色图块)。最后得到的是一个无突起的连通域(纯白的部分)。
图10 闭操作原理
4.代码
在opencv中,调用闭操作的方法是首先建立矩形模板,矩形的大小是可以设置的,由于矩形是用来覆盖以中心像素的所有其他像素,因此矩形的宽和高最好是奇数。
通过以下代码设置矩形的宽和高。
Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );
在这里,我们使用了类成员变量,这两个类成员变量在构造函数中被赋予了初始值。宽是17,高是3.
设置完矩形的宽和高以后,就可以调用形态学操作了。opencv中所有形态学操作有一个统一的函数,通过参数来区分不同的具体操作。例如MOP_CLOSE代表闭操作,MOP_OPEN代表开操作。
morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);
如果我对二值化的图像进行开操作,结果会是什么样的?下图是图像使用闭操作与开操作处理后的一个区别:
图11 开与闭的对比
晕,怎么开操作后图像没了?原因是:开操作第一步腐蚀的效果太强,直接导致接下来的膨胀操作几乎没有效果,所以图像就变几乎没了。
可以看出,使用闭操作以后,车牌字符的图块被连接成了一个较为规则的矩形,通过闭操作,将车牌中的字符连成了一个图块,同时将突出的部分进行裁剪,图块成为了一个类似于矩形的不规则图块。我们知道,车牌应该是一个规则的矩形,因此获取规则矩形的办法就是先取轮廓,再接着求最小外接矩形。
这里需要注意的是,矩形模板的宽度,17是个推荐值,低于17都不推荐。
为什么这么说,因为有一个”断节“的问题。中国车牌有一个特点,就是表示城市的字母与右边相邻的字符距离远大于其他相邻字符之间的距离。如果你设置的不够大,结果导致左边的字符与右边的字符中间断开了,如下图:
图12 “断节”效果
这种情况我称之为“断节”如果你不想字符从中间被分成"苏A"和"7EUK22"的话,那么就必须把它设置大点。
另外还有一种讨厌的情况,就是右边的字符第一个为1的情况,例如苏B13GH7。在这种情况下,由于1的字符的形态原因,导致跟左边的B的字符的距离更远,在这种情况下,低于17都有很大的可能性会断节。下图说明了矩形模板宽度过小时(例如设置为7)面对不同车牌情况下的效果。其中第二个例子选取了苏E开头的车牌,由于E在Sobel算子运算过后仅存有左边的竖杠,因此也会导致跟右边的字符相距过远的情况!
图13 “断节”发生示意
宽度过大也是不好的,因为它会导致闭操作连接不该连接的部分,例如下图的情况。
图14 矩形模板宽度过大
这种情况下,你取轮廓获得矩形肯定会大于你设置的校验规则,即便通过校验了,由于图块中有不少不是车牌的部分,会给字符识别带来麻烦。
因此,矩形的宽度是一个需要非常细心权衡的值,过大过小都不好,取决于你的环境。至于矩形的高度,3是一个较好的值,一般来说都能工作的很好,不需要改变。
记得我在前一篇文章中提到,工业用图片与生活场景下图片的区别么。笔者做了一个实验,下载了30多张左右的百度车牌图片。用plateLocate过程去识别他们。如果按照下面的方式设置参数,可以保证90%以上的定位成功率。
CPlateLocate plate; plate.setDebug(1); plate.setGaussianBlurSize(5); plate.setMorphSizeWidth(7); plate.setMorphSizeHeight(3); plate.setVerifyError(0.9); plate.setVerifyAspect(4); plate.setVerifyMin(1); plate.setVerifyMax(30);
在EasyPR的下一个版本中,会增加对于生活场景下图片的一个模式。只要选择这个模式,就适用于百度图片这种日常生活抓拍图片的效果。但是,仍然有一些图片是EasyPR不好处理的。或者可以说,按照目前的边缘检测算法,难以处理的。
请看下面一张图片:
图15 难以权衡的一张图片
这张图片最麻烦的地方在于车牌左右两侧凹下去的边侧,这个边缘在Sobel算子中非常明显,如果矩形模板过长,很容易跟它们连接起来。更麻烦的是这个车牌属于上面说的“断节”很容易发生的类型,因为车牌右侧字符的第一个字母是“1”,这个导致如果矩形模板过短,则很容易车牌断成两截。结果最后导致了如下的情况。
如果我设置矩形模板宽度为12,则会发生下面的情况:
图16 车牌被一分为二
如果我增加矩形模板宽度到13,则又会发生下面的情况。
图17 车牌区域被不不正确的放大
因此矩形模板的宽度是个整数值,在12和13中间没有中间值。这个导致几乎没有办法处理这幅车牌图像。
上面的情况属于车尾车牌的一种没办法解决的情况。下面所说的情况属于车头的情况,相比前者,错误检测的几率高的多!为什么,因为是一类型车牌无法处理。要问我这家车是哪家,我只能说:碰到开奥迪Q5及其系列的,早点嫁了吧。伤不起。
图18 奥迪Q5前部垂直边缘太多
这么多的垂直边缘,极为容易检错。已经试过了,几乎没有办法处理这种车牌。只能替换边缘检测这种思路,采用颜色区分等方法。奥体Q系列前脸太多垂直边缘了,给跪。
六.取轮廓
取轮廓操作是个相对简单的操作,因此只做简短的介绍。
1.目标
将连通域的外围勾画出来,便于形成外接矩形。
2.效果
我们这里看下经过取轮廓操作的效果。
图19 取轮廓操作
在图中,红色的线条就是轮廓,可以看到,有非常多的轮廓。取轮廓操作就是将图像中的所有独立的不与外界有交接的图块取出来。然后根据这些轮廓,求这些轮廓的最小外接矩形。这里面需要注意的是这里用的矩形是RotatedRect,意思是可旋转的。因此我们得到的矩形不是水平的,这样就为处理倾斜的车牌打下了基础。
取轮廓操作的代码如下:
1 vector< vector< Point> > contours; 2 findContours(img_threshold, 3 contours, // a vector of contours 4 CV_RETR_EXTERNAL, // 提取外部轮廓 5 CV_CHAIN_APPROX_NONE); // all pixels of each contours
七.尺寸判断
尺寸判断操作是对外接矩形进行判断,以判断它们是否是可能的候选车牌的操作。
1.目标
排除不可能是车牌的矩形。
2.效果
经过尺寸判断,会排除大量由轮廓生成的不合适尺寸的最小外接矩形。效果如下图:
图20 尺寸判断操作
通过对图像中所有的轮廓的外接矩形进行遍历,我们调用CplateLocate的另一个成员方法verifySizes,代码如下:
//! 对minAreaRect获得的最小外接矩形,用纵横比进行判断
bool CPlateLocate::verifySizes(RotatedRect mr)
{
float error = m_error;
//Spain car plate size: 52x11 aspect 4,7272
//China car plate size: 440mm*140mm,aspect 3.142857
float aspect = m_aspect;
//Set a min and max area. All other patchs are discarded
//int min= 1*aspect*1; // minimum area
//int max= 2000*aspect*2000; // maximum area
int min= 44*14*m_verifyMin; // minimum area
int max= 44*14*m_verifyMax; // maximum area
//Get only patchs that match to a respect ratio.
float rmin= aspect-aspect*error;
float rmax= aspect+aspect*error;
int area= mr.size.height * mr.size.width;
float r = (float)mr.size.width / (float)mr.size.height;
if(r < 1)
{
r= (float)mr.size.height / (float)mr.size.width;
}
if(( area < min || area > max ) || ( r < rmin || r > rmax ))
{
return false;
}
else
{
return true;
}
}
1 //! 对minAreaRect获得的最小外接矩形,用纵横比进行判断 2 bool CPlateLocate::verifySizes(RotatedRect mr) 3 { 4 float error = m_error; 5 //Spain car plate size: 52x11 aspect 4,7272 6 //China car plate size: 440mm*140mm,aspect 3.142857 7 float aspect = m_aspect; 8 //Set a min and max area. All other patchs are discarded 9 //int min= 1*aspect*1; // minimum area 10 //int max= 2000*aspect*2000; // maximum area 11 int min= 44*14*m_verifyMin; // minimum area 12 int max= 44*14*m_verifyMax; // maximum area 13 //Get only patchs that match to a respect ratio. 14 float rmin= aspect-aspect*error; 15 float rmax= aspect+aspect*error; 16 17 int area= mr.size.height * mr.size.width; 18 float r = (float)mr.size.width / (float)mr.size.height; 19 if(r < 1) 20 { 21 r= (float)mr.size.height / (float)mr.size.width; 22 } 23 24 if(( area < min || area > max ) || ( r < rmin || r > rmax )) 25 { 26 return false; 27 } 28 else 29 { 30 return true; 31 } 32 }
在原先的verifySizes方法中,使用的是针对西班牙车牌的检测。而我们的系统需要检测的是中国的车牌。因此需要对中国的车牌大小有一个认识。
中国车牌的一般大小是440mm*140mm,面积为440*140,宽高比为3.14。verifySizes使用如下方法判断矩形是否是车牌:
1.设立一个偏差率error,根据这个偏差率计算最大和最小的宽高比rmax、rmin。判断矩形的r是否满足在rmax、rmin之间。
2.设定一个面积最大值max与面积最小值min。判断矩形的面积area是否满足在max与min之间。
以上两个条件必须同时满足,任何一个不满足都代表这不是车牌。
偏差率和面积最大值、最小值都可以通过参数设置进行修改,且他们都有一个默认值。如果发现verifySizes方法无法发现你图中的车牌,试着修改这些参数。
另外,verifySizes方法是可选的。你也可以不进行verifySizes直接处理,但是这会大大加重后面的车牌判断的压力。一般来说,合理的verifySizes能够去除90%不合适的矩形。
八.角度判断
角度判断操作通过角度进一步排除一部分车牌。
1.目标
排除不可能是车牌的矩形。
通过verifySizes的矩形,还必须进行一个筛选,即角度判断。一般来说,在一副图片中,车牌不太会有非常大的倾斜,我们做如下规定:如果一个矩形的偏斜角度大于某个角度(例如30度),则认为不是车牌并舍弃。
对上面的尺寸判断结果的六个黄色矩形应用角度判断后结果如下图:
图21 角度判断后的候选车牌
可以看出,原先的6个候选矩形只剩3个。车牌两侧的车灯的矩形被成功筛选出来。角度判断会去除verifySizes筛选余下的7%矩形,使得最终进入车牌判断环节的矩形只有原先的全部矩形的3%。
角度判断以及接下来的旋转操作的代码如下:
int k = 1;
for(int i=0; i< rects.size(); i++)
{
RotatedRect minRect = rects[i];
if(verifySizes(minRect))
{
// rotated rectangle drawing
// Get rotation matrix
// 旋转这部分代码确实可以将某些倾斜的车牌调整正,
// 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。
// 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试
// 这段代码。
if(m_debug)
{
Point2f rect_points[4];
minRect.points( rect_points );
for( int j = 0; j < 4; j++ )
line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );
}
float r = (float)minRect.size.width / (float)minRect.size.height;
float angle = minRect.angle;
Size rect_size = minRect.size;
if (r < 1)
{
angle = 90 + angle;
swap(rect_size.width, rect_size.height);
}
//如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理
if (angle - m_angle < 0 && angle + m_angle > 0)
{
//Create and rotate image
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
Mat img_rotated;
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
Mat resultMat;
resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);
resultVec.push_back(resultMat);
}
}
1 int k = 1; 2 for(int i=0; i< rects.size(); i++) 3 { 4 RotatedRect minRect = rects[i]; 5 if(verifySizes(minRect)) 6 { 7 // rotated rectangle drawing 8 // Get rotation matrix 9 // 旋转这部分代码确实可以将某些倾斜的车牌调整正, 10 // 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。 11 // 2014-08-14,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试 12 // 这段代码。 13 if(m_debug) 14 { 15 Point2f rect_points[4]; 16 minRect.points( rect_points ); 17 for( int j = 0; j < 4; j++ ) 18 line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 ); 19 } 20 21 float r = (float)minRect.size.width / (float)minRect.size.height; 22 float angle = minRect.angle; 23 Size rect_size = minRect.size; 24 if (r < 1) 25 { 26 angle = 90 + angle; 27 swap(rect_size.width, rect_size.height); 28 } 29 //如果抓取的方块旋转超过m_angle角度,则不是车牌,放弃处理 30 if (angle - m_angle < 0 && angle + m_angle > 0) 31 { 32 //Create and rotate image 33 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1); 34 Mat img_rotated; 35 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC); 36 37 Mat resultMat; 38 resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++); 39 40 resultVec.push_back(resultMat); 41 } 42 }
九.旋转
旋转操作是为后面的车牌判断与字符识别提高成功率的关键环节。
1.目标
旋转操作将偏斜的车牌调整为水平。
2.效果
假设待处理的图片如下图:
图22 倾斜的车牌
使用旋转与不适用旋转的效果区别如下图:
图23 旋转的效果
可以看出,没有旋转操作的车牌是倾斜,加大了后续车牌判断与字符识别的难度。因此最好需要对车牌进行旋转。
在角度判定阈值内的车牌矩形,我们会根据它偏转的角度进行一个旋转,保证最后得到的矩形是水平的。调用的opencv函数如下:
1 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1); 2 Mat img_rotated; 3 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
这个调用使用了一个旋转矩阵,属于几何代数内容,在这里不做详细解释。
十.大小调整
结束了么?不,还没有,至少在我们把这些候选车牌导入机器学习模型之前,需要确保他们的尺寸一致。
机器学习模型在预测的时候,是通过模型输入的特征来判断的。我们的车牌判断模型的特征是所有的像素的值组成的矩阵。因此,如果候选车牌的尺寸不一致,就无法被机器学习模型处理。因此需要用resize方法进行调整。
我们将车牌resize为宽度136,高度36的矩形。为什么用这个值?这个值一开始也不是确定的,我试过许多值。最后我将近千张候选车牌做了一个统计,取它们的平均宽度与高度,因此就有了136和36这个值。所以,这个是一个统计值,平均来说,这个值的效果最好。
大小调整调用了CplateLocate的最后一个成员方法showResultMat,代码很简单,贴下,不做细讲了。
//! 显示最终生成的车牌图像,便于判断是否成功进行了旋转。
Mat CPlateLocate::showResultMat(Mat src, Size rect_size, Point2f center, int index)
{
Mat img_crop;
getRectSubPix(src, rect_size, center, img_crop);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_crop_" << index << ".jpg";
imwrite(ss.str(), img_crop);
}
Mat resultResized;
resultResized.create(HEIGHT, WIDTH, TYPE);
resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
if(m_debug)
{
stringstream ss(stringstream::in | stringstream::out);
ss << "tmp/debug_resize_" << index << ".jpg";
imwrite(ss.str(), resultResized);
}
return resultResized;
}
1 //! 显示最终生成的车牌图像,便于判断是否成功进行了旋转。 2 Mat CPlateLocate::showResultMat(Mat src, Size rect_size, Point2f center, int index) 3 { 4 Mat img_crop; 5 getRectSubPix(src, rect_size, center, img_crop); 6 7 if(m_debug) 8 { 9 stringstream ss(stringstream::in | stringstream::out); 10 ss << "tmp/debug_crop_" << index << ".jpg"; 11 imwrite(ss.str(), img_crop); 12 } 13 14 Mat resultResized; 15 resultResized.create(HEIGHT, WIDTH, TYPE); 16 17 resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC); 18 19 if(m_debug) 20 { 21 stringstream ss(stringstream::in | stringstream::out); 22 ss << "tmp/debug_resize_" << index << ".jpg"; 23 imwrite(ss.str(), resultResized); 24 } 25 26 return resultResized; 27 }
十一.总结
通过接近10多个步骤的处理,我们才有了最终的候选车牌。这些过程是一环套一环的,前步骤的输出是后步骤的输入,而且顺序也是有规则的。目前针对我的测试图片来说,它们工作的很好,但不一定适用于你的情况。车牌定位以及图像处理算法的一个大的问题就是他的弱鲁棒性,换一个场景可能就得换一套工作方式。因此结合你的使用场景来做调整吧,这是我为什么要在这里费这么多字数详细说明的原因。如果你不了解细节,你就不可能进行修改,也就无法使它适合你的工作需求。
讨论:
车牌定位全部步骤了解后,我们来讨论下。这个过程是否是一个最优的解?
毫无疑问,一个算法的好坏除了取决于它的设计思路,还取决于它是否充分利用了已知的信息。如果一个算法没有充分利用提供的信息,那么它就有进一步优化的空间。EasyPR的plateLocate过程就是如此,在实施过程中它相继抛弃掉了色彩信息,没有利用纹理信息,因此车牌定位的过程应该还有优化的空间。如果plateLocate过程无法良好的解决你的定位问题,那么尝试下能够利用其他信息的方法,也许你会大幅度提高你的定位成功率。
车牌定位讲完后,下面就是机器学习的过程。不同于前者,我不会重点说明其中的细节,而是会概括性的说明每个步骤的用途以及训练的最佳实践。在下一个章节中,我会首先介绍下什么是机器学习,为什么它如今这么火热,机器学习和大数据的关系,欢迎继续阅读。
本项目的Git地址:这里。如果有问题欢迎提issue。本文是一个系列中的第5篇,前几篇文章见前面的博客。
版权说明:
本文中的所有文字,图片,代码的版权都是属于作者和博客园共同所有。欢迎转载,但是务必注明作者与出处。任何未经允许的剽窃以及爬虫抓取都属于侵权,作者和博客园保留所有权利。
参考文献: