特征检测器实现函数:ORBextractor.cc->ORBextractor::operator()
输入一张图像,返回存放特征点的vector向量,存放描述子的mat,特征点数量,步骤大致分为以下4步
检测器构造函数:ORBextractor.cc->ORBextractor::ORBextractor()
作用是设置一下特征检测器的相关参数,主要是特征点提取数量上限和图像金字塔的相关参数,这些参数根据ORB-SLAM3的配置文件设置,一般不需要进行改变
mvScaleFactor:
用于存放图像金字塔每一层的尺度信息,每一层的尺度因子都是由上一层的尺度*缩放因子,最底层的尺度为1
mvLevelSigma2:
用于存放图像金字塔每层尺度因子的平方
mvInvScaleFactor
、mvInvLevelSigma2
存放上述尺度信息的倒数,方便计算
mvImagePyramid
:用于存放每一层金字塔图像,大小为金字塔层数
mnFeaturesPerLevel
: 记录每一层金字塔图像可以检测到的最大特征的数量,各个层之间的特征点数量又满足等比关系,根据配置文件中设置的最大特征点数量可以根据等比关系计算出每一层可以检测的特征点数量
函数:ORBextractor::ComputePyramid(cv::Mat image)
比较简单,根据尺度信息算每层金字塔图像的宽和高,然后resize
放到vector容器里
使用的是cv自带的fast角点提取函数cv::FAST(...)
,只不过这里作者设置了一个严刻的阈值iniThFAST(20)
和一个宽松的阈值minThFAST(7)
这里计算的是中心像素与周围一圈像素灰度值差异,小于这个阈值认为灰度相同即不是特征点,大于则认为不同即特征点
参考链接:https://zhuanlan.zhihu.com/p/61738607
核心函数:ORBextractor.cc->ORBextractor::ComputeKeyPointsOctTree()->DistributeOctTree()
1.首先计算每层金字塔图像应该保留多少个特征点
基本原理: 现在已知每张图像需要提取的特征点的数量,金字塔每层图像的面积,可以算出一个金字塔图像的总面积以及单位面积的特征点数量。之后根据每层金字塔图像和总面积的比例计算每层图像应该分配多少特征点(吐槽:感觉又是一个等比数列啊,比值是相邻两层图像面积的比例即尺度因子的平方)
具体实现:
我们假设第0层图像的宽为W
,长为L
,缩放因子为s
(这里的 0
)。那么整个金字塔总的面积S
为
那么,单位面积的特征点数量为N/S
那么,第0层应分配的特征点数量为
接着那么,推出了第α
层应分配的特征点数量为
2.其次基于四叉树原理,为每一层图像选择响应最好特征点并保证特征点的均匀分布
图示:假设某一层图像的特征点N=25
Step1:只有1个node
Step2:第一次分裂
(1)1个node分裂为4个node
(2)node数量4<25,还要继续分裂
Step3:第二次分裂
(1)4个node分裂成15个node,因为其中一个node没有点所以删除该node
(2)node数量16<25,还要继续分裂
Step4:第三次分裂
(1)15个node分裂为30个node,删掉了没有特征点的node
(2)node数量30>25,停止分裂
参考链接:https://zhuanlan.zhihu.com/p/61738607
ORB计算角度也比较简单,首先一个圆形区域的灰度质心,连接质心和圆心形成一个向量,这个向量的角度就是角点的角度。
图像来自:https://zhuanlan.zhihu.com/p/355441452
圆形区域的圆心是fast
特征点,圆的半径取为15
,因为整个patch一般取的是31×31
的。
灰度质心可以通过下面的公式计算:
则角度为:
至此,不仅仅提取了FAST角点,还找出了角点的角度。这个角度可以用来指导描述子的提取,保证每次都在相同的方向上计算描述子,实现角度不变性。
这就是Oriented FAST
。
BRIEF是一个二进制的描述子,计算和匹配的速度都很快。计算步骤如下:
以关键点为中心,选择一个31×31的像素块
在这个块内按照一定的方法选择N对点,N一般取256,这里的方法一般是提前通过学习选择的效果最好的256个位置
这样把256个点对的结果排起来,就形成了BRIEF描述子。
[1 0 1 0 0 ... 1 0 0]
BRIEF的匹配采用汉明距离,非常快,简单说就是看一下不相同的位数有多少个。如下的两个描述子,不同的位数为4。实际上我们选择的点对数为256,那么距离范围就是0~256。
[1 0 1 1 1 0]
[1 1 0 0 1 1]
思想简述: 不管是计算特征点的方向还是计算描述子的方向,最终的目的是为了更加准确的匹配上两张图像中对应的特征点,匹配的依据则是通过描述子进行判断。两个特征点周围的像素信息分布相差越小,描述子距离就越小,两个特征点是goodMatch的可能性就越高。
这样,判断两个特征点是否匹配的问题就转化成计算特征点周围像素分布情况差异的问题,然后就又转化为对周围像素的选取以及这种差异性计算方式的设计。
对于BRIEF描述子来说,计算方式就是计算汉明距离,点对的选择上面也提到了使用学习的方法进行设计,现在就剩下一个问题了,怎么确定参与比较的特征点周围的像素 。
BRIEF把关键点"周围像素"成为一个像素快patch
,如果不考虑旋转则选择patch就简单了,直接以特征点像素坐标为中心,选择长和宽固定的一个patch
就可以了,如下图所示。
但是存在一个问题,随着相机运动,特征点在x方向和y方向上的像素会发生变化,这样选取的图像块中的像素也会发生很大的变化,如同上图的PATCH1
和PATCH2
,如果拿这两个像素快去计算描素子距离,肯定会有比较大的差异。
基于这一现象,rBRIEF在原来的基础上增加了旋转的信息,就是当图像发生旋转了,特征点的方向肯定发生旋转,选取的的图像块范围也给他整体进行一个旋转,就变成了下图所示,使用PATCH1’
中的像素和PATCH2
中的像素进行比较。
BRIEF的旋转不变性原理大致是这样了,下面看一下具体的实现。
BRIEF描述子是没有考虑旋转不变性的,rBRIEF根据Oriented FAST计算出的角度,把原始的256个点对的坐标旋转之后,再取灰度。从而实现了旋转不变性。
原始的256个点对坐标为S
,旋转后的为S_θ
具体实现代码:
// 在特征提取的最后,会在computeOrientation函数中计算特征点的角度
float angle = (float)kpt.angle*factorPI;// 将角度转换为弧度
float a = (float)cos(angle), b = (float)sin(angle);// 计算余弦值与正弦值
// 和之前一样,这里获取的是一个指向特征点像素的uchar类型的指针,并非是特征点所对应像素的灰度值,需要注意
// 如果把img前面的取地址符&去掉,获取的就是该位置对应的灰度值了
// 换句话说是可以对center进行索引操作获取其它元素的值的,这在下面定义的宏中就有体现
const uchar* center = &img.at<uchar>(cvRound(kpt.pt.y), cvRound(kpt.pt.x));
// 这里的step是Mat的属性之一,表示一个数据所占字节长度,方便后面迭代时候跳过指定步长
// 例如一个单通道灰度影像一个像素值在我这里字节长度是700,而RGB影像一个像素的长度是2100
const int step = (int)img.step;
// 这里定义了一个宏,用于后续计算的方便
// 这里的center就是上面获取的uchar的指针,而pattern就是传入的参数
// 这里面一长串其实都是在计算索引,而对center取索引获取到的就是该索引(位置)所对应的灰度值
// 将随机点点集的x轴方向,旋转到特征点方向
#define GET_VALUE(idx) \
center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + \
cvRound(pattern[idx].x*a - pattern[idx].y*b)]
需要计算的像素点位置在旋转前后的对应关系如下图所示,即像素的坐标跟着旋转了一个特征点方向。
符号说明:
-θ:关键点的角度
-φ:像素块中某个像素在旋转前与x轴夹角
-r:像素块到原点的距离
-p(x,y):旋转前像素点坐标
-p'(x,y):旋转后像素点坐标
现在就变成了已知θ,p(x,y)
求p'(x,y)
,推导公式如下:
以上就是ORB-SLAM3图像帧构造过程中特征点提取和描述子计算部分的内容。