(01)ORB-SLAM2源码无死角解析-(31) ORB特征匹配→词袋BoW:BRIEF描述子转BoW向量

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解的(01)ORB-SLAM2源码无死角解析链接如下(本文内容来自计算机视觉life ORB-SLAM2 课程课件):
(01)ORB-SLAM2源码无死角解析-(00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/123092196
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

通过上一篇博客,已经对 BoW 进行了理论讲解。其中有一个重要的图示:
在这里插入图片描述
假设上述为构建好的 BoW词袋,那么如何把一个特征点的 BRIEF 转行成 BoW向量呢? src/Frame.cc 文件,找到函数 void Frame::ComputeBoW()。可以看到如下代码:

		// 将特征点的描述子转换成词袋向量mBowVec以及特征向量mFeatVec
        mpORBvocabulary->transform(vCurrentDesc,	//当前的描述子vector
								   mBowVec,			//输出,词袋向量,记录的是单词的id及其对应权重TF-IDF值
								   mFeatVec,		//输出,记录node id及其对应的图像 feature对应的索引
								   4);				//4表示从叶节点向前数的层数

该函数的功能,就是把一幅图所有的描述子转换成 BoW向量,也就是把 vCurrentDesc 转换成 mBowVec 以及 mFeatVec。这里先介绍以下 mBowVec 与 mFeatVec 代表什么.

DBoW2::BowVector mBowVec: 其可以存储多个元素,每个元素包含了两个值: 叶子节点 id,其对应的权重
DBoW2::FeatureVector mFeatVec: 其可以存储多个元素,每个元素包含了两个值: 第一个值为叶子节点(word)所属节点 id(注意,并非父节点id),其与参数levelsup相关;第二个值为特征点,或者说 BRIEF 描述子的索引。

转换流程 其上图中紫色的箭头线,就是转换的流程,首先输入的 BRIEF描述子 与根节点的所有之节点的 BRIEF描述子 描述子进行比较,也就是与图像的 Level1 的节点进行比较。这里注意一个点: 除了叶子(word)节点,其于节点的 BRIEF描述子 都是为其子节点的平均值。通过汉明距离比较之后,找到 Level1 中与输入 BRIEF描述子 最近的节点 称为 node1。那么下一步就是与 best_node1节点所有子节点进行比较,从中再找到汉明距离最近的节点 best_node2,这样依次循环,直到寻找到最匹配的叶子节 best_word4 点为止,找到了叶子节点,就知道叶子节点的 id 与 其对应的权重,就能构建一个元素存储于 mBowVec 之中。

如果如上图所示,levelsup 参数为3,那么 mFeatVec 元素的第一个值,就是 best_node1节点的 id; 第二个值就是输入 BRIEF 描述子对应特征点在整张图像中的索引。因为 levelsup=3,所以从叶子节点往上数3级,级为其所属节点。对应于源码中变量 nid(后面有源码讲解) 。

 

二、源码转换过程

下面来说说源码的转换流程,其实总的来说与上面的流程差不多,这里再详细的梳理一下:
( 1 ) : \color{blue}{(1)}: (1)根据权重的计算方式,运行不同的代码,主要存在四种计算权重的方式,分别为 TF_IDF,TF,IDF,BINARY。
( 2 ) : \color{blue}{(2)}: (2)取出根节点的所有子节点,赋值给 nodes,计算输入BRIEF描述子与第一个子节点,即 nodes[0] 的汉明距离。再循环与其与子节点的距离,找到最距离最近的一个节点。然后再取出该节点的所有子节点,赋值给 nodes,重复前面的流程。
( 3 ) : \color{blue}{(3)}: (3)在步骤(2)的循环过程中,会对当前汉明距离最近节点进行判断,如果其为倒数 levelsup 层级,则记录下该节点 id,对应于代码中的 nid。
( 4 ) : \color{blue}{(4)}: (4)当跳出代码中的do while 循环,则表示已经找到最匹配的叶子节点。
( 5 ) : \color{blue}{(5)}: (5)通过前面的步骤,已经获得了输入BRIEF描述子对应的子节点,以及所属节点的id,与之对应的权重,再进一步把这些信息添加到 mBowVec 与 mFeatVec 之中。对应于源码如下:

        // 如果Word 权重大于0,将其添加到BowVector 和 FeatureVector
        v.addWeight(id, w);
        fv.addFeature(nid, i_feature);

这里大家注意一点,就是再添加的时候,其会对节点id进行排序,每次会找到合适的位置插入。

 

三、源码注释

该源码位于在 src/Frame.cc 中被调用,调用源码如下:

/**
 * @brief 计算当前帧特征点对应的词袋Bow,主要是mBowVec 和 mFeatVec
 * 
 */
void Frame::ComputeBoW()
{
	
    // 判断是否以前已经计算过了,计算过了就跳过
    if(mBowVec.empty())
    {
		// 将描述子mDescriptors转换为DBOW要求的输入格式
        vector<cv::Mat> vCurrentDesc = Converter::toDescriptorVector(mDescriptors);
		// 将特征点的描述子转换成词袋向量mBowVec以及特征向量mFeatVec
        mpORBvocabulary->transform(vCurrentDesc,	//当前的描述子vector
								   mBowVec,			//输出,词袋向量,记录的是单词的id及其对应权重TF-IDF值
								   mFeatVec,		//输出,记录node id及其对应的图像 feature对应的索引
								   4);				//4表示从叶节点向前数的层数
    }
}

其上的 transform 实现于 src/Thirdpaty/DBoW2/DBoW2/TemplatedVocabulary.h 之中:


// --------------------------------------------------------------------------
/**
 * @brief 将描述子转化为Word id, Word weight,节点所属的父节点id(这里的父节点不是叶子的上一层,它距离叶子深度为levelsup)
 * 
 * @tparam TDescriptor            
 * @tparam F 
 * @param[in] feature                 特征描述子
 * @param[in & out] word_id           Word id
 * @param[in & out] weight            Word 权重
 * @param[in & out] nid               记录当前描述子转化为Word后所属的 node id,它距离叶子深度为levelsup
 * @param[in] levelsup                距离叶子的深度
 */
template<class TDescriptor, class F>
void TemplatedVocabulary<TDescriptor,F>::transform(const TDescriptor &feature, 
  WordId &word_id, WordValue &weight, NodeId *nid, int levelsup) const
{ 
  // propagate the feature down the tree
  vector<NodeId> nodes;
  typename vector<NodeId>::const_iterator nit;

  // level at which the node must be stored in nid, if given
  // m_L: depth levels, m_L = 6 in ORB-SLAM2
  // nid_level 当前特征点转化为的Word 所属的 node id,方便索引
  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;
    // 取出当前节点所有子节点的id
    nodes = m_nodes[final_id].children;
    // 取子节点中第1个的id,用于后面距离比较的初始值
    final_id = nodes[0];

    // 取当前节点第一个子节点的描述子距离初始化最佳(小)距离
    double best_d = F::distance(feature, m_nodes[final_id].descriptor);
    // 遍历nodes中所有的描述子,找到最小距离对应的描述子
    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;
      }
    }
    
    // 记录当前描述子转化为Word后所属的 node id,它距离叶子深度为levelsup
    if(nid != NULL && current_level == nid_level)
      *nid = final_id;
    
  } while( !m_nodes[final_id].isLeaf() );

  // turn node id into word id
  // 取出 vocabulary tree中node距离当前feature 描述子距离最小的那个node的 Word id 和 weight
  word_id = m_nodes[final_id].word_id;
  weight = m_nodes[final_id].weight;
}


// --------------------------------------------------------------------------
/**
 * @brief 将一幅图像所有的特征点转化为BowVector和FeatureVector
 * 
 * @tparam TDescriptor 
 * @tparam F 
 * @param[in] features      图像中所有的特征点
 * @param[in & out] v       BowVector
 * @param[in & out] fv      FeatureVector
 * @param[in] levelsup      距离叶子的深度
 */
template<class TDescriptor, class F> 
void TemplatedVocabulary<TDescriptor,F>::transform(
  const std::vector<TDescriptor>& features,
  BowVector &v, FeatureVector &fv, int levelsup) const
{
  v.clear();
  fv.clear();
  
  if(empty()) // safe for subclasses
  {
    return;
  }
  
  // normalize 
  // 根据选择的评分类型来确定是否需要将BowVector 归一化
  LNorm norm;
  bool must = m_scoring_object->mustNormalize(norm);
  
  typename vector<TDescriptor>::const_iterator fit;
  
  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;        // 叶子节点的Word id
      NodeId nid;       // FeatureVector 里的NodeId,用于加速搜索
      WordValue w;      // 叶子节点Word对应的权重

      //  将当前描述子转化为Word id, Word weight,节点所属的父节点id(这里的父节点不是叶子的上一层,它距离叶子深度为levelsup)
      // w is the idf value if TF_IDF, 1 if TF 
      transform(*fit, id, w, &nid, levelsup);
      
      if(w > 0) // not stopped
      { 
        // 如果Word 权重大于0,将其添加到BowVector 和 FeatureVector
        v.addWeight(id, w);
        fv.addFeature(nid, i_feature);
      }
    }
    
    if(!v.empty() && !must)
    {
      // unnecessary when normalizing
      const double nd = v.size();
      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;
      // w is idf if IDF, or 1 if BINARY
      transform(*fit, id, w, &nid, levelsup);
      
      if(w > 0) // not stopped
      {
        v.addIfNotExist(id, w);
        fv.addFeature(nid, i_feature);
      }
    }
  } // if m_weighting == ...
  
  if(must) v.normalize(norm);
}

 

四、结语

通过该片博客,了解了 BRIEF描述子 如果通过 词袋BoW 转换成 BoW向量。既然了解了 BoW向量 的来源,那么下面是探讨如何对其进行利用了。

 
 
本文内容来自计算机视觉life ORB-SLAM2 课程课件

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
本文主要介绍ORB-SLAM2中的一些关键实现细节,包括词袋建立、关键帧选择策略、词袋检索和位姿估计。此外还详细介绍了视觉里程计、闭环检测、地图维护等模块的实现细节。 首先,ORB-SLAM2通过建立词袋的方式实现了特征点的高效匹配。ORB-SLAM2采用了二叉树的结构生成了一个层次化的词袋,该词袋可以快速地检索到最相似的词,并将该词作为当前帧所属的类别。在后续的帧匹配过程中,ORB-SLAM2只需要搜索与当前帧类别相同的关键帧中的点即可,大大加快了匹配的效率。 其次,ORB-SLAM2采用了一种称为“闭线性三角测量”的方式估计位姿。该方法将两个视角下的匹配点转化为视差向量,并通过求解一组线性方程组来估计相邻帧之间的相对位姿。同时,该方法还能有效地处理重复区域和遮挡等问题,具有较高的鲁棒性。 此外,在关键帧选择方面,ORB-SLAM2采用了一种基于路标点的策略,即当当前帧与地图中的路标点距离较远时,就将当前帧作为新的关键帧。这种策略可以确保全局地图的均匀性和关键帧的稠密性。 最后,ORB-SLAM2采用了基于基础矩阵的闭环检测方法,该方法可以在时间和空间复杂度上达到较好的平衡。同时,ORB-SLAM2还采用了一种优化地图点云的方式,通过通过图优化的方式优化地图中的点云位置,确保了地图的准确性和一致性。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值