前段時間寫過一些關於OpenCV基礎知識方面的系列文章,主要內容是面向OpenCV初學者,介紹OpenCV中一些經常使用的函數的接口和調用方法,相關的內容在OpenCV的手冊裏都有更詳細的解釋,當時本身也是邊學邊寫,權當爲一種筆記的形式,因此不免有淺嘗輒止的感受,如今回頭看來,不少地方描述上都存在不足,之後有時間,我會從新考慮每一篇文章,讓成長系列對基礎操做的介紹更加詳細一些。html
OpenCV進階之路相比於成長系列,不會有太多的基礎函數的介紹,相對來講會更偏向於工程實踐,經過解決實際問題來講明某些較高級函數的用法和注意事項,主要內容會集中在特徵提取、機器學習和目標跟蹤幾個方向。因此這個系列文章知識點沒有前後順序之分,根據我的平時工做學習中遇到的問題而定。算法
這篇文章主要介紹OpenCV中神經網絡的用法,並經過車牌字符的識別來講明一些參數設置,函數調用順序等,而關於神經網絡的原理在博客機器學習分類裏已經詳細的講解與實現了,因此本文中就很少加說明。數組
車牌識別是計算機視覺在實際工程中一個很是成功的應用,雖然如今技術相對來講已經成熟,可是圍繞着車牌定位、車牌二值化、車牌字符識別等方向,仍是不時的有新的算法出現。經過學習車牌識別來提高本身在圖像識別方面的工程經驗是很是好的,由於它很是好的說明了計算機視覺的通常過程:網絡
圖像→預處理→圖像分析→目標提取→目標識別機器學習
而整個車牌識別過程實際上至關於包含了兩個上述過程:1,是車牌的識別;2,車牌字符的識別。函數
這篇文章其實主要是想介紹OpenCV中神經網絡的用法,而不是介紹車牌識別技術。因此咱們主要討論的內容集中在車牌字符的識別上,關於定位、分割等很少加敘述敘述。學習
在深度學習(將特徵提取做爲訓練的一部分)這個概念引入以前,通常在準備分類器進行識別以前都須要進行特徵提取。由於一幅圖像包含的內容太多,有些信息能區分差別性,而有些信息卻表明了共性。因此咱們要進行適當的特徵提取把它們之間的差別性特徵提取出來。spa
這裏面咱們計算二種簡單的字符特徵:梯度分佈特徵、灰度統計特徵。這兩個特徵只是配合本篇文章來講明神經網絡的廣泛用法,實際中進行字符識別須要考慮的字符特徵遠遠要比這複雜,還包括類似字特徵的選取等,也因爲工做上的緣由,這一部分並不深刻的介紹。code
1,首先是梯度分佈特徵,該特徵計算圖像水平方向和豎直方向的梯度圖像,而後經過給梯度圖像分劃不一樣的區域,進行梯度圖像每一個區域亮度值的統計,如下是算法步驟:htm
<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>將每一個網絡內總灰度值佔整個圖像的百分比統計在一塊兒寫入一個向量,將兩個方向各自獲得的向量並在一塊兒,組成特徵向量。
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,並將圖像展開爲一行,組成特徵向量。
關於神經網絡的原理個人博客裏已經寫了兩篇文章,而且給出了C++的實現,因此這裏我就不提了,下面主要說明在OpenCV中怎麼使用它提供的庫函數。
CvANN_MLP是OpenCV中提供的一個神經網絡的類,正如它的名字同樣(multi-layer perceptrons),它是一個多層感知網絡,它有一個輸入層,一個輸出層以及1或多個隱藏層。
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能夠採用以下定義:
Mat layerSizes=(Mat_<int>(1,3)<<x1,x2,x3);
或者用一個數組來初始化:
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。
神經網絡訓練參數的類型存放在CvANN_MLP_TrainParams這個類裏,它提供了一個默認的構造函數,咱們能夠直接調用,也能夠一項一項去設。
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次。
CvANN_MLP_TRainParams param; param.term_crit=cvTermCriteria(CV_TerMCrIT_ITER+CV_TERMCRIT_EPS,5000,0.01);
咱們先看訓練函數的接口,而後按接口去準備數據。
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的矩陣,咱們能夠這樣建立它。
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分別用於禁止輸入與輸出矩陣的歸一化。
一切都準備好後,直接開始訓練吧!
識別是經過Cv_ANN_MLP類提供的predict來實現的,知道原理的會明白,它實際上就是作了一次向前傳播。
float CvANN_MLP::predict(const Mat& inputs, Mat& outputs) const