本篇文章介绍EasyPR里新的定位功能:颜色定位与偏斜扭正。但愿这篇文档能够帮助开发者与使用者更好的理解EasyPR的设计思想。html
让咱们先看一下示例图片,这幅图片中的车牌经过颜色的定位法进行定位并从偏斜的视角中扭正为正视角(请看右图的左上角)。git
图1 新版本的定位效果 github
下面内容会对这两个特性的实现过程展开具体的介绍。首先介绍颜色定位的原理,而后是偏斜扭正的实现细节。安全
因为本文较长,为方便读者,如下是本文的目录:架构
一.颜色定位ide
1.1起源函数
1.2方法测试
1.3不足与改善网站
二.偏斜扭正spa
2.1分析
2.2ROI截取
2.3扩大化旋转
2.4偏斜判断
2.5仿射变换
2.6总结
三.总结
在前面的介绍里,咱们使用了Sobel查找垂直边缘的方法,成功定位了许多车牌。可是,Sobel法最大的问题就在于面对垂直边缘交错的状况下,没法准确地定位车牌。例以下图。为了解决这个问题,能够考虑使用颜色信息进行定位。
图2 颜色定位与Sobel定位的比较
若是将颜色定位与Sobel定位加以结合的话,可使车牌的定位准确率从75%上升到94%。
关于颜色定位首先咱们想到的解决方案就是:利用RGB值来判断。
这个想法听起来很天然:若是咱们想找出一幅图像中的蓝色部分,那么咱们只须要检查RGB份量(RGB份量由Red份量--红色,Green份量--绿色,Blue份量--蓝色共同组成)中的Blue份量就能够了。通常来讲,Blue份量是个0到255的值。若是咱们设定一个阈值,而且检查每一个像素的Blue份量是否大于它,那咱们不就能够得知这些像素是否是蓝色的了么?这个想法虽然很好,不过存在一个问题,咱们该怎么来选择这个阈值?这是第一个问题。
即使咱们用一些方法决定了阈值之后,那么下面的一个问题就会让人抓狂,颜色是组合的,即使蓝色属性在255(这样已经很‘蓝’了吧),只要另外两个份量配合(例如都为255),你最后获得的不是蓝色,而是黑色。
这还只是区分蓝色的问题,黄色更麻烦,它是由红色和绿色组合而成的,这意味着你须要考虑两个变量的配比问题。这些问题让选择RGB颜色做为判断的难度大到难以接受的地步。所以必须另想办法。
为了解决各类颜色相关的问题,人们发明了各类颜色模型。其中有一个模型,很是适合解决颜色判断的问题。这个模型就是HSV模型。
图3 HSV颜色模型
HSV模型是根据颜色的直观特性建立的一种圆锥模型。与RGB颜色模型中的每一个份量都表明一种颜色不一样的是,HSV模型中每一个份量并不表明一种颜色,而分别是:色调(H),饱和度(S),亮度(V)。
H份量是表明颜色特性的份量,用角度度量,取值范围为0~360,从红色开始按逆时针方向计算,红色为0,绿色为120,蓝色为240。S份量表明颜色的饱和信息,取值范围为0.0~1.0,值越大,颜色越饱和。V份量表明明暗信息,取值范围为0.0~1.0,值越大,色彩越明亮。
H份量是HSV模型中惟一跟颜色本质相关的份量。只要固定了H的值,而且保持S和V份量不过小,那么表现的颜色就会基本固定。为了判断蓝色车牌颜色的范围,能够固定了S和V两个值为1之后,调整H的值,而后看颜色的变化范围。经过一段摸索,能够发现当H的取值范围在200到280时,这些颜色均可以被认为是蓝色车牌的颜色范畴。因而咱们能够用H份量是否在200与280之间来决定某个像素是否属于蓝色车牌。黄色车牌也是同样的道理,经过观察,能够发现当H值在30到80时,颜色的值能够做为黄色车牌的颜色。
这里的颜色表来自于这个网站。
下图显示了蓝色的H份量变化范围。
图4 蓝色的H份量区间
下图显示了黄色的H份量变化范围。
图5 黄色的H份量区间
光判断H份量的值是否就足够了?
事实上是不足的。固定了H的值之后,若是移动V和S会带来颜色的饱和度和亮度的变化。当V和S都达到最高值,也就是1时,颜色是最纯正的。下降S,颜色愈加趋向于变白。下降V,颜色趋向于变黑,当V为0时,颜色变为黑色。所以,S和V的值也会影响最终颜色的效果。
咱们能够设置一个阈值,假设S和V都大于阈值时,颜色才属于H所表达的颜色。
在EasyPR里,这个值是0.35,也就是V属于0.35到1且S属于0.35到1的一个范围,相似于一个矩形。对V和S的阈值判断是有必要的,由于不少车牌周身的车身,都是H份量属于200-280,而V份量或者S份量小于0.35的。经过S和V的判断能够排除车牌周围车身的干扰。
图6 V和S的区间
明确了使用HSV模型以及用阈值进行判断之后,下面就是一个颜色定位的完整过程。
第一步,将图像的颜色空间从RGB转为HSV,在这里因为光照的影响,对于图像使用直方图均衡进行预处理;
第二步,依次遍历图像的全部像素,当H值落在200-280之间而且S值与V值也落在0.35-1.0之间,标记为白色像素,不然为黑色像素;
第三步,对仅有白黑两个颜色的二值图参照原先车牌定位中的方法,使用闭操做,取轮廓等方法将车牌的外接矩形截取出来作进一步的处理。
图7 蓝色定位效果
以上就完成了一个蓝色车牌的定位过程。咱们把对图像中蓝色车牌的寻找过程称为一次与蓝色模板的匹配过程。代码中的函数称之为colorMatch。通常说来,一幅图像须要进行一次蓝色模板的匹配,还要进行一次黄色模板的匹配,以此确保蓝色和黄色的车牌都被定位出来。
黄色车牌的定位方法与其相似,仅仅只是H阈值范围的不一样。事实上,黄色定位的效果通常好的出奇,能够在很是复杂的环境下将车牌极为准确的定位出来,这可能源于现实世界中黄色很是醒目的缘由。
图8 黄色定位效果
从实际效果来看,颜色定位的效果是很好的。在通用数据测试集里,大约70%的车牌均可以被定位出来(一些颜色定位不了的,咱们能够用Sobel定位处理)。
在代码中有些细节须要注意:
一. opencv为了保证HSV三个份量都落在0-255之间(确保一个char能装的下),对H份量除以了2,也就是0-180的范围,S和V份量乘以了255,将0-1的范围扩展到0-255。咱们在设置阈值的时候须要参照opencv的标准,所以对参数要进行一个转换。
二. 是v和s取值的问题。对于暗的图来讲,取值过大容易漏,而对于亮的图,取值太小则容易跟车身混淆。所以能够考虑最适应的改变阈值。
三. 是模板问题。目前的作法是针对蓝色和黄色的匹配使用了两个模板,而不是统一的模板。统一模板的问题在于担忧蓝色和黄色的干扰问题,例如黄色的车与蓝色的牌的干扰,或者蓝色的车和黄色牌的干扰,这里面最典型的例子就是一个带有蓝色车牌的黄色出租车,在不少城市里这已是“标准配置”。所以须要将蓝色和黄色的匹配分别用不一样的模板处理。
了解完这三个细节之后,下面就是代码部分。
//! 根据一幅图像与颜色模板获取对应的二值图 //! 输入RGB图像, 颜色模板(蓝色、黄色) //! 输出灰度图(只有0和255两个值,255表明匹配,0表明不匹配) Mat colorMatch(const Mat& src, Mat& match, const Color r, const bool adaptive_minsv) { // S和V的最小值由adaptive_minsv这个bool值判断 // 若是为true,则最小值取决于H值,按比例衰减 // 若是为false,则再也不自适应,使用固定的最小值minabs_sv // 默认为false const float max_sv = 255; const float minref_sv = 64; const float minabs_sv = 95; //blue的H范围 const int min_blue = 100; //100 const int max_blue = 140; //140 //yellow的H范围 const int min_yellow = 15; //15 const int max_yellow = 40; //40 Mat src_hsv; // 转到HSV空间进行处理,颜色搜索主要使用的是H份量进行蓝色与黄色的匹配工做 cvtColor(src, src_hsv, CV_BGR2HSV); vector<Mat> hsvSplit; split(src_hsv, hsvSplit); equalizeHist(hsvSplit[2], hsvSplit[2]); merge(hsvSplit, src_hsv); //匹配模板基色,切换以查找想要的基色 int min_h = 0; int max_h = 0; switch (r) { case BLUE: min_h = min_blue; max_h = max_blue; break; case YELLOW: min_h = min_yellow; max_h = max_yellow; break; } float diff_h = float((max_h - min_h) / 2); int avg_h = min_h + diff_h; int channels = src_hsv.channels(); int nRows = src_hsv.rows; //图像数据列须要考虑通道数的影响; int nCols = src_hsv.cols * channels; if (src_hsv.isContinuous())//连续存储的数据,按一行处理 { nCols *= nRows; nRows = 1; } int i, j; uchar* p; float s_all = 0; float v_all = 0; float count = 0; for (i = 0; i < nRows; ++i) { p = src_hsv.ptr<uchar>(i); for (j = 0; j < nCols; j += 3) { int H = int(p[j]); //0-180 int S = int(p[j + 1]); //0-255 int V = int(p[j + 2]); //0-255 s_all += S; v_all += V; count++; bool colorMatched = false; if (H > min_h && H < max_h) { int Hdiff = 0; if (H > avg_h) Hdiff = H - avg_h; else Hdiff = avg_h - H; float Hdiff_p = float(Hdiff) / diff_h; // S和V的最小值由adaptive_minsv这个bool值判断 // 若是为true,则最小值取决于H值,按比例衰减 // 若是为false,则再也不自适应,使用固定的最小值minabs_sv float min_sv = 0; if (true == adaptive_minsv) min_sv = minref_sv - minref_sv / 2 * (1 - Hdiff_p); // inref_sv - minref_sv / 2 * (1 - Hdiff_p) else min_sv = minabs_sv; // add if ((S > min_sv && S < max_sv) && (V > min_sv && V < max_sv)) colorMatched = true; } if (colorMatched == true) { p[j] = 0; p[j + 1] = 0; p[j + 2] = 255; } else { p[j] = 0; p[j + 1] = 0; p[j + 2] = 0; } } } //cout << "avg_s:" << s_all / count << endl; //cout << "avg_v:" << v_all / count << endl; // 获取颜色匹配后的二值灰度图 Mat src_grey; vector<Mat> hsvSplit_done; split(src_hsv, hsvSplit_done); src_grey = hsvSplit_done[2]; match = src_grey; return src_grey; }
以上说明了颜色定位的设计思想与细节。那么颜色定位是否是就是万能的?答案是否认的。在色彩充足,光照足够的状况下,颜色定位的效果很好,可是在面对光线不足的状况,或者蓝色车身的状况时,颜色定位的效果很糟糕。下图是一辆蓝色车辆,能够看出,车牌与车身内容彻底重叠,没法分割。
图9 失效的颜色定位
碰到失效的颜色定位状况时须要使用原先的Sobel定位法。
目前的新版本使用了颜色定位与Sobel定位结合的方式。首先进行颜色定位,而后根据条件使用Sobel进行再次定位,增长整个系统的适应能力。
为了增强鲁棒性,Sobel定位法能够用两阶段的查找。也就是在已经被Sobel定位的图块中,再进行一次Sobel定位。这样能够增长准确率,但会下降了速度。一个折衷的方案是让用户决定一个参数m_maxPlates的值,这个值决定了你在一幅图里最多定位多少车牌。系统首先用颜色定位出候选车牌,而后经过SVM模型来判断是不是车牌,最后统计数量。若是这个数量大于你设定的参数,则认为车牌已经定位足够了,不须要后一步处理,也就不会进行两阶段的Sobel查找。相反,若是这个数量不足,则继续进行Sobel定位。
综合定位的代码位于CPlateDectec中的的成员函数plateDetectDeep中,如下是plateDetectDeep的总体流程。
图10 综合定位所有流程
有没有颜色定位与Sobel定位都失效的状况?有的。这种状况下可能须要使用第三类定位技术--字符定位技术。这是EasyPR发展的一个方向,这里不展开讨论。
解决了颜色的定位问题之后,下面的问题是:在定位之后,咱们如何把偏斜过来的车牌扭正呢?
图11 偏斜扭转效果
这个过程叫作偏斜扭转过程。其中一个关键函数就是opencv的仿射变换函数。但在具体实施时,有不少须要解决的问题。
在任何新的功能开发以前,技术预研都是第一步。
在这篇文档介绍了opencv的仿射变换功能。效果见下图。
图12 仿射变换效果
仔细看下,貌似这个功能跟咱们的需求很类似。咱们的偏斜扭转功能,说白了,就是把对图像的观察视角进行了一个转换。
不过这篇文章里的代码基原本自于另外一篇官方文档。官方文档里还有一个例子,能够矩形扭转成平行四边形。而咱们的需求正是将平行四边形的车牌扭正成矩形。这么说来,只要使用例子中对应的反函数,应该就能够实现咱们的需求。从这个角度来看,偏斜扭转功能够实现。肯定了可行性之后,下一步就是思考如何实现。
在原先的版本中,咱们对定位出来的区域会进行一次角度判断,当角度小于某个阈值(默认30度)时就会进行全图旋转。
这种方式有两个问题:
一是咱们的策略是对整幅图像旋转。对于opencv来讲,每次旋转操做都是一个矩形的乘法过程,对于很是大的图像,这个过程是很是消耗计算资源的;
二是30度的阈值没法处理示例图片。事实上,示例图片的定位区域的角度是-50度左右,已经大于咱们的阈值了。为了处理这样的图片,咱们须要把咱们的阈值增大,例如增长到60度,那么这样的结果是带来候选区域的增多。
两个因素结合,会大幅度增长处理时间。为了避免让处理速度降低,必须想办法规避这些影响。
一个方法是再也不使用全图旋转,而是区域旋转。其实咱们在获取定位区域后,咱们并不须要定位区域之外的图像。
假若咱们能划出一块小的区域包围定位区域,而后咱们仅对定位区域进行旋转,那么计算量就会大幅度下降。而这点,在opencv里是能够实现的,咱们对定位区域RotatedRect用boundingRect()方法获取外接矩形,再使用Mat(Rect ...)方法截取这个区域图块,从而生成一个小的区域图像。因而下面的全部旋转等操做均可以基于这个区域图像进行。
在这些设计决定之后,下面就来思考整个功能的架构。
咱们要解决的问题包括三类,第一类是正的车牌,第二类是倾斜的车牌,第三类是偏斜的车牌。前两类是前面说过的,第三类是本次新增的功能需求。第二类倾斜车牌与第三类车牌的区别见下图。
图13 两类不一样的旋转
经过上图能够看出,正视角的旋转图片的观察角度仍然是正方向的,只是因为路的不平或者摄像机的倾斜等缘由,致使矩形有必定倾斜。这类图块的特色就是在RotataedRect内部,车牌部分仍然是个矩形。偏斜视角的图片的观察角度是非正方向的,是从侧面去看车牌。这类图块的特色是在RotataedRect内部,车牌部分再也不是个矩形,而是一个平行四边形。这个特性决定了咱们须要区别的对待这两类图片。
一个初步的处理思路就是下图。
图14 分析实现流程
简单来讲,整个处理流程包括下面四步:
1.感兴趣区域的截取
2.角度判断
3.偏斜判断
4.仿射变换
接下来按照这四个步骤依次介绍。
若是要使用区域旋转,首先咱们必须从原图中截取出一个包含定位区域的图块。
opencv提供了一个从图像中截取感兴趣区域ROI的方法,也就是Mat(Rect ...)。这个方法会在Rect所在的位置,截取原图中一个图块,而后将其赋值到一个新的Mat图像里。遗憾的是这个方法不支持RotataedRect,同时Rect与RotataedRect也没有继承关系。所以布不能直接调用这个方法。
咱们可使用RotataedRect的boudingRect()方法。这个方法会返回一个RotataedRect的最小外接矩形,并且这个矩形是一个Rect。所以将这个Rect传递给Mat(Rect...)方法就能够截取出原图的ROI图块,并得到对应的ROI图像。
须要注意的是,ROI图块和ROI图像的区别,当咱们给定原图以及一个Rect时,原图中被Rect包围的区域称为ROI图块,此时图块里的坐标仍然是原图的坐标。当这个图块里的内容被拷贝到一个新的Mat里时,咱们称这个新Mat为ROI图像。ROI图像里仅仅只包含原来图块里的内容,跟原图没有任何关系。因此图块和图像虽然显示的内容同样,但坐标系已经发生了改变。在从ROI图块到ROI图像之后,点的坐标要计算一个偏移量。
下一步的工做中能够仅对这个ROI图像进行处理,包括对其旋转或者变换等操做。
示例图片中的截取出来的ROI图像以下图:
图15 截取后的ROI图像
在截取中可能会发生一个问题。若是直接使用boundingRect()函数的话,在运行过程当中会常常发生这样的异常。OpenCV Error: Assertion failed (0 <= roi.x && 0 <= roi.width && roi.x + roi.width <= m.cols && 0 <= roi.y && 0 <= roi.height && roi.y + roi.height <= m.rows) incv::Mat::Mat,以下图。
图16 不安全的外接矩形函数会抛出异常
这个异常产生的缘由在于,在opencv2.4.8中(不清楚opencv其余版本是否没有这个问题),boundingRect()函数计算出的Rect的四个点的坐标没有作验证。这意味着你计算一个RotataedRect的最小外接矩形Rect时,它可能会给你一个负坐标,或者是一个超过原图片外界的坐标。因而当你把Rect做为参数传递给Mat(Rect ...)的话,它会提示你所要截取的Rect中的坐标越界了!
解决方案是实现一个安全的计算最小外接矩形Rect的函数,在boundingRect()结果之上,对角点坐标进行一次判断,若是值为负数,就置为0,若是值超过了原始Mat的rows或cols,就置为原始Mat的这些rows或cols。
这个安全函数名为calcSafeRect(...),下面是这个函数的代码。
//! 计算一个安全的Rect //! 若是不存在,返回false bool CPlateLocate::calcSafeRect(const RotatedRect& roi_rect, const Mat& src, Rect_<float>& safeBoundRect) { Rect_<float> boudRect = roi_rect.boundingRect(); // boudRect的左上的x和y有可能小于0 float tl_x = boudRect.x > 0 ? boudRect.x : 0; float tl_y = boudRect.y > 0 ? boudRect.y : 0; // boudRect的右下的x和y有可能大于src的范围 float br_x = boudRect.x + boudRect.width < src.cols ? boudRect.x + boudRect.width - 1 : src.cols - 1; float br_y = boudRect.y + boudRect.height < src.rows ? boudRect.y + boudRect.height - 1 : src.rows - 1; float roi_width = br_x - tl_x; float roi_height = br_y - tl_y; if (roi_width <= 0 || roi_height <= 0) return false; // 新建一个mat,确保地址不越界,以防mat定位roi时抛异常 safeBoundRect = Rect_<float>(tl_x, tl_y, roi_width, roi_height); return true; }
好,当我经过calcSafeRect(...)获取了一个安全的Rect,而后经过Mat(Rect ...)函数截取了这个感兴趣图像ROI之后。下面的工做就是对这个新的ROI图像进行操做。
首先是判断这个ROI图像是否要旋转。为了下降工做量,咱们不对角度在-5度到5度区间的ROI进行旋转(注意这里讲的角度针对的生成ROI的RotataedRect,ROI自己是水平的)。由于这么小的角度对于SVM判断以及字符识别来讲,都是没有影响的。
对其余的角度咱们须要对ROI进行旋转。当咱们对ROI进行旋转之后,接着把转正后的RotataedRect部分从ROI中截取出来。
但很快咱们就会碰到一个新问题。让咱们看一下下图,为何咱们截取出来的车牌区域最左边的“川”字和右边的“2”字发生了形变?为了搞清这个缘由,做者仔细地研究了旋转与截取函数,但很快发现了形变的根源在于旋转后的ROI图像。
仔细看一下旋转后的ROI图像,是否左右两侧再也不完整,像是被截去了一部分?
图17 旋转后图像被截断
要想理解这个问题,须要理解opencv的旋转变换函数的特性。做为旋转变换的核心函数,affinTransform会要求你输出一个旋转矩阵给它。这很简单,由于咱们只须要给它一个旋转中心点以及角度,它就能计算出咱们想要的旋转矩阵。旋转矩阵的得到是经过以下的函数获得的:
Mat rot_mat = getRotationMatrix2D(new_center, angle, 1);
在获取了旋转矩阵rot_mat,那么接下来就须要调用函数warpAffine来开始旋转操做。这个函数的参数包括一个目标图像、以及目标图像的Size。目标图像容易理解,大部分opencv的函数都会须要这个参数。咱们只要新建一个Mat便可。那么目标图像的Size是什么?在通常的观点中,假设咱们须要旋转一个图像,咱们给opencv一个原始图像,以及我须要在某个旋转点对它旋转一个角度的需求,那么opencv返回一个图像给我便可,这个图像的Size或者说大小应该是opencv返回给个人,为何要我来告诉它呢?
你能够试着对一个正方形进行旋转,仔细看看,这个正方形的外接矩形的大小会如何变化?当旋转角度还小时,一切都还好,当角度变大时,明显咱们看到的外接矩形的大小也在扩增。在这里,外接矩形被称为视框,也就是我须要旋转的正方形所须要的最小区域。随着旋转角度的变大,视框明显增大。
图18 矩形旋转后所需视框增大
在图像旋转完之后,有三类点会得到不一样的处理,一种是有原图像对应点且在视框内的,这些点被正常显示;一类是在视框内但找不到原图像与之对应的点,这些点被置0值(显示为黑色);最后一类是有原图像与之对应的点,但不在视框内的,这些点被悲惨的抛弃。
图19 旋转后三类不一样点的命运
这就是旋转后不一样三类点的命运,也就是新生成的图像中一些点呈现黑色(被置0),一些点被截断(被抛弃)的缘由。若是把视框调整大点的话,就能够大幅度减小被截断点的数量。因此,为了保证旋转后的图像不被截断,所以咱们须要计算一个合理的目标图像的Size,让咱们的感兴趣区域获得完整的显示。
下面的代码使用了一个极为简单的策略,它将原始图像与目标图像都进行了扩大化。首先新建一个尺寸为原始图像1.5倍的新图像,接着把原始图像映射到新图像上,因而咱们获得了一个显示区域(视框)扩大化后的原始图像。显示区域扩大之后,那些在原图像中没有值的像素被置了一个初值。
接着调用warpAffine函数,使用新图像的大小做为目标图像的大小。warpAffine函数会将新图像旋转,并用目标图像尺寸的视框去显示它。因而咱们获得了一个全部感兴趣区域都被完整显示的旋转后图像。
这样,咱们再使用getRectSubPix()函数就能够得到想要的车牌区域了。
图20 扩大化旋转后图像再也不被截断
如下就是旋转函数rotation的代码。
//! 旋转操做 bool CPlateLocate::rotation(Mat& in, Mat& out, const Size rect_size, const Point2f center, const double angle) { Mat in_large; in_large.create(in.rows*1.5, in.cols*1.5, in.type()); int x = in_large.cols / 2 - center.x > 0 ? in_large.cols / 2 - center.x : 0; int y = in_large.rows / 2 - center.y > 0 ? in_large.rows / 2 - center.y : 0; int width = x + in.cols < in_large.cols ? in.cols : in_large.cols - x; int height = y + in.rows < in_large.rows ? in.rows : in_large.rows - y; /*assert(width == in.cols); assert(height == in.rows);*/ if (width != in.cols || height != in.rows) return false; Mat imageRoi = in_large(Rect(x, y, width, height)); addWeighted(imageRoi, 0, in, 1, 0, imageRoi); Point2f center_diff(in.cols/2, in.rows/2); Point2f new_center(in_large.cols / 2, in_large.rows / 2); Mat rot_mat = getRotationMatrix2D(new_center, angle, 1); /*imshow("in_copy", in_large); waitKey(0);*/ Mat mat_rotated; warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows), CV_INTER_CUBIC); /*imshow("mat_rotated", mat_rotated); waitKey(0);*/ Mat img_crop; getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height), new_center, img_crop); out = img_crop; /*imshow("img_crop", img_crop); waitKey(0);*/ return true; }
当咱们对ROI进行旋转之后,下面一步工做就是把RotataedRect部分从ROI中截取出来,这里可使用getRectSubPix方法,这个函数能够在被旋转后的图像中截取一个正的矩形图块出来,并赋值到一个新的Mat中,称为车牌区域。
下步工做就是分析截取后的车牌区域。车牌区域里的车牌分为正角度和偏斜角度两种。对于正的角度而言,能够看出车牌区域就是车牌,所以直接输出便可。而对于偏斜角度而言,车牌是平行四边形,与矩形的车牌区域不重合。
如何判断一个图像中的图形是不是平行四边形?
一种简单的思路就是对图像二值化,而后根据二值化图像进行判断。图像二值化的方法有不少种,假设咱们这里使用一开始在车牌定位功能中使用的大津阈值二值化法的话,效果不会太好。由于大津阈值是自适应阈值,在完整的图像中二值出来的平行四边形可能在小的局部图像中就再也不是。最好的办法是使用在前面定位模块生成后的原图的二值图像,咱们经过一样的操做就能够在原图中截取一个跟车牌区域对应的二值化图像。
下图就是一个二值化车牌区域得到的过程。
图21 二值化的车牌区域
接下来就是对二值化车牌区域进行处理。为了判断二值化图像中白色的部分是平行四边形。一种简单的作法就是从图像中选择一些特定的行。计算在这个行中,第一个全为0的串的长度。从几何意义上来看,这就是平行四边形斜边上某个点距离外接矩形的长度。
假设咱们选择的这些行位于二值化图像高度的1/4,2/4,3/4处的话,若是是白色图形是矩形的话,这些串的大小应该是相等或者相差很小的,相反若是是平行四边形的话,那么这些串的大小应该不等,而且呈现一个递增或递减的关系。经过这种不一样,咱们就能够判断车牌区域里的图形,到底是矩形仍是平行四边形。
偏斜判断的另外一个重要做用就是,计算平行四边形倾斜的斜率,这个斜率值用来在下面的仿射变换中发挥做用。咱们使用一个简单的公式去计算这个斜率,那就是利用上面判断过程当中使用的串大小,假设二值化图像高度的1/4,2/4,3/4处对应的串的大小分别为len1,len2,len3,车牌区域的高度为Height。一个计算斜率slope的计算公式就是:(len3-len1)/Height*2。
Slope的直观含义见下图。
图22 slope的几何含义
须要说明的,这个计算结果在平行四边形是右斜时是负值,而在左斜时则是正值。因而能够根据slope的正负判断平行四边形是右斜或者左斜。在实践中,会发生一些公式不能应对的状况,例如像下图这种状况,斜边的部分区域发生了内凹或者外凸现象。这种现象会致使len1,len2或者len3的计算有误,所以slope也会不许。
图23 内凹现象
为了实现一个鲁棒性更好的计算方法,能够用(len2-len1)/Height*4与(len3-len1)/Height*2二者之间更靠近tan(angle)的值做为solpe的值(在这里,angle表明的是原来RotataedRect的角度)。
多采起了一个slope备选的好处是能够避免单点的内凹或者外凸,但这仍然不是最好的解决方案。在最后的讨论中会介绍一个其余的实现思路。
完成偏斜判断与斜率计算的函数是isdeflection,下面是它的代码。
//! 是否偏斜 //! 输入二值化图像,输出判断结果 bool CPlateLocate::isdeflection(const Mat& in, const double angle, double& slope) { int nRows = in.rows; int nCols = in.cols; assert(in.channels() == 1); int comp_index[3]; int len[3]; comp_index[0] = nRows / 4; comp_index[1] = nRows / 4 * 2; comp_index[2] = nRows / 4 * 3; const uchar* p; for (int i = 0; i < 3; i++) { int index = comp_index[i]; p = in.ptr<uchar>(index); int j = 0; int value = 0; while (0 == value && j < nCols) value = int(p[j++]); len[i] = j; } //cout << "len[0]:" << len[0] << endl; //cout << "len[1]:" << len[1] << endl; //cout << "len[2]:" << len[2] << endl; double maxlen = max(len[2], len[0]); double minlen = min(len[2], len[0]); double difflen = abs(len[2] - len[0]); //cout << "nCols:" << nCols << endl; double PI = 3.14159265; double g = tan(angle * PI / 180.0); if (maxlen - len[1] > nCols/32 || len[1] - minlen > nCols/32 ) { // 若是斜率为正,则底部在下,反之在上 double slope_can_1 = double(len[2] - len[0]) / double(comp_index[1]); double slope_can_2 = double(len[1] - len[0]) / double(comp_index[0]); double slope_can_3 = double(len[2] - len[1]) / double(comp_index[0]); /*cout << "slope_can_1:" << slope_can_1 << endl; cout << "slope_can_2:" << slope_can_2 << endl; cout << "slope_can_3:" << slope_can_3 << endl;*/ slope = abs(slope_can_1 - g) <= abs(slope_can_2 - g) ? slope_can_1 : slope_can_2; /*slope = max( double(len[2] - len[0]) / double(comp_index[1]), double(len[1] - len[0]) / double(comp_index[0]));*/ //cout << "slope:" << slope << endl; return true; } else { slope = 0; } return false; }
俗话说:行百里者半九十。前面已经作了如此多的工做,应该能够实现偏斜扭转功能了吧?但在最后的道路中,仍然有问题等着咱们。
咱们已经实现了旋转功能,而且在旋转后的区域中截取了车牌区域,而后判断车牌区域中的图形是一个平行四边形。下面要作的工做就是把平行四边形扭正成一个矩形。
图24 从平行四边形车牌到矩形车牌
首先第一个问题就是解决如何从平行四边形变换成一个矩形的问题。opencv提供了一个函数warpAffine,就是仿射变换函数。注意,warpAffine不只可让图像旋转(前面介绍过),也能够进行仿射变换,真是一个多才多艺的函数。o
经过仿射变换函数能够把任意的矩形拉伸成其余的平行四边形。opencv的官方文档里给了一个示例,值得注意的是,这个示例演示的是把矩形变换为平行四边形,跟咱们想要的偏偏相反。但不要紧,咱们先看一下它的使用方法。
图25 opencv官网上对warpAffine使用的示例
warpAffine方法要求输入的参数是原始图像的左上点,右上点,左下点,以及输出图像的左上点,右上点,左下点。注意,必须保证这些点的对应顺序,不然仿射的效果跟你预想的不同。经过这个方法介绍,咱们能够大概看出,opencv须要的是三个点对(共六个点)的坐标,而后创建一个映射关系,经过这个映射关系将原始图像的全部点映射到目标图像上。
图26 warpAffine须要的三个对应坐标点
再回来看一下咱们的需求,咱们的目标是把车牌区域中的平行四边形映射为一个矩形。让咱们作个假设,若是咱们选取了车牌区域中的平行四边形车牌的三个关键点,而后再肯定了咱们但愿将车牌扭正成的矩形的三个关键点的话,咱们是否就能够实现从平行四边形车牌到矩形车牌的扭正?
让咱们画一幅图像来看看这个变换的做用。有趣的是,把一个平行四边形变换为矩形会对包围平行四边形车牌的区域带来影响。
例以下图中,蓝色的实线表明扭转前的平行四边形车牌,虚线表明扭转后的。黑色的实线表明矩形的车牌区域,虚线表明扭转后的效果。能够看到,当蓝色车牌被扭转为矩形的同时,黑色车牌区域则被扭转为平行四边形。
注意,当车牌区域扭变为平行四边形之后,须要显示它的视框增大了。跟咱们在旋转图像时碰到的情形同样。
图27 平行四边形的扭转带来的变化
让咱们先实际尝试一下仿射变换吧。
根据仿射函数的须要,咱们计算平行四边形车牌的三个关键点坐标。其中左上点的值(xdiff,0)中的xdiff就是根据车牌区域的高度height与平行四边形的斜率slope计算获得的:
xidff = Height * abs(slope)
为了计算目标矩形的三个关键点坐标,咱们首先须要把扭转后的原点坐标调整到平行四边形车牌区域左上角位置。见下图。
图28 原图像的坐标计算
依次推算关键点的三个坐标。它们应该是
plTri[0] = Point2f(0 + xiff, 0); plTri[1] = Point2f(width - 1, 0); plTri[2] = Point2f(0, height - 1); dstTri[0] = Point2f(xiff, 0); dstTri[1] = Point2f(width - 1, 0); dstTri[2] = Point2f(xiff, height - 1);
根据上图的坐标,咱们开始进行一次仿射变换的尝试。
opencv的warpAffine函数不会改变变换后图像的大小。而咱们给它传递的目标图像的大小仅会决定视框的大小。不过此次咱们不用担忧视框的大小,由于根据图27看来,哪怕视框跟原始图像同样大,咱们也足够显示扭正后的车牌。
看看仿射的效果。晕,好像效果不对,视框的大小是足够了,可是图像往右偏了一些,致使最右边的字母没有显示全。
图29 被偏移的车牌区域
此次的问题再也不是目标图像的大小问题了,而是视框的偏移问题。仔细观察一下咱们的视框,假若咱们想把车牌所有显示的话,视框往右偏移一段距离,是否是就能够解决这个问题呢?为保证新的视框中心可以正好与车牌的中心重合,咱们能够选择偏移xidff/2长度。正以下图所显示的同样。
图30 考虑偏移的坐标计算
视框往右偏移的含义就是目标图像Mat的原点往右偏移。若是原点偏移的话,那么仿射后图像的三个关键点的坐标要从新计算,都须要减去xidff/2大小。
从新计算的映射点坐标为下:
plTri[0] = Point2f(0 + xiff, 0); plTri[1] = Point2f(width - 1, 0); plTri[2] = Point2f(0, height - 1); dstTri[0] = Point2f(xiff/2, 0); dstTri[1] = Point2f(width - 1 - xiff + xiff/2, 0); dstTri[2] = Point2f(xiff/2, height - 1);
再试一次。果真,视框被调整到咱们但愿的地方了,咱们能够看到全部的车牌区域了。此次解决的是warpAffine函数带来的视框偏移问题。
图31 完整的车牌区域
关于坐标调整的另外一个理解就是当中心点保持不变时,平行四边形扭正为矩形时刚好是左上的点往左偏移了xdiff/2的距离,左下的点往右偏移了xdiff/2的距离,造成一种对称的平移。可使用ps或者inkspace相似的矢量制图软件看看“斜切”的效果,
如此一来,就完成了偏斜扭正的过程。须要注意的是,向左倾斜的车牌的视框偏移方向与向右倾斜的车牌是相反的。咱们能够用slope的正负来判断车牌是左斜仍是右斜。
经过以上过程,咱们成功的将一个偏斜的车牌通过旋转变换等方法扭正过来。
让咱们回顾一下偏斜扭正过程。咱们须要将一个偏斜的车牌扭正,为了达成这个目的咱们首先须要对图像进行旋转。由于旋转是个计算量很大的函数,因此咱们须要考虑再也不用全图旋转,而是区域旋转。在旋转过程当中,会发生图像截断问题,因此须要使用扩大化旋转方法。旋转之后,只有偏斜视角的车牌才须要扭正,正视角的车牌不须要,所以还须要一个偏斜判断过程。如此一来,偏斜扭正的过程须要旋转,区域截取,扩大化,偏斜判断等等过程的协助,这就是整个流程中有这么多步须要处理的缘由。
下图从另外一个视角回顾了偏斜扭正的过程,主要说明了偏斜扭转中的两次“截取”过程。
图32 偏斜扭正全过程
整个过程有一个统一的函数--deskew。下面是deskew的代码。
//! 抗扭斜处理 int CPlateLocate::deskew(const Mat& src, const Mat& src_b, vector<RotatedRect>& inRects, vector<CPlate>& outPlates) { for (int i = 0; i < inRects.size(); i++) { RotatedRect roi_rect = inRects[i]; float r = (float)roi_rect.size.width / (float)roi_rect.size.height; float roi_angle = roi_rect.angle; Size roi_rect_size = roi_rect.size; if (r < 1) { roi_angle = 90 + roi_angle; swap(roi_rect_size.width, roi_rect_size.height); } if (roi_angle - m_angle < 0 && roi_angle + m_angle > 0) { Rect_<float> safeBoundRect; bool isFormRect = calcSafeRect(roi_rect, src, safeBoundRect); if (!isFormRect) continue; Mat bound_mat = src(safeBoundRect); Mat bound_mat_b = src_b(safeBoundRect); Point2f roi_ref_center = roi_rect.center - safeBoundRect.tl(); Mat deskew_mat; if ((roi_angle - 5 < 0 && roi_angle + 5 > 0) || 90.0 == roi_angle || -90.0 == roi_angle) { deskew_mat = bound_mat; } else { // 角度在5到60度之间的,首先须要旋转 rotation Mat rotated_mat; Mat rotated_mat_b; if (!rotation(bound_mat, rotated_mat, roi_rect_size, roi_ref_center, roi_angle)) continue; if (!rotation(bound_mat_b, rotated_mat_b, roi_rect_size, roi_ref_center, roi_angle)) continue; // 若是图片偏斜,还须要视角转换 affine double roi_slope = 0; if (isdeflection(rotated_mat_b, roi_angle, roi_slope)) { //cout << "roi_angle:" << roi_angle << endl; //cout << "roi_slope:" << roi_slope << endl; affine(rotated_mat, deskew_mat, roi_slope); } else deskew_mat = rotated_mat; } Mat plate_mat; plate_mat.create(HEIGHT, WIDTH, TYPE); if (deskew_mat.cols >= WIDTH || deskew_mat.rows >= HEIGHT) resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_AREA); else resize(deskew_mat, plate_mat, plate_mat.size(), 0, 0, INTER_CUBIC); /*if (1) { imshow("plate_mat", plate_mat); waitKey(0); destroyWindow("plate_mat"); }*/ CPlate plate; plate.setPlatePos(roi_rect); plate.setPlateMat(plate_mat); outPlates.push_back(plate); } } return 0; }
最后是改善建议:
角度偏斜判断时能够用白色区域的轮廓来肯定平行四边形的四个点,而后用这四个点来计算斜率。这样算出来的斜率的可能鲁棒性更好。
本篇文档介绍了颜色定位与偏斜扭转等功能。其中颜色定位属于做者一直想作的定位方法,而偏斜扭转则是做者之前认为不可能解决的问题。这些问题如今都基本被攻克了,并在这篇文档中阐述,但愿这篇文档能够帮助到读者。
做者但愿能在这片文档中不只传递知识,也传授我在摸索过程当中积累的经验。由于光知道怎么作并不能加深对车牌识别的认识,只有经历过失败,了解哪些思想尝试过,碰到了哪些问题,是如何解决的,才能帮助读者更好地认识这个系统的内涵。
最后,做者很感谢可以阅读到这里的读者。若是看完以为好的话,还请轻轻点一下赞,大家的鼓励就是做者继续行文的动力。
对EasyPR作下说明:EasyPR,一个开源的中文车牌识别系统,代码托管在github。其次,在前面的博客文章中,包含EasyPR至今的开发文档与介绍。在后续的文章中,做者会介绍EasyPR中字符分割与识别等相关内容,欢迎继续阅读。
版权说明:
本文中的全部文字,图片,代码的版权都是属于做者和博客园共同全部。欢迎转载,可是务必注明做者与出处。任何未经容许的剽窃以及爬虫抓取都属于侵权,做者和博客园保留全部权利。
参考文献:
1.http://blog.csdn.net/xiaowei_cqu/article/details/7616044
2.http://docs.opencv.org/doc/tutorials/imgproc/imgtrans/warp_affine/warp_affine.html