外围串关输半算法;您说为什么这么难学?

15559人阅读
opencv(18)
图像处理(13)
更新:,HyperLPR是一个基于Python的使用深度学习针对对中文车牌识别的实现,与开源的相比,它的检测速度和鲁棒性和多场景的适应性都要好于EasyPR。
非常详细的讲解车牌识别EasyPR
&&&&&&&&&& &&
我正在做一个开源的,Git地址为:https://github.com/liuruoze/EasyPR。
  我给它取的名字为EasyPR,也就是Easy to do Plate Recognition的意思。我开发这套系统的主要原因是因为我希望能够锻炼我在这方面的能力,包括C++技术、计算机图形学、机器学习等。我把这个项目开源的主要目的是:1.它基于开源的代码诞生,理应回归开源;2.我希望有人能够一起协助强化这套系统,包括代码、训练数据等,能够让这套系统的准确性更高,鲁棒性更强等等。
  相比于其他的车牌识别系统,EasyPR有如下特点:
它基于openCV这个开源库,这意味着所有它的代码都可以轻易的获取。 它能够识别中文,例如车牌为苏EUK722的图片,它可以准确地输出std:string类型的&苏EUK722&的结果。 它的识别率较高。目前情况下,字符识别已经可以达到90%以上的精度。
  系统还提供全套的训练数据提供(包括车牌检测的近500个车牌和字符识别的4000多个字符)。所有全部都可以在Github的项目地址上直接下载到。
那么,EasyPR是如何产生的呢?我简单介绍一下它的诞生过程:
  首先,在5月份左右时我考虑要做一个车牌识别系统。这个车牌系统中所有的代码都应该是开源的,不能基于任何黑盒技术。这主要起源于我想锻炼自己的C++和计算机视觉的水平。
  我在网上开始搜索了资料。由于计算机视觉中很多的算法我都是使用openCV,而且openCV发展非常良好,因此我查找的项目必须得是基于OpenCV技术的。于是我在CSDN的博客上找了一篇。
  文章的作者taotao1233在这两篇博客中以半学习笔记半开发讲解的方式说明了一个车牌识别系统的全部开发过程。非常感谢他的这些博客,借助于这些资料,我着手开始了开发。当时的想法非常朴素,就是想看看按照这些资料,能否真的实现一个车牌识别的系统。关于车牌照片数据的问题,幸运的很,我正在开发的一个项目中有大量的照片,因此数据不是问题。
  令人高兴的是,系统确实能够工作,但是让人沮丧的,似乎也就“仅仅”能够工作而已。在车牌检测这个环节中正确性已经惨不忍睹。
  这个事情给了我一拨不小的冷水,本来我以为很快的开发进度看来是乐观过头了。于是我决定沉下心来,仔细研究他的系统实现的每一个过程,结合OpenCV的官网教程与API资料,我发现他的实现系统中有很多并不适合我目前在做的场景。
  我手里的数据大部分是高速上的图像抓拍数据,其中每个车牌都偏小,而且模糊度较差。直接使用他们的方法,正确率低到了可怕的地步。于是我开始尝试利用openCv中的一些函数与功能,替代,增加,调优等等方法,不断的优化。这个过程很漫长,但是也有很多的积累。我逐渐发现,并且了解他系统中每一个步骤的目的,原理以及如果修改可以进行优化的方法。
  在最终实现的代码中,我的代码已经跟他的原始代码有很多的不一样了,但是成功率大幅度上升,而且车牌的正确检测率不断被优化。在系列文章的后面,我会逐一分享这些优化的过程与心得。
  最终我实现的系统与他的系统有以下几点不同:
他的系统代码基本上完全参照了《Mastering OpenCV with Practical Computer Vision Projects》这本书的代码,而这本书的代码是专门为西班牙车牌所开发的,因此不适合中文的环境。他的系统的代码大部分是原始代码的搬迁,并没有做到优化与改进的地步。而我的系统中对原来的识别过程,做了很多优化步骤。 车牌识别中核心的机器学习算法的模型,他直接使用了原书提供的,而我这两个过程的模型是自己生成,而且模型也做了测试,作为开源系统的一部分也提供了出来。
  尽管我和他的系统有这么多的不同,但是我们在根本的系统结构上是一致的。应该说,我们都是参照了“Mastering OpenCV”这本数的处理结构。在这点上,我并没有所“创新”,事实上,结果也证明了“Mastering OpenCV”上的车牌识别的处理逻辑,是一个实际有效的最佳处理流程。
  “Mastering OpenCV”,包括我们的系统,都是把车牌识别划分为了两个过程:即车牌检测(Plate Detection)和字符识别(Chars Recognition)两个过程。可能有些书籍或论文上不是这样叫的,但是我觉得,这样的叫法更容易理解,也不容易搞混。
车牌检测(Plate Detection):对一个包含车牌的图像进行分析,最终截取出只包含车牌的一个图块。这个步骤的主要目的是降低了在车牌识别过程中的计算量。如果直接对原始的图像进行车牌识别,会非常的慢,因此需要检测的过程。在本系统中,我们使用SVM(支持向量机)这个机器学习算法去判别截取的图块是否是真的“车牌”。字符识别(Chars Recognition):有的书上也叫Plate Recognition,我为了与整个系统的名称做区分,所以改为此名字。这个步骤的主要目的就是从上一个车牌检测步骤中获取到的车牌图像,进行光学字符识别(OCR)这个过程。其中用到的机器学习算法是著名的人工神经网络(ANN)中的多层感知机(MLP)模型。最近一段时间非常火的“深度学习”其实就是多隐层的人工神经网络,与其有非常紧密的联系。通过了解光学字符识别(OCR)这个过程,也可以知晓深度学习所基于的人工神经网路技术的一些内容。
  下图是一个完整的EasyPR的处理流程:
本开源项目的目标客户群有三类:
需要开发一个车牌识别系统的(开发者)。 需要车牌系统去识别车牌的(用户)。 急于做毕业设计的(学生)。
  第一类客户是本项目的主要使用者,因此项目特地被精心划分为了6个模块,以供开发者按需选择。
  第二类客户可能会有部分,EasyPR有一个同级项目EasyPR_Dll,可以DLL方式嵌入到其他的程序中,另外还有个一个同级项目EasyPR_Win,基于WTL开发的界面程序,可以简化与帮助车牌识别的结果比对过程。
  对于第三类客户,可以这么说,有完整的全套代码和详细的说明,我相信你们可以稍作修改就可以通过设计大考。
推荐你使用EasyPR有以下几点理由:
这里面的代码都是作者亲自优化过的,你可以在上面做修改,做优化,甚至一起协作开发,一些处理车牌的细节方法你应该是感兴趣的。 如果你对代码不感兴趣,那么经过作者精心训练的模型,包括SVM和ANN的模型,可以帮助你提升或验证你程序的正确率。 如果你对模型也不感兴趣,那么成百上千经过作者亲自挑选的训练数据生成的文件,你应该感兴趣。作者花了大量的时间处理这些训练数据与调整,现在直接提供给你,可以大幅度减轻很多人缺少数据的难题。
  有兴趣的同志可以留言或发Email: 或者直接在Git上发起pull requet,都可以,未来我会在cnblogs上发布更多的关于系统的介绍,包括编码过程,训练心得。
在中作者已经简单的介绍了EasyPR,现在在本文档中详细的介绍EasyPR的开发过程。
  正如淘宝诞生于一个购买来的LAMP系统,EasyPR也有它诞生的原型,起源于CSDN的taotao1233的一个,博主以读书笔记的形式记述了通过阅读“Mastering OpenCV”这本书完成的一个车牌系统的雏形。
  这个雏形有几个特点:1.将车牌系统划分为了两个过程,即车牌检测和字符识别。2.整个系统是针对西班牙的车牌开发的,与中文车牌不同。3.系统的训练模型来自于原书。作者基于这个系统,诞生了开发一个适用于中文的,且适合与协作开发的开源车牌系统的想法,也就是EasyPR。
& 当然了,现在车牌系统满大街都是,随便上下百度首页都是大量的广告,一些甚至宣称自己实现了99%的识别率。那么,作者为什么还要开发这个系统呢?这主要是基于时势与机遇的原因。
众所皆知,现在是大数据的时代。那么,什么是大数据?可能有些人认为这个只是一个概念或着炒作。但是大数据确是实实在在有着基础理论与科学研究背景的一门技术,其中包含着分布式计算、内存计算、机器学习、计算机视觉、语音识别、自然语言处理等众多计算机界崭新的技术,而且是这些技术综合的产物。事实上,大数据的“大”包含着4个特征,即4V理念,包括Volume(体量)、Varity(多样性)、Velocity(速度)、Value(价值)。
  见下图的说明:
图1 大数据技术的4V特征
  综上,大数据技术不仅包含数据量的大,也包含处理数据的复杂,和处理数据的速度,以及数据中蕴含的价值。而车牌识别这个系统,虽然传统,古老,却是包含了所有这四个特侦的一个大数据技术的缩影。
  在车牌识别中,你需要处理的数据是图像中海量的像素单元;你处理的数据不再是传统的结构化数据,而是图像这种复杂的数据;如果不能在很短的时间内识别出车牌,那么系统就缺少意义;虽然一副图像中有很多的信息,但可能仅仅只有那一小块的信息(车牌)以及车身的颜色是你关心,而且这些信息都蕴含着巨大的价值。也就是说,车牌识别系统事实上就是现在火热的大数据技术在某个领域的一个聚焦,通过了解车牌识别系统,可以很好的帮助你理解大数据技术的内涵,也能清楚的认识到大数据的价值。
  很神奇吧,也许你觉得车牌识别系统很低端,这不是随便大街上都有的么,而你又认为大数据技术很高端,似乎高大上的感觉。其实两者本质上是一样的。另外对于觉得大数据技术是虚幻的炒作念头的同学,你们也可以了解一下车牌识别系统,就能知道大数据落在实地,事实上已经不知不觉进入我们的生活很长时间了,像一些其他的如抢票系统,语音助手等,都是大数据技术的真真切切的体现。所谓再虚幻的概念落到实处,就成了下里巴人,应该就是这个意思。所以对于炒概念要有所警觉,但是不能因此排除一切,要了解具体的技术内涵,才能更好的利用技术为我们服务。
  除了帮忙我们更好的理解大数据技术,使我们跟的上时代,开发一个车牌系统还有其他原因。
  那就是、现在的车牌系统,仍然还有许多待解决的挑战。这个可能很多同学有疑问,你别骗我,百度上我随便一搜都是99%,只要多少多少元,就可以99%。但是事实上,车牌识别系统业界一直都没有一个成熟的百分百适用的方案。一些90%以上的车牌识别系统都是跟高清摄像机做了集成,由摄像头传入的高分辨率图片进入识别系统,可以达到较高的识别率。但是如果图像分辨率一旦下来,或者图里的车牌脏了的话,那么很遗憾,识别率远远不如我们的肉眼。也就是说,距离真正的智能的车牌识别系统,目前已有的系统还有许多挑战。什么时候能够达到人眼的精度以及识别速率,估计那时候才算是完整成熟的。
  那么,有同学问,就没有办法进一步优化了么。答案是有的,这个就需要谈到目前火热的深度学习与计算机视觉技术,使用多隐层的深度神经网络也许能够解决这个问题。但是目前EasyPR并没有采用这种技术,或许以后会采用。但是这个方向是有的。也就是说,通过研究车牌识别系统,也许会让你一领略当今人工智能与计算机视觉技术最尖端的研究方向,即深度学习技术。怎么样,听了是不是很心动?最后扯一下,前端时间非常火热Google大脑技术和百度深度学习研究院,都是跟深度学习相关的。
  下图是一个深度学习(右)与传统技术(左)的对比,可以看出深度学习对于数据的分类能力的优势。
图2 深度学习(右)与PCA技术(左)的对比
  总结一下:开发一个车牌识别系统可以让你了解最新的时势---大数据的内涵,同时,也有机遇让你了解最新的人工智能技术---深度学习。因此,不要轻易的小看这门技术中蕴含的价值。
  好,谈价值就说这么多。现在,我简单的介绍一下EasyPR的具体过程。
  在上一篇文档中,我们了解到EasyPR包括两个部分,但实际上为了更好进行模块化开发,EasyPR被划分成了六个模块,其中每个模块的准确率与速度都影响着整个系统。
  具体说来,EasyPR中PlateDetect与CharsRecognize各包括三个模块。
  PlateDetect包括的是车牌定位,SVM训练,车牌判断三个过程,见下图。
图3 PlateDetect过程详解&
  通过PlateDetect过程我们获得了许多可能是车牌的图块,将这些图块进行手工分类,聚集一定数量后,放入SVM模型中训练,得到SVM的一个判断模型,在实际的车牌过程中,我们再把所有可能是车牌的图块输入SVM判断模型,通过SVM模型自动的选择出实际上真正是车牌的图块。
  PlateDetect过程结束后,我们获得一个图片中我们真正关心的部分--车牌。那么下一步该如何处理呢。下一步就是根据这个车牌图片,生成一个车牌号字符串的过程,也就是CharsRecognisze的过程。
  CharsRecognise包括的是字符分割,ANN训练,字符识别三个过程,具体见下图。
图4 CharsRecognise过程详解
  在CharsRecognise过程中,一副车牌图块首先会进行灰度化,二值化,然后使用一系列算法获取到车牌的每个字符的分割图块。获得海量的这些字符图块后,进行手工分类(这个步骤非常耗时间,后面会介绍如何加速这个处理的方法),然后喂入神经网络(ANN)的MLP模型中,进行训练。在实际的车牌识别过程中,将得到7个字符图块放入训练好的神经网络模型,通过模型来预测每个图块所表示的具体字符,例如图片中就输出了“苏EUK722”,(这个车牌只是示例,切勿以为这个车牌有什么特定选取目标。车主既不是作者,也不是什么深仇大恨,仅仅为学术说明选择而已)。
  至此一个完整的车牌识别过程就结束了,但是在每一步的处理过程中,有许多的优化方法和处理策略。尤其是车牌定位和字符分割这两块,非常重要,它们不仅生成实际数据,还生成训练数据,因此会直接影响到模型的准确性,以及模型判断的最终结果。这两部分会是作者重点介绍的模块,至于SVM模型与ANN模型,由于使用的是OpenCV提供的类,因此可以直接看openCV的源码或者机器学习介绍的书,来了解训练与判断过程。
  好了,本期就介绍这么多。下面的篇章中作者会重点介绍其中每个模块的开发过程与内容,但是时间不定,可能几个星期发一篇吧。
  最后,祝大家国庆快乐,阖家幸福!
这篇文章是一个系列中的第三篇。前两篇的地址贴下:、。我撰写这系列文章的目的是:1、普及车牌识别中相关的技术与知识点;2、帮助开发者了解EasyPR的实现细节;3、增进沟通。
  EasyPR的项目地址在这:。要想运行EasyPR的程序,首先必须配置好openCV,具体可以参照这篇。
  在前两篇文章中,我们已经初步了解了EasyPR的大概内容,在本篇内容中我们开始深入EasyRP的程序细节。了解EasyPR是如何一步一步实现一个车牌的识别过程的。根据EasyPR的结构,我们把它分为六个部分,前三个部分统称为“Plate Detect”过程。主要目的是在一副图片中发现仅包含车牌的图块,以此提高整体识别的准确率与速度。这个过程非常重要,如果这步失败了,后面的字符识别过程就别想了。而“Plate Detect”过程中的三个部分又分别称之为“Plate Locate” ,“SVM train”,“Plate
judge”,其中最重要的部分是第一步“Plate Locate”过程。本篇文章中就是主要介绍“Plate Locate”过程,并且回答以下三个问题:
  1.此过程的作用是什么,为什么重要?
  2.此过程是如何实现车牌定位这个功能的?
  3.此过程中的细节是什么,如何进行调优?
1.“Plate Locate”的作用与重要性
  在说明“Plate Locate”的作用与重要性之前,请看下面这两幅图片。
图1 两幅包含车牌的不同形式图片
  左边的图片是作者训练的图片(作者大部分的训练与测试都是基于此类交通抓拍图片),右边的图片则是在百度图片中“车牌”获得(这个图片也可以称之为生活照片)。右边图片的问题是一个网友评论时问的。他说EasyPR在处理百度图片时的识别率不高。确实如此,由于工业与生活应用目的不同,拍摄的车牌的大小,角度,色泽,清晰度不一样。而对图像处理技术而言,一些算法对于图像的形式以及结构都有一定的要求或者假设。因此在一个场景下适应的算法并不适用其他场景。目前EasyPR所有的功能都是基于交通抓拍场景的图片制作的,因此也就导致了其无法处理生活场景中这些车牌照片。
  那么是否可以用一致的“Plate Locate”过程中去处理它?答案是也许可以,但是很难,而且最后即便处理成功,效率也许也不尽如人意。我的推荐是:对于不同的场景要做不同的适配。尽管“Plate Locate”过程无法处理生活照片的定位,但是在后面的字符识别过程中两者是通用的。可以对EasyPR的“Plate Locate”做改造,同时仍然使用整体架构,这样或许可以处理。
  有一点事实值得了解到是,在生产环境中,你所面对的图片形式是固定的,例如左边的图片。你可以根据特定的图片形式来调优你的车牌程序,使你的程序对这类图片足够健壮,效率也够高。在上线以后,也有很好的效果。但当图片形式调整时,就必须要调整你的算法了。在“Plate Locate”过程中,有一些参数可以调整。如果通过调整这些参数就可以使程序良好工作,那最好不过。当这些参数也不能够满足需求时,就需要完全修改 EasyPR的实现代码,因此需要开发者了解EasyPR是如何实现plateLocate这一过程的。
  在EasyPR中,“Plate Locate”过程被封装成了一个“CPlateLocate”类,通过“plate_locate.h”声明,在“plate_locate.cpp”中实现。
  CPlateLocate包含三个方法以及数个变量。方法提供了车牌定位的主要功能,变量则提供了可定制的参数,有些参数对于车牌定位的效果有非常明显的影响,例如高斯模糊半径、Sobel算子的水平与垂直方向权值、闭操作的矩形宽度。CPlateLocate类的声明如下:
class CPlateLocate
CPlateLocate();
//! 车牌定位
int plateLocate(Mat, vector&Mat&& );
//! 车牌的尺寸验证
bool verifySizes(RotatedRect mr);
//! 结果车牌显示
Mat showResultMat(Mat src, Size rect_size, Point2f center);
//! 设置与读取变量
protected:
//! 高斯模糊所用变量
int m_GaussianBlurS
//! 连接操作所用变量
int m_MorphSizeW
int m_MorphSizeH
//! verifySize所用变量
int m_verifyM
int m_verifyM
//! 角度判断所用变量
//! 是否开启调试模式,0关闭,非0开启
  注意,所有EasyPR中的类都声明在命名空间easypr内,这里没有列出。CPlateLocate中最核心的方法是plateLocate方法。它的声明如下:
//! 车牌定位
int plateLocate(Mat, vector&Mat&& );
 方法有两个参数,第一个参数代表输入的源图像,第二个参数是输出数组,代表所有检索到的车牌图块。返回值为int型,0代表成功,其他代表失败。plateLocate内部是如何实现的,让我们再深入下看看。
2.“Plate Locate”的实现过程
  plateLocate过程基本参考了taotao1233的的处理流程,但略有不同。
  plateLocate的总体识别思路是:如果我们的车牌没有大的旋转或变形,那么其中必然包括很多垂直边缘(这些垂直边缘往往缘由车牌中的字符),如果能够找到一个包含很多垂直边缘的矩形块,那么有很大的可能性它就是车牌。
  依照这个思路我们可以设计一个车牌定位的流程。设计好后,再根据实际效果进行调优。下面的流程是经过多次调整与尝试后得出的,包含了数月来作者针对测试图片集的一个最佳过程(这个流程并不一定适用所有情况)。plateLocate的实现代码在这里不贴了,Git上有所有。plateLocate主要处理流程图如下:
图2 plateLocate流程图
  下面会一步一步参照上面的流程图,给出每个步骤的中间临时图片。这些图片可以在1.01版的CPlateLocate中设置如下代码开启调试模式。
plate.setDebug(1);
  临时图片会生成在tmp文件夹下。对多个车牌图片处理的结果仅会保留最后一个车牌图片的临时图片。
  1、原始图片。
  2、经过高斯模糊后的图片。经过这步处理,可以看出图像变的模糊了。这步的作用是为接下来的Sobel算子去除干扰的噪声。
  3、将图像进行灰度化。这个步骤是一个分水岭,意味着后面的所有操作都不能基于色彩信息了。此步骤是利是弊,后面再做分析。
  4、对图像进行Sobel运算,得到的是图像的一阶水平方向导数。这步过后,车牌被明显的区分出来。
  5、对图像进行二值化。将灰度图像(每个像素点有256个取值可能)转化为二值图像(每个像素点仅有1和0两个取值可能)。
  6、使用闭操作。对图像进行闭操作以后,可以看到车牌区域被连接成一个矩形装的区域。
  7、求轮廓。求出图中所有的轮廓。这个算法会把全图的轮廓都计算出来,因此要进行筛选。
  8、筛选。对轮廓求最小外接矩形,然后验证,不满足条件的淘汰。经过这步,仅仅只有六个黄色边框的矩形通过了筛选。
  8、角度判断与旋转。把倾斜角度大于阈值(如正负30度)的矩形舍弃。左边第一、二、四个矩形被舍弃了。余下的矩形进行微小的旋转,使其水平。
  10、统一尺寸。上步得到的图块尺寸是不一样的。为了进入机器学习模型,需要统一尺寸。统一尺寸的标准宽度是136,长度是36。这个标准是对千个测试车牌平均后得出的通用值。下图为最终的三个候选”车牌“图块。
  这些“车牌”有两个作用:一、积累下来作为支持向量机(SVM)模型的训练集,以此训练出一个车牌判断模型;二、在实际的车牌检测过程中,将这些候选“车牌”交由训练好的车牌判断模型进行判断。如果车牌判断模型认为这是车牌的话就进入下一步即字符识别过程,如果不是,则舍弃。
3.“Plate Locate”的深入讨论与调优策略
  好了,说了这么多,读者想必对整个“Plate Locate”过程已经有了一个完整的认识。那么让我们一步步审核一下处理流程中的每一个步骤。回答下面三个问题:这个步骤的作用是什么?省略这步或者替换这步可不可以?这个步骤中是否有参数可以调优的?通过这几个问题可以帮助我们更好的理解车牌定位功能,并且便于自己做修改、定制。
  由于篇幅关系,下面的深入讨论放在下期
在中我们了解了PlateLocate的过程中的所有步骤。在本篇文章中我们对前3个步骤,分别是高斯模糊、灰度化和Sobel算子进行分析。
一、高斯模糊
  对图像去噪,为边缘检测算法做准备。  
  在我们的车牌定位中的第一步就是高斯模糊处理。
图1 高斯模糊效果
  详细说明可以看这篇:。
  高斯模糊是非常有名的一种图像处理技术。顾名思义,其一般应用是将图像变得模糊,但同时高斯模糊也应用在图像的预处理阶段。理解高斯模糊前,先看一下平均模糊算法。平均模糊的算法非常简单。见下图,每一个像素的值都取周围所有像素(共8个)的平均值。
图2 平均模糊示意图
  在上图中,左边红色点的像素值本来是2,经过模糊后,就成了1(取周围所有像素的均值)。在平均模糊中,周围像素的权值都是一样的,都是1。如果周围像素的权值不一样,并且与二维的高斯分布的值一样,那么就叫做高斯模糊。
  在上面的模糊过程中,每个像素取的是周围一圈的平均值,也称为模糊半径为1。如果取周围三圈,则称之为半径为3。半径增大的话,会更加深模糊的效果。
  在PlateLocate中是这样调用高斯模糊的。
//高斯模糊。Size中的数字影响车牌定位的效果。
GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize),
0, 0, BORDER_DEFAULT );
  其中Size字段的参数指定了高斯模糊的半径。值是CPlateLocate类的m_GaussianBlurSize变量。由于opencv的高斯模糊仅接收奇数的半径,因此变量为偶数值会抛出异常。
  这里给出了opencv的高斯模糊的(英文,2.48以上版本)。
  高斯模糊这个过程一定是必要的么。笔者的回答是必要的,倘若我们将这句代码注释并稍作修改,重新运行一下。你会发现plateLocate过程在闭操作时就和原来发生了变化。最后结果如下。
图3 不采用高斯模糊后的结果  
  可以看出,车牌所在的矩形产生了偏斜。最后得到的候选“车牌”图块如下:
图4 不采用高斯模糊后的“车牌”图块
  如果不使用高斯模糊而直接用边缘检测算法,我们得到的候选“车牌”达到了8个!这样不仅会增加车牌判断的处理时间,还增加了判断出错的概率。由于得到的车牌图块中车牌是斜着的,如果我们的字符识别算法需要一个水平的车牌图块,那么几乎肯定我们会无法得到正确的字符识别效果。
  高斯模糊中的半径也会给结果带来明显的变化。有的图片,高斯模糊半径过高了,车牌就定位不出来。有的图片,高斯模糊半径偏低了,车牌也定位不出来。因此、高斯模糊的半径既不宜过高,也不能过低。CPlateLocate类中的值为5的静态常量DEFAULT_GAUSSIANBLUR_SIZE,标示着推荐的高斯模糊的半径。这个值是对于近千张图片经过测试后得出的综合定位率最高的一个值。在CPlateLocate类的构造函数中,m_GaussianBlurSize被赋予了DEFAULT_GAUSSIANBLUR_SIZE的值,因此,默认的高斯模糊的半径就是5。如果不是特殊情况,不需要修改它。
  在数次的实验以后,必须承认,保留高斯模糊过程与半径值为5是最佳的实践。为应对特殊需求,在CPlateLocate类中也应该提供了方法修改高斯半径的值,调用代码(假设需要一个为3的高斯模糊半径)如下:
plate.setGaussianBlurSize(3);
& 目前EasyPR的处理步骤是先进行高斯模糊,再进行灰度化。从目前的实验结果来看,基于色彩的高斯模糊过程比灰度后的高斯模糊过程更容易检测到边缘点。
二、灰度化处理
  为边缘检测算法准备灰度化环境。
灰度化的效果如下。
图5 灰度化效果
  在灰度化处理步骤中,争议最大的就是信息的损失。无疑的,原先plateLocate过程面对的图片是彩色图片,而从这一步以后,就会面对的是灰度图片。在前面,已经说过这步骤是利是弊是需要讨论的。
   无疑,对于计算机而言,色彩图像相对于灰度图像难处理多了,很多图像处理算法仅仅只适用于灰度图像,例如后面提到的Sobel算子。在这种情况下,你除 了把图片转成灰度图像再进行处理别无它法,除非重新设计算法。但另一方面,转化成灰度图像后恰恰失去了最丰富的细节。要知道,真实世界是彩色的,人类对于 事物的辨别是基于彩色的框架。甚至可以这样说,因为我们的肉眼能够区别彩色,所以我们对于事物的区分,辨别,记忆的能力就非常的强。
  车牌定位环节中去掉彩色的利弊也是同理。转换成灰度图像虽然利于使用各种专用的算法,但失去了真实世界中辨别的最重要工具---色彩的区分。举个简单的例子,人怎么在一张图片中找到车牌?非常简单,一眼望去,一个合适大小的矩形,蓝色的、或者黄色的、或者其他颜色的在另一个黑色,或者白色的大的跟车形类似的矩形中。这个过程非常直观,明显,而且可以排除模糊,色泽,不清楚等很多影响。如果使用灰度图像,就必须借助水平,垂直求导等方法。
  未来如果PlateLocate过程可以使用颜色来判断,可能会比现在的定位更清楚、准确。但这需要研究与实验过程,在EasyPR的未来版本中可能会实现。但无疑,使用色彩判断是一种趋势,因为它不仅符合人眼识别的规律,更趋近于人工智能的本质,而且它更准确,速度更快。
  在PlateLocate过程中是这样调用灰度化的。
cvtColor( src_blur, src_gray, CV_RGB2GRAY );
  这里给出了opencv的灰度化的英文,2.48以上版本)。
三.Sobel算子
  检测图像中的垂直边缘,便于区分车牌。
下图是Sobel算子的效果。
图6 Sobel效果
  如果要说哪个步骤是plateLocate中的核心与灵魂,毫无疑问是Sobel算子。没有Sobel算子,也就没有垂直边缘的检测,也就无法得到车牌的可能位置,也就没有后面的一系列的车牌判断、字符识别过程。通过Sobel算子,可以很方便的得到车牌的一个相对准确的位置,为我们的后续处理打好坚实的基础。在上面的plateLocate的执行过程中可以看到,正是通过Sobel算子,将车牌中的字符与车的背景明显区分开来,为后面的二值化与闭操作打下了基础。那么Sobel算子是如何运作的呢?
  Soble算子原理是对图像求一阶的水平与垂直方向导数,根据导数值的大小来判断是否是边缘。请详见CSDN小魏的(小心她博客里把Gx和Gy弄反了)。
  为了计算方便,Soble算子并没有真正去求导,而是使用了周边值的加权和的方法,学术上称作“卷积”。权值称为“卷积模板”。例如下图左边就是Sobel的Gx卷积模板(计算垂直边缘),中间是原图像,右边是经过卷积模板后的新图像。
图7 Sobel算子Gx示意图
  在这里演示了通过卷积模板,原始图像红色的像素点原本是5的值,经过卷积计算(- 1 * 3 - 2 * 3 - 1 * 4 + 1 * 5 + 2 * 7 + 1 * 6 = 12)后红色像素的值变成了12。
  在代码中调用Soble算子需要较多的步骤。
/// 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 );
  这里给出了opencv的Sobel的(英文,2.48以上版本)
  在调用参数中有两个常量SOBEL_X_WEIGHT与SOBEL_Y_WEIGHT代表水平方向和垂直方向的权值,默认前者是1,后者是0,代表仅仅做水平方向求导,而不做垂直方向求导。这样做的意义是,如果我们做了垂直方向求导,会检测出很多水平边缘。水平边缘多也许有利于生成更精确的轮廓,但是由于有些车子前端太多的水平边缘了,例如车头排气孔,标志等等,很多的水平边缘会误导我们的连接结果,导致我们得不到一个恰好的车牌位置。例如,我们对于测试的图做如下实验,将SOBEL_X_WEIGHT与SOBEL_Y_WEIGHT都设置为0.5(代表两者的权值相等),那么最后得到的闭操作后的结果图为
  由于Sobel算子如此重要,可以将车牌与其他区域明显区分出来,那么问题就来了,有没有与Sobel功能类似的算子可以达到一致的效果,或者有没有比Sobel效果更好的算子?
  Sobel算子求图像的一阶导数,Laplace算子则是求图像的二阶导数,在通常情况下,也能检测出边缘,不过Laplace算子的检测不分水平和垂直。下图是Laplace算子与Sobel算子的一个对比。
图8 Sobel与Laplace示意图
  可以看出,通过Laplace算子的图像包含了水平边缘和垂直边缘,根据我们刚才的描述。水平边缘对于车牌的检测一般无利反而有害。经过对近百幅图像的测试,Sobel算子的效果优于Laplace算子,因此不适宜采用Laplace算子替代Sobel算子。
  除了Sobel算子,还有一个算子,Shcarr算子。但这个算子其实只是Sobel算子的一个变种,由于Sobel算子在3*3的卷积模板上计算往往不太精确,因此有一个特殊的Sobel算子,其权值按照下图来表达,称之为Scharr算子。下图是Sobel算子与Scharr算子的一个对比。
图9 Sobel与Scharr示意图
  一般来说,Scharr算子能够比Sobel算子检测边缘的效果更好,从上图也可以看出。但是,这个“更好”是一把双刃剑。我们的目的并不是画出图像的边缘,而是确定车牌的一个区域,越精细的边缘越会干扰后面的闭运算。因此,针对大量的图片的测试,Sobel算子一般都优于Scharr 算子。
  关于Sobel算子更详细的解释和Scharr算子与Sobel算子的同异,可以参看官网的介绍:。
  综上所述,在求图像边缘的过程中,Sobel算子是一个最佳的契合车牌定位需求的算子,Laplace算子与Scharr算子的效果都不如它。
  有一点要说明的:Sobel算子仅能对灰度图像有效果,不能将色彩图像作为输入。因此在进行Soble算子前必须进行前面的灰度化工作
根据前文的内容,车牌定位的功能还剩下如下的步骤,见下图中未涂灰的部分。
图1 车牌定位步骤
  我们首先从Soble算子分析出来的边缘来看。通过下图可见,Sobel算子有很强的区分性,车牌中的字符被清晰的描绘出来,那么如何根据这些信息定位出车牌的位置呢?
图2 Sobel后效果
  我们的车牌定位功能做了个假设,即车牌是包含字符图块的一个最小的外接矩形。在大部分车牌处理中,这个假设都能工作的很好。我们来看下这个假设是如何工作的。
  车牌定位过程的全部代码如下:
//! 定位车牌图像
//! src 原始图像
//! resultVec 一个Mat的向量,存储所有抓取到的图像
//! 成功返回0,否则返回-1
int CPlateLocate::plateLocate(Mat src, vector&Mat&& resultVec)
Mat src_blur, src_
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);
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& &
findContours(img_threshold,
contours, // a vector of contours
CV_RETR_EXTERNAL, // 提取外部轮廓
CV_CHAIN_APPROX_NONE); // all pixels of each contours
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&
//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);
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
// 旋转这部分代码确实可以将某些倾斜的车牌调整正,
// 但是它也会误将更多正的车牌搞成倾斜!所以综合考虑,还是不使用这段代码。
// ,由于新到的一批图片中发现有很多车牌是倾斜的,因此决定再次尝试
// 这段代码。
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.
float angle = minRect.
Size rect_size = minRect.
if (r & 1)
angle = 90 +
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);
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
Mat resultM
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);
  首先,我们通过二值化处理将Sobel生成的灰度图像转变为二值图像。
四.二值化
  二值化算法非常简单,就是对图像的每个像素做一个阈值处理。
  为后续的形态学算子Morph等准备二值化的图像。 
  经过二值化处理后的图像效果为下图,与灰度图像仔细区分下,二值化图像中的白色是没有颜色强与暗的区别的。
图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即可。在后面的字符识别中我们会同时使用到正二值化与反二值化两种例子。因为中国的车牌有很多类型,最常见的是蓝牌和黄牌。其中蓝牌字符浅,背景深,黄牌则是字符深,背景浅,因此需要正二值化方法与反二值化两种方法来处理,其中正二值化处理蓝牌,反二值化处理黄牌。
闭操作是个非常重要的操作,我会花很多的字数与图片介绍它。
  将车牌字母连接成为一个连通域,便于取轮廓。 
  我们这里看下经过闭操作后图像连接的效果。
图4 闭操作后效果
  在做闭操作的说明前,必须简单介绍一下腐蚀和膨胀两个操作。
  在图像处理技术中,有一些的操作会对图像的形态发生改变,这些操作一般称之为形态学操作。形态学操作的对象是二值化图像。
有名的形态学操作中包括腐蚀,膨胀,开操作,闭操作等。其中腐蚀,膨胀是许多形态学操作的基础。
  腐蚀操作:
  顾名思义,是将物体的边缘加以腐蚀。具体的操作方法是拿一个宽m,高n的矩形作为模板,对图像中的每一个像素x做如下处理:像素x至于模板的中心,根据模版的大小,遍历所有被模板覆盖的其他像素,修改像素x的值为所有像素中最小的值。这样操作的结果是会将图像外围的突出点加以腐蚀。如下图的操作过程:
图5 腐蚀操作原理
  上图演示的过程是背景为黑色,物体为白色的情况。腐蚀将白色物体的表面加以“腐蚀”。在opencv的官方教程中,是以如下的图示说明腐蚀过程的,与我上面图的区别在于:背景是白色,而物体为黑色(这个不太符合一般的情况,所以我没有拿这张图作为通用的例子)。读者只需要了解背景为不同颜色时腐蚀也是不同的效果就可以了。
图6 腐蚀操作原理2
  膨胀操作:
  膨胀操作与腐蚀操作相反,是将图像的轮廓加以膨胀。操作方法与腐蚀操作类似,也是拿一个矩形模板,对图像的每个像素做遍历处理。不同之处在于修改像素的值不是所有像素中最小的值,而是最大的值。这样操作的结果会将图像外围的突出点连接并向外延伸。如下图的操作过程:
图7 膨胀操作原理
  下面是在opencv的官方教程中,膨胀过程的图示:
图8 膨胀操作原理2
  开操作:
  开操作就是对图像先腐蚀,再膨胀。其中腐蚀与膨胀使用的模板是一样大小的。为了说明开操作的效果,请看下图的操作过程:
图9 开操作原理
  由于开操作是先腐蚀,再膨胀。因此可以结合图5和图7得出图9,其中图5的输出是图7的输入,所以开操作的结果也就是图7的结果。
  闭操作:
  闭操作就是对图像先膨胀,再腐蚀。闭操作的结果一般是可以将许多靠近的图块相连称为一个无突起的连通域。在我们的图像定位中,使用了闭操作去连接所有的字符小图块,然后形成一个车牌的大致轮廓。闭操作的过程我会讲的细致一点。为了说明字符图块连接的过程。在这里选取的原图跟上面三个操作的原图不大一样,是一个由两个分开的图块组成的图。原图首先经过膨胀操作,将两个分开的图块结合起来(注意我用偏白的灰色图块表示由于膨胀操作而产生的新的白色)。接着通过腐蚀操作,将连通域的边缘和突起进行削平(注意我用偏黑的灰色图块表示由于腐蚀被侵蚀成黑色图块)。最后得到的是一个无突起的连通域(纯白的部分)。
图10 闭操作原理
  在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%以上的定位成功率。
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系列前脸太多垂直边缘了,给跪。
取轮廓操作是个相对简单的操作,因此只做简短的介绍。
  将连通域的外围勾画出来,便于形成外接矩形。 
  我们这里看下经过取轮廓操作的效果。
图19 取轮廓操作
  在图中,红色的线条就是轮廓,可以看到,有非常多的轮廓。取轮廓操作就是将图像中的所有独立的不与外界有交接的图块取出来。然后根据这些轮廓,求这些轮廓的最小外接矩形。这里面需要注意的是这里用的矩形是RotatedRect,意思是可旋转的。因此我们得到的矩形不是水平的,这样就为处理倾斜的车牌打下了基础。
  取轮廓操作的代码如下:
vector& vector& Point& &
findContours(img_threshold,
contours, // a vector of contours
CV_RETR_EXTERNAL, // 提取外部轮廓
CV_CHAIN_APPROX_NONE); // all pixels of each contours
七.尺寸判断
尺寸判断操作是对外接矩形进行判断,以判断它们是否是可能的候选车牌的操作。
  排除不可能是车牌的矩形。 
  经过尺寸判断,会排除大量由轮廓生成的不合适尺寸的最小外接矩形。效果如下图:
图20 尺寸判断操作
  通过对图像中所有的轮廓的外接矩形进行遍历,我们调用CplateLocate的另一个成员方法verifySizes,代码如下:
显示最终生成的车牌图像,便于判断是否成功进行了旋转。
Mat CPlateLocate::showResultMat(Mat src, Size rect_size, Point2f center, int index)
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 resultR
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 resultR
  在原先的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%不合适的矩形。
八.角度判断
角度判断操作通过角度进一步排除一部分车牌。
  排除不可能是车牌的矩形。 
  通过verifySizes的矩形,还必须进行一个筛选,即角度判断。一般来说,在一副图片中,车牌不太会有非常大的倾斜,我们做如下规定:如果一个矩形的偏斜角度大于某个角度(例如30度),则认为不是车牌并舍弃。
  对上面的尺寸判断结果的六个黄色矩形应用角度判断后结果如下图:
图21 角度判断后的候选车牌
  可以看出,原先的6个候选矩形只剩3个。车牌两侧的车灯的矩形被成功筛选出来。角度判断会去除verifySizes筛选余下的7%矩形,使得最终进入车牌判断环节的矩形只有原先的全部矩形的3%。
  角度判断以及接下来的旋转操作的代码如下:
旋转操作是为后面的车牌判断与字符识别提高成功率的关键环节。
  旋转操作将偏斜的车牌调整为水平。 
  假设待处理的图片如下图:
图22 倾斜的车牌
  使用旋转与不适用旋转的效果区别如下图:
图23 旋转的效果
  可以看出,没有旋转操作的车牌是倾斜,加大了后续车牌判断与字符识别的难度。因此最好需要对车牌进行旋转。
  在角度判定阈值内的车牌矩形,我们会根据它偏转的角度进行一个旋转,保证最后得到的矩形是水平的。调用的opencv函数如下:
Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
  这个调用使用了一个旋转矩阵,属于几何代数内容,在这里不做详细解释。
十.大小调整
  结束了么?不,还没有,至少在我们把这些候选车牌导入机器学习模型之前,需要确保他们的尺寸一致。
  机器学习模型在预测的时候,是通过模型输入的特征来判断的。我们的车牌判断模型的特征是所有的像素的值组成的矩阵。因此,如果候选车牌的尺寸不一致,就无法被机器学习模型处理。因此需要用resize方法进行调整。
  我们将车牌resize为宽度136,高度36的矩形。为什么用这个值?这个值一开始也不是确定的,我试过许多值。最后我将近千张候选车牌做了一个统计,取它们的平均宽度与高度,因此就有了136和36这个值。所以,这个是一个统计值,平均来说,这个值的效果最好。
  大小调整调用了CplateLocate的最后一个成员方法showResultMat,代码很简单,贴下,不做细讲了。
  通过接近10多个步骤的处理,我们才有了最终的候选车牌。这些过程是一环套一环的,前步骤的输出是后步骤的输入,而且顺序也是有规则的。目前针对我的测试图片来说,它们工作的很好,但不一定适用于你的情况。车牌定位以及图像处理算法的一个大的问题就是他的弱鲁棒性,换一个场景可能就得换一套工作方式。因此结合你的使用场景来做调整吧,这是我为什么要在这里费这么多字数详细说明的原因。如果你不了解细节,你就不可能进行修改,也就无法使它适合你的工作需求。
  讨论:
  车牌定位全部步骤了解后,我们来讨论下。这个过程是否是一个最优的解?
  毫无疑问,一个算法的好坏除了取决于它的设计思路,还取决于它是否充分利用了已知的信息。如果一个算法没有充分利用提供的信息,那么它就有进一步优化的空间。EasyPR的 plateLocate过程就是如此,在实施过程中它相继抛弃掉了色彩信息,没有利用纹理信息,因此车牌定位的过程应该还有优化的空间。如果 plateLocate过程无法良好的解决你的定位问题,那么尝试下能够利用其他信息的方法,也许你会大幅度提高你的定位成功率。
  车牌定位讲完后,下面就是机器学习的过程。不同于前者,我不会重点说明其中的细节,而是会概括性的说明每个步骤的用途以及训练的最佳实践。在下一个章节中,我会首先介绍下什么是机器学习,为什么它如今这么火热,机器学习和大数据的关系,欢迎继续阅读。
  本项目的Git地址:。如果有问题欢迎提issue。本文是一个系列中的第5篇,前几篇文章见前面的博客
本篇文章介绍EasyPR里新的定位功能:颜色定位与偏斜扭正。希望这篇文档可以帮助开发者与使用者更好的理解EasyPR的设计思想。
  让我们先看一下示例图片,这幅图片中的车牌通过颜色的定位法进行定位并从偏斜的视角中扭正为正视角(请看右图的左上角)。
图1 新版本的定位效果
下面内容会对这两个特性的实现过程展开具体的介绍。首先介绍颜色定位的原理,然后是偏斜扭正的实现细节。
  由于本文较长,为方便读者,以下是本文的目录:
  一.颜色定位
  二.偏斜扭正
  三.总结
一. 颜色定位
  在前面的介绍里,我们使用了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;
const int max_blue = 140;
//yellow的H范围
const int min_yellow = 15; //15
const int max_yellow = 40; //40
// 转到HSV空间进行处理,颜色搜索主要使用的是H分量进行蓝色与黄色的匹配工作
cvtColor(src, src_hsv, CV_BGR2HSV);
vector&Mat& hsvS
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_
max_h = max_
case YELLOW:
min_h = min_
max_h = max_
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.
//图像数据列需要考虑通道数的影响;
int nCols = src_hsv.cols *
if (src_hsv.isContinuous())//连续存储的数据,按一行处理
nCols *= nR
nRows = 1;
float s_all = 0;
float v_all = 0;
float count = 0;
for (i = 0; i & nR ++i)
p = src_hsv.ptr&uchar&(i);
for (j = 0; j & nC j += 3)
int H = int(p[j]); //0-180
int S = int(p[j + 1]);
int V = int(p[j + 2]);
s_all += S;
v_all += V;
bool colorMatched =
if (H & min_h && H & max_h)
int Hdiff = 0;
if (H & avg_h)
Hdiff = H - avg_h;
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)
min_sv = minabs_ // add
if ((S & min_sv && S & max_sv) && (V & min_sv && V & max_sv))
colorMatched =
if (colorMatched == true) {
p[j] = 0; p[j + 1] = 0; p[j + 2] = 255;
p[j] = 0; p[j + 1] = 0; p[j + 2] = 0;
//cout && &avg_s:& && s_all / count &&
//cout && &avg_v:& && v_all / count &&
// 获取颜色匹配后的二值灰度图
vector&Mat& hsvSplit_
split(src_hsv, hsvSplit_done);
src_grey = hsvSplit_done[2];
match = src_
return src_
  以上说明了颜色定位的设计思想与细节。那么颜色定位是不是就是万能的?答案是否定的。在色彩充足,光照足够的情况下,颜色定位的效果很好,但是在面对光线不足的情况,或者蓝色车身的情况时,颜色定位的效果很糟糕。下图是一辆蓝色车辆,可以看出,车牌与车身内容完全重叠,无法分割。
图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(...),下面是这个函数的代码。
3.扩大化旋转
  好,当我通过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)
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 -
int height = y + in.rows & in_large.rows ? in.rows : in_large.rows -
/*assert(width == in.cols);
assert(height == in.rows);*/
if (width != in.cols || height != in.rows)
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);*/
warpAffine(in_large, mat_rotated, rot_mat, Size(in_large.cols, in_large.rows), CV_INTER_CUBIC);
/*imshow(&mat_rotated&, mat_rotated);
waitKey(0);*/
getRectSubPix(mat_rotated, Size(rect_size.width, rect_size.height), new_center, img_crop);
out = img_
/*imshow(&img_crop&, img_crop);
waitKey(0);*/
4.偏斜判断
  当我们对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.
int nCols = in.
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*
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++]);
//cout && &len[0]:& && len[0] &&
//cout && &len[1]:& && len[1] &&
//cout && &len[2]:& && len[2] &&
double maxlen = max(len[2], len[0]);
double minlen = min(len[2], len[0]);
double difflen = abs(len[2] - len[0]);
//cout && &nCols:& && nCols &&
double PI = 3.;
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 &&
cout && &slope_can_2:& && slope_can_2 &&
cout && &slope_can_3:& && slope_can_3 &&*/
slope = abs(slope_can_1 - g) &= abs(slope_can_2 - g) ? slope_can_1 : slope_can_2;
/*slope = max(
doubl}

我要回帖

更多关于 关灯游戏算法 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信