zoukankan      html  css  js  c++  java
  • 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分别定义为

    [ ext{IDF}_k=logleft(frac{ ext{number of all images}}{ ext{number of images reach k-th leaf node}} ight) ]

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

    [ ext{TF-IDF}_k= ext{TF}_k* ext{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 imes 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|} ight| ]

    有了距离定义,即可根据距离大小选取合适的备选图像。

    正向索引与反向索引

    在视觉词典之上,作者还加入了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(Nlog 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|} ight| ]

    因此,对于(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})=sumlimits_{j=n_i}^{m_i}eta(v_t,v_{t_j}) ]

    下面给出一个简单的分组示意图。

    image

    当一个真正的回环出现时,回环附近的图像与当前图像的相似度都会比较高,因此计算累积得分能更好地区分出回环图像。
    最后,选取得分最高的分组(V_{T'})

    时间一致性

    找到最好的分组后,还要检查在一定时间内回环是否稳定存在。假设(t)时刻出现一个真正的回环,那么在接下来的一定时间内,回环应当是稳定存在的。因此,回环应该在时间上具有一致性。具体而言,(v_{t+kdelta 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在重定位特征匹配回环检测中的应用。想强调的是,不要局限于作者提供的特征,结合使用场景,尝试自定义特征和训练词典。

  • 相关阅读:
    Swift入门篇-Hello World
    Swift入门篇-swift简介
    Minecraft 插件 world edit 的cs 命令
    搭建本地MAVEN NEXUS 服务
    MC java 远程调试 plugin 开发
    企业内部从零开始安装docker hadoop 提纲
    javascript 命令方式 测试例子
    ca des key crt scr
    JSF 抽象和实现例子 (函数和属性)
    form 上传 html 代码
  • 原文地址:https://www.cnblogs.com/luyb/p/6033196.html
Copyright © 2011-2022 走看看