Oriented Fast神奇高效的代码实现方式——ORBSLAM2源码讲解(二)


前言

本博客结合哔哩大学视频ORBSLAM2【ORBSLAM2源码讲解专题一】ORB特征点提取与均匀化策略和高翔的《视觉SLAM十四讲》总结。
代码参照github的ORB_SLAM2_detailed_comments

ORBSLAM2代码很经典,而且代码量大,会分成多个博客研究,下为slam专题的其他链接:
代码要求环境做了总结,解决了配置中的一些常见问题:slam的环境配置大全–保姆教学
代码如何运行记录:ORB_SLAM2代码的简介安装运行
有篇博客和本博客内容一样,他写的更详细:orb_slam代码解析(2)Tracking线程


一、基础知识

特征点法:我们需要从图像中选取比较有代表性的点,这些点在相机发生少量视角变化时会保持不变,在此基础上讨论相机位姿估计问题,在经典的SLAM模型中我们称他们为路标,在视觉SLAM中路标则是图像特征(feature)。

特征点: 特征点是图像里一些特别的地方,我们可以把图像中的角点、边缘和区块都当成图像中的代表性的地方。为此,计算机视觉领域研究出了很多特征点的提取方法,如著名的SIFT、SURF、ORB,这些人工设计的特征点有一下特点:

  • 可重复性:形同的“区域”可以在不同的图像中找到。
  • 可区别性:不同的“区域”有不同的表达。
  • 高效率:同一图像中,特征点的数量远小于像素的数量。
  • 本地性:特征仅与一小片图像区域有关。

关键点和描述子: 特征点由关键点(Key-point)和描述子(Descriptor)两部分组成,比如当要谈论SIFT特征时,是指“提取SIFT关键点,并计算SIFT描述子”两件事情。关键点是指该特征点在图像里的位置,有些特征点还具有朝向、大小等信息。描述子通常是一个向量,按照认为的方式描述该点周围像素的信息。描述子是按照“外观相似的特征应该有相似的描述子”的原则设计的。

ORB特征: 不同的图像特征特点不同。例如SIFT充分考虑了图像变换中的关照、尺度等变化,很精确但计算量很大,普通的CPU无法实时计算SIFT特征,进行定位建图。ORB适当的降低了精度和健壮性以提升计算速度,在同一图像中提取1000个特征点,ORB要15.3ms,SURF要217.3ms,SIFT约花费5228.7ms。ORB特征也是由关键点和描述子组成,他的关键点称为“Oriented FAST”,Oriented FAST的详细描述为本篇博客主要内容。他的描述子称为BRIEF(Binary Robust Independent Elementary Feature)。因此,提取ORB特征分为如下两个步骤:

  • FAST角点提取:找出图像中的“角点”。相较于原版的FAST,ORB中计算了特征点的主方向,为后续的BRIEF描述子增加了旋转不变的特性。他的思想是,如果一个像素与邻域的像素差别较大,那么他很可能是角点。

  • BRIEF描述子:对前一步提取出特征点的周围图像区域进行描述。

Oriented FAST的详细描述为本篇博客主要内容

二、灰度质心法原理

P为几何中心,Q为灰度质心
在一个圆内计算灰度质心(思考:为什么是圆?不是正方形?)
ORB通过灰度质心法来找Oriented FAST,为什么要画一个圆行区域的像素, 因为圆具有旋转不变性。每个特征点都会找灰度置信,每次要旋转到PQ为x轴上,方形无法旋转。但圆形区域就需要一些算法来实现。

在这里插入图片描述
具体计算方法为公式,我也看不懂。
在这里插入图片描述
目前ORB提取的代码都在ORBextractor.cc里,里面有很对函数,我们先看ORBextractor的构造函数,其代码注释如下:

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);

	//成员变量pattern的长度,也就是点的个数,这里的512表示512个点(上面的数组中是存储的坐标所以是256*2*2)
    const int npoints = 512;
	//获取用于计算BRIEF描述子的随机采样点点集头指针
	//注意到pattern0数据类型为Points*,bit_pattern_31_是int[]型,所以这里需要进行强制类型转换
    const Point* pattern0 = (const Point*)bit_pattern_31_;	
	//使用std::back_inserter的目的是可以快覆盖掉这个容器pattern之前的数据
	//其实这里的操作就是,将在全局变量区域的、int格式的随机采样点以cv::point格式复制到当前类对象中的成员变量中
    std::copy(pattern0, pattern0 + npoints, std::back_inserter(pattern));

    //This is for orientation
	//下面的内容是和特征点的旋转计算有关的
    // pre-compute the end of a row in a circular patch
	//预先计算圆形patch中行的结束位置
	//+1中的1表示那个圆的中间行
    umax.resize(HALF_PATCH_SIZE + 1);
	
	//cvFloor返回不大于参数的最大整数值,cvCeil返回不小于参数的最小整数值,cvRound则是四舍五入
    int v,		//循环辅助变量
		v0,		//辅助变量
		vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1);	//计算圆的最大行号,+1应该是把中间行也给考虑进去了
				//NOTICE 注意这里的最大行号指的是计算的时候的最大行号,此行的和圆的角点在45°圆心角的一边上,之所以这样选择
				//是因为圆周上的对称特性
				
	//这里的二分之根2就是对应那个45°圆心角
    
    int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);
	//半径的平方
    const double hp2 = HALF_PATCH_SIZE*HALF_PATCH_SIZE;

	//利用圆的方程计算每行像素的u坐标边界(max)
    for (v = 0; v <= vmax; ++v)
        umax[v] = cvRound(sqrt(hp2 - v * v));		//结果都是大于0的结果,表示x坐标在这一行的边界

    // Make sure we are symmetric
	//这里其实是使用了对称的方式计算上四分之一的圆周上的umax,目的也是为了保持严格的对称(如果按照常规的想法做,由于cvRound就会很容易出现不对称的情况,
	//同时这些随机采样的特征点集也不能够满足旋转之后的采样不变性了)
	for (v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v)
    {
        while (umax[v0] == umax[v0 + 1])
            ++v0;
        umax[v] = v0;
        ++v0;
    }
}

三、UMAX

上面代码的后半部分在算umax, umax在后面算方向很重要。umax是四分之一圆的每一行的u轴坐标边界。如下如的右上四分之一圆,横轴为u,竖轴为v,点在圆上滑动,其中横坐标到原点的距离就是umax,对应着图上三个点的AC、AH、AD距离是三个点的umax,E点的umax是0.

在这里插入图片描述

四、IC_Angle如何做加速运算

要计算特征点的角度,我们要在一个圆域中算出m10和m01,计算步骤是先算出中间红线的m10,然后在平行于x轴算出m10和m01,一次计算相当于图像中的同个颜色的两个line。先算出中间红线的m10,然后在平行于x轴算出m10和m01,一次计算 相当于图像中的同个颜色的两个line。
在这里插入图片描述
其代码注释如下:

static float IC_Angle(const Mat& image, Point2f pt,  const vector<int> & u_max)
{
	//图像的矩,前者是按照图像块的y坐标加权,后者是按照图像块的x坐标加权
    int m_01 = 0, m_10 = 0;

	//获得这个特征点所在的图像块的中心点坐标灰度值的指针center
    const uchar* center = &image.at<uchar> (cvRound(pt.y), cvRound(pt.x));

    // Treat the center line differently, v=0
	//这条v=0中心线的计算需要特殊对待
    //后面是以中心行为对称轴,成对遍历行数,所以PATCH_SIZE必须是奇数
    for (int u = -HALF_PATCH_SIZE; u <= HALF_PATCH_SIZE; ++u)
		//注意这里的center下标u可以是负的!中心水平线上的像素按x坐标(也就是u坐标)加权
        m_10 += u * center[u];

    // Go line by line in the circular patch  
	//这里的step1表示这个图像一行包含的字节总数。参考[https://blog.csdn.net/qianqing13579/article/details/45318279]
    int step = (int)image.step1();
	//注意这里是以v=0中心线为对称轴,然后对称地每成对的两行之间进行遍历,这样处理加快了计算速度
    for (int v = 1; v <= HALF_PATCH_SIZE; ++v)
    {
        // Proceed over the two lines
		//本来m_01应该是一列一列地计算的,但是由于对称以及坐标x,y正负的原因,可以一次计算两行
        int v_sum = 0;
		// 获取某行像素横坐标的最大范围,注意这里的图像块是圆形的!
        int d = u_max[v];
		//在坐标范围内挨个像素遍历,实际是一次遍历2个
        // 假设每次处理的两个点坐标,中心线下方为(x,y),中心线上方为(x,-y) 
        // 对于某次待处理的两个点:m_10 = Σ x*I(x,y) =  x*I(x,y) + x*I(x,-y) = x*(I(x,y) + I(x,-y))
        // 对于某次待处理的两个点:m_01 = Σ y*I(x,y) =  y*I(x,y) - y*I(x,-y) = y*(I(x,y) - I(x,-y))
        for (int u = -d; u <= d; ++u)
        {
			//得到需要进行加运算和减运算的像素灰度值
			//val_plus:在中心线下方x=u时的的像素灰度值
            //val_minus:在中心线上方x=u时的像素灰度值
            int val_plus = center[u + v*step], val_minus = center[u - v*step];
			//在v(y轴)上,2行所有像素灰度值之差
            v_sum += (val_plus - val_minus);
			//u轴(也就是x轴)方向上用u坐标加权和(u坐标也有正负符号),相当于同时计算两行
            m_10 += u * (val_plus + val_minus);
        }
        //将这一行上的和按照y坐标加权
        m_01 += v * v_sum;
    }

    //为了加快速度还使用了fastAtan2()函数,输出为[0,360)角度,精度为0.3°
    return fastAtan2((float)m_01, (float)m_10);
}

总结

提示:这里对文章进行总结:例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值