首页 / 资讯中心 / 文章详情

从零构建2D激光SLAM建图模块:前端里程计、栅格地图与回环检测实战解析

从零构建2D激光SLAM建图模块:前端里程计、栅格地图与回环检测实战解析 ★ FEATURED ARTICLE
说实话刚开始接手 lingbot-map 的时候我一度以为这就是跑通一套开源建图算法的事——传感器接上点云灌进去地图自己就出来了。结果真做实了才发现从能出图到出好图中间隔着的全是细节。这篇文章就把我做 lingbot-map 的全过程拆开讲讲包括前端里程计怎么处理、地图表示怎么选、回环检测踩了哪些坑、真机实测又暴露了什么问题适合正在做机器人建图、导航地图模块或者接手了类似建图项目的同学参考。内容偏实操能动手的地方我都尽量给了具体参数和做法。1. lingbot-map 的最初定位不追求大而全只求建图够可靠1.1 项目起因和任务边界lingbot-map 是在某款室内移动机器人原型上做的地图构建模块。机器人的定位很简单在几百平米的办公室、走廊和展厅环境里自主行走需要输出一张可供导航使用的二维栅格地图。底盘是常见的两轮差速结构传感器配置是一颗单线激光雷达加一个低成本的九轴 IMU轮速计集成在驱动电机上。没有视觉传感器所以整个建图方案完全围绕 2D 激光、IMU 和轮速这三类数据展开。一开始我确实想过直接用现成方案但很快就碰到了问题现成方案对传感器的内外参数标定要求比较高而且很多参数是针对特定型号雷达调过的换一颗雷达就要重新折腾。团队里有人提过用较重的开源框架但那个框架的设计逻辑偏通用集成到我们这套嵌入式主控上内存和CPU占用明显偏高。主控是一块四核 ARM 开发板还要同时跑导航、避障和UI进程留给建图的资源非常有限。所以 lingbot-map 的定位从一开始就很明确轻量、可裁剪、依赖少能在资源受限的板子上稳定跑完数据进来—里程计更新—地图更新—回环修正这条链路。更重要的是方便我们针对自己的底盘和传感器做定制而不是被通用方案牵着走。1.2 功能边界与性能指标为了让项目不失控我把 lingbot-map 的应用场景限定为室内结构化环境也就是以墙面、走廊、桌椅腿、货架等静态结构为主的空间。环境中的动态物体人、门、移动的箱子只做滤除不做分类识别。性能指标上当时定了三条硬指标在室内 300-500 平米区域连续建图 30 分钟地图漂移不超过 20 厘米。这里说的漂移是建图结束后用同一位置、同一角度去做定位比对算出来的起点位置偏移。单帧激光数据从进模块到地图更新完成耗时小于 50 毫秒。这个数据决定了建图线程能不能在 10Hz 激光频率下跑得过来。建图进程峰值内存控制在 800MB 以内其中地图存储占大头。这三条指标直接影响后面的算法选型。比如帧间匹配用点线 ICP 而不是点云配准内存里不保留全量点云只保存关键帧位姿和子图信息。说白了很多看似好用的重型算法在这个资源约束下根本跑不动你必须学会做减法。1.3 模块总体划分思路lingbot-map 在代码结构上分成三层数据接入层负责雷达、IMU、轮速数据的接收、时间戳对齐和畸形数据过滤。建图核心层包括前端里程计、子图更新、后端全局优化、回环检测四部分。输出层提供二维栅格地图的实时发布、保存以及地图坐标系下的机器人位姿查询。这个分层的核心用意是让前后端可以独立开发。前端里程计和后端优化虽然共享关键帧数据但两者运行的频率完全不同。前端跟着传感器数据跑可能是 10 到 20Hz后端优化则只在插入关键帧或回环检测成功时触发通常是每秒 1 到 2 次。如果写成一个进程里的连续循环很多时间片会浪费在等待上。2. 前端里程计数据对齐和运动畸变才是真正的门槛2.1 多传感器的时间戳同步策略刚开始做的时候我把主要精力放在配准算法选型上结果第一个坑就出在数据同步上。雷达大约 10Hz 发布一次完整扫描IMU 是 200Hz轮速计是 50Hz三个数据的频率不匹配而且各自的时钟由不同驱动维护时间戳只是驱动收到数据时的系统时间存在几十毫秒的抖动。在室内移动机器人的行进速度下0.1 秒的时间偏差意味着几毫米到几厘米的位移误差。这个误差放在单帧配准里看似不大但建图是连续累积的过程一小时下来就非常可观。我最后采用的做法是在数据接入层为每类传感器各开一个环形缓冲缓存最近 0.5 秒的数据。前端里程计每收到一帧激光就根据这帧激光的起始时间戳从 IMU 和轮速缓冲里取对应时间附近的采样点做一次线性插值计算此时刻的短时间增量位姿。这里的插值不是对位置做线性插值就完事了姿态分量要先转成旋转向量再插值否则欧拉角的插值在大角度变化时会出现跳变。另外还有一个容易被忽略的问题就是雷达驱动本身的时间戳不一定是扫描开始时间有些驱动给的是扫描结束时间。如果拿结束时间去对齐 IMU等于整帧数据都带着一帧扫描周期的延迟。我当时在代码里加了配置项允许指定时间戳含义是开始还是结束这样换不同型号雷达的时候不用改代码只需要改配置。2.2 激光扫描运动畸变的补偿原理雷达在工作时激光发射器是旋转的一帧完整的扫描通常需要几十毫秒。如果雷达装在一个静止的底座上这一帧雷达数据可以认为都对应同一个位姿。但如果雷达在装着它的机器人上移动扫描前半段和后半段对应的是不同的机器人位置这就产生了运动畸变。打个比方你在旋转手电筒扫过一面墙的同时人还在往前走。你看到的墙的模样就是旋转扫描和直线运动叠加出来的结果。对激光建图来说如果不做运动畸变补偿墙壁和障碍物在图上会边沿模糊甚至出现拖影在快速转弯的时候雷达数据甚至会被拉成斜线。解畸变的做法并不复杂拿到一帧激光扫描时先根据 IMU 和轮速插值出扫描期间每个激光点的对应时刻的机器人位姿增量再把每个激光点从雷达当前时刻坐标系变换到扫描起始时刻坐标系。这样一帧数据就相当于机器人瞬间扫出来的消除了运动影响。实际操作里我对每一帧扫描做了分段处理把整帧按角度均分成 4 段每段使用该段中间时刻的插值位姿来补偿。分段越多精度越高但计算量也大。在嵌入式板子上4 段是一个性能和精度的平衡点实测畸变残差从补偿前的 5-8 厘米降到了 1 厘米以内。2.3 帧间匹配的选型点线 ICP 为什么比点云配准合适前端里程计的核心任务是估计相邻两帧激光之间的相对位姿。可选的方向有两种一种是基于特征匹配比如提取环境中的线段和角点来匹配另一种是迭代最近点家族的算法。考虑到室内环境墙角和直线非常多特征的重复度高直接做特征匹配很容易出现多解。我最终选了点到线的 ICP也就是在每次迭代时把当前帧的激光点投影到参考帧上找到最近的两个点拟合出一条线段然后计算点到这条线段的距离作为误差项来优化位姿。相比点到点 ICP点到线 ICP 在走廊这类环境中收敛更稳定因为单点可能因为雷达的离散角度出现错配而线段约束更连续。调参的时候有几点值得注意。首先是迭代次数我设置了最多 30 次迭代但实际大部分帧 5-10 次就收敛了。其次是初始位姿如果直接给单位矩阵在快速旋转时很容易收敛到局部极小。所以我把 IMU 的航向角变化和轮速的位移量作为初始猜测传进去IMU 的短时间航向变化非常准这样初始位姿和真实值之间的偏差被控制在很小的范围内。匹配器跑完之后我还会做一步验证如果最终匹配残差过大或者收敛后的位姿变化量超过了轮速计给出的合理范围就判断这一帧数据可疑不更新里程计状态等下几帧再恢复。这种保守策略虽然会在极端情况下丢失几帧数据但能避免一次失误污染整个轨迹。3. 地图表示设计分辨率和数据结构的组合拳3.1 占据栅格地图的分辨率选择lingbot-map 输出的地图是典型的二维占据栅格地图。每个栅格存放的是该位置被占据的概率常用 log-odds 的形式来存储。为什么用 log-odds 而不是直接存 0 到 1 的概率因为后续地图更新时我们需要做概率的累乘而直接乘概率非常容易因为数值下溢出变成 0。用对数形式后每次更新只需做加法数值范围稳定最后要显示的时候再通过 sigmoid 转回概率即可。分辨率选择上我最初想直接用 2 厘米的栅格觉得越精细越好。实测后发现两个问题一是内存和文件体量骤增一张 500 平米的精细地图文件能达到几十甚至上百 MB加载和传输都非常慢二是对于低成本的单线雷达2 厘米分辨率带来的点云噪声也暴露得更明显墙面边缘会显得毛糙。最后我采用的方案是建图时使用 5 厘米分辨率地图更新用双线性插值把激光命中点投影到邻近栅格兼顾边缘平滑。实测下来5 厘米分辨率已经能满足导航模块的路径规划和避障需求。地图文件用简单压缩后一张 500 平米的地图大概只有 1 到 2 MB。3.2 关键帧和子图内存和精度之间的折中方案如果每一帧激光都参与后端优化关键帧数量会在半小时内达到上万帧后端优化的规模会变成一个无底洞。我做了一个简单的策略来控制关键帧插入机器人位姿相对上一关键帧平移超过 0.3 米或者旋转超过 10 度就生成一个新关键帧。新关键帧保存的是当前帧配准后的位姿、原始激光点云的前向投影、以及当前局部子图的快照描述。关键帧之间不再保存完整点云只保存位姿和压缩后的子图索引。这样半小时的建图过程关键帧数量大约在 400 到 600 个之间后端的图优化规模完全可控。子图的组织方式则参考了滑动窗口思想每累计 15 到 20 个关键帧就生成一个新的局部子图子图内做局部位姿优化。多个子图之间通过关键帧在位姿图中建立约束这样如果某个区域被反复扫描了很多次区域内的局部最优就能被更精确地解算出来。我清楚记得有个同事问过我为什么不直接把所有关键帧一次性做全局优化答案很直白位姿图优化的复杂度随节点数增长很快几百个节点时还行上千个节点时每次优化都要花几十秒建图线程会卡死。而分层做局部子图优化加全局回环修正能保证实时性。3.3 地图更新时如何处理动态障碍物室内场景最大的干扰源不是固定的墙而是移动的物体和人。如果雷达扫到一个正在走路的人栅格地图上就会短暂地出现一堵墙等这个人走了以后原来的位置反而空下来形成地图上的空洞。最基础的滤波策略就是概率栅格本身的天然特性用 log-odds 更新时每个栅格被击中的概率和未击中的概率同时更新。如果一个位置只是偶尔出现障碍物它的被占据概率会在 0.5 附近震荡不会稳定变成占据。但仅仅这样是不够的因为人走过的时候激光点会把墙背后的空间一起更新等于在墙里扫出一条通道。我在 lingbot-map 里增加了一个两步处理。第一步是命中点距离检测当某条激光射线的命中点距离明显短于上一帧同角度的命中点距离且差值超过 30 厘米时认为这是一个潜在动态点它在当前帧的命中更新权重被降低。第二步是区域重信度衰减被标记为动态的区域在连续若干帧内没有命中时栅格值会以更快的速度向未知回退避免残留鬼影。这套方案在办公室场景下效果显著地图上基本看不到行人拖出的轨迹。它的问题在于对快速移动的物体适应性有限物体移动速度太快时降低权重的作用就不明显了。如果后续要支持更复杂的动态场景可能得引入基于深度学习的动态目标分割但当前算力下我没有做这一步。4. 后端优化与回环检测精度和算力的平衡艺术4.1 位姿图优化的结构与实现细节后端负责持续修正前端累计的轨迹误差。它的本质是把每个关键帧看作图中的一个节点把相邻关键帧之间的相对位姿约束看成边再加上回环检测成功后的额外边然后最小化所有边的残差总和。很多人第一次写位姿图优化时会忽略信息矩阵的设置。信息矩阵描述的是这条约束在网络中的可信度。如果直接用单位矩阵等于把每一帧的相对约束都看得一样重。实际中点击配准的约束在走廊方向的置信度低在垂直墙面方向置信度更高。更合理的做法是把帧间匹配器的协方差提取出来转成信息矩阵后传给优化器。我用的是一个轻量的开源图优化库提前把每个关键帧与前后 5 个关键帧建立约束边。优化轮次控制在迭代到收敛或者最多 50 次。每次优化完成后关键帧位姿会整体更新这时需要用更新后的位姿重新计算当前子图并更新发布出去的地图。这个重投影过程是后端优化的开销大户我在实现时做了一步优化只重新投影受影响的栅格区域而不是全图重算。实测一次回环修正后的重投影耗时能控制在 300 毫秒以内。4.2 回环检测的误报怎么防回环检测的任务是让机器人识别出我回到曾经来过的地方从而把轨迹首尾的累积误差一次性修正。2D 激光的回环检测比视觉简单粗暴提取当前帧的全局描述子和之前所有关键帧的描述子做匹配搜索找到相似度足够高的候选帧再进一步做几何验证。但在室内对称环境中误报几乎无法避免。办公室的走廊、相似结构的房间、一样的会议室长桌都很容易让描述子给出高的相似分。我曾经在一条两侧完全对称的走廊里做实验回环检测模块连续找回了多个错误的候选帧导致优化后整个走廊区域的地图发生了翻转。防误报我用了三道关卡时间约束候选帧与当前帧的时间间隔必须大于 60 秒并且空间距离估计大于 20 米。这样做是为了排除本来就很近的帧因为近距离帧的匹配本来就有很高的相似度。几何一致性候选帧与当前帧做点云配准后残差必须小于阈值同时匹配得到的旋转量必须与轨迹上累加的旋转量接近。跨时间验证候选匹配成功后不立即加入优化而是先用此后 5 帧的数据验证这个回环是否持续成立。只有连续 5 帧中至少 4 帧都成功匹配到同一候选才最终确认回环有效。这三道关卡加下来误报率在我测试的数据集上降到了几乎为零代价是回环纠正的响应会滞后大约 1 秒。对建图任务来说这个延迟完全可接受。4.3 资源受限时的后端降级策略嵌入式板子上的算力终究有限。为了让建图模块在长时间运行时不会因为后端优化占用过多 CPU 而卡顿我做了动态降级策略。策略分三层。第一层调节后端优化触发频率。默认情况下每插入 5 个关键帧就触发一次全局优化如果单次优化耗时超过 2 秒系统会自动把触发间隔拉长到 10 个关键帧。第二层降级优化算法。全局优化退化到滑动窗口优化只优化与最新关键帧相连的最近 30 个节点。第三层稀疏化。当关键帧数量超过 800 个时对空间上拥挤区域的关键帧做抽取相邻关键帧之间距离小于 0.15 米的直接丢弃同时保留它们的观测信息给剩余帧。这套策略的降级速度比较平稳。实测在 ARM 板子上连续运行两小时CPU 占用峰值约 45%没有再出现过建图线程卡死导致地图旋转一下跳回去的严重事故。5. 真机实测踩坑记录光看代码远远不够5.1 雷达外参标定误差如何污染整张地图lingbot-map 在仿真环境里跑得非常顺利一到真机就出了怪问题地图整体没问题但墙角、桌腿的边缘都有一圈重影宽大约 5 厘米。开始我以为是雷达噪声反复调整配准参数都没有改善。排查到最后问题出在雷达安装的外参上面。雷达装在机器人前方偏左的位置安装角度还略向下倾斜。我给系统提供的外参只是一个大概值忽略了下倾角。对 10 米外的障碍物来说几度的安装倾斜会带来几十厘米的投影偏差即便室内平均探测距离只有 3 到 5 米偏差也足以形成明显的边缘重影。解决办法是重新做外参标定。我用了一个自制的标定板放在雷达正前方多个距离上扫描通过拟合直线来反算雷达在当前安装位置下的平移量和俯仰角。标定完成后同样的建图过程重影直接消失。这个坑让我学到一个教训建图之前先标定传感器标定的优先级高于调参。5.2 长走廊和重复纹理场景的建图退化长走廊是 2D 激光雷达的经典退化场景。雷达在走廊里只能看到左右两侧平行的墙壁前后方向无法提供有效约束。表现在轨迹上就是建图过程中机器人在走廊方向上漂移很自由转弯后回到同一位置时轨迹会闭合不上。我在走廊场景里实测时最严重的一次漂移达到了 40 厘米。尝试过提高激光配准的角分辨率、增加 IMU 权重等方法效果都不够稳定。最后真正解决问题的是充分使用轮速计轮速计在走廊直线行驶中提供的前向位移精度很高而 IMU 提供的航向角精度能抑制旋转漂移。我把前端里程计的融合方式改成了IMU 陀螺仪直接积分航向 轮速计积分位移 激光修正侧向并以加权方式融合三者走廊场景的漂移降到了 10 厘米以内。类似的问题也出现在大面积平整玻璃幕墙或者白墙无纹理的环境中。激光打到玻璃上会发生镜面反射产生虚假的远距离点。我在数据接入层增加了距离一致性过滤对反复出现且距离跳变超过 1 米的点直接丢弃。5.3 对接导航模块时的坐标系坑lingbot-map 建好的地图最终要交给导航模块使用。这里的坐标系问题看起来简单做起来很容易出错。地图坐标系的原点一般选在建图启动时机器人的位置X 轴指向机器人当时的朝向。导航模块期望的是以某个设定的地图原点为准而且通常要求机器人的起始位姿大致落在某个已设定的区域中。如果两边没有对齐导航模块要么无法定位要么一启动就认为机器人在墙里。踩过的坑是坐标系的旋转方向。雷达数据按照右手坐标系输出Y 轴向左但部分导航算法内部使用的是左手坐标系Y 轴向右。如果没有把地图翻转或转换路径规划出来的结果会和实际左右相反。我在输出层加了一个显式的坐标系配置项默认使用右手系同时支持切换 Y 轴方向。另一个细节是地图的栅格索引和坐标原点的对应关系。地图文件存储的是从原点开始、以分辨率为单位的行列索引。加载地图时原点位置要换算成栅格行列号而不是简单地认为栅格坐标就是物理坐标。6. 代码组织与扩展建议让建图模块能持续演进6.1 模块解耦与参数配置化lingbot-map 的功能逐步增多后代码组织如果不做约束很快会变成一坨分不清职责的意大利面。我在项目中期做了一次重构把模块按功能分成独立库sensor_io只做数据接入输出时间戳对齐后的传感器数据。frontend负责帧间配准、运动畸变补偿、关键帧生成。mapping管理子图、栅格地图更新、动态障碍物滤除。backend位姿图优化、回环检测、整体轨迹修正。output地图发布、地图保存、坐标系转换。每个模块通过配置文件告诉上层模块它的能力和参数。比如 frontend 模块的配置文件里可以指定匹配器类型是 point_to_line 还是 point_to_pointmapping 模块可以指定分辨率是 2 厘米还是 5 厘米。这样根据不同机器人平台做适配只需要改配置不需要改代码。同时建图过程中的关键帧位姿、激光原始数据、匹配残差等关键信息我都写了日志记录功能。出了问题时可以用录制好的数据离线重放建图过程一边跑一边观察哪一帧出了问题。这个功能在排查前端里程计异常时帮助极大比在真机上反复跑实验高效得多。6.2 给想自建建图模块的人几条建议做完 lingbot-map 这个项目我最大的感受是建图算法本身并不神秘真正的难点在于系统集成和大量的边界情况处理。如果让我从头再做一遍我会给自己三条建议第一先把传感器标定做好再写算法。雷达外参、IMU 内参、轮速计标定这三样不做好后面所有调试都是浪费时间的苦劳。标定工具可以自己花点功夫搭一套半自动的流程或者直接用项目里现成的标定代码改一改。第二先跑通最小闭环再逐步加功能。最小闭环是雷达数据进栅格地图出先不计较精度先把链路跑通。路跑通了之后再加运动畸变补偿、回环检测、动态滤除。一步一步加每一步都有可以验证的效果出现问题时也容易定位。第三建图模块的设计要有测试数据意识。建图算法的表现高度依赖环境如果不做数据录制和回放每次都靠真机复现问题效率极低。我在项目推进过程中几乎每一个关键版本都会录制一整轮覆盖典型场景的建图数据保存在本地作为回归测试的素材库。后续改代码时跑一遍录制数据的重建过程就能快速判断改动没有破坏已有功能。这条经验帮我在后期改代码时省下了大量时间。所有代码重构完成之后我只需要把录制数据重新输入观察地图质量指标是否下降就能判断改动是否安全。建图模块这种强耦合传感器数据的系统有稳定的回归测试数据比什么都重要。lingbot-map 到目前为止还是一个专注在室内结构化环境的小模块很多环节都还比较简单。但它的价值在于帮我彻底理清了建图系统的完整链路从传感器数据接入、前端里程计、地图表示到后端优化每一步的取舍都有了实际的依据。后面如果有机会把视觉信息加进来或者支持 3D 建图这套分层结构和调试方法还是可以继续复用的。
阅读完成 · 觉得有帮助?
咨询建站