(1)尺度不变性:一幅图片中的某个物体的大小永远是那么大(设置一个标准尺度来度量),而不是因为它所在图片的放大缩小而改变。这就是尺度不变性。
将原始图像按照 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
ORB 的关键点是在 FAST 关键点基础上进行了改进,主要是增加了特征点的主方向,称之为 Oriented FAST。
描述子是在 BRIEF 描述子基础上加入了上述方向信息,称之为 Rotated BRIEF。
构造函数原型
ORBextractor::ORBextractor(int _nfeatures, float _scaleFactor, int _nlevels, int _iniThFAST, int _minThFAST)
(1)从 yaml 文件中读取参数(以 KITTI00-02.yaml 为例)
成员变量 | 意义 | 在yaml中变量名 | 值 |
---|---|---|---|
int _nfeatures |
期望提取的特征点个数 | ORBextractor.nFeatures |
2000 |
float _scaleFactor |
金字塔相邻层级缩放因子 | ORBextractor.scaleFactor |
1.2 |
int _nlevels |
金字塔层数 | ORBextractor.nLevels |
8 |
int _iniThFAST |
提取 FAST 特征点的默认阈值 | ORBextractor.iniThFAST |
20 |
int _minThFAST |
如果使用默认阈值提取不到特征点则使用最小阈值再次提取 | ORBextractor.minThFAST |
7 |
根据上述变量的值计算出下述成员变量:
变量 | 意义 | 值 |
---|---|---|
std::vector |
各层级缩放系数 | {1, 1.2, 1.44, 1.728, 2.074, 2.488, 2.986, 3.583 |
std::vector |
各层级缩放系数的倒数 | {1, 0.833, 0.694, 0.579, 0.482, 0.402, 0.335, 0.2791} |
std::vector |
各层级缩放系数的平方 | {1, 1.44, 2.074, 2.986, 4.300, 6.190, 8.916, 12.838} |
std::vector |
各层级缩放系数的平方的倒数 | {1, 0.694, 0.482, 0.335, 0.233, 0.162, 0.112, 0.078} |
std::vector |
每一层期望提取的特征点个数(正比于图层边长,总和为 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
中存储的是逼近圆的第一象限内 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;
}
变量 | 访问控制 | 意义 |
---|---|---|
std::vector |
public |
存储图像金字塔每层的图像 |
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
// 将扩充后的图像拷贝给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
我们希望 特征点均匀地分布在图像的所有部分。所以在提取时会将图片分成 30*30(单位像素)的一个一个小格子(cell)来提取特征点。并且在提取 FAST 角点时,我们设计了两个阈值 _iniThFAST
和 _minThFAST
,这样在每个 cell 中,就可以根据实际情况进行调整,尽可能保证每个 cell 中都能提取到特征点。
代码实现主要有两步:
筛选完特征点后,还是可能出现某些 cell 中特征点密集,某些 cell 中稀疏甚至没有特征点。因此采用类似八叉树的方法重新分发角点,使一个区域只有一个特征点(注意,这里的区域是重新划分的,不是之前的 cell)。
(没有特征点或只有一个特征点的区域不再分裂。)
使用特征点周围半径为 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=−R∑RY=−R∑RI(x,y)M10=X=−R∑RX=−R∑RxI(x,y)M01=X=−R∑RX=−R∑RyI(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=−R∑Ry=−R∑RI(x,y)x=−R∑y=−R∑RxI(x,y) m10,cy=m00 x=−R∑Ry=−R∑RI(x,y)x=−R∑Ry=−R∑RyI(x,y) m01θ=arctan2(cy,cx)=arctan2(m01,m10)
在特征点周围半径为 16 的圆域内选取 256 对点,比较,得到 256 位描述子。
在 computeOrientation()
中,我们求出了每个特征点的主方向,因此在计算描述子之前,要先将特征点周围像素旋转到主方向上来。
ORBextractor 类用于 tracking 线程中第一步预处理。
Frame类 中与 ORBextractor 有关的成员变量和函数
成员变量/函数 | 访问控制 | 意义 |
---|---|---|
ORBextractor* mpORBextractorLeft |
public | 左目特征点提取器 |
ORBextractor* mpORBextractorRight |
public | 右目特征点提取器(单目/RGBD时为空指针) |
ExtractORB() |
public | 提取特征点,直接调用 mpORBextractorLeft 和 mpORBextractorRight |
Frame |
public | Frame 类的构造函数,调用 ExtractORB() 提取特征点 |
每次提取完 ORB 特征点之后,图像金字塔信息就会作废,下一帧图像到来时调用 ComputePyramid()
函数会覆盖掉上一帧的图像金字塔信息;但已经提取到的特征点信息会被保留在 Frame
对象中。所以 ORB-SLAM2 是稀疏重建,每帧图像只会保留最多 nfeatures
个特征点。