从零使用OpenCV快速实现简单车牌识别系统 这篇文章献给所有第一次听说车牌识别ANPR但需要短时间实现的苦逼同学们。
最近的小学期实训做的是一个车牌识别系统,说实话真不知道学校怎么想的,虽然说图像处理也算的上是数字媒体很重要的一块分支了,但咱这几年学的全是图形渲染啊。图形与图像虽然只差了一个字,但内容真是差了十万八千里了(当然这话是夸张了,事实上在使用shader进行特效渲染的最后一步往往都是在做图像处理,如Bloom, Outline, Field Depth等,但这些也只是用到了图像处理中很基础的一部分)。 小学期不到10天的时间要从零搞出个车牌识别系统,更别说我们所有组员全都有实习,老师大撒把,发了需求直接不管。当时本来还觉得这种应用广泛的东西网上肯定有现成的例子,随便改改就好,结果搜了半天,最令我崩溃的一句话就是:“这东西你要真做出来了就卖钱去吧”。擦,算了,求人不如求自己,最终在OpenCV的帮助下我看了两天资料,写了两天程序,居然就实现了,看到从原始图片中抠出车牌,再从车牌中抠出数字,再匹配出结果(这步是我同学做的),我自己都觉得神奇啊!虽然肯定是买不了钱,但还是很激动啊~~ 废话不多说了,以后和我一样做这个实训项目的同学们可有福了,接下来就详细讲讲如何简单实现ANPR(Automatic Number Plate Recognition)吧。
配置环境
我使用的是OpenCV 2.3.1和VS2010,下载与配置方法在opencv的中国官网www.上都有详细介绍。像这种开源库最麻烦的就是环境配置了,什么makefile这种东西我看着就头大,当然对于我这种菜鸟人家提供了CMake来帮你进行傻瓜式的一键配置,不过这对于我来说还是麻烦,毕竟还要再装个程序。幸运的是我下载的是SuperPack版本,也就是说在opencv/build目录下已经有人家编译好的全部语言、开发平台的lib, dll以及头文件。虽然很大,但是下下来就能用^_^,咱的追求就是简单,更简单! 建立工程后要做的就是在工程属性的C++目录中将相应的include文件夹,lib文件夹配置进去,另外还要链接上你需要使用的库。 看网上一片教程说在属性——链接器——输入中配置附加依赖项:opencv_calib3d231d.lib; opencv_contrib231d.lib; opencv_core231d.lib;opencv_features2d231d.lib; opencv_flann231d.lib; opencv_gpu231d.lib;opencv_highgui231d.lib; opencv_imgproc231d.lib; opencv_legacy231d.lib;opencv_ml231d.lib; opencv_objdetect231d.lib; opencv_ts231d.lib;opencv_video231d.lib。 NND,这么多哪儿记得住啊,下次新建一个工程还得网上找这篇文章拷贝粘贴么?当然不用,只少在车牌识别系统中,我们所需要的只有三个库,而且在VS中我们可以使用预编译指令连接这些库,这样在你将工程拷贝给同学的时候就不用再担心环境配置的问题了。 #pragma comment(lib, “opencv_core231d.lib”) #pragma comment(lib, “opencv_imgproc231d.lib”) #pragma comment(lib, “opencv_highgui231d.lib”) core是opencv的核心库,一些主要的数据结构都在这里定义,imgproc顾名思义包含了主要的图像处理函数,highgui是一个简单的显示框架,帮助快速创建窗口显示图像等,就好像opengl中的glut。如果使用MFC框架进行显示的话需要额外添加一个类CvvImage,具体情况网上随便一搜就有,不废话了。
基本算法
OpenCV只是一个提供基本图像处理方法的工具库,具体应用到车牌识别系统中,需要综合实用图像处理方法,可不能指望OpenCV中直接有个函数帮你把大部分事儿都做了(我一开始就这么期望的…)。 大致的算法网上可以找到很多论文,其中我主要参考了以下两篇: http://blog.csdn.net/heihei723/article/details/728046 http://www.doc88.com/p-677404951164.html 这俩篇都通俗易懂,算法比较简单实用,适合初学者上手。从整体看,车牌号识别主要分三步走:1.提取车牌 2.提取字符 3.字符匹配识别。下面就一一来介绍一下具体步骤,及其使用到的opencv函数,函数的具体使用方法同学们就自己查查吧,懒得赘述了。 车牌提取
灰度化:灰度化的概念就是将一张三通道RGB颜色的图像变成单通道灰度图,为接下来的图像处理做准备。 CvCvtColor。cvCvtColor(image, grayScale, CV_BGR2GRAY);
竖向边缘检测:首先车牌上的数字都有很锐利的边缘,另外这些边缘主要都是纵向的,因此通过这一步可以去除图像上的大量无用信息。 sobel = cvCreateImage(cvGetSize(grayScale), IPL_DEPTH_16S,1); cvSobel(grayScale, sobel, 2, 0, 7); IplImage* temp = cvCreateImage(cvGetSize(sobel), IPL_DEPTH_8U,1); cvConvertScale(sobel, temp, 0.00390625, 0); 第一步首先创建一张深度为16位有符号(-65536~65535)的的图像区域保持处理结果。 第二步进行x方向的sobel检测,算子的大小(最后一个参数)我选择了7*7,完全时瞎选的,可以结合实际效果进行调整。 最后将图像格式转换回8位深度已进行下一步处理
自适应二值化处理:二值化的处理强化了锐利的边缘,进一步去除图像中无用的信息,使用过程中主要注意阀值的选取,我为了省事儿使用了opencv自带的自适应的的二值化处理,缺点是无用信息有点多,但车牌数字信息也会更为凸显。 cvThreshold(sobel, threshold, 0, 255, CV_THRESH_BINARY| CV_THRESH_OTSU); 最后的参数CV_THRESH_OTSU就是使用自适应算法,千万不要看学习OpenCV那本书上介绍的cvAdaptiveThreshold方法,那完全时披着二值化皮的边缘检测函数,坑死人!
形态学(膨胀腐蚀)处理:膨胀与腐蚀的处理效果就如其名字一样,我们通过膨胀连接相近的图像区域,通过腐蚀去除孤立细小的色块。通过这一步,我们希望将所有的车牌号字符连通起来,这样为我们接下来通过轮廓识别来选取车牌区域做准备。由于字符都是横向排列的,因此要连通这些字符我们只需进行横向的膨胀即可。 //自定义1*3的核进行X方向的膨胀腐蚀 IplConvKernel* kernal = cvCreateStructuringElementEx(3,1, 1, 0, CV_SHAPE_RECT); cvDilate(threshold, erode_dilate, kernal, 2);//X方向膨胀连通数字 vErode(erode_dilate, erode_dilate, kernal, 4);//X方向腐蚀去除碎片 cvDilate(erode_dilate, erode_dilate, kernal, 2);//X方向膨胀回复形态 //自定义3*1的核进行Y方向的膨胀腐蚀 kernal = cvCreateStructuringElementEx(1, 3, 0, 1, CV_SHAPE_RECT); cvErode(erode_dilate, erode_dilate, kernal, 1);// Y方向腐蚀去除碎片 cvDilate(erode_dilate, erode_dilate, kernal, 2);//回复形态 进行膨胀腐蚀操作需要注意的是要一次到位,如果一次膨胀没有连通到位,那么再次腐蚀将会将图像回复原装,因此我首先做了2次迭代的膨胀,保证数字区域能连通起来,再进行4次迭代腐蚀,尽可能多的去除小块碎片,随后2次迭代膨胀,保证膨胀次数与腐蚀次数相同,以回复连通区域形态大小。
矩形轮廓查找与筛选:经过上一步操作,理论上来说车牌上的字符连通成一个矩形区域,通过轮廓查找我们可以定位该区域。当然,更为准确的说,经过上面的操作,我们将原始图片中在X方向排列紧密的纵向边缘区域连通成了一个矩形区域,出了车牌符合这个特点外,其他一些部分如路间栏杆,车头的纹理等同样符合。因此我们会找到很多这样的区域,这就需要我们进一步根据一些关于车牌特点的先验知识对这些矩形进行进一步筛选。最终,定位车牌所在的矩形区。 首先来看轮廓检测: IplImage* copy = cvCloneImage(img); CvMemStorage* storage = cvCreateMemStorage(); CvSeq* contours; cvFindContours(copy, storage, &contours);
while(contours != nullptr) { rects.push_back(cvBoundingRect(contours));//list<CvRect> rects contours= contours->h_next; } 使用list存储全部查找到的CvRect,因为接下来我们要频繁的对容器中的元素进行插入删除操作。 矩形的筛选算法完全就要自己写啦~这一步筛选效果的好坏直接决定了整个一套识别算法是否能得到一个好的结果。在之后对于算法的调整也主要是集中于这一部分,调整一些先验知识的参数。我设计的筛选算法主要涉及这几个部分:1.大小(图片大小的5%以下) 2.位置(图片高度的40%~90%之间) 3.X方向合并(有时车牌会处理成两个或多个相邻的矩形区,需要进行合并) 4.大小形状(宽高比) 最后,找到筛选之后最大的那个矩形就是了,注意下图的红色线框!
字符提取
字符提取的步骤与车牌提取的思想大致相同,但无需再做形态学处理。仍然是灰度化->二值化->轮廓检测->自定义筛选->定位。(另外提一句,二值化之后可能还要做一下反色处理,因为有黄底/白底 - 黑字这种车牌,不过因为给我们的样本中没有这类车牌,因此偷个懒就不做了^^) 相应的步骤所使用的OpenCV函数与之前别无二致,因此不做赘述,这里主要讲一下自定义的筛选和去除边框铆钉这一核心步骤。
首先,最容易导致字符提取失败的情况是亮色的边框与铆钉,他们会与车牌字母连通在一起,导致轮廓检测失败,如下图几种情况: 3与H和铆钉连接,在轮廓检测是无法提取
全部字符的底部都连住了车牌底框,轮廓检测将彻底失败
因此在进行第一次初步的轮廓检测之后,还需要进行除边框与铆钉的处理,之后再进行轮廓检测,再经过筛选,基本上妥妥的能分离出每个字符了。
去除边框:首先对图像进行Y方向的逐步裁剪,直至轮廓检测可以检测到超过5个轮廓。这一步保证不会与铆钉连接的中间几个字符可以通过轮廓检测识别出来。 去除铆钉:根据识别出来的几个字符矩形轮廓确定车牌照所有字符的上下界,根据其切割图像。(前提是认为所有字符是水平排列的,因此当车牌倾斜时需要用到更为复杂的算法) 下图显示了这一过程:
矩形筛选:同学们肯定也注意到了,上图中左右两个边框在作轮廓拾取时也可以获取到,而且很容易与数字1混淆,另外想D, 0, P, R, 8, 9, 6这种数字也会把内部的封闭区域检测出轮廓,因此同车牌提取一样,对检测出的矩形轮廓还要进一步筛选,筛选主要包括排序;高度、宽窄、面积(去除左右边框、圆点、8,p等小的内轮廓);包含检测剔除(去除0、D、非单连通的汉字这种大的内轮廓)。 中文字符定位:中文字符由于有些是非单连通的,因此在进行筛选时可能会被筛掉,即使没有被筛掉,所得的矩形也很可能并非包含了全部字体,如京。因此在字符提取的最后一步我们需要重定位中文字符的矩形截取框。算法很简单,以第二个字符的矩形轮廓为基准,大小不变,y坐标不变,根据字符间平均间距(不包括2-3字符的间距)确定x坐标即可。 double d = lastChar.x + 0.5 * lastChar.width - llChar.x- llChar.width * 0.5 - avgWidth; 注意在求字符间平均间距的时候不要简单的用a.next.x– a.x这种方法,一旦那个字符是数组1结果会错的很难看。要用字符中点间距减去字符正常宽带这种方式。获取正常宽度的算法也很简单,根据先验知识I的矩形包围框宽度一般小于25像素(我提取的车牌大小统一为400 * 100),因此只要找到一个宽度大于25的矩形框,把它的宽度即可作为平均宽度。
最后总结下来,提取字符的流程是这样的 灰度化 -> 二值化 -> 轮廓检测 –> 去除边框 ->轮廓检测 -> 大小筛选 -> 去除铆钉 –> 轮廓检测 -> 筛选 -> 重定位中文字符 字符匹配进行匹配之前你首先需要有一套标准字符模板作为参考,然后将提取出来的字符图片与每个模板进行某种算法的匹配,求得一个匹配值,最终将最佳匹配结果作为该字符图片所代表的字符。这种方法比较笨,也耗时,但是简单,好实现,如果你对这方面想有更深入研究可以在网上搜索一下OCR(Optical Character Recognition)。 那么匹配算法是什么样的呢?OpenCV提供了许多匹配函数,如直方图匹配,Hu矩匹配,轮廓匹配。如果你上网上搜字符匹配,你可能最多看到的就是模板匹配,还有什么字符匹配最适合用模板匹配之类的话。我可以在这里负责任的告诉你,以上方法统统都不行!! 至少在我的算法中切出来的字符图片用不了,尤其是神马模板匹配,可能是我的理解不对,模板检测是用来在一张大图中检测出一部分形状的,和我想要的根本就是风马牛不相及嘛;还有那个轮廓检测,听名字感觉很靠谱啊~但实际是如果你得到的字符不是标准到跟模板一模一样(在这个项目中主要是线条的粗细),效果差极了。 怎么办?显然这时我已经绕进去了,不过好在我同学还保持清醒,最终的解决办法简单的无法想象。。。。逐像素点匹配!神马?这从如此高深的算法跳到如此简单的方式我一时不能接受,但是我负责任的告诉你,如果你不考虑速度、实时性,也不觉得使用这么简单的方法掉价的话,这个方法绝对靠谱!!!(注意这个项目可没打算卖钱也没打算申请啥技术专利啊^_^;)
逐点匹配:很简单,经过处理的带匹配图片与模板图片此时大小相同,且都为2值图(非0即255)遍历全部像素点,记录两张图中值不同的像素个数,除以全部像素数量即为匹配率。显然越接近0越匹配。 近似区分:上一步的匹配结果已经非常好了,但对于一些容易混淆的字符还需要进一步区分如0和U, B和8和9等等。对于这些字符,我们在通过逐点匹配后不能马上认定,而需要进行特征检测。最简单的特征如连通区域个数(区分0 – U, 8–9),直线检测(区分B和8)等等。
改进加入图像旋转变化,这点对于识别倾斜车牌及其上的数字很重要,可用的方法再这篇文章中http://www.doc88.com/p-677404951164.html有提到,有时间可以试试效果。 二值化前加入锐化处理,采用OpenCV的自适应阀值函数会提取出大量模糊的边缘及碎片,直接影响是导致字符变粗,易于车牌边框连接,影响匹配效果。 中文字符的识别,目前纯靠人品。。。。 |
|