EasyPR--开发详解

我正在作一个开源的中文车牌识别系统,Git地址为:https://github.com/liuruoze/EasyPR。 css

  我给它取的名字为EasyPR,也就是Easy to do Plate Recognition的意思。我开发这套系统的主要缘由是由于我但愿可以锻炼我在这方面的能力,包括C++技术、计算机图形学、机器学习等。我把这个项目开源的主要目的是:1.它基于开源的代码诞生,理应回归开源;2.我但愿有人可以一块儿协助强化这套系统,包括代码、训练数据等,可以让这套系统的准确性更高,鲁棒性更强等等。 html

  相比于其余的车牌识别系统,EasyPR有以下特色: 前端

  1. 它基于openCV这个开源库,这意味着全部它的代码均可以轻易的获取。
  2. 它可以识别中文,例如车牌为苏EUK722的图片,它能够准确地输出std:string类型的"苏EUK722"的结果。
  3. 它的识别率较高。目前状况下,字符识别已经能够达到90%以上的精度。

  系统还提供全套的训练数据提供(包括车牌检测的近500个车牌和字符识别的4000多个字符)。全部所有均可以在Github的项目地址上直接下载到。 git

那么,EasyPR是如何产生的呢?我简单介绍一下它的诞生过程: github

  首先,在5月份左右时我考虑要作一个车牌识别系统。这个车牌系统中全部的代码都应该是开源的,不能基于任何黑盒技术。这主要起源于我想锻炼本身的C++和计算机视觉的水平。 算法

  我在网上开始搜索了资料。因为计算机视觉中不少的算法我都是使用openCV,并且openCV发展很是良好,所以我查找的项目必须得是基于OpenCV技术的。因而我在CSDN的博客上找了一篇文章数据库

  文章的做者taotao1233在这两篇博客中以半学习笔记半开发讲解的方式说明了一个车牌识别系统的所有开发过程。很是感谢他的这些博客,借助于这些资料,我着手开始了开发。当时的想法很是朴素,就是想看看按照这些资料,可否真的实现一个车牌识别的系统。关于车牌照片数据的问题,幸运的很,我正在开发的一个项目中有大量的照片,所以数据不是问题。 数组

  使人高兴的是,系统确实可以工做,可是让人沮丧的,彷佛也就“仅仅”可以工做而已。在车牌检测这个环节中正确性已经惨不忍睹。 安全

  这个事情给了我一拨不小的冷水,原本我觉得很快的开发进度看来是乐观过头了。因而我决定沉下心来,仔细研究他的系统实现的每个过程,结合OpenCV的官网教程与API资料,我发现他的实现系统中有不少并不适合我目前在作的场景。 网络

  我手里的数据大部分是高速上的图像抓拍数据,其中每一个车牌都偏小,并且模糊度较差。直接使用他们的方法,正确率低到了可怕的地步。因而我开始尝试利用openCv中的一些函数与功能,替代,增长,调优等等方法,不断的优化。这个过程很漫长,可是也有不少的积累。我逐渐发现,而且了解他系统中每个步骤的目的,原理以及若是修改能够进行优化的方法。

  在最终实现的代码中,个人代码已经跟他的原始代码有不少的不同了,可是成功率大幅度上升,并且车牌的正确检测率不断被优化。在系列文章的后面,我会逐一分享这些优化的过程与心得。

  最终我实现的系统与他的系统有如下几点不一样:

  1. 他的系统代码基本上彻底参照了《Mastering OpenCV with Practical Computer Vision Projects》这本书的代码,而这本书的代码是专门为西班牙车牌所开发的,所以不适合中文的环境。
  2. 他的系统的代码大部分是原始代码的搬迁,并无作到优化与改进的地步。而个人系统中对原来的识别过程,作了不少优化步骤。
  3. 车牌识别中核心的机器学习算法的模型,他直接使用了原书提供的,而我这两个过程的模型是本身生成,并且模型也作了测试,做为开源系统的一部分也提供了出来。

  尽管我和他的系统有这么多的不一样,可是咱们在根本的系统结构上是一致的。应该说,咱们都是参照了“Mastering OpenCV”这本数的处理结构。在这点上,我并无所“创新”,事实上,结果也证实了“Mastering OpenCV”上的车牌识别的处理逻辑,是一个实际有效的最佳处理流程。

  “Mastering OpenCV”,包括咱们的系统,都是把车牌识别划分为了两个过程:即车牌检测(Plate Detection)和字符识别(Chars Recognition)两个过程。可能有些书籍或论文上不是这样叫的,可是我以为,这样的叫法更容易理解,也不容易搞混。

  • 车牌检测(Plate Detection):对一个包含车牌的图像进行分析,最终截取出只包含车牌的一个图块。这个步骤的主要目的是下降了在车牌识别过程当中的计算量。若是直接对原始的图像进行车牌识别,会很是的慢,所以须要检测的过程。在本系统中,咱们使用SVM(支持向量机)这个机器学习算法去判别截取的图块是不是真的“车牌”。
  • 字符识别(Chars Recognition):有的书上也叫Plate Recognition,我为了与整个系统的名称作区分,因此改成此名字。这个步骤的主要目的就是从上一个车牌检测步骤中获取到的车牌图像,进行光学字符识别(OCR)这个过程。其中用到的机器学习算法是著名的人工神经网络(ANN)中的多层感知机(MLP)模型。最近一段时间很是火的“深度学习”其实就是多隐层的人工神经网络,与其有很是紧密的联系。经过了解光学字符识别(OCR)这个过程,也能够知晓深度学习所基于的人工神经网路技术的一些内容。

  下图是一个完整的EasyPR的处理流程:

本开源项目的目标客户群有三类:
  1. 须要开发一个车牌识别系统的(开发者)。
  2. 须要车牌系统去识别车牌的(用户)。
  3. 急于作毕业设计的(学生)。

  第一类客户是本项目的主要使用者,所以项目特意被精心划分为了6个模块,以供开发者按需选择。
  第二类客户可能会有部分,EasyPR有一个同级项目EasyPR_Dll,能够DLL方式嵌入到其余的程序中,另外还有个一个同级项目EasyPR_Win,基于WTL开发的界面程序,能够简化与帮助车牌识别的结果比对过程。
  对于第三类客户,能够这么说,有完整的全套代码和详细的说明,我相信大家能够稍做修改就能够经过设计大考。

推荐你使用EasyPR有如下几点理由:

  • 这里面的代码都是做者亲自优化过的,你能够在上面作修改,作优化,甚至一块儿协做开发,一些处理车牌的细节方法你应该是感兴趣的。
  • 若是你对代码不感兴趣,那么通过做者精心训练的模型,包括SVM和ANN的模型,能够帮助你提高或验证你程序的正确率。
  • 若是你对模型也不感兴趣,那么成百上千通过做者亲自挑选的训练数据生成的文件,你应该感兴趣。做者花了大量的时间处理这些训练数据与调整,如今直接提供给你,能够大幅度减轻不少人缺乏数据的难题。

  有兴趣的同志能够留言或发Email:liuruoze@163.com 或者直接在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。我撰写这系列文章的目的是:一、普及车牌识别中相关的技术与知识点;二、帮助开发者了解EasyPR的实现细节;三、增进沟通。

  EasyPR的项目地址在这:GitHub。要想运行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 
{
public:
    CPlateLocate();

    //! 车牌定位
    int plateLocate(Mat, vector<Mat>& );

    //! 车牌的尺寸验证
    bool verifySizes(RotatedRect mr);

    //! 结果车牌显示
    Mat showResultMat(Mat src, Size rect_size, Point2f center);

    //! 设置与读取变量
    //...

protected:
    //! 高斯模糊所用变量
    int m_GaussianBlurSize;

    //! 链接操做所用变量
    int m_MorphSizeWidth;
    int m_MorphSizeHeight;

    //! verifySize所用变量
    float m_error;
    float m_aspect;
    int m_verifyMin;
    int m_verifyMax;

    //! 角度判断所用变量
    int m_angle;

    //! 是否开启调试模式,0关闭,非0开启
    int m_debug;
};

复制代码

  注意,全部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中设置以下代码开启调试模式。

    CPlateLocate plate;
    plate.setDebug(1);

  临时图片会生成在tmp文件夹下。对多个车牌图片处理的结果仅会保留最后一个车牌图片的临时图片。

  一、原始图片。

  二、通过高斯模糊后的图片。通过这步处理,能够看出图像变的模糊了。这步的做用是为接下来的Sobel算子去除干扰的噪声。

  三、将图像进行灰度化。这个步骤是一个分水岭,意味着后面的全部操做都不能基于色彩信息了。此步骤是利是弊,后面再作分析。

  四、对图像进行Sobel运算,获得的是图像的一阶水平方向导数。这步事后,车牌被明显的区分出来。

  五、对图像进行二值化。将灰度图像(每一个像素点有256个取值可能)转化为二值图像(每一个像素点仅有1和0两个取值可能)。

  六、使用闭操做。对图像进行闭操做之后,能够看到车牌区域被链接成一个矩形装的区域。

  七、求轮廓。求出图中全部的轮廓。这个算法会把全图的轮廓都计算出来,所以要进行筛选。

  八、筛选。对轮廓求最小外接矩形,而后验证,不知足条件的淘汰。通过这步,仅仅只有六个黄色边框的矩形经过了筛选。

  八、角度判断与旋转。把倾斜角度大于阈值(如正负30度)的矩形舍弃。左边第1、2、四个矩形被舍弃了。余下的矩形进行微小的旋转,使其水平。

  十、统一尺寸。上步获得的图块尺寸是不同的。为了进入机器学习模型,须要统一尺寸。统一尺寸的标准宽度是136,长度是36。这个标准是对千个测试车牌平均后得出的通用值。下图为最终的三个候选”车牌“图块。

  这些“车牌”有两个做用:1、积累下来做为支持向量机(SVM)模型的训练集,以此训练出一个车牌判断模型;2、在实际的车牌检测过程当中,将这些候选“车牌”交由训练好的车牌判断模型进行判断。若是车牌判断模型认为这是车牌的话就进入下一步即字符识别过程,若是不是,则舍弃。

3.“Plate Locate”的深刻讨论与调优策略

  好了,说了这么多,读者想必对整个“Plate Locate”过程已经有了一个完整的认识。那么让咱们一步步审核一下处理流程中的每个步骤。回答下面三个问题:这个步骤的做用是什么?省略这步或者替换这步可不能够?这个步骤中是否有参数能够调优的?经过这几个问题能够帮助咱们更好的理解车牌定位功能,而且便于本身作修改、定制。

  因为篇幅关系,下面的深刻讨论放在下期

上篇文章中咱们了解了PlateLocate的过程当中的全部步骤。在本篇文章中咱们对前3个步骤,分别是高斯模糊、灰度化和Sobel算子进行分析。

1、高斯模糊

1.目标

  对图像去噪,为边缘检测算法作准备。  

2.效果

  在咱们的车牌定位中的第一步就是高斯模糊处理。

图1 高斯模糊效果

3.理论

  详细说明能够看这篇:阮一峰讲高斯模糊

  高斯模糊是很是有名的一种图像处理技术。顾名思义,其通常应用是将图像变得模糊,但同时高斯模糊也应用在图像的预处理阶段。理解高斯模糊前,先看一下平均模糊算法。平均模糊的算法很是简单。见下图,每个像素的值都取周围全部像素(共8个)的平均值。


图2 平均模糊示意图

  在上图中,左边红色点的像素值原本是2,通过模糊后,就成了1(取周围全部像素的均值)。在平均模糊中,周围像素的权值都是同样的,都是1。若是周围像素的权值不同,而且与二维的高斯分布的值同样,那么就叫作高斯模糊。

  在上面的模糊过程当中,每一个像素取的是周围一圈的平均值,也称为模糊半径为1。若是取周围三圈,则称之为半径为3。半径增大的话,会更加深模糊的效果。

4.实践

  在PlateLocate中是这样调用高斯模糊的。

    //高斯模糊。Size中的数字影响车牌定位的效果。
    GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize), 
        0, 0, BORDER_DEFAULT );

  其中Size字段的参数指定了高斯模糊的半径。值是CPlateLocate类的m_GaussianBlurSize变量。因为opencv的高斯模糊仅接收奇数的半径,所以变量为偶数值会抛出异常。
  这里给出了opencv的高斯模糊的API(英文,2.48以上版本)。
  高斯模糊这个过程必定是必要的么。笔者的回答是必要的,假若咱们将这句代码注释并稍做修改,从新运行一下。你会发现plateLocate过程在闭操做时就和原来发生了变化。最后结果以下。

图3 不采用高斯模糊后的结果  

  能够看出,车牌所在的矩形产生了偏斜。最后获得的候选“车牌”图块以下:

图4 不采用高斯模糊后的“车牌”图块

  若是不使用高斯模糊而直接用边缘检测算法,咱们获得的候选“车牌”达到了8个!这样不只会增长车牌判断的处理时间,还增长了判断出错的几率。因为获得的车牌图块中车牌是斜着的,若是咱们的字符识别算法须要一个水平的车牌图块,那么几乎确定咱们会没法获得正确的字符识别效果。

  高斯模糊中的半径也会给结果带来明显的变化。有的图片,高斯模糊半径太高了,车牌就定位不出来。有的图片,高斯模糊半径偏低了,车牌也定位不出来。所以、高斯模糊的半径既不宜太高,也不能太低。CPlateLocate类中的值为5的静态常量DEFAULT_GAUSSIANBLUR_SIZE,标示着推荐的高斯模糊的半径。这个值是对于近千张图片通过测试后得出的综合定位率最高的一个值。在CPlateLocate类的构造函数中,m_GaussianBlurSize被赋予了DEFAULT_GAUSSIANBLUR_SIZE的值,所以,默认的高斯模糊的半径就是5。若是不是特殊状况,不须要修改它。

  在数次的实验之后,必须认可,保留高斯模糊过程与半径值为5是最佳的实践。为应对特殊需求,在CPlateLocate类中也应该提供了方法修改高斯半径的值,调用代码(假设须要一个为3的高斯模糊半径)以下:

    CPlateLocate plate;
    plate.setGaussianBlurSize(3);

  目前EasyPR的处理步骤是先进行高斯模糊,再进行灰度化。从目前的实验结果来看,基于色彩的高斯模糊过程比灰度后的高斯模糊过程更容易检测到边缘点。

2、灰度化处理

1.目标

  为边缘检测算法准备灰度化环境。

2.效果

灰度化的效果以下。

图5 灰度化效果

 3.理论

  在灰度化处理步骤中,争议最大的就是信息的损失。无疑的,原先plateLocate过程面对的图片是彩色图片,而从这一步之后,就会面对的是灰度图片。在前面,已经说过这步骤是利是弊是须要讨论的。

   无疑,对于计算机而言,色彩图像相对于灰度图像难处理多了,不少图像处理算法仅仅只适用于灰度图像,例如后面提到的Sobel算子。在这种状况下,你除 了把图片转成灰度图像再进行处理别无它法,除非从新设计算法。但另外一方面,转化成灰度图像后偏偏失去了最丰富的细节。要知道,真实世界是彩色的,人类对于 事物的辨别是基于彩色的框架。甚至能够这样说,由于咱们的肉眼可以区别彩色,因此咱们对于事物的区分,辨别,记忆的能力就很是的强。
  车牌定位环节中去掉彩色的利弊也是同理。转换成灰度图像虽然利于使用各类专用的算法,但失去了真实世界中辨别的最重要工具---色彩的区分。举个简单的例子,人怎么在一张图片中找到车牌?很是简单,一眼望去,一个合适大小的矩形,蓝色的、或者黄色的、或者其余颜色的在另外一个黑色,或者白色的大的跟车形相似的矩形中。这个过程很是直观,明显,并且能够排除模糊,色泽,不清楚等不少影响。若是使用灰度图像,就必须借助水平,垂直求导等方法。
  将来若是PlateLocate过程能够使用颜色来判断,可能会比如今的定位更清楚、准确。但这须要研究与实验过程,在EasyPR的将来版本中可能会实现。但无疑,使用色彩判断是一种趋势,由于它不只符合人眼识别的规律,更趋近于人工智能的本质,并且它更准确,速度更快。

4.实践

  在PlateLocate过程当中是这样调用灰度化的。

cvtColor( src_blur, src_gray, CV_RGB2GRAY );

  这里给出了opencv的灰度化的API(英文,2.48以上版本)。

三.Sobel算子

1.目标

  检测图像中的垂直边缘,便于区分车牌。

 2.效果

下图是Sobel算子的效果。


图6 Sobel效果

3.理论

  若是要说哪一个步骤是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。

 4.实践

  在代码中调用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的API(英文,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与Scharr
  综上所述,在求图像边缘的过程当中,Sobel算子是一个最佳的契合车牌定位需求的算子,Laplace算子与Scharr算子的效果都不如它。
  有一点要说明的:Sobel算子仅能对灰度图像有效果,不能将色彩图像做为输入。所以在进行Soble算子前必须进行前面的灰度化工做

根据前文的内容,车牌定位的功能还剩下以下的步骤,见下图中未涂灰的部分。

图1 车牌定位步骤

  咱们首先从Soble算子分析出来的边缘来看。经过下图可见,Sobel算子有很强的区分性,车牌中的字符被清晰的描绘出来,那么如何根据这些信息定位出车牌的位置呢?

图2 Sobel后效果

  咱们的车牌定位功能作了个假设,即车牌是包含字符图块的一个最小的外接矩形。在大部分车牌处理中,这个假设都能工做的很好。咱们来看下这个假设是如何工做的。

  车牌定位过程的所有代码以下:

View Code

  首先,咱们经过二值化处理将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,代码以下:

显示最终生成的车牌图像,便于判断是否成功进行了旋转。
 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;
 }

  在原先的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%。

  角度判断以及接下来的旋转操做的代码以下:

View Code

九.旋转

旋转操做是为后面的车牌判断与字符识别提升成功率的关键环节。

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,代码很简单,贴下,不作细讲了。

View Code

十一.总结

  经过接近10多个步骤的处理,咱们才有了最终的候选车牌。这些过程是一环套一环的,前步骤的输出是后步骤的输入,并且顺序也是有规则的。目前针对个人测试图片来讲,它们工做的很好,但不必定适用于你的状况。车牌定位以及图像处理算法的一个大的问题就是他的弱鲁棒性,换一个场景可能就得换一套工做方式。所以结合你的使用场景来作调整吧,这是我为何要在这里费这么多字数详细说明的缘由。若是你不了解细节,你就不可能进行修改,也就没法使它适合你的工做需求。
  讨论:
  车牌定位所有步骤了解后,咱们来讨论下。这个过程是不是一个最优的解?
  毫无疑问,一个算法的好坏除了取决于它的设计思路,还取决于它是否充分利用了已知的信息。若是一个算法没有充分利用提供的信息,那么它就有进一步优化的空间。EasyPR的 plateLocate过程就是如此,在实施过程当中它相继抛弃掉了色彩信息,没有利用纹理信息,所以车牌定位的过程应该还有优化的空间。若是 plateLocate过程没法良好的解决你的定位问题,那么尝试下可以利用其余信息的方法,也许你会大幅度提升你的定位成功率。
  车牌定位讲完后,下面就是机器学习的过程。不一样于前者,我不会重点说明其中的细节,而是会归纳性的说明每一个步骤的用途以及训练的最佳实践。在下一个章节中,我会首先介绍下什么是机器学习,为何它现在这么火热,机器学习和大数据的关系,欢迎继续阅读。
  本项目的Git地址:这里。若是有问题欢迎提issue。本文是一个系列中的第5篇,前几篇文章见前面的博客

 

本篇文章介绍EasyPR里新的定位功能:颜色定位与偏斜扭正。但愿这篇文档能够帮助开发者与使用者更好的理解EasyPR的设计思想。

  让咱们先看一下示例图片,这幅图片中的车牌经过颜色的定位法进行定位并从偏斜的视角中扭正为正视角(请看右图的左上角)。

图1 新版本的定位效果

下面内容会对这两个特性的实现过程展开具体的介绍。首先介绍颜色定位的原理,而后是偏斜扭正的实现细节。

  因为本文较长,为方便读者,如下是本文的目录:

  一.颜色定位

  1.1起源

  1.2方法

  1.3不足与改善

  二.偏斜扭正

  2.1分析

  2.2ROI截取

  2.3扩大化旋转

  2.4偏斜判断

  2.5仿射变换

  2.6总结

  三.总结

一. 颜色定位

1.起源

  在前面的介绍里,咱们使用了Sobel查找垂直边缘的方法,成功定位了许多车牌。可是,Sobel法最大的问题就在于面对垂直边缘交错的状况下,没法准确地定位车牌。例以下图。为了解决这个问题,能够考虑使用颜色信息进行定位。

图2 颜色定位与Sobel定位的比较

  若是将颜色定位与Sobel定位加以结合的话,能够使车牌的定位准确率从75%上升到94%。

2.方法

  关于颜色定位首先咱们想到的解决方案就是:利用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;
    }

复制代码

3.不足

  以上说明了颜色定位的设计思想与细节。那么颜色定位是否是就是万能的?答案是否认的。在色彩充足,光照足够的状况下,颜色定位的效果很好,可是在面对光线不足的状况,或者蓝色车身的状况时,颜色定位的效果很糟糕。下图是一辆蓝色车辆,能够看出,车牌与车身内容彻底重叠,没法分割。

图9 失效的颜色定位

  碰到失效的颜色定位状况时须要使用原先的Sobel定位法。

  目前的新版本使用了颜色定位与Sobel定位结合的方式。首先进行颜色定位,而后根据条件使用Sobel进行再次定位,增长整个系统的适应能力。

  为了增强鲁棒性,Sobel定位法能够用两阶段的查找。也就是在已经被Sobel定位的图块中,再进行一次Sobel定位。这样能够增长准确率,但会下降了速度。一个折衷的方案是让用户决定一个参数m_maxPlates的值,这个值决定了你在一幅图里最多定位多少车牌。系统首先用颜色定位出候选车牌,而后经过SVM模型来判断是不是车牌,最后统计数量。若是这个数量大于你设定的参数,则认为车牌已经定位足够了,不须要后一步处理,也就不会进行两阶段的Sobel查找。相反,若是这个数量不足,则继续进行Sobel定位。

  综合定位的代码位于CPlateDectec中的的成员函数plateDetectDeep中,如下是plateDetectDeep的总体流程。

图10 综合定位所有流程

  有没有颜色定位与Sobel定位都失效的状况?有的。这种状况下可能须要使用第三类定位技术--字符定位技术。这是EasyPR发展的一个方向,这里不展开讨论。

二. 偏斜扭转

  解决了颜色的定位问题之后,下面的问题是:在定位之后,咱们如何把偏斜过来的车牌扭正呢?

图11 偏斜扭转效果

  这个过程叫作偏斜扭转过程。其中一个关键函数就是opencv的仿射变换函数。但在具体实施时,有不少须要解决的问题。

1.分析

  在任何新的功能开发以前,技术预研都是第一步。

  在这篇文档介绍了opencv的仿射变换功能。效果见下图。

图12 仿射变换效果 

  仔细看下,貌似这个功能跟咱们的需求很类似。咱们的偏斜扭转功能,说白了,就是把对图像的观察视角进行了一个转换。

  不过这篇文章里的代码基原本自于另外一篇官方文档。官方文档里还有一个例子,能够矩形扭转成平行四边形。而咱们的需求正是将平行四边形的车牌扭正成矩形。这么说来,只要使用例子中对应的反函数,应该就能够实现咱们的需求。从这个角度来看,偏斜扭转功能够实现。肯定了可行性之后,下一步就是思考如何实现。

  在原先的版本中,咱们对定位出来的区域会进行一次角度判断,当角度小于某个阈值(默认30度)时就会进行全图旋转。

  这种方式有两个问题:

  一是咱们的策略是对整幅图像旋转。对于opencv来讲,每次旋转操做都是一个矩形的乘法过程,对于很是大的图像,这个过程是很是消耗计算资源的;

  二是30度的阈值没法处理示例图片。事实上,示例图片的定位区域的角度是-50度左右,已经大于咱们的阈值了。为了处理这样的图片,咱们须要把咱们的阈值增大,例如增长到60度,那么这样的结果是带来候选区域的增多。

  两个因素结合,会大幅度增长处理时间。为了避免让处理速度降低,必须想办法规避这些影响。

  一个方法是再也不使用全图旋转,而是区域旋转。其实咱们在获取定位区域后,咱们并不须要定位区域之外的图像。

  假若咱们能划出一块小的区域包围定位区域,而后咱们仅对定位区域进行旋转,那么计算量就会大幅度下降。而这点,在opencv里是能够实现的,咱们对定位区域RotatedRect用boundingRect()方法获取外接矩形,再使用Mat(Rect ...)方法截取这个区域图块,从而生成一个小的区域图像。因而下面的全部旋转等操做均可以基于这个区域图像进行。

  在这些设计决定之后,下面就来思考整个功能的架构。

  咱们要解决的问题包括三类,第一类是正的车牌,第二类是倾斜的车牌,第三类是偏斜的车牌。前两类是前面说过的,第三类是本次新增的功能需求。第二类倾斜车牌与第三类车牌的区别见下图。

图13 两类不一样的旋转

  经过上图能够看出,正视角的旋转图片的观察角度仍然是正方向的,只是因为路的不平或者摄像机的倾斜等缘由,致使矩形有必定倾斜。这类图块的特色就是在RotataedRect内部,车牌部分仍然是个矩形。偏斜视角的图片的观察角度是非正方向的,是从侧面去看车牌。这类图块的特色是在 RotataedRect内部,车牌部分再也不是个矩形,而是一个平行四边形。这个特性决定了咱们须要区别的对待这两类图片。

  一个初步的处理思路就是下图。

图14 分析实现流程

  简单来讲,整个处理流程包括下面四步:

  1.感兴趣区域的截取
  2.角度判断
  3.偏斜判断
  4.仿射变换 

  接下来按照这四个步骤依次介绍。

2.ROI截取

  若是要使用区域旋转,首先咱们必须从原图中截取出一个包含定位区域的图块。

  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(...),下面是这个函数的代码。

View Code

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)
{
    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;

    
}

复制代码

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.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;
}

复制代码

5.仿射变换

  俗话说:行百里者半九十。前面已经作了如此多的工做,应该能够实现偏斜扭转功能了吧?但在最后的道路中,仍然有问题等着咱们。

  咱们已经实现了旋转功能,而且在旋转后的区域中截取了车牌区域,而后判断车牌区域中的图形是一个平行四边形。下面要作的工做就是把平行四边形扭正成一个矩形。

图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的正负来判断车牌是左斜仍是右斜。

6.总结

  经过以上过程,咱们成功的将一个偏斜的车牌通过旋转变换等方法扭正过来。

  让咱们回顾一下偏斜扭正过程。咱们须要将一个偏斜的车牌扭正,为了达成这个目的咱们首先须要对图像进行旋转。由于旋转是个计算量很大的函数,因此咱们须要考虑再也不用全图旋转,而是区域旋转。在旋转过程当中,会发生图像截断问题,因此须要使用扩大化旋转方法。旋转之后,只有偏斜视角的车牌才须要扭正,正视角的车牌不须要,所以还须要一个偏斜判断过程。如此一来,偏斜扭正的过程须要旋转,区域截取,扩大化,偏斜判断等等过程的协助,这就是整个流程中有这么多步须要处理的缘由。

  下图从另外一个视角回顾了偏斜扭正的过程,主要说明了偏斜扭转中的两次“截取”过程。

图32 偏斜扭正全过程

  1. 首先咱们获取RotatedRect,而后对每一个RotatedRect获取外界矩形,也就是ROI区域。外接矩形的计算有可能得到不安全的坐标,所以须要使用安全的获取外界矩形的函数。
  2. 获取安全外接矩形之后,在原图中截取这部分区域,并放置到一个新的Mat里,称之为ROI图像。这是本过程当中第一次截取,使用Mat(Rect ...)函数。
  3. 接下来对ROI图像根据RotatedRect的角度展开旋转,旋转的过程当中使用了放大化旋转法,以此防止车牌区域被截断。
  4. 旋转完之后,咱们把已经转正的RotatedRect部分截取出来,称之为车牌区域。这是本过程当中第二次截取,与第一次不一样,此次截取使用getRectSubPix()方法。
  5. 接下里使用偏斜判断函数来判断车牌区域里的车牌是不是倾斜的。
  6. 若是是,则继续使用仿射变换函数wrapAffine来进行扭正处理,处理过程当中要注意三个关键点的坐标。
  7. 最后使用resize函数将车牌区域统一化为EasyPR的车牌大小。

  整个过程有一个统一的函数--deskew。下面是deskew的代码。

View Code

  最后是改善建议:

  角度偏斜判断时能够用白色区域的轮廓来肯定平行四边形的四个点,而后用这四个点来计算斜率。这样算出来的斜率的可能鲁棒性更好。

三. 总结

  本篇文档介绍了颜色定位与偏斜扭转等功能。其中颜色定位属于做者一直想作的定位方法,而偏斜扭转则是做者之前认为不可能解决的问题。这些问题如今都基本被攻克了,并在这篇文档中阐述,但愿这篇文档能够帮助到读者。

  做者但愿能在这片文档中不只传递知识,也传授我在摸索过程当中积累的经验。由于光知道怎么作并不能加深对车牌识别的认识,只有经历过失败,了解哪些思想尝试过,碰到了哪些问题,是如何解决的,才能帮助读者更好地认识这个系统的内涵。

  最后,做者很感谢可以阅读到这里的读者。若是看完以为好的话,还请轻轻点一下赞,大家的鼓励就是做者继续行文的动力。

对EasyPR作下说明:EasyPR,一个开源的中文车牌识别系统,代码托管在github。其次,在前面的博客文章中,包含EasyPR至今的开发文档与介绍。在后续的文章中,做者会介绍EasyPR中字符分割与识别等相关内容,欢迎继续阅读

在前面的几篇文章中,咱们介绍了EasyPR中车牌定位模块的相关内容。本文开始分析车牌定位模块后续步骤的车牌判断模块。车牌判断模块是EasyPR中的基于机器学习模型的一个模块,这个模型就是做者前文中从机器学习谈起中提到的SVM(支持向量机)。
  咱们已经知道,车牌定位模块的输出是一些候选车牌的图片。但如何从这些候选车牌图片中甄选出真正的车牌,就是经过SVM模型判断/预测获得的。

图1 从候选车牌中选出真正的车牌

  简单来讲,EasyPR的车牌判断模块就是将候选车牌的图片一张张地输入到SVM模型中,而后问它,这是车牌么?若是SVM模型回答不是,那么就继续下一张,若是是,则把图片放到一个输出列表里。最后把列表输入到下一步处理。因为EasyPR使用的是列表做为输出,所以它能够输出一副图片中全部的车牌,不像一些车牌识别程序,只能输出一个车牌结果。

图2 EasyPR输出多个车牌

  如今,让咱们一步步地,进入这个SVM模型的核心看看,它是如何作到判断一副图片是车牌仍是不是车牌的?本文主要分为三个大的部分:

  1. SVM应用:描述如何利用SVM模型进行车牌图片的判断。
  2. SVM训练:说明如何经过一系列步骤获得SVM模型。
  3. SVM调优:讨论如何对SVM模型进行优化,使其效果更加好。

一.SVM应用

  人类是如何判断一个张图片所表达的信息呢?简单来讲,人类在成长过程当中,大脑记忆了无数的图像,而且依次给这些图像打上了标签,例如太阳,天空,房子,车子等等。大家还记得当年上幼儿园时的那些教科书么,上面一个太阳,下面是文字。图像的组成事实上就是许多个像素,由像素组成的这些信息被输入大脑中,而后得出这个是什么东西的回答。咱们在SVM模型中一开始输入的原始信息也是图像的全部像素,而后SVM模型经过对这些像素进行分析,输出这个图片是不是车牌的结论。

图3 经过图像来学习

  SVM模型处理的是最简单的状况,它只要回答是或者不是这个“二值”问题,比从许多类中检索要简单不少。

  咱们能够看一下SVM进行判断的代码:

View Code

  首先咱们读取这幅图片,而后把这幅图片转为OPENCV须要的格式;

    Mat p = histeq(inMat).reshape(1, 1);
    p.convertTo(p, CV_32FC1);

  接着调用svm的方法predict;

    int response = (int)svm.predict(p);

  perdict方法返回的值是1的话,就表明是车牌,不然就不是;

    if (response == 1)
    {
        resultVec.push_back(inMat);
    }

  svm是类CvSVM的一个对象。这个类是opencv里内置的一个机器学习类。

    CvSVM svm;

  opencv的CvSVM的实现基于libsvm(具体信息能够看opencv的官方文档的介绍 )。

  libsvm是台湾大学林智仁(Lin Chih-Jen)教授写的一个世界知名的svm库(可能算是目前业界使用率最高的一个库)。官方主页地址是这里

  libsvm的实现基于SVM这个算法,90年代初由Vapnik等人提出。国内几篇较好的解释svm原理的博文:cnblog的LeftNotEasy(解释的易懂),pluskid的博文(专业有配图)。

  做为支持向量机的发明者,Vapnik是一位机器学习界极为重要的大牛。最近这位大牛也加入了Facebook

图4 SVM之父Vapnik

  svm的perdict方法的输入是待预测数据的特征,也称之为features。在这里,咱们输入的特征是图像所有的像素。因为svm要求输入的特征应该是一个向量,而Mat是与图像宽高对应的矩阵,所以在输入前咱们须要使用reshape(1,1)方法把矩阵拉伸成向量。除了所有像素之外,也能够有其余的特征,具体看第三部分“SVM调优”。
  predict方法的输出是float型的值,咱们须要把它转变为int型后再进行判断。若是是1表明就是车牌,不然不是。这个"1"的取值是由你在训练时输入的标签决定的。标签,又称之为label,表明某个数据的分类。若是你给 SVM模型输入一个车牌,并告诉它,这个图片的标签是5。那么你这边判断时所用的值就应该是5。
  以上就是svm模型判断的全过程。事实上,在你使用EasyPR的过程当中,这些所有都是透明的。你不须要转变图片格式,也不须要调用svm模型preditct方法,这些所有由EasyPR在内部调用。
  那么,咱们应该作什么?这里的关键在于CvSVM这个类。我在前面的机器学习论文中介绍过,机器学习过程的步骤就是首先你搜集大量的数据,而后把这些数据输入模型中训练,最后再把生成的模型拿出来使用。
  训练和预测两个过程是分开的。也就是说大家在使用EasyPR时用到的CvSVM类是我在先前就训练好的。我是如何把我训练好的模型交给各位使用的呢?CvSVM类有个方法,把训练好的结果以xml文件的形式存储,我就是把这个xml文件随EasyPR发布,并让程序在执行前先加载好这个xml。这个xml的位置就是在文件夹Model下面--svm.xml文件。

图5 model文件夹下的svm.xml

  若是看CPlateJudge的代码,在构造函数中调用了LoadModel()这个方法。

CPlateJudge::CPlateJudge()
{
    //cout << "CPlateJudge" << endl;
    m_path = "model/svm.xml";
    LoadModel();
}

  LoadModel()方法的主要任务就是装载model文件夹下svm.xml这个模型。

void CPlateJudge::LoadModel()
{
    svm.clear();
    svm.load(m_path.c_str(), "svm");
}

  若是你把这个xml文件换成其余的,那么你就能够改变EasyPR车牌判断的内核,从而实现你本身的车牌判断模块。
  后面的部分所有是告诉你如何有效地实现一个本身的模型(也就是svm.xml文件)。若是你对EasyPR的需求仅仅在应用层面,那么到目前的了解就足够了。若是你但愿可以改善EasyPR的效果,定制一个本身的车牌判断模块,那么请继续往下看。

二.SVM训练
  恭喜你!从如今开始起,你将真正踏入机器学习这个神秘而且充满未知的领域。至今为止,机器学习不少方法的背后原理都很是复杂,但众多的实践都证实了其有效性。与许多其余学科不一样,机器学习界更为关注的是最终方法的效果,也就是偏重以实践效果做为评判标准。所以很是适合从工程的角度入手,经过本身动手实践一个项目里来学习,而后再转入理论。这个过程已经被证实是有效的,本文的做者在开发EasyPR的时候,尚未任何机器学习的理论基础。后来的知识是将经过学习相关课程后获取的。
  简而言之,SVM训练部分的目标就是经过一批数据,而后生成一个表明咱们模型的xml文件。

  EasyPR中全部关于训练的方法均可以在svm_train.cpp中找到(1.0版位于train/code文件夹下,1.1版位于src/train文件夹下)。

  一个训练过程包含5个步骤,见下图:

图6 一个完整的SVM训练流程

  下面具体讲解一下这5个步骤,步骤后面的括号里表明的是这个步骤主要的输入与输出。
1. preprocss(原始数据->学习数据(未标签))

  预处理步骤主要处理的是原始数据到学习数据的转换过程。原始数据(raw data),表示你一开始拿到的数据。这些数据的状况是取决你具体的环境的,可能有各类问题。学习数据(learn data),是能够被输入到模型的数据。

  为了可以进入模型训练,必须将原始数据处理为学习数据,同时也可能进行了数据的筛选。比方说你有10000张原始图片,出于性能考虑,你只想用 1000张图片训练,那么你的预处理过程就是将这10000张处理为符合训练要求的1000张。你生成的1000张图片中应该包含两类数据:真正的车牌图片和不是车牌的图片。若是你想让你的模型可以区分这两种类型。你就必须给它输入这两类的数据。

  经过EasyPR的车牌定位模块PlateLocate能够生成大量的候选车牌图片,里面包括模型须要的车牌和非车牌图片。但这些候选车牌是没有通过分类的,也就是说没有标签。下步工做就是给这些数据贴上标签。
2. label (学习数据(未标签)->学习数据)

  训练过程的第二步就是将未贴标签的数据转化为贴过标签的学习数据。咱们所要作的工做只是将车牌图片放到一个文件夹里,非车牌图片放到另外一个文件夹里。在EasyPR里,这两个文件夹分别叫作HasPlate和NoPlate。若是你打开train/data/plate_detect_svm 后,你就会看到这两个压缩包,解压后就是打好标签的数据(1.1版本在同层learn data文件夹下面)。
  若是有人问我开发一个机器学习系统最耗时的步骤是哪一个,我会绝不犹豫的回答:“贴标签”。诚然,各位看到的压缩包里已经有打好标签的数据了。但各位可能不知道做者花在贴这些标签上的时间。粗略估计,整个EasyPR开发过程当中有70%的时间都在贴标签。SVM模型还好,只有两个类,训练数据仅有1000张。到了ANN模型那里,字符的类数有40多个,并且训练数据有4000张左右。那时候的贴标签过程,真是不堪回首的回忆,来回移动文件致使做者手常常性的很是酸。后来我一度想找个实习生帮我作这些工做。但转念一想,这些苦我都不肯承担,何苦还要那些小伙子承担呢。“己所不欲,勿施于人”。算了,既然这是机器学习者的命,那就欣然接受吧。幸亏在这段磨砺的时光,我逐渐掌握了一个方法,大幅度减小了我贴标签的时间与精力。否则,我可能还未开始写这个系列的教程,就已经累吐血了。开发EasyPR1.1版本时,新增了一大批数据,所以又有了贴标签的过程。幸亏使用这个方法,使得相关时间大幅度减小。这个方法叫作逐次迭代自动标签法。在后面会介绍这个方法。

  贴标签后的车牌数据以下图:

图7 在HasPlate文件夹下的图片

  贴标签后的非车牌数据下图:

图8 在NoPlate文件夹下的图片

  拥有了贴好标签的数据之后,下面的步骤是分组,也称之为divide过程。
3. divide (学习数据->分组数据)

  分组这个过程是EasyPR1.1版新引入的方法。

  在贴完标签之后,我拥有了车牌图片和非车牌图片共几千张。在我直接训练前,不急。先拿出30%的数据,只用剩下的70%数据进行SVM模型的训练,训练好的模型再用这30%数据进行一个效果测试。这30%数据充当的做用就是一个评判数据测试集,称之为test data,另70%数据称之为train data。因而一个完整的learn data被分为了train data和test data。

图9 数据分组过程

  在EasyPR1.0版是没有test data概念的,全部数据都输入训练,而后直接在原始的数据上进行测试。直接在原始的数据集上测试与单独划分出30%的数据测试效果究竟有多少不一样?

事实上,咱们训练出模型的根本目的是为了对未知的,新的数据进行预测与判断。

  当使用训练的数据进行测试时,因为模型已经考虑到了训练数据的特征,所以很难将这个测试效果推广到其余未知数据上。若是使用单独的测试集进行验证,因为测试数据集跟模型的生成没有关联,所以能够很好的反映出模型推广到其余场景下的效果。这个过程就能够简单描述为你不能够拿你给学生的复习提纲卷去考学生,而是应该出一份考察知识点同样,但题目不同的卷子。前者的方式没法区分出真正学会的人和死记硬背的人,然后者就能有效地反映出哪些人才是真正“学会”的。
  在divide的过程当中,注意不管在train data和test data中都要保持数据的标签,也就是说车牌数据仍然归到HasPlate文件夹,非车牌数据归到NoPlate文件夹。因而,车牌图片30%归到 test data下面的hasplate文件夹,70%归到train data下面的hasplate文件夹,非车牌图片30%归到test data下面的noplate文件夹,70%归到train data下面的noplate文件夹。因而在文件夹train 和 test下面又有两个子文件夹,他们的结构树就是下图:

图10 分组后的文件树

  divide数据结束之后,咱们就能够进入真正的机器学习过程。也就是对数据的训练过程。
4. train (训练数据->模型)

  模型在代码里的表明就是CvSVM类。在这一步中所要作的就是加载train data,而后用CvSVM类的train方法进行训练。这个步骤只针对的是上步中生成的总数据70%的训练数据。

  具体来讲,分为如下几个子步骤:
  1) 加载待训练的车牌数据。见下面这段代码。

复制代码

void getPlate(Mat& trainingImages, vector<int>& trainingLabels)
{

    char * filePath = "train/data/plate_detect_svm/HasPlate/HasPlate";
    vector<string> files;

    getFiles(filePath, files );

    int size = files.size();
    if (0 == size)
        cout << "No File Found in train HasPlate!" << endl;

    for (int i = 0;i < size;i++)
    {
        cout << files[i].c_str() << endl;
        Mat img = imread(files[i].c_str());

        img= img.reshape(1, 1);
                trainingImages.push_back(img);
                trainingLabels.push_back(1);
    }
}        

复制代码

  注意看,车牌图像我存储在的是一个vector<Mat>中,而标签数据我存储在的是一个vector<int>中。我将train/HasPlate中的图像依次取出来,存入vector<Mat>。每存入一个图像,同时也往 vector<int>中存入一个int值1,也就是说图像和标签分别存在不一样的vector对象里,可是保持一一对应的关系。
  2) 加载待训练的非车牌数据,见下面这段代码中的函数。基本内容与加载车牌数据相似,不一样之处在于文件夹是train/NoPlate,而且我往vector<int>中存入的是int值0,表明无车牌。

复制代码

void getNoPlate(Mat& trainingImages, vector<int>& trainingLabels)
{

    char * filePath = "train/data/plate_detect_svm/NoPlate/NoPlate";
    vector<string> files;

    getFiles(filePath, files );
    int size = files.size();
    if (0 == size)
        cout << "No File Found in train NoPlate!" << endl;

    for (int i = 0;i < size;i++)
    {
        cout << files[i].c_str() << endl;
        Mat img = imread(files[i].c_str());
        
        img= img.reshape(1, 1);
                trainingImages.push_back(img);
                trainingLabels.push_back(0);
    }
}

复制代码

  3) 将二者合并。目前拥有了两个vector<Mat>和两个vector<int>。将表明车牌图片和非车牌图片数据的两个 vector<Mat>组成一个新的Mat--trainingData,而表明车牌图片与非车牌图片标签的两个 vector<int>组成另外一个Mat--classes。接着作一些数据类型的调整,以让其符合svm训练函数train的要求。这些作完后,数据的准备工做基本结束,下面就是参数配置的工做。

复制代码

    Mat classes;//(numPlates+numNoPlates, 1, CV_32FC1);
    Mat trainingData;//(numPlates+numNoPlates, imageWidth*imageHeight, CV_32FC1 );

    Mat trainingImages;
    vector<int> trainingLabels;

    getPlate(trainingImages, trainingLabels);
    getNoPlate(trainingImages, trainingLabels);

    Mat(trainingImages).copyTo(trainingData);
    trainingData.convertTo(trainingData, CV_32FC1);
    Mat(trainingLabels).copyTo(classes);

复制代码

  4) 配置SVM模型的训练参数。SVM模型的训练须要一个CvSVMParams的对象,这个类是SVM模型中训练对象的参数的组合,如何给这里的参数赋值,是颇有讲究的一个工做。注意,这里是SVM训练的核心内容,也是最能体现一个机器学习专家和新手区别的地方。机器学习最后模型的效果差别有很大因素取决与模型训练时的参数,尤为是SVM,有很是多的参数供你配置(见下面的代码)。参数众可能是一个问题,更为显著的是,机器学习模型中参数的一点微调均可能带来最终结果的巨大差别。

复制代码

    CvSVMParams SVM_params;
    SVM_params.svm_type = CvSVM::C_SVC;
    SVM_params.kernel_type = CvSVM::LINEAR; //CvSVM::LINEAR;
    SVM_params.degree = 0;
    SVM_params.gamma = 1;
    SVM_params.coef0 = 0;
    SVM_params.C = 1;
    SVM_params.nu = 0;
    SVM_params.p = 0;
    SVM_params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER, 1000, 0.01);

复制代码

  opencv官网文档对CvSVMParams类的各个参数有一个详细的解释。若是你上过SVM课程的理论部分,你可能对这些参数的意思能搞的明白。但在这里,咱们能够不去管参数的含义,由于咱们有更好的方法去解决这个问题。

图11 SVM各参数的做用

  这个缘由在于:EasyPR1.0使用的是liner核,也称之为线型核,所以degree和gamma还有coef0三个参数没有做用。同时,在这里SVM模型用做的问题是分类问题,那么nu和p两个参数也没有影响。最后惟一能影响的参数只有Cvalue。到了EasyPR1.1版本之后,默认使用的是RBF核,所以须要调整的参数多了一个gamma。

  以上参数的选择均可以用自动训练(train_auto)的方法去解决,在下面的SVM调优部分会具体介绍train_auto。

  5) 开始训练。OK!数据载入完毕,参数配置结束,一切准备就绪,下面就是交给opencv的时间。咱们只要将前面的 trainingData,classes,以及CvSVMParams的对象SVM_params交给CvSVM类的train函数就能够。另外,直接使用CvSVM的构造函数,也能够完成训练过程。例以下面这行代码:

    CvSVM svm(trainingData, classes, Mat(), Mat(), SVM_params);

  训练开始后,慢慢等一会。机器学习中数据训练的计算量每每是很是大的,即使现代计算机也要运行很长时间。具体的时间取决于你训练的数据量的大小以及模型的复杂度。在个人2.0GHz的机器上,训练1000条数据的SVM模型的时间大约在1分钟左右。
  训练完成之后,咱们就能够用CvSVM类的对象svm去进行预测了。若是咱们仅仅须要这个模型,如今能够把它存到xml文件里,留待下次使用:

    FileStorage fsTo("train/svm.xml", cv::FileStorage::WRITE);
    svm.write(*fsTo, "svm");

  5. test (测试数据->评判指标)

  记得咱们还有30%的测试数据了么?如今是使用它们的时候了。将这些数据以及它们的标签加载如内存,这个过程与加载训练数据的过程是同样的。接着使用咱们训练好的SVM模型去判断这些图片。
  下面的步骤是对咱们的模型作指标评判的过程。首先,测试数据是有标签的数据,这意味着咱们知道每张图片是车牌仍是不是车牌。另外,用新生成的svm模型对数据进行判断,也会生成一个标签,叫作“预测标签”。“预测标签”与“标签”通常是存在偏差的,这也就是模型的偏差。这种偏差有两种状况:1.这副图片是真的车牌,可是svm模型判断它是“非车牌”;2.这幅图片不是车牌,但svm模型判断它是“车牌”。无疑,这两种状况都属于svm模型判断失误的状况。咱们须要设计出来两个指标,来分别评测这两种失误状况发生的几率。这两个指标就是下面要说的“准确率”(precision)和“查全率” (recall)。

  准确率是统计在我已经预测为车牌的图片中,真正车牌数据所占的比例。假设咱们用ptrue_rtrue表示预测(p)为车牌而且实际(r)为车牌的数量,而用ptrue_rfalse表示实际不为车牌的数量。

  准确率的计算公式是:

图12 precise 准确率

  查全率是统计真正的车牌图片中,我预测为车牌的图片所占的比例。同上,咱们用ptrue_rtrue表示预测与实际都为车牌的数量。用pfalse_rtrue表示实际为车牌,但我预测为非车牌的数量。

  查全率的计算公式是:

图13 recall 查全率

  recall的公式与precision公式惟一的区别在于右下角。precision是ptrue_rfalse,表明预测为车牌但实际不是的数量;而recall是pfalse_rtrue,表明预测是非车牌但实际上是车牌的数量。

  简单来讲,precision指标的指望含义就是要“查的准”,recall的指望含义就是“不要漏”。查全率还有一个翻译叫作“召回率”。但很明显,召回这个词没有反映出查全率所体现出的不要漏的含义。

  值得说明的是,precise和recall这两个值天然是越高越好。可是若是一个高,一个低的话效果会如何,如何跟两个都中等的状况进行比较?为了可以数字化这种比较。机器学习界又引入了FScore这个数值。当precise和recall二者中任一者较高,而另外一者较低是,FScore 都会较低。二者中等的状况下Fscore表现比一高一低要好。当二者都很高时,FScore会很高。

  FScore的计算公式以下图:

图14 Fscore计算公式

  模型测试以及评价指标是EasyPR1.1中新增的功能。在svm_train.cpp的最下面能够看到这三个指标的计算过程。

  训练心得

  经过以上5个步骤,咱们就完成了模型的准备,训练,测试的所有过程。下面,说一说过程当中的几点心得。
  1. 完善EasyPR的plateLocate功能

  在1.1版本中的EasyPR的车牌定位模块仍然不够完善。若是你的全部的图片符合某种通用的模式,参照前面的车牌定位的几篇教程,以及使用EasyPR新增的Debug模式,你能够将EasyPR的plateLocate模块改造为适合你的状况。因而,你就能够利用EasyPR为你制造大量的学习数据。经过原始数据的输入,而后经过plateLocate进行定位,再使用EasyPR已有的车牌判断模块进行图片的分类,因而你就能够获得一个基本分好类的学习数据。下面所须要作的就是人工核对,确认一下,保证每张图片的标签是正确的,而后再输入模型进行训练。

  2. 使用“逐次迭代自动标签法”。

  上面讨论的贴标签方法是在EasyPR已经提供了一个训练好的模型的状况下。若是一开始手上任何模型都没有,该怎么办?假设目前手里有成千上万个经过定位出来的各类候选车牌,手工一个个贴标签的话,岂不会让人累吐血?在前文中说过,我在一开始贴标签过程当中碰到了这个问题,在不断被折磨与痛苦中,我发现了一个好方法,大幅度减轻了这整个工做的痛苦性。

  固然,这个方法很简单。我若是说出来你必定也不以为有什么奇妙的。可是若是在你准备对1000张图片进行手工贴标签时,相信我,使用这个方法会让你最后的时间节省一半。若是你须要雇10我的来贴标签的话,那么用了这个方法,可能你最后一我的都不用雇。

  这个方法被我称为“逐次迭代自动标签法”。

  方法核心很简单。就是假设你有3000张未分类的图片。你从中选出1%,也就是30张出来,手工给它们每一个图片进行分类工做。好的,现在你有了 30张贴好标签的数据了,下步你把它直接输入到SVM模型中训练,得到了一个简单粗旷的模型。以后,你从图片集中再取出3%的图片,也就是90张,而后用刚训练好的模型对这些图片进行预测,根据预测结果将它们自动分到hasplate和noplate文件夹下面。分完之后,你到这两个文件夹下面,看看哪些是预测错的,把hasplate里预测错的移动到noplate里,反之,把noplate里预测错的移动到hasplate里。

  接着,你把一开始手工分类好的那30张图片,结合调整分类的90张图片,总共120张图片再输入svm模型中进行训练。因而你得到一个比最开始粗旷模型更精准点的模型。而后,你从3000张图片中再取出6%的图片来,用这个模型再对它们进行预测,分类....

  以上反复。你每训练出一个新模型,用它来预测后面更多的数据,而后自动分类。这样作最大的好处就是你只须要移动那些被分类错误的图片。其余的图片已经被正 确的归类了。注意,在整个过程当中,你每次只须要对新拿出的数据进行人工确认,由于前面的数据已经分好类了。所以,你最好使用两个文件夹,一个是已经分好类 的数据,另外一个是自动分类数据,须要手工确认的。这样二者不容易乱。

  每次从未标签的原始数据库中取出的数据不要多,最好不要超过上次数据的两倍。这样能够保证你的模型的准确率稳步上升。若是想一口吃个大胖子,例如用30张图片训练出的模型,去预测1000张数据,那最后结果跟你手工分类没有任何区别了。

  整个方法的原理很简单,就是不断迭代循环细化的思想。跟软件工程中迭代开发过程有殊途同归之妙。你只要理解了其原理,很容易就能够复用在任何其余机器学习模型的训练中,从而大幅度(或者部分)减轻机器学习过程当中贴标签的巨大负担。

  回到一个核心问题,对于开发者而言,什么样的方法才是本身实现一个svm.xml的最好方法。有如下几种选择。

  1.你使用EasyPR提供的svm.xml,这个方式等同于你没有训练,那么EasyPR识别的效率取决于你的环境与EasyPR的匹配度。运气好的话,这个效果也会不错。但若是你的环境下车牌跟EasyPR默认的不同。那么可能就会有点问题。

  2.使用EasyPR提供的训练数据,例如train/data文件下的数据,这样生成的效果等同于第一步的,不过你能够调整参数,试试看模型的表现会不会更好一点。

  3.使用本身的数据进行训练。这个方法的适应性最好。首先你得准备你原始的数据,而且写一个处理方法,可以将原始数据转化为学习数据。下面你调用EasyPR的PlateLocate方法进行处理,将候选车牌图片从原图片截取出来。你能够使用逐次迭代自动标签思想,使用EasyPR已有的svm 模型对这些候选图片进行预标签。而后再进行肉眼确认和手工调整,以生成标准的贴好标签的数据。后面的步骤就能够按照分组,训练,测试等过程顺次走下去。若是你使用了EasyPR1.1版本,后面的这几个过程已经帮你实现好代码了,你甚至能够直接在命令行选择操做。
  以上就是SVM模型训练的部分,经过这个步骤的学习,你知道如何经过已有的数据去训练出一个本身的模型。下面的部分,是对这个训练过程的一个思考,讨论经过何种方法能够改善我最后模型的效果。

三.SVM调优
  SVM调优部分,是经过对SVM的原理进行了解,并运用机器学习的一些调优策略进行优化的步骤。

  在这个部分里,最好要懂一点机器学习的知识。同时,本部分也会讲的尽可能通俗易懂,让人不会有理解上的负担。在EasyPR1.0版本中,SVM 模型的代码彻底参考了mastering opencv书里的实现思路。从1.1版本开始,EasyPR对车牌判断模块进行了优化,使得模型最后的效果有了较大的改善。
  具体说来,本部分主要包括以下几个子部分:1.RBF核;2.参数调优;3.特征提取;4.接口函数;5.自动化。

  下面分别对这几个子部分展开介绍。
1.RBF核
  SVM中最关键的技巧是核技巧。“核”实际上是一个函数,经过一些转换规则把低维的数据映射为高维的数据。在机器学习里,数据跟向量是等同的意思。例如,一个 [174, 72]表示人的身高与体重的数据就是一个两维的向量。在这里,维度表明的是向量的长度。(务必要区分“维度”这个词在不一样语境下的含义,有的时候咱们会说向量是一维的,矩阵是二维的,这种说法针对的是数据展开的层次。机器学习里讲的维度表明的是向量的长度,与前者不一样)

  简单来讲,低维空间到高维空间映射带来的好处就是能够利用高维空间的线型切割模拟低维空间的非线性分类效果。也就是说,SVM模型其实只能作线型分类,可是在线型分类前,它能够经过核技巧把数据映射到高维,而后在高维空间进行线型切割。高维空间的线型切割完后在低维空间中最后看到的效果就是划出了一条复杂的分线型分类界限。从这点来看,SVM并无完成真正的非线性分类,而是经过其它方式达到了相似目的,可谓“曲径通幽”。

  SVM模型总共能够支持多少种核呢。根据官方文档,支持的核类型有如下几种:

  1. liner核,也就是无核。
  2. rbf核,使用的是高斯函数做为核函数。
  3. poly核,使用多项式函数做为核函数。
  4. sigmoid核,使用sigmoid函数做为核函数。

  liner核和rbf核是全部核中应用最普遍的。

  liner核,虽然名称带核,但它实际上是无核模型,也就是没有使用核函数对数据进行转换。所以,它的分类效果仅仅比逻辑回归好一点。在EasyPR1.0版中,咱们的SVM模型应用的是liner核。咱们用的是图像的所有像素做为特征。

  rbf核,会将输入数据的特征维数进行一个维度转换,具体会转换为多少维?这个等于你输入的训练量。假设你有500张图片,rbf核会把每张图片的数据转 换为500维的。若是你有1000张图片,rbf核会把每幅图片的特征转到1000维。这么说来,随着你输入训练数据量的增加,数据的维数越多。更方便在高维空间下的分类效果,所以最后模型效果表现较好。

  既然选择SVM做为模型,并且SVM中核心的关键技巧是核函数,那么理应使用带核的函数模型,充分利用数据高维化的好处,利用高维的线型分类带来低维空间下的非线性分类效果。可是,rbf核的使用是须要条件的。

当你的数据量很大,可是每一个数据量的维度通常时,才适合用rbf核。相反,当你的数据量很少,可是每一个数据量的维数都很大时,适合用线型核。

在EasyPR1.0版中,咱们用的是图像的所有像素做为特征,那么根据车牌图像的136×36的大小来看的话,就是4896维的数据,再加上咱们输入的 是彩色图像,也就是说有R,G,B三个通道,那么数量还要乘以3,也就是14688个维度。这是一个很是庞大的数据量,你能够把每幅图片的数据理解为长度 为14688的向量。这个时候,每一个数据的维度很大,而数据的总数不多,若是用rbf核的话,相反效果反而不如无核。

  在EasyPR1.1版本时,输入训练的数据有3000张图片,每一个数据的特征改用直方统计,共有172个维度。这个场景下,若是用rbf核的话,就会将每一个数据的维度转化为与数据总数同样的数量,也就是3000的维度,能够充分利用数据高维化后的好处。

  所以能够看出,为了让EasyPR新版使用rbf核技巧,咱们给训练数据作了增长,扩充了两倍的数据,同时,减少了每一个数据的维度。以此知足了rbf核的使用条件。经过使用rbf核来训练,充分发挥了非线性模型分类的优点,所以带来了较好的分类效果。  

  可是,使用rbf核也有一个问题,那就是参数设置的问题。在rbf训练的过程当中,参数的选择会显著的影响最后rbf核训练出模型的效果。所以必须对参数进行最优选择。

2.参数调优

  传统的参数调优方法是人手完成的。机器学习工程师观察训练出的模型与参数的对应关系,不断调整,寻找最优的参数。因为机器学习工程师大部分时间在调整模型的参数,也有了“机器学习就是调参”这个说法。

  幸亏,opencv的svm方法中提供了一个自动训练的方法。也就是由opencv帮你,不断改变参数,训练模型,测试模型,最后选择模型效果最好的那些参数。整个过程是全自动的,彻底不须要你参与,你只须要输入你须要调整参数的参数类型,以及每次参数调整的步长便可。

  如今有个问题,如何验证svm参数的效果?你可能会说,使用训练集之外的那30%测试集啊。但事实上,机器学习模型中专门有一个数据集,是用来验证参数效果的。也就是交叉验证集(cross validation set,简称validate data) 这个概念。

validate data就是专门从train data中取出一部分数据,用这部分数据来验证参数调整的效果。比方说如今有70%的训练数据,从中取出20%的数据,剩下50%数据用来训练,再用训练出来的模型在20%数据上进行测试。这20%的数据就叫作validate data。真正拿来训练的数据仅仅只是50%的数据。
  正如上面把数据划分为test data和train data的理由同样。为了验证参数在新数据上的推广性,咱们不能用一个训练数据集,因此咱们要把训练数据集再细分为train data和validate data。在train data上训练,而后在validate data上测试参数的效果。因此说,在一个更通常的机器学习场景中,机器学习工程师会把数据分为train data,validate data,以及test data。在train data上训练模型,用validate data测试参数,最后用test data测试模型和参数的总体表现。
  说了这么多,那么,你们可能要问,是否是还缺乏一个数据集,须要再划分出来一个validate data吧。可是答案是No。opencv的train_auto函数帮你完成了全部工做,你只须要告诉它,你须要划分多少个子分组,以及validate data所占的比例。而后train_auto函数会自动帮你从你输入的train data中划分出一部分的validate data,而后自动测试,选择表现效果最好的参数。

  感谢train_auto函数!既帮咱们划分了参数验证的数据集,还帮咱们一步步调整参数,最后选择效果最好的那个参数,可谓是节省了调优过程当中80%的工做。

  train_auto函数的调用代码以下:

复制代码

    svm.train_auto(trainingData, classes, Mat(), Mat(), SVM_params, 10, 
                CvSVM::get_default_grid(CvSVM::C),
                CvSVM::get_default_grid(CvSVM::GAMMA), 
                CvSVM::get_default_grid(CvSVM::P), 
                CvSVM::get_default_grid(CvSVM::NU), 
                CvSVM::get_default_grid(CvSVM::COEF),
                CvSVM::get_default_grid(CvSVM::DEGREE),
                true);

复制代码

  你惟一须要作的就是泡杯茶,翻翻书,而后慢慢等待这计算机帮你处理好全部事情(时间较长,由于每次调整参数又得从新训练一次)。做者最近的一次训练的耗时为1个半小时)。

  训练完毕后,看看模型和参数在test data上的表现把。99%的precise和98%的recall。很是棒,比任何一次手工配的效果都好。
3.特征提取
  在rbf核介绍时提到过,输入数据的特征的维度如今是172,那么这个数字是如何计算出来的?如今的特征用的是直方统计函数,也就是先把图像二值化,而后统计图像中一行元素中1的数目,因为输入图像有36行,所以有36个值,再统计图像中每一列中1的数目,图像有136列,所以有136个值,二者相加正好等于172。新的输入数据的特征提取函数就是下面的代码:

复制代码

// ! EasyPR的getFeatures回调函数
// !本函数是获取垂直和水平的直方图图值
void getHistogramFeatures(const Mat& image, Mat& features)
{
    Mat grayImage;
    cvtColor(image, grayImage, CV_RGB2GRAY);
    Mat img_threshold;
    threshold(grayImage, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
    features = getTheFeatures(img_threshold);
}

复制代码

  咱们输入数据的特征再也不是所有的三原色的像素值了,而是抽取过的一些特征。从原始的图像到抽取后的特征的过程就被称为特征提取的过程。在1.0版中没有特征提取的概念,是直接把图像中所有像素做为特征的。这要感谢群里的“若是有一天”同窗,他坚持认为所有像素的输入是最低级的作法,认为用特征提取后的效果会好多。我问大概能到多少准确率,当时的准确率有92%,我觉得已经很高了,结果他说能到99%。在半信半疑中我尝试了,果然如他所说,结合了rbf核与新特征训练的模型达到的precise在99%左右,并且recall也有98%,这真是个使人咋舌而且很是惊喜的成绩。

  “若是有一天”建议用的是SFIT特征提取或者HOG特征提取,因为时间缘由,这二者我没有实现,可是把函数留在了那里。留待之后有时间完成。在这个过程当中,我充分体会到了开源的力量,若是不是把软件开源,若是不是有这么多优秀的你们一块儿讨论,这样的思路与改善是不可能出现的。

4.接口函数

  因为有SIFT以及HOG等特征没有实现,并且将来有可能会有更多有效的特征函数出现。所以我把特征函数抽象为借口。使用回调函数的思路实现。全部回调函数的代码都在feature.cpp中,开发者能够实现本身的回调函数,并把它赋值给EasyPR中的某个函数指针,从而实现自定义的特征提取。也许大家会有更多更好的特征的想法与创意。

  关于特征其实有更多的思考,原始的SVM模型的输入是图像的所有像素,正如人类在小时候经过图像识别各类事物的过程。后来SVM模型的输入是通过抽取的特 征。正如随着人类接触的事物愈来愈多,会发现单凭图像愈来愈难区分一些很是类似的东西,因而学会了总结特征。例如太阳就是圆的,黄色,在天空等,能够凭借 这些特征就进行区分和判断。

从本质上说,特征是区分事物的关键特性。这些特性,必定是从某些维度去看待的。例如,苹果和梨子,一个是绿色,一个是黄色,这就是颜色的维度;鱼和鸟,一个在水里,一个在空中,这是位置的区分,也就是空间的维度。特征,是许多维度中最有区分意义的维度。传统数据仓库中的OLAP,也称为多维分析,提供了人类从多个维度观察,比较的能力。经过人类的观察比较,从多个维度中挑选出来的维度,就是要分析目标的特征。从这点来看,机器学习与多维分析有了关联。多维分析提供了选择特征的能力。而机器学习能够根据这些特征进行建模。

  机器学习界也有不少算法,专门是用来从数据中抽取特征信息的。例如传统的PCA(主成分分析)算法以及最近流行的深度学习中的 AutoEncoder(自动编码机)技术。这些算法的主要功能就是在数据中学习出最可以明显区分数据的特征,从而提高后续的机器学习分类算法的效果。

  说一个特征学习的案例。做者买车时,常常会把大众的两款车--迈腾与帕萨特给弄混,由于二者实在太像了。你们能够到网上去搜一下这两车的图片。若是不依赖后排的文字,光靠外形实在难以将两车区分开来(虽然从生产商来讲,前者是一汽大众生产的,产地在长春,后者是上海大众生产的,产地在上海。两个不一样的公司,南北两个地方,相差了十万八千里)。后来我经过仔细观察,终于发现了一个明显区分两辆车的特征,后来我再也没有认错过。这个特征就是:迈腾的前脸有四条银杠,而帕萨特只有三条,迈腾比帕萨特多一条银杠。能够这么说,就是这么一条银杠,分割了北和南两个地方生产的汽车。


图15 一条银杠,分割了“北”和“南”

  在这里区分的过程,我是经过不断学习与研究才发现了这些区分的特征,这充分说明了事物的特征也是能够被学习的。若是让机器学习中的特征选择方法 PCA和AutoEncoder来分析的话,按理来讲它们也应该找出这条银杠,不然它们就没法作到对这两类最有效的分类与判断。若是没有找到的话,证实咱们目前的特征选择算法还有不少的改进空间(与这个案例相似的还有大众的另两款车,高尔夫和Polo。它们两的区分也是用一样的道理。相比迈腾和帕萨特,高尔夫和Polo价格差异的更大,因此区分的特征也更有价值)。

  5.自动化
  最后我想简单谈一下EasyPR1.1新增的自动化训练功能与命令行。你们可能看到第二部分介绍SVM训练时我将过程分红了5个步骤。事实上,这些步骤中的不少过程是能够模块化的。一开始的时候我写一些不相关的代码函数,帮我处理各类须要解决的问题,例如数据的分组,打标签等等。但后来,我把思路理清后,我以为这几个步骤中不少的代码均可以通用。因而我把一些步骤模块化出来,造成通用的函数,并写了一个命令行界面去调用它们。在你运行EasyPR1.1版后,在你看到的第一个命令行界面选择“3.SVM训练过程”,你就能够看到这些所有的命令。

图16 svm训练命令行

  这里的命令主要有6个部分。第一个部分是最可能须要修改代码的地方,由于每一个人的原始数据(raw data)都是不同的,所以你须要在data_prepare.cpp中找到这个函数,改写成适应你格式的代码。接下来的第二个部分之后的功能基本均可以复用。例如自动贴标签(注意贴完之后要人工核对一下)。
  第三个到第六部分功能相似。若是你的数据还没分组,那么你执行3之后,系统自动帮你分组,而后训练,再测试验证。第四个命令行省略了分组过程。第五个命令行部分省略训练过程。第六个命令行省略了前面全部过程,只作最后模型的测试部分。

  让咱们回顾一下SVM调优的五个思路。第一部分是rbf核,也就是模型选择层次,根据你的实际环境选择最合适的模型。第二部分是参数调优,也就是参数优化层次,这部分的参数最好经过一个验证集来确认,也能够使用opencv自带的train_auto函数。第三部分是特征抽取部分,也就是特征甄选们,要能选择出最能反映数据本质区别的特征来。在这方面,pca以及深度学习技术中的autoencoder可能都会有所帮助。第四部分是通用接口部分,为了给优化留下空间,须要抽象出接口,方便后续的改进与对比。第五部分是自动化部分,为了节省时间,将大量能够自动化处理的功能模块化出来,而后提供一些方便的操做界面。前三部分是从机器学习的效果来提升,后两部分是从软件工程的层面去优化。

  总结起来,就是模型,参数,特征,接口,模块五个层面。经过这五个层面,能够有效的提升机器学习模型训练的效果与速度,从而下降机器学习工程实施的难度与提高相关的效率。当须要对机器学习模型进行调优的时候,咱们能够从这五个层面去考虑。

  后记

  讲到这里,本次的SVM开发详解也算是结束了。相信经过这篇文档,以及做者的一些心得,会对你在SVM模型的开发上面带来一些帮助。下面的工做能够考虑把这些相关的方法与思路运用到其余领域,或着改善EasyPR目前已有的模型与算法。若是你找出了比目前更好实现的思路,而且你愿意跟咱们分享,那咱们是很是欢迎的。  EasyPR1.1的版本发生了较大的变化。我为了让它拥有多人协做,众包开发的能力,想过不少办法。最后决定引入了GDTS(General Data Test Set,通用测试数据集,也就是新的image/general_test下的众多车牌图片)以及GDSL(General Data Share License,通用数据分享协议,image/GDSL.txt)。这些概念与协议的引入很是重要,可能会改变目前车牌识别与机器学习在国内学习研究的格局。在下期的EasyPR开发详解中我会重点介绍1.1版的新加入功能以及这两个概念和背后的思想,欢迎继续阅读。  上一篇仍是第四篇,为何本期SVM开发详解属于EasyPR开发的第六篇?事实上,对于目前的车牌定位模块咱们团队以为还有改进空间,因此第五篇的详解内容是留给改进后的车牌定位模块的。若是有车牌定位模块方面好的建议或者但愿加入开源团队,欢迎跟咱们团队联系(easypr_dev@163.com )。您也能够为中国的开源事业作出一份贡献

相关文章
相关标签/搜索