1. 程式人生 > >opencv 車牌字符分割 ANN網絡識別字符

opencv 車牌字符分割 ANN網絡識別字符

minus pri net mage 場景 ofstream tail 參考 params

  最近在復習OPENCV的知識,學習caffe的深度神經網絡,正好想起以前做過的車牌識別項目,可以拿出來研究下

  

  以前的環境是VS2013和OpenCV2.4.9,感覺OpenCV2.4.9是個經典版本啊!不過要使用caffe模型的話,還是要最新的OpenCV3.3更合適!

  一、車牌圖片庫

  以前也是網上下的,如果找不到的小夥伴可以從我這兒下: 鏈接:http://pan.baidu.com/s/1hrQF92G 密碼:43jl

  裏面有數字 “0-9”,字母“A-Z”的訓練圖片各50張。

  

  測試車牌圖片當時是從他人得到已經定位到車牌的圖片,類似如下:

技術分享

  目標當然就是對這些車牌圖片進行預處理,單字符分割,單字符識別!

  二、預處理

  圖像的預處理做來做去就是濾波去噪,光照補償,灰度/二值化,形態學基本操作等等。這些圖片都是自然場景得到所以基本的去噪操作可以做一下,然後為了單字符分割,灰度化和形態學可以結合效果調整。

  光照補償其實一直是個問題,大多數有直方圖均衡化,亮度參考白,利用公式統計補償圖片。這方面也可以結合圖像增強方法來做!筆者當時覺得前兩者對大多數場景已經適用。

  二值化可以使用 cv::threshold函數,如:

1 Mat t1=imread("2.png",1);
2 cvtColor(inimg, gimg, CV_BGR2GRAY);
3 threshold(gimg, gimg, 100, 255, CV_THRESH_BINARY); 4 imshow("gimg", gimg);

  第一行imread(),由於flag設為1所以讀的是彩圖,采用cvtColor函數轉化為灰度圖。如果你讀入就是灰度圖可以省略第二行代碼。第三行就是轉化為二值化函數,閾值100可以修改,在灰度對比不明顯是有必要!

  效果:技術分享

  如果預處理做的好,某些小的白色區域是可以去掉的。這個效果也可以識別。

  同時可以發現車牌外圍被一圈白色包圍,如若能去除外圍白色,對於單字符分割更有益。但其實通過尋找列像素之間的變化,白色區域只是影響了閾值不會對結果太大影響。

  想要去除白色外圈可以參考:http://blog.csdn.net/u011630458/article/details/43733057

  如果想要使用直方圖均衡化,OPENCV有equalizeHist(inputmat, outputmat);非常方便,但是效果不好。

使用直方圖均衡化後的上述車牌二值化圖片:

  技術分享

效果更慘烈了,因為均衡化就是讓直方圖的像素分布更加平衡,上圖黑色多,均衡之後自然白色多了,反而不好!

  二、單字符分割

  單字符分割主要策略就是檢測列像素的總和變化,因為沒有字符的區域基本是黑色,像素值低;有字符的區域白色較多,列像素和就變大了!

  列像素變化的閾值是個問題,看到很多博客是固定的閾值進行檢測,除非你處理後的二值化圖像非常完美,不然有的圖片混入了白色區域就會分割錯誤!而且對於得到分割寬度如果太小也應該使用策略進行剔除,沒有一定的寬度限制分割後的圖片可能是很多個窄窄的小區域。。。  

技術分享
 1 int getColSum(Mat& bimg, int col)
 2 {
 3     int height = bimg.rows;
 4     int sum = 0;
 5     for (int i = 1; i < height; i++)
 6     {
 7         sum += bimg.at<uchar>(i, col);
 8     }
 9     cout << sum << endl;
10     return sum;
11 }
12 
13 int cutLeft(Mat& src, int Tsum, int right)//左右切割  
14 {
15     int left;
16     left = 0;
17 
18     int i;
19     for (i=0; i < src.cols; i++)
20     {
21         int colValue = getColSum(src, i);  
22         if (colValue> Tsum)
23         {
24             left = i;
25             break;
26         }
27     }
28     int roiWidth=src.cols/7;
29     for (; i < src.cols; i++)
30     {
31         int colValue = getColSum(src, i);
32         if (colValue < Tsum)
33         {
34             right = i;
35             if ((right - left) < (src.cols/7))
36                 continue;
37             else
38             {
39                 roiWidth = right - left;
40                 break;
41             }
42             
43         }
44     }
45     return roiWidth;
46 }
47 
48 int getOne(Mat& inimg)
49 {
50     Mat gimg,histimg;
51     cvtColor(inimg, gimg, CV_BGR2GRAY);
52     equalizeHist(gimg,histimg);
53     //imshow("histimg", histimg);
54     threshold(gimg, gimg, 100, 255, CV_THRESH_BINARY);
55     imshow("gimg", gimg);
56     waitKey(0);
57 
58     int psum=0;
59     for (int i = 0; i < gimg.cols; i++)
60     {
61         psum+=getColSum(gimg, i);
62     }
63     cout <<"psum/col:"<< psum/gimg.cols << endl;
64     int Tsum = 0.6*(psum / gimg.cols);
65     int roiWid= cutLeft(gimg, Tsum, 0);
66 
67     return roiWid;
68 }
View Code

筆者思路也很簡單:

  首先統計所有列像素的總和,取其列像素的均值作為參考標準之一(也可以選用其他數學指標參考),列像素的閾值Tsum設置為列像素均值的百分比(如60%,是情景定)。

  利用cutLeft()函數對圖片進行列掃描,將列像素超過閾值的列標記為左邊,再繼續尋找右邊,將滿足閾值的右邊進行標記。左右相減即可得到寬度分割字符。

  考慮到車牌中只有7個字符,所以先判斷得到寬度大小,如果小於總寬的七分之一視為幹擾放棄;其實也可以加大到總寬的8分之一(因為車牌中間可能有連接符)。

  getColSum()函數是求一列的像素和,這裏用到了.at<> 方式,其實還有別的方法也可以,只要獲得當前的像素值,並累加整列即可!

  上圖車牌的分割效果:

技術分享   技術分享  技術分享  技術分享  技術分享  技術分享  技術分享

  因為第三張有車牌的連接符,所以導致第三張和第四張稍有瑕疵,但總體分割還是滿意的!

  三、單字符識別

  只論字符識別其實有不少選擇方案,一開始筆者嘗試了ORB特征,想利用特征匹配計算相似度來判斷最優的字符結果。ORB特征相比SURF/SIFT更加快速,而且特征不變性也不錯。但是在匹配時發現單字符的圖片像素點太少,提取的特征點數極少,無法得到較好的匹配結果,只能放棄!

  其實也有模板匹配來做字符識別的,但是OPENCV提供的模板匹配對於從同一副圖片提取的模板圖去匹配樣本圖效果很好,不是同一副圖片時效果很一般。因為筆者用OPENCV的模板匹配一般用來找重復區域。

  OCR識別是可以完全用在此處的,OCR識別甚至可以識別漢字,安裝OCR的庫之後就可以嘗試一番!

  筆者最後選擇了神經網絡ANN來做字符分類識別,利用SVM也可以都是分類器之一。使用神經網絡可以和caffe的mnist模型有所對比的感覺!  

技術分享
  1 void ann10(Mat& testroi)
  2 {
  3     const string fileform = "*.png";
  4     const string perfileReadPath = "E:\\vswork\\charSamples";
  5 
  6     const int sample_mun_perclass = 50;//訓練字符每類數量  
  7     const int class_mun = 34;//訓練字符類數 0-9 A-Z 除了I、O  
  8 
  9     const int image_cols = 8;
 10     const int image_rows = 16;
 11     string  fileReadName,fileReadPath;
 12     char temp[256];
 13 
 14     float trainingData[class_mun*sample_mun_perclass][image_rows*image_cols] = { { 0 } };//每一行一個訓練樣本  
 15     float labels[class_mun*sample_mun_perclass][class_mun] = { { 0 } };//訓練樣本標簽  
 16 
 17     for (int i = 0; i <= class_mun - 1; i++)//不同類  
 18     {
 19         //讀取每個類文件夾下所有圖像  
 20         int j = 0;//每一類讀取圖像個數計數  
 21 
 22         if (i <= 9)//0-9  
 23         {
 24             sprintf(temp, "%d", i);
 25             //printf("%d\n", i);  
 26         }
 27         else//A-Z  
 28         {
 29             sprintf(temp, "%c", i + 55);
 30             //printf("%c\n", i+55);  
 31         }
 32 
 33         fileReadPath = perfileReadPath + "/" + temp + "/" + fileform;
 34         cout << "文件夾" << fileReadPath << endl;
 35 
 36         HANDLE hFile;
 37         LPCTSTR lpFileName = StringToWchar(fileReadPath);//指定搜索目錄和文件類型,如搜索d盤的音頻文件可以是"D:\\*.mp3"  
 38         WIN32_FIND_DATA pNextInfo;  //搜索得到的文件信息將儲存在pNextInfo中;  
 39         hFile = FindFirstFile(lpFileName, &pNextInfo);//請註意是 &pNextInfo , 不是 pNextInfo;  
 40         if (hFile == INVALID_HANDLE_VALUE)
 41         {
 42             continue;//搜索失敗  
 43         }
 44         //do-while循環讀取  
 45         do
 46         {
 47             if (pNextInfo.cFileName[0] == .)//過濾.和..  
 48                 continue;
 49             j++;//讀取一張圖  
 50             //wcout<<pNextInfo.cFileName<<endl;  
 51             //printf("%s\n",WcharToChar(pNextInfo.cFileName));  
 52             //對讀入的圖片進行處理  
 53             Mat srcImage = imread(perfileReadPath + "/" + temp + "/" + WcharToChar(pNextInfo.cFileName), CV_LOAD_IMAGE_GRAYSCALE);
 54             Mat resizeImage;
 55             Mat trainImage;
 56             Mat result;
 57 
 58             resize(srcImage, resizeImage, Size(image_cols, image_rows), (0, 0), (0, 0), CV_INTER_AREA);//使用象素關系重采樣。當圖像縮小時候,該方法可以避免波紋出現  
 59             threshold(resizeImage, trainImage, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
 60 
 61             for (int k = 0; k<image_rows*image_cols; ++k)
 62             {
 63                 trainingData[i*sample_mun_perclass + (j - 1)][k] = (float)trainImage.data[k];
 64                 //trainingData[i*sample_mun_perclass+(j-1)][k] = (float)trainImage.at<unsigned char>((int)k/8,(int)k%8);//(float)train_image.data[k];  
 65                 //cout<<trainingData[i*sample_mun_perclass+(j-1)][k] <<" "<< (float)trainImage.at<unsigned char>(k/8,k%8)<<endl;  
 66             }
 67 
 68         } while (FindNextFile(hFile, &pNextInfo) && j<sample_mun_perclass);//如果設置讀入的圖片數量,則以設置的為準,如果圖片不夠,則讀取文件夾下所有圖片  
 69 
 70     }
 71 
 72     // Set up training data Mat  
 73     Mat trainingDataMat(class_mun*sample_mun_perclass, image_rows*image_cols, CV_32FC1, trainingData);
 74     cout << "trainingDataMat——OK!" << endl;
 75 
 76     // Set up label data   
 77     for (int i = 0; i <= class_mun - 1; ++i)
 78     {
 79         for (int j = 0; j <= sample_mun_perclass - 1; ++j)
 80         {
 81             for (int k = 0; k < class_mun; ++k)
 82             {
 83                 if (k == i)
 84                     if (k == 18)
 85                     {
 86                         labels[i*sample_mun_perclass + j][1] = 1;
 87                     }
 88                     else if (k == 24)
 89                     {
 90                         labels[i*sample_mun_perclass + j][0] = 1;
 91                     }
 92                     else
 93                     {
 94                         labels[i*sample_mun_perclass + j][k] = 1;
 95                     }
 96                 else
 97                     labels[i*sample_mun_perclass + j][k] = 0;
 98             }
 99         }
100     }
101     Mat labelsMat(class_mun*sample_mun_perclass, class_mun, CV_32FC1, labels);
102     cout << "labelsMat:" << endl;
103     ofstream outfile("out.txt");
104     outfile << labelsMat;
105     //cout<<labelsMat<<endl;  
106     cout << "labelsMat——OK!" << endl;
107 
108     //訓練代碼  
109 
110     cout << "training start...." << endl;
111     CvANN_MLP bp;
112     // Set up BPNetwork‘s parameters  
113     CvANN_MLP_TrainParams params;
114     params.train_method = CvANN_MLP_TrainParams::BACKPROP;
115     params.bp_dw_scale = 0.001;
116     params.bp_moment_scale = 0.1;
117     params.term_crit = cvTermCriteria(CV_TERMCRIT_ITER | CV_TERMCRIT_EPS, 10000, 0.0001);  //設置結束條件  
118     //params.train_method=CvANN_MLP_TrainParams::RPROP;  
119     //params.rp_dw0 = 0.1;  
120     //params.rp_dw_plus = 1.2;  
121     //params.rp_dw_minus = 0.5;  
122     //params.rp_dw_min = FLT_EPSILON;  
123     //params.rp_dw_max = 50.;  
124 
125     //Setup the BPNetwork  
126     Mat layerSizes = (Mat_<int>(1, 5) << image_rows*image_cols, 128, 128, 128, class_mun);
127     bp.create(layerSizes, CvANN_MLP::SIGMOID_SYM, 1.0, 1.0);//CvANN_MLP::SIGMOID_SYM  
128     //CvANN_MLP::GAUSSIAN  
129     //CvANN_MLP::IDENTITY  
130     cout << "training...." << endl;
131     bp.train(trainingDataMat, labelsMat, Mat(), Mat(), params);
132 
133     bp.save("../bpcharModel.xml"); //save classifier  
134     cout << "training finish...bpModel.xml saved " << endl;
135     return;
136 }
ann10

  ann10函數主要完成讀取圖片訓練ANN網絡的功能。

  註意點:

  修改圖片文件類型 fileform;

  修改訓練圖片路徑 perfileReadPath等;

  修改訓練圖片數量 sample_mun_perclass;

  修改訓練類別數 class_mun;(34類是因為IO與10很像,所以少了兩類);

  image_cols和image_rows根據自己圖片情況修改;

觀察代碼發現訓練文件在工程目錄的 bpcharModel.xml;之後調用該網絡模型即可,網上有很多網絡調用和網絡訓練沒有分開,這樣你每預測分類一個字符都要重新訓練網絡會相當浪費時間的,筆者的渣電腦訓練一次就要幾分鐘,每次分類都訓練時間有點傷不起。。。真正的實際應用也是用訓練好的網絡參數直接調用,速度很快。就像caffe中的深度神經網絡,使用網絡分類時也只是調用生成好的caffemodel和標簽、solver文件就行了,如果還要重新訓練一小時根本沒有實用性。  

技術分享
 1 void predictann(Mat testroi)
 2 {
 3     //測試神經網絡 
 4     CvANN_MLP bp;
 5     bp.load("E:\\vswork\\CarNumRecog\\bpcharModel.xml");
 6     const int image_cols = 8;
 7     const int image_rows = 16;
 8 
 9     cout << "測試:" << endl;
10     //Mat test_image = imread("E:\\vswork\\charSamples\\3.png", CV_LOAD_IMAGE_GRAYSCALE);
11     Mat test_temp;
12     resize(testroi, test_temp, Size(image_cols, image_rows), (0, 0), (0, 0), CV_INTER_AREA);//使用象素關系重采樣。當圖像縮小時候,該方法可以避免波紋出現  
13     threshold(test_temp, test_temp, 0, 255, CV_THRESH_BINARY | CV_THRESH_OTSU);
14     Mat_<float>sampleMat(1, image_rows*image_cols);
15     for (int i = 0; i<image_rows*image_cols; ++i)
16     {
17         sampleMat.at<float>(0, i) = (float)test_temp.at<uchar>(i / 8, i % 8);
18     }
19 
20     Mat responseMat;
21     bp.predict(sampleMat, responseMat);
22     Point maxLoc;
23     double maxVal = 0;
24     minMaxLoc(responseMat, NULL, &maxVal, NULL, &maxLoc);
25     char temp[256];
26 
27     if (maxLoc.x <= 9)//0-9  
28     {
29         sprintf(temp, "%d", maxLoc.x);
30         //printf("%d\n", i);  
31     }
32     else//A-Z  
33     {
34         sprintf(temp, "%c", maxLoc.x + 55);
35         //printf("%c\n", i+55);  
36     }
37 
38     cout << "識別結果:" << temp << "    相似度:" << maxVal * 100 << "%" << endl;
39     imshow("test_image", testroi);
40     waitKey(0);
41 
42     return;
43 }
predictann

  predictann函數就是調用ann10函數生成的網絡模型文件,進行預測分類的功能。

  上述車牌的單字符識別效果如下:

  技術分享  技術分享  技術分享

  可以看到有的相似度很高,有的卻很低,也有一些識別錯誤的,我不再顯示。。。

  相比之前使用的caffe mnist識別率真的是差距有點大,以後有機會將mnist的模型來識別車牌字符試試~~

opencv 車牌字符分割 ANN網絡識別字符