02 特征点提取器 ORBextractor

02 特征点提取器 ORBextractor

2.0 基础知识

2.0.1 图像金字塔

(1)尺度不变性:一幅图片中的某个物体的大小永远是那么大(设置一个标准尺度来度量),而不是因为它所在图片的放大缩小而改变。这就是尺度不变性。

(2)图像金字塔
在这里插入图片描述

将原始图像按照 1:1.2 的比例依次缩小,得到 n 幅图像,形如金字塔。观察这 n 幅图像,我们似乎离场景越来越远,这样就用一幅图像制造出了深度变化的效果。

在 SLAM 中,我们对这 n 张图像分别提取特征点,就相当于得到了不同距离的特征,涵盖了更多的尺度。试想一下,假设第一次我们在 10m 处获取一幅图像,经过图像金字塔,相当于得到了 10 1.2 \frac{10}{1.2} 1.210 10 1. 2 2 \frac{10}{1.2^2} 1.2210 10 1. 2 3 \frac{10}{1.2^3} 1.2310… 等处共 n 个不同距离的图像;当我们下次再处于不同距离拍摄该处场景时,就有更多的图像与特征点进行匹配。这样就解决了尺度导致的特征点匹配问题。

参考:https://blog.csdn.net/u011341856/article/details/103707313

2.0.2 ORB 特征点的关键点和描述子

ORB 的关键点是在 FAST 关键点基础上进行了改进,主要是增加了特征点的主方向,称之为 Oriented FAST。

描述子是在 BRIEF 描述子基础上加入了上述方向信息,称之为 Rotated BRIEF。

2.1 构造函数:ORBextractor()

构造函数原型

ORBextractor::ORBextractor(int _nfeatures, float _scaleFactor, int _nlevels, int _iniThFAST, int _minThFAST)

(1)从 yaml 文件中读取参数(以 KITTI00-02.yaml 为例)

成员变量意义在yaml中变量名
int _nfeatures期望提取的特征点个数ORBextractor.nFeatures2000
float _scaleFactor金字塔相邻层级缩放因子ORBextractor.scaleFactor1.2
int _nlevels金字塔层数ORBextractor.nLevels8
int _iniThFAST提取 FAST 特征点的默认阈值ORBextractor.iniThFAST20
int _minThFAST如果使用默认阈值提取不到特征点则使用最小阈值再次提取ORBextractor.minThFAST7

根据上述变量的值计算出下述成员变量:

变量意义
std::vector<float> mvScaleFactor各层级缩放系数{1, 1.2, 1.44, 1.728, 2.074, 2.488, 2.986, 3.583
std::vector<float> mvInvScaleFactor各层级缩放系数的倒数{1, 0.833, 0.694, 0.579, 0.482, 0.402, 0.335, 0.2791}
std::vector<float> mvLevelSigma2各层级缩放系数的平方{1, 1.44, 2.074, 2.986, 4.300, 6.190, 8.916, 12.838}
std::vector<float> mvInvLevelSigma2各层级缩放系数的平方的倒数{1, 0.694, 0.482, 0.335, 0.233, 0.162, 0.112, 0.078}
std::vector<int> mnFeaturesPerLevel每一层期望提取的特征点个数(正比于图层边长,总和为 nfeatures{122, 146, 174, 210, 252, 302, 362, 432}

(2)初始化用于计算描述子的 pattern 变量,也就是用于计算描述子的 256 对坐标

static int bit_pattern_31_[256*4] =
{
    8,-3, 9,5/*mean (0), correlation (0)*/,
    4,2, 7,-12/*mean (1.12461e-05), correlation (0.0437584)*/,
    -11,9, -8,2/*mean (3.37382e-05), correlation (0.0617409)*/,
    ...
}

共 256 行,每一行表示一对坐标点,如第一行为 (8, -3)(9, 5)

(3) 在提取 Oriented FAST 关键点后,还需要计算每个点的描述子。即以关键点为圆心,在半径为 16 的圆的范围内,计算特征点主方向和描述子。

在这里插入图片描述

(注:图片中半径为 8,仅作示意)

成员变量 std::vector<int> umax 中存储的是逼近圆的第一象限内 1 4 \frac{1}{4} 41 圆周上每个 v 坐标对应的 u 坐标。为保证严格对称性,先计算下 45° 圆周上点的坐标,再根据对称性补全上 45° 圆周上点的坐标。

int vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1); 	// 45°射线与圆周交点的纵坐标
int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);			// 45°射线与圆周交点的纵坐标

// 先计算下半45度的umax
for (int v = 0; v <= vmax; ++v) {
	umax[v] = cvRound(sqrt(15 * 15 - v * v));	
}

// 根据对称性补出上半45度的umax
for (int v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v) {
    while (umax[v0] == umax[v0 + 1])
        ++v0;
    umax[v] = v0;
    ++v0;
}

2.2 构建图像金字塔 ComputePyramid()

变量访问控制意义
std::vector<cv::Mat> mvImagePyramidpublic存储图像金字塔每层的图像
const int EDGE_THRESHOLD全局变量为计算描述子和提取特征点补的 padding 厚度

函数原型

void ORBextractor::ComputePyramid(cv::Mat image)
{
	//开始遍历所有的图层,levels是yaml文件里面的
    for (int level = 0; level < nlevels; ++level)
    {
		//获取本层图像的缩放系数,mvInvScaleFactor[level]是从orbextrator得到的
        float scale = mvInvScaleFactor[level];
		//计算本层图像的像素尺寸大小
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
		//全尺寸图像。包括无效图像区域的大小。将图像进行“补边”,EDGE_THRESHOLD区域外的图像不进行FAST角点检测
        Size wholeSize(sz.width + EDGE_THRESHOLD*2, sz.height + EDGE_THRESHOLD*2);
		// temp是扩展了边界的图像,是一个构造函数,拷贝了wholeSize的图像
        Mat temp(wholeSize, image.type()), masktemp;
        // mvImagePyramid 刚开始时是个...空的vector<Mat>
		// 将扩充后的图像拷贝给mvImagePyramid容器
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));

        // Compute the resized image
		//计算第0层以上resize后的图像
        if( level != 0 )
        {
			//将上一层金字塔图像根据前文设定sz缩放到当前层级
            resize(mvImagePyramid[level-1],	//输入图像
				   mvImagePyramid[level], 	//输出图像
				   sz, 						//输出图像的尺寸
				   0, 						//水平方向上的缩放系数,留0表示自动计算
				   0,  						//垂直方向上的缩放系数,留0表示自动计算
				   cv::INTER_LINEAR);		//图像缩放的差值算法类型,这里的是线性插值算法


			//把源图像拷贝到目的图像的中央,四面填充指定的像素。图片如果已经拷贝到中间,只填充边界
			//这样做是为了能够正确提取边界的FAST角点
			//EDGE_THRESHOLD指的这个边界的宽度,由于这个边界之外的像素不是原图像素而是算法生成出来的,所以不能够在EDGE_THRESHOLD之外提取特征点			
            copyMakeBorder(mvImagePyramid[level], 					//源图像
						   temp, 									//目标图像(此时其实就已经有大了一圈的尺寸了)
						   EDGE_THRESHOLD, EDGE_THRESHOLD, 			//top & bottom 需要扩展的border大小
						   EDGE_THRESHOLD, EDGE_THRESHOLD,			//left & right 需要扩展的border大小
                           BORDER_REFLECT_101+BORDER_ISOLATED);     //扩充方式,opencv给出的解释:						
        }
        else
        {
			//对于第0层未缩放图像,直接将图像深拷贝到temp的中间,并且对其周围进行边界扩展。此时temp就是对原图扩展后的图像
            copyMakeBorder(image,			//这里是原图像
						   temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
                           BORDER_REFLECT_101);            
        }
    }
}

包括两步:

  • 将图像缩放到 mvInvScaleFactor 对应尺寸;

  • 在图像四周补一圈厚度为 EDGE_THRESHOLD 的 padding(提取 FAST 特征点需要特征点周围半径为 3 的圆域,计算描述子需要特征点周围半径为 16 的圆域),copyMakeBorder() 函数实现。

在这里插入图片描述

  • 深灰色 为缩放后的原始图像;

  • 包含绿色边界在内的矩形 用于提取 FAST 特征点;

  • 包含浅灰色边界在内的整个矩形 用于计算 ORB 描述子。

为什么要扩充图像边界呢?

利用 FAST 算法在提取特征点时,图像边缘的特征点半径为3的圆无法取到(边界外无像素点),为了解决此问题,我们对图像边界进行填充。

参考:https://www.pudn.com/news/62f50f18f97302478e3581fc.html

2.3 提取特征点并筛选 ComputeKeyPointsOctTree()

在这里插入图片描述

我们希望 特征点均匀地分布在图像的所有部分。所以在提取时会将图片分成 30*30(单位像素)的一个一个小格子(cell)来提取特征点。并且在提取 FAST 角点时,我们设计了两个阈值 _iniThFAST_minThFAST,这样在每个 cell 中,就可以根据实际情况进行调整,尽可能保证每个 cell 中都能提取到特征点。
在这里插入图片描述
代码实现主要有两步:

  • 划分 cell,先用默认阈值提取特征点,如果找不到,就降低阈值,用 _minThFAST 搜索特征点;

  • 对得到的所有特征点进行八叉树筛选,若某区域内特征点数目过于密集,则只取其中响应值最大的那个。
    在这里插入图片描述

2.4 筛选特征点 DistributeOctTree()

筛选完特征点后,还是可能出现某些 cell 中特征点密集,某些 cell 中稀疏甚至没有特征点。因此采用类似八叉树的方法重新分发角点,使一个区域只有一个特征点(注意,这里的区域是重新划分的,不是之前的 cell)。
在这里插入图片描述

(没有特征点或只有一个特征点的区域不再分裂。)

2.5 计算特征点方向 computeOrientation()

使用特征点周围半径为 19 的圆的重心方向作为特征点方向。

M 00 = ∑ X = − R R ∑ Y = − R R I ( x , y ) M 10 = ∑ X = − R R ∑ X = − R R x I ( x , y ) M 01 = ∑ X = − R R ∑ X = − R R y I ( x , y ) Q X = = M 10 M 00 , Q Y = M 01 M 00 C = ( m 10 m 00 , m 00 m 00 ) θ = atan ⁡ 2 ( m 01 , m 10 ) \begin{aligned} & M_{00}=\sum_{X=-R}^R \sum_{Y=-R}^R I(x, y) \\ & M_{10}=\sum_{X=-R}^R \sum_{X=-R}^R x I(x, y) \\ & M_{01}=\sum_{X=-R}^R \sum_{X=-R}^R y I(x, y) \\ & Q_{X=}=\frac{M_{10}}{M_{00}}, Q_Y=\frac{M_{01}}{M_{00}} \\ & C=\left(\frac{m_{10}}{m_{00}}, \frac{m_{00}}{m_{00}}\right) \\ & \theta=\operatorname{atan} 2\left(m_{01}, m_{10}\right) \end{aligned} M00=X=RRY=RRI(x,y)M10=X=RRX=RRxI(x,y)M01=X=RRX=RRyI(x,y)QX==M00M10,QY=M00M01C=(m00m10,m00m00)θ=atan2(m01,m10)
c x = ∑ x = − R ∑ y = − R R x I ( x , y ) ⏞ m 10 ∑ x = − R R ∑ y = − R R I ( x , y ) ⏟ m 00 , c y = ∑ x = − R R ∑ y = − R R y I ( x , y ) ⏞ m 01 ∑ x = − R R ∑ y = − R R I ( x , y ) ⏟ m 00 θ = arctan ⁡ 2 ( c y , c x ) = arctan ⁡ 2 ( m 01 , m 10 ) \begin{aligned} & c_x=\frac{\overbrace{\sum_{x=-R} \sum_{y=-R}^R x I_{(x, y)}}^{m_{10}}}{\underbrace{\sum_{x=-R}^R \sum_{y=-R}^R I_{(x, y)}}_{m_{00}}}, c_y=\frac{\overbrace{\sum_{x=-R}^R \sum_{y=-R}^R y I_{(x, y)}}^{m_{01}}}{\underbrace{\sum_{x=-R}^R \sum_{y=-R}^R I_{(x, y)}}_{m_{00}}} \\ & \theta=\arctan 2\left(c_y, c_x\right)=\arctan 2\left(m_{01}, m_{10}\right) \\ & \end{aligned} cx=m00 x=RRy=RRI(x,y)x=Ry=RRxI(x,y) m10,cy=m00 x=RRy=RRI(x,y)x=RRy=RRyI(x,y) m01θ=arctan2(cy,cx)=arctan2(m01,m10)

2.6 计算特征点描述子 computeOrbDescriptor()

在特征点周围半径为 16 的圆域内选取 256 对点,比较,得到 256 位描述子。
在这里插入图片描述

computeOrientation() 中,我们求出了每个特征点的主方向,因此在计算描述子之前,要先将特征点周围像素旋转到主方向上来。
在这里插入图片描述

2.7 ORBextractor 类总结

2.7.1 主要流程

ORBextractor 类用于 tracking 线程中第一步预处理。

主要流程为
在这里插入图片描述

2.7.2 与其他类的关系

Frame类 中与 ORBextractor 有关的成员变量和函数

成员变量/函数访问控制意义
ORBextractor* mpORBextractorLeftpublic左目特征点提取器
ORBextractor* mpORBextractorRightpublic右目特征点提取器(单目/RGBD时为空指针)
ExtractORB()public提取特征点,直接调用 mpORBextractorLeftmpORBextractorRight
FramepublicFrame类的构造函数,调用 ExtractORB() 提取特征点

每次提取完 ORB 特征点之后,图像金字塔信息就会作废,下一帧图像到来时调用 ComputePyramid() 函数会覆盖掉上一帧的图像金字塔信息;但已经提取到的特征点信息会被保留在 Frame 对象中。所以 ORB-SLAM2 是稀疏重建,每帧图像只会保留最多 nfeatures 个特征点。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值