先挖個坑,快期末考試了,有空填上w
好了,今晚剛好有點閑,就把坑填上吧。
//-------------------------------開篇-------------------------------------------
首先講一下,這篇隨筆不是講HOG特征是什么,怎么提取(這種圖像特征網上一搜一大把),也不是講BP神經網絡工作原理,發展史啥的(機器學習小白,ANN深究我也不懂)。在這里我要講的是,車標識別怎么code,怎么使用OpenCV自帶的BP神經網絡訓練,以及識別。好了廢話不多說,咱們開始吧。
//------------------------要准備的東西---------------------------------
正式講代碼前講一下正式完成本工程需要什么准備工作。
1、配置Opencv 2.4.6及以上版本的VS 2010+,OpenCV 3可能改動的比較大,對本工程來說不建議。
2、一個車標庫,盡量多一點,本工程共使用900來張車標樣本,分為訓練集(560張)和測試集(371張)
(工程遷移到我的Github倉庫了)
//--------------------------正篇-----------------------------------------
准備好了以上的東西,我們就可以開始了。
我將結合代碼分幾步來講怎么識別圖像特征、喂給BP神經網絡來識別車標,一步一步擴充代碼
////第一步 建立工程
簡單講一下工程預處理吧,本工程就一個main.cpp文件,因此在建好工程后,建一個main.cpp就可以了。然后將我們的車標庫test測試集和train訓練集放在和整個工程同一目錄下。
為了讀取圖片方便,我們使用兩個txt文件trainpath、和testpath來保存每張圖片的路徑(路徑文件在車標庫里有,但是如果要使用這兩個文件,就必須把他們放在main.cpp同一個目錄下,同時,車標庫也必須放在和工程同一目錄下)
這是車標庫和工程的存放位置關系
這是圖片路徑文件和main.cpp的關系
為main.cpp添加opencv的頭文件,因為對OpenCV的結構不是特別熟,因此大家只要把萬用的頭文件一股腦寫出來就行
1 #include <iostream> 2 #include <fstream> 3 #include <opencv.hpp> 4 #include <highgui.h> 5 #include <cxcore.h> 6 #include <cstring> 7 #include <ml.h> 8 #include <vector> 9 #include <iomanip>
本工程用了以上頭文件。
哦對了,還有兩個名字空間,寫一下吧
1 using namespace std; 2 using namespace cv;
有些頭文件就用了一兩個函數啦,比如cstring就用到了個memset()23333,還有些OpenCV 1.x版本的頭文件也挺多余的,不過還是都寫上吧,免得報錯了
////第二步 初始化工作
然后我們就開始帶代碼的編寫工作了,我們按照main.cpp的代碼順序來講。
我們先定義代碼要用到的全局變量:
1 //----------------------------全局變量定義--------------------------------- 2 vector<float> descriptors; //HOG特征存放向量 3 float data[m_NUM][F_NUM]; //樣本特征存放數組 4 float f[1][F_NUM]; //測試樣本特征存放數組 5 float dataCls[m_NUM][CLASSNUM]; //樣本所屬類別 6 int mClass ; //訓練樣本所屬類別 7 int dNum; //訓練樣本個數
其中有幾個宏要在之前定義一下
1 #define F_NUM 1764 //7*7*9*4 車標特征維數 2 #define m_NUM 560 //訓練樣本個數 3 #define CLASSNUM 4 //車標種類
解釋一下兩段數據的設置
首先講一下特征數目吧,HOG特征其實是一個1×N維的特征矩陣,N的確定由檢測窗口大小、塊大小、胞元大小決定。每個胞元9個bin。
本實驗檢測窗口定為64×64,就是整張圖片的大小,塊大小16×16,胞元8×8,步進8×8,這樣一張圖片就有(64/8-1)*(64/8-1)*9*(16*16)/(8*8)=1764維特征
那么560個樣本就有560*1764個特征,就構成了特征矩陣data[560][1764]。
來看看OpenCV的神經網絡訓練函數
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 );
這是我們之后要用到的關鍵函數,OpenCV自帶的神經網絡訓練函數,我們依次來看下參數
第一個input是一個num×F_NUM的訓練數據輸入矩陣,num是樣本個數,F_NUM是每個樣本的特征數目,是不是剛好對應了我們的data矩陣。但是我們看到,data是浮點型數組,inputs是Mat陣,怎么統一呢?其實OpenCV在初始化Mat的時候,就可以使用一般的二維數組進行賦值,只要數據類型匹配,並且維度相等就行了,就像下面一樣
1 Mat trainDate(m_NUM,F_NUM,CV_32FC1,data);
這里使用一個data的首地址初始化了trainData這個輸入陣。
再來解釋下dataCls為什么是560×4的。
繼續看trian()函數的第二個參數,outputs,是一個num×CLASSNUM的數據陣,num是樣本個數,CLASSNUM是樣本的總類別數。
當然對於560個數據,每個數據都要有一個類別。CLASSNUM是4,那么這個陣具體是怎么樣初始化的呢?
舉個例子,0號樣本屬於第1類,那么dataCls[0]={1,0,0,0} 也就是說,對應的那一類初始化為1,其余的都是0。
我們同樣使用上述的初始化數據陣的方法將dataCls的內容復制到trainLable中(注意,dataCls和data數組要嚴格對齊,就是說,x號樣本的特征放在data[x]里,所屬類別放在dataCls[x]里)
1 Mat trainLable(m_NUM,CLASSNUM,CV_32FC1,dataCls);
對於train的其他參數,除了params需要注意下,其他都是默認的。
////第三步 讀取訓練樣本,填充數據矩陣data和類別矩陣dataCls
首先,我們定義了全局變量,要進行初始化工作,那么寫完void main()后的第一件事就是調用init()函數,進行初始化工作,init()代碼如下
/************************************************** *名稱:init() *參數:void *返回值:void *作用:初始化各類參數 ****************************************************/ void init() { memset(data,0,sizeof(data)); memset(dataCls,0,sizeof(dataCls)); mClass = -1; //初始類別為-1 dNum = 0; //統計樣本個數,其實沒軟用,對於本工程樣本個數固定為560 }
之后是讀入圖像和提取HOG特征,並記錄樣本所屬類別和填充數據矩陣,代碼如下
init(); ifstream in("trainpath.txt");string s,ss; while( in >> s){ if(ss != s.substr(0,19)){ mClass++; //類別是0,1,2,3 cout<<mClass<<endl; } ss = s.substr(0,19); cout<<s<<endl; //------------------------讀入圖像,縮放圖像---------------------------- Mat imge = imread(s),img; if(imge.empty()) { cout<<"image load error!"<<endl; system("pause"); return 0; } resize(imge,img,Size(64,64)); //------------------------提取HOG特征,放入特征數組--------------------- getHOG(img); packData(sta); //填充特征數組和類別數組 }
稍微解釋一下流程。
先定義一個文件流用於讀取訓練集路徑文件trainpath.txt,這個txt文件保存了所有訓練集的文件路徑,一行一個,像這樣
..\..\data\train\Citroen\X-雪鐵龍_1350198-01_201502010833146800.jpg ..\..\data\train\Citroen\X-雪鐵龍_1350198-01_201502010841008800.jpg ..\..\data\train\Citroen\X-雪鐵龍_1350198-01_201502010845367300.jpg
而且,不同類別的車標放在一起,舉個例子,共400行,前100行是雪鐵龍,再100行本田,再100行一汽,再100行福田(每個字符串的前17行是一樣的,19行肯定不一樣)
這樣有個好處,可以方便地統計這個圖片路徑對應的圖片屬於哪個種類的車。我們從代碼來看這個過程。
先定義兩個字符串ss和s,文件流一行行讀入並保存一行到s,取前19行,如果不等於ss,就讓mClass+1。
可以看到,初始mClass=-1.並且第一個字符串肯定不等於ss(因為此時ss為空),那么第一個圖片數據就屬於類別0,之后保存ss為s的前19位。
這樣,讀完整個圖片路徑,4種車標就可以很清楚地被區分了。
然后后面這個是讀圖保護,不管他,
然后讀入圖片,使用resize函數將其壓縮到64×64,看到沒,這就是我們提取HOG時候的檢測窗口大小。
調用getHog(img)獲取圖像的HOG特征,這個getHog是自定義函數,寫在main函數前面就行,代碼如下:
1 /************************************************** 2 *名稱:getHOG() 3 *參數:Mat& img 4 *返回值:void 5 *作用:獲取圖像的HOG特征 6 ****************************************************/ 7 void getHOG(Mat& img) 8 { 9 HOGDescriptor *hog = new HOGDescriptor( 10 Size(64,64), //win_size 檢測窗口大小,這里即完整的圖 11 Size(16,16), //block_size 塊大小 12 Size(8,8), //blackStride 步進 13 Size(8,8), //cell_size 細胞塊大小 14 9 //9個bin 15 ); 16 hog -> compute( //提取HOG特征向量 17 img, 18 descriptors, //存放特征向量 19 Size(64,64), //滑動步進 20 Size(0,0) 21 ); 22 delete hog; 23 hog = NULL; 24 }
這里其實就調用了幾個openCV自帶的函數,對傳進來的圖片進行特征提取而已。有一點要注意,compute函數的第二個參數
descriptors是全局變量,記不起來的可以去前面的全局變量定義的地方找找,它就是用來保存提取到的HOG特征。
剛才我們也計算過了,一張圖1764個特征,也就是一次提取,descriptors就放一次1×1764的數據。
那么提取到一張圖的特征后,我們要把他放到data里,那么就是packData了,同樣,packData是一個全局函數
void packData() { int p = 0; for (vector<float>::iterator it = descriptors.begin(); it != descriptors.end(); it++) { data[dNum][p++] = *it; } dataCls[dNum++][mClass] = 1.0; }
前一半的for循環用來從descriptors中的向量填到data矩陣中,后一個語句就是更新它對應的類別矩陣。
循環執行完,我們的數據也填充完畢了,接下來就是建立網絡訓練了。
////第四步 建立神經網絡 訓練參數矩陣
先上這部分代碼
1 //------------------------建BP神經網絡,開始訓練------------------------ 2 CvANN_MLP bp; 3 4 CvANN_MLP_TrainParams params; 5 params.term_crit=cvTermCriteria(CV_TERMCRIT_ITER+CV_TERMCRIT_EPS,7000,0.001); //迭代次數7000,最小誤差0.001 6 params.train_method=CvANN_MLP_TrainParams::BACKPROP; //訓練方法反向傳播 7 params.bp_moment_scale=0.1; 8 params.bp_dw_scale=0.1; 9 10 11 Mat layerSizes = (Mat_<int>(1,3) << F_NUM,48,4 ); //3層神經網絡 12 Mat trainDate(m_NUM,F_NUM,CV_32FC1,data); 13 Mat trainLable(m_NUM,CLASSNUM,CV_32FC1,dataCls); 14 bp.create(layerSizes, CvANN_MLP::SIGMOID_SYM); //激活函數sigmoid 15 system("cls"); 16 cout<<"訓練中...訓練時間大致需要6分鍾,請耐心等待"; 17 bp.train(trainDate,trainLable, Mat(),Mat(), params); //開始訓練 18 19 system("cls"); 20 cout << "訓練完成!!" <<endl;
CvANN_MLP是openCV自帶的人工神經網絡類,可以直接用,很方便吧。
我們先定義了一個CvANN_MLP類,然后看第二塊,第二塊就是神經網絡的一些參數的設定,具體注釋都有,就不講了
第三塊:第11行,定義神經網絡層數為3層,第一層:F_NUM個神經元,還記得F_NUM嗎?全局變量,就是特征數1764!
總之神經網絡就是1764,48,4共3層,每次節點數就是這么幾個。
12-13行,看見沒,這就是把我們填充完的數據數組和類別數組賦值給Mat陣,之后就能調用create函數啦,創建一個網絡,使用SIGMOID函數什么的。
然后是訓練,train()之前也說過。SO EASY!
等待6分鍾左右,訓練就結束了,之后就是測試了
////第五步 測試神經網絡
老規矩,上代碼再說
1 //---------------------------------讀入圖像,開始測試-------------------------- 2 system("cls"); 3 cout<<"開始測試..."<<endl; 4 system("cls"); 5 Mat imge,img; 6 7 ifstream ins("testpath.txt"); 8 9 int cls = -1; 10 int num=0,c_num=0; 11 while( ins >> s){ 12 memset(f,0,sizeof(f)); 13 if(ss != s.substr(0,19)){ 14 cls++; 15 cout<<cls<<endl; 16 } 17 cout<<s<<endl; 18 ss = s.substr(0,19); 19 imge = imread(s); 20 resize(imge,img,Size(64,64)); //使用線性插值 21 num++; 22 if (classifier(img,bp) == cls) 23 { 24 c_num++; 25 } 26 27 } 28 system("cls"); 29 cout<<"測試完成"<<endl; 30 cout<<"***************************************"<<endl; 31 cout<<"*樣本個數:"<<num<<endl; 32 cout<<"*正確個數:"<<c_num<<endl; 33 cout<<"*正確率:"<<setprecision(4)<<(float)c_num/num*100<<"%"<<endl; 34 cout<<"***************************************"<<endl; 35 system("pause");
測試就不說太多了了,無非讀一下測試路徑集,匹配一下,唯一要講的就是那個第22行的classiffier函數,這個也是個全局函數,上代碼吧2333
/************************************************** *名稱:classifier() *參數:Mat& CvANN_MLP& *返回值:int *作用:求解測試結果最相鄰類別 ****************************************************/ int classifier(Mat& image,CvANN_MLP& bp) { getHOG(image); int p = 0;
for (vector<float>::iterator it = descriptors.begin(); it != descriptors.end(); it++) { f[0][p++] = *it;
}
Mat nearest(1, CLASSNUM, CV_32FC1, Scalar(0)); Mat charFeature(1, F_NUM, CV_32FC1,f); bp.predict(charFeature, nearest); Point maxLoc; minMaxLoc(nearest, NULL, NULL, NULL, &maxLoc); int result = maxLoc.x; return result; }
這個函數返回神經網絡預測測試圖片最可能的所屬類別。之后就是統計正確個數了。
//--------------------------結語-----------------------------------------
代碼全寫在一個cpp里了2333,為了方便講解,也方便自己學習嘛,不知道你有沒有看明白我講的呢ww。
可能以上講解也有疏漏,如果建完工程還是有問題的話,就直接下載下面的工程對照着這個講解再看一遍吧Orz(注意,運行前保證環境搭好,而且文件路徑不要更改)
(附完整工程下載地址:https://github.com/Holy-Shine/carLogoRec)
有興趣的小伙伴star一下倉庫吧嘻嘻。