大家好,好久不見了。
一轉眼距離上一篇部落格已經是4個月前的事了。要問博主這段時間去幹了什麼,我只能說:我去“外面看了看”。
圖1 我想去看看
在外面跟幾家創業公司談了談,交流了一些大資料與機器視覺相關的心得與經驗。不過由於各種原因,博主又回來了。
目前,博主的工作是在本地的一個高校做科研。而研究的方向主要是計算機視覺。
圖2 科研就是不斷的探索過程
由於我所做的是計算機視覺方向,跟EasyPR本身非常契合。未來這個這個系列的部落格會繼續下去,並且以後會有更加專業的內容。
目前我研究的方向是文字定位,這個技術跟車牌定位很像,都是在圖中去定位一些語言相關的位置。不同之處在於,車牌定位只需要處理的是在車牌中出現的文字,字型,顏色都比較固定,背景也比相對單一(藍色和黃色等)。
文字定位則複雜很多,研究界目前要處理的是是各種型別,不同字型,且擁有複雜背景的文字。下圖是一張樣例:
圖3 文字定點陣圖片樣例
可以看出,文字定位要處理的問題是類似車牌定位的,不過難度要更大。一些文字定位的技術也應該可以應用於車牌的定位和識別。
未來EasyPR會借鑑文字定位的一些思想和技術,來強化其定位的效果。
一.前言
今天繼續我們EasyPR的開發詳解。
這幾個月我收到了不少的郵件問:為什麼EasyPR開發詳解教程中只有車牌定位的部分,而沒有字元識別的部分?
這個原因一是由於整個開發詳解是按照車牌識別的流程順序來的,因此先講定位,後面再講字元識別。所以字元識別的部分出來的比較晚。
二是由於字元識別相對於前面的車牌定位而言,顯得較為簡單。不像在一個複雜和低分辨場景下進行車牌定位,在字元分割和識別的部分時,所需要處理的場景已經較為固定了,因此其處理技術也較為單一。
這兩個原因是字元分割和識別部分出來較晚的原因。不過在本篇部落格中我們會將字元分割部分講完。
二.整體流程
我們首先看一下,字元分割所需要處理的輸入: 即是前面車牌定位中的結果,一個完整的車牌。
圖4 字元分割模組的輸入
由於在車牌定位中,我們使用了歸一化過程。因此所需要處理的車牌的大小是統一的,在目前的版本中(v1.3),這個值是136*36。
那麼字元分割的結果就是將車牌中的所有文字一一分割開來,形成單一的字元塊。生成的字元塊就可以輸入下一步的字元識別部分進行識別。在EasyPR裡,字元識別所使用的技術是人工神經網路,也就是ANN。
具體而言,字元分割過程是如何做的呢?簡單說,就是:灰度化->顏色判斷->二值化->取輪廓->找外接矩形->擷取圖塊。
圖5 字元分割處理流程
下面,我們使用下圖的車牌完整的跑一遍字元分割的流程,以此對其有一個全域性的認識。
圖6 原始圖片
1.灰度化
首先,我們把彩色的圖片轉化為灰度化圖片。注意:為了以後可以利用彩色資訊,在前面的車牌檢測過程中,我們的輸出結果不是灰度化圖片,而是彩色圖片。這樣以後當我們改正演算法,想利用彩色資訊時就可以使用了。
但是在這裡,我們的演算法還是針對的是灰度化圖片,因此首先進行灰度化處理。
灰度化後的圖片見下圖:
圖7 灰度化後結果
2.顏色判斷
灰度化之後,為了分割字元。我們需要獲取字元的輪廓。注意:分割字元有很多種方法。例如投影法,滑動視窗判斷法,在這裡,EasyPR使用的是取字元輪廓法。
因為需要取輪廓,就需要把圖片轉化成一個二值化圖片。不過,由於藍色和黃色車牌圖片的區別,兩者需要用的二值化引數不一樣,因此這裡需要對車牌圖片的顏色進行一個判斷。車牌顏色對二值化的影響的分析見後面“其他細節”章節。
這裡顏色判斷的使用的是前面顏色定位詳解裡的模板匹配法。
圖8 顏色判斷
3.二值化
獲取顏色後,就可以選擇不同的引數進行大津閾值法來進行二值化。對於本示例圖片中的藍色車牌而言,使用的引數為CV_THRESH_BINARY。
二值化後的效果見下圖:
圖9 二值化後結果
4.取輪廓
接下來,使用被多次用到的取輪廓方法findContours。關於這個方法的具體內容,在前面的開發詳解中已做過介紹,這裡不再贅述。
取輪廓後的結果如下圖:
圖10 取輪廓操作
注意:直接使用findContours方法取輪廓時,在處理中文字元,也就是“蘇”時,會發生斷裂現象。因此為了處理中文字元,EasyPR換了一種思路,使用了額外的步驟來解決這個問題。具體可以見後面的“中文字元處理”章節。
5.找外接矩形
使用了中文字元處理方法以後,成功獲取了所有的字元的外接矩形。
具體見下圖:
圖11 所有字元的外接矩形
6.擷取圖塊
最後,把圖中的外接矩形一一擷取出來,歸一化到統一格式。留待輸入下個步驟--字元識別模組處理。
歸一化後字元圖塊見下圖:
圖12 擷取並歸一化的圖塊
三.中文字元處理
上面的流程在處理英文車牌時,效果是很好的。但是在處理中文車牌時,存在一個很大的問題。
在取輪廓時,中文由於自身的特性,例如有筆畫區間,取輪廓會造成斷裂現象。例如下圖中的“蘇”。英文字元通過取輪廓都被完整的包括了,而“蘇”字則分成了兩個連通區域。
圖13 取輪廓操作示例
雖然並不是所有的中文都會存在這個問題(例如下圖的“津”字),但直接用取輪廓操作已經不合適了。
EasyPR是如何解決這個問題的呢?其實想法很簡單。那就是既然有些中文字元沒辦法用取輪廓處理,那麼就乾脆先不處理中文字元,而是用取輪廓操作處理中文字元後面的字元。例如“蘇A88M88”,其中“A88M88”這六個字元我都能用取輪廓操作獲得。我先獲取這六個字元,再想辦法獲取中文字元。
圖14 “津”字
獲取這六個字元後,接下來該如何獲取“蘇”這個中文字元的輪廓呢?
這裡的關鍵就是“蘇”字元後面的“A”字元,這個字元在中文車牌裡代表城市的程式碼,我們在這裡簡稱它為“城市字元”或者“特殊字元”。
這個字元有一個特徵,就是與後面的字元存在一定的間隔。但是與前面的中文字元靠的較緊。倘若我獲取了這個特殊字元的外接矩形,只要把這個外接矩形向左做一些的偏移(偏移的大小可以通過經驗指定,例如設定為字元寬度的1.15倍),這樣這個外接矩形就成了包含中文字元的一個矩形了。下面就可以擷取中文字元的圖塊。
下圖就是“特殊字元”與被反推得到的“中文字元”的矩形,在圖中用紅色矩形表示。
圖15 反推得到的中文字元位置
下面的問題就是如何獲取“特殊字元”的位置?
一種方法是把所有取輪廓操作獲取到的矩形進行排序,最左邊的就是特殊字元的圖塊。但是有些中文字元會被取輪廓操作擷取為一個連通區域。在這種情況下,最左邊的圖塊矩形是中文字元的矩形,而不是特殊字元的矩形了。所以這個方法不能用。
另一種方法就是依次判斷所有取輪廓操作得到的矩形的位置,設矩形的中點恰好在整個車牌的1/7到2/7之間時的矩形為特殊矩形。這樣操作的前提是我們的車牌定位的非常準確,恰到把整個車牌擷取的正正好。在這種情況下,只要外接矩形滿足這些條件,就可以判斷為特殊字元的矩形。
這個方法思路很簡單,實際中應用效果也不錯,因此也是EasyPR目前採用的方法。
圖16 獲取特殊字元的位置
以下是特殊字元判斷的程式碼:
//! 找出指示城市的字元的Rect,例如蘇A7003X,就是"A"的位置 int CCharsSegment::GetSpecificRect(const vector<Rect>& vecRect) { vector<int> xpositions; int maxHeight = 0; int maxWidth = 0; for (size_t i = 0; i < vecRect.size(); i++) { xpositions.push_back(vecRect[i].x); if (vecRect[i].height > maxHeight) { maxHeight = vecRect[i].height; } if (vecRect[i].width > maxWidth) { maxWidth = vecRect[i].width; } } int specIndex = 0; for (size_t i = 0; i < vecRect.size(); i++) { Rect mr = vecRect[i]; int midx = mr.x + mr.width / 2; //如果一個字元有一定的大小,並且在整個車牌的1/7到2/7之間,則是我們要找的特殊字元 //當前字元和下個字元的距離在一定的範圍內 if ((mr.width > maxWidth * 0.8 || mr.height > maxHeight * 0.8) && (midx < int(m_theMatWidth / 7) * 2 && midx > int(m_theMatWidth / 7) * 1)) { specIndex = i; } } return specIndex; }
以上就是EasyPR能處理中文車牌的主要原因。原先的taotao1233的程式碼中無法處理中文的原因就是沒有這樣一步預處理。其實這是一個很簡單的思想,但在之前並沒有被實現。EasyPR裡實現了這個思路,同時發現,這個方法效果出奇的好。基本可以應對所有的情況。所以說,這個方法可以說是一個簡單,有效的處理中文車牌的方法。
四.其他一些細節
1.顏色判斷
在進行二值化前,需要進行一次顏色判斷,這是因為對於藍色和黃色車牌而言,使用的二值化策略必須不同。
圖17 藍色與黃色車牌的不同
對於藍色車牌而言,使用的引數為CV_THRESH_BINARY。
而對於黃色車牌而言,使用的引數為CV_THRESH_BINARY_INV。
假設黃色車牌使用了CV_THRESH_BINARY作為引數,則會發生如下圖一樣的二值化結果,其中字元部分變成了黑色,而背景則是白色(同理,藍色車牌使用CV_THRESH_BINARY_INV也是一樣的效果)。
在這種不正確的引數帶來的二值化情況下,取輪廓操作將無法按照預期的行為進行處理。因此,必須使用正確的二值化引數。
圖18 不正確引數的二值化效果
在顏色判斷時,有一個小技巧,就是先把四周的“邊”擷取後再進行顏色的判斷,這樣可以消除車牌定位時一些多餘的四周的干擾。
程式碼如下:
1 Mat tmpMat = input(Rect_<double>(w * 0.1, h * 0.1, w * 0.8, h * 0.8)); 2 3 // 判斷車牌顏色以此確認threshold方法 4 Color plateType = getPlateType(tmpMat, true);
顏色判斷方法的程式碼如下:
1 // getPlateType 2 //判斷車牌的型別 3 Color getPlateType(const Mat& src, const bool adaptive_minsv) { 4 float max_percent = 0; 5 Color max_color = UNKNOWN; 6 7 float blue_percent = 0; 8 float yellow_percent = 0; 9 float white_percent = 0; 10 11 if (plateColorJudge(src, BLUE, adaptive_minsv, blue_percent) == true) { 12 // cout << "BLUE" << endl; 13 return BLUE; 14 } else if (plateColorJudge(src, YELLOW, adaptive_minsv, yellow_percent) == 15 true) { 16 // cout << "YELLOW" << endl; 17 return YELLOW; 18 } else if (plateColorJudge(src, WHITE, adaptive_minsv, white_percent) == 19 true) { 20 // cout << "WHITE" << endl; 21 return WHITE; 22 } else { 23 // cout << "OTHER" << endl; 24 25 // 如果任意一者都不大於閾值,則取值最大者 26 max_percent = blue_percent > yellow_percent ? blue_percent : yellow_percent; 27 max_color = blue_percent > yellow_percent ? BLUE : YELLOW; 28 29 max_color = max_percent > white_percent ? max_color : WHITE; 30 return max_color; 31 } 32 }
2.排除縫隙
在獲得中文字元圖塊以後,下面一步就是把剩下的圖塊獲取了。不過由於中文車牌一般只有7個字元,所以可以把後面的圖塊從左到右排序,依次選擇6個即可。一些會被誤判為“I”的縫隙可以通過這種方法排除出去。
例如下圖中,最右邊的一個縫隙會被誤識別為"1"。但是倘若從左到右依次選擇的話,這個縫隙並不會被選入候選集合中,因為它已經是“第八個”字元了。
圖19 最右邊會被誤判為"1"的縫隙
排序與依次選擇的程式碼如下:
1 //! 這個函式做兩個事情 2 // 1.把特殊字元Rect左邊的全部Rect去掉,後面再重建中文字元的位置。 3 // 2.從特殊字元Rect開始,依次選擇6個Rect,多餘的捨去。 4 int CCharsSegment::RebuildRect(const vector<Rect>& vecRect, 5 vector<Rect>& outRect, int specIndex) { 6 int count = 6; 7 for (size_t i = specIndex; i < vecRect.size() && count; ++i, --count) { 8 outRect.push_back(vecRect[i]); 9 } 10 11 return 0; 12 }
3.去除柳釘
有些中國的車牌中有一個非常妨礙識別的東西,那就是柳釘。倘若對一副含有柳釘的圖進行二值化,極有可能會出現下圖的結果。一些字元圖塊(下圖的"9"和"1")通過柳釘的原因聯絡到了一體,那樣的話就無法通過取輪廓操作來分割了。
圖20 柳釘的影響
因此在二值化之後,還需要一個去除柳釘的操作。
去除柳釘的思想也並不複雜,就是依次掃描每行,判斷跳變次數。車牌字元所在的行的跳變次數是很多的,而柳釘所在的行就會偏少。因此當發現某行跳變次數較少,則可以把該行的所有畫素值賦值為0,這樣就會大幅度消除柳釘的影響了。
下圖就是去除柳釘後的效果。
圖21 去除柳釘後的效果
去除柳釘函式的程式碼如下:
1 //去除車牌上方的鈕釘 2 //計算每行元素的階躍數,如果小於X認為是柳丁,將此行全部填0(塗黑) 3 // X的推薦值為,可根據實際調整 4 bool clearLiuDing(Mat& img) { 5 vector<float> fJump; 6 int whiteCount = 0; 7 const int x = 7; 8 Mat jump = Mat::zeros(1, img.rows, CV_32F); 9 for (int i = 0; i < img.rows; i++) { 10 int jumpCount = 0; 11 12 for (int j = 0; j < img.cols - 1; j++) { 13 if (img.at<char>(i, j) != img.at<char>(i, j + 1)) jumpCount++; 14 15 if (img.at<uchar>(i, j) == 255) { 16 whiteCount++; 17 } 18 } 19 20 jump.at<float>(i) = (float)jumpCount; 21 } 22 23 int iCount = 0; 24 for (int i = 0; i < img.rows; i++) { 25 fJump.push_back(jump.at<float>(i)); 26 if (jump.at<float>(i) >= 16 && jump.at<float>(i) <= 45) { 27 //車牌字元滿足一定跳變條件 28 iCount++; 29 } 30 } 31 32 ////這樣的不是車牌 33 if (iCount * 1.0 / img.rows <= 0.40) { 34 //滿足條件的跳變的行數也要在一定的閾值內 35 return false; 36 } 37 //不滿足車牌的條件 38 if (whiteCount * 1.0 / (img.rows * img.cols) < 0.15 || 39 whiteCount * 1.0 / (img.rows * img.cols) > 0.50) { 40 return false; 41 } 42 43 for (int i = 0; i < img.rows; i++) { 44 if (jump.at<float>(i) <= x) { 45 for (int j = 0; j < img.cols; j++) { 46 img.at<char>(i, j) = 0; 47 } 48 } 49 } 50 return true; 51 }
五.總結
最後回顧一下整體的處理流程,首先是對車牌影像進行灰度化,然後根據車牌的不同顏色來進行不同的二值化處理。二值化完後首先去除柳釘,然後進行取輪廓操作。
取輪廓操作以後,在所有的輪廓中根據先驗知識,找到代表城市的字元,也就是“蘇A”中“A”的位置,根據“A”的位置來反推“蘇”的位置。
最後將找到的這些輪廓依次排序,從左到右依次選擇6個,和第一個的中文字元組成7個字元的圖塊陣列,輸入到下一步字元識別模組中進行處理。
整個字元分割流程就到此結束了,還是比較簡單的。其中的中文字元位置的確定使用了“先驗知識”這種方法。這種方法在面對固定已知場景中是較好的方法,但是面對特殊情況時就可能會有不太好的效果,因此要根據具體情況來權衡。
六.未來展望
本篇字元分割流程就到此結束。當下,EasyPR1.3 版也釋出了,對整體架構以及處理效率都有所提升,可以下載試用。
未來的部落格會按照每2個月一篇的速度誕生,下篇部落格的內容是”字元識別與人工神經網路”。
版權說明:
本文中的所有文字,圖片,程式碼的版權都是屬於作者和部落格園共同所有。歡迎轉載,但是務必註明作者與出處。任何未經允許的剽竊以及爬蟲抓取都屬於侵權,作者和部落格園保留所有權利。