详谈ORB-SLAM2的特征点提取器ORBextractor

ORB-SLAM2的三大线程中用的类和变量,一个图像输入,最先进行处理的变量就是ORBextractor(),ORB特征提取器。

一、变量的结构

1、构造函数: ORBextractor()

(1) 特点

使用了图像金字塔,越往上图像越小,等级(Level)越高, 所以提取同样的特征点的话特征点本身也就越大,暗含的意思就是特征点对应在地图上,在三维结构上里相机更近,Level 1就是把Level 0缩小了,Level 2就是把Level 1缩放,……一层层的缩放关系,所以Level越大,层次越高,提取到的特征点本身就越大,也就是特征点对应实际的物体位置离相机是越近的;离相机越远,映射在ORBextractor上金字塔的层次级别越小。近大远小

FAST特征点和ORB描述子本身不具有尺度信息,ORBextractor通过构建图像金字塔来得到特征点尺度信息。将输入图片逐级缩放得到图像金字塔,金字塔层级越高,图片分辨率越低,ORB特征点越大。

在这里插入图片描述

2、构造函数ORBextractor(int nfeatures, float scaleFactor, int nlevels, int iniThFAST, int minThFAST)的流程:

初始化图像金字塔相关变量
初始化用于计算描述子的pattern
计算近似圆形的边界坐标umax

创建特征点提取器ORBextractor的三件事:

(1)初始化图像金字塔相关变量

nfeatures变量表示整个extrator所要提取的特征点;金字塔相邻层级间的Level0是Level1的多少倍,Level1是Level2的多少倍……缩放系数;nlevels表示金字塔层级数,iniThFAST和minThFAST表示提取特征点描述值得阈值,这些都输从配置文件TUM1.yaml中读取的,在配置文件中都有默认值。
在这里插入图片描述

成员变量 访问控制 意义 配置文件TUM1.yaml中变量名
int nfeatures protected 所有层级提取到的特征点数之和金字塔层数 ORBextractor.nFeatures 1000
double scaleFactor protected 图像金字塔相邻层级间的缩放系数 ORBextractor.scaleFactor 1.2
int nlevels protected 金字塔层级数 ORBextractor.nLevels 8
int iniThFAST protected 提取特征点的描述子门槛(高) ORBextractor.iniThFAST 20
int minThFAST protected 提取特征点的描述子门槛(低) ORBextractor.minThFAST 7

基于这几个成员变量为了方便后面的运算,又构建了几个变量,主要是这五个数组:、

mnFeaturesPerLevel每一层要求的特征点数,八层一共提取1000个特征点,层之间有大有小,根据面积和边长均分的话,每一层都提取一定量的特征点,最小一层61个,最大一层216个特征点,原理是根据等比公式。

mvScaleFactor是各层的缩放系数,每一层是上面一层的1.2倍,所以最顶层是最底层的3.586倍

mvInvScaleFactor是各层级缩放系数的倒数

mvLevelSigma2是各层级缩放系数的平方

mvInvLevelSigma2是各层级缩放系数的平方倒数

这就是初始化金字塔相关的变量

(2)初始化用于计算描述子的pattern

初始化用于计算描述子的变量,特征描述子是对于ORB特征点来说,相当于特征点的身份证,pattern用来计算特征点描述子,本质上pattern就是256对坐标也就是512个点,512个点的值写入固定在ORB-SLAM中了,这是精心设计的256对坐标,在程序中就读成OpenCV的类型

初始化用于计算描述子的pattern变量,pattern是用于计算描述子的256对坐标,其值写死在源码文件ORBextractor.cc里,在构造函数里做类型转换将其转换为const cv::Point*变量

(3)计算一个半径为16的圆的近似坐标

第三个部分是为了便于运算,计算描述子所用,ORB的全称是Oriented FAST and Rotated BRIEF,OR的意思就是特征点的描述子是带方向的,所以在计算描述子的时候计算的特征点主方向的描述子,所以每当后面得到描述子的时候会把特征点的周围的像素旋转到主方向上来计算,需要旋转的区域有多大就需要构造一个半径为16的圆,这么大的区域,所以就要计算一个径为16的圆的近似坐标,因为这是一个多个方格组成的,方格足够大的话就会逼近一个圆。
在这里插入图片描述
这次拿半径为8的圆做演示,如图中的圆,用正方形来逼近,记录信息只用记录图上带黑边的这些点就可以,相当于记录这些边的位置了,这几个点的坐标就是umax,在圆内v取0的时候u最大能取到8,所以umax[0]=8;v(纵坐标)取1的时候u最大能取到7,同理v取2、3的话u都可以取到7……如此将逼近圆形的坐标记录下来。

notes:
计算的是1/8圆,然后根据对称性进行补齐。实际上是逼近圆的第一象限内1/4圆周上每个v坐标对应的u坐标。为保证严格对称性,先计算下45°圆周上点的坐标,再根据对称性补全上45°圆周上点的坐标。这样是为了逼近的精度越大,实际上正方形逼近它总会有问题,所以要想逼近最大最直接的方法就是增大圆的半径,所以增大到16。

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

3、构建图像金字塔: ComputePyramid()

直观上理解构建金字塔就是一个不断缩放的过程,从下往上缩放,第一层是原图,然后下一层缩放1.2倍,再下一层继续缩放1.2倍……然而实际上每一层金字塔除了原图的缩放部分之外,还增加了两个区域,深灰色的区域是原图经过缩放的区域,在灰色外加入了半径为3的padding,外面有加了一层边长为16的边,所以一共补了19(16+3)像素的边

函数void ORBextractor::ComputePyramid(cv::Mat image)逐层计算图像金字塔,对于每层图像进行以下两步:

(1)先进行图片缩放,缩放到mvInvScaleFactor对应尺寸

首先提取FAST特征点,提取的时候需要3X3大小的圆,所以给100X100的图像,所以只有中心的区域94x94有像素,外面的一圈提取圆有半径,半径的部分就被浪费了,需要最大部分利用原图,所以手动把图补齐,避免浪费,外面添加长度为3的边,把想要提取的放置中间,这样的话就不会造成浪费。

(2)在图像外补一圈厚度为19的padding(提取FAST特征点需要特征点周围半径为3的圆域,计算ORB描述子需要特征点周围半径为16的圆域)

①计算描述子的时候需要16的圆,所以外面再加一层16的padding,所以一共加了19(16+3)像素的边
金字塔的每一层都是这样的
在这里插入图片描述

  • 深灰色为缩放后的原始图像
  • 包含绿色边界在内的矩形用于提取FAST特征点
  • 包含浅灰色边界在内的整个矩形用于计算ORB描述子

金字塔的结构本身,在这里是使用verctor变量mvImagePyramid图像金字塔,类型是vector动态数组,每个vector里面存的是cv::Mat矩阵(矩阵就是一张图片),所以这就是一个长度为8的数组,数组的每个元素都是一张图像,图像经过缩放、不变得到结果。

补边的厚度是一个全局变量,实际上在程序中就是固定值19。

成员变量:

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

②伪代码描述思想:
主要逻辑:
for循环遍历每一层,每一层计算缩放后尺寸,然后进行缩放,如果第0层就不缩放,直接把图像复制到金字塔上。在这里用了一个copyMakeBorder()函数(OpenCV的函数),同时进行图像复制和填充的过程,中间传递参数BORDER_REFLECT_101

void ORBextractor::ComputePyramid(cv::Mat image) {
    
    
    for (int level = 0; level < nlevels; ++level) {
    
    
        // 计算缩放+补padding后该层图像的尺寸
        float scale = mvInvScaleFactor[level];
        Size sz(cvRound((float)image.cols*scale), cvRound((float)image.rows*scale));
        Size wholeSize(sz.width + EDGE_THRESHOLD * 2, sz.height + EDGE_THRESHOLD * 2);
        Mat temp(wholeSize, image.type());
        
		// 缩放图像并复制到对应图层并补边
        mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
        if( level != 0 ) {
    
    
            resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, cv::INTER_LINEAR);
            copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, 
                           BORDER_REFLECT_101+BORDER_ISOLATED);            
        } else {
    
    
            copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, 
                           BORDER_REFLECT_101);            
        }
    }
}

copyMakeBorder函数实现了复制和padding填充,其参数BORDER_REFLECT_101参数指定对padding进行镜像填充
同时进行复制和填充的过程,中间传递参数BORDER_REFLECT_101,填充的效果是这样的:
左边为原始图片,最外层添加一层padding,添加的不是普通的黑边,而是左边图像的镜像,这样检测的时候就会方便一些,当然这样检测也会存在误差,但是效果比填充黑边的效果更好
在这里插入图片描述

4、提取特征点并进行筛选

提取特征点最重要的是就是特征点要均匀,因为ORB-SLAM2系统的三大线程做的工作主要在特征点选取的如何。
图像特征点,20个特征点均匀分布在图像的整个区域效果好,如果聚集在某个部分那效果就不太好,右边的图像,如果相机向左移动,那么能跟踪上,如果相机向右移动,右边没有特征点,那就跟踪丢了就需要重定位,进行系统复位,就变得麻烦。

所以在图像输入进来,图像预处理操作的时候,就要把特征点分布的均匀
在这里插入图片描述
但是图像经过光照,图像的特征点天生并不好,有明有暗对比强和弱的地方,所以为了让特征点分布均匀,使用两个小技巧:
(1)分CELL搜索特征点,若某CELL内特征点响应值普遍较小的话就降低分数线再搜索一遍.
(2)对得到的所有特征点进行八叉树筛选,若某区域内特征点数目过于密集,则只取其中响应值最大的那个

5、特征点响应值/描述子的区别

  • 响应值描述的是该特征点的区分度大小
    响应值越大的点越应该被留用作特征点.
    响应值类似于分数,分数越高的学生越好,越应该被录取

  • 描述子是特征点的一个哈希运算
    其大小无意义,仅用来在数据库中快速找回某特征点
    描述子相当于学生的学号,系统随机运算出的一串数,用于找到学生

6、流程

(1)
根据流程图,遍历所有的区域,把图像划分成30X30的区域(900个格),每个格式用高响应值先筛选一遍,高响应值筛选到的先记录下来,如果高响应值没筛选到,就降低响应阈值来寻找,如果找了一遍所有特征点都没找到,那就不再寻找。

找到之后的下一步就是进行八叉树筛选,非极大值抑制 。之后计算描述子

在这里插入图片描述
在这里插入图片描述

(2) 网格搜索是30X30的网格,有的时候30X30的网格是64X48,如果图像到边界不够的话,剩下的地方也算作一个网格

(3)代码实现:
首先计算网格,每一行、每一列的网格,宽度和高度,遍历每一行和每一列,每一列中找FAST角点,阈值中iniThFAST(是高的20),20没有就找minThFAST(小的7),找到的话把放到容器vToDistributeKeys里,用来分配容器里,容器会传给八叉树函数,函数会处理非极大值抑制阈值筛选,得到均匀图像,计算每个特征点的方向。

void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)	{
    
    
    for (int level = 0; level < nlevels; ++level)
        // 计算图像边界
        const int minBorderX = EDGE_THRESHOLD-3;		
        const int minBorderY = minBorderX;				
        const int maxBorderX = mvImagePyramid[level].cols-EDGE_THRESHOLD+3;
        const int maxBorderY = mvImagePyramid[level].rows-EDGE_THRESHOLD+3;
        const float width = (maxBorderX-minBorderX);
        const float height = (maxBorderY-minBorderY);
        const int nCols = width/W;				// 每一列有多少cell
        const int nRows = height/W;				// 每一行有多少cell
        const int wCell = ceil(width/nCols);	// 每个cell的宽度
        const int hCell = ceil(height/nRows);	// 每个cell的高度

        // 存储需要进行平均分配的特征点
        vector<cv::KeyPoint> vToDistributeKeys;
		
    	// step1. 遍历每行和每列,依次分别用高低阈值搜索FAST特征点
        for(int i=0; i<nRows; i++) {
    
    
            const float iniY = minBorderY + i * hCell;
            const float maxY = iniY + hCell + 6;
            for(int j=0; j<nCols; j++) {
    
    
                const float iniX =minBorderX + j * wCell;
                const float maxX = iniX + wCell + 6;
                vector<cv::KeyPoint> vKeysCell;
				
                // 先用高阈值搜索FAST特征点
                FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),	vKeysCell, iniThFAST, true);
                // 高阈值搜索不到的话,就用低阈值搜索FAST特征点
                if(vKeysCell.empty()) {
    
    
                    FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),	vKeysCell, minThFAST, true);
                }
				// 把 vKeysCell 中提取到的特征点全添加到 容器vToDistributeKeys 中
                for(KeyPoint point :vKeysCell) {
    
    
                    point.pt.x+=j*wCell;
                    point.pt.y+=i*hCell;
                    vToDistributeKeys.push_back(point);
                }
            }
        }
		
    	// step2. 对提取到的特征点进行八叉树筛选,见 DistributeOctTree() 函数
        keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX, minBorderY, maxBorderY, mnFeaturesPerLevel[level], level);
    }
	// 计算每个特征点的方向
    for (int level = 0; level < nlevels; ++level)
        computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);				
	}
}

7、八叉树筛选特征点

(1)八叉树筛选的思路:
对图像而言,其实是四叉树,因为业界统一称为八叉树。

此博客参考ncepu_Chen的《5小时让你假装大概看懂ORB-SLAM2源码》

猜你喜欢

转载自blog.csdn.net/Prototype___/article/details/127266856