ORB-SLAM3 细读单目初始化过程(上)
作者:乔不思
来源:微信公众号|3D视觉工坊(系投稿)
3D视觉精品文章汇总:https://github.com/qxiaofan/awesome-3D-Vision-Papers/
学习ORB-SLAM3单目视觉SLAM中,发现有很多知识点需要展开和深入,同时又需要对系统有整体的认知,为了强化记忆,记录该系列笔记,为自己图方便,也希望对大家有所启发。
因为知识有限,因此先记录初始化过程中的重要节点,并非全部细节,如果需要看代码的话,建议直接去看作者的源代码ORB_SLAM3(https://github.com/UZ-SLAMLab/ORB_SLAM3)。
这是我自己稍微做了点修改,可以跑数据集的版本,可以参考一下。https://github.com/shanpenghui/ORB_SLAM3_Fixed
TrackMonocular是ORBSLAM单目视觉SLAM的追踪器接口,因此从这里入手。其中GrabImageMonocular下⾯有2个主要的函数:Frame::Frame()和Tracking::Track()。我会按照下⾯的框架流程来分解单⽬初始化过程,以便对整个流程有⽐较清晰的认识。
1.Frame::Frame()
1)作用
主要完成工作是特征点提取,涉及到的知识点其实很多,包括图像金字塔、特征点均匀化、四叉树算法分发特征点、特征点方向计算等等
2)主要的三个函数 ExtractORB UndistortKeyPoints AssignFeaturesToGrid
Frame()中其实调用的是ORBextractor::operator(),是一个重载操作符函数,此系列笔记主要针对重点理论如何落实到代码上,不涉及编程技巧,因此不讨论该函数的原理和实现,直接深入,探寻本质。
对这个单目图像进行提取特征点 Frame::ExtractORB
用OpenCV的矫正函数、内参对提取到的特征点进行矫正 Frame::UndistortKeyPoints
将特征点分配到图像网格中 Frame::AssignFeaturesToGrid
3)Frame::ExtractORB
3-1)作用
主要完成工作是提取图像的ORB特征点和计算描述子
3-2)主要的函数 ComputePyramid ComputeKeyPointsOctTree computeDescriptors
构建图像金字塔 ORBextractor::ComputePyramid
利用四叉树算法均分特征点 ORBextractor::ComputeKeyPointsOctTree
计算某层金字塔图像上特征点的描述子 static ORBextractor::computeDescriptors
3-3)构建图像金字塔 ComputePyramid
3-3-1图像金字塔是什么东东?
首先,图像金字塔的概念是: 图像金字塔是图像中多尺度表达的一种,是一种以多分辨率来解释图像的有效但概念简单的结构。如图:
3-3-2代码怎么实现图像金字塔?
上面讨论了搭建图像金字塔,那怎么搭建呢?ORBSLAM3中,作者调用OpenCV的resize函数实现图像缩放,构建每层金字塔的图像,在函数ORBextractor::ComputePyramid中。
resize(mvImagePyramid[level-1], mvImagePyramid[level], sz, 0, 0, INTER_LINEAR);
3-3-3尺度不变性是什么东东?
我们搭建完金字塔了,但是有个问题,图像的进行了缩放之后,假如要用同一个相机去看,则需要根据缩放的程度来调整相机到图像的距离,来保持其观测的一致性,这就是尺度不变性由来。
在ORB-SLAM3中,为了实现特征尺度不变性采用了图像金字塔,金字塔的缩放因子为1.2。其思路就是对原始图形(第0层)依次进行1/1.2缩放比例进行降采样得到共计8张图片(包括原始图像),然后分别对得到的图像进行特征提取,并记录特征所在金字塔的第几层,这样得到一帧图像的特征点,如图1所示。
现在假设在第二层中有一特征点F,为了避免缩放带来特征点F在纵向的移动,为简化叙述,选择的特征点F位于图像中心,如图2所示。根据相机成像“物近像大,物远像小”的原理,如图2所示为相机成像的示意图。假设图1中摄像机原始图像即金字塔第0层对应图2中成像视野I0 ,则图1中图像金字塔第2层图像可以相应对应于图2中成像视野I2 。
有了以上铺垫现在,再来说说,尺度不变性。简单来说,因为图像金字塔对图像进行了缩放,假如要把该层的图像特征点移到其他层上,就要对应的放大图像,同时相机与图像的距离也要对应着进行缩放,保证其尺度不变性。
3-3-4代码哪里用到了尺度不变性?
3-3-4-1MapPoint::PredictScale
ORBSLAM3中,作者调用MapPoint::PredictScale函数,根据地图点到光心的距离,来预测一个类似特征金字塔的尺度。
因为在进行投影匹配的时候会给定特征点的搜索范围,由于考虑到处于不同尺度(也就是距离相机远近,位于图像金字塔中不同图层)的特征点受到相机旋转的影响不同,因此会希望距离相机近的点的搜索范围更大一点,距离相机更远的点的搜索范围更小一点,所以要在这里,根据点到关键帧/帧的距离来估计它在当前的关键帧/帧中,会大概处于哪个尺度。
可以参考下图示意:
ORB_SLAM3 MapPoint.cc 函数 MapPoint::PredictScale Line 536 539
ratio = mfMaxDistance/currentDist;
int nScale = ceil(log(ratio)/pKF->mfLogScaleFactor);
3-3-4-2MapPoint::UpdateNormalAndDepth
ORBSLAM3中,作者调用MapPoint::UpdateNormalAndDepth函数,来更新平均观测方向以及观测距离范围。由于一个MapPoint会被许多相机观测到,因此在插入关键帧后,需要更新相应变量,创建新的关键帧的时候会调用该函数。上面变量和代码中的对应关系是:
在ORB_SLAM3 MapPoint.cc 函数 MapPoint::UpdateNormalAndDepth Line 490-491
// 观测相机位置到该点的距离上限
mfMaxDistance = dist*levelScaleFactor;
// 观测相机位置到该点的距离下限
mfMinDistance = mfMaxDistance/pRefKF->mvScaleFactors[nLevels-1];
至此,构建图像金字塔 ComputePyramid记录完毕,再来回顾一下,说到底,搭建图像金字塔就是为了在不同尺度上来描述图像,从而达到充分解释图像的目的。
3-4)四叉树算法 ComputeKeyPointsOctTree
其实,代码中,核心算法在ORBextractor::DistributeOctTree中实现的。先讲原理吧。
3-4-1四叉树是什么东东?
装逼地说,啊不,专业地说,四叉树或四元树也被称为Q树(Q-Tree)。四叉树广泛应用于图像处理、空间数据索引、2D中的快速碰撞检测、存储稀疏数据等,而八叉树(Octree)主要应用于3D图形处理。这里可能会有歧义,代码中明明是Octree,不是八叉树吗?为什么这里讲的是四叉树原理呢?其实ORBSLAM里面是用四叉树来均分特征点,后来有人用八叉树来构建和管理地图,可能因为考虑到3D原因,作者在这里才把函数定义成OctTree,但实际用到的是四叉树原理。
QuadTree四叉树顾名思义就是树状的数据结构,其每个节点有四个孩子节点,可将二维平面递归分割子区域。QuadTree常用于空间数据库索引,3D的椎体可见区域裁剪,甚至图片分析处理,我们今天介绍的是QuadTree最常被游戏领域使用到的碰撞检测。采用QuadTree算法将大大减少需要测试碰撞的次数,从而提高游戏刷新性能。
不得不感慨,作者怎么懂那么多?大神就是大神,各种学科专业交叉融合,膜拜。GO ON。
四叉树很简单,就是把一块2d的区域,等分成4份,如下图: 我们把4块区域从右上象限开始编号, 逆时针。
四叉树起始于单节点。对象会被添加到四叉树的单节点上。
当更多的对象被添加到四叉树里时,它们最终会被分为四个子节点。(我是这么理解的:下面的图片不是分为四个区域吗,每个区域就是一个孩子或子节点)然后每个物体根据他在2D空间的位置而被放入这些子节点中的一个里。任何不能正好在一个节点区域内的物体会被放在父节点。(这点我不是很理解,就这幅图来说,那根节点的子节点岂不是有五个节点了。)
如果有更多的对象被添加进来,那么每个子节点要继续划分(成四个节点)。
大概也可以是这样:
好了。概念普及完了。那在ORB-SLAM3中,它到底想干嘛呢?
3-4-2四叉树用来干嘛?
ORB-SLAM中使用四叉树来快速筛选特征点,筛选的目的是非极大值抑制,取局部特征点邻域中FAST角点相应值最大的点,而如何搜索到这些扎堆的特征点,则采用的是四叉树的分快思想,递归找到成群的点,并从中找到相应值最大的点。
3-4-3代码怎么实现的?
在ORBextractor.cc 函数 ORBextractor::DistributeOctTree
第一部分:
- 输入图像未分的关键点 对应ORBextractor::DistributeOctTree函数中的形参vToDistributeKeysORBextractor.cc#L537
- 根据图像区域构造初始的根结点,每个根结点包含图像的一个区域,每个根结点同样包括4个子结点定义一个提取器 ExtractorNode ni;ORBextractor.cc#L552设置提取器节点的图像边界 ni.UL ni.UR ni.BL ni.BRORBextractor.cc#L552-L556 将刚才生成的提取节点添加到列表中lNodes.push_back(ni);ORBextractor.cc#L559 存储这个初始的提取器节点句柄vpIniNodes[i] = &lNodes.back();ORBextractor.cc#L560
- 将未分的所有关键点分配给2中构造的根结点,这样每个根节点都包含了其所负责区域内的所有关键点 按特征点的横轴位置,分配给属于那个图像区域的提取器节点vpIniNodes[kp.pt.x/hX]->vKeys.push_back(vToDistributeKeys[i]);ORBextractor.cc#L567
- 根结点构成一个根结点list,代码中是lNodes用来更新与存储所有的根结点 遍历lNodes,标记不可再分的节点,用的标记变量是lit->bNoMoreORBextractor.cc#L576
第二部分
- 当列表中还有可分的结点区域的时候:while(!bFinish)ORBextractor.cc#L592
- 开始遍历列表中所有的提取器节点,并进行分解或者保留:while(lit!=lNodes.end())ORBextractor.cc#L604
- 判断当前根结点是否可分,可分的意思是,它包含的关键点能够继续分配到其所属的四个子结点所在区域中(左上,右上,左下,右下),代码中是判断标志位if(lit->bNoMore)ORBextractor.cc#L606意思是如果当前的提取器节点具有超过一个的特征点,那么就要进行继续细分
- 如果可分,将分出来的子结点作为新的根结点放入INodes的前部,e.g. lNodes.front().lit = lNodes.begin();ORBextractor.cc#L626,就是在四个if(n*.vKeys.size()>0)条件中执行。然后将原先的根结点从列表中删除,e.g.lit=lNodes.erase(lit);ORBextractor.cc#L660。由于新加入的结点是从列表头加入的,不会影响这次的循环,该次循环只会处理当前级别的根结点。
- 当所有结点不可分,e.g(int)lNodes.size()==prevSizeORBextractor.cc#L667,或者结点已经超过需要的点(int)lNodes.size()>=NORBextractor.cc#L667时,跳出循环bFinish = true;ORBextractor.cc#L669。
3-5)计算特征点描述子 computeDescriptors
3-5-1描述子是什么东东?
图像的特征点可以简单的理解为图像中比较显著显著的点,如轮廓点,较暗区域中的亮点,较亮区域中的暗点等。
ORB采用的是哪种描述子呢?是用FAST(features from accelerated segment test)算法来检测特征点。这个定义基于特征点周围的图像灰度值,检测候选特征点周围一圈的像素值,如果候选点周围领域内有足够多的像素点与该候选点的灰度值差别够大,则认为该候选点为一个特征点。
3-5-2计算特征描述子
利用上述步骤得到特征点后,我们需要以某种方式描述这些特征点的属性。
这些属性的输出我们称之为该特征点的描述子(Feature DescritorS)。
ORB采用BRIEF算法来计算一个特征点的描述子。BRIEF算法的核心思想是在关键点P的周围以一定模式选取N个点对,把这N个点对的比较结果组合起来作为描述子。
如上图所示,计算特征描述子的步骤分四步:
3-5-3如何保证描述子旋转不变性?
在现实生活中,我们从不同的距离,不同的方向、角度,不同的光照条件下观察一个物体时,物体的大小,形状,明暗都会有所不同。但我们的大脑依然可以判断它是同一件物体。理想的特征描述子应该具备这些性质。即,在大小、方向、明暗不同的图像中,同一特征点应具有足够相似的描述子,称之为描述子的可复现性。当以某种理想的方式分别计算描述子时,应该得出同样的结果。即描述子应该对光照(亮度)不敏感,具备尺度一致性(大小 ),旋转一致性(角度)等。
前面为了解决尺度一致性问题,采用了图像金字塔来改善这方面的性能。而现在,主要解决BRIEF描述子不具备旋转不变性的问题。
那我们如何来解决该问题呢?
在当前关键点P周围以一定模式选取N个点对,组合这N个点对的T操作的结果就为最终的描述子。当我们选取点对的时候,是以当前关键点为原点,以水平方向为X轴,以垂直方向为Y轴建立坐标系。当图片发生旋转时,坐标系不变,同样的取点模式取出来的点却不一样,计算得到的描述子也不一样,这是不符合我们要求的。因此我们需要重新建立坐标系,使新的坐标系可以跟随图片的旋转而旋转。这样我们以相同的取点模式取出来的点将具有一致性。
打个比方,我有一个印章,上面刻着一些直线。用这个印章在一张图片上盖一个章子,图片上分处直线2头的点将被取出来。印章不变动的情况下,转动下图片,再盖一个章子,但这次取出来的点对就和之前的不一样。为了使2次取出来的点一样,我需要将章子也旋转同一个角度再盖章。(取点模式可以认为是章子上直线的分布情况)
ORB在计算BRIEF描述子时建立的坐标系是以关键点为圆心,以关键点P和取点区域的质心Q的连线为X轴建立2维坐标系。P为关键点。圆内为取点区域,每个小格子代表一个像素。现在我们把这块圆心区域看做一块木板,木板上每个点的质量等于其对应的像素值。根据积分学的知识我们可以求出这个密度不均匀木板的质心Q。
我们知道圆心是固定的而且随着物体的旋转而旋转。当我们以PQ作为坐标轴时,在不同的旋转角度下,我们以同一取点模式取出来的点是一致的。这就解决了旋转一致性的问题。
3-5-4如何计算上面提到的质心?灰度质心法
说到解决该问题,这就不得不提到关键函数static IC_Angle ORBextractor.cc#L75 这个计算的方法是大家耳熟能详的灰度质心法:以几何中心和灰度质心的连线作为该特征点方向。具体是什么原理呢?
其实原理网上很多文章讲解的很多了,我就直接贴上公式了。
3-5-5代码如何实现?
- 首先取出关键点P。const uchar* centerORBextractor.cc#L79
- 因为实现中利用了一个技巧,就是同时计算圆对称上下两条线的和,这样可以加速计算过程。所以计算中间的一条线上的点的和进行单独处理。m_10 += u * center[u];ORBextractor.cc#L82
- 要在一个图像块区域HALF_PATCH_SIZE中循环计算得到图像块的矩,这里结合四叉树算法,要明白在ORBSLAM中一个图像块区域的大小是30,而这里说过,用了一个技巧是同时计算两条线,因此分一半,就是15,所以HALF_PATCH_SIZE=15
- 一条直线上的像素坐标开头和结尾分别是-d和d,所以for (int u = -d; u <= d; ++u)ORBextractor.cc#L92
- 位于直线关键点P上方的像素点坐标是val_plus = center[u + v*step]ORBextractor.cc#L94
- 位于直线关键点P下方的像素点坐标是val_minus = center[u - v*step]ORBextractor.cc#L94
- 因为$m_{10}$只和X有关,像素坐标中对应着u,所以$m_{10}$ = X坐标*像素值 = u * (val_plus+val_minus)
- 因为$m_{01}$只和Y有关,像素坐标中对应着v,所以$m_{01}$ = Y坐标*像素值 = v * v_sum = v * (循环和(val_plus - val_minus))ORBextractor.cc#L95
3-5-6高斯模糊是什么?有什么用?怎么实现?
所谓”模糊”,可以理解成每一个像素都取周边像素的平均值。图中,2是中间点,周边点都是1。“中间点”取”周围点”的平均值,就会变成1。在数值上,这是一种”平滑化”。在图形上,就相当于产生”模糊”效果,”中间点”失去细节。
显然,计算平均值时,取值范围越大,”模糊效果”越强烈。
注意:提取特征点的时候,使用的是清晰的原图像。这里计算描述子的时候,为了避免图像噪声的影响,使用了高斯模糊。
从数学的角度来看,高斯模糊的处理过程就是图像与其正态分布做卷积。
正态分布
我们可以计算当前像素一定范围内的像素的权重,越靠近当前像素权重越大,形成一个符合正态分布的权重矩阵。
卷积
利用卷积算法,我们可以将当前像素的颜色与周围像素的颜色按比例进行融合,得到一个相对均匀的颜色。
卷积核
卷积核一般为矩阵,我们可以将它想象成卷积过程中使用的模板,模板中包含了当前像素周围每个像素颜色的权重。
有了这些基础,我们再来看ORBSLAM到底怎么实现这个高斯模糊的?在代码中,使用的是OpenCV的GaussianBlur函数。
对每层金字塔的图像for (int level = 0; level < nlevels; ++level)ORBextractor.cc#L1105,ORBSLAM都进行高斯模糊:ORBextractor.cc#L1115
GaussianBlur(workingMat, workingMat, Size(7, 7), 2, 2, BORDER_REFLECT_101);
3-5-7怎么实现描述子的计算?
在《3-4-4-2计算特征描述子》说过,BRIEF算法的核心思想是在关键点P的周围以一定模式选取N个点对,把这N个点对的比较结果组合起来作为描述子。
其描述子desc[i]为一个字节val8位,每一位是来自于两个像素点灰度的直接比较:ORBextractor.cc#L124
t0 = GET_VALUE(0); t1 = GET_VALUE(1);
val = t0 < t1; //描述子本字节的bit0
t0 = GET_VALUE(2); t1 = GET_VALUE(3);
val |= (t0 < t1) << 1; //描述子本字节的bit1
t0 = GET_VALUE(4); t1 = GET_VALUE(5);
val |= (t0 < t1) << 2; //描述子本字节的bit2
t0 = GET_VALUE(6); t1 = GET_VALUE(7);
val |= (t0 < t1) << 3; //描述子本字节的bit3
t0 = GET_VALUE(8); t1 = GET_VALUE(9);
val |= (t0 < t1) << 4; //描述子本字节的bit4
t0 = GET_VALUE(10); t1 = GET_VALUE(11);
val |= (t0 < t1) << 5; //描述子本字节的bit5
t0 = GET_VALUE(12); t1 = GET_VALUE(13);
val |= (t0 < t1) << 6; //描述子本字节的bit6
t0 = GET_VALUE(14); t1 = GET_VALUE(15);
val |= (t0 < t1) << 7; //描述子本字节的bit7
每比较出8bit结果,需要16个随机点(参考《3-4-4-1描述子是什么东东?》)。ORBextractor.cc#L121
pattern += 16
其中,定义描述子是32个字节长,所以ORBSLAM的描述子一共是32*8=256位组成。
在《3-4-4-3如何保证描述子旋转不变性?》中说过,ORBSLAM的描述子是带旋转不变性的,有些人评价说这可能也是ORB-SLAM的最大贡献(知识有限,无法做评价,只是引入,无关对错),这么重要的地方具体体现在代码的哪里呢?作者定义了一共局部宏ORBextractor.cc#L116
#define GET_VALUE(idx) center[cvRound(pattern[idx].x*b + pattern[idx].y*a)*step + cvRound(pattern[idx].x*a - pattern[idx].y*b)]
其中,a = (float)cos(angle)和b = (float)sin(angle)
背后的原理呢?可能大家这么多知识点看下来都懵逼了,我自己一次性梳理起来也很凌乱的。那就回顾一下《3-5-3如何保证描述子旋转不变性?》,其实就是把灰度质心法找到的质心Q和特征点P就连成的直线PQ和坐标轴对齐,转个角度,就是二维坐标系的旋转公式:
3-6)总结
Frame::ExtractORB 主要完成工作是提取图像的ORB特征点和计算描述子,其主要的函数分别是ComputePyramid、ComputeKeyPointsOctTree和computeDescriptors。
ComputePyramid函数主要完成了构建图像金字塔功能。ComputeKeyPointsOctTree函数使用四叉树法对一个图像金字塔图层中的特征点进行平均和分发。computeDescriptors函数用来计算某层金字塔图像上特征点的描述子。
至此,完成了图像特征点的提取,并且将提取的关键点和描述子存放在mvKeys和mDescriptors中。
4)Frame::UndistortKeyPoints
因为ORB-SLAM3中新增了虚拟相机的模型,论文中提及:
Our goal is to abstract the camera model from the whole SLAM pipeline by extracting all properties and functions related to the camera model (projection and unprojection functions, Jacobian, etc) to separate modules. This allows our system to use any camera model by providing the corresponding camera module.In ORB-SLAM3 library, apart from the pinhole model, we provide the Kannala-Brandt fisheye model.
其实跑TUM_VI的时候,就是用的KannalaBrandt8模型,感兴趣的话可以下载数据集跑跑效果,具体方法可参考文章:
EVO Evaluation of SLAM 4 --- ORB-SLAM3 编译和利用数据集运行(https://blog.csdn.net/shanpenghui/article/details/109354918)
其中,矫正就是用的Pinhole模型,就是针孔相机模型,在代码中有体现Frame.cc#L751
cv::undistortPoints(mat,mat, static_cast<Pinhole*>(mpCamera)->toK(),mDistCoef,cv::Mat(),mK);
我们针对针孔相机模型来讨论一下。因为知识浅薄,所以想从基础讨论起,大神们可直接略过。
4-1为什么要矫正?
图像成像模型
说到相机成像,就不得不说到初中物理,透视投影。
我们可以将透镜的成像简单地抽象成下图所示:
畸变校正
理想的针孔成像模型确定的坐标变换关系均为线性的,而实际上,现实中使用的相机由于镜头中镜片因为光线的通过产生的不规则的折射,镜头畸变(lens distortion)总是存在的,即根据理想针孔成像模型计算出来的像点坐标与实际坐标存在偏差。畸变的引入使得成像模型中的几何变换关系变为非线性,增加了模型的复杂度,但更接近真实情形。畸变导致的成像失真可分为径向失真和切向失真两类:
径向畸变(Radial Distortion)
简单来说,由透镜形状(相机镜头径向曲率的不规则变化)引起的畸变称为径向畸变,是导致相机成像变形的主要因素。径向畸变主要分为桶形畸变和枕型畸变。在针孔模型中,一条直线投影到像素平面上还是一条直线。但在实际中,相机的透镜往往使得真实环境中的一条直线在图片中变成了曲线。越靠近图像的边缘现象越明显。由于透镜往往是中心对称的,这使得不规则畸变通常径向对称。(成像中心处的径向畸变最小,距离中心越远,产生的变形越大,畸变也越明显 )
- 正向畸变(枕型畸变):从图像中心开始,径向曲率逐渐增加。
- 负向畸变(桶形畸变):边缘的径向曲率小于中心的径向曲率。(鱼眼相机)
实际摄像机的透镜总是在成像仪的边缘产生显著的畸变,这种现象来源于“筒形”或“鱼眼”的影响。如下图,光线在原理透镜中心的地方比靠近中心的地方更加弯曲。对于常用的普通透镜来说,这种现象更加严重。筒形畸变在便宜的网络摄像机中非常厉害,但在高端摄像机中不明显,因为这些透镜系统做了很多消除径向畸变的工作。
切向畸变(Tangential Distortion)
切向畸变是由于相机镜头在制造安装过程中并非完全平行于成像平面造成的。不同于径向畸变在图像中心径向方向上发生偏移变形,切向畸变主要表现为图像点相对理想成像点产生切向偏移。
4-2怎么矫正?
径向畸变模型:r 为像平面坐标系中点(x, y)与图像中心(x0, y0)的像素距离。
切向畸变模型可以描述为:$p_1$和$p_2$,镜头的切向畸变系数。
所以要想矫正图像,最终需要得到的5个畸变参数:
我们来理一理矫正和不矫正坐标之间的关系。
4-3代码怎么实现?
ORBSLAM中,用内参对特征点去畸变。
- 首先判断是否需要去畸变。
if(mDistCoef.at<float>(0)==0.0)
- 利用OpenCV的函数进行矫正
cv::undistortPoints(mat,mat, static_cast<Pinhole*>(mpCamera)->toK(),mDistCoef,cv::Mat(),mK);
具体实现就不展开了,感兴趣可以找OpenCV相关资料。
5)Frame::AssignFeaturesToGrid
将图片分割为64*48大小的栅格,并将关键点按照位置分配到相应栅格中,从而降低匹配时的复杂度,实现加速计算。举个例子:
当我们需要在一条图片上搜索特征点的时候,是按照grid搜索还是按照pixel搜索好?毫无疑问,先粗(grid)再细(pixel)搜索效率比较高。
这也是Frame::GetFeaturesInArea函数里面用的方法,变量mGrid联系了 AssignFeaturesToGrid 的结果和其他函数:Frame.cc#L676
for(int ix = nMinCellX; ix<=nMaxCellX; ix++)
{
for(int iy = nMinCellY; iy<=nMaxCellY; iy++)
{
const vector<size_t> vCell = (!bRight) ? mGrid[ix][iy] : mGridRight[ix][iy];
而mGrid这个结果在代码中,后面的流程里,有几个函数都要用:
SearchForInitialization 函数 单目初始化中用于参考帧和当前帧的特征点匹配
SearchByProjection 函数 通过投影地图点到当前帧,对Local MapPoint进行跟踪
具体怎么分配呢?
- 先分配空间
mGrid[i][j].reserve(nReserve);
- 如果找到特征点所在网格坐标,将这个特征点的索引添加到对应网格的数组mGrid中
if(PosInGrid(kp,nGridPosX,nGridPosY))
mGrid[nGridPosX][nGridPosY].push_back(i);
6)总结
总而言之,Frame起到的是前端的作用,主要的作用完成对图像特征点进行提取以及描述子的计算:
1. 通过构建图像金字塔,多尺度表达图像,提高抗噪性;
2. 根据尺度不变性,计算相机与各图层图像的距离,准备之后的计算;
3. 利用四叉树的快分思想,快速筛选特征点,避免特征点扎堆;
4. 利用灰度质心法解决BRIEF描述子不具备旋转不变性的问题,增强了描述子的鲁棒性;
5. 利用相机模型,对提取的特征点进行畸变校正;
6. 最终通过大网格形式快速分配特征点,加速了运行速度。
以上仅是个人见解,如有纰漏望各位指出,谢谢。
参考:
1.数字图像处理(21): 图像金字塔(高斯金字塔 与 拉普拉斯金字塔)
2.ORB_SLAM2中特征提取之图像金字塔尺度不变性理解
3.ORB-SLAM(一):关于ORB-SLAM中四叉树的使用
4.ORB-SLAM中的ORB特征(提取)
5.ORB-SLAM中四叉树管理角点
6.高斯模糊(高斯滤波)的原理与算法
7.相机的那些事儿 (二)成像模型
8.【二】[详细]针孔相机模型、相机镜头畸变模型、相机标定与OpenCV实现
9.全局视觉定位
备注:作者也是我们「3D视觉从入门到精通」特邀嘉宾:一个超干货的3D视觉学习社区
本文仅做学术分享,如有侵权,请联系删文。