DBoW2庫是University of Zaragoza里的Lopez等人開發的開源軟件庫。
由於在SLAM回環檢測上的優異表現(特別是ORB-SLAM2),DBoW2庫受到了廣大SLAM愛好者的關注。本文希望通過深入解析DBoW2庫及相關的DLoopDetector庫,為讀者后續使用這兩個庫提供參考。
git地址:
DBoW2
DLoopDetector
論文:Bags of Binary Words for Fast Place Recognition in Image Sequences
DBoW2庫介紹
詞袋模型
BoW(Bag of Words,詞袋模型),是自然語言處理領域經常使用的一個概念。以文本為例,一篇文章可能有一萬個詞,其中可能只有500個不同的單詞,每個詞出現的次數各不相同。詞袋就像一個個袋子,每個袋子里裝着同樣的詞。這構成了一種文本的表示方式。這種表示方式不考慮文法以及詞的順序。
在計算機視覺領域,圖像通常以特征點及其特征描述來表達。如果把特征描述看做單詞,那么就能構建出相應的詞袋模型。這就是本文介紹的DBoW2庫所做的工作。利用DBoW2庫,圖像可以方便地轉化為一個低維的向量表示。比較兩個圖像的相似度也就轉化為比較兩個向量的相似度。它本質上是一個信息壓縮的過程。
視覺詞典
詞袋模型利用視覺詞典(vocabulary)來把圖像轉化為向量。視覺詞典有多種組織方式,對應於不同的搜索復雜度。DBoW2庫采用樹狀結構存儲詞袋,搜索復雜度一般在log(N),有點像決策樹。
詞典的生成過程如下圖。
這棵樹里面總共有\(1+K+\cdots+K^L=(k^{L+1}-1)/(K-1)\)個節點。所有葉節點在\(L\)層形成\(W=K^L\)類,每一類用該類中所有特征的平均特征(meanValue)作為代表,稱為單詞(word)。每個葉節點被賦予一個權重。作者提供了TF、IDF、BINARY、TF-IDF等權重作為備選,默認為TF-IDF。
TF-IDF的主要思想是:如果某個詞或短語在一篇文章中出現的頻率TF高,並且在其他文章中很少出現,則認為此詞或者短語具有很好的類別區分能力,適合用來分類。TF-IDF實際上是TF * IDF,TF代表詞頻(Term Frequency),表示詞條在文檔d中出現的頻率。IDF代表逆向文件頻率(Inverse Document Frequency)。如果包含詞條t的文檔越少,IDF越大,表明詞條t具有很好的類別區分能力。(來自百度百科)
第k個葉節點(單詞)的TF和IDF分別定義為
視覺詞典可以通過離線訓練大量數據得到。訓練中只計算和保存單詞的IDF值,即單詞在眾多圖像中的區分度。TF則是從實際圖像中計算得到各個單詞的頻率。單詞的TF越高,說明單詞在這幅圖像中出現的越多;單詞的IDF越高,說明單詞本身具有高區分度。二者結合起來,即可得到這幅圖像的BoW描述。
假設訓練集有10萬幅圖像,每幅圖像提取出200個特征,總共有兩千萬個特征。如果我們取K=10,L=6,那么詞典總共有十萬個節點,壓縮了200倍。K和L需要根據場景的豐富程度和特征的區分度選取。
在DBoW2庫中,如果特征描述是ORB特征,那就訓練得到ORB詞典;如果是SIFT特征,那就訓練得到SIFT詞典。DBoW2庫利用一個大的圖像數據庫,離線訓練好了ORB庫和SIFT庫,供大家使用。因此,在使用DBoW2庫時,首先需要載入一個離線視覺詞典。
ORB-SLAM2中,那個100多兆的文件就是ORB詞典。
注意ORB特征和SIFT特征對於meanValue()和distance()的定義有所不同。
代碼解析
生成詞典的函數位於TemplatedVocabulary.h
,具體實現為
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor,F>::create(
const std::vector<std::vector<TDescriptor> > &training_features, // 圖像特征集合
int k, // 每層的類的個數
int L, // 樹的層數
WeightingType weighting, // 權重的類型,默認為TF-IDF
ScoringType scoring) // 得分的類型,默認為L1-norm
{
m_nodes.clear();
m_words.clear();
// 節點數 = Sum_{i=0..L} ( k^i )
int expected_nodes = (int)((pow((double)m_k, (double)m_L + 1) - 1)/(m_k - 1));
m_nodes.reserve(expected_nodes); // avoid allocations when creating the tree
// 將所有特征描述集合到一個vector
std::vector<pDescriptor> features;
getFeatures(training_features, features);
// 生成根節點
m_nodes.push_back(Node(0)); // root
// k-means++(內有遞歸)
HKmeansStep(0, features, 1);
// 建立一個只有葉節點的序列m_words
createWords();
// 為每個葉節點生成權重,此處計算IDF部分,如果不用IDF,則設為1
setNodeWeights(training_features);
}
k-means++過程
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor, F>::HKmeansStep
(
NodeId parent_id, // 父節點id
const std::vector<pDescriptor> &descriptors, // 該父節點對應的特征描述集合
int current_level // 當前層數
)
{
if (descriptors.empty()) return;
// 用來存儲子節點的特征描述 features associated to each cluster
std::vector<TDescriptor> clusters;
// 用來存儲每個子節點對應的特征描述在descriptors向量中的id
std::vector<std::vector<unsigned int> > groups; // groups[i] = [j1, j2, ...]
// j1, j2, ... indices of descriptors associated to cluster i
clusters.reserve(m_k);
groups.reserve(m_k);
// 如果特征描述個數小於m_k,直接分類
if ((int)descriptors.size() <= m_k) {
// trivial case: one cluster per feature
groups.resize(descriptors.size());
for (unsigned int i = 0; i < descriptors.size(); i++) {
groups[i].push_back(i);
clusters.push_back(*descriptors[i]);
}
} else {
// k-means分類
bool first_time = true;
bool goon = true;
// 用於檢查迭代過程中前后兩次分類結果是否一致,如一致,分類結束
std::vector<int> last_association, current_association;
// 迭代過程
while (goon) {
// 1. 分類
if (first_time) {
// 第一次,初始化分類
initiateClusters(descriptors, clusters);
} else {
// 計算每一類的meanValue
for (unsigned int c = 0; c < clusters.size(); ++c) {
std::vector<pDescriptor> cluster_descriptors;
cluster_descriptors.reserve(groups[c].size());
// 利用group,讀取每一類對應的id
std::vector<unsigned int>::const_iterator vit;
for (vit = groups[c].begin(); vit != groups[c].end(); ++vit) {
cluster_descriptors.push_back(descriptors[*vit]);
}
// 計算meanValue
F::meanValue(cluster_descriptors, clusters[c]);
}
} // if(!first_time)
// 2. 利用1計算的中心重新分類
groups.clear();
groups.resize(clusters.size(), std::vector<unsigned int>());
current_association.resize(descriptors.size());
typename std::vector<pDescriptor>::const_iterator fit;
// 對每一個特征,計算它與K個中心特征的距離,標記距離最小的中心特征的id
for (fit = descriptors.begin(); fit != descriptors.end(); ++fit) { //, ++d)
double best_dist = F::distance(*(*fit), clusters[0]);
unsigned int icluster = 0;
for (unsigned int c = 1; c < clusters.size(); ++c) {
double dist = F::distance(*(*fit), clusters[c]);
if (dist < best_dist) {
best_dist = dist;
icluster = c;
}
}
// 記錄分類信息
groups[icluster].push_back(fit - descriptors.begin());
current_association[ fit - descriptors.begin() ] = icluster;
}
// kmeans++ ensures all the clusters has any feature associated with them
// 3. 檢查前后兩次分類結果是否一致,如一致,分類結束
if (first_time) {
first_time = false;
} else {
goon = false;
for (unsigned int i = 0; i < current_association.size(); i++) {
if (current_association[i] != last_association[i]) {
goon = true;
break;
}
}
}
// 如果不一致,存儲本次分類信息
if (goon) {
// copy last feature-cluster association
last_association = current_association;
}
} // while(goon)
} // if must run kmeans
// 生成本層的節點,其特征描述為每一類的meanValue
for (unsigned int i = 0; i < clusters.size(); ++i) {
NodeId id = m_nodes.size();
m_nodes.push_back(Node(id));
m_nodes.back().descriptor = clusters[i];
m_nodes.back().parent = parent_id;
m_nodes[parent_id].children.push_back(id);
}
// 如果沒有達到L層,繼續分類
if (current_level < m_L) {
// iterate again with the resulting clusters
const std::vector<NodeId> &children_ids = m_nodes[parent_id].children;
for (unsigned int i = 0; i < clusters.size(); ++i) {
// 當前層的節點id
NodeId id = children_ids[i];
std::vector<pDescriptor> child_features;
child_features.reserve(groups[i].size());
std::vector<unsigned int>::const_iterator vit;
// 該id對應的特征描述集合
for (vit = groups[i].begin(); vit != groups[i].end(); ++vit) {
child_features.push_back(descriptors[*vit]);
}
// 進入下一層,繼續分類
if (child_features.size() > 1) {
HKmeansStep(id, child_features, current_level + 1);
}
}
}
}
可以看出,詞典樹的所有節點是按照層數來排列的。
圖像識別
離線生成視覺詞典以后,我們就能在線進行圖像識別或者場景識別。實際應用中分為兩步進行。
第一步:為圖像生成一個表征向量\(v_{1\times W}\)。圖像中的每個特征都在詞典中搜索其最近鄰的葉節點。所有葉節點上的權重集合構成了BoW向量\(v\)。
第二步:根據BoW向量,計算當前圖像和其它圖像之間的距離\(s(v_1,v_2)\)
有了距離定義,即可根據距離大小選取合適的備選圖像。
正向索引與反向索引
在視覺詞典之上,作者還加入了Database的概念,並引入了正向索引(direct index)和反向索引(inverse index)的概念。這部分代碼位於TemplatedDatabase.h
中。
反向索引
作者用反向索引記錄每個葉節點對應的圖像編號。當識別圖像時,根據反向索引選出有着公共葉節點的備選圖像並計算得分,而不需要計算與所有圖像的得分。反向索引定義為
// InvertedFile為所有葉節點反向索引的集合
// 每個葉節點(word)有一個反向索引,定義為IFRow
typedef std::vector<IFRow> InvertedFile;
// InvertedFile[word_id] --> inverted file of that word
// IFRow定義list,為一系列圖像編號的集合
// IFRows根據圖像編號的升序排列
typedef std::list<IFPair> IFRow;
struct IFPair
{
// Entry id,圖像編號
EntryId entry_id;
// Word weight in this entry,葉節點權重
WordValue word_weight;
}
其中IFPair儲存圖像編號和葉節點的權重(此處保存權重可方便得分s的計算)。
正向索引
當兩幅圖像進行特征匹配時,如果極線約束未知,那么只有暴力匹配,復雜度為\(O(N^2)\),或者先為特征生成k-d樹再利用k-d樹匹配,復雜度為\(O(N\log N)\)。作者提供了一種正向索引用於加速特征匹配。正向索引需要指定詞典樹中的層數,比如第m層。每幅圖像對應一個正向索引,儲存該圖像生成BoW向量時曾經到達過的第m層上節點的編號,以及路過這個節點的那些特征的編號。正向索引的具體定義為
// DirectFile為所有圖像正向索引的集合
// 每個圖像有一個FeatureVector,
// 每個FeaturVector定義為std::map,map的元素為<node_id, std::vector<feature_id>>
typedef std::vector<FeatureVector> DirectFile;
// DirectFile[entry_id] --> [ node_id, vector<feature_id> ]
FeatureVector通過下面介紹的transform()函數得到。
假設兩幅圖像為A和B,下圖說明如何利用正向索引來加速特征匹配的計算。
當然上述算法也可通過循環A或者B的正向索引來做。
這種加速特征匹配的方法在ORB-SLAM2中被大量使用。注意到,正向索引的層數如果選擇第0層(根節點),那么時間復雜度和暴力搜索一樣。如果是葉節點層,則搜索范圍有可能太小,錯失正確的特征點匹配。作者一般選擇第二層或者第三層作為父節點(L=6)。正向索引的復雜度約為\(O(N^2/K^m)\)。
代碼解析
圖像轉化為BoW向量(包含正向索引)
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor, F>::transform
(
const std::vector<TDescriptor> &features, // 圖像特征集合
BowVector &v, // bow向量,std::map<leaf_node_id, weight>
FeatureVector &fv, // 正向索引向量,std::map<direct_index_node_id, feature_id>
int levelsup // 正向索引的層數=L-levelsup
) const
{
// ignore some unimportant code here
// whether a vector must be normalized before scoring according
// to the scoring scheme
LNorm norm;
bool must = m_scoring_object->mustNormalize(norm);
typename std::vector<TDescriptor>::const_iterator fit;
// 依據權重類型,bow向量加入權重的方式有所不同
if (m_weighting == TF || m_weighting == TF_IDF) {
unsigned int i_feature = 0;
for (fit = features.begin(); fit < features.end(); ++fit, ++i_feature) {
WordId id;
NodeId nid;
WordValue w;
// 如果權重類型為TF-IDF,w為IDF。如為TF,w為1
transform(*fit, id, w, &nid, levelsup);
// 加入權重
if (w > 0) { // not stopped
// 累積該葉節點的idf權重,v(id).weight += w
// 最后v(id).weight實際上等於M*idf,M為插入該葉節點的特征描述的個數
v.addWeight(id, w);
// 插入<node_id, feature_id>
fv.addFeature(nid, i_feature);
}
}
if (!v.empty() && !must) {
// unnecessary when normalizing
const double nd = v.size();
// 只有SCORING_CLASS=DotProductScoring時
for (BowVector::iterator vit = v.begin(); vit != v.end(); vit++)
vit->second /= nd;
}
} else { // IDF || BINARY
unsigned int i_feature = 0;
for (fit = features.begin(); fit < features.end(); ++fit, ++i_feature) {
WordId id;
NodeId nid;
WordValue w;
// 如果權重類型為IDF,w為IDF。如為BINARY,w為1
transform(*fit, id, w, &nid, levelsup);
if (w > 0) { // not stopped
// 插入該葉節點的權重,v.insert(id,w)
v.addIfNotExist(id, w);
// 插入<node_id, feature_id>
fv.addFeature(nid, i_feature);
}
}
} // if m_weighting == ...
// 歸一化bow向量,v=v/|v|
// 因為要歸一化,所以之前計算的TF-IDF並沒有除以TF的分母(特征的總數,對於bow向量中的所有項都相等)
if (must) v.normalize(norm);
}
單個圖像特征尋找葉節點
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor, F>::transform
(
const TDescriptor &feature, // 當前帶插入的特征描述
WordId &word_id, // 待取出的葉節點id(葉節點序列中的id,非樹中的id)
WordValue &weight, // 待取出的權重
NodeId *nid, // 該特征描述對應的正向索引(樹中某一層的父節點id)
int levelsup // 正向索引在第(L-levelsup)層上
) const
{
// 將當前特征描述插入詞典樹的葉節點層
std::vector<NodeId> nodes;
typename std::vector<NodeId>::const_iterator nit;
// 如果nid不為空,則nid儲存該特征在第(L-levelsup)層上的父節點
// 用於正向指標
const int nid_level = m_L - levelsup;
if (nid_level <= 0 && nid != NULL) *nid = 0; // root
NodeId final_id = 0; // root
int current_level = 0;
// 逐層插入,直到葉節點層
do {
++current_level;
nodes = m_nodes[final_id].children;
final_id = nodes[0];
// 計算該特征與本層節點的距離,選取距離最小的節點
double best_d = F::distance(feature, m_nodes[final_id].descriptor);
for (nit = nodes.begin() + 1; nit != nodes.end(); ++nit) {
NodeId id = *nit;
double d = F::distance(feature, m_nodes[id].descriptor);
if (d < best_d) {
best_d = d;
final_id = id;
}
}
// 存儲正向索引nid
if (nid != NULL && current_level == nid_level)
*nid = final_id;
} while ( !m_nodes[final_id].isLeaf() );
// 取出葉節點對應的word id(所有葉節點集合內的編號)和權重
word_id = m_nodes[final_id].word_id;
weight = m_nodes[final_id].weight;
}
權重更新過程
// 每幅圖像有一個BoWVector
// TF-IDF或者TF采用這個函數
// 累積節點權重,bow向量是一個按WordId排序的有序序列
void BowVector::addWeight(WordId id, WordValue v)
{
// 找到第一個大於等於id的節點
BowVector::iterator vit = this->lower_bound(id);
// 找到了輸入id對應的節點
// 權重+=v
if(vit != this->end() && !(this->key_comp()(id, vit->first)))
{
vit->second += v;
}
// 沒有找到輸入id,插入<id,v>
// vit==end()(id比現有WordId都大)
// 或者vit的id不等於輸入的id
else
{
this->insert(vit, BowVector::value_type(id, v));
}
}
// IDF或者BINARY采用這個函數
// 當id不存在時,插入<id,v>
// 因為不考慮詞頻,所以每個葉節點只需要插入第一個到達此節點的權重值
void BowVector::addIfNotExist(WordId id, WordValue v)
{
BowVector::iterator vit = this->lower_bound(id);
if(vit == this->end() || (this->key_comp()(id, vit->first)))
{
this->insert(vit, BowVector::value_type(id, v));
}
}
FeatureVector更新過程
// 儲存所有到達過某個node_id的feature_id(正向索引)
// 每幅圖像有一個FeatureVector
void FeatureVector::addFeature(NodeId id, unsigned int i_feature)
{
// 找到第一個key大於等於node_id的項
FeatureVector::iterator vit = this->lower_bound(id);
// 如果key==node_id,push_back
if(vit != this->end() && vit->first == id)
{
vit->second.push_back(i_feature);
}
// 如果id還沒有出現,插入<node_id,feature_id>
else
{
vit = this->insert(vit, FeatureVector::value_type(id,
std::vector<unsigned int>() ));
vit->second.push_back(i_feature);
}
}
DLoopDetector庫介紹
在SLAM中,追蹤(Tracking)得到的位姿通常都是有誤差的。隨着路徑的不斷延伸,前面幀的誤差會一直傳遞到后面去,導致后續幀的姿態估計的誤差越來越大。就好比一個人走在陌生的城市里,可能一開始還能分清東南西北,但隨着在小街小巷轉來轉去,大概率已經無法定位自身的准確位置了。通過認真辨識周邊環境,他可以建立起局部的地圖信息(局部優化)。再回憶以前走過的路徑,他可以糾正一些以前的地圖信息(全局優化)。然而他還是不能確定自己在城市的精確方位。直到他看到了一個之前路過的地方,就會恍然大悟,“噢!原來我回到了這個地方”。此時,將這個信息傳遞回整個地圖,配合全局優化,就可以很好地修正當前的地圖信息。回環檢測就是想辦法找到以前經過的地方。
回環檢測已經成為現代SLAM框架中非常重要的一環,特別是在大尺度地圖構建上。回環檢測如果從圖像出發,就是比較兩個圖像的相似度。這就可以利用上面介紹DBoW2庫來實現快速選取備選的回環圖像。這就是DLoopDetector庫的工作。
ORB-SLAM2中,作者利用DBoW2庫,按照自己的標准選取回環圖像,並沒有用DLoopDetector庫。具體可以參考作者文章和代碼。
DLoopDetector庫默認只輸出一幅回環圖像。如果需要多幅圖像備選,自己修改一下程序即可。
為了魯棒地選取回環圖像,DLoopDetector庫采用了如下准則。
歸一化
根據前面的定義,兩個BoW向量之間的得分定義為
因此,對於\(t\)時刻的圖像,可以找到一系列圖像和它有較高的得分,記為\(\{t_j\}\)。作者注意到,盡管\(v\)已經歸一化,\(s\)還是會受到不同圖像的特征分布的影響。作者進一步將得分歸一化為
其中\(s(v_t,v_{t-\delta t})\)為\(t\)時刻圖像與\(t-\delta t\)時刻圖像的得分。當相機旋轉很快時,分母會偏小,\(\eta\)會偏大,因此還要規定一個最小的\(s(v_t,v_{t-\delta t})\),默認值為0.005。另外,\(\eta\)也需要達到一個最小值,默認值為0.3。
ORB-SLAM2用另外一種思路做了歸一化。
最后,選取\(\eta\)最大的若干幅圖像,作為回環的備選圖像。
分組
計算得分時,相鄰兩幅備選圖像與當前圖像的得分會很接近。為了選取更具代表性的圖像,作者根據圖像id(即時間順序)對備選圖像進行分組,計算和比較組間的得分,從而避免在小時間段內重復選取。定義一組(island)的得分為
下面給出一個簡單的分組示意圖。
當一個真正的回環出現時,回環附近的圖像與當前圖像的相似度都會比較高,因此計算累積得分能更好地區分出回環圖像。
最后,選取得分最高的分組\(V_{T'}\)。
時間一致性
找到最好的分組后,還要檢查在一定時間內回環是否穩定存在。假設\(t\)時刻出現一個真正的回環,那么在接下來的一定時間內,回環應當是穩定存在的。因此,回環應該在時間上具有一致性。具體而言,\(v_{t+k\delta t}\)時刻應該也檢測出一個回環\(V_{T'_k}\),並且和\(V_{T'}\)很接近(指組內的圖像序列編號),\(k=1,\cdots,K\)。如果回環在\(K\)個時刻都滿足一致性,那么認為這是一個好的回環檢測。默認參數\(K=3\)。
幾何一致性
選定了回環圖像后,作者還檢查了兩幅圖像之間的幾何一致性。通過計算兩幅圖像之間的基本矩陣(fundamental matrix),判斷其內點數是否足夠(作者選擇的閾值是12)。如果不夠,說明兩幅圖像之間的特征匹配並不可靠,予以拒絕。作者利用之前介紹的正向索引來加速特征匹配的計算。作者也提供了直接配對和k-d tree配對的算法。
代碼分析
這里就不逐行分析代碼了,介紹一下DLoopDetector里面的參數設置。代碼位於TemplatedLoopDetector.h
。
template <class TDescriptor, class F>
void TemplatedLoopDetector<TDescriptor,F>::Parameters::set(float f)
{
/// 計算得分的參數
// 回環的圖片id應當小於當前id-dislocal
dislocal = 20 * f;
// 從data base中選出來的最大備選圖像數量
max_db_results = 50 * f;
// s(v_t,v_{t-\delta t})的最小值
min_nss_factor = 0.005;
// \alpha:\eta的最小值,default=0.3
/// 分組的參數
// 組內最少的圖像數量
min_matches_per_group = f; //
// 兩組之間最小的圖像編號差,見示意圖中的gap
max_intragroup_gap = 3 * f; //
/// 回環檢測找到備選分組時,進行時間一致性檢查的參數
// 前后兩個時刻最佳備選分組之間的時間間隔應當比較小
max_distance_between_groups = 3 * f; //
// 當前圖像id與上次一致性檢查時的圖像id的最大距離
// 前后兩次檢查之間的時間間隔應當較小
max_distance_between_queries = 2 * f;
// k: 最小entry的數目,default=3
// RANSAC 計算F矩陣的參數
min_Fpoints = 12; //
max_ransac_iterations = 500; //
ransac_probability = 0.99; //
max_reprojection_error = 2.0; //
// isGeometricallyConsistent_Flann中用到的參數
max_neighbor_ratio = 0.6; //
}
分析
基於DBoW2的方法有一些非常好的優勢
- 速度快
- 場景識別的速度很快。小尺寸的圖像可以在毫秒級別完成。
- 很多VSLAM本身就要計算特征點和特征描述,因此使用BoW方法不需要太多額外的計算時間。
- 利用詞典可以加速特征匹配,特別是在大尺度場景上。
- 擴展性好
- 庫本身並沒有限制特征的類型,不局限於圖像特征,只需要定義好distance()的計算方法。
- 可以用自定義的特征訓練視覺詞典。
- 使用方便
- 詞典可以離線訓練。作者提供了通過大量數據訓練出來的BRIEF和SIFT的詞典。
- 依賴性少
- 基本上只依賴於OpenCV和boost庫(BRIEF需要boost::dynamic_bitset),非常輕量級。
當然它也有自己的劣勢
- 作者提供的詞典基於非常豐富的場景,因此占用空間大,加載速度慢。
- DBoW2中只考慮圖像中的特征描述,丟失了特征的幾何約束。
一些comments
- 如果應用本身不需要計算特征,那要考慮額外的計算時間。比如直接法。
- 推薦像ORB-SLAM2一樣,利用DBoW2選出若干幅備選圖像,后續通過其它方法驗證。
- ORB-SLAM2利用Sim3優化來驗證回環。
- RGBD-SLAM可利用點雲匹配或者BA去驗證回環。
- 如果場景特征很少,或者重復的特征太多,效果可能不佳。
- 現在有研究者基於深度學習來識別重復場景,也是非常好的思路,准確率更高。
SLAM中的應用
可參考ORB-SLAM2在重定位、特征匹配和回環檢測中的應用。想強調的是,不要局限於作者提供的特征,結合使用場景,嘗試自定義特征和訓練詞典。