1. 程式人生 > >採用SVM和神經網路的車牌識別(流程圖及詳細解釋)

採用SVM和神經網路的車牌識別(流程圖及詳細解釋)

一、整個程式的流程圖:

二、車牌定位中分割流程圖:

關於程式碼兩個if(r<1)的詳解:

參考:RotatedRect和CvBox2D。CvBox2D結構如下:(重點是angle的註釋)

三、車牌識別中字元分割流程圖:

預處理字元:對字元的預處理重要,因為我們將使用三個特徵(水平方向的直方圖,垂直方向的直方圖和影象的區域的部分畫素)作為一個整的特徵向量。

第一步我們通過仿射變換warpAffine影象進行平移。

如果高度大於寬度,如下圖:

情況二類似。

在預處理中,如果我們只是簡單的對影象的大小進行歸一化,如在文章中,歸一化為20*20。那麼我們將看到如下圖所示的情況。左圖是使用了仿射變換後再resize,右圖之間使用了resize(特徵丟失了)。而且我們檢測後字元會全是0。

書名:《Mastering OpenCV with Practical Computer Vision Projects》

不免有錯,不在本文章中修改了,以防再次出現亂碼,這裡整理好了,但是程式碼沒有中文註釋,word版本中有。

本人英語四級水平,以下翻譯只供自己存檔和像我一樣的初學者參閱。裡面錯誤肯定很多,也有好多英語句子我不太明白的。敬請大家糾正。我最近還要好好的消化一下。還會進行修改的。最近剛看了opencv中文版的機器學習篇,opencv自帶的特徵資料都是已經做好了的,對於特徵資料的生成,矩陣的操作不熟悉的我,還是在腦子裡難以形成一個整體的思路。想在網上下載一些例項專案的程式碼,好像很難,即使找到了又沒有詳細的說明,對於一個初學者可能要花好久的時間才能完全整理清楚(還必須查閱相關資料)。最近有幸在opencv論壇,看到朋友們推薦的這本書,而且附有原始碼,能夠執行,看到了效果。這使我決定要看一看,對於一個不喜歡看英文的我,硬著頭皮把第五章看完了。讀英文最大的痛苦是句子長,來回的修飾,然而第二大痛苦就是,我們記憶裡不怎麼好,讀完一段話,即使每句話都好像能理解,然而將的是什麼還是雲裡霧裡。其實我感覺最好的方法,就是自己大約理解了,翻譯下來,整成中文的,然後推敲一下是不是語義上合理。

<內容>

本章向大家介紹建立一個自動車牌識別應用(ANPR)所需要的步驟。基於不同的情況,有不同的方法和技術。例如,IR camera(紅外線攝像機),固定車位置,光亮情況,等等。我們開始構建一個ANPR應用,來檢測離車2-3米拍的照片中的車牌。在模糊的光線下,並且不是平行與地面而是與車牌的有個小角度的傾斜。

本章的主要目的是向大家介紹影象的分割和特徵提取,模式識別的基礎,和兩個重要的模式識別演算法:支援向量機和人工神經網路。在這一章,我們將包含以下內容:

1、ANPR(自動車牌識別)

2、車牌檢測

3、車牌識別

ANPR介紹:

ANRP也就是眾所周知的ALPR,或者AVI,或者CPR,是一種用在光學字元識別的監視方法和其他方法,例如分割和檢測來讀取車牌號。

在ANPR系統中最好的結果是使用一個紅外線攝像機,因為檢測和OCR(光學字元識別)分割之前的分割步驟變的簡單,乾淨和錯誤最小化。這是由於光線法則,最基本的是因為入射角度等於反射角度。當我們看一個光滑的表面例如一個平面鏡,我們能看到這個基本的反射。粗糙表面的反射例如一張紙導致的反射稱為漫射或者散射。車牌號的主要部分有一個特殊的特性叫做回覆反射。車牌的表面

是用覆蓋有成千上萬個細小半球的材料做成的。這樣會使光線回覆反射到光線源,我們從下面的圖可以看到:

                    角度反射             散射或者漫射          回覆反射

如果我們使用一個帶有紅外線投影結構和濾波的攝像機,我們使用帶有紅外線的攝像機重新獲取,將得到一個非常高質量的照片用來分割和隨後的檢測和識別車牌數字。即不依賴於任何光線環境,如下圖所示:

在這一章,我們沒有使用紅外線攝像,我們使用常規的攝像。我們這樣做,以至於我們沒有得到最好的結果,得到的是一個更高水平的檢測錯誤和高的錯誤識別率。這與我們使用紅外攝像機所期待的結果截然相反。然而,兩者的步驟是一樣的。

每個國家車牌的大小和規格不同,為了獲得更好的結果和減少錯誤,我們知道這些規格是佷有用的。本章使用的演算法意圖是闡述ANPR的基本原理和西班牙車牌,但是我們能把他們擴到任何國家或者規格車牌。

在本章,我將使用來之西班牙的車牌。在西班牙,有三種不同大小和形狀的車牌。我們將使用最普通(使用最多)的車牌,其大小是520*110mm。兩種字元(數字和字母)的間距是41mm。數字和數字之前(或者字母和字母之間)距離是14mm。第一組字元含有四個數字。另外一組含有三個字母,其中不包括母音字母:A,E,I,O,U。和N,Q。所有的字元大小為45*77。

這些資料對於字元分割很重要,因為我們能夠檢查兩個字元和空格,來核實我們得到是一個字元而沒有其它圖片部分。如下是一個車牌圖。

ANPR 演算法

在解釋ANPR程式碼之前,我們需要定義演算法的主要步驟和任務。ANPR主要分為兩步:車牌的檢測和車牌的識別。車牌檢測就是檢測車牌在整個影象幀中的位置。當一個影象中的車牌檢測到時,車牌的分割將交給接下來的一步——車牌識別。在車牌識別中,我們用OCR演算法來決定車牌上的字母數字的字元。

在下圖我們可以看到兩個主要演算法的步驟,車牌檢測和車牌識別。車牌識別之後,程式將在影象幀中畫出檢測到的車牌。這個演算法能返回壞的結果甚至沒有結果(檢測不到)。

每個步驟都展示在上邊的圖中,我們來定義另外三個步驟。他們通常用在模式識別演算法中。

1、分割。該步檢測和移動影象中每個感興趣的區域。

2、特徵提取。該步提取每個塊的一系列的特徵。

3、分類。該步從車牌識別步驟或者把影象部分分為有車牌和無車牌的車牌檢測的步驟,(上述兩個步驟中)提取每個字元。

下圖向我們展示了在整個演算法中模式識別的步驟。

拋開主要的應用,即該應用的目的是檢測和識別一個車牌數字,我們來簡單介紹兩個不經常被介紹的任務:

1、怎麼樣訓練一個模式識別系統?

2、怎麼樣來評估這樣的一個系統?

然而,這些任務通常比主要應用本身更重要。因為,如果我們不能正確的訓練模式識別系統,我們的系統就會失敗並且不能正確的工作。不同的模式需要不同型別的訓練和評估。我們需要在不同的環境,條件,帶有不同特徵,來評估我們的系統,進而得到最好的效果。這兩個任務有時一起使用,因為不同的特徵能產生不同的結果,這種情況我們會在評估部分看到。

車牌檢測

在這一步中,我們需要檢測在一個影象幀中所有的車牌。為了做這個任務。我們分為兩個主要的步驟:分割和分割分類。特徵步驟不在闡述,是因為我們用影象部分作為一個特徵向量。

第一步(分割),我們應用不同的濾波器,形態學操作,輪廓演算法,和確認獲取影象的這些部分可能有一個車牌。

第二步(分類),我們採用支援向量機(SVM)分類出每個影象部分——我們的特徵。在建立主程式之前,我們訓練兩個不同的類別——有車牌和無車牌。我們採用前向_水平視覺的彩色影象,寬度為800畫素,從離車的2到4米處獲取的。這樣要求對確保正確的分割很重要。如果我們建立了一個多尺度影象演算法,我們能夠展示檢測。

在下面的影象中,我們展示了車牌檢測所包含的所有處理:

1、Sobel濾波

2、閾值操作

3、閉操作

4、填充區域的掩膜

5、把可能檢測到車牌標記為紅色(特徵影象)

6、SVM分類後,檢測到的車牌

分割

分割是把一幅影象分割成許多部分的過程。這個過程簡化影象分析,使特徵提取更容易。

車牌分割的一個重要特徵是在車牌中的高數量的垂直邊緣(就是垂直邊緣比較多)(假定照片是從前面拍的,車牌沒有旋轉,並且沒有視覺上的扭曲。這個特徵可以用來在分割的第一步(sobel濾波),來排除那些沒有垂直邊緣的區域。

在尋找垂直邊緣之前,我們需要把彩色影象轉換為灰度影象,因為彩色在我們的任務中沒有幫助,並且移除來之相機或者外界的噪聲。如果我們不應用去噪方法,我們將得到許多的垂直邊緣,將會產生檢測失敗。

  1. //convert image to gray  
  2. Mat img_gray;  
  3. cvtColor(input,img_gray,CV_BGR2GRAY);  
  4. blur(img_gray,img_gray,Size(5,5));  

為了尋找垂直邊緣,我們採用sobel濾波並且找到一階垂直方向導數。這個導數是個數學函式,允許我們找到影象上的垂直邊緣。Opencv中Sobel函式的定義如下:

  1. void Sobel(InputArray src,OutputArray dst,int ddepth,int xorder,int yoder,int ksize=3,double scale=1,double delta=0,int borderType=BORDER_DEFAULT)  

這裡,ddepth是目的影象的深度,xorder是x導數的次序(即x的order階導數),yorder為y導數的次序。ksize核的大小要麼是1,3,5要麼是7。scale用在計算導數值,是個可選項。delta是一個加到結果的可選項。bordertype是畫素的插值方法。

在本程式中,我們使用xorder=1,yorder=0,ksize=3;

  1. //尋找垂直方向的線,車牌還很的垂直線密度  
  2. Mat img_sobel;  
  3. Sobel(img_gray,img_sobel,CV_8U,1,0,3,1,0);  

Sobel濾波後,我們應用一個閾值濾波器來獲得一個二值影象,閾值的通過otsu方法獲得。Ostu演算法需要一個8點陣圖像作為輸入,該方法自動的決定最佳的閾值。

  1. Mat img_threshold;  
  2. threshold(img_sobel,img_threhold,0,255,CV_THRESH_OTSU+CV_THRESH_BINARY);  

為了在閾threshold函式中定義ostus方法,我們使用CV_THRESH_OTST值混合引數。則閾值引數被忽略。 (小心:當CV_THRSH_OTST被定義,threshold函式會通過ostus演算法返回最優閾值) 通過應用一個閉操作,我們能夠去掉每個垂直邊緣線的空白部分。並且連線有含有邊緣數量很多的所有區域。在這一步,我們得到可能的含有車牌的區域。

  1. Mat element = getStructuringElement(MORPH_RECT, Size(17, 3));  

在morphologEx函式中使用上述定義的結構元。

  1. morphologyEx(img_threshold, img_threshold, CV_MOP_CLOSE, element);  

應用完這些操作之後,我們的得到了可能含有車牌的區域,大部分的這些區域將沒有包含車牌。這些區域用連通部分分析(opencv 中文版319頁)或者使用findContours函式來分開。最後一個函式用不同的方法和結果來獲得一個二值影象的輪廓。我們只需要用任何分層關係和任何多邊形近似結果來獲得外輪廓。

  1. //Find contours of possibles plates  
  2. vector< vector< Point> > contours;  
  3. findContours(img_threshold,  
  4.             contours,           // a vector of contours  
  5.             CV_RETR_EXTERNAL,   // retrieve the external contours  
  6.             CV_CHAIN_APPROX_NONE); // all pixels of each contour  

為了檢測每個輪廓,提取輪廓的最小矩形邊界框。OpenCV採用minAreaRect函式來完成這個任務。這個函式返回一個旋轉矩形類物件:RotatedRect。我們使用vector容器迭代器訪問每一個輪廓,我們可以得到旋轉的矩行,在分類前做一些初步的確認。

  1. //Start to iterate to each contour found  
  2. vector<vector<Point> >::iterator itc= contours.begin();  
  3. vector<RotatedRect> rects;  
  4. //Remove patch that has  no inside limits of aspect ratio and area.   
  5. while (itc!=contours.end()) {  
  6. //Create bounding rect of object  
  7.   RotatedRect mr= minAreaRect(Mat(*itc));  
  8.   if( !verifySizes(mr)){  
  9.     itc= contours.erase(itc);  
  10.   }else{  
  11.   ++itc;  
  12.   rects.push_back(mr);  
  13.   }  
  14. }  

我們基於面積和寬高比,對於檢查到的區域做一下確認。如果寬高比大於為520/110=4.727272(車牌寬除以車牌高)(允許帶有40%的誤差)和邊界在15畫素和125畫素高的區域,我們才認為是一個車牌區域。這些值根據影象的大小和相機的位置進行計算。

  1. bool DetectRegions::verifySizes(RotatedRect candidate ){  
  2.   float error=0.4;  
  3. //Spain car plate size: 52x11 aspect 4,7272  
  4.   const float aspect=4.7272;  
  5. //Set a min and max area. All other patches are discarded  
  6.   int min= 15*aspect*15; // minimum area  
  7.   int max= 125*aspect*125; // maximum area  
  8. //Get only patches that match to a respect ratio.  
  9.   float rmin= aspect-aspect*error;  
  10.   float rmax= aspect+aspect*error;  
  11.   int area= candidate.size.height * candidate.size.width;  
  12.   float r= (float)candidate.size.width / (float)candidate.size.height;  
  13.   if(r<1)  
  14. r= 1/r;  
  15.   if(( area < min || area > max ) || ( r < rmin || r > rmax )){  
  16.     return false;  
  17.   }else{  
  18.   return true;  
  19.   }  
  20. }  

我們利用車牌的白色背景屬性可以進一步改善。所有的車牌都有統一的背景顏色。我們可以使用漫水填充演算法來獲取旋轉矩陣的精確修剪。

剪下車牌的第一步是在最後一個旋轉矩陣中心的附近得到一些種子,在寬度和高度中得到最小的車牌,用它來產生離中心近的種子。

我們想要選擇白色區域,我們需要一些種子,至少有一個種子接觸到白色區域。接著對每一個種子,我們使用floodFill函式來得到一個掩碼影象,用來儲存新的最接近的修剪區域。

  1. for(int i=0; i< rects.size(); i++){  
  2. //For better rect cropping for each possible box  
  3. //Make floodfill algorithm because the plate has white background  
  4. //And then we can retrieve more clearly the contour box  
  5. circle(result, rects[i].center, 3, Scalar(0,255,0), -1);  
  6. //get the min size between width and height  
  7. float minSize=(rects[i].size.width < rects[i].size.height)?rects[i].  
  8. size.width:rects[i].size.height;  
  9. minSize=minSize-minSize*0.5;  
  10. //initialize rand and get 5 points around center for floodfill   
  11. algorithm  
  12. srand ( time(NULL) );  
  13. //Initialize floodfill parameters and variables  
  14. Mat mask;  
  15. mask.create(input.rows + 2, input.cols + 2, CV_8UC1);  
  16. mask= Scalar::all(0);  
  17. int loDiff = 30;  
  18. int upDiff = 30;  
  19. int connectivity = 4;  
  20. int newMaskVal = 255;  
  21. int NumSeeds = 10;  
  22. Rect ccomp;  
  23. int flags = connectivity + (newMaskVal << 8 ) + CV_FLOODFILL_FIXED_  
  24. RANGE + CV_FLOODFILL_MASK_ONLY;  
  25. for(int j=0; j<NumSeeds; j++){  
  26.   Point seed;  
  27.   seed.x=rects[i].center.x+rand()%(int)minSize-(minSize/2);  
  28.   seed.y=rects[i].center.y+rand()%(int)minSize-(minSize/2);  
  29.   circle(result, seed, 1, Scalar(0,255,255), -1);  
  30.   int area = floodFill(input, mask, seed, Scalar(255,0,0), &ccomp,     
  31. Scalar(loDiff, loDiff, loDiff), Scalar(upDiff, upDiff, upDiff),   
  32. flags);  
  33.   }  

漫水填充函式用顏色把連通區域填充到掩碼影象,填充從種子開始。設定與相鄰畫素或者種子畫素之間差異的最下界和最上界(如果設定了CV_FLOODFILL_FIXED_RANGE,則填充的畫素點都是與種子點進行比較。就是如果畫素值為x,seed-low<=x<=seed+up,則該位置將被填充)

  1. int floodFill(InputOutputArray image, InputOutputArray mask, Point   
  2. seed, Scalar newVal, Rect* rect=0, Scalar loDiff=Scalar(), Scalar   
  3. upDiff=Scalar(), int flags=4 )  

引數newVal是填充到影象的新值.引數loDiff和upDiff就是上邊描述的。 引數flag由以下組成:

1、低位:包含連通的值,預設4連通,或者8連通。連通決定了畫素的哪個鄰居畫素被考慮進來。

2、高位:可以為0,也可以是下邊值的組合:CV_FLOODFILL_FIXED_RANGE 和CV_FLOODFILL_MASK_ONLY.

CV_FLOODFILL_FIXED_RANGE用來設定當前畫素和種子畫素之間的差異。

CV_FLOODFILL_MASK_ONLY,將填充掩碼影象,而不是影象本身。

一旦我們得到了用來剪下的掩碼影象,我們進而得到掩碼影象點的最小外接矩形,再次檢查矩形大小。對於每一個掩碼,一個白色畫素獲得位置用minAreaRect函式重新得到最相近的修剪區域。

//檢查新城的漫水填充掩碼是不是一個正確的塊。(因為使用車牌,車牌有邊界,漫水填充不會超過車牌的邊界,而對於其他區域(檢查出來的矩形)漫水填充會佔據很多區域,形成的矩形也很大,再進入verifySizes函式時,可能就會被丟棄,得到更可能是車牌的區域)

//得到所有的點為最小旋轉矩形

  1. //Check new floodfill mask match for a correct patch.  
  2. //Get all points detected for minimal rotated Rect  
  3. vector<Point> pointsInterest;  
  4. Mat_<uchar>::iterator itMask= mask.begin<uchar>();  
  5. Mat_<uchar>::iterator end= mask.end<uchar>();  
  6. for( ; itMask!=end; ++itMask)  
  7.   if(*itMask==255)  
  8.   pointsInterest.push_back(itMask.pos());  
  9.   RotatedRect minRect = minAreaRect(pointsInterest);  
  10.   if(verifySizes(minRect)){  
  11. …  

既然分割過程已經完成並且我們得到了有效的區域。我們能夠修剪每一個檢測到的區域,去掉那些可能存在的旋轉,修剪影象區域,重新設定影象的大小,並且均衡化修剪過的區域。 首先,我們需要通過函式getRotationMatrix2D來獲得轉換矩陣,用來去掉那些檢測區域的旋轉。我們需要注意高度,因為RectatedRect類能夠被返回並且旋轉了90度。因此我們必須檢查矩形的寬高比,如果它小於1,則進行90度的旋轉。

  1. //Get rotation matrix  
  2. float r= (float)minRect.size.width / (float)minRect.size.height;  
  3. float angle=minRect.angle;  
  4. if(r<1)  
  5.   angle=90+angle;  
  6.   Mat rotmat= getRotationMatrix2D(minRect.center, angle,1);  

用轉換矩陣,我們現在能通過仿射變換旋轉輸入影象了(幾何中的仿射變換是平行線到平行線(可以參考opencv中文版 186頁,仿射變換和透視變換的區別)。在warpAffine函式中,我們設定輸入輸出影象,轉換矩陣,輸出影象的大小(在我們的程式中,我們使用和輸入影象一樣的大小),插值方法。如果我們需要的話,我們可以定義邊界方法和邊界值。

  1. //Create and rotate image  
  2. Mat img_rotated;  
  3. warpAffine(input, img_rotated, rotmat, input.size(), CV_INTER_CUBIC);  

我們旋轉影象之後,我們用getRectSubPix函式來修剪影象,該函式修剪拷貝給定長度和寬度,以及中心點的影象部分。如果影象旋轉了,我們需要使用C++swap函式來改變寬和高的大小。

  1. //Crop image  
  2. Size rect_size=minRect.size;  
  3. if(r < 1)  
  4. swap(rect_size.width, rect_size.height);  
  5. Mat img_crop;  
  6. getRectSubPix(img_rotated, rect_size, minRect.center, img_crop);  

修剪的影象不能很好的在訓練和分類中使用,因為他們沒有相同的大小。並且,每個影象包含不同的光照條件,增加了他們之間的差別。為了解決這個問題。我們把所有的影象調整為統一大的大小,採用直方圖均衡化。

  1. Mat resultResized;  
  2. resultResized.create(33,144, CV_8UC3);  
  3. resize(img_crop, resultResized, resultResized.size(), 0, 0, INTER_  
  4. CUBIC);  
  5. //Equalize cropped image  
  6. Mat grayResult;  
  7. cvtColor(resultResized, grayResult, CV_BGR2GRAY);  
  8. blur(grayResult, grayResult, Size(3,3));  
  9. equalizeHist(grayResult, grayResult);  

對於每一個檢測到區域,我們儲存修剪過的影象。把他們的位置儲存在vector中.

  1. output.push_back(Plate(grayResult,minRect.boundingRect()));  

分類

我們預處理和分割影象的所有可能部分之後,我們現在需要判別每一個分割是不是一個車牌。為這樣做,我們使用SVM演算法。

支援向量機是一個模式識別演算法,它是監督學習演算法的一份子,最初是建立是為了二值分類的。有監督的學習是一種機器學習演算法,它通過標籤資料的使用進行學習。我們需要一些帶有標籤的資料來訓練這個演算法。每一個數據集需要有一個類別。

SVM建立一個或多個超平面,用來區分每類資料。

一個典型的例子是2維點集,它定義了兩個類。SVM尋找最優線來區分每個類。

在任一分類之前的第一個任務是訓練我們的分類器。這項工作的完成優先於開始主要的應用程式。它被稱為離線訓練。這不是一個簡單的工作,因為它需要充足的資料來訓練這個系統。但是大的資料集並不總是暗示最好的結果。在我們的例子中,我們沒有充足的資料,是因為沒有一個公共的車牌資料的事實。正因為如此,我們需要拍數百張車照,然後預處理和分割所有的照片。

我們用大小為144*33的75張車牌和35非車牌來訓練我們的系統。我們在下面的圖中能看到資料的一個樣本。這並不是個大的資料集,對我們的需求來說,它已經可以充足的得到一個體面的結果。在實際的應用中,我們需要訓練更多的資料。

很容易理解機器學習是怎樣工作的,我們使用分類器演算法的影象畫素特徵(想一下,有很多更好的方法和特徵來訓練一個SVM,比如主成分分析,傅立葉變換,紋理分析,等等)

我們需要通過DectectRegions類建立影象和訓練我們的系統。把變數savingRegions設定為真用來儲存影象。我們可以通過segementAllFiles.sh bash指令碼檔案把資料夾下的所有影象檔案上重複這個過程。該檔案可以從書的原始碼中獲得。

為了使這個更簡單,我們儲存了已經處理好和準備好是所有影象的資料,放在了xml檔案裡,之間使用SVM函式呼叫。trainSVM.cpp程式用一些資料夾下的數張圖片檔案建立的xml檔案。

小心:為機器學習的Opencv演算法訓練的資料儲存在一個N*M的矩陣中,N表示樣本數,M表示特徵數。每個資料集作為一行儲存在訓練矩陣中(就是N*M個畫素點,展開成一行,作為訓練矩陣的N*M個特徵。詳細可以參考trainSVM.cpp程式碼中)

類別儲存在另外一個大小為N*1的矩陣中。每一個類通過一個浮點數來識別。(這裡程式碼中是不是int?)

opencv有個簡單的方式來管理xml或者Json個數的資料檔案,即FileStorage類。這個類使我們儲存和讀取opencv變數和結構體或者我們傳統的變數。使用這個函式,我們嫩而過讀取訓練的資料矩陣和訓練的類別,並且把他們儲存在SVM_TrainingData 和SVM_Classes中:

  1. FileStorage fs;  
  2. fs.open("SVM.xml", FileStorage::READ);  
  3. Mat SVM_TrainingData;  
  4. Mat SVM_Classes;  
  5. fs["TrainingData"] >> SVM_TrainingData;  
  6. fs["classes"] >> SVM_Classes;  

現在我們需要設定SVM引數,定義最基本的引數來供SVM演算法的使用。我們使用CvSVMParam結構來定義它。它是一個對映,用來把訓練的資料提升到一個線性可分的資料集合。這種對映包括資料維數的增加,通過一個核函式可以有效的得到。我們在這裡選用CvSVM::LINEAR型別,這就是意味著沒有對映。

  1. //Set SVM params  
  2. CvSVMParams SVM_params;  
  3. SVM_params.kernel_type = CvSVM::LINEAR;  

這時我們建立和訓練我們的分離器。Opencv為支援向量機演算法定義了CvSVM類。我們用訓練的資料來,類別和引數資料來初始化它。

  1. CvSVM svmClassifier(SVM_TrainingData, SVM_Classes, Mat(), Mat(), SVM_  
  2. params);  

我們的分離器準備好了,我們可以使用SVM類的predict函式來預測一個可能的修剪影象。這個函式返回類別i。在我們的例項中,我們標記每一個車牌類別為1,非車牌類別標記為0。對於每個檢測到的區域,我們使用SVM來分出它是車牌還是非車牌,並且只儲存正確的響應。下面的程式碼是主程式的一部分,成為線上處理:

  1. vector<Plate> plates;  
  2. for(int i=0; i< possible_regions.size(); i++)  
  3. {  
  4.   Mat img=possible_regions[i].plateImg;  
  5.   Mat p= img.reshape(1, 1);//convert img to 1 row m features  
  6.   p.convertTo(p, CV_32FC1);  
  7.   int response = (int)svmClassifier.predict( p );  
  8.   if(response==1)  
  9.     plates.push_back(possible_regions[i]);  
  10. }  

車牌識別

車牌識別目標的第二步就是用光符字元識別來獲取車牌上的字元。對於每個檢測到的車牌,我開始分割車牌得到每個字元,並且使用人工神經網路機器學習演算法來識別字符。同時在這一部分我們也將學習怎麼樣評估一個分類演算法。

OCR分割

首先,我們獲得車牌影象的部分作為OCR分割函式是輸入(已經均衡化直方圖的影象)。我們應用一個閾值濾波器濾波,並把濾波後的閾值影象作為尋找輪廓演算法的輸入。我們可以通過下圖看到過程:

分割處理的程式碼如下;

  1. Mat img_threshold;  
  2. threshold(input, img_threshold, 60, 255, CV_THRESH_BINARY_INV);  
  3. if(DEBUG)  
  4.   imshow("Threshold plate", img_threshold);  
  5. Mat img_contours;  
  6. img_threshold.copyTo(img_contours);  
  7. //Find contours of possibles characters  
  8. vector< vector< Point> > contours;  
  9. findContours(img_contours,  
  10.             contours,            // a vector of contours  
  11.             CV_RETR_EXTERNAL,    // retrieve the external contours  
  12.             CV_CHAIN_APPROX_NONE); // all pixels of each contour  

我們使用CV_THRESH_BINARY引數通過把白色值變為黑色,黑色值變為白色來實現閾值輸出的反轉。因為我們需要獲取字元的輪廓,而輪廓的演算法尋找的是白色畫素。

對於每一個檢測到的輪廓,我們核實一下大小,去除那些規格太小的或者寬高比不正確的區域。字元是45/77的寬高比。我們允許用於選擇或者扭曲帶來的百分之35的誤差。如果一個區域面積高於80%(就是畫素大於0的超過80%),則我們認為這個區域是一個黑色塊.(因為白變黑,黑變白處理了),不是字元。為了計算面積我們使用countNonZero函式來計算高於0值的畫素值。

  1. bool OCR::verifySizes(Mat r)  
  2. {  
  3.   //Char sizes 45x77  
  4.   float aspect=45.0f/77.0f;  
  5.   float charAspect= (float)r.cols/(float)r.rows;  
  6.   float error=0.35;  
  7.   float minHeight=15;  
  8.   float maxHeight=28;  
  9.   //We have a different aspect ratio for number 1, and it can be    
  10.   //~0.2  
  11.   float minAspect=0.2;  
  12.   float maxAspect=aspect+aspect*error;  
  13.   //area of pixels  
  14.   float area=countNonZero(r);  
  15.   //bb area  
  16.   float bbArea=r.cols*r.rows;  
  17.   //% of pixel in area  
  18.   float percPixels=area/bbArea;  
  19.   if(percPixels < 0.8 && charAspect > minAspect && charAspect <  
  20.   maxAspect && r.rows >= minHeight && r.rows < maxHeight)  
  21.   return true;  
  22.   else  
  23.   return false;  
  24. }  

如果分割字元被證實了,我們必須對它進行預處理,設定同樣的大小和所有字元的位置,把它儲存在一個附加的charsegment類的物件中。這個類儲存了字元的影象和位置,我們需要給字元排序,因為尋找輪廓的演算法返回的輪廓並不是規定的順序。

特徵提取

下一步是對每個分割的字元進行特徵提取用神經網路演算法訓練和分類。不像車牌檢測特徵提取的步驟那樣使用的是SVM,我們不用影象的所有畫素。我們使用一個更加通用的特徵在光符字元識別中,其中包括水平和垂直累加直方圖,一個低解析度樣本。我們在下圖可以看到更加形象的特徵,每個影象有一個5*5的低解析度和直方圖的累加。

對於每一個字元,我們用countNonZero計算每一行或者每一列的非零的個數,把他們儲存在一個新的資料矩陣mhist中。我們使用minMaxLoc函式找到資料矩陣的最大值,並用這個值來歸一化,即採用convertTo函式將mhist的所有元素都除以這個最大的值。我們建立一個ProjecteddHistotram函式來建立累積直方圖,這個函式帶有兩個輸入的引數,一個是一個二值影象,一個是我們需要的直方圖的型別即水平或者垂直。

  1. Mat OCR::ProjectedHistogram(Mat img, int t)  
  2. {  
  3.   int sz=(t)?img.rows:img.cols;  
  4.   Mat mhist=Mat::zeros(1,sz,CV_32F);  
  5.   for(int j=0; j<sz; j++){  
  6.     Mat data=(t)?img.row(j):img.col(j);  
  7.     mhist.at<float>(j)=countNonZero(data);  
  8.   }  
  9. //Normalize histogram  
  10. double min, max;  
  11. minMaxLoc(mhist, &min, &max);  
  12. if(max>0)  
  13. mhist.convertTo(mhist,-1 , 1.0f/max, 0);  
  14.   return mhist;  
  15. }  

其他特徵使用一個低解析度的樣本影象。替代使用整個字元影象,我們建立一個低解析度字元,例如5*5。我們用5*5,10*10,20*20大小的字元來訓練我們的系統(本程式中Charsize=20)。然後評估哪一個返回的是最好的結果,然後我們在系統中使用它。一旦我們擁有了特徵,我們建立一個M列的矩陣,矩陣的每一行的每一列都是特徵值。

  1. Mat OCR::features(Mat in, int sizeData)  
  2. {  
  3. //Histogram features  
  4.   Mat vhist=ProjectedHistogram(in,VERTICAL);  
  5.   Mat hhist=ProjectedHistogram(in,HORIZONTAL);  
  6. //Low data feature  
  7.   Mat lowData;  
  8.   resize(in, lowData, Size(sizeData, sizeData) );  
  9.   int numCols=vhist.cols + hhist.cols + lowData.cols *   
  10.   lowData.cols;  
  11.   Mat out=Mat::zeros(1,numCols,CV_32F);  
  12.   //Assign values to feature  
  13.   int j=0;  
  14.   for(int i=0; i<vhist.cols; i++)  
  15.   {  
  16.     out.at<float>(j)=vhist.at<float>(i);   
  17.     j++;  
  18.   }  
  19.   for(int i=0; i<hhist.cols; i++)  
  20.   {  
  21.   out.at<float>(j)=hhist.at<float>(i);  
  22.   j++;  
  23.   }  
  24.   for(int x=0; x<lowData.cols; x++)  
  25.   {  
  26.     for(int y=0; y<lowData.rows; y++)  
  27.     {  
  28.       out.at<float>(j)=(float)lowData.at<unsigned char>(x,y);  
  29.       j++;  
  30.     }  
  31.   }  
  32.   return out;  
  33. }  

OCR分類

在分類這一部,我們使用人工神經網路機器學習演算法。更具體一點,多層感知器(MLP),一個廣泛使用的人工神經網路演算法。

MLP神經網路有一個輸入層,輸出層和一個或多個隱層。每一層有一個活多個神經元連線著前向和後向層。

下面的例子表示一個3層感知器(它是一個二值分類器,它對映輸入的一個實值向量,輸出單一的二值),它帶有三個輸入,兩個輸出和一個含有5個神經元的隱層。

MLP中所有的神經元,都相似,每個神經元有幾個輸出(前向連線神經元)和幾個輸出連線著相同的值(後向連線神經元)。每個神經元用帶有權重的輸入的和加上一個偏移量再經過一個選擇的啟用函式轉換後得到輸出結果。

有三個廣泛使用的啟用函式:Identiy,S函式,高斯函式。最常用的預設的啟用函式是S函式。其中alpha和beta設定為1:

一個訓練人工神經網路的輸入是一個特徵向量。它傳輸值到隱層。用權重和啟用函式來計算結果。它進一步的把輸出結果往下傳輸直到到達含有一定數量的神經元類別時。

每一層的權重,突觸,和神經元通過訓練神經網路演算法來計算和學習。為了訓練我們的分類器。在SVM訓練時,我們建立兩個資料矩陣,但是訓練的標籤有點不同。替代N*1的矩陣(這裡N代表訓練資料的行數,1是矩陣的列),那裡我們使用了數字作為表示符。我們必須建立一個N*M大小的矩陣,這裡的N是訓練/樣本的資料,M是類別(我們的示例中是10個數字和20個字母)。如果資料行i歸宿與類別j,則我們將位置(i,j)的設定為1.

我們建立OCR::train函式來建立我們所需要的矩陣並且用訓練資料矩陣,類別矩陣和隱層神經元的數目來訓練我們的系統。訓練資料從xml檔案匯入,就像我們為SVM訓練做的那樣。

我們必須定義每一層的神經元的數目來初始化ANN類。在我們的例子中,我們只只用了一個隱層,因此我們定義一個1行3列的矩陣。第一類位置是特徵的數目,第二列位置是隱層所含隱藏的神經元的數目,第三列的位置是類的數目。

Opencv為ANN定義了一個CvANN_MLP類。通過定義的層的數目和原子數,啟用函式和alpha和beta引數用建立函式來初始化類。

  1. void OCR::train(Mat TrainData, Mat classes, int nlayers)  
  2. {  
  3.   Mat layerSizes(1,3,CV_32SC1);  
  4.   layerSizes.at<int>(0)= TrainData.cols;  
  5.   layerSizes.at<int>(1)= nlayers;  
  6.   layerSizes.at<int>(2)= numCharacters;  
  7.   ann.create(layerSizes, CvANN_MLP::SIGMOID_SYM, 1, 1); //ann is    
  8.   global class variable  
  9.   //Prepare trainClasses  
  10.   //Create a mat with n trained data by m classes  
  11.   Mat trainClasses;  
  12.   trainClasses.create( TrainData.rows, numCharacters, CV_32FC1 );  
  13.   for( int i = 0; i <  trainClasses.rows; i++ )  
  14.   {  
  15.     for( int k = 0; k < trainClasses.cols; k++ )  
  16.     {  
  17.     //If class of data i is same than a k class  
  18.     if( k == classes.at<int>(i) )  
  19.     trainClasses.at<float>(i,k) = 1;  
  20.     else  
  21.     trainClasses.at<float>(i,k) = 0;  
  22.     }  
  23.   }  
  24.   Mat weights( 1, TrainData.rows, CV_32FC1, Scalar::all(1) );  
  25.   //Learn classifier  
  26.   ann.train( TrainData, trainClasses, weights );  
  27.   trained=true;  
  28. }  

訓練之後,我們能使用OCR::classify函式對任何分割的特徵車牌進行分類。

  1. int OCR::classify(Mat f)  
  2. {  
  3.   int result=-1;  
  4.   Mat output(1, numCharacters, CV_32FC1);  
  5.   ann.predict(f, output);  
  6.   Point maxLoc;  
  7.   double maxVal;  
  8.   minMaxLoc(output, 0, &maxVal, 0, &maxLoc);  
  9.   //We need to know where in output is the max val, the x (cols) is   
  10.   //the class.  
  11.   return maxLoc.x;  
  12. }  

CvANN_MLP類使用predict函式來為一個特徵向量分類。不想SVM的分類函式,ANN的predict函式返回的是一個向量,其大小和類的數目相同,帶有屬於每一個類的輸入特徵的機率。(計算向量中的每一個值,相當於該向量屬於概率的概率,大題可以這樣認為)

為了得到更好的效果,我們使用minMaxLoc函式來獲得響應的最大數和最小數(這裡只用了最大數),以及在矩陣中的位置。我們字元的類通過x位置的一個高值(值大的)來指定。

為了完成每一個車牌的檢測,我們對字元排好序,用Plate類的str()函式來返回一個string。並且我們可以在原圖上畫出來。

  1. string licensePlate=plate.str();  
  2. rectangle(input_image, plate.position, Scalar(0,0,200));  
  3. putText(input_image, licensePlate, Point(plate.position.x, plate.  
  4. position.y), CV_FONT_HERSHEY_SIMPLEX, 1, Scalar(0,0,200),2);  

評估

我們的工程完成了,但是當我們訓練一個機器學習演算法比如Ocr,例如,我們需要指定最好的特徵和引數來使用,怎樣在我們的系統中正確的分類,識別和檢測錯誤。

我們需要用不同的情況和引數來評估我們的系統,評價錯誤的產生,和得到最好的引數來使這些錯誤最小化。

在這一章,我們評估OCR任務使用下面的變數:低解析度影象的大小和隱層含有的神經元的數目。

我們已經建立了evalOCR.cpp程式,在那裡我們將使用通過trainOCR.cpp產生的XML訓練資料檔案。OCR.xml檔案包含5*5,10*10,15*15,20*20下采樣影象特徵構成的訓練資料矩陣。

  1. Mat classes;  
  2. Mat trainingData;  
  3. //Read file storage.  
  4. FileStorage fs;  
  5. fs.open("OCR.xml", FileStorage::READ);  
  6. fs[data] >> trainingData;  
  7. fs["classes"] >> classes;  

評估應用獲取每一個下采樣矩陣特徵,獲取100個隨機行用來訓練,和其他用來測試ANN演算法的行一樣並且檢查錯誤。

在訓練系統之前,我們測試每一個隨機樣本,檢查一下響應是不是正確。如果響應不正確,我們增加錯誤計算變數,接著除以樣本的數量用來評估。這表示用隨機資料訓練的錯誤(該值在0到1之間)

  1. float test(Mat samples, Mat classes)  
  2. {  
  3.   float errors=0;  
  4.   for(int i=0; i<samples.rows; i++)  
  5.   {  
  6.     int result= ocr.classify(samples.row(i));  
  7.     if(result!= classes.at<int>(i))  
  8.     errors++;  
  9.   }  
  10.   return errors/samples.rows;  
  11. }  

這個程式返回每一個樣本集的錯誤率。對於一個好的評估,我們需要用不同的隨機訓練行資料來訓練我們的程式。這會產生不同的測試錯誤值。我們可以把這樣錯誤加起來,然後求平均值。為了完成這個任務。我們建立了下面的bash unix指令碼來自動執行:

  1. #!/bin/bash  
  2. echo "#ITS \t 5 \t 10 \t 15 \t 20" > data.txt  
  3. folder=$(pwd)  
  4. for numNeurons in 10 20 30 40 50 60 70 80 90 100 120 150 200 500  
  5. do  
  6.   s5=0;  
  7.   s10=0;  
  8.   s15=0;  
  9.   s20=0;  
  10.   for j  in {1..100}  
  11.   do  
  12.     echo $numNeurons $j  
  13.     a=$($folder/build/evalOCR $numNeurons TrainingDataF5)  
  14.     s5=$(echo "scale=4; $s5+$a" | bc -q 2>/dev/null)  
  15.     a=$($folder/build/evalOCR $numNeurons TrainingDataF10)  
  16.     s10=$(echo "scale=4; $s10+$a" | bc -q 2>/dev/null)  
  17.     a=$($folder/build/evalOCR $numNeurons TrainingDataF15)  
  18.     s15=$(echo "scale=4; $s15+$a" | bc -q 2>/dev/null)  
  19.     a=$($folder/build/evalOCR $numNeurons TrainingDataF20)  
  20.     s20=$(echo "scale=4; $s20+$a" | bc -q 2>/dev/null)  
  21.   done  
  22.   echo "$i \t $s5 \t $s10 \t $s15 \t $s20"  
  23.   echo "$i \t $s5 \t $s10 \t $s15 \t $s20" >> data.txt  
  24. done  

這個指令碼儲存了一個data.text檔案,它包含了每個大學的所有結果和隱層神經元的數目。這個可以使用gnuplot畫出來。我們可以在下面的圖中看到結果:

我們可以看到最低的錯誤在8%以下,它是使用隱層包含20個神經元和從規格是10*10d的影象塊中提取的特徵。

總結

在這一章,我們學習了自動車牌識別專案是怎麼工作的,並且它有兩個重要的步驟:

車牌定位和車牌識別。

在第一步中,我們學習了怎麼樣分割影象來找到存在車牌的部分。和怎麼樣用一個簡單的啟發法和支援向量機來對有車牌和無車牌進行二值分類。

在第二步我們學習了怎麼樣通過輪廓演算法來分割影象。從每個字元提取特徵向量,並且使用人工神經網路把對特徵進行分類,分到一個字元類中。

我們也學習了怎麼樣用訓練隨機樣本來評估一個機器演算法。並且使用不同的引數和特徵對它進行評估。