ORBSLAM特征提取流程

特征提取函数流程

这部分工作是主要是在ORBSLAM中的工作,这是ORBSLAM2在选取特征点中所采取的一种策略。在今年的暑假也就是202007将这个部分提取出来整合成为一个模块。现在九月份来做一个总结工作。

这里不会详细去讲述具体的数据结构以及各种解决这个办法的trick,着重于整个流程的简介。关于trick应该会在之后的一些文章中去解释说明一下。

## 入口

整个特征提取的入口首先是在Tracking.cc中构造Frame的部分

1
2
3
4
5
if(mState==NOT_INITIALIZED || mState==NO_IMAGES_YET)
mCurrentFrame =Frame(mImGray,timestamp,mpIniORBextractor,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);
else
mCurrentFrame =Frame(mImGray,timestamp,mpORBextractorLeft,mpORBVocabulary,mK,mDistCoef,mbf,mThDepth);

进入到Frame的构造函数,在一系列规定ORB提取的参数之后其进行Frame类内的一个函数ExtractORB来做单幅图像的一个特征点提取工作。

1
2
3
// ORB extraction
// 对单目图像进行特征点提取,0-left,1-right
ExtractORB(0,imGray);

进入函数之后,可以发现Frame类中一个ORBextractor的成员变量实例,并使用重载operator()来做实现特征点提取的工作。

1
2
3
4
5
6
7
8
9
10
void Frame::ExtractORB(int flag, const cv::Mat &im)
{
if(flag==0)
// 仿函数
// 仿函数中特性,通过对象类似函数调用,隐式调用重载运算符
// https://blog.csdn.net/u010710458/article/details/79734558?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-3.channel_param
(*mpORBextractorLeft)(im,cv::Mat(),mvKeys,mDescriptors);
else
(*mpORBextractorRight)(im,cv::Mat(),mvKeysRight,mDescriptorsRight);
}

所以真正的函数的入口就是在ORBextractor.cc文件中的函数括号的重载

1
2
3
4
5
6
7
8
9
/**
* @brief 用仿函数(重载括号运算符)方法来计算图像特征点
*
* @param[in] _image 输入原始图的图像
* @param[in] _mask 掩膜mask
* @param[in & out] _keypoints 存储特征点关键点的向量
* @param[in & out] _descriptors 存储特征点描述子的矩阵
*/
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector<KeyPoint>& _keypoints, OutputArray _descriptors)

对于这个函数,其主要的功能就是计算图像中的特征点位置以及各个特征点所具有的特征点描述

Step1: 构建图像金字塔

首先需要对原始图像进行尺度上的变换,构建不同尺度的金字塔,所使用的主要方式就是使用OpenCV自带的函数resize。

这里需要注意的就是,构建金字塔的所有参数就是之前在Frame类中所赋予的,在ORBextractor的构造函数中将这些参数传递给ORBex这个类中。

1
2
3
// Pre-compute the scale pyramid
// Step 1: 构建图像金字塔
ComputePyramid(image);

Step2: 计算图像特征点

这里所提取得到的特征点是均匀化之后的,所使用的是一个二维vector来进行存储

这里二维的vector,第一维存储的是金字塔的层数,第二维存储的是那一层金字塔图像里提取的所有特征点

1
2
vector < vector<KeyPoint> > allKeypoints;
ComputeKeyPointsOctTree(allKeypoints);

Step3: 构造存储描述子的容器Mat

到遍历金字塔每一层图像之前的操作就是在做给描述子初始化的工作,也就是对每一个描述子给定一个初始的大小与空间上的分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 如果本图像金字塔中没有任何的特征点
if( nkeypoints == 0 )
//通过调用cv::mat类的.release方法,强制清空矩阵的引用计数,这样就可以强制释放矩阵的数据了

_descriptors.release();
else
{
// 如果图像金字塔中有特征点,那么就创建这个存储描述子的矩阵,注意这个矩阵是存储整个图像金字塔中特征点的描述子的
_descriptors.create(nkeypoints, // 矩阵的行数,对应为特征点的总个数
32, // 矩阵的列数,对应为使用32*8=256位描述子
CV_8U); // 矩阵元素的格式
// 获取这个描述子的矩阵信息
descriptors = _descriptors.getMat();
}

这里重新新建了一个变量,复制矩阵。

Step4: 遍历每层图像,做高斯模糊

对金字塔每层的图像做一个高斯模糊,来防止图像噪声。

在提取特征点的时候用的是清晰的原图像,计算描述子的时候,用的是高斯模糊之后的图像

1
2
3
4
5
6
GaussianBlur(workingMat,                // 源图像
workingMat, // 输出图像
Size(7, 7), // 高斯滤波器kernel大小,必须为正的奇数
2, // 高斯滤波在x方向的标准差
2, // 高斯滤波在y方向的标准差
BORDER_REFLECT_101); // 边缘拓展点插值类型

Step5: 计算高斯模糊之后的描述子

1
2
3
4
computeDescriptors(workingMat,      //高斯模糊之后的图层图像 
keypoints, //当前图层中的特征点集合
desc, //存储计算之后的描述子
pattern); //随机采样点集

Step6: 将非0层的特征点坐标恢复到第0层下

也就是将金字塔高层的特征点映射回原图像中

1
2
3
4
5
6
7
8
9
10
if (level != 0)
{
// 获取当前图层上的缩放系数
float scale = mvScaleFactor[level];
// 遍历本层所有的特征点
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
// 特征点本身直接乘缩放倍数就可以了
keypoint->pt *= scale;
}