[ORBSLAM2源码笔记(2)]ORBextractor.cc文件解析-1
根据图像数据流的的输入顺序,每帧图像数据首先输入到ORBextractor.cc文件中进行处理,下面将以数据的流向为线索展开对ORBextractor.cc代码的学习和记录。
1 构造函数ORBextractor()
FAST特征点和ORB描述子本身不具有尺度信息,但是ORBextractor通过构建图像金字塔来得到特征点的尺度信息,图片输入后,首先逐级缩放构建图像金字塔,金字塔中的层级越高,图片分辨率越低,ORB特征点相对越大。
首先,构造函数ORBextractor()的实现流程如下:
step1. 初始化图像金字塔相关变量
step2. 初始化用于计算描述子的pattern
step3. 计算一个半径为HALF_PATCH_SIZE的圆的近似坐标
1.1 初始化图像金字塔相关变量
在利用TUM数据集运行ORBSLAM2的时候我们可以发现,需要提供一份TUM1.yaml的配置文件,实际上这个配置文件中就包含了在ORBextractor()需要初始化的相关变量。
TUMq.yaml内容:
%YAML:1.0
#--------------------------------------------------------------------------------------------
# Camera Parameters. Adjust them!
#--------------------------------------------------------------------------------------------
# Camera calibration and distortion parameters (OpenCV)
Camera.fx: 517.306408
Camera.fy: 516.469215
Camera.cx: 318.643040
Camera.cy: 255.313989
Camera.k1: 0.262383
Camera.k2: -0.953104
Camera.p1: -0.005358
Camera.p2: 0.002628
Camera.k3: 1.163314
Camera.width: 640
Camera.height: 480
# Camera frames per second
Camera.fps: 30.0
# IR projector baseline times fx (aprox.)
Camera.bf: 40.0
# Color order of the images (0: BGR, 1: RGB. It is ignored if images are grayscale)
Camera.RGB: 1
# Close/Far threshold. Baseline times.
ThDepth: 40.0
# Deptmap values factor
DepthMapFactor: 5000.0
#--------------------------------------------------------------------------------------------
# ORB Parameters
#--------------------------------------------------------------------------------------------
# ORB Extractor: Number of features per image
ORBextractor.nFeatures: 1000
# ORB Extractor: Scale factor between levels in the scale pyramid
ORBextractor.scaleFactor: 1.2
# ORB Extractor: Number of levels in the scale pyramid
ORBextractor.nLevels: 8
# ORB Extractor: Fast threshold
# Image is divided in a grid. At each cell FAST are extracted imposing a minimum response.
# Firstly we impose iniThFAST. If no corners are detected we impose a lower value minThFAST
# You can lower these values if your images have low contrast
ORBextractor.iniThFAST: 20
ORBextractor.minThFAST: 7
#--------------------------------------------------------------------------------------------
# Viewer Parameters
#--------------------------------------------------------------------------------------------
Viewer.KeyFrameSize: 0.05
Viewer.KeyFrameLineWidth: 1
Viewer.GraphLineWidth: 0.9
Viewer.PointSize:2
Viewer.CameraSize: 0.08
Viewer.CameraLineWidth: 3
Viewer.ViewpointX: 0
Viewer.ViewpointY: -0.7
Viewer.ViewpointZ: -1.8
Viewer.ViewpointF: 500
其中,ORBextractor()的如下变量就是从上面的yaml文件中读入后完成的初始化
1.2 初始化用于计算描述子的pattern
pattern变量是用于计算描述子的256对坐标,其值直接写在了ORBextractor.cc文件中。
注:
从运行效率的角度考虑的话,这里将大量参数直接从源码文件中读取会在一定程度上影响代码运行效率和实时性,可以将参数放入配置文件中进行进一步的优化。
下面函数中共有256对点,每个点有x,y两个坐标,一共是256x4个坐标点,在构造函数里会做类型转换将其转换为const cv::Point*变量。
代码如下:
// pattern是用于计算描述子的256对坐标,其值写死在下面,在构造函数里会做类型转换将其转换为const cv::Point*变量
// 256对点,每个点有x,y两个坐标,一共是256*4个坐标点
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)*/,
7,-12, 12,-13/*mean (5.62303e-05), correlation (0.0636977)*/,
2,-13, 2,12/*mean (0.000134953), correlation (0.085099)*/,
1,-7, 1,6/*mean (0.000528565), correlation (0.0857175)*/,
............
}
1.3 计算一个半径为HALF_PATCH_SIZE的圆的近似坐标
因为描述子不仅描述特征点的距离远近,还要进行方向的计算,在方向的计算过程中需要将特征点周围的像素旋转到主方向上,但是由于在特征点的圆形区域边界进行计算较为繁琐,所以采用利用一定数量的小正方形进行逼近的方法近似拟合出特征点圆的边界。
所以实际上,在第三步中,代码中要做的就是计算一个半径为HALF_PATCH_SIZE的圆的近似坐标(HALF_PATCH_SIZE为特征点圆的半径,在代码中赋值)。
// ***********************************************************
// 3.计算一个半径为HALF_PATCH_SIZE的圆的近似坐标
// ***********************************************************
// ------------------------------------------------------------------------------
// 计算过程:
// 将特征点周围像素旋转到主方向上,因此计算一个半径为HALF_PATCH_SIZE的圆的近似坐标,用于后面计算描述子时进行旋转操作
// 以下计算的是特征点主方向上的描述子
// ------------------------------------------------------------------------------
// 构建一个半径为HALF_PATCH_SIZE的圆
// vmax存储的是逼近ORB定义圆内的第一象限内1/4圆周上每个v坐标(纵)对应的u坐标(横)
// 为保证严格对称性,先计算下45°圆周上点的坐标,再根据对称性补全上45°圆周上点的坐标
// 45°射线与圆周交点的纵坐标
int v, v0, vmax = cvFloor(HALF_PATCH_SIZE * sqrt(2.f) / 2 + 1);
int vmin = cvCeil(HALF_PATCH_SIZE * sqrt(2.f) / 2);
const double hp2 = HALF_PATCH_SIZE*HALF_PATCH_SIZE;
// 先计算下半45°(0°-45°)的umax
for (v = 0; v <= vmax; ++v)
umax[v] = cvRound(sqrt(hp2 - v * v));
// Make sure we are symmetric
// 根据对称性补出上半45°(45°-90°)的umax
for (v = HALF_PATCH_SIZE, v0 = 0; v >= vmin; --v)
{
while (umax[v0] == umax[v0 + 1])
++v0;
umax[v] = v0;
++v0;
}
2 逐层计算图像金字塔ComputePyramid(cv::Mat image)
逐层计算图像金字塔时,对于每层图像进行如下两步:
1.先进行图片缩放,缩放到mvInvScaleFactor对应尺寸
2.在图像外padding一圈宽度为EDGE_THRESHOLD(=19)的圆域(提取FAST特征点需要特征点周围半径为3的圆域,计算ORB描述子需要特征点周围半径为16的圆域)
注:进行padding操作的原因,与深度学习中对输入图像进行padding的操作类似,因为使用方形进行圆形拟合的时候输入图像中有一部分像素未使用,为最大限度使用原图每个像素点,在计算前进行padding操作,并且ORBSLAM中进行填充的值并不是空白像素,是将边界处内部指定padding宽度的像素进行镜像复制到padding区域内,能够保证特征点提取效果
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()), masktemp;
// 缩放图像并复制到对应图层并补边
mvImagePyramid[level] = temp(Rect(EDGE_THRESHOLD, EDGE_THRESHOLD, sz.width, sz.height));
// Compute the resized image
// 非0层先缩放,后复制到金字塔内
// copyMakeBorder()函数,实现了复制和padding填充,参数BORDER_REFLECT_101制定对padding进行镜像填充(填充的不是空白像素,是将边界处内部指定padding宽度的像素进行镜像复制到padding区域内,能够保证特征点提取效果)
if( level != 0 )
{
resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
copyMakeBorder(mvImagePyramid[level], temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101+BORDER_ISOLATED);
}
// 金字塔第0层不缩放,直接把图像复制到金字塔内
else
{
copyMakeBorder(image, temp, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD, EDGE_THRESHOLD,
BORDER_REFLECT_101);
}
}
}
3 提取特征点并进行筛选ComputeKeyPointsOctTree()
对于输入图像进行特征点提取时,最佳效果就是所求得的特征点能够均匀地分布在整张图片的所有部分,为实现更好的提取效果,作者使用了下面几个***编程技巧***,值得借鉴学习:
1.分区域搜索特征点,如果某一个区域内的特征点响应值普遍较小,就降低响应阈值重新搜索一遍;
2.对提取到的所有特征点进行八叉树筛选,若某个区域内的特征点数目过多,则只提取该区域中响应值最大的那个特征点作为该区域的代表特征点。
总体提取流程如下:
void ORBextractor::ComputeKeyPointsOctTree(vector<vector<KeyPoint> >& allKeypoints)
{
allKeypoints.resize(nlevels);
const float W = 30;
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;
// 存储需要进行平均分配的特征点
vector<cv::KeyPoint> vToDistributeKeys;
vToDistributeKeys.reserve(nfeatures*10);
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的高度
// step1. 遍历每行和每列,依次用高低阈值搜索FAST特征点
for(int i=0; i<nRows; i++)
{
const float iniY =minBorderY+i*hCell;
float maxY = iniY+hCell+6;
if(iniY>=maxBorderY-3)
continue;
if(maxY>maxBorderY)
maxY = maxBorderY;
for(int j=0; j<nCols; j++)
{
const float iniX =minBorderX+j*wCell;
float maxX = iniX+wCell+6;
if(iniX>=maxBorderX-6)
continue;
if(maxX>maxBorderX)
maxX = maxBorderX;
// 用来存储找到的FAST关键点
vector<cv::KeyPoint> vKeysCell;
// 先用高阈值iniThFAST进行FAST特征点搜索
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,iniThFAST,true);
// 如果没有找到就用更小的minThFAST阈值进行特征点搜索
if(vKeysCell.empty())
{
FAST(mvImagePyramid[level].rowRange(iniY,maxY).colRange(iniX,maxX),
vKeysCell,minThFAST,true);
}
if(!vKeysCell.empty())
{
// 把vKeyCell中提取到的特征点全部添加到vToDistributeKeys中
for(vector<cv::KeyPoint>::iterator vit=vKeysCell.begin(); vit!=vKeysCell.end();vit++)
{
(*vit).pt.x+=j*wCell;
(*vit).pt.y+=i*hCell;
vToDistributeKeys.push_back(*vit);
}
}
}
}
vector<KeyPoint> & keypoints = allKeypoints[level];
keypoints.reserve(nfeatures);
// step2. 遍历完所有的CELL之后,调用DistributeOctTree对上面找到的所有特征点进行八叉树筛选,对特征点密集区域进行非极大值抑制
keypoints = DistributeOctTree(vToDistributeKeys, minBorderX, maxBorderX,
minBorderY, maxBorderY,mnFeaturesPerLevel[level], level);
const int scaledPatchSize = PATCH_SIZE*mvScaleFactor[level];
// Add border to coordinates and scale information
const int nkps = keypoints.size();
for(int i=0; i<nkps ; i++)
{
keypoints[i].pt.x+=minBorderX;
keypoints[i].pt.y+=minBorderY;
keypoints[i].octave=level;
keypoints[i].size = scaledPatchSize;
}
}
// compute orientations
// step3. 最后调用computeOrientation计算每个特征点的主方向
for (int level = 0; level < nlevels; ++level)
computeOrientation(mvImagePyramid[level], allKeypoints[level], umax);
}