你們好,很久不見了。html
一轉眼距離上一篇博客已是4個月前的事了。要問博主這段時間去幹了什麼,我只能說:我去「外面看了看」。git
圖1 我想去看看 github
在外面跟幾家創業公司談了談,交流了一些大數據與機器視覺相關的心得與經驗。不過因爲各類緣由,博主又回來了。算法
目前,博主的工做是在本地的一個高校作科研。而研究的方向主要是計算機視覺。數組
圖2 科研就是不斷的探索過程網絡
因爲我所作的是計算機視覺方向,跟EasyPR自己很是契合。將來這個這個系列的博客會繼續下去,而且之後會有更加專業的內容。架構
目前我研究的方向是文字定位,這個技術跟車牌定位很像,都是在圖中去定位一些語言相關的位置。不一樣之處在於,車牌定位只須要處理的是在車牌中出現的文字,字體,顏色都比較固定,背景也比相對單一(藍色和黃色等)。ide
文字定位則複雜不少,研究界目前要處理的是是各類類型,不一樣字體,且擁有複雜背景的文字。下圖是一張樣例:函數
圖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個月一篇的速度誕生,下篇博客的內容是」字符識別與人工神經網絡」。
版權說明:
本文中的全部文字,圖片,代碼的版權都是屬於做者和博客園共同全部。歡迎轉載,可是務必註明做者與出處。任何未經容許的剽竊以及爬蟲抓取都屬於侵權,做者和博客園保留全部權利。