ORB-SLAM3源码阅读笔记4:ORB-SLAM3中的相机模型与特征提取、匹配

Aug 1,2021   6524 words   24 min

Tags: SLAM

在ORB-SLAM3中,一大特色就是使用了更为通用和抽象的相机模型,使其可以支持多种相机,如针孔、广角等。本文档简单介绍ORB-SLAM3中相机模型相关实现,不过多涉及理论知识。

1.支持的相机模型

在ORB-SLAM3中,相机模型相关文件放在include/CameraModels文件夹下,如下所示。 目前主要支持两种相机模型:KannalaBrandt8Pinhole。后者就是我们熟悉的针孔相机模型,对应于普通常见的相机。而前者,是ORB-SLAM3中新引入的模型。KannalaBrandt8全称Kannala-Brandt Camera Model,它是由J. Kannala、S.S. Brandt等人在2006年提出的一种通用相机模型,可以覆盖针孔、广角和鱼眼成像模式。相关论文是《A generic camera model and calibration method for conventional, wide-angle, and fish-eye lenses》,发表于2006年的PAMI。KB模型假设图像光心到投影点的距离和角度的多项式存在比例关系,这个角度是点所在的投影光线和主轴之间的夹角。KB模型有时候被作为针孔相机的畸变模型,同时也是OpenCV中使用的鱼眼相机模型,而在Kalibr 中,被用作等距畸变模型(equidistant distortion model)。

在ORB-SLAM3中, GeometricCamera则是一个抽象类,在其中定义了一个成像模型应该包含的函数,以虚函数形式进行编写,部分如下。 不同的相机模型继承GeometricCamera并实现对应的虚函数,由对应的CPP和H文件组成,如KannalaBrandt8类、Pinhole类。

2.相机模型中重要的成员函数和变量

我们首先以Pinhole模型为例进行介绍。

2.1 Pinhole相机模型

Pinhole相机模型类是集成于GeometricCamera类,所以一些成员变量也直接继承,如下。 在GeometricCamera类中,有如下成员变量。 mvParameters就是存放成像模型参数的vector。mnId用于指定当前对象的ID,mnType则是用于说明成像模型的类型。在GeometricCamera类中定义了public的类型,如下。 回到Pinhole类,我们可以看到,构造函数中主要就是设置了相机参数vector的长度、ID以及成像类型。 这里nNextId是在Pinhole.cpp文件中被定义并且赋初值为0的,如下。 其核心函数包括参数赋值、投影等。Pinhole类重载了«运算符,从而使得我们可以方便的将相机参数传递进来,如下所示。 也写了SkewSymmetricMatrix()函数来方便的将传入的相机参数转换成相机内参矩阵,如下。 同时,Pinhole类也封装了一些和投影相关的函数,比如计算点的投影(3D->2D)等,如下。 包含project() 、projectMat()函数,当然针对不同输入数据进行了重载。同时也有将2D->3D的函数unproject()和unprojectMat(),如下。 同时,还有返回某个观测点的不确定性的函数uncertainty2(),如下。 不过目前在ORB-SLAM3中, 对于Pinhole和KannalaBrandt相机模型,这个值都是一个常数1.0

2.2 KannalaBrandt相机模型

KB相机模型与Pinhole相机模型是类似的,只是在具体的实现方式上不同。对于KB模型,有8个模型参数 通过重载«运算符实现赋值,如下。 其它所有函数的输入和输出都和Pinhole类型一样。

3.系统中的调用

3.1 相机模型的建立

在ORB-SLAM3系统中,通过Tracking的构造函数加载相机模型参数。当然除了加载相机模型参数,也加载了ORB参数IMU参数。对应编写了ParseCamParamFile()ParseORBParamFile()ParseIMUParamFile()函数。 更具体来说,在Tracking类中定义了ParseCamParamFile()函数来专门处理相机模型。在这个函数中,根据不同的输入传感器配置进行参数设置,构造相机模型。 首先,函数会获取配置文件中的Camera.type属性,目前ORB-SLAM3支持的有两种模型:PinHole和KannalaBrandt8。在Tracking类中,有专门的mpCamera和mpCamera2这两个protected变量来处理相机模型,如下所示。 关于这两者的具体说明和区别,下面会进行介绍。

3.1.1 Pinhole

对于PinHole模型,分别从配置文件中读取相机的内参(fx,fy,cx,cy)以及畸变参数(k1,k2,p1,p2,k3),比如以fy为例如下。 对于所有的内参,构造了临时vector变量vCamCalib用于存放。对于所有的畸变参数,都赋给了Tracking的Mat成员变量mDistCoef

在加载完所有参数以后,我们利用传入的内参新建一个Pinhole相机模型对象并将其赋给Tracking的成员变量mpCamera,再将其添加到地图集mpAtlas中,如下。 对于Atlas的成员函数AddCamera()而言,其实就是将传入的相机模型添加(push_back)到Atlas的mvpCameras成员变量中,如下。 这样Pinhole模型就加载并新建完成了。

3.1.2 KannalaBrandt8

对于KannalaBrandt8相机模型,流程是类似的,只是具体的一些相机参数不同。具体而言,ORB-SLAM3会加载相机内参(fx,fy,cx,cy)畸变参数(k1,k2,k3,k4)共8个参数。如果这几个参数都顺利加载,那么就将其全部放到临时vector变量vCamCalib中,并基于此新建相机模型对象再将其赋给Tracking成员变量mpCamera,如下所示。 与Pinhole模型不同的是,这里对传感器类型进行了判断。如果是KannalaBrandt8成像模型,并且传感器包含双目相机,那么就再添加一个右相机模型对象之前添加的那个mpCamera就被当作是左相机模型对象,如下图所示。 同时,对于KannalaBrandt8模型的双目相机而言,如果配置文件中有lapping相关属性的话,也读取进来,每个相机有起始和终止数值,一共有leftLappingBegin, leftLappingEnd, rightLappingBegin, rightLappingEnd四个变量。

如果加载的参数都正确,就再新建一个相机模型,并将其赋给Tracking的成员变量mpCamera2,如下图所示。同时将刚刚读取的四个lapping相关变量赋给mvLappingArea。需要注意的是,如果没有读取,这些变量默认值都是-1。 最后再将新建处理好的mpCamera和mpCamera2都添加到地图中,如下图所示。至此就完成了KannalaBrandt8相机模型的创建以及与地图的绑定。 这里需要注意的一点是,在向地图中添加相机模型时对于KannalaBrandt8相机模型而言,无论是单目还是双目,都会添加两个相机模型。其区别在于,如果是单目的话,mpCamera不为空,而mpCamera2为空(因为没有执行上面提到的初始化代码);如果是双目,mpCamera和mpCamera2两个都不为空。因为mpCamera2这个变量在Tracking的构造函数中被赋初值为空指针,如下图所示。

3.1.3 其它步骤

在ParseCamParamFile()函数根据不同条件创建完不同相机模型对象以后,还进行了一些操作。

首先程序会判断当前传感器是不是包含双目相机(不管是什么相机模型),如果包含,就再读取一个Camera.bf属性。该属性用于表述双目相机的基线长度,如下图所示。 然后会读取一系列相机属性,如Camera.fps,Camera.RGB等。然后还是和上面类似的,如果传感器包含双目相机或者是RGBD相机,再读取一个ThDepth属性,如下所示。该属性用于表示近远点深度的阈值。 最后,如果当前传感器是RGBD的话,再读取一个DepthMapFactor属性,如下所示。

3.1.4 小结

所以,对于ORB-SLAM3中的相机模型,我们可以得到一个结论:对于Pinhole相机模型而言,无论真实传感器是否为单目或者双目,在ORB-SLAM3中只创建一个相机模型对象mpCamera;对于KannalaBrandt8相机模型而言,如果传感器是单目,就只创建一个相机模型mpCamera,如果是双目,则创建mpCamera、mpCamera2两个对象。同时,下表总结了现在的ORB-SLAM3自带的配置文件中不同的相机模型: 从表中也可以看到,绝大部分情况下,我们用不到mpCamera2,换句话说就是mpCamera2变量为nullptr只有在KannalaBrandt8模型,并且为双目配置的时候,它才有实际的值

3.2 相机模型在系统中的调用

本部分由一个问题引出:ORB-SLAM3中是如何表示双目左右影像上的特征点个数的?进一步又有以下问题:ORB-SLAM3在哪个函数中提取了特征点哪个函数调用了特征点提取函数

对于特征点提取相关问题,在之前的文档中其实已经回答的相对清楚了。ORB特征的提取是在Frame类的构造函数中进行的。而Frame类的构造函数又是在Tracking类的GrabImageStereo()函数中被调用的,如下图所示。如果再往上说的话,GrabImageStereo()函数在System类的TrackStereo()中被调用 可以看到重载了多个Frame类构造函数,应对不同传感器和数据,简单列举如下:

  • 双目相机(Pinhole)
  • 双目相机(KannalaBrandt8)
  • IMU+双目相机(Pinhole)
  • IMU+双目相机(KannalaBrandt8)
3.2.1 ORB特征提取

ORB特征提取被写在Frame的成员函数中。在Frame类中有public类型的ExtractORB()函数负责特征提取,如下所示。这其实是对ORBExtractor括号运算符的二次封装 第一个flag参数表示是双目的左影像还是右影像,如果是左影像就为0,右影像就为1。第二个参数是传入的影像。第三个参数专门为双目KannalaBrandt模型设计,表示LappingArea起始位置,第四个参数则表示LappingArea结束位置。

在Frame()构造函数中,通过开启两个线程运行ExtractORB()函数,同时,由于Pinhole和KannalaBrandt8模型有不同的参数,所以调用起来也有细微的差别,如下图所示是Pinhole双目模型提取ORB特征的代码。 下面则是KannalaBrandt8双目模型提取ORB特征的代码。 可以看到最核心的区别就是,对于Pinhole而言,由于没有lapping,所以传入的相关参数都为0,而对于KannalaBrandt8双目而言,是有lapping的,所以传入相关读取的参数(当然也不是所有KannalaBrandt8都有lapping属性,如果没有,这里传入的值就都为默认值-1)

对于ORB特征提取而言,有没有IMU是没有任何影响的,所以IMU+Pinhole双目调用的函数还是和Pinhole双目是一样的。

3.2.2 特征的数量表示

那么回到Frame类的ExtractORB()函数,执行完之后,提取的左右影像的特征点会分别被放到Frame的public成员变量mvKeys和mvKeysRight中,描述子会被放到public成员变量mDescriptors和mDescriptorsRight中。所以,我们也就可以很容易的获取到左右影像上特征点的个数。这里,针对Pinhole和KannalaBrandt8模型,又有不同的方式。

对于双目Pinhole模型而言,无论是否有IMU,我们都只取左影像上的特征点个数(mvKeys.size())作为该帧的特征点个数N,没有用到右影像上提取的特征点个数(mvKeysRight.size()),如下图所示。 对于双目KannalaBrandt8模型而言,我们会分别获取左右影像上的特征点个数,并将其分别赋给Nleft、Nright。而最终的总特征点个数N为两者之和 也就是说,只有双目KannalaBrandt8模型Nleft、Nright才有值,其余情况下,只有N有值,Nleft和Nright默认都为-1,我们可以简单将各种情况总结如下表。 在实际代码中,会经常看到取出Frame中的N、Nleft、Nright进行判断或者作为迭代上下界。 比如说如上图所示,在PoseInertialOptimizationLastKeyFrame()函数中,就分别获取了N和Nleft。并且还新建了一个临时变量bRight,用于判断是哪种相机模型(如果为-1,则说明是Pinhole,否则为KannalaBrandt8)。

又比如说,在Tracking类的StereoInitialization()函数中,就通过mpCamera2这个变量来对不同相机模型进行判断,如下。 在双目的前提下,如果没有mpCamera2,那么就说明是普通的Pinhole,N表示特征点个数;否则就说明是KannalaBrandt8模型,Nleft表示特征点个数。

我们也可以简单总结一下传感器、成像模式与变量之间的关系,如下表。

3.2.3 双目相机左右影像的匹配

在前面,我们分别在左右影像上提取了特征点,并将特征点分别放在了mvKeys和mvKeysRight中,描述子放在mDescriptors和mDescriptorsRight中。有了左右影像的特征点,自然就是要进行特征点的匹配了。左右影像的匹配同样是在Frame()构造函数中完成的,不同成像模型有不同函数。对于Pinhole,使用ComputeStereoMatches()函数;对于KannalaBrandt8,使用ComputeStereoFishEyeMatches()函数

对于Pinhole模型的ComputeStereoMatches()函数,整体的思路就是以左影像上的特征点为准,逐个迭代,在右影像对应的范围内进行搜索,找到最匹配的那个特征点,如下图所示。 找到匹配以后,将右影像上对应点的横坐标存储在mvuRight,计算的深度存储在mvDepth。

对于双目KannalaBrandt8模型,使用ComputeStereoFishEyeMatches()函数,核心流程如下。 首先对mvLeftToRightMatch、mvRightToLeftMatch、mvDepth、mvuRight、mvStereo3Dpoints进行了一系列的赋初值,都为-1。这样的作用就是,当后续需要判断的时候,如果某个值为-1,则说明匹配不成功,就跳过

这里涉及到三个Frame的public成员变量:mvLeftToRightMatchmvRightToLeftMatchmvStereo3Dpoints,如下图所示。前两个用于保存某个特征点在另一幅影像中所对应的索引,而最后一个则是保存根据匹配点得到的三维点坐标 当然最后还有一个ComputeStereoFromRGBD()函数,如下所示。顾名思义就是用于RGBD的传感器,但这里用不到,所以就不过多介绍了。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部