如何设计好词袋模型BoW模型的类类型
回顧過去自己寫過的一些詞袋模型,比如BoW圖像檢索Python實戰、圖像檢索(CBIR)三劍客之BoF、VLAD、FV以及Bag of Words cpp實現,這些寫出來的要么只是助于自己理解詞袋模型的有關理論,要么也只是面向實驗的一些驗證,或者更直接點可以說只是些小玩具擺了。
在我2016年的計劃列表里,存放著一條由2015年拖過來的目標,就是寫出一個可以面向商業級別的詞袋模型,這條計劃伴隨著成功將VLfeat的一些c接口打通而變成了可能,并且在過去的大半年里,自己也一直留意在具體編寫的時候選用哪些庫比較合適的問題。機緣巧合,這一段時間又重新開始造起了輪子,并有了初步的成功,所以在此梳理總結一下。在談怎樣設計一個詞袋模型的類類型之前,先談談庫的選用問題。
選取合適的庫
在具體編寫一個面向應用級別的詞袋模型的時候,大概會經歷這么幾個步驟:SIFT特征抽取,特征采樣,聚類,構建KD樹,統計詞頻,計算詞頻權重,計算詞頻直方圖,保存數據。這8個步驟在具體實現的時候,會設計到一些庫的選取問題,下面對其進行細談。
1) SIFT特征抽取提取選用哪個庫?
提取SIFT的庫有很多,主要有以下幾個大家用得比較多:
- Lowe的SIFT,效果只提供SIFT的二進制可執行文件,棄用;
- Robwhess的OpenSIFT,開源,效果也還不錯,需要一些別的依賴庫,不再更新,棄用;
- OpenCV的SIFT,這個當然在使用上是最方便的,文檔全,不依賴別的庫,但SIFT的實現效果并不是很好,棄用;
- VLfeat里的SIFT,SIFT的實現效果是比較好的,缺點是c接口文檔不怎么全,網上提供的資料也比較少,但多讀讀它的C源碼,還是可以搞定的,而且在不用依賴其他的庫,所以選擇這個庫來提取SIFT還是很好的,在實際提取的時候,我選用的是covdet函數來提取SIFT,它是一個功能更強大的提取co-variant特征提取器。
在去年的時候,基本弄清了VLfeat中的一些函數的C接口調用方式,covdet這個函數通過閱讀寫給matlab的接口源碼轉成的C,對比matlab提取的結果和自己轉成C之后提取的結果,兩者完全一致。
2) 矩陣運算庫的選取
雖然使用矩陣操作并不是必須的,除了OpenCV的矩陣,還有可能引入其他的矩陣運算庫,這些矩陣的引入會給后面的實現帶來巨大的方便,比如聚類,KD樹的構建以及后面詞頻統計等。作為運算的基礎庫,在矩陣庫的選擇上主要有下面幾個用得比較多:
- Eigen,使用的只需要把頭文件包含進工程里即可,提供了多個平臺的版本,比如可以運行于安卓上,矩陣運算操作還是比較方便的,更新得比較快,不過在PC平臺上開發,我比較傾向于使用下面要說的Armadillo。
- Armadillo,這個庫是我非常喜歡的矩陣運算庫,此矩陣庫在使用語法上Matlabjie借鑒了Matlab的語法使用習慣,所以熟悉Matlab的開發者在使用此庫的時候會覺得非常的舒服,并且有名的MLPack是建立在它的基礎之上,另外它的矩陣運算效率也是非常高的,使用的時候同Eigen一樣只需要包含頭文件目錄即可,最新版本中添加了KMeans聚類。因而,基于以上這些優點,在實現詞袋模型的時候,對于矩陣運算庫的選取,選擇這個無疑是最優的。
選用矩陣庫雖然能極大的方便我們的程序編寫,但是會涉及到數據類型的轉換,比如STL的vector存儲的數據要轉成Armadillo的矩陣進行運算,如果數據頻繁的進行類型的轉換,必然會降低程序的運行效率,因而在程序的編寫中,不必要轉換的盡量避免轉換。
3) 多線程并行處理
為了使程序的SIFT特征提取、KMeans聚類、統計詞頻等過程支持并行處理,在選擇并行計算庫的時候,有兩種選擇,一種是采用OpenMP,另一種是選擇MPI。OpenMP是采用的是內存共享的方式,只需要對原程序進行小幅調整即可實現并行處理,并且語法易讀已寫;MPI需要對原來的程序進行大幅重構,寫出來的代碼也不是很好讀。所以,在多線程并處計算庫選擇這塊,選擇OpenMP是比較好的。
詞袋模型的類類型設計
終于可以講核心的了,這一部分講講在編寫程序的時候自己對詞袋模型的類類型設計的一點心得。先上自己寫的詞袋模型的類類型,設計了兩個類,一個是SIFT特征提取的類類型,另一個是詞袋模型的類類型。先談談SIFT特征提取的類類型:
class siftDesctor{ public:siftDesctor(){};std::string imageName;std::vector<std::vector<float>> frame;std::vector<std::vector<float>> desctor;void covdet_keypoints_and_descriptors(cv::Mat &img, std::vector<std::vector<float>> &frames, std::vector<std::vector<float>> &desctor, bool rooSIFT, bool verbose);std::vector<float> rootsift(std::vector<float> &dst);void Serialize(std::ofstream &outfile) const {std::string tmpImageName = imageName;int strSize = (int)imageName.size();outfile.write((char *)&strSize, sizeof(int));outfile.write((char *)&tmpImageName[0], sizeof(char)*strSize); // 寫入文件名 int descSize = (int)desctor.size();outfile.write((char *)&descSize, sizeof(int));// 寫入sift特征 for(int i = 0; i < descSize; i++ ){outfile.write((char *)&(desctor[i][0]), sizeof(float) * 128);outfile.write((char *)&(frame[i][0]), sizeof(float) * 6);}}static siftDesctor Deserialize(std::ifstream &ifs) {siftDesctor siftDesc;int strSize = 0;ifs.read((char *)&strSize, sizeof(int)); // 寫入文件名 siftDesc.imageName = "";siftDesc.imageName.resize(strSize);ifs.read((char *)&(siftDesc.imageName[0]), sizeof(char)*strSize); // 讀入文件名 int descSize = 0;ifs.read((char *)&descSize, sizeof(int));// 讀入sift特征和frame for(int i = 0; i < descSize; i++ ){std::vector<float> tmpDesc(128);ifs.read((char *)&(tmpDesc[0]), sizeof(float) * 128);siftDesc.desctor.push_back(tmpDesc);std::vector<float> tmpFrame(6);ifs.read((char *)&(tmpFrame[0]), sizeof(float) * 6);siftDesc.frame.push_back(tmpFrame);}return siftDesc;}};在設計SIFT特征提取的類類型的時候,對于每一幅圖像,提取SIFT特征之后,由于需要保存圖像名、128維的SIFT特征以及6維的frame,因為imageName、desctor和frame這三個成員是必須的,這里說一下對于imageName這個成員,在最后保存方式文件名的時候,更合理的方式是不應該帶入文件所在目錄路徑的,因為最終寫入的數據有可能轉移到別的計算機上,帶入路徑不便于共享后使用者對數據的處理,這個地方我在剛開始設計的時候有欠考慮。另外,剛開始在設計covdet_keypoints_and_descriptors()方法的時候,是在方法里讀入圖片然后在提取特征的,當時想得是想讓這樣一個特征提取器更加的方便使用(只需要傳入圖像待路徑的文件名就可以處理),但后來發現其實根本沒必要這么設計這個方法,這種蹩腳的方式使得后面在顯示結果的時候,你需要再次讀入圖片,降低了程序的執行效率,因而改成了現在的這種傳入已讀入數據的方式。
上面除了三個重要的成員變量,兩個序列化和反序列化的方法是極其重要的,序列化的目的使得上面三個重要的成員變量得以保存,這樣可以避免當我們想再次聚類時又要提取特征的尷尬;反序列化使得我們可以讀取保存的數據,因而,三個成員變量加兩個方法都是必不可少的。
再談詞袋模型的類類型,先看類定義:
class bowModel { public:bowModel(){};bowModel(int _numWords,std::vector<siftDesctor> _imgFeatures, std::vector<std::vector<int>> _words):numWords(_numWords),imgFeatures(_imgFeatures),words(_words){};int numNeighbors = 1;int numWords;std::vector<siftDesctor> imgFeatures;std::vector<std::vector<int>> words;cv::Mat centroids_opencvMat;cv::flann::Index opencv_buildKDTree(cv::Mat ¢roids_opencvMat);void Serialize(std::ofstream &outfile) const {int imgFeatsSize = (int)imgFeatures.size();outfile.write((char *)&imgFeatsSize, sizeof(int));// 寫入imgFeatures和words for(int i = 0; i < imgFeatsSize; i++ ){imgFeatures[i].Serialize(outfile);outfile.write((char *)&(words[i][0]), sizeof(int) * imgFeatures[i].desctor.size());}}static bowModel Deserialize(std::ifstream &ifs) {bowModel BoW;int imgFeatsSize;ifs.read((char *)&imgFeatsSize, sizeof(int));BoW.words.resize(imgFeatsSize);for (int i = 0; i < imgFeatsSize; i++) {// 讀入imgFeatures auto siftDesc = siftDesctor::Deserialize(ifs);BoW.imgFeatures.push_back(siftDesc);// 讀入words BoW.words[i].resize(siftDesc.desctor.size());ifs.read((char *)&(BoW.words[i][0]), sizeof(int) * siftDesc.desctor.size());}return BoW;}};上面最重要的有三個東西,一是成員std::vector<siftDesctor> imgFeatures,另外是序列化和反序列化方法。對于每一個圖片提取的特征,將imageName、desctor和frame通過實例化一個siftDesctor將其保存起來,這樣我們將所有圖片的siftDesctor實例用STL的vector保存下來,在序列化的時候,對每一個實例通過調用SIFT特征提取的類類型中定義的序列化方法將其保存下來,讀取數據的時候,其過程基本就是原來的一個擬過程。通過這樣設計這樣兩個SIFT特征提取的類類型和詞袋模型的類類型,在數據讀寫的時候,通過內外兩重循環,內部循環完成一個實例的數據讀寫,外部循環完成所有圖片實例的讀寫,使得我們可以比較優雅地完成圖片的特征提取、數據保存以及讀寫。
對于數據讀寫,做過一番調研,一種是通過HDF5的方式,一種是通過BOOST庫。HDF5很適合大數據量的保存,而且讀寫高效,但在C++里,寫起來沒有在Python里使用HDF5方便,BOOST也查閱過相應的資料,寫起來也比較繁雜,所以最后選擇了只用fstream來進行數據讀寫保存了,測了一下,數據讀寫還是比較高效的,所以暫且采用這種方案。
目前,已經對重造的輪子展開了測試,如下圖:
在ukbench和oxford building這兩個數據庫的結果會在測試結果出來后補充到本文后面。
from:?http://yongyuan.name/blog/how-to-design-a-good-bow-class.html
總結
以上是生活随笔為你收集整理的如何设计好词袋模型BoW模型的类类型的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: BoW图像检索Python实战
- 下一篇: 如何实现拼音与汉字的互相转换