ORB-SLAM2算法7了解了System
主类和多线程,如下图,本文主要学习ORB-SLAM2
中的图像预处理的ORB
部分(Oriented FAST and Rotated BRIEF
),也就是视觉特征点提取器ORBextractor
,从全称来看,ORB-SLAM2
的ORB
部分主要涉及图像的特征点提取和描述子生成。
ORB
特征点提取使用了FAST
(Features from Accelerated Segment Test
)算法。FAST
算法是一种高效的特征点检测算法,通过比较像素点与其周围邻域像素的亮度差异来确定特征点。ORB
中,FAST
算法被扩展成了Oriented FAST
,它能够在检测到的特征点周围计算出一个主方向。这个主方向在后续的描述子生成中起到了重要作用。ORB-SLAM2
还能够提取多尺度的特征点,以适应不同距离的场景。ORB
中,每个特征点都有一个对应的局部区域。描述子的生成是在这个局部区域内进行的。ORB
使用了一个256
维的灰度差向量,它记录了周围像素与特征点的灰度差异情况。ORB
描述子。生成描述子的方式是将灰度差向量中的每个元素与零进行比较,将比零大的元素置为1
,否则置为0
。ORB-SLAM2
中的特征点提取前大致流程分以下几个步骤:
main
函数中先建立slam
的Tracking
线程,以ORB_SLAM2/Examples/Monocular/mono_tum.cc
单目为例,main
函数中的SLAM.TrackMonocular(im,tframe);
建立跟踪线程;ORB_SLAM2/src/System.cc
进入到TrackMonocular
函数中,通过cv::Mat Tcw = mpTracker->GrabImageMonocular(im,timestamp);
获取相机位姿;ORB_SLAM2/src/Tracking.cc
进入到GrabImageMonocular
函数中,输入灰度图,构造mCurrentFrame
关键帧;Frame
跳转到ORB_SLAM2/src/Frame.cc
进入到实现帧检测的函数Frame::Frame
,其中就调用ExtractORB(0,imGray);
来对单目图像进行特征点提取;ORB_SLAM2/src/ORBextractor.cc
特征点提取的函数实现文件(包含描述子生成)。直接查看ORB_SLAM2/src/ORBextractor.cc
文件,或者也可以查看ORB_SLAM2/include/ORBextractor.h
头文件也可以清晰的看出特征点提取器的成员函数和成员变量定义。
成员函数 | 类型 | 定义 |
---|---|---|
static float IC_Angle(const Mat& image, Point2f pt, const vector |
static |
计算特征点的方向 |
static void computeOrbDescriptor(const KeyPoint& kpt, const Mat& img, const Point* pattern, uchar* desc) |
static |
计算ORB特征点的描述子 |
ORBextractor::ORBextractor(...) |
public |
特征点提取器的构造函数 |
static void computeOrientation(const Mat& image, vector |
static |
计算特征点的方向 |
vector |
protected |
使用四叉树法对一个图像金字塔图层中的特征点进行平均和分发 |
void ORBextractor::ComputeKeyPointsOctTree(vector |
protected |
计算四叉树的特征点 |
void ORBextractor::ComputeKeyPointsOld(std::vector |
protected |
计算特征点的旧方法 |
static void computeDescriptors(const Mat& image, vector |
static |
计算某层金字塔图像上特征点的描述子 |
void ORBextractor::operator()( InputArray _image, InputArray _mask, vector |
public |
用仿函数(重载括号运算符)方法来计算图像特征点 |
void ORBextractor::ComputePyramid(cv::Mat image) |
protected |
构建图像金字塔 |
成员变量 | 类型 | 定义 |
---|---|---|
std::vector |
public |
存储图像金字塔的变量,一个元素存储一层图像 |
std::vector |
protected |
计算描述子的随机采样点集合 |
int nfeatures |
protected |
整个图像金字塔中,要提取的特征点数目 |
double scaleFactor |
protected |
图像金字塔层与层之间的缩放因子 |
int nlevels |
protected |
图像金字塔的层数 |
int iniThFAST |
protected |
初始的FAST响应值阈值 |
int minThFAST |
protected |
最小的FAST响应值阈值 |
std::vector |
protected |
分配到每层图像中,要提取的特征点数目 |
std::vector |
protected |
计算特征点方向的时候,有个圆形的图像区域,这个vector中存储了每行u轴的边界(四分之一,其他部分通过对称获得) |
std::vector |
protected |
每层图像的缩放因子 |
std::vector |
protected |
以及每层缩放因子的倒数 |
std::vector |
protected |
存储每层的sigma^2 ,即上面每层图像相对于底层图像缩放倍数的平方 |
std::vector |
protected |
sigma^2 的倒数 |
ORB-SLAM2
中构造图像金字塔的目的是为了实现多尺度的特征点提取和匹配,以增强系统的鲁棒性和适应性。图像金字塔是通过对原始图像进行不同尺度的降采样得到的一系列图像,每个图像都具有不同的分辨率。
图像金字塔把具有最高级别分辨率的图像放在底部,以金字塔形状排列,往上是一系列像素(尺寸)逐渐降低的图像,一直到金字塔的顶部只包含一个像素点的图像,这就构成了传统意义上的图像金字塔。
提醒:向下与向上采样,是对图像的尺寸而言的(和金字塔的方向相反),向下取样(缩小图像),向上取样(放大图像),向下取样是分辨率越来越小(往金字塔塔尖方向),向上取样是增加像素。
尺度不变性:在图像中,物体的尺度可能随着距离的变化而变化。通过构建图像金字塔,可以在不同尺度上检测和描述特征点,使ORB-SLAM2
对于不同距离的场景具有尺度不变性。这意味着即使物体在图像中的大小不同,ORB-SLAM2
仍能够检测到相应的特征点。
增强鲁棒性:构建图像金字塔可以在不同分辨率上进行特征提取,从而使ORB-SLAM2
对于图像中的噪声、遮挡和光照变化等干扰因素具有更强的鲁棒性。在低分辨率图像上提取的特征点通常对这些干扰因素更具有抵抗能力。
速度优化:通过在较高分辨率的图像上进行特征点提取,可以提高ORB-SLAM2
的精度。然而,在计算资源有限的情况下,仅在高分辨率图像上进行特征提取可能会导致计算效率低下。通过构建图像金字塔,ORB-SLAM2
可以根据需要选择适当的分辨率进行特征点提取,从而在保持一定精度的同时提高计算速度。
综上所述,通过构建图像金字塔,ORB-SLAM2
能够实现多尺度的特征点提取和匹配,提高系统的鲁棒性、适应性和计算效率。这对于处理不同尺度和复杂度的场景是非常重要的。
下面成员变量从配置文件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 |
其中参数中的响应值和要计算的描述子需要重点解释下:
特征点响应值和描述子的区别:
根据上述变量的值计算出下述成员变量:
成员变量 | 类型 | 定义 | 值 |
---|---|---|---|
std::vector |
protected |
金字塔每层级中提取的特征点数,正比于图层边长,总和为nfeatures |
{61, 73, 87, 105, 126, 151, 181, 216} |
std::vector |
protected |
各层级的缩放系数 | {1, 1.2, 1.44, 1.728, 2.074, 2.488, 2.986, 3.583} |
std::vector |
protected |
各层级缩放系数的倒数 | {1, 0.833, 0.694, 0.579, 0.482, 0.402, 0.335, 0.2791} |
std::vector |
protected |
各层级缩放系数的平方 | {1, 1.44, 2.074, 2.986, 4.300, 6.190, 8.916, 12.838} |
std::vector |
protected |
各层级缩放系数的平方倒数 | {1, 0.694, 0.482, 0.335, 0.233, 0.162, 0.112, 0.078} |
Fast
角点检测以速度著称,主要检测局部像素灰度变化明显的地方。核心思想:如果一个像素与它邻域的像素差值较大(过暗或过亮),那它极有可能是角点。
提取步骤:
p
,假设它的亮度为 Ip
。T
(比如 Ip
的 20%
)。p
为中心, 选取半径为 3
的圆上的 16
个像素点。N
个点的亮度大于 Ip + T
或小于 Ip −T
,那么像素 p
可以被认为是特征点 (N
通常取 12
,即为 FAST-12
。其它常用的 N
取值为 9
和 11
, 他们分别被称为 FAST-9
,FAST-11
)。
Fast
角点提取文献参考:Machine Learning for High-Speed Corner Detection
但OpenCV
中的FAST角点检测容易出现扎堆现象,后面会结合非极大值抑制(Non-maximal suppression
)来优化这点;而且Fast
角点本不具有方向,但是由于特征点匹配需要,ORB-SLAM2
对Fast
角点进行了改进,改进后的 FAST
被称为 Oriented FAST
,不仅进一步提高了检测效率,还具有旋转和尺度的描述。
前面已经定义并初始化了图像金字塔,Oriented FAST
角点检测前,先对金字塔每一层的图像遍历进行特征点检测,而提取特征点最重要的就是希望特征点均匀地分布在图像的所有部分,所以实现的核心的有两步:
int minThFAST
(类似降低分数线)再搜索一遍;cell
区域取响应值最大的那个特征点。如下流程图和详细实现代码,ORB-SLAM2
中的特征点检测时设置每个cell大小为30*30
,先用高响应值int iniThFAST
检测,如果检测不到,再用低响应值int minThFAST
检测,最后把所有检测到特征点再统一做八叉树筛选,比较密集的部分特征点用非极大值抑制,选出最优的特征点。
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);
}
}
主要通过函数DistributeOctTree()
进行八叉树筛选(非极大值抑制),如下图,不断将记录特征点的图像区域进行4等分,直到分出了足够多的分区,每个分区内只保留响应值最大的特征点。
利用非极大值抑制得到更加均分分布的优质特征点之后,还需要用灰度质心法来求特征点的方向。灰度质心(Gray-level centroid
)是一个用于计算图像或图像区域灰度分布中心的指标。详细的数学原理定义如下:
假设选择图像块B
,定义图像块B
的矩 m p q m_{pq} mpq定义为:
m p q = ∑ x , y ∈ B x p y q I ( x , y ) , p , q = 0 , 1 (1) m_{pq}=\sum_{{x,y}\in B}x^py^qI(x,y),\quad\quad p,q={0,1} \tag{1} mpq=x,y∈B∑xpyqI(x,y),p,q=0,1(1)
其中 x , y x,y x,y表示像素坐标, I ( x , y ) I(x,y) I(x,y)表示此像素坐标的灰度值;
图像块B
的质心C
可以通过公式(1)中的矩来进一步计算:
m 00 = ∑ x , y ∈ B I ( x , y ) , m 10 = ∑ x , y ∈ B x ∗ I ( x , y ) , m 01 = ∑ x , y ∈ B y ∗ I ( x , y ) (2) m_{00}=\sum_{{x,y}\in B}I(x,y), \quad m_{10}=\sum_{{x,y}\in B}x*I(x,y), \quad m_{01}=\sum_{{x,y}\in B}y*I(x,y) \tag{2} m00=x,y∈B∑I(x,y),m10=x,y∈B∑x∗I(x,y),m01=x,y∈B∑y∗I(x,y)(2)
C = ( c x , c y ) = ( m 10 m 00 , m 01 m 00 ) (3) C=(c_x,c_y)=\left (\frac{m_{10}}{m_{00}}, \frac{m_{01}}{m_{00}} \right ) \tag{3} C=(cx,cy)=(m00m10,m00m01)(3)
而我们要求的特征点方向,即是图像块B
的几何中心O
和质心C
连接在一起的方向向量 O C ⃗ \vec{OC} OC,所以定义特征点的方向 θ \theta θ为:
θ = a r c t a n 2 ( c y , c x ) = a r c t a n 2 ( m 01 m 10 ) (4) \theta =arctan2(cy,cx)=arctan2(\frac{m_{01}}{m_{10}}) \tag{4} θ=arctan2(cy,cx)=arctan2(m10m01)(4)
特别说明:ORB-SLAM2
中的图像块B
不是矩形块,是圆形块,因为相对于矩形,圆形才能保证关键点的旋转不变性。
详细的实现代码如下,主要是 computeOrientation()
和IC_Angle
两个函数:
// 计算特征点的方向
static void computeOrientation(const Mat& image, vector<KeyPoint>& keypoints, const vector<int>& umax)
{
// 遍历所有的特征点
for (vector<KeyPoint>::iterator keypoint = keypoints.begin(),
keypointEnd = keypoints.end(); keypoint != keypointEnd; ++keypoint)
{
// 调用IC_Angle 函数计算这个特征点的方向
keypoint->angle = IC_Angle(image, //特征点所在的图层的图像
keypoint->pt, //特征点在这张图像中的坐标
umax); //每个特征点所在图像区块的每行的边界 u_max 组成的vector
}
}
// 这个函数用于计算特征点的方向,这里是返回角度作为方向。
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);
}
为了让尺度体现其连续性,还需对图像金字塔每层图像使用不同参数做高斯模糊,主要抑制图像中的噪声和细节信息,更好的计算描述子。提取特征点时使用的是清晰的原图像,而计算描述子是需要先进行高斯模糊。
高斯模糊的代码如下,主要是GaussianBlur
函数来实现:
// preprocess the resized image
// Step 5 对图像进行高斯模糊
// 深拷贝当前金字塔所在层级的图像
Mat workingMat = mvImagePyramid[level].clone();
// 注意:提取特征点的时候,使用的是清晰的原图像;这里计算描述子的时候,为了避免图像噪声的影响,使用了高斯模糊
GaussianBlur(workingMat, //源图像
workingMat, //输出图像
Size(7, 7), //高斯滤波器kernel大小,必须为正的奇数
2, //高斯滤波在x方向的标准差
2, //高斯滤波在y方向的标准差
BORDER_REFLECT_101);//边缘拓展点插值类型
Brief
描述子文献参考:https://www.cs.ubc.ca/~lowe/525/papers/calonder_eccv10.pdf
计算BRIEF
描述子的核心步骤是在特征点周围半径为16
的圆域内选取256
对点对,在圆内随机挑选(挑选策略:均匀分布采样、高斯分布采样、完全随机采样等)点对,点对里第一个点像素值如果大于第二个点的像素值,则描述子对应位的值为1
,否则为0
,共得到256
位的描述子,为保计算的一致性,工程上使用特定设计的点对pattern
,在程序里被硬编码为成员变量了。
//下面就是预先定义好的随机点集,256是指可以提取出256bit的描述子信息,每个bit由一对点比较得来;4=2*2,前面的2是需要两个点(一对点)进行比较,后面的2是一个点有两个坐标
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)*/,
...
但如下图所示,Brief
描述子对旋转敏感,可在做特征点匹配中,希望的是特征描述子是与旋转无关的,这就用到了刚刚灰度质心法计算的特征点主方向,计算描述子时,会把特征点周围像素旋转到特征点主方向上来计算,为了编程方便,代码中是对pattern
进行旋转。
计算描述子的代码如下,主要是通过computeOrientation()
函数来实现:
/**
* @brief 计算某层金字塔图像上特征点的描述子
*
* @param[in] image 某层金字塔图像
* @param[in] keypoints 特征点vector容器
* @param[out] descriptors 描述子
* @param[in] pattern 计算描述子使用的固定随机点集
*/
static void computeDescriptors(const Mat& image, vector<KeyPoint>& keypoints, Mat& descriptors,
const vector<Point>& pattern)
{
//清空保存描述子信息的容器
descriptors = Mat::zeros((int)keypoints.size(), 32, CV_8UC1);
//开始遍历特征点
for (size_t i = 0; i < keypoints.size(); i++)
//计算这个特征点的描述子
computeOrbDescriptor(keypoints[i], //要计算描述子的特征点
image, //以及其图像
&pattern[0], //随机点集的首地址
descriptors.ptr((int)i)); //提取出来的描述子的保存位置
}
至此,详细学习了ORB-SLAM2
中的ORBextractor
特征点提取器的核心原理和实现细节,后续在此基础上继续学习ORB-SLAM2
中的特征点匹配、地图点、关键帧及三大线程等。
Reference:
⭐️