個人博客:http://www.chenjianqu.com/
原文鏈接:http://www.chenjianqu.com/show-101.html
本文是<視覺SLAM14講>的學習筆記,今天學習到詞袋模型,可以用來計算圖像間的相似度。
基本概念
詞袋(Bag-of-Words,BoW),是用“圖像上有哪幾種特徵”來描述一個圖像的方法。圖像的詞袋模型可以度量兩個圖像的相似性:首先需要確定BoW中的“單詞”,許多單詞放在一起,組成“字典”。然後確定一張圖像中出現了哪些單詞(這裏的單詞對應的是特徵),把圖像轉換成了一個向量。最後根據向量,設計一定的計算方式,就能確定圖像間的相似性了。
通過字典和單詞,只需一個向量就可以描述整張圖像了。該向量描述的是“圖像是否含有某類特徵”的信息,比單純的灰度值更加穩定。因爲描述向量說的是“是否出現”,而不管它們“在哪兒出現”,所以與物體的空間位置和排列順序無關,因此稱它爲Bag-of-Words。在相機發生少量運動時,只要物體仍在視野中出現,就仍然保證描述向量不發生變化。
單詞的權重
一張圖像可以提取出N個特徵,這N個特徵可以對應到字典中的N各單詞,即得到了該圖像的單詞序列。考慮到不同的單詞(特徵)在區分性上的重要性並不相同,我們希望對單詞的區分性或重要性加以評估,給它們不同的權值以起到更好的效果,常用的一種做法稱爲 TF-IDF(Term Frequency–Inverse Document Frequency,頻率-逆文檔頻率)。TF 部分的思想是,某單詞在一個圖像中經常出現,它的區分度就高。IDF 的思想是,某單詞在字典中出現的頻率越低,則分類圖像時區分度越高。
在詞袋模型中,在建立字典時計算 IDF 部分。統計某個單詞 wi 中的特徵數量相對於所有特徵數量的比例,作爲 IDF 部分。假設所有特徵數量爲 n,wi 數量爲 ni,那麼:IDFi=log(n/ni) 。
TF 部分則是指某個特徵在單個圖像中出現的頻率。假設圖像 A 中,單詞wi 出現了 ni 次,而一共出現的單詞次數爲 n,那麼:TFi = ni/n。
單詞 wi 的TF-IDF權重:ni = TFi * IDFi
對於某個圖像 A,它的特徵點可對應到許多個單詞,組成它的 Bag-of Words:A = {(w1,η1),(w2,η2), . . . ,(wn,ηn)} = vA 。通過詞袋,我們用單個向量 vA 描述了圖像 A。向量 vA 是一個稀疏的向量,它的非零部分指示出圖像 A 中含有哪些單詞,而這些部分的值爲 TF-IDF 的值。
字典的構建
字典由很多單詞組成,而每一個單詞代表了一類特徵。一個單詞與一個單獨的特徵點不同,它不是從單個圖像上提取出來的,而是某一類特徵的組合。所以,字典生成問題類似於一個聚類(Clustering)問題。假設對大量的圖像提取了N 個特徵點,我們想找一個有 k 個單詞的字典,每個單詞可以看作局部相鄰特徵點的集合,這可以用經典的 K-means(K 均值)算法解決。K-means算法流程:
1. 隨機選取 k 箇中心點:c1, . . . , ck;
2. 對每一個樣本,計算與每個中心點之間的距離,取最小的作爲它的歸類;
3. 重新計算每個類的中心點。
4. 如果每個中心點都變化很小,則算法收斂,退出;否則返回 1。
考慮到字典的通用性,通常會使用一個較大規模的字典,以保證當前使用環境中的圖像特徵都曾在字典裏出現過。爲了加快字典的查找效率,常用K叉樹表達字典。假定有 N 個特徵點,希望構建一個深度爲 d,每次分叉爲 k 的樹,那麼做法如下:
1. 在根節點,用 k-means 把所有樣本聚成 k 類(實際中爲保證聚類均勻性會使用k-means++)。這樣得到了第一層。
2. 對第一層的每個節點,把屬於該節點的樣本再聚成 k 類,得到下一層。
3. 依此類推,最後得到葉子層。葉子層即爲所謂的 Words。
如下圖:
最終在葉子層構建了單詞,樹結構中的中間節點僅供快速查找時使用。這樣一個 k 分支,深度爲 d 的樹,可以容納 kd 個單詞。在查找某個給定特徵對應的單詞時,只需將它與每個中間結點的聚類中心比較(一共 d 次),即可找到最後的單詞,保證了對數級別的查找效率。
相似度計算
給定兩張圖像的單詞向量vA ,vB,可以通過多種方式計算它們的差異。比如這裏使用L1範數形式:
代碼實現
這裏使用ORB特徵的描述子作爲BoW的特徵,使用DBoW3庫實現詞袋模型。字典往往是從更大的數據集中生成的,而且最好是來自目標應該環境類似的地方。我們通常使用較大規模的字典——越大代表字典單詞量越豐富,容易找到與當前圖像對應的單詞。下面是代碼實現:
CMakeLists.txt
cmake_minimum_required(VERSION 2.6) project(dbow3_test) set( CMAKE_CXX_FLAGS "-std=c++11" ) find_package( OpenCV 3 REQUIRED ) include_directories( ${OpenCV_INCLUDE_DIRS} ) set( DBoW3_INCLUDE_DIRS "/usr/local/include" ) set( DBoW3_LIBS "/usr/local/lib/libDBoW3.a" ) add_executable(dbow3_test main.cpp) target_link_libraries(dbow3_test ${OpenCV_LIBS} ${DBoW3_LIBS}) install(TARGETS dbow3_test RUNTIME DESTINATION bin)
main.cpp
#include <iostream> #include <string> #include <vector> #include <string.h> #include <dirent.h> #include <DBoW3/DBoW3.h> #include <opencv2/core/core.hpp> #include <opencv2/highgui/highgui.hpp> #include <opencv2/features2d/features2d.hpp> using namespace std; using namespace cv; //讀取某路徑下的所有文件名 int getFiles(const string path, vector<string>& files) { int iFileCnt = 0; DIR *dirptr = NULL; struct dirent *dirp; if((dirptr = opendir(path.c_str())) == NULL)//打開一個目錄 return 0; while ((dirp = readdir(dirptr)) != NULL){ if ((dirp->d_type == DT_REG) && 0 ==(strcmp(strchr(dirp->d_name, '.'), ".png")))//判斷是否爲文件以及文件後綴名 files.push_back(dirp->d_name); iFileCnt++; } closedir(dirptr); return iFileCnt; } //構建字典 void generateDict() { string dataPath = "/media/chen/chen/SLAM/projects_test/DBow3_test/data"; vector<string> files; getFiles(dataPath,files);//獲取圖片名 //讀取圖片並提取ORB描述子 vector<Mat> descriptors; Ptr< Feature2D > detector = ORB::create(); for(const auto &x: files) { string picName=dataPath+"/"+x; cout << picName << endl; vector<KeyPoint> keypoints; Mat descriptor; detector->detectAndCompute( imread(picName), Mat(), keypoints, descriptor ); descriptors.push_back( descriptor ); } DBoW3::Vocabulary vocab; vocab.create( descriptors ); cout<<"vocabulary info: "<<vocab<<endl; vocab.save( "../vocabulary.yml.gz" ); cout<<"done"<<endl; } int main(int argc, char** argv) { //生成字典 //generateDict(); //用字典判斷圖像相似度 //DBoW3::Vocabulary vocab("../vocab_larger.yml.gz");//10張圖像得到的字典 DBoW3::Vocabulary vocab("../vocab_larger.yml.gz");//2900張圖像得到的字典 //提取ORB的描述子 Mat despA,despB,despC; Ptr< Feature2D > detector = ORB::create(); vector<KeyPoint> keypoints; detector->detectAndCompute( imread("../test/A.png"), Mat(), keypoints, despA ); detector->detectAndCompute( imread("../test/B.png"), Mat(), keypoints, despB ); detector->detectAndCompute( imread("../test/C.png"), Mat(), keypoints, despC ); //構建圖像的單詞向量 DBoW3::BowVector vA,vB,vC; vocab.transform(despA,vA ); vocab.transform(despB,vB ); vocab.transform(despC,vC ); //比較各圖片單詞向量的相似度 double scoreAB=vocab.score(vA, vB); double scoreAC=vocab.score(vA, vC); double scoreBC=vocab.score(vB, vC); double scoreAA=vocab.score(vA, vA); cout<<"scoreAB:"<<scoreAB<<endl; cout<<"scoreAC:"<<scoreAC<<endl; cout<<"scoreBC:"<<scoreBC<<endl; cout<<"scoreAA:"<<scoreAA<<endl; return 0; }
代碼中的A.png、B.png、C.png分別如下:
代碼輸出如下:
Starting: /media/chen/chen/SLAM/projects_test/DBow3_test/build/dbow3_test scoreAB:0.214842 scoreAC:0.0299757 scoreBC:0.0276582 scoreAA:1 *** Exited normally ***
可以看到A-B相似,因此得分較高,而A-C、B-C不相似,因此得分較低。