1. 關於OpenCV進階之路
前段時間寫過一些關於OpenCV基礎知識方面的系列文章,主要內容是面向OpenCV初學者,介紹OpenCV中一些常用的函數的接口和調用方法,相關的內容在OpenCV的手冊里都有更詳細的解釋,當時自己也是邊學邊寫,權當為一種筆記的形式,所以難免有淺嘗輒止的感覺,現在回頭看來,很多地方描述上都存在不足,以后有時間,我會重新考慮每一篇文章,讓成長系列對基礎操作的介紹更加詳細一些。
OpenCV進階之路相比於成長系列,不會有太多的基礎函數的介紹,相對來說會更偏向於工程實踐,通過解決實際問題來說明某些較高級函數的用法和注意事項,主要內容會集中在特征提取、機器學習和目標跟蹤幾個方向。所以這個系列文章知識點沒有先后順序之分,根據個人平時工作學習中遇到的問題而定。
這篇文章主要介紹OpenCV中神經網絡的用法,並通過車牌字符的識別來說明一些參數設置,函數調用順序等,而關於神經網絡的原理在博客機器學習分類里已經詳細的講解與實現了,所以本文中就不多加說明。
2. 車牌字符識別
車牌識別是計算機視覺在實際工程中一個非常成功的應用,雖然現在技術相對來說已經成熟,但是圍繞着車牌定位、車牌二值化、車牌字符識別等方向,還是不時的有新的算法出現。通過學習車牌識別來提升自己在圖像識別方面的工程經驗是非常好的,因為它非常好的說明了計算機視覺的一般過程:
圖像$\to$預處理$\to$圖像分析$\to$目標提取$\to$目標識別
而整個車牌識別過程實際上相當於包含了兩個上述過程:1,是車牌的識別;2,車牌字符的識別。
這篇文章其實主要是想介紹OpenCV中神經網絡的用法,而不是介紹車牌識別技術。所以我們主要討論的內容集中在車牌字符的識別上,關於定位、分割等不多加敘述敘述。
3. 字符特征提取
在深度學習(將特征提取作為訓練的一部分)這個概念引入之前,一般在准備分類器進行識別之前都需要進行特征提取。因為一幅圖像包含的內容太多,有些信息能區分差異性,而有些信息卻代表了共性。所以我們要進行適當的特征提取把它們之間的差異性特征提取出來。
這里面我們計算二種簡單的字符特征:梯度分布特征、灰度統計特征。這兩個特征只是配合本篇文章來說明神經網絡的普遍用法,實際中進行字符識別需要考慮的字符特征遠遠要比這復雜,還包括相似字特征的選取等,也由於工作上的原因,這一部分並不深入的介紹。
1,首先是梯度分布特征,該特征計算圖像水平方向和豎直方向的梯度圖像,然后通過給梯度圖像分划不同的區域,進行梯度圖像每個區域亮度值的統計,以下是算法步驟:
<1>將字符由RGB轉化為灰度,然后將圖像歸一化到16*8。
<2>定義soble水平檢測算子:$x\_mask = [-1,0,1;-2,0,2; –1,0,1]$和豎直方向梯度檢測算子$y\_mask=x\_mask^T$。
<3>對圖像分別用$mask\_x$和$mask\_y$進行圖像濾波得到$SobelX$和$SobelY$,下圖分別代表原圖像、$SobelX$和$SobelY$。
<4>對濾波后的圖像,計算圖像總的像素和,然后划分4*2的網絡,計算每個網格內的像素值的總和。
<5>將每個網絡內總灰度值占整個圖像的百分比統計在一起寫入一個向量,將兩個方向各自得到的向量並在一起,組成特征向量。

1 void calcGradientFeat(const Mat& imgSrc, vector<float>& feat) 2 { 3 float sumMatValue(const Mat& image); // 計算圖像中像素灰度值總和 4 5 Mat image; 6 cvtColor(imgSrc,image,CV_BGR2GRAY); 7 resize(image,image,Size(8,16)); 8 9 // 計算x方向和y方向上的濾波 10 float mask[3][3] = { { 1, 2, 1 }, { 0, 0, 0 }, { -1, -2, -1 } }; 11 12 Mat y_mask = Mat(3, 3, CV_32F, mask) / 8; 13 Mat x_mask = y_mask.t(); // 轉置 14 Mat sobelX, sobelY; 15 16 filter2D(image, sobelX, CV_32F, x_mask); 17 filter2D(image, sobelY, CV_32F, y_mask); 18 19 sobelX = abs(sobelX); 20 sobelY = abs(sobelY); 21 22 float totleValueX = sumMatValue(sobelX); 23 float totleValueY = sumMatValue(sobelY); 24 25 // 將圖像划分為4*2共8個格子,計算每個格子里灰度值總和的百分比 26 for (int i = 0; i < image.rows; i = i + 4) 27 { 28 for (int j = 0; j < image.cols; j = j + 4) 29 { 30 Mat subImageX = sobelX(Rect(j, i, 4, 4)); 31 feat.push_back(sumMatValue(subImageX) / totleValueX); 32 Mat subImageY= sobelY(Rect(j, i, 4, 4)); 33 feat.push_back(sumMatValue(subImageY) / totleValueY); 34 } 35 } 36 } 37 float sumMatValue(const Mat& image) 38 { 39 float sumValue = 0; 40 int r = image.rows; 41 int c = image.cols; 42 if (image.isContinuous()) 43 { 44 c = r*c; 45 r = 1; 46 } 47 for (int i = 0; i < r; i++) 48 { 49 const uchar* linePtr = image.ptr<uchar>(i); 50 for (int j = 0; j < c; j++) 51 { 52 sumValue += linePtr[j]; 53 } 54 } 55 return sumValue; 56 }
2,第二個特征非常簡單,只需要將圖像歸一化到特定的大小,然后將圖像每個點的灰度值作為特征即可。
<1>將圖像由RGB圖像轉換為灰度圖像;
<2>將圖像歸一化大小為$8×4$,並將圖像展開為一行,組成特征向量。
4. OpenCV中的神經網絡
關於神經網絡的原理我的博客里已經寫了兩篇文章,並且給出了C++的實現,所以這里我就不提了,下面主要說明在OpenCV中怎么使用它提供的庫函數。
CvANN_MLP是OpenCV中提供的一個神經網絡的類,正如它的名字一樣(multi-layer perceptrons),它是一個多層感知網絡,它有一個輸入層,一個輸出層以及1或多個隱藏層。
4.1. 首先我們來創建一個網絡,我們可以利用CvANN_MLP的構造函數或者create函數。
1 CvANN_MLP::CvANN_MLP(const Mat& layerSizes, int activateFunc=CvANN_MLP::SIGMOID_SYM, double fparam1=0, double fparam2=0 ); 2 void CvANN_MLP::create(const Mat& layerSizes, int activateFunc=CvANN_MLP::SIGMOID_SYM, double fparam1=0, double fparam2=0 );
上面是分別是構造函數和cteate成員函數的接口,我們來分析各個形參的意思。
layerSizes:一個整型的數組,這里面用Mat存儲。它是一個1*N的Mat,N代表神經網絡的層數,第$i$列的值表示第$i$層的結點數。這里需要注意的是,在創建這個Mat時,一定要是整型的,uchar和float型都會報錯。
比如我們要創建一個3層的神經網絡,其中第一層結點數為$x_1$,第二層結點數為$x_2$,第三層結點數為$x_3$,則layerSizes可以采用如下定義:
1 Mat layerSizes=(Mat_<int>(1,3)<<x1,x2,x3);
或者用一個數組來初始化:
1 int ar[]={x1,x2,x3}; 2 Mat layerSizes(1,3,CV_32S,ar);
activateFunc:這個參數用於指定激活函數,不熟悉的可以去看我博客里的這篇文章《神經網絡:感知器與梯度下降》,一般情況下我們用SIGMOID函數就可以了,當然你也可以選擇正切函數或高斯函數作為激活函數。OpenCV里提供了三種激活函數,線性函數(CvANN_MLP::IDENTITY)、sigmoid函數(CvANN_MLP::SIGMOID_SYM)和高斯激活函數(CvANN_MLP::GAUSSIAN)。
后面兩個參數則是SIGMOID激活函數中的兩個參數$\alpha$和$\beta$,默認情況下會都被設置為1。
$$f(x)=\beta \frac{1-e^{-\alpha x}}{1+e^{-\alpha x}}$$
4.2. 設置神經網絡訓練參數
神經網絡訓練參數的類型存放在CvANN_MLP_TrainParams這個類里,它提供了一個默認的構造函數,我們可以直接調用,也可以一項一項去設。
1 CvANN_MLP_TrainParams::CvANN_MLP_TrainParams() 2 { 3 term_crit = cvTermCriteria( CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 1000, 0.01 ); 4 train_method = RPROP; 5 bp_dw_scale = bp_moment_scale = 0.1; 6 rp_dw0 = 0.1; rp_dw_plus = 1.2; rp_dw_minus = 0.5; 7 rp_dw_min = FLT_EPSILON; rp_dw_max = 50.; 8 }
它的參數大概包括以下幾項。
term_crit:終止條件,它包括了兩項,迭代次數(CV_TERMCRIT_ITER)和誤差最小值(CV_TERMCRIT_EPS),一旦有一個達到條件就終止訓練。
train_method:訓練方法,OpenCV里提供了兩個方法一個是很經典的反向傳播算法BACKPROP,另一個是彈性反饋算法RPROP,對第二種訓練方法,沒有仔細去研究過,這里我們運用第一種方法。
剩下就是關於每種訓練方法的相關參數,針對於反向傳播法,主要是兩個參數,一個是權值更新率bp_dw_scale和權值更新沖量bp_moment_scale。這兩個量一般情況設置為0.1就行了;太小了網絡收斂速度會很慢,太大了可能會讓網絡越過最小值點。
我們一般先運用它的默認構造函數,然后根據需要再修改相應的參數就可以了。如下面代碼所示,我們將迭代次數改為了5000次。
1 CvANN_MLP_TRainParams param; 2 param.term_crit=cvTermCriteria(CV_TerMCrIT_ITER+CV_TERMCRIT_EPS,5000,0.01);
4.3. 神經網絡的訓練
我們先看訓練函數的接口,然后按接口去准備數據。
1 int CvANN_MLP::train(const Mat& inputs, const Mat& outputs, const Mat& sampleWeights, const Mat& sampleIdx=Mat(), CvANN_MLP_TrainParams params=CvANN_MLP_TrainParams(), int flags=0 );
inputs:輸入矩陣。它存儲了所有訓練樣本的特征。假設所有樣本總數為nSamples,而我們提取的特征維數為ndims,則inputs是一個$nSamples*ndims$的矩陣,我們可以這樣創建它。
1 Mat inputs(nSamples,ndims,CV_32FC1); //CV_32FC1說明它儲存的數據是float型的。
我們需要將我們的訓練集,經過特征提取把得到的特征向量存儲在inputs中,每個樣本的特征占一行。
outputs:輸出矩陣。我們實際在訓練中,我們知道每個樣本所屬的種類,假設一共有nClass類。那么我們將outputs設置為一個nSample行nClass列的矩陣,每一行表示一個樣本的預期輸出結果,該樣本所屬的那類對應的列設置為1,其他都為0。比如我們需要識別0-9這10個數字,則總的類數為10類,那么樣本數字“3”的預期輸出為[0,0,1,0,0,0,0,0,0,0];
sampleWeights:一個在使用RPROP方法訓練時才需要的數據,所以這里我們不設置,直接設置為Mat()即可。
sampleIdx:相當於一個遮罩,它指定哪些行的數據參與訓練。如果設置為Mat(),則所有行都參與。
params:這個在剛才已經說過了,是訓練相關的參數。
flag:它提供了3個可選項參數,用來指定數據處理的方式,我們可以用邏輯符號去組合它們。UPDATE_WEIGHTS指定用一定的算法去初始化權值矩陣而不是用隨機的方法。NO_INPUT_SCALE和NO_OUTPUT_SCALE分別用於禁止輸入與輸出矩陣的歸一化。
一切都准備好后,直接開始訓練吧!
4.4. 識別
識別是通過Cv_ANN_MLP類提供的predict來實現的,知道原理的會明白,它實際上就是做了一次向前傳播。
1 float CvANN_MLP::predict(const Mat& inputs, Mat& outputs) const
在進行識別的時候,我們對圖像進行特征提取,把它保存在inputs里,通過調用predict函數,我們得到一個輸出向量,它是一個1*nClass的行向量,其中每一列說明它與該類的相似程度(0-1之間),也可以說是置信度。我們只用對output求一個最大值,就可得到結果。這個函數的返回值是一個無用的float值,可以忽略。
5. 車牌字符識別測試
1,我們需要讀取所有的訓練樣本,將它們的路徑在保存在vector<string>中。
這里面我的車牌字符,因為1和I、0和O是一樣的,所以數字加字母一共34類,其中每類有200個樣本圖像,共34*200個訓練樣本。
2,計算特征。我們按順序讀入圖像,調用特征計算函數,把得到的結合保存在input對應的行中,同時把圖像對應的預期輸出保存在output中。
3,創建神經網絡,這里我們計算得到的特征維數為48維,所以我們簡單的設計一個3層的神經網絡,輸入層有48個結點,隱藏層也為48個結點,輸出層為34個結點。然后神經網絡的訓練方法選用BACKPROP,迭代次數設置為5000次。
4,調用訓練函數進行訓練,並保存訓練得到的權值矩陣,直接調用save成員函數即可。
nnetwork.save(“mlp.xml”);
5,識別測試,我們可以用單張圖像進行測試,也可以選定一個測試集去進行測試,比如可以用一半的圖像作為訓練集,一半的圖像作為測試集。這里我們可以加載已經訓練好的權值矩陣,而不用重新訓練,只要開始有保存了xml文件。但是記得你還是要創建一個網絡后,才能加載進來。
1 int NNClassifier::classifier(const Mat& image) 2 { 3 Mat nearest(1, nclass, CV_32FC1, Scalar(0)); 4 Mat charFeature; 5 calcFeature(image, charFeature); 6 7 neuralNetwork.predict(charFeature, nearest); 8 Point maxLoc; 9 minMaxLoc(nearest, NULL, NULL, NULL, &maxLoc); 10 int result = maxLoc.x; 11 return result; 12 }
這里我簡單的做了一下測試,在這兩個特征下,網絡設置為3層[48,48,34],一半圖像為測試集,得到的識別率為98%,我相信通過嘗試調整網絡的層數以及選用更好的特征,一定會得到更滿意的識別率。PS(工作中用的是SVM識別器,正常采集到的車牌,字符識別率在99.8%以上)。但是神經網絡識別器有個很大的優點就是,一旦網絡訓練好,識別需要的數據文件非常小,而且速度很快。
6. 字符樣本的下載
看到文章下的評論多是需求字符樣本的,希望拿到字符樣本的同學不要將其用於商業用途或者創建分享下載的鏈接。博文里用的樣本是每類200張圖像的測試樣本,下面給出一份每類50個圖像的樣本子集,用來做測試已經夠了。
鏈接:http://pan.baidu.com/s/1pLPeZkZ 密碼:26eb