回顧我過去寫過的壹些詞袋模型,如弓圖像檢索Python實戰、CBIR三劍客BoF、VLAD、FV和詞袋cpp,我所寫的只是對我理解詞袋模型理論有幫助,或者只是壹些面向實驗的驗證,或者更直接的說,只是壹些小玩具。
在我2016的計劃清單裏,有壹個從2015拖過來的目標,就是寫壹個可以面向業務層面的字袋模型。這個計劃隨著VLfeat部分C接口的成功開放而成為可能,這半年來我壹直在關註詳細寫的時候選擇哪些庫的問題。巧的是,這段時間輪子又造出來了,而且已經初見成效,我就在這裏整理總結壹下。在說如何設計壹個課型的書包模型之前,先說壹下庫的選擇。
選擇適當的庫
寫壹個面向應用的詞袋模型,大概會經歷幾個步驟:SIFT特征提取、特征采樣、聚類、構建KD樹、統計詞頻、計算詞頻權重、計算詞頻直方圖、保存數據。當這八個步驟實現後,他們會設計壹些選庫問題,下面會詳細討論。
1)SIFT特征提取選哪個庫?
提取SIFT的庫有很多,下面主要是人用的:
Lowe的SIFT,效果只提供SIFT的二進制可執行文件,丟棄;
Robwhess的OpenSIFT是開源的,效果還不錯。它需要壹些其他的依賴庫,所以不會被更新或丟棄。
OpenCV的SIFT當然是最方便用的,文檔完整,不依賴其他庫,但是SIFT的實現效果不是很好,所以棄用了;
VLfeat中的SIFT和SIFT效果不錯,缺點是C接口文檔不全,網上提供的資料較少,但多讀它的C源代碼就可以了,不依賴其他庫,選擇這個庫提取SIFT就不錯了。在實際提取中,我選擇covdet函數來提取SIFT,這是壹個提取共變的更強大的特征。
去年基本搞清楚了VLfeat中壹些函數的C接口調用方法。covdet把寫給matlab的接口源代碼轉換成C,把matlab提取的結果和我自己轉換成C後提取的結果對比,完全壹致。
2)矩陣運算庫的選擇
雖然不壹定要用矩陣運算,但是除了OpenCV矩陣之外,還可以引入其他矩陣運算庫,給後期實現帶來很大的便利,比如聚類、KD樹構建、詞頻統計等。作為基本庫的操作,以下主要用於選擇矩陣庫:
Eigen,只需要在項目中包含頭文件,提供多平臺版本,比如可以在Android上運行,矩陣操作相對方便,更新快。但是,在PC平臺上開發時,我更喜歡使用下面的Armadillo。
犰狳,這個庫是我最喜歡的矩陣運算庫。在使用語法上,MATLABjie借鑒了Matlab的語法使用習慣,所以熟悉Matlab的開發人員在使用這個庫的時候會覺得很舒服,著名的MLPack就是基於它。另外,它的矩陣運算效率也很高。就像Eigen壹樣,使用時只需要包含頭文件目錄,最新版本中加入了KMeans集群。因此,基於這些優點,在實現字袋模型時,矩陣運算庫的選擇無疑是最佳選擇。
雖然選擇矩陣庫可以大大方便我們的編程,但是會涉及到數據類型的轉換。比如存儲在STL的vector中的數據,會轉換成Armadillo的矩陣進行運算。如果數據轉換頻繁,必然會降低程序的運行效率。所以在編程中,要盡量避免不必要的轉換。
3)多線程並行處理
為了支持SIFT特征提取、KMeans聚類和詞頻統計過程中的並行處理,在選擇並行計算庫時有兩種選擇,壹是使用OpenMP,二是選擇MPI。OpenMP采用* * *內存共享的模式,只需對原程序稍加調整即可實現並行處理,語法易於讀寫;MPI需要對原程序進行很大的重構,寫出來的代碼不是很好讀。所以多線程並行計算庫中選這壹塊,選OpenMP比較好。
單詞袋模型的課型設計
終於可以說核心了。這壹部分講講我自己寫程序時對書包模型的課型設計的體會。首先介紹了自己寫的詞袋模型的類類型,設計了兩個類,壹個是SIFT特征提取的類類型,壹個是詞袋模型的類類型。先說SIFT特征提取的類類型:
分類篩選描述符{
公共:
sift descriptor(){ };
STD::string imageName;
STD::vector & lt;STD::vector & lt;float & gt& gt框架;
STD::vector & lt;STD::vector & lt;float & gt& gt描述者;
void cov det _ key points _ and _ descriptor(cv::Mat & amp;img,STD::vector & lt;STD::vector & lt;float & gt& gt& amp框架,STD::vector & lt;STD::vector & lt;float & gt& gt& ampdesctor,bool rooSIFT,bool verbose);
STD::vector & lt;float & gtroot sift(STD::vector & lt;float & gt& ampdst);
void序列化(STD::of stream & amp;outfile) const {
STD::string tmpImageName = imageName;
int strSize =(int)imagename . size();
outfile . write((char *)& amp;strSize,sizeof(int));
outfile . write((char *)& amp;tmpImageName[0],sizeof(char)* strSize);//寫入文件名
int descSize =(int)desctor . size();
outfile . write((char *)& amp;descSize,sizeof(int));
//寫入sift特征
for(int I = 0;我& ltdescSizei++ ){
outfile . write((char *)& amp;(desctor[i][0]),sizeof(float)* 128);
outfile . write((char *)& amp;(frame[i][0]),sizeof(float)* 6);
}
}
靜態siftDesctor反序列化(STD::if stream & amp;ifs) {
sift descriptor sift desc;
int strSize = 0;
ifs . read((char *)& amp;strSize,sizeof(int));//寫入文件名
siftDesc.imageName =
sift desc . imagename . resize(strSize);
ifs . read((char *)& amp;(siftDesc.imageName[0]),sizeof(char)* strSize);//讀入文件名
int descSize = 0;
ifs . read((char *)& amp;descSize,sizeof(int));
//讀入sift特征和幀。
for(int I = 0;我& ltdescSizei++ ){
STD::vector & lt;float & gttmpDesc(128);
ifs . read((char *)& amp;(tmpDesc[0]),sizeof(float)* 128);
sift desc . desctor . push _ back(tmpDesc);
STD::vector & lt;float & gttmp frame(6);
ifs . read((char *)& amp;(tmpFrame[0]),sizeof(float)* 6);
sift desc . frame . push _ back(tmp frame);
}
返回siftDesc
}
};
在設計SIFT特征提取的類類型時,對於每張圖像,SIFT特征提取後,需要保存imageName、128維SIFT特征和6維frame,因為imageName、desctor和frame三個成員是必須的。先說這個成員,ImageName。在最後保存文件名時,比較合理的做法是不要帶入文件所在的目錄路徑,因為最後寫入的數據可能會轉移到其他電腦上,帶入路徑不方便用戶欣賞後對數據的處理。剛開始設計的時候沒考慮這個地方。另外,最初設計cov det _ key points _ and _ descriptors()方法時,在方法中讀取圖像,然後提取特征。當時的想法是讓這樣的特征提取器使用起來更方便(只能處理要走線的圖像的文件名),但後來發現沒必要設計這種方法。這種蹩腳的方式使得您在稍後顯示結果時需要再次讀取圖像,從而降低了成本。
除了三個重要的成員變量之外,序列化和反序列化的兩種方法也非常重要。序列化的目的是保存三個重要的成員變量,可以避免我們再想聚類時提取特征的尷尬。反序列化允許我們讀取保存的數據,所以需要添加三個成員變量和兩個方法。
先說壹下單詞袋模型的類類型,先看類定義:
bowModel類{
公共:
bow model(){ };
bowModel(int _numWords,STD::vector & lt;sift descriptor & gt;_imgFeatures,STD::vector & lt;STD::vector & lt;int & gt& gt_words):numWords(_numWords)、imgFeatures(_imgFeatures)、words(_ words){ };
int numNeighbors = 1;
int numWords
STD::vector & lt;sift descriptor & gt;imgFeatures
STD::vector & lt;STD::vector & lt;int & gt& gt詞;
cv::Mat centroids _ opencvMat;
cv::flann::Index opencv _ buildKDTree(cv::Mat & amp;centroids _ opencvMat);
void序列化(STD::of stream & amp;outfile) const {
int imgFeatsSize =(int)img features . size();
outfile . write((char *)& amp;imgFeatsSize,sizeof(int));
//編寫imgFeatures和單詞
for(int I = 0;我& ltimgFeatsSizei++ ){
imgFeatures[i]。序列化(outfile);
outfile . write((char *)& amp;(words[i][0]),sizeof(int)* img features[I]. desctor . size());
}
}
靜態bowModel反序列化(STD::if stream & amp;ifs) {
bowModel弓;
int imgFeatsSize
ifs . read((char *)& amp;imgFeatsSize,sizeof(int));
bow . words . resize(imgFeatsSize);
for(int I = 0;我& ltimgFeatsSizei++) {
//讀入imgFeatures
auto sift desc = sift descriptor::Deserialize(ifs);
bow . img features . push _ back(sift desc);
//讀入單詞
BoW.words[i]。resize(sift desc . desctor . size());
ifs . read((char *)& amp;(BoW.words[i][0]),sizeof(int)* sift desc . desctor . size());
}
回力弓;
}
};
上面最重要的有三個,壹個是成員STD::vector < sift descriptor & gt;ImgFeatures以及序列化和反序列化方法。對於從圖像中提取的每個特征,通過實例化SIFTDESTOR來保存imageName、desctor和frame。這樣,我們用STL的vector保存了所有圖像的SIFTDESTOR實例。序列化時,我們通過調用從SIFT特征中提取的類類型中定義的序列化方法來保存每個實例。在讀取數據時,進程基本上是原始的準進程。通過這樣設計SIFT特征提取的類類型和bag模型的類類型,在讀寫數據時,我們可以通過內循環和外循環讀寫數據的壹個實例,從而優雅地完成圖片的特征提取、數據保存和讀寫。
對於數據讀寫,我們做了壹些研究,壹個是通過HDF5,壹個是通過BOOST庫。HDF5非常適合保存大量數據,讀寫效率高。但是在C++中,不如在Python中使用HDF5方便,而且BOOST也查閱了相應的資料,所以寫起來也比較復雜,所以最終只選擇了fstream進行數據讀寫。經過測試,數據讀寫還是比較高效的,所以暫時采用這個方案。