1. 程式人生 > >車牌識別EasyPR--開發詳解

車牌識別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的處理流程:

本開源專案的目標客戶群有三類:
  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(價值)。

  見下圖的說明:

圖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。我撰寫這系列文章的目的是: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”的作用與重要性之前,請看下面這兩幅圖片。

圖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資料夾下。對多個車牌圖片處理的結果僅會保留最後一個車牌圖片的臨時圖片。

  1、原始圖片。

  2、經過高斯模糊後的圖片。經過這步處理,可以看出影象變的模糊了。這步的作用是為接下來的Sobel運算元去除干擾的噪聲。

  3、將影象進行灰度化。這個步驟是一個分水嶺,意味著後面的所有操作都不能基於色彩資訊了。此步驟是利是弊,後面再做分析。

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

  5、對影象進行二值化。將灰度影象(每個畫素點有256個取值可能)轉化為二值影象(每個畫素點僅有1和0兩個取值可能)。

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

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

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

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

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

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

3.“Plate Locate”的深入討論與調優策略

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

  由於篇幅關係,下面的深入討論放在下期

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

一、高斯模糊

1.目標

  對影象去噪,為邊緣檢測演算法做準備。  

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的處理步驟是先進行高斯模糊,再進行灰度化。從目前的實驗結果來看,基於色彩的高斯模糊過程比灰度後的高斯模糊過程更容易檢測到邊緣點。

二、灰度化處理

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後效果

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

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

  1. //! 定位車牌影象
  2. //! src 原始影象
  3. //! resultVec 一個Mat的向量,儲存所有抓取到的影象
  4. //! 成功返回0,否則返回-1
  5. int CPlateLocate::plateLocate(Mat src, vector<Mat>& resultVec)  
  6. {  
  7.     Mat src_blur, src_gray;  
  8.     Mat grad;  
  9.     int scale = SOBEL_SCALE;  
  10.     int delta = SOBEL_DELTA;  
  11.     int ddepth = SOBEL_DDEPTH;  
  12.     if( !src.data )  
  13.     { return -1; }  
  14.     //高斯模糊。Size中的數字影響車牌定位的效果。
  15.     GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize),   
  16.         0, 0, BORDER_DEFAULT );  
  17.     if(m_debug)  
  18.     {   
  19.         stringstream ss(stringstream::in | stringstream::out);  
  20.         ss << "tmp/debug_GaussianBlur" << ".jpg";  
  21.         imwrite(ss.str(), src_blur);  
  22.     }  
  23.     /// Convert it to gray
  24.     cvtColor( src_blur, src_gray, CV_RGB2GRAY );  
  25.     if(m_debug)  
  26.     {   
  27.         stringstream ss(stringstream::in | stringstream::out);  
  28.         ss << "tmp/debug_gray" << ".jpg";  
  29.         imwrite(ss.str(), src_gray);  
  30.     }  
  31.     /// Generate grad_x and grad_y
  32.     Mat grad_x, grad_y;  
  33.     Mat abs_grad_x, abs_grad_y;  
  34.     /// Gradient X
  35.     //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
  36.     Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );  
  37.     convertScaleAbs( grad_x, abs_grad_x );  
  38.     /// Gradient Y
  39.     //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
  40.     Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );  
  41.     convertScaleAbs( grad_y, abs_grad_y );  
  42.     /// Total Gradient (approximate)
  43.     addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad );  
  44.     //Laplacian( src_gray, grad_x, ddepth, 3, scale, delta, BORDER_DEFAULT );  
  45.     //convertScaleAbs( grad_x, grad );  
  46.     if(m_debug)  
  47.     {   
  48.         stringstream ss(stringstream::in | stringstream::out);  
  49.         ss << "tmp/debug_Sobel" << ".jpg";  
  50.         imwrite(ss.str(), grad);  
  51.     }  
  52.     Mat img_threshold;  
  53.     threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);  
  54.     //threshold(grad, img_threshold, 75, 255, CV_THRESH_BINARY);
  55.     if(m_debug)  
  56.     {   
  57.         stringstream ss(stringstream::in | stringstream::out);  
  58.         ss << "tmp/debug_threshold" << ".jpg";  
  59.         imwrite(ss.str(), img_threshold);  
  60.     }  
  61.     Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );  
  62.     morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);  
  63.     if(m_debug)  
  64.     {   
  65.         stringstream ss(stringstream::in | stringstream::out);  
  66.         ss << "tmp/debug_morphology" << ".jpg";  
  67.         imwrite(ss.str(), img_threshold);  
  68.     }  
  69.     //Find 輪廓 of possibles plates
  70.     vector< vector< Point> > contours;  
  71.     findContours(img_threshold,  
  72.         contours, // a vector of contours
  73.         CV_RETR_EXTERNAL, // 提取外部輪廓
  74.         CV_CHAIN_APPROX_NONE); // all pixels of each contours
  75.     Mat result;  
  76.     if(m_debug)  
  77.     {   
  78.         //// Draw blue contours on a white image
  79.         src.copyTo(result);  
  80.         drawContours(result, contours,  
  81.             -1, // draw all contours
  82.             Scalar(0,0,255), // in blue
  83.             1); // with a thickness of 1
  84.         stringstream ss(stringstream::in | stringstream::out);  
  85.         ss << "tmp/debug_Contours" << ".jpg";  
  86.         imwrite(ss.str(), result);  
  87.     }  
  88.     //Start to iterate to each contour founded
  89.     vector<vector<Point> >::iterator itc = contours.begin();  
  90.     vector<RotatedRect> rects;  
  91.     //Remove patch that are no inside limits of aspect ratio and area.
  92.     int t = 0;  
  93.     while (itc != contours.end())  
  94.     {  
  95.         //Create bounding rect of object
  96.         RotatedRect mr = minAreaRect(Mat(*itc));  
  97.         //large the rect for more
  98.         if( !verifySizes(mr))  
  99.         {  
  100.             itc = contours.erase(itc);  
  101.         }  
  102.         else
  103.         {  
  104.             ++itc;  
  105.             rects.push_back(mr);  
  106.         }  
  107.     }  
  108.     int k = 1;  
  109.     for(int i=0; i< rects.size(); i++)  
  110.     {  
  111.         RotatedRect minRect = rects[i];  
  112.         if(verifySizes(minRect))  
  113.         {      
  114.             // rotated rectangle drawing 
  115.             // Get rotation matrix
  116.             // 旋轉這部分程式碼確實可以將某些傾斜的車牌調整正,
  117.             // 但是它也會誤將更多正的車牌搞成傾斜!所以綜合考慮,還是不使用這段程式碼。
  118.             // 2014-08-14,由於新到的一批圖片中發現有很多車牌是傾斜的,因此決定再次嘗試
  119.             // 這段程式碼。
  120.             if(m_debug)  
  121.             {   
  122.                 Point2f rect_points[4];   
  123.                 minRect.points( rect_points );  
  124.                 forint j = 0; j < 4; j++ )  
  125.                     line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );  
  126.             }  
  127.             float r = (float)minRect.size.width / (float)minRect.size.height;  
  128.             float angle = minRect.angle;  
  129.             Size rect_size = minRect.size;  
  130.             if (r < 1)  
  131.             {  
  132.                 angle = 90 + angle;  
  133.                 swap(rect_size.width, rect_size.height);  
  134.             }  
  135.             //如果抓取的方塊旋轉超過m_angle角度,則不是車牌,放棄處理
  136.             if (angle - m_angle < 0 && angle + m_angle > 0)  
  137.             {  
  138.                 //Create and rotate image
  139.                 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);  
  140.                 Mat img_rotated;  
  141.                 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);  
  142.                 Mat resultMat;  
  143.                 resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);  
  144.                 resultVec.push_back(resultMat);  
  145.             }  
  146.         }  
  147.     }  
  148.     if(m_debug)  
  149.     {   
  150.         stringstream ss(stringstream::in | stringstream::out);  
  151.         ss << "tmp/debug_result" << ".jpg";  
  152.         imwrite(ss.str(), result);  
  153.     }  
  154.     return 0;  
  155. }  

  首先,我們通過二值化處理將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,,計算公