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 )。您也能夠爲中國的開源事業作出一份貢獻

相關文章
相關標籤/搜索