EasyPR--開發詳解(7)字元分割

計算機的潛意識發表於2015-07-28

 

  大家好,好久不見了。

  一轉眼距離上一篇部落格已經是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;
}
View Code

 

  以上就是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 }
View Code

 

  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 }
View Code

 

  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 }
View Code

 

五.總結 

  最後回顧一下整體的處理流程,首先是對車牌影象進行灰度化,然後根據車牌的不同顏色來進行不同的二值化處理。二值化完後首先去除柳釘,然後進行取輪廓操作。

  取輪廓操作以後,在所有的輪廓中根據先驗知識,找到代表城市的字元,也就是A”中“A”的位置,根據“A的位置來反推“蘇”的位置。

  最後將找到的這些輪廓依次排序,從左到右依次選擇6個,和第一個的中文字元組成7個字元的圖塊陣列,輸入到下一步字元識別模組中進行處理。

  整個字元分割流程就到此結束了,還是比較簡單的。其中的中文字元位置的確定使用了先驗知識這種方法。這種方法在面對固定已知場景中是較好的方法,但是面對特殊情況時就可能會有不太好的效果,因此要根據具體情況來權衡。

 

六.未來展望

  本篇字元分割流程就到此結束。當下,EasyPR1.3 版也釋出了,對整體架構以及處理效率都有所提升,可以下載試用。

  未來的部落格會按照每2個月一篇的速度誕生,下篇部落格的內容是”字元識別與人工神經網路”。

 

 

版權說明:

 

  本文中的所有文字,圖片,程式碼的版權都是屬於作者和部落格園共同所有。歡迎轉載,但是務必註明作者與出處。任何未經允許的剽竊以及爬蟲抓取都屬於侵權,作者和部落格園保留所有權利。

 

相關文章