DBoW2庫介紹


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分別定義為

\[\text{IDF}_k=\log\left(\frac{\text{number of all images}}{\text{number of images reach k-th leaf node}}\right) \]

\[\text{TF}_k=\frac{\text{number of features locates in leaf node k}}{\text{number of all features}} \]

\[\text{TF-IDF}_k=\text{TF}_k*\text{IDF}_k \]

視覺詞典可以通過離線訓練大量數據得到。訓練中只計算和保存單詞的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)\)

\[s(v_1,v_2)=1-\frac{1}{2}\left |\frac{v_1}{|v_1|}-\frac{v_2}{|v_2|}\right| \]

有了距離定義,即可根據距離大小選取合適的備選圖像。

正向索引與反向索引

在視覺詞典之上,作者還加入了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向量之間的得分定義為

\[s(v_1,v_2)=1-\frac{1}{2}\left |\frac{v_1}{|v_1|}-\frac{v_2}{|v_2|}\right| \]

因此,對於\(t\)時刻的圖像,可以找到一系列圖像和它有較高的得分,記為\(\{t_j\}\)。作者注意到,盡管\(v\)已經歸一化,\(s\)還是會受到不同圖像的特征分布的影響。作者進一步將得分歸一化為

\[\eta(v_t,v_j)=\frac{s(v_t,v_{t_j})}{s(v_t,v_{t-\delta t})} \]

其中\(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)的得分為

\[H(v_t,V_{T_i})=\sum\limits_{j=n_i}^{m_i}\eta(v_t,v_{t_j}) \]

下面給出一個簡單的分組示意圖。

image

當一個真正的回環出現時,回環附近的圖像與當前圖像的相似度都會比較高,因此計算累積得分能更好地區分出回環圖像。
最后,選取得分最高的分組\(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在重定位特征匹配回環檢測中的應用。想強調的是,不要局限於作者提供的特征,結合使用場景,嘗試自定義特征和訓練詞典。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM