EasyPR--开发详解
我正在做一個開源的中文車牌識別系統,Git地址為:https://github.com/liuruoze/EasyPR。
我給它取的名字為EasyPR,也就是Easy to do Plate Recognition的意思。我開發這套系統的主要原因是因為我希望能夠鍛煉我在這方面的能力,包括C++技術、計算機圖形學、機器學習等。我把這個項目開源的主要目的是:1.它基于開源的代碼誕生,理應回歸開源;2.我希望有人能夠一起協助強化這套系統,包括代碼、訓練數據等,能夠讓這套系統的準確性更高,魯棒性更強等等。
相比于其他的車牌識別系統,EasyPR有如下特點:
系統還提供全套的訓練數據提供(包括車牌檢測的近500個車牌和字符識別的4000多個字符)。所有全部都可以在Github的項目地址上直接下載到。
那么,EasyPR是如何產生的呢?我簡單介紹一下它的誕生過程:
首先,在5月份左右時我考慮要做一個車牌識別系統。這個車牌系統中所有的代碼都應該是開源的,不能基于任何黑盒技術。這主要起源于我想鍛煉自己的C++和計算機視覺的水平。
我在網上開始搜索了資料。由于計算機視覺中很多的算法我都是使用openCV,而且openCV發展非常良好,因此我查找的項目必須得是基于OpenCV技術的。于是我在CSDN的博客上找了一篇文章。
文章的作者taotao1233在這兩篇博客中以半學習筆記半開發講解的方式說明了一個車牌識別系統的全部開發過程。非常感謝他的這些博客,借助于這些資料,我著手開始了開發。當時的想法非常樸素,就是想看看按照這些資料,能否真的實現一個車牌識別的系統。關于車牌照片數據的問題,幸運的很,我正在開發的一個項目中有大量的照片,因此數據不是問題。
令人高興的是,系統確實能夠工作,但是讓人沮喪的,似乎也就“僅僅”能夠工作而已。在車牌檢測這個環節中正確性已經慘不忍睹。
這個事情給了我一撥不小的冷水,本來我以為很快的開發進度看來是樂觀過頭了。于是我決定沉下心來,仔細研究他的系統實現的每一個過程,結合OpenCV的官網教程與API資料,我發現他的實現系統中有很多并不適合我目前在做的場景。
我手里的數據大部分是高速上的圖像抓拍數據,其中每個車牌都偏小,而且模糊度較差。直接使用他們的方法,正確率低到了可怕的地步。于是我開始嘗試利用openCv中的一些函數與功能,替代,增加,調優等等方法,不斷的優化。這個過程很漫長,但是也有很多的積累。我逐漸發現,并且了解他系統中每一個步驟的目的,原理以及如果修改可以進行優化的方法。
在最終實現的代碼中,我的代碼已經跟他的原始代碼有很多的不一樣了,但是成功率大幅度上升,而且車牌的正確檢測率不斷被優化。在系列文章的后面,我會逐一分享這些優化的過程與心得。
最終我實現的系統與他的系統有以下幾點不同:
盡管我和他的系統有這么多的不同,但是我們在根本的系統結構上是一致的。應該說,我們都是參照了“Mastering OpenCV”這本數的處理結構。在這點上,我并沒有所“創新”,事實上,結果也證明了“Mastering OpenCV”上的車牌識別的處理邏輯,是一個實際有效的最佳處理流程。
“Mastering OpenCV”,包括我們的系統,都是把車牌識別劃分為了兩個過程:即車牌檢測(Plate Detection)和字符識別(Chars Recognition)兩個過程。可能有些書籍或論文上不是這樣叫的,但是我覺得,這樣的叫法更容易理解,也不容易搞混。
- 車牌檢測(Plate Detection):對一個包含車牌的圖像進行分析,最終截取出只包含車牌的一個圖塊。這個步驟的主要目的是降低了在車牌識別過程中的計算量。如果直接對原始的圖像進行車牌識別,會非常的慢,因此需要檢測的過程。在本系統中,我們使用SVM(支持向量機)這個機器學習算法去判別截取的圖塊是否是真的“車牌”。
- 字符識別(Chars Recognition):有的書上也叫Plate Recognition,我為了與整個系統的名稱做區分,所以改為此名字。這個步驟的主要目的就是從上一個車牌檢測步驟中獲取到的車牌圖像,進行光學字符識別(OCR)這個過程。其中用到的機器學習算法是著名的人工神經網絡(ANN)中的多層感知機(MLP)模型。最近一段時間非常火的“深度學習”其實就是多隱層的人工神經網絡,與其有非常緊密的聯系。通過了解光學字符識別(OCR)這個過程,也可以知曉深度學習所基于的人工神經網路技術的一些內容。
下圖是一個完整的EasyPR的處理流程:
本開源項目的目標客戶群有三類:
第一類客戶是本項目的主要使用者,因此項目特地被精心劃分為了6個模塊,以供開發者按需選擇。
第二類客戶可能會有部分,EasyPR有一個同級項目EasyPR_Dll,可以DLL方式嵌入到其他的程序中,另外還有個一個同級項目EasyPR_Win,基于WTL開發的界面程序,可以簡化與幫助車牌識別的結果比對過程。
對于第三類客戶,可以這么說,有完整的全套代碼和詳細的說明,我相信你們可以稍作修改就可以通過設計大考。
推薦你使用EasyPR有以下幾點理由:
- 這里面的代碼都是作者親自優化過的,你可以在上面做修改,做優化,甚至一起協作開發,一些處理車牌的細節方法你應該是感興趣的。
- 如果你對代碼不感興趣,那么經過作者精心訓練的模型,包括SVM和ANN的模型,可以幫助你提升或驗證你程序的正確率。
- 如果你對模型也不感興趣,那么成百上千經過作者親自挑選的訓練數據生成的文件,你應該感興趣。作者花了大量的時間處理這些訓練數據與調整,現在直接提供給你,可以大幅度減輕很多人缺少數據的難題。
有興趣的同志可以留言或發Email: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。我撰寫這系列文章的目的是: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_yMat 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,,計算公式如下:
如果想使用反二值化,可以使用參數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中,調用閉操作的方法是首先建立矩形模板,矩形的大小是可以設置的,由于矩形是用來覆蓋以中心像素的所有其他像素,因此矩形的寬和高最好是奇數。
通過以下代碼設置矩形的寬和高。
在這里,我們使用了類成員變量,這兩個類成員變量在構造函數中被賦予了初始值。寬是17,高是3.
設置完矩形的寬和高以后,就可以調用形態學操作了。opencv中所有形態學操作有一個統一的函數,通過參數來區分不同的具體操作。例如MOP_CLOSE代表閉操作,MOP_OPEN代表開操作。
如果我對二值化的圖像進行開操作,結果會是什么樣的?下圖是圖像使用閉操作與開操作處理后的一個區別:
圖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// 默認為falseconst float max_sv = 255;const float minref_sv = 64;const float minabs_sv = 95;//blue的H范圍const int min_blue = 100; //100const int max_blue = 140; //140//yellow的H范圍const int min_yellow = 15; //15const int max_yellow = 40; //40Mat 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-180int S = int(p[j + 1]); //0-255int V = int(p[j + 2]); //0-255s_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;elseHdiff = avg_h - H;float Hdiff_p = float(Hdiff) / diff_h;// S和V的最小值由adaptive_minsv這個bool值判斷// 如果為true,則最小值取決于H值,按比例衰減// 如果為false,則不再自適應,使用固定的最小值minabs_svfloat 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)elsemin_sv = minabs_sv; // addif ((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計算得到的:
為了計算目標矩形的三個關鍵點坐標,我們首先需要把扭轉后的原點坐標調整到平行四邊形車牌區域左上角位置。見下圖。
圖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 偏斜扭正全過程
整個過程有一個統一的函數--deskew。下面是deskew的代碼。
View Code
最后是改善建議:
角度偏斜判斷時可以用白色區域的輪廓來確定平行四邊形的四個點,然后用這四個點來計算斜率。這樣算出來的斜率的可能魯棒性更好。
三. 總結
本篇文檔介紹了顏色定位與偏斜扭轉等功能。其中顏色定位屬于作者一直想做的定位方法,而偏斜扭轉則是作者以前認為不可能解決的問題。這些問題現在都基本被攻克了,并在這篇文檔中闡述,希望這篇文檔可以幫助到讀者。
作者希望能在這片文檔中不僅傳遞知識,也傳授我在摸索過程中積累的經驗。因為光知道怎么做并不能加深對車牌識別的認識,只有經歷過失敗,了解哪些思想嘗試過,碰到了哪些問題,是如何解決的,才能幫助讀者更好地認識這個系統的內涵。
最后,作者很感謝能夠閱讀到這里的讀者。如果看完覺得好的話,還請輕輕點一下贊,你們的鼓勵就是作者繼續行文的動力。
對EasyPR做下說明:EasyPR,一個開源的中文車牌識別系統,代碼托管在github。其次,在前面的博客文章中,包含EasyPR至今的開發文檔與介紹。在后續的文章中,作者會介紹EasyPR中字符分割與識別等相關內容,歡迎繼續閱讀
在前面的幾篇文章中,我們介紹了EasyPR中車牌定位模塊的相關內容。本文開始分析車牌定位模塊后續步驟的車牌判斷模塊。車牌判斷模塊是EasyPR中的基于機器學習模型的一個模塊,這個模型就是作者前文中從機器學習談起中提到的SVM(支持向量機)。
我們已經知道,車牌定位模塊的輸出是一些候選車牌的圖片。但如何從這些候選車牌圖片中甄選出真正的車牌,就是通過SVM模型判斷/預測得到的。
圖1 從候選車牌中選出真正的車牌
簡單來說,EasyPR的車牌判斷模塊就是將候選車牌的圖片一張張地輸入到SVM模型中,然后問它,這是車牌么?如果SVM模型回答不是,那么就繼續下一張,如果是,則把圖片放到一個輸出列表里。最后把列表輸入到下一步處理。由于EasyPR使用的是列表作為輸出,因此它可以輸出一副圖片中所有的車牌,不像一些車牌識別程序,只能輸出一個車牌結果。
圖2 EasyPR輸出多個車牌
現在,讓我們一步步地,進入這個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) 加載待訓練的車牌數據。見下面這段代碼。
注意看,車牌圖像我存儲在的是一個vector<Mat>中,而標簽數據我存儲在的是一個vector<int>中。我將train/HasPlate中的圖像依次取出來,存入vector<Mat>。每存入一個圖像,同時也往 vector<int>中存入一個int值1,也就是說圖像和標簽分別存在不同的vector對象里,但是保持一一對應的關系。
2) 加載待訓練的非車牌數據,見下面這段代碼中的函數。基本內容與加載車牌數據類似,不同之處在于文件夾是train/NoPlate,并且我往vector<int>中存入的是int值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文件里,留待下次使用:
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模型總共可以支持多少種核呢。根據官方文檔,支持的核類型有以下幾種:
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。新的輸入數據的特征提取函數就是下面的代碼:
我們輸入數據的特征不再是全部的三原色的像素值了,而是抽取過的一些特征。從原始的圖像到抽取后的特征的過程就被稱為特征提取的過程。在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 )。您也可以為中國的開源事業做出一份貢獻
轉載于:https://www.cnblogs.com/asks/p/4372736.html
總結
以上是生活随笔為你收集整理的EasyPR--开发详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Maven 入门 (1)—— 安装
- 下一篇: modelsim 的高效使用