你们好,很久不见了。html
一转眼距离上一篇博客已是4个月前的事了。要问博主这段时间去干了什么,我只能说:我去“外面看了看”。git
图1 我想去看看 github
在外面跟几家创业公司谈了谈,交流了一些大数据与机器视觉相关的心得与经验。不过因为各类缘由,博主又回来了。算法
目前,博主的工做是在本地的一个高校作科研。而研究的方向主要是计算机视觉。数组
图2 科研就是不断的探索过程网络
因为我所作的是计算机视觉方向,跟EasyPR自己很是契合。将来这个这个系列的博客会继续下去,而且之后会有更加专业的内容。架构
目前我研究的方向是文字定位,这个技术跟车牌定位很像,都是在图中去定位一些语言相关的位置。不一样之处在于,车牌定位只须要处理的是在车牌中出现的文字,字体,颜色都比较固定,背景也比相对单一(蓝色和黄色等)。ide
文字定位则复杂不少,研究界目前要处理的是是各类类型,不一样字体,且拥有复杂背景的文字。下图是一张样例:函数
图3 文字定位图片样例字体
能够看出,文字定位要处理的问题是相似车牌定位的,不过难度要更大。一些文字定位的技术也应该能够应用于车牌的定位和识别。
将来EasyPR会借鉴文字定位的一些思想和技术,来强化其定位的效果。
一.前言
今天继续咱们EasyPR的开发详解。
这几个月我收到了很多的邮件问:为何EasyPR开发详解教程中只有车牌定位的部分,而没有字符识别的部分?
这个缘由一是因为整个开发详解是按照车牌识别的流程顺序来的,所以先讲定位,后面再讲字符识别。因此字符识别的部分出来的比较晚。
二是因为字符识别相对于前面的车牌定位而言,显得较为简单。不像在一个复杂和低分辨场景下进行车牌定位,在字符分割和识别的部分时,所须要处理的场景已经较为固定了,所以其处理技术也较为单一。
这两个缘由是字符分割和识别部分出来较晚的缘由。不过在本篇博客中咱们会将字符分割部分讲完。
二.总体流程
咱们首先看一下,字符分割所须要处理的输入: 便是前面车牌定位中的结果,一个完整的车牌。
图4 字符分割模块的输入
因为在车牌定位中,咱们使用了归一化过程。所以所须要处理的车牌的大小是统一的,在目前的版本中(v1.3),这个值是136*36。
那么字符分割的结果就是将车牌中的全部文字一一分割开来,造成单一的字符块。生成的字符块就能够输入下一步的字符识别部分进行识别。在EasyPR里,字符识别所使用的技术是人工神经网络,也就是ANN。
具体而言,字符分割过程是如何作的呢?简单说,就是:灰度化->颜色判断->二值化->取轮廓->找外接矩形->截取图块。
图5 字符分割处理流程
下面,咱们使用下图的车牌完整的跑一遍字符分割的流程,以此对其有一个全局的认识。
图6 原始图片
1.灰度化
首先,咱们把彩色的图片转化为灰度化图片。注意:为了之后能够利用彩色信息,在前面的车牌检测过程当中,咱们的输出结果不是灰度化图片,而是彩色图片。这样之后当咱们改正算法,想利用彩色信息时就可使用了。
可是在这里,咱们的算法仍是针对的是灰度化图片,所以首先进行灰度化处理。
灰度化后的图片见下图:
图7 灰度化后结果
2.颜色判断
灰度化以后,为了分割字符。咱们须要获取字符的轮廓。注意:分割字符有不少种方法。例如投影法,滑动窗口判断法,在这里,EasyPR使用的是取字符轮廓法。
由于须要取轮廓,就须要把图片转化成一个二值化图片。不过,因为蓝色和黄色车牌图片的区别,二者须要用的二值化参数不同,所以这里须要对车牌图片的颜色进行一个判断。车牌颜色对二值化的影响的分析见后面“其余细节”章节。
这里颜色判断的使用的是前面颜色定位详解里的模板匹配法。
图8 颜色判断
3.二值化
获取颜色后,就能够选择不一样的参数进行大津阈值法来进行二值化。对于本示例图片中的蓝色车牌而言,使用的参数为CV_THRESH_BINARY。
二值化后的效果见下图:
图9 二值化后结果
4.取轮廓
接下来,使用被屡次用到的取轮廓方法findContours。关于这个方法的具体内容,在前面的开发详解中已作过介绍,这里再也不赘述。
取轮廓后的结果以下图:
图10 取轮廓操做
注意:直接使用findContours方法取轮廓时,在处理中文字符,也就是“苏”时,会发生断裂现象。所以为了处理中文字符,EasyPR换了一种思路,使用了额外的步骤来解决这个问题。具体能够见后面的“中文字符处理”章节。
5.找外接矩形
使用了中文字符处理方法之后,成功获取了全部的字符的外接矩形。
具体见下图:
图11 全部字符的外接矩形
6.截取图块
最后,把图中的外接矩形一一截取出来,归一化到统一格式。留待输入下个步骤--字符识别模块处理。
归一化后字符图块见下图:
图12 截取并归一化的图块
三.中文字符处理
上面的流程在处理英文车牌时,效果是很好的。可是在处理中文车牌时,存在一个很大的问题。
在取轮廓时,中文因为自身的特性,例若有笔画区间,取轮廓会形成断裂现象。例以下图中的“苏”。英文字符经过取轮廓都被完整的包括了,而“苏”字则分红了两个连通区域。
图13 取轮廓操做示例
虽然并非全部的中文都会存在这个问题(例以下图的“津”字),但直接用取轮廓操做已经不合适了。
EasyPR是如何解决这个问题的呢?其实想法很简单。那就是既然有些中文字符没办法用取轮廓处理,那么就干脆先不处理中文字符,而是用取轮廓操做处理中文字符后面的字符。例如“苏A88M88”,其中“A88M88”这六个字符我都能用取轮廓操做得到。我先获取这六个字符,再想办法获取中文字符。
图14 “津”字
获取这六个字符后,接下来该如何获取“苏”这个中文字符的轮廓呢?
这里的关键就是“苏”字符后面的“A”字符,这个字符在中文车牌里表明城市的代码,咱们在这里简称它为“城市字符”或者“特殊字符”。
这个字符有一个特征,就是与后面的字符存在必定的间隔。可是与前面的中文字符靠的较紧。假若我获取了这个特殊字符的外接矩形,只要把这个外接矩形向左作一些的偏移(偏移的大小能够经过经验指定,例如设置为字符宽度的1.15倍),这样这个外接矩形就成了包含中文字符的一个矩形了。下面就能够截取中文字符的图块。
下图就是“特殊字符”与被反推获得的“中文字符”的矩形,在图中用红色矩形表示。
图15 反推获得的中文字符位置
下面的问题就是如何获取“特殊字符”的位置?
一种方法是把全部取轮廓操做获取到的矩形进行排序,最左边的就是特殊字符的图块。可是有些中文字符会被取轮廓操做截取为一个连通区域。在这种状况下,最左边的图块矩形是中文字符的矩形,而不是特殊字符的矩形了。因此这个方法不能用。
另外一种方法就是依次判断全部取轮廓操做获得的矩形的位置,设矩形的中点刚好在整个车牌的1/7到2/7之间时的矩形为特殊矩形。这样操做的前提是咱们的车牌定位的很是准确,恰到把整个车牌截取的正正好。在这种状况下,只要外接矩形知足这些条件,就能够判断为特殊字符的矩形。
这个方法思路很简单,实际中应用效果也不错,所以也是EasyPR目前采用的方法。
图16 获取特殊字符的位置
如下是特殊字符判断的代码:
//! 找出指示城市的字符的Rect,例如苏A7003X,就是"A"的位置 int CCharsSegment::GetSpecificRect(const vector<Rect>& vecRect) { vector<int> xpositions; int maxHeight = 0; int maxWidth = 0; for (size_t i = 0; i < vecRect.size(); i++) { xpositions.push_back(vecRect[i].x); if (vecRect[i].height > maxHeight) { maxHeight = vecRect[i].height; } if (vecRect[i].width > maxWidth) { maxWidth = vecRect[i].width; } } int specIndex = 0; for (size_t i = 0; i < vecRect.size(); i++) { Rect mr = vecRect[i]; int midx = mr.x + mr.width / 2; //若是一个字符有必定的大小,而且在整个车牌的1/7到2/7之间,则是咱们要找的特殊字符 //当前字符和下个字符的距离在必定的范围内 if ((mr.width > maxWidth * 0.8 || mr.height > maxHeight * 0.8) && (midx < int(m_theMatWidth / 7) * 2 && midx > int(m_theMatWidth / 7) * 1)) { specIndex = i; } } return specIndex; }
以上就是EasyPR能处理中文车牌的主要缘由。原先的taotao1233的代码中没法处理中文的缘由就是没有这样一步预处理。其实这是一个很简单的思想,但在以前并无被实现。EasyPR里实现了这个思路,同时发现,这个方法效果出奇的好。基本能够应对全部的状况。因此说,这个方法能够说是一个简单,有效的处理中文车牌的方法。
四.其余一些细节
1.颜色判断
在进行二值化前,须要进行一次颜色判断,这是由于对于蓝色和黄色车牌而言,使用的二值化策略必须不一样。
图17 蓝色与黄色车牌的不一样
对于蓝色车牌而言,使用的参数为CV_THRESH_BINARY。
而对于黄色车牌而言,使用的参数为CV_THRESH_BINARY_INV。
假设黄色车牌使用了CV_THRESH_BINARY做为参数,则会发生以下图同样的二值化结果,其中字符部分变成了黑色,而背景则是白色(同理,蓝色车牌使用CV_THRESH_BINARY_INV也是同样的效果)。
在这种不正确的参数带来的二值化状况下,取轮廓操做将没法按照预期的行为进行处理。所以,必须使用正确的二值化参数。
图18 不正确参数的二值化效果
在颜色判断时,有一个小技巧,就是先把四周的“边”截取后再进行颜色的判断,这样能够消除车牌定位时一些多余的四周的干扰。
代码以下:
1 Mat tmpMat = input(Rect_<double>(w * 0.1, h * 0.1, w * 0.8, h * 0.8)); 2 3 // 判断车牌颜色以此确认threshold方法 4 Color plateType = getPlateType(tmpMat, true);
颜色判断方法的代码以下:
1 // getPlateType 2 //判断车牌的类型 3 Color getPlateType(const Mat& src, const bool adaptive_minsv) { 4 float max_percent = 0; 5 Color max_color = UNKNOWN; 6 7 float blue_percent = 0; 8 float yellow_percent = 0; 9 float white_percent = 0; 10 11 if (plateColorJudge(src, BLUE, adaptive_minsv, blue_percent) == true) { 12 // cout << "BLUE" << endl; 13 return BLUE; 14 } else if (plateColorJudge(src, YELLOW, adaptive_minsv, yellow_percent) == 15 true) { 16 // cout << "YELLOW" << endl; 17 return YELLOW; 18 } else if (plateColorJudge(src, WHITE, adaptive_minsv, white_percent) == 19 true) { 20 // cout << "WHITE" << endl; 21 return WHITE; 22 } else { 23 // cout << "OTHER" << endl; 24 25 // 若是任意一者都不大于阈值,则取值最大者 26 max_percent = blue_percent > yellow_percent ? blue_percent : yellow_percent; 27 max_color = blue_percent > yellow_percent ? BLUE : YELLOW; 28 29 max_color = max_percent > white_percent ? max_color : WHITE; 30 return max_color; 31 } 32 }
2.排除缝隙
在得到中文字符图块之后,下面一步就是把剩下的图块获取了。不过因为中文车牌通常只有7个字符,因此能够把后面的图块从左到右排序,依次选择6个便可。一些会被误判为“I”的缝隙能够经过这种方法排除出去。
例以下图中,最右边的一个缝隙会被误识别为"1"。可是假若从左到右依次选择的话,这个缝隙并不会被选入候选集合中,由于它已是“第八个”字符了。
图19 最右边会被误判为"1"的缝隙
排序与依次选择的代码以下:
1 //! 这个函数作两个事情 2 // 1.把特殊字符Rect左边的所有Rect去掉,后面再重建中文字符的位置。 3 // 2.从特殊字符Rect开始,依次选择6个Rect,多余的舍去。 4 int CCharsSegment::RebuildRect(const vector<Rect>& vecRect, 5 vector<Rect>& outRect, int specIndex) { 6 int count = 6; 7 for (size_t i = specIndex; i < vecRect.size() && count; ++i, --count) { 8 outRect.push_back(vecRect[i]); 9 } 10 11 return 0; 12 }
3.去除柳钉
有些中国的车牌中有一个很是妨碍识别的东西,那就是柳钉。假若对一副含有柳钉的图进行二值化,极有可能会出现下图的结果。一些字符图块(下图的"9"和"1")经过柳钉的缘由联系到了一体,那样的话就没法经过取轮廓操做来分割了。
图20 柳钉的影响
所以在二值化以后,还须要一个去除柳钉的操做。
去除柳钉的思想也并不复杂,就是依次扫描每行,判断跳变次数。车牌字符所在的行的跳变次数是不少的,而柳钉所在的行就会偏少。所以当发现某行跳变次数较少,则能够把该行的全部像素值赋值为0,这样就会大幅度消除柳钉的影响了。
下图就是去除柳钉后的效果。
图21 去除柳钉后的效果
去除柳钉函数的代码以下:
1 //去除车牌上方的钮钉 2 //计算每行元素的阶跃数,若是小于X认为是柳丁,将此行所有填0(涂黑) 3 // X的推荐值为,可根据实际调整 4 bool clearLiuDing(Mat& img) { 5 vector<float> fJump; 6 int whiteCount = 0; 7 const int x = 7; 8 Mat jump = Mat::zeros(1, img.rows, CV_32F); 9 for (int i = 0; i < img.rows; i++) { 10 int jumpCount = 0; 11 12 for (int j = 0; j < img.cols - 1; j++) { 13 if (img.at<char>(i, j) != img.at<char>(i, j + 1)) jumpCount++; 14 15 if (img.at<uchar>(i, j) == 255) { 16 whiteCount++; 17 } 18 } 19 20 jump.at<float>(i) = (float)jumpCount; 21 } 22 23 int iCount = 0; 24 for (int i = 0; i < img.rows; i++) { 25 fJump.push_back(jump.at<float>(i)); 26 if (jump.at<float>(i) >= 16 && jump.at<float>(i) <= 45) { 27 //车牌字符知足必定跳变条件 28 iCount++; 29 } 30 } 31 32 ////这样的不是车牌 33 if (iCount * 1.0 / img.rows <= 0.40) { 34 //知足条件的跳变的行数也要在必定的阈值内 35 return false; 36 } 37 //不知足车牌的条件 38 if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 || 39 whiteCount * 1.0 / (img.rows * img.cols) > 0.50) { 40 return false; 41 } 42 43 for (int i = 0; i < img.rows; i++) { 44 if (jump.at<float>(i) <= x) { 45 for (int j = 0; j < img.cols; j++) { 46 img.at<char>(i, j) = 0; 47 } 48 } 49 } 50 return true; 51 }
五.总结
最后回顾一下总体的处理流程,首先是对车牌图像进行灰度化,而后根据车牌的不一样颜色来进行不一样的二值化处理。二值化完后首先去除柳钉,而后进行取轮廓操做。
取轮廓操做之后,在全部的轮廓中根据先验知识,找到表明城市的字符,也就是“苏A”中“A”的位置,根据“A”的位置来反推“苏”的位置。
最后将找到的这些轮廓依次排序,从左到右依次选择6个,和第一个的中文字符组成7个字符的图块数组,输入到下一步字符识别模块中进行处理。
整个字符分割流程就到此结束了,仍是比较简单的。其中的中文字符位置的肯定使用了“先验知识”这种方法。这种方法在面对固定已知场景中是较好的方法,可是面对特殊状况时就可能会有不太好的效果,所以要根据具体状况来权衡。
六.将来展望
本篇字符分割流程就到此结束。当下,EasyPR1.3 版也发布了,对总体架构以及处理效率都有所提高,能够下载试用。
将来的博客会按照每2个月一篇的速度诞生,下篇博客的内容是”字符识别与人工神经网络”。
版权说明:
本文中的全部文字,图片,代码的版权都是属于做者和博客园共同全部。欢迎转载,可是务必注明做者与出处。任何未经容许的剽窃以及爬虫抓取都属于侵权,做者和博客园保留全部权利。