大家好,好久不見了。
一轉眼距離上一篇博客已經是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個月一篇的速度誕生,下篇博客的內容是”字符識別與人工神經網絡”。
版權說明:
本文中的所有文字,圖片,代碼的版權都是屬於作者和博客園共同所有。歡迎轉載,但是務必注明作者與出處。任何未經允許的剽竊以及爬蟲抓取都屬於侵權,作者和博客園保留所有權利。