從零使用OpenCV快速實現簡單車牌識別系統
不錯的計算機視覺部落格:http://blog.csdn.net/lee_cv/article/details/9180719
篇文章獻給所有第一次聽說車牌識別ANPR但需要短時間實現的苦逼同學們。
最近的小學期實訓做的是一個車牌識別系統,說實話真不知道學校怎麼想的,雖然說影象處理也算的上是數字媒體很重要的一塊分支了,但咱這幾年學的全是圖形渲染啊。圖形與影象雖然只差了一個字,但內容真是差了十萬八千里了(當然這話是誇張了,事實上在使用shader進行特效渲染的最後一步往往都是在做影象處理,如Bloom, Outline, Field Depth等,但這些也只是用到了影象處理中很基礎的一部分)。
小學期不到10天的時間要從零搞出個車牌識別系統,更別說我們所有組員全都有實習,老師大撒把,發了需求直接不管。當時本來還覺得這種應用廣泛的東西網上肯定有現成的例子,隨便改改就好,結果搜了半天,最令我崩潰的一句話就是:“這東西你要真做出來了就賣錢去吧”。擦,算了,求人不如求自己,最終在OpenCV的幫助下我看了兩天資料,寫了兩天程式,居然就實現了,看到從原始圖片中摳出車牌,再從車牌中摳出數字,再匹配出結果(這步是我同學做的),我自己都覺得神奇啊!雖然肯定是買不了錢,但還是很激動啊~~
廢話不多說了,以後和我一樣做這個實訓專案的同學們可有福了,接下來就詳細講講如何簡單實現ANPR(Automatic Number Plate Recognition)吧。
配置環境
我使用的是OpenCV 2.3.1和VS2010,下載與配置方法在opencv的中國官網www.opencv.org.cn上都有詳細介紹。像這種開源庫最麻煩的就是環境配置了,什麼makefile這種東西我看著就頭大,當然對於我這種菜鳥人家提供了CMake來幫你進行傻瓜式的一鍵配置,不過這對於我來說還是麻煩,畢竟還要再裝個程式。幸運的是我下載的是SuperPack版本,也就是說在opencv/build目錄下已經有人家編譯好的全部語言、開發平臺的lib, dll以及標頭檔案。雖然很大,但是下下來就能用^_^,咱的追求就是簡單,更簡單!
建立工程後要做的就是在工程屬性的C++目錄中將相應的include資料夾,lib資料夾配置進去,另外還要連結上你需要使用的庫。
看網上一片教程說在屬性——連結器——輸入中配置附加依賴項:opencv_calib3d231d.lib; opencv_contrib231d.lib; opencv_core231d.lib;opencv_features2d231d.lib; opencv_flann231d.lib; opencv_gpu231d.lib;opencv_highgui231d.lib; opencv_imgproc231d.lib; opencv_legacy231d.lib;opencv_ml231d.lib; opencv_objdetect231d.lib; opencv_ts231d.lib;opencv_video231d.lib。
NND,這麼多哪兒記得住啊,下次新建一個工程還得網上找這篇文章拷貝貼上麼?當然不用,只少在車牌識別系統中,我們所需要的只有三個庫,而且在VS中我們可以使用預編譯指令連線這些庫,這樣在你將工程拷貝給同學的時候就不用再擔心環境配置的問題了。
#pragma comment(lib, “opencv_core231d.lib”)
#pragma comment(lib, “opencv_imgproc231d.lib”)
#pragma comment(lib, “opencv_highgui231d.lib”)
core是opencv的核心庫,一些主要的資料結構都在這裡定義,imgproc顧名思義包含了主要的影象處理函式,highgui是一個簡單的顯示框架,幫助快速建立視窗顯示影象等,就好像opengl中的glut。如果使用MFC框架進行顯示的話需要額外新增一個類CvvImage,具體情況網上隨便一搜就有,不廢話了。
基本演算法
OpenCV只是一個提供基本影象處理方法的工具庫,具體應用到車牌識別系統中,需要綜合實用影象處理方法,可不能指望OpenCV中直接有個函式幫你把大部分事兒都做了(我一開始就這麼期望的…)。
大致的演算法網上可以找到很多論文,其中我主要參考了以下兩篇:
這倆篇都通俗易懂,演算法比較簡單實用,適合初學者上手。從整體看,車牌號識別主要分三步走:1.提取車牌 2.提取字元 3.字元匹配識別。下面就一一來介紹一下具體步驟,及其使用到的opencv函式,函式的具體使用方法同學們就自己查查吧,懶得贅述了。
車牌提取
灰度化:灰度化的概念就是將一張三通道RGB顏色的影象變成單通道灰度圖,為接下來的影象處理做準備。
CvCvtColor。cvCvtColor(image, grayScale, CV_BGR2GRAY);
豎向邊緣檢測:首先車牌上的數字都有很銳利的邊緣,另外這些邊緣主要都是縱向的,因此通過這一步可以去除影象上的大量無用資訊。
sobel = cvCreateImage(cvGetSize(grayScale), IPL_DEPTH_16S,1);
cvSobel(grayScale, sobel, 2, 0, 7);
IplImage* temp = cvCreateImage(cvGetSize(sobel), IPL_DEPTH_8U,1);
cvConvertScale(sobel, temp, 0.00390625, 0);
第一步首先建立一張深度為16位有符號(-65536~65535)的的影象區域保持處理結果。
第二步進行x方向的sobel檢測,運算元的大小(最後一個引數)我選擇了7*7,完全時瞎選的,可以結合實際效果進行調整。
最後將影象格式轉換回8位深度已進行下一步處理
自適應二值化處理:二值化的處理強化了銳利的邊緣,進一步去除影象中無用的資訊,使用過程中主要注意閥值的選取,我為了省事兒使用了opencv自帶的自適應的的二值化處理,缺點是無用資訊有點多,但車牌數字資訊也會更為凸顯。
cvThreshold(sobel, threshold, 0, 255, CV_THRESH_BINARY| CV_THRESH_OTSU);
最後的引數CV_THRESH_OTSU就是使用自適應演算法,千萬不要看學習OpenCV那本書上介紹的cvAdaptiveThreshold方法,那完全時披著二值化皮的邊緣檢測函式,坑死人!
形態學(膨脹腐蝕)處理:膨脹與腐蝕的處理效果就如其名字一樣,我們通過膨脹連線相近的影象區域,通過腐蝕去除孤立細小的色塊。通過這一步,我們希望將所有的車牌號字元連通起來,這樣為我們接下來通過輪廓識別來選取車牌區域做準備。由於字元都是橫向排列的,因此要連通這些字元我們只需進行橫向的膨脹即可。
//自定義1*3的核進行X方向的膨脹腐蝕
IplConvKernel* kernal = cvCreateStructuringElementEx(3,1, 1, 0, CV_SHAPE_RECT);
cvDilate(threshold, erode_dilate, kernal, 2);//X方向膨脹連通數字
vErode(erode_dilate, erode_dilate, kernal, 4);//X方向腐蝕去除碎片
cvDilate(erode_dilate, erode_dilate, kernal, 2);//X方向膨脹回覆形態
//自定義3*1的核進行Y方向的膨脹腐蝕
kernal = cvCreateStructuringElementEx(1, 3, 0, 1, CV_SHAPE_RECT);
cvErode(erode_dilate, erode_dilate, kernal, 1);// Y方向腐蝕去除碎片
cvDilate(erode_dilate, erode_dilate, kernal, 2);//回覆形態
進行膨脹腐蝕操作需要注意的是要一次到位,如果一次膨脹沒有連通到位,那麼再次腐蝕將會將影象回覆原裝,因此我首先做了2次迭代的膨脹,保證數字區域能連通起來,再進行4次迭代腐蝕,儘可能多的去除小塊碎片,隨後2次迭代膨脹,保證膨脹次數與腐蝕次數相同,以回覆連通區域形態大小。
矩形輪廓查詢與篩選:經過上一步操作,理論上來說車牌上的字元連通成一個矩形區域,通過輪廓查詢我們可以定位該區域。當然,更為準確的說,經過上面的操作,我們將原始圖片中在X方向排列緊密的縱向邊緣區域連通成了一個矩形區域,出了車牌符合這個特點外,其他一些部分如路間欄杆,車頭的紋理等同樣符合。因此我們會找到很多這樣的區域,這就需要我們進一步根據一些關於車牌特點的先驗知識對這些矩形進行進一步篩選。最終,定位車牌所在的矩形區。
首先來看輪廓檢測:
IplImage* copy = cvCloneImage(img);
CvMemStorage* storage = cvCreateMemStorage();
CvSeq* contours;
cvFindContours(copy, storage, &contours);
while(contours != nullptr)
{
rects.push_back(cvBoundingRect(contours));//list<CvRect> rects
contours= contours->h_next;
}
使用list儲存全部查詢到的CvRect,因為接下來我們要頻繁的對容器中的元素進行插入刪除操作。
矩形的篩選演算法完全就要自己寫啦~這一步篩選效果的好壞直接決定了整個一套識別演算法是否能得到一個好的結果。在之後對於演算法的調整也主要是集中於這一部分,調整一些先驗知識的引數。我設計的篩選演算法主要涉及這幾個部分:1.大小(圖片大小的5%以下) 2.位置(圖片高度的40%~90%之間) 3.X方向合併(有時車牌會處理成兩個或多個相鄰的矩形區,需要進行合併) 4.大小形狀(寬高比)
最後,找到篩選之後最大的那個矩形就是了,注意下圖的紅色線框!
字元提取
字元提取的步驟與車牌提取的思想大致相同,但無需再做形態學處理。仍然是灰度化->二值化->輪廓檢測->自定義篩選->定位。(另外提一句,二值化之後可能還要做一下反色處理,因為有黃底/白底 - 黑字這種車牌,不過因為給我們的樣本中沒有這類車牌,因此偷個懶就不做了^^)
相應的步驟所使用的OpenCV函式與之前別無二致,因此不做贅述,這裡主要講一下自定義的篩選和去除邊框鉚釘這一核心步驟。
首先,最容易導致字元提取失敗的情況是亮色的邊框與鉚釘,他們會與車牌字母連通在一起,導致輪廓檢測失敗,如下圖幾種情況:
3與H和鉚釘連線,在輪廓檢測是無法提取
全部字元的底部都連住了車牌底框,輪廓檢測將徹底失敗
因此在進行第一次初步的輪廓檢測之後,還需要進行除邊框與鉚釘的處理,之後再進行輪廓檢測,再經過篩選,基本上妥妥的能分離出每個字元了。
去除邊框:首先對影象進行Y方向的逐步裁剪,直至輪廓檢測可以檢測到超過5個輪廓。這一步保證不會與鉚釘連線的中間幾個字元可以通過輪廓檢測識別出來。
去除鉚釘:根據識別出來的幾個字元矩形輪廓確定車牌照所有字元的上下界,根據其切割影象。(前提是認為所有字元是水平排列的,因此當車牌傾斜時需要用到更為複雜的演算法)
下圖顯示了這一過程:
矩形篩選:同學們肯定也注意到了,上圖中左右兩個邊框在作輪廓拾取時也可以獲取到,而且很容易與數字1混淆,另外想D, 0, P, R, 8, 9, 6這種數字也會把內部的封閉區域檢測出輪廓,因此同車牌提取一樣,對檢測出的矩形輪廓還要進一步篩選,篩選主要包括排序;高度、寬窄、面積(去除左右邊框、圓點、8,p等小的內輪廓);包含檢測剔除(去除0、D、非單連通的漢字這種大的內輪廓)。
中文字元定位:中文字元由於有些是非單連通的,因此在進行篩選時可能會被篩掉,即使沒有被篩掉,所得的矩形也很可能並非包含了全部字型,如京。因此在字元提取的最後一步我們需要重定位中文字元的矩形擷取框。演算法很簡單,以第二個字元的矩形輪廓為基準,大小不變,y座標不變,根據字元間平均間距(不包括2-3字元的間距)確定x座標即可。
double d = lastChar.x + 0.5 * lastChar.width - llChar.x- llChar.width * 0.5 - avgWidth;
注意在求字元間平均間距的時候不要簡單的用a.next.x– a.x這種方法,一旦那個字元是陣列1結果會錯的很難看。要用字元中點間距減去字元正常寬頻這種方式。獲取正常寬度的演算法也很簡單,根據先驗知識I的矩形包圍框寬度一般小於25畫素(我提取的車牌大小統一為400 * 100),因此只要找到一個寬度大於25的矩形框,把它的寬度即可作為平均寬度。
最後總結下來,提取字元的流程是這樣的 灰度化 -> 二值化 -> 輪廓檢測 –> 去除邊框 ->輪廓檢測 -> 大小篩選 -> 去除鉚釘 –> 輪廓檢測 -> 篩選 -> 重定位中文字元
字元匹配
進行匹配之前你首先需要有一套標準字元模板作為參考,然後將提取出來的字元圖片與每個模板進行某種演算法的匹配,求得一個匹配值,最終將最佳匹配結果作為該字元圖片所代表的字元。這種方法比較笨,也耗時,但是簡單,好實現,如果你對這方面想有更深入研究可以在網上搜索一下OCR(Optical Character Recognition)。
那麼匹配演算法是什麼樣的呢?OpenCV提供了許多匹配函式,如直方圖匹配,Hu矩匹配,輪廓匹配。如果你上網上搜字元匹配,你可能最多看到的就是模板匹配,還有什麼字元匹配最適合用模板匹配之類的話。我可以在這裡負責任的告訴你,以上方法統統都不行!!
至少在我的演算法中切出來的字元圖片用不了,尤其是神馬模板匹配,可能是我的理解不對,模板檢測是用來在一張大圖中檢測出一部分形狀的,和我想要的根本就是風馬牛不相及嘛;還有那個輪廓檢測,聽名字感覺很靠譜啊~但實際是如果你得到的字元不是標準到跟模板一模一樣(在這個專案中主要是線條的粗細),效果差極了。
怎麼辦?顯然這時我已經繞進去了,不過好在我同學還保持清醒,最終的解決辦法簡單的無法想象。。。。逐畫素點匹配!神馬?這從如此高深的演算法跳到如此簡單的方式我一時不能接受,但是我負責任的告訴你,如果你不考慮速度、實時性,也不覺得使用這麼簡單的方法掉價的話,這個方法絕對靠譜!!!(注意這個專案可沒打算賣錢也沒打算申請啥技術專利啊^_^;)
逐點匹配:很簡單,經過處理的帶匹配圖片與模板圖片此時大小相同,且都為2值圖(非0即255)遍歷全部畫素點,記錄兩張圖中值不同的畫素個數,除以全部畫素數量即為匹配率。顯然越接近0越匹配。
近似區分:上一步的匹配結果已經非常好了,但對於一些容易混淆的字元還需要進一步區分如0和U, B和8和9等等。對於這些字元,我們在通過逐點匹配後不能馬上認定,而需要進行特徵檢測。最簡單的特徵如連通區域個數(區分0 – U, 8–9),直線檢測(區分B和8)等等。
改進
二值化前加入銳化處理,採用OpenCV的自適應閥值函式會提取出大量模糊的邊緣及碎片,直接影響是導致字元變粗,易於車牌邊框連線,影響匹配效果。
中文字元的識別,目前純靠人品。。。。