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

 

  你們好,很久不見了。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;
}
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個月一篇的速度誕生,下篇博客的內容是」字符識別與人工神經網絡」。

 

 

版權說明:

 

  本文中的全部文字,圖片,代碼的版權都是屬於做者和博客園共同全部。歡迎轉載,可是務必註明做者與出處。任何未經容許的剽竊以及爬蟲抓取都屬於侵權,做者和博客園保留全部權利。

相關文章
相關標籤/搜索