VSLAM系列原创03讲 | 为什么需要ORB特征点均匀化?

2021-12-07 15:59:01 浏览数 (1)

本文系ORB-SLAM2原理 代码实战系列原创文章,对应的视频课程见: 如何真正搞透视觉SLAM?

大家好,从今天开始我们陆续更新ORB-SLAM2/3系列的原创文章,以小白和师兄对话的形式阐述背景原理 代码解析,喜欢的点个赞分享,支持的人越多,更新越有动力!如有错误欢迎留言指正!

代码注释地址:

https://github.com/electech6/ORB_SLAM2_detailed_comments

VSLAM系列原创01讲 | 深入理解ORB关键点提取:原理 代码

VSLAM系列原创02讲 | ORB描述子如何实现旋转不变性?原理 代码

接上回继续。。。

为什么需要特征点均匀化?

小白:师兄,ORB-SLAM2 中代码里 ORB 特征点为什么没有直接调用 OpenCV 的函数呢?

师兄:OpenCV 的 ORB 特征提取方法有个问题,就是特征点往往集中在纹理丰富的区域,而缺乏特征的区域特征点数量会少很多,这会带来不好的影响。比如。。。

小白:比如这会导致一部分特征点是没有用的,本来一个特征点就可以表达清楚一个小的区域,现在在这个区域附近提取了十个特征点,那么其他九个就是冗余的。

师兄:是的,除了冗余因素外,还有一个重要的影响就是会影响位姿的解算,特征点在空间中分布的层次越多,越均匀,那么特征匹配越能精确地表达出空间的几何关系。极端来说,比如所有特征点都集中在了一个点,那么我们是无法计算出相机的位姿的。也就是说,特征点分布太过集中,会影响 SLAM 的精度。

因此,ORB-SLAM2 中采用了特征点均匀化的方法来避免特征点过于集中,我们先来看一下同一张图中 ORB-SLAM2 中 ORB 特征点提取结果和 OpenCV 提取结果的对比。

opencv-orbslam2特征点提取对比

小白:从图中看,ORB-SLAM2 均匀化效果非常明显啊,它是怎么样做的呢?

师兄:如果让你实现特征点均匀化,你有没有什么思路?

小白:额,我想想。。。我刚想到一种比较简单的方法:先根据要求提取的特征点数目,先把图像划分成许多小格子,这样得到每个小格子里需要提取的特征点数目,然后在每个小格子里单独提取,最后把这些特征点再汇聚到一起,这样可以吗?

师兄:理论上是可以,但是在实际操作过程中可能会出现一些问题:

  • 很难达到要求的特征点数量。比如某个小格子是在弱纹理区域,那么这个小区域内可能提取到的有效的特征点数目会达不到要求,这样最后整张图像上提取的特征点总数就达不到我们要求的数量。
  • 每个小格子是独立的不相关的,这样可能会出现“鸡头”不如“凤尾”的情况,也就是某个小格子里提取的最好的特征点质量比其他小格子里最差的还要差。

小白:那 ORB-SLAM2 里是怎么做的呢?

师兄:其实基本思想和你的差不多,只不过是优化了流程,加入了四叉树的方法来实现的。下面具体来看一下步骤:

  • 第1步:根据总的图像金字塔层数和待提取的特征点总数,计算每一层图像金字塔中需要提取的特征点数量。
  • 第2步:划分格子,ORB-SLAM2 中格子固定尺寸为 。
  • 第3步:对每个格子提取 FAST 角点,如果初始的 FAST 阈值没有检测到角点,就降低 FAST 阈值。这样可以在弱纹理区域也能提取到更多的角点。如果降低一次阈值后,还是提取不到角点,则不再这个格子里提取。这样可以避免提取到质量特别差的角点。
  • 第4步:使用四叉树来均匀的选取 FAST 角点,直到达到特征点总数。

下面分别详细介绍。

如何给金字塔分配特征点数量?

师兄:图像金字塔层数越高,对应层数的图像分辨率越低,面积(高 宽)越小,所能提取到的特征点数量就越少。所以分配策略就是根据图像的面积来定,将总特征点数目根据面积比例均摊到每层图像上。

我们假设需要提取的特征点数目为 ,金字塔总共有 层,第 0 层图像的宽为 ,高为 ,对应的面积 ,图像金字塔缩放因子为 ,,在 ORB-SLAM2 中, 。

那么整个金字塔总的图像面积是:

单位面积应该分配的特征点数量为:

第 0 层应该分配的特征点数量为:

第 i 层应该分配的特征点数量为:

在ORB-SLAM2 的代码里,不是按照面积均摊的,而是按照面积的开方来均摊特征点的,也就是将上述公式中的 换成 即可。

这部分代码实现见:

代码语言:javascript复制
ORBextractor::ORBextractor(int _nfeatures,  //指定要提取的特征点数目
         float _scaleFactor, //指定图像金字塔的缩放系数
         int _nlevels,  //指定图像金字塔的层数
         int _iniThFAST,  //指定初始的FAST角点阈值,可以提取出最明显的角点
         int _minThFAST):  //如果初始阈值没有检测到角点,降低到这个阈值提取出弱一点的角点
    nfeatures(_nfeatures), scaleFactor(_scaleFactor), nlevels(_nlevels),
    iniThFAST(_iniThFAST), minThFAST(_minThFAST)//设置这些参数
{
 //存储每层图像缩放系数的vector调整为符合图层数目的大小
    mvScaleFactor.resize(nlevels);  
 //存储这个sigma^2,其实就是每层图像相对初始图像缩放因子的平方
    mvLevelSigma2.resize(nlevels);
 //对于初始图像,这两个参数都是1
    mvScaleFactor[0]=1.0f;
    mvLevelSigma2[0]=1.0f;
 //然后逐层计算图像金字塔中图像相当于初始图像的缩放系数 
    for(int i=1; i<nlevels; i  )  
    {
  //其实就是这样累乘计算得出来的
        mvScaleFactor[i]=mvScaleFactor[i-1]*scaleFactor;
  //原来这里的sigma^2就是每层图像相对于初始图像缩放因子的平方
        mvLevelSigma2[i]=mvScaleFactor[i]*mvScaleFactor[i];
    }

    //接下来的两个向量保存上面的参数的倒数
    mvInvScaleFactor.resize(nlevels);
    mvInvLevelSigma2.resize(nlevels);
    for(int i=0; i<nlevels; i  )
    {
        mvInvScaleFactor[i]=1.0f/mvScaleFactor[i];
        mvInvLevelSigma2[i]=1.0f/mvLevelSigma2[i];
    }

    //调整图像金字塔vector以使得其符合设定的图像层数
    mvImagePyramid.resize(nlevels);

 //每层需要提取出来的特征点个数,这个向量也要根据图像金字塔设定的层数进行调整
    mnFeaturesPerLevel.resize(nlevels);
 
 //图片降采样缩放系数的倒数
    float factor = 1.0f / scaleFactor;
 //第0层图像应该分配的特征点数量
    float nDesiredFeaturesPerScale = nfeatures*(1 - factor)/(1 - (float)pow((double)factor, (double)nlevels));

 //特征点的累计计数清空
    int sumFeatures = 0;
 //开始逐层计算要分配的特征点个数,顶层图像除外(看循环后面)
    for( int level = 0; level < nlevels-1; level   )
    {
  //分配 cvRound : 返回个参数最接近的整数值
        mnFeaturesPerLevel[level] = cvRound(nDesiredFeaturesPerScale);
  //累计
        sumFeatures  = mnFeaturesPerLevel[level];
  //乘系数
        nDesiredFeaturesPerScale *= factor;
    }
    //由于前面的特征点个数取整,可能会导致剩余一些特征点个数没有被分配,这里将多的特征点分配到最高的图层中
    mnFeaturesPerLevel[nlevels-1] = std::max(nfeatures - sumFeatures, 0);

 // ...
}

(左右滑动看完整代码)

参考:https://zhuanlan.zhihu.com/p/61738607

0 人点赞