特龙智慧-GMapping
特龙智慧
我在大二上学期购买了《概率机器人》一书,当时还没有学概率论,所以我看不懂(但是我大受震撼)。大二下的暑假曾经学了一段时间,但是没有深入,到第五章就结束了。直到现在,大四了,项目有这方面的需求了,才重新开始看特龙(Sebstian Thrun)这本口碑很好的书。尽管这本书是2006年出版的,其中的很多思想在现在的我看来,都还很有指导意义。非常后悔,自己没能在之前花精力啃下这本SLAM以及移动机器人著作。
GMapping(以及相关的粒子滤波SLAM方法)或多或少都有他的参与,最近也刚好读了相关的论文,并且仔细研读了其代码(OpenSLAM上的💩山,literally),故我把这些笔记整理成了一篇文章:
- 本文作者并没有特龙,但是估计是相关团队的文章:Improved Techniques for Grid Mapping with Rao-Blackwellized Particle Filters
- 上面这篇论文可以说是:IROS2003:An Efficient FastSLAM Algorithm for Generating Maps of Large-Scale Cyclic Environments from Raw Laser Range Measurements的升级版(这篇论文的建议分布是“学”出来的)。
II. GMapping 论文分析
2.1 大致思想
gmapping是一篇基于粒子滤波的配准工作,其主要创新点只有一点:就是得到了一个改进的粒子滤波建议分布(improved proposal distribution),这篇文章既没有涉及到回环检测、后端优化,也没有深入分析前端的配准算法。其主要贡献就是:改进粒子滤波在某些传感器上的表现。
我们考虑激光雷达,一般来说,激光雷达的测距精度很高,在配准算法具有很好的表现情况下时,可以认为配准也有比较好,配准结果噪声较小。因为粒子滤波的主要思想是:
- 我先建模一个易于获取的建议分布(proposal)
- 再建模一个【近似的】建议分布与目标分布之间的差别w(权重)(根据贝叶斯理论,一般是乘性的因子)
可见,如果目标分布(贝叶斯后验)很好获得,那么我们可以直接将目标分布当作建议分布,并且使得这差别权重都相等,为1。但是一般来说,需要粒子滤波解决的问题都不会有简单的目标后验,故建模一个好的建议分布是非常必要的:
- 一个好的建议分布可以使得【近似的】差别权重对于其近似性的要求降低
- 好的建议分布可以提高采样的质量
传统意义上,建议分布一般会使用odometry的运动模型分布,比如沿着运动方向的与垂直运动方向两个方向为主轴的高斯分布。在粒子的更新阶段(也即从建议分布采样阶段),我们根据控制
When using the odometry model as the proposal distribution in such a case, the importance weights of the individual samples can differ signifificantly from each other since only a fraction of the drawn samples cover the regions of state space that have a high likelihood under the observation model
举一个例子:假设我们有一条长走廊,长走廊较为理想。那么对于配准来说,长走廊情况下的配准协方差沿着走廊方向的分量更大,但是垂直走廊方向很小,而另一方面,odometry在两个方向上可能都有较大的协方差。如果我们使用odometry的协方差,在更新采样环节可能会采到次优的位置,但如果此时结合配准误差、里程计协方差,可以将:
- 垂直长走廊方向的协方差降下来,沿着长走廊方向的协方差则可以由odometry主导。
从这样的分布中采样,可以得到更好的结果。
此外,还有另一个问题。为什么你在实现PF时,使用完全随机的采样策略效果并不好?这篇论文中的一个解释我觉得非常有道理:
However, if the observation likelihood is peaked the number of pose samples
that has to be sampled from the motion model is high, since a dense sampling is needed for sufficiently capturing the typically small areas of high likelihood. This results in a similar problem than using the motion model as the proposal: a high number of samples is needed to sufficiently cover the meaningful region of the distribution.
之前也没有想得太深,只是觉得角度上的采样不足,于是增大了角度采样。现在想想,整个后验分布确实就是一个具有突出峰值的分布,在实验过程中也发现了这一点(实验是上一篇博客中的粒子滤波,更新过程中发现,收敛时的粒子分布的确呈尖峰形状)。
2.2 如何结合观测
PF更新公式如下:
- 首先我根据scan-matcher确定一个ROM(region of meaning,有意义的区域),也即是配准认为可能的分布位置
- 在这个有意义的区域内进行多次采样。多次采样的结果,使用scan-matcher得到
(似然),而由于控制已知(均值已知),参数一般也已知,那么分布就已知,则在选取的点可以计算 - 我们需要计算的improved proposal是:
。这个的物理意义是:在第i个粒子对应的map 下,由 由控制量 转移,观测到 的条件下,在 位置的概率。看起来这个非常像后验,但是其计算是近似的。
但是实际上,在RBPF中,更新公式有所不同。因为RBPF是专门针对SLAM设计的一种粒子滤波,在滤波过程中,各种概率分布需要包含地图信息。针对RBPF的权重更新公式如下:
论文中的更新迭代策略是这样的:
- 根据里程计信息,作为初值,进行配准,得到带有观测信息的
周围(一个球内)采样,每个采样点 都可以根据里程计的高斯分布以及配准的似然域计算点权( ),注意,所有点的点权之和,就是更新公式的 。- 根据采样的点可以计算高斯分布,最后得到的粒子从这个高斯分布中采样。
非常奇怪的是,网上所有GMapping的实现,都没有实现多点采样生成高斯分布再进行重新采样的步骤。
III. GMapping 代码流程
3.1 processScan
processScan是核心函数:
3.1.1 运动模型
首先从运动模型中采样(drawFromMotion
函数,此函数的原理在《概率机器人上》)
3.1.2 计算移动距离
计算移动距离:m_linearDistance
,
m_angularDistance
- 保证平移距离不过大,过大则认为位置发生了突变
- 当移动距离过大,或是长时间没有更新,那么就需要进行一次配准
3.1.3 scanMatch
scanMatch首先根据plainReading(也就是点云原始数据)进行配准,配准使用搜索算法,在optimize
函数中。注意代码里有bug:
1 | if (bestScore>=currentScore) |
六个方向移动,直到达到最大分数或者迭代次数,每次减小步长
score的计算,在之后的函数中说. 配准函数针对了每一个粒子,每个粒子都进行了一次优化
likelihoodAndAScore
函数
进行似然计算:大概做了这样的事情:
- skip是跳过处理的一种实现,指的是:激光线太多,信息冗余,可以跳过一个激光线计算似然域
- 首先把激光器的位姿投影到世界坐标系下,此后对于每一条需要处理的激光线,也转到世界坐标系下,并且确定一个位于空域的点位置(激光线截短一截)
- 在一个领域内,查找最适合的障碍物点(因为激光测距会存在噪声),障碍物点需要满足:该点占用概率大于阈值,对应空域点占用概率小于阈值
- 求均值,并求激光点世界坐标位置与该均值的距离(越小说明越应该是这个障碍点)
- 求似然。这样求出的似然域是较为平滑的,故在scanMatch的optimize中,计算score也是用了类似的平滑函数
computeActiveArea
:是一个更新地图操作,因为地图是基于占用栅格的,未知区域也是需要表征的,限制范围可以节约内存资源。computeActiveArea更新了地图大小(每个粒子):
1 | //this allocates the unallocated cells in the active area of the map |
在此前,先根据scanMatch计算的位姿,计算了占用栅格(每条激光线经过的每个点),将占用栅格加入到map中,等待对地图进行更新。
3.2 updateTreeWeights
updateTreeWeights只做了三件事:
- 归一化权重
- 轨迹树重置
- 权重沿着树传播
轨迹树会不会做的是这样一件事情呢?初始时粒子都在同一位置,在迭代过程中,也可能出现两个粒子位姿一致的情况吗?会有一个parent对应了很多child的情况吗,轨迹树的child会怎么来?
propagateWeights就是一层一层将叶子节点的weight向上传播(因为在scanMatch中会重新衡量叶子节点的weight,而上层weight(相当于之前的位姿)是逐层累加的)。
但是resample计算使用的是当前叶子节点的权重,与上层的权重无关。那么这样逐层累加的weight,其作用是什么?weightSum
在另一个类中被使用了。看代码时没有注意继承关系,以为只有GridSlamProcessor
是主要模块,最后发现openslam版本中,还有一个类叫做:GridSlamProcessorThread
,内部实现了一些父类没有实现的回调函数,在这个类中定义的onScanmatchUpdate
函数中,用到了weightSum:
1 | if(part->weightSum>bestWeight){ |
算法选择权重综合最大的一个粒子(故与历史信息相关了),取该粒子对应的map作为当前地图,根据最大权重的粒子更新地图,并且选择最优粒子的位姿当作自身当前位姿。
不过根据算法的逻辑,每次计算了底层叶子节点的weight之后,上层需要根据底层来的值修改权重累加,但是非叶子节点的weightSum并没有用(weightSum本身就没有被用过多少次),只有叶子节点(当前最新的particles)对应的weightSum才有价值,故propagateWeights以及相应函数其实是没有作用的。
3.3 重采样
resmapleIndexes函数:这里的重采样策略使用的也是《概率机器人》上的低方差重采样。故可能出现:两个粒子重复的情况。返回的是被采样到的粒子的下标。在这个函数的内部,由于使用的策略是低方差采样,下标是有顺序的,故采样的结果也是有顺序的。
而new TNode
在整个项目中出现在:
gridslamprocessor_tree.cpp
的integrateScanSequence
中,没有实际调用gridslamprocessor_tree.cpp
的getTrajectory
中,在GSP构造时使用,所以对代码逻辑的影响应该不大gridslamprocessor.hxx
的resample
函数中用到过,此处的使用对代码逻辑有实际的影响
建立以及更新权重轨迹树的流程与作用:
- 根据权重进行低方差采样,结果被保存在
m_indexes
之中,注意m_indexes
是顺序化的,比如:原来有8个粒子,id从0-7,重采样之后成为:1 1 1 4 4 4 7 7,那么在轨迹树中,0,2,3,5,6均会被删除(假设没有其他关联,整条与这几个节点有关的树枝均会被切除) - 轨迹树实际上就是重采样树,0 1 2 3 4 5 6 7 这8个节点,按照上面的重采样例子会生成如下图所示的树:
重采样得到点就相当于子节点,重采样前的节点是父节点。
- 删除所有重采样后没有结果的点,比如假设上述例子又发生了一次重采样,(7-1)(7-2)权重过小,那么(7-1)(7-2)(7)都会被删除
- 对重采样后的新particles,根据当前的点云信息,更新地图(activeArea重新计算),但是权重会被重置为0。那么这个为0的权重,将会在scanMatch函数中被修改(计算完likelihood之后,权重会被设置为likelihood),并且在weightSum中也累加一份。
IV. 流程总结
我已经发现了:openslam的代码又臭又长,还不能编译,令人根本不知道processScan这个函数究竟在什么情况下被使用,貌似因为下载的是最原始的版本。
V. 效果
2007啊!他们太强了。不过也不能说完美,有如下几个问题:
- 5cm grid 精度可以适应大多数的需求,所以2D SLAM方向做的人越来越少了(基本满足需求了),而我尝试使用更高精度grid的时候(比如2.5cm),配准将发生错误(我猜是grid太小了,导致了激光器模型的稀疏性问题)
- 同时,grid变小将会占用过多的内存(1cm大小的格子跑Intel数据集貌似可以跑到10GB内存占用,真的无法想象当年他们那个条件怎么做实验的)
- 丢帧将会导致非常严重的问题,当点云频率很高的时候,丢帧将直接导致配飞(很奇怪,里程计不是用上了吗?)
- 貌似没有实现2007年论文的improved proposal思想。
以下测试的地图均为5cm大小grid,intel数据包由于点云帧率低,我的i5上可以开8倍速播放包,而我自己做的仿真数据集只能2倍速(否则丢帧直接死掉),粒子个数均为15个,其他参数都是默认参数。