1. 關於OpenCV進階之路
前段時間寫過一些關於OpenCV基礎知識方面的系列文章,主要內容是面向OpenCV初學者,介紹OpenCV中一些常用的函式的介面和呼叫方法,相關的內容在OpenCV的手冊裡都有更詳細的解釋,當時自己也是邊學邊寫,權當為一種筆記的形式,所以難免有淺嘗輒止的感覺,現在回頭看來,很多地方描述上都存在不足,以後有時間,我會重新考慮每一篇文章,讓成長系列對基礎操作的介紹更加詳細一些。
OpenCV進階之路相比於成長系列,不會有太多的基礎函式的介紹,相對來說會更偏向於工程實踐,通過解決實際問題來說明某些較高階函式的用法和注意事項,主要內容會集中在特徵提取、機器學習和目標跟蹤幾個方向。所以這個系列文章知識點沒有先後順序之分,根據個人平時工作學習中遇到的問題而定。
這篇文章主要介紹OpenCV中神經網路的用法,並通過車牌字元的識別來說明一些引數設定,函式呼叫順序等,而關於神經網路的原理在部落格機器學習分類裡已經詳細的講解與實現了,所以本文中就不多加說明。
2. 車牌字元識別
車牌識別是計算機視覺在實際工程中一個非常成功的應用,雖然現在技術相對來說已經成熟,但是圍繞著車牌定位、車牌二值化、車牌字元識別等方向,還是不時的有新的演算法出現。通過學習車牌識別來提升自己在影像識別方面的工程經驗是非常好的,因為它非常好的說明了計算機視覺的一般過程:
影像→預處理→影像分析→目標提取→目標識別
而整個車牌識別過程實際上相當於包含了兩個上述過程:1,是車牌的識別;2,車牌字元的識別。
這篇文章其實主要是想介紹OpenCV中神經網路的用法,而不是介紹車牌識別技術。所以我們主要討論的內容集中在車牌字元的識別上,關於定位、分割等不多加敘述敘述。
3. 字元特徵提取
在深度學習(將特徵提取作為訓練的一部分)這個概念引入之前,一般在準備分類器進行識別之前都需要進行特徵提取。因為一幅影像包含的內容太多,有些資訊能區分差異性,而有些資訊卻代表了共性。所以我們要進行適當的特徵提取把它們之間的差異性特徵提取出來。
這裡面我們計算二種簡單的字元特徵:梯度分佈特徵、灰度統計特徵。這兩個特徵只是配合本篇文章來說明神經網路的普遍用法,實際中進行字元識別需要考慮的字元特徵遠遠要比這複雜,還包括相似字特徵的選取等,也由於工作上的原因,這一部分並不深入的介紹。
1,首先是梯度分佈特徵,該特徵計算影像水平方向和豎直方向的梯度影像,然後通過給梯度影像分劃不同的區域,進行梯度影像每個區域亮度值的統計,以下是演算法步驟:
1 2 3 4 5 6 7 8 9 |
<1>將字元由RGB轉化為灰度,然後將影像歸一化到16*8。 <2>定義soble水平檢測運算元:x_mask=[−1,0,1;−2,0,2;–1,0,1]和豎直方向梯度檢測運算元y_mask=x_maskT。 <3>對影像分別用mask_x和mask_y進行影像濾波得到SobelX和SobelY,下圖分別代表原影像、SobelX和SobelY。 <4>對濾波後的影像,計算影像總的畫素和,然後劃分4*2的網路,計算每個網格內的畫素值的總和。 <5>將每個網路內總灰度值佔整個影像的百分比統計在一起寫入一個向量,將兩個方向各自得到的向量並在一起,組成特徵向量。 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
void calcGradientFeat(const Mat& imgSrc, vector<float>& feat) { float sumMatValue(const Mat& image); // 計算影像中畫素灰度值總和 Mat image; cvtColor(imgSrc,image,CV_BGR2GRAY); resize(image,image,Size(8,16)); // 計算x方向和y方向上的濾波 float mask[3][3] = { { 1, 2, 1 }, { 0, 0, 0 }, { -1, -2, -1 } }; Mat y_mask = Mat(3, 3, CV_32F, mask) / 8; Mat x_mask = y_mask.t(); // 轉置 Mat sobelX, sobelY; filter2D(image, sobelX, CV_32F, x_mask); filter2D(image, sobelY, CV_32F, y_mask); sobelX = abs(sobelX); sobelY = abs(sobelY); float totleValueX = sumMatValue(sobelX); float totleValueY = sumMatValue(sobelY); // 將影像劃分為4*2共8個格子,計算每個格子裡灰度值總和的百分比 for (int i = 0; i < image.rows; i = i + 4) { for (int j = 0; j < image.cols; j = j + 4) { Mat subImageX = sobelX(Rect(j, i, 4, 4)); feat.push_back(sumMatValue(subImageX) / totleValueX); Mat subImageY= sobelY(Rect(j, i, 4, 4)); feat.push_back(sumMatValue(subImageY) / totleValueY); } } } float sumMatValue(const Mat& image) { float sumValue = 0; int r = image.rows; int c = image.cols; if (image.isContinuous()) { c = r*c; r = 1; } for (int i = 0; i < r; i++) { const uchar* linePtr = image.ptr<uchar>(i); for (int j = 0; j < c; j++) { sumValue += linePtr[j]; } } return sumValue; } |
2,第二個特徵非常簡單,只需要將影像歸一化到特定的大小,然後將影像每個點的灰度值作為特徵即可。
<1>將影像由RGB影像轉換為灰度影像;
<2>將影像歸一化大小為8×4,並將影像展開為一行,組成特徵向量。
4. OpenCV中的神經網路
關於神經網路的原理我的部落格裡已經寫了兩篇文章,並且給出了C++的實現,所以這裡我就不提了,下面主要說明在OpenCV中怎麼使用它提供的庫函式。
CvANN_MLP是OpenCV中提供的一個神經網路的類,正如它的名字一樣(multi-layer perceptrons),它是一個多層感知網路,它有一個輸入層,一個輸出層以及1或多個隱藏層。
4.1. 首先我們來建立一個網路,我們可以利用CvANN_MLP的建構函式或者create函式。
1 2 |
CvANN_MLP::CvANN_MLP(const Mat& layerSizes, int activateFunc=CvANN_MLP::SIGMOID_SYM, double fparam1=0, double fparam2=0 ); 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層的神經網路,其中第一層結點數為x1,第二層結點數為x2,第三層結點數為x3,則layerSizes可以採用如下定義:
1 |
Mat layerSizes=(Mat_<int>(1,3)<<x1,x2,x3); |
或者用一個陣列來初始化:
1 2 |
int ar[]={x1,x2,x3}; Mat layerSizes(1,3,CV_32S,ar); |
activateFunc:這個引數用於指定啟用函式,不熟悉的可以去看我部落格裡的這篇文章《神經網路:感知器與梯度下降》,一般情況下我們用SIGMOID函式就可以了,當然你也可以選擇正切函式或高斯函式作為啟用函式。OpenCV裡提供了三種啟用函式,線性函式(CvANN_MLP::IDENTITY)、sigmoid函式(CvANN_MLP::SIGMOID_SYM)和高斯啟用函式(CvANN_MLP::GAUSSIAN)。
後面兩個引數則是SIGMOID啟用函式中的兩個引數α和β,預設情況下會都被設定為1。
4.2. 設定神經網路訓練引數
神經網路訓練引數的型別存放在CvANN_MLP_TrainParams這個類裡,它提供了一個預設的建構函式,我們可以直接呼叫,也可以一項一項去設。
1 2 3 4 5 6 7 8 |
CvANN_MLP_TrainParams::CvANN_MLP_TrainParams() { term_crit = cvTermCriteria( CV_TERMCRIT_ITER + CV_TERMCRIT_EPS, 1000, 0.01 ); train_method = RPROP; bp_dw_scale = bp_moment_scale = 0.1; rp_dw0 = 0.1; rp_dw_plus = 1.2; rp_dw_minus = 0.5; rp_dw_min = FLT_EPSILON; rp_dw_max = 50.; } |
它的引數大概包括以下幾項。
term_crit:終止條件,它包括了兩項,迭代次數(CV_TERMCRIT_ITER)和誤差最小值(CV_TERMCRIT_EPS),一旦有一個達到條件就終止訓練。
train_method:訓練方法,OpenCV裡提供了兩個方法一個是很經典的反向傳播演算法BACKPROP,另一個是彈性反饋演算法RPROP,對第二種訓練方法,沒有仔細去研究過,這裡我們運用第一種方法。
剩下就是關於每種訓練方法的相關引數,針對於反向傳播法,主要是兩個引數,一個是權值更新率bp_dw_scale和權值更新衝量bp_moment_scale。這兩個量一般情況設定為0.1就行了;太小了網路收斂速度會很慢,太大了可能會讓網路越過最小值點。
我們一般先運用它的預設建構函式,然後根據需要再修改相應的引數就可以了。如下面程式碼所示,我們將迭代次數改為了5000次。
1 2 |
CvANN_MLP_TRainParams param; 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 2 3 4 5 6 7 8 9 10 11 12 |
int NNClassifier::classifier(const Mat& image) { Mat nearest(1, nclass, CV_32FC1, Scalar(0)); Mat charFeature; calcFeature(image, charFeature); neuralNetwork.predict(charFeature, nearest); Point maxLoc; minMaxLoc(nearest, NULL, NULL, NULL, &maxLoc); int result = maxLoc.x; return result; } |
這裡我簡單的做了一下測試,在這兩個特徵下,網路設定為3層[48,48,34],一半影像為測試集,得到的識別率為98%,我相信通過嘗試調整網路的層數以及選用更好的特徵,一定會得到更滿意的識別率。PS(工作中用的是SVM識別器,正常採集到的車牌,字元識別率在99.8%以上)。但是神經網路識別器有個很大的優點就是,一旦網路訓練好,識別需要的資料檔案非常小,而且速度很快。
6. 字元樣本的下載
看到文章下的評論多是需求字元樣本的,希望拿到字元樣本的同學不要將其用於商業用途或者建立分享下載的連結。博文裡用的樣本是每類200張影像的測試樣本,下面給出一份每類50個影像的樣本子集,我以為用來做學術測試已經夠了,出於公司利益考慮,請勿再向我索要完整樣本。
百度網盤:http://pan.baidu.com/s/1mgLUWBm