EasyPR--開發詳解(4)形態學操作、尺寸驗證、旋轉等操作

計算機的潛意識發表於2014-10-24

  在上一篇深度分析與調優討論中,我們介紹了高斯模糊,灰度化和Sobel運算元。在本文中,會分析剩餘的定位步驟。

  根據前文的內容,車牌定位的功能還剩下如下的步驟,見下圖中未塗灰的部分。

圖1 車牌定位步驟

  我們首先從Soble運算元分析出來的邊緣來看。通過下圖可見,Sobel運算元有很強的區分性,車牌中的字元被清晰的描繪出來,那麼如何根據這些資訊定位出車牌的位置呢?

圖2 Sobel後效果

  我們的車牌定位功能做了個假設,即車牌是包含字元圖塊的一個最小的外接矩形。在大部分車牌處理中,這個假設都能工作的很好。我們來看下這個假設是如何工作的。

  車牌定位過程的全部程式碼如下:

  1 //! 定位車牌影像
  2 //! src 原始影像
  3 //! resultVec 一個Mat的向量,儲存所有抓取到的影像
  4 //! 成功返回0,否則返回-1
  5 int CPlateLocate::plateLocate(Mat src, vector<Mat>& resultVec)
  6 {
  7     Mat src_blur, src_gray;
  8     Mat grad;
  9 
 10     int scale = SOBEL_SCALE;
 11     int delta = SOBEL_DELTA;
 12     int ddepth = SOBEL_DDEPTH;
 13 
 14     if( !src.data )
 15     { return -1; }
 16 
 17     //高斯模糊。Size中的數字影響車牌定位的效果。
 18     GaussianBlur( src, src_blur, Size(m_GaussianBlurSize, m_GaussianBlurSize), 
 19         0, 0, BORDER_DEFAULT );
 20 
 21     if(m_debug)
 22     { 
 23         stringstream ss(stringstream::in | stringstream::out);
 24         ss << "tmp/debug_GaussianBlur" << ".jpg";
 25         imwrite(ss.str(), src_blur);
 26     }
 27 
 28     /// Convert it to gray
 29     cvtColor( src_blur, src_gray, CV_RGB2GRAY );
 30 
 31     if(m_debug)
 32     { 
 33         stringstream ss(stringstream::in | stringstream::out);
 34         ss << "tmp/debug_gray" << ".jpg";
 35         imwrite(ss.str(), src_gray);
 36     }
 37 
 38     /// Generate grad_x and grad_y
 39     Mat grad_x, grad_y;
 40     Mat abs_grad_x, abs_grad_y;
 41 
 42     /// Gradient X
 43     //Scharr( src_gray, grad_x, ddepth, 1, 0, scale, delta, BORDER_DEFAULT );
 44     Sobel( src_gray, grad_x, ddepth, 1, 0, 3, scale, delta, BORDER_DEFAULT );
 45     convertScaleAbs( grad_x, abs_grad_x );
 46 
 47     /// Gradient Y
 48     //Scharr( src_gray, grad_y, ddepth, 0, 1, scale, delta, BORDER_DEFAULT );
 49     Sobel( src_gray, grad_y, ddepth, 0, 1, 3, scale, delta, BORDER_DEFAULT );
 50     convertScaleAbs( grad_y, abs_grad_y );
 51 
 52     /// Total Gradient (approximate)
 53     addWeighted( abs_grad_x, SOBEL_X_WEIGHT, abs_grad_y, SOBEL_Y_WEIGHT, 0, grad );
 54 
 55     //Laplacian( src_gray, grad_x, ddepth, 3, scale, delta, BORDER_DEFAULT );  
 56     //convertScaleAbs( grad_x, grad );  
 57 
 58 
 59     if(m_debug)
 60     { 
 61         stringstream ss(stringstream::in | stringstream::out);
 62         ss << "tmp/debug_Sobel" << ".jpg";
 63         imwrite(ss.str(), grad);
 64     }
 65 
 66     Mat img_threshold;
 67     threshold(grad, img_threshold, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY);
 68     //threshold(grad, img_threshold, 75, 255, CV_THRESH_BINARY);
 69 
 70     if(m_debug)
 71     { 
 72         stringstream ss(stringstream::in | stringstream::out);
 73         ss << "tmp/debug_threshold" << ".jpg";
 74         imwrite(ss.str(), img_threshold);
 75     }
 76 
 77     Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );
 78     morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);
 79     
 80     if(m_debug)
 81     { 
 82         stringstream ss(stringstream::in | stringstream::out);
 83         ss << "tmp/debug_morphology" << ".jpg";
 84         imwrite(ss.str(), img_threshold);
 85     }
 86 
 87     //Find 輪廓 of possibles plates
 88     vector< vector< Point> > contours;
 89     findContours(img_threshold,
 90         contours, // a vector of contours
 91         CV_RETR_EXTERNAL, // 提取外部輪廓
 92         CV_CHAIN_APPROX_NONE); // all pixels of each contours
 93 
 94     Mat result;
 95     if(m_debug)
 96     { 
 97         //// Draw blue contours on a white image
 98         src.copyTo(result);
 99         drawContours(result, contours,
100             -1, // draw all contours
101             Scalar(0,0,255), // in blue
102             1); // with a thickness of 1
103         stringstream ss(stringstream::in | stringstream::out);
104         ss << "tmp/debug_Contours" << ".jpg";
105         imwrite(ss.str(), result);
106     }
107 
108 
109     //Start to iterate to each contour founded
110     vector<vector<Point> >::iterator itc = contours.begin();
111     
112     vector<RotatedRect> rects;
113     //Remove patch that are no inside limits of aspect ratio and area.
114     int t = 0;
115     while (itc != contours.end())
116     {
117         //Create bounding rect of object
118         RotatedRect mr = minAreaRect(Mat(*itc));
119 
120         //large the rect for more
121         if( !verifySizes(mr))
122         {
123             itc = contours.erase(itc);
124         }
125         else
126         {
127             ++itc;
128             rects.push_back(mr);
129         }
130     }
131 
132     int k = 1;
133     for(int i=0; i< rects.size(); i++)
134     {
135         RotatedRect minRect = rects[i];
136         if(verifySizes(minRect))
137         {    
138             // rotated rectangle drawing 
139             // Get rotation matrix
140             // 旋轉這部分程式碼確實可以將某些傾斜的車牌調整正,
141             // 但是它也會誤將更多正的車牌搞成傾斜!所以綜合考慮,還是不使用這段程式碼。
142             // 2014-08-14,由於新到的一批圖片中發現有很多車牌是傾斜的,因此決定再次嘗試
143             // 這段程式碼。
144             if(m_debug)
145             { 
146                 Point2f rect_points[4]; 
147                 minRect.points( rect_points );
148                 for( int j = 0; j < 4; j++ )
149                     line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );
150             }
151 
152             float r = (float)minRect.size.width / (float)minRect.size.height;
153             float angle = minRect.angle;
154             Size rect_size = minRect.size;
155             if (r < 1)
156             {
157                 angle = 90 + angle;
158                 swap(rect_size.width, rect_size.height);
159             }
160             //如果抓取的方塊旋轉超過m_angle角度,則不是車牌,放棄處理
161             if (angle - m_angle < 0 && angle + m_angle > 0)
162             {
163                 //Create and rotate image
164                 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
165                 Mat img_rotated;
166                 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
167 
168                 Mat resultMat;
169                 resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);
170 
171                 resultVec.push_back(resultMat);
172             }
173         }
174     }
175 
176     if(m_debug)
177     { 
178         stringstream ss(stringstream::in | stringstream::out);
179         ss << "tmp/debug_result" << ".jpg";
180         imwrite(ss.str(), result);
181     }
182 
183     return 0;
184 }
View Code

  

  首先,我們通過二值化處理將Sobel生成的灰度影像轉變為二值影像。

四.二值化

  二值化演算法非常簡單,就是對影像的每個畫素做一個閾值處理。

 1.目標

  為後續的形態學運算元Morph等準備二值化的影像。 

 2.效果

  經過二值化處理後的影像效果為下圖,與灰度影像仔細區分下,二值化影像中的白色是沒有顏色強與暗的區別的。

圖3 二值化後效果

  3.理論

  在灰度影像中,每個畫素的值是0-255之間的數字,代表灰暗的程度。如果設定一個閾值T,規定畫素的值x滿足如下條件時則:

 if x < t then x = 0; if x >= t then x = 1

  如此一來,每個畫素的值僅有{0,1}兩種取值,0代表黑、1代表白,影像就被轉換成了二值化的影像。在上面的公式中,閾值T應該取多少?由於不同影像的光造程度不同,導致作為二值化區分的閾值T也不一樣。因此一個簡單的做法是直接使用opencv的二值化函式時加上自適應閾值引數。如下:

threshold(src, dest, 0, 255, CV_THRESH_OTSU+CV_THRESH_BINARY); 

  通過這種方法,我們不需要計算閾值的取值,直接使用即可。
  threshold函式是二值化函式,引數src代表源影像,dest代表目標影像,兩者的型別都是cv::Mat型,最後的引數代表二值化時的選項,
CV_THRESH_OTSU代表自適應閾值,CV_THRESH_BINARY代表正二值化。正二值化意味著畫素的值越接近0,越可能被賦值為0,反之則為1。而另外一種二值化方法表示反二值化,其含義是畫素的值越接近0,越可能被賦值1,,計算公式如下: 

 if x < t then x = 1; if x >= t then x = 0

  如果想使用反二值化,可以使用引數CV_THRESH_BINARY_INV代替CV_THRESH_BINARY即可。在後面的字元識別中我們會同時使用到正二值化與反二值化兩種例子。因為中國的車牌有很多型別,最常見的是藍牌和黃牌。其中藍牌字元淺,背景深,黃牌則是字元深,背景淺,因此需要正二值化方法與反二值化兩種方法來處理,其中正二值化處理藍牌,反二值化處理黃牌。

 

五.閉操作

  閉操作是個非常重要的操作,我會花很多的字數與圖片介紹它。

 1.目標

  將車牌字母連線成為一個連通域,便於取輪廓。 

 2.效果

  我們這裡看下經過閉操作後影像連線的效果。

圖4 閉操作後效果

 3.理論

  在做閉操作的說明前,必須簡單介紹一下腐蝕和膨脹兩個操作。

  在影像處理技術中,有一些的操作會對影像的形態發生改變,這些操作一般稱之為形態學操作。形態學操作的物件是二值化影像。
有名的形態學操作中包括腐蝕,膨脹,開操作,閉操作等。其中腐蝕,膨脹是許多形態學操作的基礎。

  腐蝕操作:
  顧名思義,是將物體的邊緣加以腐蝕。具體的操作方法是拿一個寬m,高n的矩形作為模板,對影像中的每一個畫素x做如下處理:畫素x至於模板的中心,根據模版的大小,遍歷所有被模板覆蓋的其他畫素,修改畫素x的值為所有畫素中最小的值。這樣操作的結果是會將影像外圍的突出點加以腐蝕。如下圖的操作過程:

圖5 腐蝕操作原理 

  上圖演示的過程是背景為黑色,物體為白色的情況。腐蝕將白色物體的表面加以“腐蝕”。在opencv的官方教程中,是以如下的圖示說明腐蝕過程的,與我上面圖的區別在於:背景是白色,而物體為黑色(這個不太符合一般的情況,所以我沒有拿這張圖作為通用的例子)。讀者只需要瞭解背景為不同顏色時腐蝕也是不同的效果就可以了。

  圖6 腐蝕操作原理2

  膨脹操作:
  膨脹操作與腐蝕操作相反,是將影像的輪廓加以膨脹。操作方法與腐蝕操作類似,也是拿一個矩形模板,對影像的每個畫素做遍歷處理。不同之處在於修改畫素的值不是所有畫素中最小的值,而是最大的值。這樣操作的結果會將影像外圍的突出點連線並向外延伸。如下圖的操作過程:


  圖7 膨脹操作原理

  下面是在opencv的官方教程中,膨脹過程的圖示:

  圖8 膨脹操作原理2

  開操作:
  開操作就是對影像先腐蝕,再膨脹。其中腐蝕與膨脹使用的模板是一樣大小的。為了說明開操作的效果,請看下圖的操作過程:

圖9 開操作原理

  由於開操作是先腐蝕,再膨脹。因此可以結合圖5和圖7得出圖9,其中圖5的輸出是圖7的輸入,所以開操作的結果也就是圖7的結果。


  閉操作:
  閉操作就是對影像先膨脹,再腐蝕。閉操作的結果一般是可以將許多靠近的圖塊相連稱為一個無突起的連通域。在我們的影像定位中,使用了閉操作去連線所有的字元小圖塊,然後形成一個車牌的大致輪廓。閉操作的過程我會講的細緻一點。為了說明字元圖塊連線的過程。在這裡選取的原圖跟上面三個操作的原圖不大一樣,是一個由兩個分開的圖塊組成的圖。原圖首先經過膨脹操作,將兩個分開的圖塊結合起來(注意我用偏白的灰色圖塊表示由於膨脹操作而產生的新的白色)。接著通過腐蝕操作,將連通域的邊緣和突起進行削平(注意我用偏黑的灰色圖塊表示由於腐蝕被侵蝕成黑色圖塊)。最後得到的是一個無突起的連通域(純白的部分)。

 圖10 閉操作原理

 4.程式碼 

  在opencv中,呼叫閉操作的方法是首先建立矩形模板,矩形的大小是可以設定的,由於矩形是用來覆蓋以中心畫素的所有其他畫素,因此矩形的寬和高最好是奇數。
  通過以下程式碼設定矩形的寬和高。

    Mat element = getStructuringElement(MORPH_RECT, Size(m_MorphSizeWidth, m_MorphSizeHeight) );

  在這裡,我們使用了類成員變數,這兩個類成員變數在建構函式中被賦予了初始值。寬是17,高是3.

  設定完矩形的寬和高以後,就可以呼叫形態學操作了。opencv中所有形態學操作有一個統一的函式,通過引數來區分不同的具體操作。例如MOP_CLOSE代表閉操作,MOP_OPEN代表開操作。

morphologyEx(img_threshold, img_threshold, MORPH_CLOSE, element);

  如果我對二值化的影像進行開操作,結果會是什麼樣的?下圖是影像使用閉操作與開操作處理後的一個區別:

  圖11 開與閉的對比

  暈,怎麼開操作後影像沒了?原因是:開操作第一步腐蝕的效果太強,直接導致接下來的膨脹操作幾乎沒有效果,所以影像就變幾乎沒了。
  可以看出,使用閉操作以後,車牌字元的圖塊被連線成了一個較為規則的矩形,通過閉操作,將車牌中的字元連成了一個圖塊,同時將突出的部分進行裁剪,圖塊成為了一個類似於矩形的不規則圖塊。我們知道,車牌應該是一個規則的矩形,因此獲取規則矩形的辦法就是先取輪廓,再接著求最小外接矩形。

  這裡需要注意的是,矩形模板的寬度,17是個推薦值,低於17都不推薦。
  為什麼這麼說,因為有一個”斷節“的問題。中國車牌有一個特點,就是表示城市的字母與右邊相鄰的字元距離遠大於其他相鄰字元之間的距離。如果你設定的不夠大,結果導致左邊的字元與右邊的字元中間斷開了,如下圖:

  圖12 “斷節”效果

  這種情況我稱之為“斷節”如果你不想字元從中間被分成"蘇A"和"7EUK22"的話,那麼就必須把它設定大點。

  另外還有一種討厭的情況,就是右邊的字元第一個為1的情況,例如蘇B13GH7。在這種情況下,由於1的字元的形態原因,導致跟左邊的B的字元的距離更遠,在這種情況下,低於17都有很大的可能性會斷節。下圖說明了矩形模板寬度過小時(例如設定為7)面對不同車牌情況下的效果。其中第二個例子選取了蘇E開頭的車牌,由於E在Sobel運算元運算過後僅存有左邊的豎槓,因此也會導致跟右邊的字元相距過遠的情況!

  圖13 “斷節”發生示意


  寬度過大也是不好的,因為它會導致閉操作連線不該連線的部分,例如下圖的情況。


  圖14 矩形模板寬度過大

  這種情況下,你取輪廓獲得矩形肯定會大於你設定的校驗規則,即便通過校驗了,由於圖塊中有不少不是車牌的部分,會給字元識別帶來麻煩。

  因此,矩形的寬度是一個需要非常細心權衡的值,過大過小都不好,取決於你的環境。至於矩形的高度,3是一個較好的值,一般來說都能工作的很好,不需要改變。

  記得我在前一篇文章中提到,工業用圖片與生活場景下圖片的區別麼。筆者做了一個實驗,下載了30多張左右的百度車牌圖片。用plateLocate過程去識別他們。如果按照下面的方式設定引數,可以保證90%以上的定位成功率。

    CPlateLocate plate;
    plate.setDebug(1);
    plate.setGaussianBlurSize(5);
    plate.setMorphSizeWidth(7);
    plate.setMorphSizeHeight(3);
    plate.setVerifyError(0.9);
    plate.setVerifyAspect(4);
    plate.setVerifyMin(1);
    plate.setVerifyMax(30);

  在EasyPR的下一個版本中,會增加對於生活場景下圖片的一個模式。只要選擇這個模式,就適用於百度圖片這種日常生活抓拍圖片的效果。但是,仍然有一些圖片是EasyPR不好處理的。或者可以說,按照目前的邊緣檢測演算法,難以處理的。

  請看下面一張圖片:


圖15 難以權衡的一張圖片

  這張圖片最麻煩的地方在於車牌左右兩側凹下去的邊側,這個邊緣在Sobel運算元中非常明顯,如果矩形模板過長,很容易跟它們連線起來。更麻煩的是這個車牌屬於上面說的“斷節”很容易發生的型別,因為車牌右側字元的第一個字母是“1”,這個導致如果矩形模板過短,則很容易車牌斷成兩截。結果最後導致瞭如下的情況。

  如果我設定矩形模板寬度為12,則會發生下面的情況:

  圖16 車牌被一分為二

  如果我增加矩形模板寬度到13,則又會發生下面的情況。

圖17 車牌區域被不不正確的放大

  因此矩形模板的寬度是個整數值,在12和13中間沒有中間值。這個導致幾乎沒有辦法處理這幅車牌影像。

  上面的情況屬於車尾車牌的一種沒辦法解決的情況。下面所說的情況屬於車頭的情況,相比前者,錯誤檢測的機率高的多!為什麼,因為是一型別車牌無法處理。要問我這家車是哪家,我只能說:碰到開奧迪Q5及其系列的,早點嫁了吧。傷不起。

    圖18 奧迪Q5前部垂直邊緣太多

  這麼多的垂直邊緣,極為容易檢錯。已經試過了,幾乎沒有辦法處理這種車牌。只能替換邊緣檢測這種思路,採用顏色區分等方法。奧體Q系列前臉太多垂直邊緣了,給跪。

 

六.取輪廓

  取輪廓操作是個相對簡單的操作,因此只做簡短的介紹。

 1.目標

  將連通域的外圍勾畫出來,便於形成外接矩形。 

 2.效果

  我們這裡看下經過取輪廓操作的效果。

圖19 取輪廓操作

  在圖中,紅色的線條就是輪廓,可以看到,有非常多的輪廓。取輪廓操作就是將影像中的所有獨立的不與外界有交接的圖塊取出來。然後根據這些輪廓,求這些輪廓的最小外接矩形。這裡面需要注意的是這裡用的矩形是RotatedRect,意思是可旋轉的。因此我們得到的矩形不是水平的,這樣就為處理傾斜的車牌打下了基礎。

  取輪廓操作的程式碼如下:

1     vector< vector< Point> > contours;
2     findContours(img_threshold,
3         contours, // a vector of contours
4         CV_RETR_EXTERNAL, // 提取外部輪廓
5         CV_CHAIN_APPROX_NONE); // all pixels of each contours

 

七.尺寸判斷

  尺寸判斷操作是對外接矩形進行判斷,以判斷它們是否是可能的候選車牌的操作。

 1.目標

  排除不可能是車牌的矩形。 

 2.效果

  經過尺寸判斷,會排除大量由輪廓生成的不合適尺寸的最小外接矩形。效果如下圖:

圖20 尺寸判斷操作
  

  通過對影像中所有的輪廓的外接矩形進行遍歷,我們呼叫CplateLocate的另一個成員方法verifySizes,程式碼如下:

 1 //! 對minAreaRect獲得的最小外接矩形,用縱橫比進行判斷
 2 bool CPlateLocate::verifySizes(RotatedRect mr)
 3 {
 4     float error = m_error;
 5     //Spain car plate size: 52x11 aspect 4,7272
 6     //China car plate size: 440mm*140mm,aspect 3.142857
 7     float aspect = m_aspect;
 8     //Set a min and max area. All other patchs are discarded
 9     //int min= 1*aspect*1; // minimum area
10     //int max= 2000*aspect*2000; // maximum area
11     int min= 44*14*m_verifyMin; // minimum area
12     int max= 44*14*m_verifyMax; // maximum area
13     //Get only patchs that match to a respect ratio.
14     float rmin= aspect-aspect*error;
15     float rmax= aspect+aspect*error;
16 
17     int area= mr.size.height * mr.size.width;
18     float r = (float)mr.size.width / (float)mr.size.height;
19     if(r < 1)
20     {
21         r= (float)mr.size.height / (float)mr.size.width;
22     }
23 
24     if(( area < min || area > max ) || ( r < rmin || r > rmax ))
25     {
26         return false;
27     }
28     else
29     {
30         return true;
31     }
32 }
View Code


  在原先的verifySizes方法中,使用的是針對西班牙車牌的檢測。而我們的系統需要檢測的是中國的車牌。因此需要對中國的車牌大小有一個認識。

  中國車牌的一般大小是440mm*140mm,面積為440*140,寬高比為3.14。verifySizes使用如下方法判斷矩形是否是車牌:

  1.設立一個偏差率error,根據這個偏差率計算最大和最小的寬高比rmax、rmin。判斷矩形的r是否滿足在rmax、rmin之間。
  2.設定一個面積最大值max與面積最小值min。判斷矩形的面積area是否滿足在max與min之間。

  以上兩個條件必須同時滿足,任何一個不滿足都代表這不是車牌。

  偏差率和麵積最大值、最小值都可以通過引數設定進行修改,且他們都有一個預設值。如果發現verifySizes方法無法發現你圖中的車牌,試著修改這些引數。

  另外,verifySizes方法是可選的。你也可以不進行verifySizes直接處理,但是這會大大加重後面的車牌判斷的壓力。一般來說,合理的verifySizes能夠去除90%不合適的矩形。

八.角度判斷

  角度判斷操作通過角度進一步排除一部分車牌。

 1.目標

  排除不可能是車牌的矩形。 

  通過verifySizes的矩形,還必須進行一個篩選,即角度判斷。一般來說,在一副圖片中,車牌不太會有非常大的傾斜,我們做如下規定:如果一個矩形的偏斜角度大於某個角度(例如30度),則認為不是車牌並捨棄。

  對上面的尺寸判斷結果的六個黃色矩形應用角度判斷後結果如下圖:


  圖21 角度判斷後的候選車牌

  可以看出,原先的6個候選矩形只剩3個。車牌兩側的車燈的矩形被成功篩選出來。角度判斷會去除verifySizes篩選餘下的7%矩形,使得最終進入車牌判斷環節的矩形只有原先的全部矩形的3%。

  角度判斷以及接下來的旋轉操作的程式碼如下:

 1 int k = 1;
 2     for(int i=0; i< rects.size(); i++)
 3     {
 4         RotatedRect minRect = rects[i];
 5         if(verifySizes(minRect))
 6         {    
 7             // rotated rectangle drawing 
 8             // Get rotation matrix
 9             // 旋轉這部分程式碼確實可以將某些傾斜的車牌調整正,
10             // 但是它也會誤將更多正的車牌搞成傾斜!所以綜合考慮,還是不使用這段程式碼。
11             // 2014-08-14,由於新到的一批圖片中發現有很多車牌是傾斜的,因此決定再次嘗試
12             // 這段程式碼。
13             if(m_debug)
14             { 
15                 Point2f rect_points[4]; 
16                 minRect.points( rect_points );
17                 for( int j = 0; j < 4; j++ )
18                     line( result, rect_points[j], rect_points[(j+1)%4], Scalar(0,255,255), 1, 8 );
19             }
20 
21             float r = (float)minRect.size.width / (float)minRect.size.height;
22             float angle = minRect.angle;
23             Size rect_size = minRect.size;
24             if (r < 1)
25             {
26                 angle = 90 + angle;
27                 swap(rect_size.width, rect_size.height);
28             }
29             //如果抓取的方塊旋轉超過m_angle角度,則不是車牌,放棄處理
30             if (angle - m_angle < 0 && angle + m_angle > 0)
31             {
32                 //Create and rotate image
33                 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
34                 Mat img_rotated;
35                 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);
36 
37                 Mat resultMat;
38                 resultMat = showResultMat(img_rotated, rect_size, minRect.center, k++);
39 
40                 resultVec.push_back(resultMat);
41             }
42         }
View Code

 

九.旋轉

  旋轉操作是為後面的車牌判斷與字元識別提高成功率的關鍵環節。

 1.目標

  旋轉操作將偏斜的車牌調整為水平。 

 2.效果

  假設待處理的圖片如下圖:

圖22 傾斜的車牌
  

  使用旋轉與不適用旋轉的效果區別如下圖:

圖23 旋轉的效果

  可以看出,沒有旋轉操作的車牌是傾斜,加大了後續車牌判斷與字元識別的難度。因此最好需要對車牌進行旋轉。

  在角度判定閾值內的車牌矩形,我們會根據它偏轉的角度進行一個旋轉,保證最後得到的矩形是水平的。呼叫的opencv函式如下:

1                 Mat rotmat = getRotationMatrix2D(minRect.center, angle, 1);
2                 Mat img_rotated;
3                 warpAffine(src, img_rotated, rotmat, src.size(), CV_INTER_CUBIC);


  這個呼叫使用了一個旋轉矩陣,屬於幾何代數內容,在這裡不做詳細解釋。

十.大小調整

  結束了麼?不,還沒有,至少在我們把這些候選車牌匯入機器學習模型之前,需要確保他們的尺寸一致。

  機器學習模型在預測的時候,是通過模型輸入的特徵來判斷的。我們的車牌判斷模型的特徵是所有的畫素的值組成的矩陣。因此,如果候選車牌的尺寸不一致,就無法被機器學習模型處理。因此需要用resize方法進行調整。

  我們將車牌resize為寬度136,高度36的矩形。為什麼用這個值?這個值一開始也不是確定的,我試過許多值。最後我將近千張候選車牌做了一個統計,取它們的平均寬度與高度,因此就有了136和36這個值。所以,這個是一個統計值,平均來說,這個值的效果最好。

  大小調整呼叫了CplateLocate的最後一個成員方法showResultMat,程式碼很簡單,貼下,不做細講了。

 1 //! 顯示最終生成的車牌影像,便於判斷是否成功進行了旋轉。
 2 Mat CPlateLocate::showResultMat(Mat src, Size rect_size, Point2f center, int index)
 3 {
 4     Mat img_crop;
 5     getRectSubPix(src, rect_size, center, img_crop);
 6 
 7     if(m_debug)
 8     { 
 9         stringstream ss(stringstream::in | stringstream::out);
10         ss << "tmp/debug_crop_" << index << ".jpg";
11         imwrite(ss.str(), img_crop);
12     }
13 
14     Mat resultResized;
15     resultResized.create(HEIGHT, WIDTH, TYPE);
16 
17     resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_CUBIC);
18 
19     if(m_debug)
20     { 
21         stringstream ss(stringstream::in | stringstream::out);
22         ss << "tmp/debug_resize_" << index << ".jpg";
23         imwrite(ss.str(), resultResized);
24     }
25 
26     return resultResized;
27 }
View Code

 

十一.總結

  通過接近10多個步驟的處理,我們才有了最終的候選車牌。這些過程是一環套一環的,前步驟的輸出是後步驟的輸入,而且順序也是有規則的。目前針對我的測試圖片來說,它們工作的很好,但不一定適用於你的情況。車牌定位以及影像處理演算法的一個大的問題就是他的弱魯棒性,換一個場景可能就得換一套工作方式。因此結合你的使用場景來做調整吧,這是我為什麼要在這裡費這麼多字數詳細說明的原因。如果你不瞭解細節,你就不可能進行修改,也就無法使它適合你的工作需求。

  討論:
  車牌定位全部步驟瞭解後,我們來討論下。這個過程是否是一個最優的解?
  毫無疑問,一個演算法的好壞除了取決於它的設計思路,還取決於它是否充分利用了已知的資訊。如果一個演算法沒有充分利用提供的資訊,那麼它就有進一步優化的空間。EasyPR的plateLocate過程就是如此,在實施過程中它相繼拋棄掉了色彩資訊,沒有利用紋理資訊,因此車牌定位的過程應該還有優化的空間。如果plateLocate過程無法良好的解決你的定位問題,那麼嘗試下能夠利用其他資訊的方法,也許你會大幅度提高你的定位成功率。

  車牌定位講完後,下面就是機器學習的過程。不同於前者,我不會重點說明其中的細節,而是會概括性的說明每個步驟的用途以及訓練的最佳實踐。在下一個章節中,我會首先介紹下什麼是機器學習,為什麼它如今這麼火熱,機器學習和大資料的關係,歡迎繼續閱讀。

  本專案的Git地址:這裡。如果有問題歡迎提issue。本文是一個系列中的第5篇,前幾篇文章見前面的部落格。


版權說明:

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

 

參考文獻:

  1.http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/imgproc/erosion_dilatation/erosion_dilatation.html



 

相關文章