天天看點

EasyPR--開發詳解

我正在做一個開源的中文車牌識别系統,Git位址為:https://github.com/liuruoze/EasyPR。

  我給它取的名字為EasyPR,也就是Easy to do Plate Recognition的意思。我開發這套系統的主要原因是因為我希望能夠鍛煉我在這方面的能力,包括C++技術、計算機圖形學、機器學習等。我把這個項目開源的主要目的是:1.它基于開源的代碼誕生,理應回歸開源;2.我希望有人能夠一起協助強化這套系統,包括代碼、訓練資料等,能夠讓這套系統的準确性更高,魯棒性更強等等。

  相比于其他的車牌識别系統,EasyPR有如下特點:

  1. 它基于openCV這個開源庫,這意味着所有它的代碼都可以輕易的擷取。
  2. 它能夠識别中文,例如車牌為蘇EUK722的圖檔,它可以準确地輸出std:string類型的"蘇EUK722"的結果。
  3. 它的識别率較高。目前情況下,字元識别已經可以達到90%以上的精度。

  系統還提供全套的訓練資料提供(包括車牌檢測的近500個車牌和字元識别的4000多個字元)。所有全部都可以在Github的項目位址上直接下載下傳到。

那麼,EasyPR是如何産生的呢?我簡單介紹一下它的誕生過程:

  首先,在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的處理流程:

EasyPR--開發詳解

本開源項目的目标客戶群有三類:

  1. 需要開發一個車牌識别系統的(開發者)。
  2. 需要車牌系統去識别車牌的(使用者)。
  3. 急于做畢業設計的(學生)。

  第一類客戶是本項目的主要使用者,是以項目特地被精心劃分為了6個子產品,以供開發者按需選擇。

  第二類客戶可能會有部分,EasyPR有一個同級項目EasyPR_Dll,可以DLL方式嵌入到其他的程式中,另外還有個一個同級項目EasyPR_Win,基于WTL開發的界面程式,可以簡化與幫助車牌識别的結果比對過程。

  對于第三類客戶,可以這麼說,有完整的全套代碼和詳細的說明,我相信你們可以稍作修改就可以通過設計大考。

推薦你使用EasyPR有以下幾點理由:

  • 這裡面的代碼都是作者親自優化過的,你可以在上面做修改,做優化,甚至一起協作開發,一些處理車牌的細節方法你應該是感興趣的。
  • 如果你對代碼不感興趣,那麼經過作者精心訓練的模型,包括SVM和ANN的模型,可以幫助你提升或驗證你程式的正确率。
  • 如果你對模型也不感興趣,那麼成百上千經過作者親自挑選的訓練資料生成的檔案,你應該感興趣。作者花了大量的時間處理這些訓練資料與調整,現在直接提供給你,可以大幅度減輕很多人缺少資料的難題。

  有興趣的同志可以留言或發Email:[email protected] 或者直接在Git上發起pull requet,都可以,未來我會在cnblogs上釋出更多的關于系統的介紹,包括編碼過程,訓練心得。

在上篇文檔中作者已經簡單的介紹了EasyPR,現在在本文檔中詳細的介紹EasyPR的開發過程。

  正如淘寶誕生于一個購買來的LAMP系統,EasyPR也有它誕生的原型,起源于CSDN的taotao1233的一個部落格,部落客以讀書筆記的形式記述了通過閱讀“Mastering OpenCV”這本書完成的一個車牌系統的雛形。

  這個雛形有幾個特點:1.将車牌系統劃分為了兩個過程,即車牌檢測和字元識别。2.整個系統是針對西班牙的車牌開發的,與中文車牌不同。3.系統的訓練模型來自于原書。作者基于這個系統,誕生了開發一個适用于中文的,且适合與協作開發的開源車牌系統的想法,也就是EasyPR。

  當然了,現在車牌系統滿大街都是,随便上下百度首頁都是大量的廣告,一些甚至宣稱自己實作了99%的識别率。那麼,作者為什麼還要開發這個系統呢?這主要是基于時勢與機遇的原因。

衆所皆知,現在是大資料的時代。那麼,什麼是大資料?可能有些人認為這個隻是一個概念或着炒作。但是大資料确是實實在在有着基礎理論與科學研究背景的一門技術,其中包含着分布式計算、記憶體計算、機器學習、計算機視覺、語音識别、自然語言處理等衆多計算機界嶄新的技術,而且是這些技術綜合的産物。事實上,大資料的“大”包含着4個特征,即4V理念,包括Volume(體量)、Varity(多樣性)、Velocity(速度)、Value(價值)。

  見下圖的說明:

EasyPR--開發詳解

圖1 大資料技術的4V特征

  綜上,大資料技術不僅包含資料量的大,也包含處理資料的複雜,和處理資料的速度,以及資料中蘊含的價值。而車牌識别這個系統,雖然傳統,古老,卻是包含了所有這四個特偵的一個大資料技術的縮影。

  在車牌識别中,你需要處理的資料是圖像中海量的像素單元;你處理的資料不再是傳統的結構化資料,而是圖像這種複雜的資料;如果不能在很短的時間内識别出車牌,那麼系統就缺少意義;雖然一副圖像中有很多的資訊,但可能僅僅隻有那一小塊的資訊(車牌)以及車身的顔色是你關心,而且這些資訊都蘊含着巨大的價值。也就是說,車牌識别系統事實上就是現在火熱的大資料技術在某個領域的一個聚焦,通過了解車牌識别系統,可以很好的幫助你了解大資料技術的内涵,也能清楚的認識到大資料的價值。

  很神奇吧,也許你覺得車牌識别系統很低端,這不是随便大街上都有的麼,而你又認為大資料技術很高端,似乎高大上的感覺。其實兩者本質上是一樣的。另外對于覺得大資料技術是虛幻的炒作念頭的同學,你們也可以了解一下車牌識别系統,就能知道大資料落在實地,事實上已經不知不覺進入我們的生活很長時間了,像一些其他的如搶票系統,語音助手等,都是大資料技術的真真切切的展現。所謂再虛幻的概念落到實處,就成了下裡巴人,應該就是這個意思。是以對于炒概念要有所警覺,但是不能是以排除一切,要了解具體的技術内涵,才能更好的利用技術為我們服務。

  除了幫忙我們更好的了解大資料技術,使我們跟的上時代,開發一個車牌系統還有其他原因。

  那就是、現在的車牌系統,仍然還有許多待解決的挑戰。這個可能很多同學有疑問,你别騙我,百度上我随便一搜都是99%,隻要多少多少元,就可以99%。但是事實上,車牌識别系統業界一直都沒有一個成熟的百分百适用的方案。一些90%以上的車牌識别系統都是跟高清錄影機做了內建,由攝像頭傳入的高分辨率圖檔進入識别系統,可以達到較高的識别率。但是如果圖像分辨率一旦下來,或者圖裡的車牌髒了的話,那麼很遺憾,識别率遠遠不如我們的肉眼。也就是說,距離真正的智能的車牌識别系統,目前已有的系統還有許多挑戰。什麼時候能夠達到人眼的精度以及識别速率,估計那時候才算是完整成熟的。

  那麼,有同學問,就沒有辦法進一步優化了麼。答案是有的,這個就需要談到目前火熱的深度學習與計算機視覺技術,使用多隐層的深度神經網絡也許能夠解決這個問題。但是目前EasyPR并沒有采用這種技術,或許以後會采用。但是這個方向是有的。也就是說,通過研究車牌識别系統,也許會讓你一領略當今人工智能與計算機視覺技術最尖端的研究方向,即深度學習技術。怎麼樣,聽了是不是很心動?最後扯一下,前端時間非常火熱Google大腦技術和百度深度學習研究院,都是跟深度學習相關的。

  下圖是一個深度學習(右)與傳統技術(左)的對比,可以看出深度學習對于資料的分類能力的優勢。

EasyPR--開發詳解

圖2 深度學習(右)與PCA技術(左)的對比

  總結一下:開發一個車牌識别系統可以讓你了解最新的時勢---大資料的内涵,同時,也有機遇讓你了解最新的人工智能技術---深度學習。是以,不要輕易的小看這門技術中蘊含的價值。

  好,談價值就說這麼多。現在,我簡單的介紹一下EasyPR的具體過程。

  在上一篇文檔中,我們了解到EasyPR包括兩個部分,但實際上為了更好進行子產品化開發,EasyPR被劃分成了六個子產品,其中每個子產品的準确率與速度都影響着整個系統。

  具體說來,EasyPR中PlateDetect與CharsRecognize各包括三個子產品。

  PlateDetect包括的是車牌定位,SVM訓練,車牌判斷三個過程,見下圖。

EasyPR--開發詳解

圖3 PlateDetect過程詳解 

  通過PlateDetect過程我們獲得了許多可能是車牌的圖塊,将這些圖塊進行手工分類,聚集一定數量後,放入SVM模型中訓練,得到SVM的一個判斷模型,在實際的車牌過程中,我們再把所有可能是車牌的圖塊輸入SVM判斷模型,通過SVM模型自動的選擇出實際上真正是車牌的圖塊。

  PlateDetect過程結束後,我們獲得一個圖檔中我們真正關心的部分--車牌。那麼下一步該如何處理呢。下一步就是根據這個車牌圖檔,生成一個車牌号字元串的過程,也就是CharsRecognisze的過程。

  CharsRecognise包括的是字元分割,ANN訓練,字元識别三個過程,具體見下圖。

EasyPR--開發詳解

圖4 CharsRecognise過程詳解

  在CharsRecognise過程中,一副車牌圖塊首先會進行灰階化,二值化,然後使用一系列算法擷取到車牌的每個字元的分割圖塊。獲得海量的這些字元圖塊後,進行手工分類(這個步驟非常耗時間,後面會介紹如何加速這個處理的方法),然後喂入神經網絡(ANN)的MLP模型中,進行訓練。在實際的車牌識别過程中,将得到7個字元圖塊放入訓練好的神經網絡模型,通過模型來預測每個圖塊所表示的具體字元,例如圖檔中就輸出了“蘇EUK722”,(這個車牌隻是示例,切勿以為這個車牌有什麼特定選取目标。車主既不是作者,也不是什麼深仇大恨,僅僅為學術說明選擇而已)。

  至此一個完整的車牌識别過程就結束了,但是在每一步的處理過程中,有許多的優化方法和處理政策。尤其是車牌定位和字元分割這兩塊,非常重要,它們不僅生成實際資料,還生成訓練資料,是以會直接影響到模型的準确性,以及模型判斷的最終結果。這兩部分會是作者重點介紹的子產品,至于SVM模型與ANN模型,由于使用的是OpenCV提供的類,是以可以直接看openCV的源碼或者機器學習介紹的書,來了解訓練與判斷過程。

  好了,本期就介紹這麼多。下面的篇章中作者會重點介紹其中每個子產品的開發過程與内容,但是時間不定,可能幾個星期發一篇吧。

  最後,祝大家國慶快樂,阖家幸福!

這篇文章是一個系列中的第三篇。前兩篇的位址貼下:介紹、詳解1。我撰寫這系列文章的目的是:1、普及車牌識别中相關的技術與知識點;2、幫助開發者了解EasyPR的實作細節;3、增進溝通。

  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”的作用與重要性之前,請看下面這兩幅圖檔。

EasyPR--開發詳解
EasyPR--開發詳解

圖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類的聲明如下:

EasyPR--開發詳解
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中的類都聲明在命名空間easypr内,這裡沒有列出。CPlateLocate中最核心的方法是plateLocate方法。它的聲明如下:

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

  方法有兩個參數,第一個參數代表輸入的源圖像,第二個參數是輸出數組,代表所有檢索到的車牌圖塊。傳回值為int型,0代表成功,其他代表失敗。plateLocate内部是如何實作的,讓我們再深入下看看。

2.“Plate Locate”的實作過程

  plateLocate過程基本參考了taotao1233的部落格的處理流程,但略有不同。

  plateLocate的總體識别思路是:如果我們的車牌沒有大的旋轉或變形,那麼其中必然包括很多垂直邊緣(這些垂直邊緣往往緣由車牌中的字元),如果能夠找到一個包含很多垂直邊緣的矩形塊,那麼有很大的可能性它就是車牌。

  依照這個思路我們可以設計一個車牌定位的流程。設計好後,再根據實際效果進行調優。下面的流程是經過多次調整與嘗試後得出的,包含了數月來作者針對測試圖檔集的一個最佳過程(這個流程并不一定适用所有情況)。plateLocate的實作代碼在這裡不貼了,Git上有所有源碼。plateLocate主要處理流程圖如下:

EasyPR--開發詳解

圖2 plateLocate流程圖

  下面會一步一步參照上面的流程圖,給出每個步驟的中間臨時圖檔。這些圖檔可以在1.01版的CPlateLocate中設定如下代碼開啟調試模式。

CPlateLocate plate;
    plate.setDebug(1);      

  臨時圖檔會生成在tmp檔案夾下。對多個車牌圖檔處理的結果僅會保留最後一個車牌圖檔的臨時圖檔。

  1、原始圖檔。

EasyPR--開發詳解

  2、經過高斯模糊後的圖檔。經過這步處理,可以看出圖像變的模糊了。這步的作用是為接下來的Sobel算子去除幹擾的噪聲。

EasyPR--開發詳解

  3、将圖像進行灰階化。這個步驟是一個分水嶺,意味着後面的所有操作都不能基于色彩資訊了。此步驟是利是弊,後面再做分析。

EasyPR--開發詳解

  4、對圖像進行Sobel運算,得到的是圖像的一階水準方向導數。這步過後,車牌被明顯的區分出來。

EasyPR--開發詳解

  5、對圖像進行二值化。将灰階圖像(每個像素點有256個取值可能)轉化為二值圖像(每個像素點僅有1和0兩個取值可能)。

EasyPR--開發詳解

  6、使用閉操作。對圖像進行閉操作以後,可以看到車牌區域被連接配接成一個矩形裝的區域。

EasyPR--開發詳解

  7、求輪廓。求出圖中所有的輪廓。這個算法會把全圖的輪廓都計算出來,是以要進行篩選。

EasyPR--開發詳解

  8、篩選。對輪廓求最小外接矩形,然後驗證,不滿足條件的淘汰。經過這步,僅僅隻有六個黃色邊框的矩形通過了篩選。

EasyPR--開發詳解

  8、角度判斷與旋轉。把傾斜角度大于門檻值(如正負30度)的矩形舍棄。左邊第一、二、四個矩形被舍棄了。餘下的矩形進行微小的旋轉,使其水準。

EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解

  10、統一尺寸。上步得到的圖塊尺寸是不一樣的。為了進入機器學習模型,需要統一尺寸。統一尺寸的标準寬度是136,長度是36。這個标準是對千個測試車牌平均後得出的通用值。下圖為最終的三個候選”車牌“圖塊。

EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解

  這些“車牌”有兩個作用:一、積累下來作為支援向量機(SVM)模型的訓練集,以此訓練出一個車牌判斷模型;二、在實際的車牌檢測過程中,将這些候選“車牌”交由訓練好的車牌判斷模型進行判斷。如果車牌判斷模型認為這是車牌的話就進入下一步即字元識别過程,如果不是,則舍棄。

3.“Plate Locate”的深入讨論與調優政策

  好了,說了這麼多,讀者想必對整個“Plate Locate”過程已經有了一個完整的認識。那麼讓我們一步步稽核一下處理流程中的每一個步驟。回答下面三個問題:這個步驟的作用是什麼?省略這步或者替換這步可不可以?這個步驟中是否有參數可以調優的?通過這幾個問題可以幫助我們更好的了解車牌定位功能,并且便于自己做修改、定制。

  由于篇幅關系,下面的深入讨論放在下期

在上篇文章中我們了解了PlateLocate的過程中的所有步驟。在本篇文章中我們對前3個步驟,分别是高斯模糊、灰階化和Sobel算子進行分析。

一、高斯模糊

1.目标

  對圖像去噪,為邊緣檢測算法做準備。  

2.效果

  在我們的車牌定位中的第一步就是高斯模糊處理。

EasyPR--開發詳解

圖1 高斯模糊效果

3.理論

  詳細說明可以看這篇:阮一峰講高斯模糊。

  高斯模糊是非常有名的一種圖像處理技術。顧名思義,其一般應用是将圖像變得模糊,但同時高斯模糊也應用在圖像的預處理階段。了解高斯模糊前,先看一下平均模糊算法。平均模糊的算法非常簡單。見下圖,每一個像素的值都取周圍所有像素(共8個)的平均值。

EasyPR--開發詳解

圖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過程在閉操作時就和原來發生了變化。最後結果如下。

EasyPR--開發詳解

圖3 不采用高斯模糊後的結果  

  可以看出,車牌所在的矩形産生了偏斜。最後得到的候選“車牌”圖塊如下:

EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解

圖4 不采用高斯模糊後的“車牌”圖塊

  如果不使用高斯模糊而直接用邊緣檢測算法,我們得到的候選“車牌”達到了8個!這樣不僅會增加車牌判斷的處理時間,還增加了判斷出錯的機率。由于得到的車牌圖塊中車牌是斜着的,如果我們的字元識别算法需要一個水準的車牌圖塊,那麼幾乎肯定我們會無法得到正确的字元識别效果。

  高斯模糊中的半徑也會給結果帶來明顯的變化。有的圖檔,高斯模糊半徑過高了,車牌就定位不出來。有的圖檔,高斯模糊半徑偏低了,車牌也定位不出來。是以、高斯模糊的半徑既不宜過高,也不能過低。CPlateLocate類中的值為5的靜态常量DEFAULT_GAUSSIANBLUR_SIZE,标示着推薦的高斯模糊的半徑。這個值是對于近千張圖檔經過測試後得出的綜合定位率最高的一個值。在CPlateLocate類的構造函數中,m_GaussianBlurSize被賦予了DEFAULT_GAUSSIANBLUR_SIZE的值,是以,預設的高斯模糊的半徑就是5。如果不是特殊情況,不需要修改它。

  在數次的實驗以後,必須承認,保留高斯模糊過程與半徑值為5是最佳的實踐。為應對特殊需求,在CPlateLocate類中也應該提供了方法修改高斯半徑的值,調用代碼(假設需要一個為3的高斯模糊半徑)如下:

CPlateLocate plate;
    plate.setGaussianBlurSize(3);      

  目前EasyPR的處理步驟是先進行高斯模糊,再進行灰階化。從目前的實驗結果來看,基于色彩的高斯模糊過程比灰階後的高斯模糊過程更容易檢測到邊緣點。

二、灰階化處理

1.目标

  為邊緣檢測算法準備灰階化環境。

2.效果

灰階化的效果如下。

EasyPR--開發詳解

圖5 灰階化效果

 3.理論

  在灰階化處理步驟中,争議最大的就是資訊的損失。無疑的,原先plateLocate過程面對的圖檔是彩色圖檔,而從這一步以後,就會面對的是灰階圖檔。在前面,已經說過這步驟是利是弊是需要讨論的。

   無疑,對于計算機而言,色彩圖像相對于灰階圖像難處理多了,很多圖像處理算法僅僅隻适用于灰階圖像,例如後面提到的Sobel算子。在這種情況下,你除 了把圖檔轉成灰階圖像再進行處理别無它法,除非重新設計算法。但另一方面,轉化成灰階圖像後恰恰失去了最豐富的細節。要知道,真實世界是彩色的,人類對于 事物的辨識是基于彩色的架構。甚至可以這樣說,因為我們的肉眼能夠差別彩色,是以我們對于事物的區分,辨識,記憶的能力就非常的強。

  車牌定位環節中去掉彩色的利弊也是同理。轉換成灰階圖像雖然利于使用各種專用的算法,但失去了真實世界中辨識的最重要工具---色彩的區分。舉個簡單的例子,人怎麼在一張圖檔中找到車牌?非常簡單,一眼望去,一個合适大小的矩形,藍色的、或者黃色的、或者其他顔色的在另一個黑色,或者白色的大的跟車形類似的矩形中。這個過程非常直覺,明顯,而且可以排除模糊,色澤,不清楚等很多影響。如果使用灰階圖像,就必須借助水準,垂直求導等方法。

  未來如果PlateLocate過程可以使用顔色來判斷,可能會比現在的定位更清楚、準确。但這需要研究與實驗過程,在EasyPR的未來版本中可能會實作。但無疑,使用色彩判斷是一種趨勢,因為它不僅符合人眼識别的規律,更趨近于人工智能的本質,而且它更準确,速度更快。

4.實踐

  在PlateLocate過程中是這樣調用灰階化的。

cvtColor( src_blur, src_gray, CV_RGB2GRAY );      

  這裡給出了opencv的灰階化的API(英文,2.48以上版本)。

三.Sobel算子

1.目标

  檢測圖像中的垂直邊緣,便于區分車牌。

 2.效果

下圖是Sobel算子的效果。

EasyPR--開發詳解

圖6 Sobel效果

3.理論

  如果要說哪個步驟是plateLocate中的核心與靈魂,毫無疑問是Sobel算子。沒有Sobel算子,也就沒有垂直邊緣的檢測,也就無法得到車牌的可能位置,也就沒有後面的一系列的車牌判斷、字元識别過程。通過Sobel算子,可以很友善的得到車牌的一個相對準确的位置,為我們的後續處理打好堅實的基礎。在上面的plateLocate的執行過程中可以看到,正是通過Sobel算子,将車牌中的字元與車的背景明顯區分開來,為後面的二值化與閉操作打下了基礎。那麼Sobel算子是如何運作的呢?

  Soble算子原理是對圖像求一階的水準與垂直方向導數,根據導數值的大小來判斷是否是邊緣。請詳見CSDN小魏的部落格(小心她部落格裡把Gx和Gy弄反了)。

  為了計算友善,Soble算子并沒有真正去求導,而是使用了周邊值的權重和的方法,學術上稱作“卷積”。權值稱為“卷積模闆”。例如下圖左邊就是Sobel的Gx卷積模闆(計算垂直邊緣),中間是原圖像,右邊是經過卷積模闆後的新圖像。

EasyPR--開發詳解

圖7 Sobel算子Gx示意圖

  在這裡示範了通過卷積模闆,原始圖像紅色的像素點原本是5的值,經過卷積計算(- 1 * 3 - 2 * 3 - 1 * 4 + 1 * 5 + 2 * 7 + 1 * 6 = 12)後紅色像素的值變成了12。

 4.實踐

  在代碼中調用Soble算子需要較多的步驟。

EasyPR--開發詳解
/// 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 );      
EasyPR--開發詳解

  這裡給出了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算子的一個對比。

EasyPR--開發詳解

圖8 Sobel與Laplace示意圖

  可以看出,通過Laplace算子的圖像包含了水準邊緣和垂直邊緣,根據我們剛才的描述。水準邊緣對于車牌的檢測一般無利反而有害。經過對近百幅圖像的測試,Sobel算子的效果優于Laplace算子,是以不适宜采用Laplace算子替代Sobel算子。

  除了Sobel算子,還有一個算子,Shcarr算子。但這個算子其實隻是Sobel算子的一個變種,由于Sobel算子在3*3的卷積模闆上計算往往不太精确,是以有一個特殊的Sobel算子,其權值按照下圖來表達,稱之為Scharr算子。下圖是Sobel算子與Scharr算子的一個對比。

EasyPR--開發詳解

圖9 Sobel與Scharr示意圖

  一般來說,Scharr算子能夠比Sobel算子檢測邊緣的效果更好,從上圖也可以看出。但是,這個“更好”是一把雙刃劍。我們的目的并不是畫出圖像的邊緣,而是确定車牌的一個區域,越精細的邊緣越會幹擾後面的閉運算。是以,針對大量的圖檔的測試,Sobel算子一般都優于Scharr 算子。

  關于Sobel算子更詳細的解釋和Scharr算子與Sobel算子的同異,可以參看官網的介紹:Sobel與Scharr。

  綜上所述,在求圖像邊緣的過程中,Sobel算子是一個最佳的契合車牌定位需求的算子,Laplace算子與Scharr算子的效果都不如它。

  有一點要說明的:Sobel算子僅能對灰階圖像有效果,不能将色彩圖像作為輸入。是以在進行Soble算子前必須進行前面的灰階化工作

根據前文的内容,車牌定位的功能還剩下如下的步驟,見下圖中未塗灰的部分。

EasyPR--開發詳解

圖1 車牌定位步驟

  我們首先從Soble算子分析出來的邊緣來看。通過下圖可見,Sobel算子有很強的區分性,車牌中的字元被清晰的描繪出來,那麼如何根據這些資訊定位出車牌的位置呢?

EasyPR--開發詳解

圖2 Sobel後效果

  我們的車牌定位功能做了個假設,即車牌是包含字元圖塊的一個最小的外接矩形。在大部分車牌進行中,這個假設都能工作的很好。我們來看下這個假設是如何工作的。

  車牌定位過程的全部代碼如下:

EasyPR--開發詳解

View Code

  首先,我們通過二值化處理将Sobel生成的灰階圖像轉變為二值圖像。

四.二值化

  二值化算法非常簡單,就是對圖像的每個像素做一個門檻值處理。

1.目标

  為後續的形态學算子Morph等準備二值化的圖像。 

2.效果

  經過二值化處理後的圖像效果為下圖,與灰階圖像仔細區分下,二值化圖像中的白色是沒有顔色強與暗的差別的。

EasyPR--開發詳解

圖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.效果

  我們這裡看下經過閉操作後圖像連接配接的效果。

EasyPR--開發詳解

圖4 閉操作後效果

3.理論

  在做閉操作的說明前,必須簡單介紹一下腐蝕和膨脹兩個操作。

  在圖像處理技術中,有一些的操作會對圖像的形态發生改變,這些操作一般稱之為形态學操作。形态學操作的對象是二值化圖像。

有名的形态學操作中包括腐蝕,膨脹,開操作,閉操作等。其中腐蝕,膨脹是許多形态學操作的基礎。

  腐蝕操作:

  顧名思義,是将物體的邊緣加以腐蝕。具體的操作方法是拿一個寬m,高n的矩形作為模闆,對圖像中的每一個像素x做如下處理:像素x至于模闆的中心,根據模版的大小,周遊所有被模闆覆寫的其他像素,修改像素x的值為所有像素中最小的值。這樣操作的結果是會将圖像外圍的突出點加以腐蝕。如下圖的操作過程:

EasyPR--開發詳解

圖5 腐蝕操作原理

  上圖示範的過程是背景為黑色,物體為白色的情況。腐蝕将白色物體的表面加以“腐蝕”。在opencv的官方教程中,是以如下的圖示說明腐蝕過程的,與我上面圖的差別在于:背景是白色,而物體為黑色(這個不太符合一般的情況,是以我沒有拿這張圖作為通用的例子)。讀者隻需要了解背景為不同顔色時腐蝕也是不同的效果就可以了。

EasyPR--開發詳解

圖6 腐蝕操作原理2

  膨脹操作:

  膨脹操作與腐蝕操作相反,是将圖像的輪廓加以膨脹。操作方法與腐蝕操作類似,也是拿一個矩形模闆,對圖像的每個像素做周遊處理。不同之處在于修改像素的值不是所有像素中最小的值,而是最大的值。這樣操作的結果會将圖像外圍的突出點連接配接并向外延伸。如下圖的操作過程:

EasyPR--開發詳解

圖7 膨脹操作原理

  下面是在opencv的官方教程中,膨脹過程的圖示:

EasyPR--開發詳解

圖8 膨脹操作原理2

  開操作:

  開操作就是對圖像先腐蝕,再膨脹。其中腐蝕與膨脹使用的模闆是一樣大小的。為了說明開操作的效果,請看下圖的操作過程:

EasyPR--開發詳解

圖9 開操作原理

  由于開操作是先腐蝕,再膨脹。是以可以結合圖5和圖7得出圖9,其中圖5的輸出是圖7的輸入,是以開操作的結果也就是圖7的結果。

  閉操作:

  閉操作就是對圖像先膨脹,再腐蝕。閉操作的結果一般是可以将許多靠近的圖塊相連稱為一個無突起的連通域。在我們的圖像定位中,使用了閉操作去連接配接所有的字元小圖塊,然後形成一個車牌的大緻輪廓。閉操作的過程我會講的細緻一點。為了說明字元圖塊連接配接的過程。在這裡選取的原圖跟上面三個操作的原圖不大一樣,是一個由兩個分開的圖塊組成的圖。原圖首先經過膨脹操作,将兩個分開的圖塊結合起來(注意我用偏白的灰色圖塊表示由于膨脹操作而産生的新的白色)。接着通過腐蝕操作,将連通域的邊緣和突起進行削平(注意我用偏黑的灰色圖塊表示由于腐蝕被侵蝕成黑色圖塊)。最後得到的是一個無突起的連通域(純白的部分)。

EasyPR--開發詳解

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

  如果我對二值化的圖像進行開操作,結果會是什麼樣的?下圖是圖像使用閉操作與開操作處理後的一個差別:

EasyPR--開發詳解

  圖11 開與閉的對比

  暈,怎麼開操作後圖像沒了?原因是:開操作第一步腐蝕的效果太強,直接導緻接下來的膨脹操作幾乎沒有效果,是以圖像就變幾乎沒了。

  可以看出,使用閉操作以後,車牌字元的圖塊被連接配接成了一個較為規則的矩形,通過閉操作,将車牌中的字元連成了一個圖塊,同時将突出的部分進行裁剪,圖塊成為了一個類似于矩形的不規則圖塊。我們知道,車牌應該是一個規則的矩形,是以擷取規則矩形的辦法就是先取輪廓,再接着求最小外接矩形。

  這裡需要注意的是,矩形模闆的寬度,17是個推薦值,低于17都不推薦。

  為什麼這麼說,因為有一個”斷節“的問題。中國車牌有一個特點,就是表示城市的字母與右邊相鄰的字元距離遠大于其他相鄰字元之間的距離。如果你設定的不夠大,結果導緻左邊的字元與右邊的字元中間斷開了,如下圖:

EasyPR--開發詳解

 圖12 “斷節”效果

  這種情況我稱之為“斷節”如果你不想字元從中間被分成"蘇A"和"7EUK22"的話,那麼就必須把它設定大點。

  另外還有一種讨厭的情況,就是右邊的字元第一個為1的情況,例如蘇B13GH7。在這種情況下,由于1的字元的形态原因,導緻跟左邊的B的字元的距離更遠,在這種情況下,低于17都有很大的可能性會斷節。下圖說明了矩形模闆寬度過小時(例如設定為7)面對不同車牌情況下的效果。其中第二個例子選取了蘇E開頭的車牌,由于E在Sobel算子運算過後僅存有左邊的豎杠,是以也會導緻跟右邊的字元相距過遠的情況!

EasyPR--開發詳解

圖13 “斷節”發生示意

  寬度過大也是不好的,因為它會導緻閉操作連接配接不該連接配接的部分,例如下圖的情況。

EasyPR--開發詳解

圖14 矩形模闆寬度過大

  這種情況下,你取輪廓獲得矩形肯定會大于你設定的校驗規則,即便通過校驗了,由于圖塊中有不少不是車牌的部分,會給字元識别帶來麻煩。

  是以,矩形的寬度是一個需要非常細心權衡的值,過大過小都不好,取決于你的環境。至于矩形的高度,3是一個較好的值,一般來說都能工作的很好,不需要改變。

  記得我在前一篇文章中提到,工業用圖檔與生活場景下圖檔的差別麼。筆者做了一個實驗,下載下傳了30多張左右的百度車牌圖檔。用plateLocate過程去識别他們。如果按照下面的方式設定參數,可以保證90%以上的定位成功率。

EasyPR--開發詳解
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的下一個版本中,會增加對于生活場景下圖檔的一個模式。隻要選擇這個模式,就适用于百度圖檔這種日常生活抓拍圖檔的效果。但是,仍然有一些圖檔是EasyPR不好處理的。或者可以說,按照目前的邊緣檢測算法,難以處理的。

  請看下面一張圖檔:

EasyPR--開發詳解

圖15 難以權衡的一張圖檔

  這張圖檔最麻煩的地方在于車牌左右兩側凹下去的邊側,這個邊緣在Sobel算子中非常明顯,如果矩形模闆過長,很容易跟它們連接配接起來。更麻煩的是這個車牌屬于上面說的“斷節”很容易發生的類型,因為車牌右側字元的第一個字母是“1”,這個導緻如果矩形模闆過短,則很容易車牌斷成兩截。結果最後導緻了如下的情況。

  如果我設定矩形模闆寬度為12,則會發生下面的情況:

EasyPR--開發詳解

圖16 車牌被一分為二

  如果我增加矩形模闆寬度到13,則又會發生下面的情況。

EasyPR--開發詳解

圖17 車牌區域被不不正确的放大

  是以矩形模闆的寬度是個整數值,在12和13中間沒有中間值。這個導緻幾乎沒有辦法處理這幅車牌圖像。

  上面的情況屬于車尾車牌的一種沒辦法解決的情況。下面所說的情況屬于車頭的情況,相比前者,錯誤檢測的幾率高的多!為什麼,因為是一類型車牌無法處理。要問我這家車是哪家,我隻能說:碰到開奧迪Q5及其系列的,早點嫁了吧。傷不起。

EasyPR--開發詳解

圖18 奧迪Q5前部垂直邊緣太多

  這麼多的垂直邊緣,極為容易檢錯。已經試過了,幾乎沒有辦法處理這種車牌。隻能替換邊緣檢測這種思路,采用顔色區分等方法。奧體Q系列前臉太多垂直邊緣了,給跪。

六.取輪廓

取輪廓操作是個相對簡單的操作,是以隻做簡短的介紹。

1.目标

  将連通域的外圍勾畫出來,便于形成外接矩形。 

2.效果

  我們這裡看下經過取輪廓操作的效果。

EasyPR--開發詳解

圖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.效果

  經過尺寸判斷,會排除大量由輪廓生成的不合适尺寸的最小外接矩形。效果如下圖:

EasyPR--開發詳解

圖20 尺寸判斷操作

  通過對圖像中所有的輪廓的外接矩形進行周遊,我們調用CplateLocate的另一個成員方法verifySizes,代碼如下:

EasyPR--開發詳解
顯示最終生成的車牌圖像,便于判斷是否成功進行了旋轉。
 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;
 }      
EasyPR--開發詳解

  在原先的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度),則認為不是車牌并舍棄。

  對上面的尺寸判斷結果的六個黃色矩形應用角度判斷後結果如下圖:

EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解

圖21 角度判斷後的候選車牌

  可以看出,原先的6個候選矩形隻剩3個。車牌兩側的車燈的矩形被成功篩選出來。角度判斷會去除verifySizes篩選餘下的7%矩形,使得最終進入車牌判斷環節的矩形隻有原先的全部矩形的3%。

  角度判斷以及接下來的旋轉操作的代碼如下:

EasyPR--開發詳解

View Code

九.旋轉

旋轉操作是為後面的車牌判斷與字元識别提高成功率的關鍵環節。

1.目标

  旋轉操作将偏斜的車牌調整為水準。 

2.效果

  假設待處理的圖檔如下圖:

EasyPR--開發詳解

圖22 傾斜的車牌

  使用旋轉與不适用旋轉的效果差別如下圖:

EasyPR--開發詳解

圖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,代碼很簡單,貼下,不做細講了。

EasyPR--開發詳解

View Code

十一.總結

  通過接近10多個步驟的處理,我們才有了最終的候選車牌。這些過程是一環套一環的,前步驟的輸出是後步驟的輸入,而且順序也是有規則的。目前針對我的測試圖檔來說,它們工作的很好,但不一定适用于你的情況。車牌定位以及圖像處理算法的一個大的問題就是他的弱魯棒性,換一個場景可能就得換一套工作方式。是以結合你的使用場景來做調整吧,這是我為什麼要在這裡費這麼多字數詳細說明的原因。如果你不了解細節,你就不可能進行修改,也就無法使它适合你的工作需求。

  讨論:

  車牌定位全部步驟了解後,我們來讨論下。這個過程是否是一個最優的解?

  毫無疑問,一個算法的好壞除了取決于它的設計思路,還取決于它是否充分利用了已知的資訊。如果一個算法沒有充分利用提供的資訊,那麼它就有進一步優化的空間。EasyPR的 plateLocate過程就是如此,在實施過程中它相繼抛棄掉了色彩資訊,沒有利用紋理資訊,是以車牌定位的過程應該還有優化的空間。如果 plateLocate過程無法良好的解決你的定位問題,那麼嘗試下能夠利用其他資訊的方法,也許你會大幅度提高你的定位成功率。

  車牌定位講完後,下面就是機器學習的過程。不同于前者,我不會重點說明其中的細節,而是會概括性的說明每個步驟的用途以及訓練的最佳實踐。在下一個章節中,我會首先介紹下什麼是機器學習,為什麼它如今這麼火熱,機器學習和大資料的關系,歡迎繼續閱讀。

  本項目的Git位址:這裡。如果有問題歡迎提issue。本文是一個系列中的第5篇,前幾篇文章見前面的部落格

本篇文章介紹EasyPR裡新的定位功能:顔色定位與偏斜扭正。希望這篇文檔可以幫助開發者與使用者更好的了解EasyPR的設計思想。

  讓我們先看一下示例圖檔,這幅圖檔中的車牌通過顔色的定位法進行定位并從偏斜的視角中扭正為正視角(請看右圖的左上角)。

EasyPR--開發詳解

圖1 新版本的定位效果

下面内容會對這兩個特性的實作過程展開具體的介紹。首先介紹顔色定位的原理,然後是偏斜扭正的實作細節。

  由于本文較長,為友善讀者,以下是本文的目錄:

  一.顔色定位

  1.1起源

  1.2方法

  1.3不足與改善

  二.偏斜扭正

  2.1分析

  2.2ROI截取

  2.3擴大化旋轉

  2.4偏斜判斷

  2.5仿射變換

  2.6總結

  三.總結

一. 顔色定位

1.起源

  在前面的介紹裡,我們使用了Sobel查找垂直邊緣的方法,成功定位了許多車牌。但是,Sobel法最大的問題就在于面對垂直邊緣交錯的情況下,無法準确地定位車牌。例如下圖。為了解決這個問題,可以考慮使用顔色資訊進行定位。

EasyPR--開發詳解

圖2 顔色定位與Sobel定位的比較

  如果将顔色定位與Sobel定位加以結合的話,可以使車牌的定位準确率從75%上升到94%。

2.方法

  關于顔色定位首先我們想到的解決方案就是:利用RGB值來判斷。

  這個想法聽起來很自然:如果我們想找出一幅圖像中的藍色部分,那麼我們隻需要檢查RGB分量(RGB分量由Red分量--紅色,Green分量 --綠色,Blue分量--藍色共同組成)中的Blue分量就可以了。一般來說,Blue分量是個0到255的值。如果我們設定一個門檻值,并且檢查每個像素的Blue分量是否大于它,那我們不就可以得知這些像素是不是藍色的了麼?這個想法雖然很好,不過存在一個問題,我們該怎麼來選擇這個門檻值?這是第一個問題。

  即便我們用一些方法決定了門檻值以後,那麼下面的一個問題就會讓人抓狂,顔色是組合的,即便藍色屬性在255(這樣已經很‘藍’了吧),隻要另外兩個分量配合(例如都為255),你最後得到的不是藍色,而是黑色。

  這還隻是區分藍色的問題,黃色更麻煩,它是由紅色和綠色組合而成的,這意味着你需要考慮兩個變量的配比問題。這些問題讓選擇RGB顔色作為判斷的難度大到難以接受的地步。是以必須另想辦法。

  為了解決各種顔色相關的問題,人們發明了各種顔色模型。其中有一個模型,非常适合解決顔色判斷的問題。這個模型就是HSV模型。

EasyPR--開發詳解

圖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分量變化範圍。

EasyPR--開發詳解

圖4 藍色的H分量區間 

  下圖顯示了黃色的H分量變化範圍。 

EasyPR--開發詳解

圖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的判斷可以排除車牌周圍車身的幹擾。

EasyPR--開發詳解

圖6 V和S的區間

  明确了使用HSV模型以及用門檻值進行判斷以後,下面就是一個顔色定位的完整過程。

  第一步,将圖像的顔色空間從RGB轉為HSV,在這裡由于光照的影響,對于圖像使用直方圖均衡進行預處理;

  第二步,依次周遊圖像的所有像素,當H值落在200-280之間并且S值與V值也落在0.35-1.0之間,标記為白色像素,否則為黑色像素;

  第三步,對僅有白黑兩個顔色的二值圖參照原先車牌定位中的方法,使用閉操作,取輪廓等方法将車牌的外接矩形截取出來做進一步的處理。

EasyPR--開發詳解

圖7 藍色定位效果

  以上就完成了一個藍色車牌的定位過程。我們把對圖像中藍色車牌的尋找過程稱為一次與藍色模闆的比對過程。代碼中的函數稱之為colorMatch。一般說來,一幅圖像需要進行一次藍色模闆的比對,還要進行一次黃色模闆的比對,以此確定藍色和黃色的車牌都被定位出來。

  黃色車牌的定位方法與其類似,僅僅隻是H門檻值範圍的不同。事實上,黃色定位的效果一般好的出奇,可以在非常複雜的環境下将車牌極為準确的定位出來,這可能源于現實世界中黃色非常醒目的原因。

EasyPR--開發詳解

圖8 黃色定位效果

  從實際效果來看,顔色定位的效果是很好的。在通用資料測試集裡,大約70%的車牌都可以被定位出來(一些顔色定位不了的,我們可以用Sobel定位處理)。

  在代碼中有些細節需要注意:

  一. opencv為了保證HSV三個分量都落在0-255之間(確定一個char能裝的下),對H分量除以了2,也就是0-180的範圍,S和V分量乘以了 255,将0-1的範圍擴充到0-255。我們在設定門檻值的時候需要參照opencv的标準,是以對參數要進行一個轉換。

  二. 是v和s取值的問題。對于暗的圖來說,取值過大容易漏,而對于亮的圖,取值過小則容易跟車身混淆。是以可以考慮最适應的改變門檻值。

  三. 是模闆問題。目前的做法是針對藍色和黃色的比對使用了兩個模闆,而不是統一的模闆。統一模闆的問題在于擔心藍色和黃色的幹擾問題,例如黃色的車與藍色的牌的幹擾,或者藍色的車和黃色牌的幹擾,這裡面最典型的例子就是一個帶有藍色車牌的黃色計程車,在很多城市裡這已經是“标準配置”。是以需要将藍色和黃色的比對分别用不同的模闆處理。

  了解完這三個細節以後,下面就是代碼部分。

EasyPR--開發詳解
EasyPR--開發詳解
//! 根據一幅圖像與顔色模闆擷取對應的二值圖
    //! 輸入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;
    }      
EasyPR--開發詳解

3.不足

  以上說明了顔色定位的設計思想與細節。那麼顔色定位是不是就是萬能的?答案是否定的。在色彩充足,光照足夠的情況下,顔色定位的效果很好,但是在面對光線不足的情況,或者藍色車身的情況時,顔色定位的效果很糟糕。下圖是一輛藍色車輛,可以看出,車牌與車身内容完全重疊,無法分割。

EasyPR--開發詳解

圖9 失效的顔色定位

  碰到失效的顔色定位情況時需要使用原先的Sobel定位法。

  目前的新版本使用了顔色定位與Sobel定位結合的方式。首先進行顔色定位,然後根據條件使用Sobel進行再次定位,增加整個系統的适應能力。

  為了加強魯棒性,Sobel定位法可以用兩階段的查找。也就是在已經被Sobel定位的圖塊中,再進行一次Sobel定位。這樣可以增加準确率,但會降低了速度。一個折衷的方案是讓使用者決定一個參數m_maxPlates的值,這個值決定了你在一幅圖裡最多定位多少車牌。系統首先用顔色定位出候選車牌,然後通過SVM模型來判斷是否是車牌,最後統計數量。如果這個數量大于你設定的參數,則認為車牌已經定位足夠了,不需要後一步處理,也就不會進行兩階段的Sobel查找。相反,如果這個數量不足,則繼續進行Sobel定位。

  綜合定位的代碼位于CPlateDectec中的的成員函數plateDetectDeep中,以下是plateDetectDeep的整體流程。

EasyPR--開發詳解

圖10 綜合定位全部流程

  有沒有顔色定位與Sobel定位都失效的情況?有的。這種情況下可能需要使用第三類定位技術--字元定位技術。這是EasyPR發展的一個方向,這裡不展開讨論。

二. 偏斜扭轉

  解決了顔色的定位問題以後,下面的問題是:在定位以後,我們如何把偏斜過來的車牌扭正呢?

EasyPR--開發詳解

圖11 偏斜扭轉效果

  這個過程叫做偏斜扭轉過程。其中一個關鍵函數就是opencv的仿射變換函數。但在具體實施時,有很多需要解決的問題。

1.分析

  在任何新的功能開發之前,技術預研都是第一步。

  在這篇文檔介紹了opencv的仿射變換功能。效果見下圖。

EasyPR--開發詳解

圖12 仿射變換效果 

  仔細看下,貌似這個功能跟我們的需求很相似。我們的偏斜扭轉功能,說白了,就是把對圖像的觀察視角進行了一個轉換。

  不過這篇文章裡的代碼基本來自于另一篇官方文檔。官方文檔裡還有一個例子,可以矩形扭轉成平行四邊形。而我們的需求正是将平行四邊形的車牌扭正成矩形。這麼說來,隻要使用例子中對應的反函數,應該就可以實作我們的需求。從這個角度來看,偏斜扭轉功可以實作。确定了可行性以後,下一步就是思考如何實作。

  在原先的版本中,我們對定位出來的區域會進行一次角度判斷,當角度小于某個門檻值(預設30度)時就會進行全圖旋轉。

  這種方式有兩個問題:

  一是我們的政策是對整幅圖像旋轉。對于opencv來說,每次旋轉操作都是一個矩形的乘法過程,對于非常大的圖像,這個過程是非常消耗計算資源的;

  二是30度的門檻值無法處理示例圖檔。事實上,示例圖檔的定位區域的角度是-50度左右,已經大于我們的門檻值了。為了處理這樣的圖檔,我們需要把我們的門檻值增大,例如增加到60度,那麼這樣的結果是帶來候選區域的增多。

  兩個因素結合,會大幅度增加處理時間。為了不讓處理速度下降,必須想辦法規避這些影響。

  一個方法是不再使用全圖旋轉,而是區域旋轉。其實我們在擷取定位區域後,我們并不需要定位區域以外的圖像。

  倘若我們能劃出一塊小的區域包圍定位區域,然後我們僅對定位區域進行旋轉,那麼計算量就會大幅度降低。而這點,在opencv裡是可以實作的,我們對定位區域RotatedRect用boundingRect()方法擷取外接矩形,再使用Mat(Rect ...)方法截取這個區域圖塊,進而生成一個小的區域圖像。于是下面的所有旋轉等操作都可以基于這個區域圖像進行。

  在這些設計決定以後,下面就來思考整個功能的架構。

  我們要解決的問題包括三類,第一類是正的車牌,第二類是傾斜的車牌,第三類是偏斜的車牌。前兩類是前面說過的,第三類是本次新增的功能需求。第二類傾斜車牌與第三類車牌的差別見下圖。

EasyPR--開發詳解

圖13 兩類不同的旋轉

  通過上圖可以看出,正視角的旋轉圖檔的觀察角度仍然是正方向的,隻是由于路的不平或者錄影機的傾斜等原因,導緻矩形有一定傾斜。這類圖塊的特點就是在RotataedRect内部,車牌部分仍然是個矩形。偏斜視角的圖檔的觀察角度是非正方向的,是從側面去看車牌。這類圖塊的特點是在 RotataedRect内部,車牌部分不再是個矩形,而是一個平行四邊形。這個特性決定了我們需要差別的對待這兩類圖檔。

  一個初步的處理思路就是下圖。

EasyPR--開發詳解

圖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圖像如下圖:

EasyPR--開發詳解

圖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,如下圖。

EasyPR--開發詳解

圖16 不安全的外接矩形函數會抛出異常

  這個異常産生的原因在于,在opencv2.4.8中(不清楚opencv其他版本是否沒有這個問題),boundingRect()函數計算出的Rect的四個點的坐标沒有做驗證。這意味着你計算一個RotataedRect的最小外接矩形Rect時,它可能會給你一個負坐标,或者是一個超過原圖檔外界的坐标。于是當你把Rect作為參數傳遞給Mat(Rect ...)的話,它會提示你所要截取的Rect中的坐标越界了!

  解決方案是實作一個安全的計算最小外接矩形Rect的函數,在boundingRect()結果之上,對角點坐标進行一次判斷,如果值為負數,就置為0,如果值超過了原始Mat的rows或cols,就置為原始Mat的這些rows或cols。

  這個安全函數名為calcSafeRect(...),下面是這個函數的代碼。

EasyPR--開發詳解

View Code

3.擴大化旋轉

  好,當我通過calcSafeRect(...)擷取了一個安全的Rect,然後通過Mat(Rect ...)函數截取了這個感興趣圖像ROI以後。下面的工作就是對這個新的ROI圖像進行操作。

  首先是判斷這個ROI圖像是否要旋轉。為了降低工作量,我們不對角度在-5度到5度區間的ROI進行旋轉(注意這裡講的角度針對的生成ROI的RotataedRect,ROI本身是水準的)。因為這麼小的角度對于SVM判斷以及字元識别來說,都是沒有影響的。

  對其他的角度我們需要對ROI進行旋轉。當我們對ROI進行旋轉以後,接着把轉正後的RotataedRect部分從ROI中截取出來。

  但很快我們就會碰到一個新問題。讓我們看一下下圖,為什麼我們截取出來的車牌區域最左邊的“川”字和右邊的“2”字發生了形變?為了搞清這個原因,作者仔細地研究了旋轉與截取函數,但很快發現了形變的根源在于旋轉後的ROI圖像。

  仔細看一下旋轉後的ROI圖像,是否左右兩側不再完整,像是被截去了一部分?

EasyPR--開發詳解

圖17 旋轉後圖像被截斷

  要想了解這個問題,需要了解opencv的旋轉變換函數的特性。作為旋轉變換的核心函數,affinTransform會要求你輸出一個旋轉矩陣給它。這很簡單,因為我們隻需要給它一個旋轉中心點以及角度,它就能計算出我們想要的旋轉矩陣。旋轉矩陣的獲得是通過如下的函數得到的:

  Mat rot_mat = getRotationMatrix2D(new_center, angle, 1);      

  在擷取了旋轉矩陣rot_mat,那麼接下來就需要調用函數warpAffine來開始旋轉操作。這個函數的參數包括一個目标圖像、以及目标圖像的Size。目标圖像容易了解,大部分opencv的函數都會需要這個參數。我們隻要建立一個Mat即可。那麼目标圖像的Size是什麼?在一般的觀點中,假設我們需要旋轉一個圖像,我們給opencv一個原始圖像,以及我需要在某個旋轉點對它旋轉一個角度的需求,那麼opencv傳回一個圖像給我即可,這個圖像的Size或者說大小應該是opencv傳回給我的,為什麼要我來告訴它呢?

  你可以試着對一個正方形進行旋轉,仔細看看,這個正方形的外接矩形的大小會如何變化?當旋轉角度還小時,一切都還好,當角度變大時,明顯我們看到的外接矩形的大小也在擴增。在這裡,外接矩形被稱為視框,也就是我需要旋轉的正方形所需要的最小區域。随着旋轉角度的變大,視框明顯增大。

EasyPR--開發詳解

圖18 矩形旋轉後所需視框增大 

  在圖像旋轉完以後,有三類點會獲得不同的處理,一種是有原圖像對應點且在視框内的,這些點被正常顯示;一類是在視框内但找不到原圖像與之對應的點,這些點被置0值(顯示為黑色);最後一類是有原圖像與之對應的點,但不在視框内的,這些點被悲慘的抛棄。

EasyPR--開發詳解

圖19 旋轉後三類不同點的命運

  這就是旋轉後不同三類點的命運,也就是新生成的圖像中一些點呈現黑色(被置0),一些點被截斷(被抛棄)的原因。如果把視框調整大點的話,就可以大幅度減少被截斷點的數量。是以,為了保證旋轉後的圖像不被截斷,是以我們需要計算一個合理的目标圖像的Size,讓我們的感興趣區域得到完整的顯示。

  下面的代碼使用了一個極為簡單的政策,它将原始圖像與目标圖像都進行了擴大化。首先建立一個尺寸為原始圖像1.5倍的新圖像,接着把原始圖像映射到新圖像上,于是我們得到了一個顯示區域(視框)擴大化後的原始圖像。顯示區域擴大以後,那些在原圖像中沒有值的像素被置了一個初值。

  接着調用warpAffine函數,使用新圖像的大小作為目标圖像的大小。warpAffine函數會将新圖像旋轉,并用目标圖像尺寸的視框去顯示它。于是我們得到了一個所有感興趣區域都被完整顯示的旋轉後圖像。

  這樣,我們再使用getRectSubPix()函數就可以獲得想要的車牌區域了。

EasyPR--開發詳解

圖20 擴大化旋轉後圖像不再被截斷

  以下就是旋轉函數rotation的代碼。

EasyPR--開發詳解
EasyPR--開發詳解
//! 旋轉操作
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;

    
}      
EasyPR--開發詳解

4.偏斜判斷

  當我們對ROI進行旋轉以後,下面一步工作就是把RotataedRect部分從ROI中截取出來,這裡可以使用getRectSubPix方法,這個函數可以在被旋轉後的圖像中截取一個正的矩形圖塊出來,并指派到一個新的Mat中,稱為車牌區域。

  下步工作就是分析截取後的車牌區域。車牌區域裡的車牌分為正角度和偏斜角度兩種。對于正的角度而言,可以看出車牌區域就是車牌,是以直接輸出即可。而對于偏斜角度而言,車牌是平行四邊形,與矩形的車牌區域不重合。

  如何判斷一個圖像中的圖形是否是平行四邊形?

  一種簡單的思路就是對圖像二值化,然後根據二值化圖像進行判斷。圖像二值化的方法有很多種,假設我們這裡使用一開始在車牌定位功能中使用的大津門檻值二值化法的話,效果不會太好。因為大津門檻值是自适應門檻值,在完整的圖像中二值出來的平行四邊形可能在小的局部圖像中就不再是。最好的辦法是使用在前面定位子產品生成後的原圖的二值圖像,我們通過同樣的操作就可以在原圖中截取一個跟車牌區域對應的二值化圖像。

  下圖就是一個二值化車牌區域獲得的過程。

EasyPR--開發詳解

圖21 二值化的車牌區域

  接下來就是對二值化車牌區域進行處理。為了判斷二值化圖像中白色的部分是平行四邊形。一種簡單的做法就是從圖像中選擇一些特定的行。計算在這個行中,第一個全為0的串的長度。從幾何意義上來看,這就是平行四邊形斜邊上某個點距離外接矩形的長度。

  假設我們選擇的這些行位于二值化圖像高度的1/4,2/4,3/4處的話,如果是白色圖形是矩形的話,這些串的大小應該是相等或者相差很小的,相反如果是平行四邊形的話,那麼這些串的大小應該不等,并且呈現一個遞增或遞減的關系。通過這種不同,我們就可以判斷車牌區域裡的圖形,究竟是矩形還是平行四邊形。

  偏斜判斷的另一個重要作用就是,計算平行四邊形傾斜的斜率,這個斜率值用來在下面的仿射變換中發揮作用。我們使用一個簡單的公式去計算這個斜率,那就是利用上面判斷過程中使用的串大小,假設二值化圖像高度的1/4,2/4,3/4處對應的串的大小分别為 len1,len2,len3,車牌區域的高度為Height。一個計算斜率slope的計算公式就是:(len3-len1)/Height*2。

  Slope的直覺含義見下圖。

EasyPR--開發詳解

圖22 slope的幾何含義

  需要說明的,這個計算結果在平行四邊形是右斜時是負值,而在左斜時則是正值。于是可以根據slope的正負判斷平行四邊形是右斜或者左斜。在實踐中,會發生一些公式不能應對的情況,例如像下圖這種情況,斜邊的部分區域發生了内凹或者外凸現象。這種現象會導緻len1,len2或者len3的計算有誤,是以slope也會不準。

EasyPR--開發詳解

圖23 内凹現象

  為了實作一個魯棒性更好的計算方法,可以用(len2-len1)/Height*4與(len3-len1)/Height*2兩者之間更靠近tan(angle)的值作為solpe的值(在這裡,angle代表的是原來RotataedRect的角度)。

  多采取了一個slope備選的好處是可以避免單點的内凹或者外凸,但這仍然不是最好的解決方案。在最後的讨論中會介紹一個其他的實作思路。

  完成偏斜判斷與斜率計算的函數是isdeflection,下面是它的代碼。

EasyPR--開發詳解
EasyPR--開發詳解
//! 是否偏斜
//! 輸入二值化圖像,輸出判斷結果
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;
}      
EasyPR--開發詳解

5.仿射變換

  俗話說:行百裡者半九十。前面已經做了如此多的工作,應該可以實作偏斜扭轉功能了吧?但在最後的道路中,仍然有問題等着我們。

  我們已經實作了旋轉功能,并且在旋轉後的區域中截取了車牌區域,然後判斷車牌區域中的圖形是一個平行四邊形。下面要做的工作就是把平行四邊形扭正成一個矩形。

EasyPR--開發詳解

圖24 從平行四邊形車牌到矩形車牌

  首先第一個問題就是解決如何從平行四邊形變換成一個矩形的問題。opencv提供了一個函數warpAffine,就是仿射變換函數。注意,warpAffine不僅可以讓圖像旋轉(前面介紹過),也可以進行仿射變換,真是一個多才多藝的函數。o

  通過仿射變換函數可以把任意的矩形拉伸成其他的平行四邊形。opencv的官方文檔裡給了一個示例,值得注意的是,這個示例示範的是把矩形變換為平行四邊形,跟我們想要的恰恰相反。但沒關系,我們先看一下它的使用方法。

EasyPR--開發詳解

圖25 opencv官網上對warpAffine使用的示例

  warpAffine方法要求輸入的參數是原始圖像的左上點,右上點,左下點,以及輸出圖像的左上點,右上點,左下點。注意,必須保證這些點的對應順序,否則仿射的效果跟你預想的不一樣。通過這個方法介紹,我們可以大概看出,opencv需要的是三個點對(共六個點)的坐标,然後建立一個映射關系,通過這個映射關系将原始圖像的所有點映射到目标圖像上。 

EasyPR--開發詳解

圖26 warpAffine需要的三個對應坐标點

  再回來看一下我們的需求,我們的目标是把車牌區域中的平行四邊形映射為一個矩形。讓我們做個假設,如果我們選取了車牌區域中的平行四邊形車牌的三個關鍵點,然後再确定了我們希望将車牌扭正成的矩形的三個關鍵點的話,我們是否就可以實作從平行四邊形車牌到矩形車牌的扭正?

  讓我們畫一幅圖像來看看這個變換的作用。有趣的是,把一個平行四邊形變換為矩形會對包圍平行四邊形車牌的區域帶來影響。

  例如下圖中,藍色的實線代表扭轉前的平行四邊形車牌,虛線代表扭轉後的。黑色的實線代表矩形的車牌區域,虛線代表扭轉後的效果。可以看到,當藍色車牌被扭轉為矩形的同時,黑色車牌區域則被扭轉為平行四邊形。

  注意,當車牌區域扭變為平行四邊形以後,需要顯示它的視框增大了。跟我們在旋轉圖像時碰到的情形一樣。

EasyPR--開發詳解

圖27 平行四邊形的扭轉帶來的變化

  讓我們先實際嘗試一下仿射變換吧。

  根據仿射函數的需要,我們計算平行四邊形車牌的三個關鍵點坐标。其中左上點的值(xdiff,0)中的xdiff就是根據車牌區域的高度height與平行四邊形的斜率slope計算得到的:

xidff = Height * abs(slope)      

  為了計算目标矩形的三個關鍵點坐标,我們首先需要把扭轉後的原點坐标調整到平行四邊形車牌區域左上角位置。見下圖。

EasyPR--開發詳解

圖28 原圖像的坐标計算

  依次推算關鍵點的三個坐标。它們應該是

EasyPR--開發詳解
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);      
EasyPR--開發詳解

  根據上圖的坐标,我們開始進行一次仿射變換的嘗試。

  opencv的warpAffine函數不會改變變換後圖像的大小。而我們給它傳遞的目标圖像的大小僅會決定視框的大小。不過這次我們不用擔心視框的大小,因為根據圖27看來,哪怕視框跟原始圖像一樣大,我們也足夠顯示扭正後的車牌。

  看看仿射的效果。暈,好像效果不對,視框的大小是足夠了,但是圖像往右偏了一些,導緻最右邊的字母沒有顯示全。

EasyPR--開發詳解

圖29 被偏移的車牌區域

  這次的問題不再是目标圖像的大小問題了,而是視框的偏移問題。仔細觀察一下我們的視框,倘若我們想把車牌全部顯示的話,視框往右偏移一段距離,是不是就可以解決這個問題呢?為保證新的視框中心能夠正好與車牌的中心重合,我們可以選擇偏移xidff/2長度。正如下圖所顯示的一樣。

EasyPR--開發詳解

圖30 考慮偏移的坐标計算

  視框往右偏移的含義就是目标圖像Mat的原點往右偏移。如果原點偏移的話,那麼仿射後圖像的三個關鍵點的坐标要重新計算,都需要減去xidff/2大小。

  重新計算的映射點坐标為下:

EasyPR--開發詳解
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);      
EasyPR--開發詳解

  再試一次。果然,視框被調整到我們希望的地方了,我們可以看到所有的車牌區域了。這次解決的是warpAffine函數帶來的視框偏移問題。

EasyPR--開發詳解

圖31 完整的車牌區域

  關于坐标調整的另一個了解就是當中心點保持不變時,平行四邊形扭正為矩形時恰好是左上的點往左偏移了xdiff/2的距離,左下的點往右偏移了xdiff/2的距離,形成一種對稱的平移。可以使用ps或者inkspace類似的矢量制圖軟體看看“斜切”的效果, 

  如此一來,就完成了偏斜扭正的過程。需要注意的是,向左傾斜的車牌的視框偏移方向與向右傾斜的車牌是相反的。我們可以用slope的正負來判斷車牌是左斜還是右斜。

6.總結

  通過以上過程,我們成功的将一個偏斜的車牌經過旋轉變換等方法扭正過來。

  讓我們回顧一下偏斜扭正過程。我們需要将一個偏斜的車牌扭正,為了達成這個目的我們首先需要對圖像進行旋轉。因為旋轉是個計算量很大的函數,是以我們需要考慮不再用全圖旋轉,而是區域旋轉。在旋轉過程中,會發生圖像截斷問題,是以需要使用擴大化旋轉方法。旋轉以後,隻有偏斜視角的車牌才需要扭正,正視角的車牌不需要,是以還需要一個偏斜判斷過程。如此一來,偏斜扭正的過程需要旋轉,區域截取,擴大化,偏斜判斷等等過程的協助,這就是整個流程中有這麼多步需要處理的原因。

  下圖從另一個視角回顧了偏斜扭正的過程,主要說明了偏斜扭轉中的兩次“截取”過程。

EasyPR--開發詳解

圖32 偏斜扭正全過程

  1. 首先我們擷取RotatedRect,然後對每個RotatedRect擷取外界矩形,也就是ROI區域。外接矩形的計算有可能獲得不安全的坐标,是以需要使用安全的擷取外界矩形的函數。
  2. 擷取安全外接矩形以後,在原圖中截取這部分區域,并放置到一個新的Mat裡,稱之為ROI圖像。這是本過程中第一次截取,使用Mat(Rect ...)函數。
  3. 接下來對ROI圖像根據RotatedRect的角度展開旋轉,旋轉的過程中使用了放大化旋轉法,以此防止車牌區域被截斷。
  4. 旋轉完以後,我們把已經轉正的RotatedRect部分截取出來,稱之為車牌區域。這是本過程中第二次截取,與第一次不同,這次截取使用getRectSubPix()方法。
  5. 接下裡使用偏斜判斷函數來判斷車牌區域裡的車牌是否是傾斜的。
  6. 如果是,則繼續使用仿射變換函數wrapAffine來進行扭正處理,處理過程中要注意三個關鍵點的坐标。
  7. 最後使用resize函數将車牌區域統一化為EasyPR的車牌大小。

  整個過程有一個統一的函數--deskew。下面是deskew的代碼。

EasyPR--開發詳解

View Code

  最後是改善建議:

  角度偏斜判斷時可以用白色區域的輪廓來确定平行四邊形的四個點,然後用這四個點來計算斜率。這樣算出來的斜率的可能魯棒性更好。

三. 總結

  本篇文檔介紹了顔色定位與偏斜扭轉等功能。其中顔色定位屬于作者一直想做的定位方法,而偏斜扭轉則是作者以前認為不可能解決的問題。這些問題現在都基本被攻克了,并在這篇文檔中闡述,希望這篇文檔可以幫助到讀者。

  作者希望能在這片文檔中不僅傳遞知識,也傳授我在摸索過程中積累的經驗。因為光知道怎麼做并不能加深對車牌識别的認識,隻有經曆過失敗,了解哪些思想嘗試過,碰到了哪些問題,是如何解決的,才能幫助讀者更好地認識這個系統的内涵。

  最後,作者很感謝能夠閱讀到這裡的讀者。如果看完覺得好的話,還請輕輕點一下贊,你們的鼓勵就是作者繼續行文的動力。

對EasyPR做下說明:EasyPR,一個開源的中文車牌識别系統,代碼托管在github。其次,在前面的部落格文章中,包含EasyPR至今的開發文檔與介紹。在後續的文章中,作者會介紹EasyPR中字元分割與識别等相關内容,歡迎繼續閱讀

在前面的幾篇文章中,我們介紹了EasyPR中車牌定位子產品的相關内容。本文開始分析車牌定位子產品後續步驟的車牌判斷子產品。車牌判斷子產品是EasyPR中的基于機器學習模型的一個子產品,這個模型就是作者前文中從機器學習談起中提到的SVM(支援向量機)。

  我們已經知道,車牌定位子產品的輸出是一些候選車牌的圖檔。但如何從這些候選車牌圖檔中甄選出真正的車牌,就是通過SVM模型判斷/預測得到的。

EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解
EasyPR--開發詳解

圖1 從候選車牌中選出真正的車牌

  簡單來說,EasyPR的車牌判斷子產品就是将候選車牌的圖檔一張張地輸入到SVM模型中,然後問它,這是車牌麼?如果SVM模型回答不是,那麼就繼續下一張,如果是,則把圖檔放到一個輸出清單裡。最後把清單輸入到下一步處理。由于EasyPR使用的是清單作為輸出,是以它可以輸出一副圖檔中所有的車牌,不像一些車牌識别程式,隻能輸出一個車牌結果。

EasyPR--開發詳解
EasyPR--開發詳解

圖2 EasyPR輸出多個車牌

  現在,讓我們一步步地,進入這個SVM模型的核心看看,它是如何做到判斷一副圖檔是車牌還是不是車牌的?本文主要分為三個大的部分:

  1. SVM應用:描述如何利用SVM模型進行車牌圖檔的判斷。
  2. SVM訓練:說明如何通過一系列步驟得到SVM模型。
  3. SVM調優:讨論如何對SVM模型進行優化,使其效果更加好。

一.SVM應用

  人類是如何判斷一個張圖檔所表達的資訊呢?簡單來說,人類在成長過程中,大腦記憶了無數的圖像,并且依次給這些圖像打上了标簽,例如太陽,天空,房子,車子等等。你們還記得當年上幼稚園時的那些教科書麼,上面一個太陽,下面是文字。圖像的組成事實上就是許多個像素,由像素組成的這些資訊被輸入大腦中,然後得出這個是什麼東西的回答。我們在SVM模型中一開始輸入的原始資訊也是圖像的所有像素,然後SVM模型通過對這些像素進行分析,輸出這個圖檔是否是車牌的結論。

EasyPR--開發詳解

圖3 通過圖像來學習

  SVM模型處理的是最簡單的情況,它隻要回答是或者不是這個“二值”問題,比從許多類中檢索要簡單很多。

  我們可以看一下SVM進行判斷的代碼:

EasyPR--開發詳解

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。

EasyPR--開發詳解

圖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檔案。

EasyPR--開發詳解

圖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個步驟,見下圖:

EasyPR--開發詳解

圖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版本時,新增了一大批資料,是以又有了貼标簽的過程。幸好使用這個方法,使得相關時間大幅度減少。這個方法叫做逐次疊代自動标簽法。在後面會介紹這個方法。

  貼标簽後的車牌資料如下圖:

EasyPR--開發詳解

圖7 在HasPlate檔案夾下的圖檔

  貼标簽後的非車牌資料下圖:

EasyPR--開發詳解

圖8 在NoPlate檔案夾下的圖檔

  擁有了貼好标簽的資料以後,下面的步驟是分組,也稱之為divide過程。

3. divide (學習資料->分組資料)

  分組這個過程是EasyPR1.1版新引入的方法。

  在貼完标簽以後,我擁有了車牌圖檔和非車牌圖檔共幾千張。在我直接訓練前,不急。先拿出30%的資料,隻用剩下的70%資料進行SVM模型的訓練,訓練好的模型再用這30%資料進行一個效果測試。這30%資料充當的作用就是一個評判資料測試集,稱之為test data,另70%資料稱之為train data。于是一個完整的learn data被分為了train data和test data。

EasyPR--開發詳解

圖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下面又有兩個子檔案夾,他們的結構樹就是下圖:

EasyPR--開發詳解

圖10 分組後的檔案樹

  divide資料結束以後,我們就可以進入真正的機器學習過程。也就是對資料的訓練過程。

4. train (訓練資料->模型)

  模型在代碼裡的代表就是CvSVM類。在這一步中所要做的就是加載train data,然後用CvSVM類的train方法進行訓練。這個步驟隻針對的是上步中生成的總資料70%的訓練資料。

  具體來說,分為以下幾個子步驟:

  1) 加載待訓練的車牌資料。見下面這段代碼。

EasyPR--開發詳解
EasyPR--開發詳解
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);
    }
}              
EasyPR--開發詳解

  注意看,車牌圖像我存儲在的是一個vector<Mat>中,而标簽資料我存儲在的是一個vector<int>中。我将train/HasPlate中的圖像依次取出來,存入vector<Mat>。每存入一個圖像,同時也往 vector<int>中存入一個int值1,也就是說圖像和标簽分别存在不同的vector對象裡,但是保持一一對應的關系。

  2) 加載待訓練的非車牌資料,見下面這段代碼中的函數。基本内容與加載車牌資料類似,不同之處在于檔案夾是train/NoPlate,并且我往vector<int>中存入的是int值0,代表無車牌。

EasyPR--開發詳解
EasyPR--開發詳解
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);
    }
}      
EasyPR--開發詳解

  3) 将兩者合并。目前擁有了兩個vector<Mat>和兩個vector<int>。将代表車牌圖檔和非車牌圖檔資料的兩個 vector<Mat>組成一個新的Mat--trainingData,而代表車牌圖檔與非車牌圖檔标簽的兩個 vector<int>組成另一個Mat--classes。接着做一些資料類型的調整,以讓其符合svm訓練函數train的要求。這些做完後,資料的準備工作基本結束,下面就是參數配置的工作。

EasyPR--開發詳解
EasyPR--開發詳解
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);      
EasyPR--開發詳解

  4) 配置SVM模型的訓練參數。SVM模型的訓練需要一個CvSVMParams的對象,這個類是SVM模型中訓練對象的參數的組合,如何給這裡的參數指派,是很有講究的一個工作。注意,這裡是SVM訓練的核心内容,也是最能展現一個機器學習專家和新手差別的地方。機器學習最後模型的效果差異有很大因素取決與模型訓練時的參數,尤其是SVM,有非常多的參數供你配置(見下面的代碼)。參數衆多是一個問題,更為顯著的是,機器學習模型中參數的一點微調都可能帶來最終結果的巨大差異。

EasyPR--開發詳解
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);      
EasyPR--開發詳解

  opencv官網文檔對CvSVMParams類的各個參數有一個詳細的解釋。如果你上過SVM課程的理論部分,你可能對這些參數的意思能搞的明白。但在這裡,我們可以不去管參數的含義,因為我們有更好的方法去解決這個問題。

EasyPR--開發詳解

圖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表示實際不為車牌的數量。

  準确率的計算公式是:

EasyPR--開發詳解

圖12 precise 準确率

  查全率是統計真正的車牌圖檔中,我預測為車牌的圖檔所占的比例。同上,我們用ptrue_rtrue表示預測與實際都為車牌的數量。用pfalse_rtrue表示實際為車牌,但我預測為非車牌的數量。

  查全率的計算公式是:

EasyPR--開發詳解

圖13 recall 查全率

  recall的公式與precision公式唯一的差別在于右下角。precision是ptrue_rfalse,代表預測為車牌但實際不是的數量;而recall是pfalse_rtrue,代表預測是非車牌但其實是車牌的數量。

  簡單來說,precision名額的期望含義就是要“查的準”,recall的期望含義就是“不要漏”。查全率還有一個翻譯叫做“召回率”。但很明顯,召回這個詞沒有反映出查全率所展現出的不要漏的含義。

  值得說明的是,precise和recall這兩個值自然是越高越好。但是如果一個高,一個低的話效果會如何,如何跟兩個都中等的情況進行比較?為了能夠數字化這種比較。機器學習界又引入了FScore這個數值。當precise和recall兩者中任一者較高,而另一者較低是,FScore 都會較低。兩者中等的情況下Fscore表現比一高一低要好。當兩者都很高時,FScore會很高。

  FScore的計算公式如下圖:

EasyPR--開發詳解

圖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函數的調用代碼如下:

EasyPR--開發詳解
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);      
EasyPR--開發詳解

  你唯一需要做的就是泡杯茶,翻翻書,然後慢慢等待這計算機幫你處理好所有事情(時間較長,因為每次調整參數又得重新訓練一次)。作者最近的一次訓練的耗時為1個半小時)。

  訓練完畢後,看看模型和參數在test data上的表現把。99%的precise和98%的recall。非常棒,比任何一次手工配的效果都好。

3.特征提取

  在rbf核介紹時提到過,輸入資料的特征的次元現在是172,那麼這個數字是如何計算出來的?現在的特征用的是直方統計函數,也就是先把圖像二值化,然後統計圖像中一行元素中1的數目,由于輸入圖像有36行,是以有36個值,再統計圖像中每一列中1的數目,圖像有136列,是以有136個值,兩者相加正好等于172。新的輸入資料的特征提取函數就是下面的代碼:

EasyPR--開發詳解
// ! 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);
}      
EasyPR--開發詳解

  我們輸入資料的特征不再是全部的三原色的像素值了,而是抽取過的一些特征。從原始的圖像到抽取後的特征的過程就被稱為特征提取的過程。在1.0版中沒有特征提取的概念,是直接把圖像中全部像素作為特征的。這要感謝群裡的“如果有一天”同學,他堅持認為全部像素的輸入是最低級的做法,認為用特征提取後的效果會好多。我問大概能到多少準确率,當時的準确率有92%,我以為已經很高了,結果他說能到99%。在半信半疑中我嘗試了,果真如他所說,結合了rbf核與新特征訓練的模型達到的precise在99%左右,而且recall也有98%,這真是個令人咋舌并且非常驚喜的成績。

  “如果有一天”建議用的是SFIT特征提取或者HOG特征提取,由于時間原因,這兩者我沒有實作,但是把函數留在了那裡。留待以後有時間完成。在這個過程中,我充分體會到了開源的力量,如果不是把軟體開源,如果不是有這麼多優秀的大家一起讨論,這樣的思路與改善是不可能出現的。

4.接口函數

  由于有SIFT以及HOG等特征沒有實作,而且未來有可能會有更多有效的特征函數出現。是以我把特征函數抽象為借口。使用回調函數的思路實作。所有回調函數的代碼都在feature.cpp中,開發者可以實作自己的回調函數,并把它指派給EasyPR中的某個函數指針,進而實作自定義的特征提取。也許你們會有更多更好的特征的想法與創意。

  關于特征其實有更多的思考,原始的SVM模型的輸入是圖像的全部像素,正如人類在小時候通過圖像識别各種事物的過程。後來SVM模型的輸入是經過抽取的特 征。正如随着人類接觸的事物越來越多,會發現單憑圖像越來越難區分一些非常相似的東西,于是學會了總結特征。例如太陽就是圓的,黃色,在天空等,可以憑借 這些特征就進行區分和判斷。

從本質上說,特征是區分事物的關鍵特性。這些特性,一定是從某些次元去看待的。例如,蘋果和梨子,一個是綠色,一個是黃色,這就是顔色的次元;魚和鳥,一個在水裡,一個在空中,這是位置的區分,也就是空間的次元。特征,是許多元度中最有區分意義的次元。傳統資料倉庫中的OLAP,也稱為多元分析,提供了人類從多個次元觀察,比較的能力。通過人類的觀察比較,從多個次元中挑選出來的次元,就是要分析目标的特征。從這點來看,機器學習與多元分析有了關聯。多元分析提供了選擇特征的能力。而機器學習可以根據這些特征進行模組化。

  機器學習界也有很多算法,專門是用來從資料中抽取特征資訊的。例如傳統的PCA(主成分分析)算法以及最近流行的深度學習中的 AutoEncoder(自動編碼機)技術。這些算法的主要功能就是在資料中學習出最能夠明顯區分資料的特征,進而提升後續的機器學習分類算法的效果。

  說一個特征學習的案例。作者買車時,經常會把大衆的兩款車--邁騰與帕薩特給弄混,因為兩者實在太像了。大家可以到網上去搜一下這兩車的圖檔。如果不依賴後排的文字,光靠外形實在難以将兩車區分開來(雖然從生産商來說,前者是一汽大衆生産的,産地在長春,後者是上海大衆生産的,産地在上海。兩個不同的公司,南北兩個地方,相差了十萬八千裡)。後來我通過仔細觀察,終于發現了一個明顯區分兩輛車的特征,後來我再也沒有認錯過。這個特征就是:邁騰的前臉有四條銀杠,而帕薩特隻有三條,邁騰比帕薩特多一條銀杠。可以這麼說,就是這麼一條銀杠,分割了北和南兩個地方生産的汽車。

EasyPR--開發詳解

圖15 一條銀杠,分割了“北”和“南”

  在這裡區分的過程,我是通過不斷學習與研究才發現了這些區分的特征,這充分說明了事物的特征也是可以被學習的。如果讓機器學習中的特征選擇方法 PCA和AutoEncoder來分析的話,按理來說它們也應該找出這條銀杠,否則它們就無法做到對這兩類最有效的分類與判斷。如果沒有找到的話,證明我們目前的特征選擇算法還有很多的改進空間(與這個案例類似的還有大衆的另兩款車,高爾夫和Polo。它們兩的區分也是用同樣的道理。相比邁騰和帕薩特,高爾夫和Polo價格差别的更大,是以區分的特征也更有價值)。

  5.自動化

  最後我想簡單談一下EasyPR1.1新增的自動化訓練功能與指令行。大家可能看到第二部分介紹SVM訓練時我将過程分成了5個步驟。事實上,這些步驟中的很多過程是可以子產品化的。一開始的時候我寫一些不相關的代碼函數,幫我處理各種需要解決的問題,例如資料的分組,打标簽等等。但後來,我把思路理清後,我覺得這幾個步驟中很多的代碼都可以通用。于是我把一些步驟子產品化出來,形成通用的函數,并寫了一個指令行界面去調用它們。在你運作EasyPR1.1版後,在你看到的第一個指令行界面選擇“3.SVM訓練過程”,你就可以看到這些全部的指令。

EasyPR--開發詳解

圖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開發的第六篇?事實上,對于目前的車牌定位子產品我們團隊覺得還有改進空間,是以第五篇的詳解内容是留給改進後的車牌定位子產品的。如果有車牌定位子產品方面好的建議或者希望加入開源團隊,歡迎跟我們團隊聯系([email protected] )。您也可以為中國的開源事業做出一份貢獻

繼續閱讀