从撞墙到稳定导航:室内机器人激光SLAM建图与定位实践

📅 发布时间:2026/10/21 5:25:14
从撞墙到稳定导航:室内机器人激光SLAM建图与定位实践
这个项目叫lingbot-map是我在给一台室内巡检机器人做自主导航改造时沉淀下来的一个地图构建与定位模块。起因特别简单——机器人在所有人注视下沿着一条笔直的走廊直直地朝着墙壁撞了过去。那次翻车现场我印象很深新来的同学在可视化界面里给机器人设了一个走廊尽头的目标点底盘开始移动速度正常方向却偏了大概二十多度走了一半就贴上墙了。把地图调出来一看走廊出现了明显的双层重影机器人在地图上的位姿和真实位置差了将近半米。问题不在地面太滑也不在轮子磨损而是整个“建图—定位—规划”链条里好几个环节都在悄悄出错。lingbot-map就是为了把这条链从底层理顺而写的。它做的事情可以拆成三块用激光雷达和里程计算法生成环境栅格地图在地图里做实时位姿估计然后基于地图生成可执行的路径。如果你正在做移动机器人导航、刚接触SLAM或者要接手一个“能跑但总是跑偏”的机器人项目这篇内容应该能帮你省掉不少弯路。1. 从一次“撞墙”说起lingbot-map要解决什么1.1 那台机器人的问题到底出在哪撞墙之后我先把现场数据翻了一遍。当时车上挂着一颗入门级单线激光雷达底盘自带轮式编码器另外加了一块九轴IMU。这三个传感器单独看数据都正常雷达点云能扫出走廊轮廓编码器里程计在短距离内也凑合IMU输出的角速度曲线没有毛刺。可一旦把三者拼在一起矛盾就出现了激光雷达认为机器人在走廊正中里程计认为机器人已经偏了十几厘米IMU又给出另一个弯道角度。这是很典型的“传感器各自为政”问题。在导航系统里每个传感器都带着不同的噪声特性和漂移规律——编码器在直行时很准但转弯时会因为轮距不准累积角度误差IMU短时间内角速度可信长时间会有积分漂移激光雷达精度高但在空旷走廊里容易因为特征稀疏而匹配错位。谁都不服谁地图自然就建歪了。lingbot-map的第一个目标就是把“谁的观测更可信、什么时候该信谁”这个问题用一套确定的流程解决掉而不是靠肉眼挑一帧能用的数据。1.2 导航系统里至少藏着三个独立难题很多刚开始做机器人的同学会把“导航”理解成一个黑盒输入目标点输出速度指令。实际上这个黑盒至少包含三个互不相同的问题建图搞清楚环境长什么样。激光扫出来的是一帧一帧的点云需要把这些点云拼到同一个坐标系里最终输出一张栅格地图。定位机器人已经有一张地图了实时数据到了需要知道“我现在在地图上的哪个位置、朝哪个方向”并且每个时刻都要更新一次。路径规划知道了起点和目标点要在布满障碍的地图上找到一条能走的路再把这条路转成底盘能执行的速度指令。这三个问题互相依赖但绝对不能混为一谈。定位依赖建图的结果建图的过程本身又依赖一个粗略的定位规划则需要前两者提供的地图和位姿。lingbot-map在架构上把这三层拆成了独立模块彼此通过清晰的接口通信后续替换算法时不需要动其他部分。1.3 为什么没有直接套一套现成的开源导航方案这个问题我经常被问到。现成的方案当然能用甚至大部分时候效果不错。但我在实际使用中遇到了几个具体约束一是我们的底盘和激光雷达不是某厂全家桶传感器驱动和标定参数得自己调二是板载电脑算力有限全功能方案跑起来CPU占用率太高影响了底盘响应。更重要的是排查“撞墙”这类问题时如果你只知道“点一下启动就行”到了定位漂移、地图重影这种需要动内部参数的环节会完全无从下手。lingbot-map的定位是不用很花哨的算法但是把每个环节的参数变化、背后的物理意义和排查路径都暴露出来。它更适合“想搞明白原理再动手”的开发者而不是“只想跑通Demo”的用户。2. 选型权衡激光、IMU、里程计与SLAM方案的取舍2.1 传感器的分工谁负责精度谁负责短时谁负责积分先说说我最后定下来的传感器配置这套配置比较适合室内单层、低速最高1米每秒的轮式机器人传感器作用采样频率特点 / 易出问题的点单线激光雷达环境几何信息、建图与定位主数据10-15Hz精度高但空旷环境特征稀疏玻璃和镜面会穿透或反射异常轮式编码器里程计短距离位姿增量50Hz以上直行准确转弯累积角度误差轮径/轮距不准影响很大六轴IMU角速度/加速度平滑100-200Hz短时数据可信积分后漂移严重可选深度相机辅助识别动态障碍物15-30Hz光照敏感计算量大非必须这里最关键的一条经验是不要指望某一个传感器在所有时刻都可靠。激光雷达在长走廊里向远处看时点云变得越来越稀疏匹配容易跳到错误位置里程计在转过一个90度急弯后角度误差可能直接让后续整段建图偏差超过十厘米。正确的做法是给每个传感器划定“主场”——短时运动平滑交给IMU加里程计全局几何修正交给激光雷达然后用一个融合节点把它们的优势按协方差加权组合起来。2.2 SLAM算法前端匹配、后端优化、回环检测各自干多少活实现SLAM建图时算法可以粗分为三段前端负责把相邻两帧激光点云匹配起来这一步决定“短距离内我走了多少”后端负责把这些帧之间的约束揉成一个全局一致的位姿图这一步决定“累计走了多远以后还在正确位置”回环检测负责“我重新回到了曾经来过的地方”时把累计误差一把拉回来。我在前端选型时对比过ICP和NDT两种匹配方式。ICP的直觉很好理解找两帧点云之间最近点对算一个刚体变换让两组点尽可能重合。它在特征丰富的室内环境里表现不错但遇到走廊这种结构重复、点云稀疏的场景常常会陷入局部极小。NDT把空间划分成体素网格每个格子用正态分布描述点云分布匹配时优化的是“点落在目标分布里的概率”对初始位姿误差的容忍度明显更高。我最后选了NDT变体作为前端实时性满足10Hz的处理需求。后端之所以选位姿图优化而不是滤波类方法原因是滤波方式在算力有限且建图范围大时协方差矩阵会越来越稠密计算量会失控。位姿图只在发生关键帧和回环时插入约束稀疏的图结构用一般的优化库求解非常快。对于一个约2000平方米的室内区域这个方案在板载CPU上跑得很从容。2.3 为什么没走纯视觉或视觉惯性路线室内单线激光雷达方案已经能满足需求我没有把主传感器换成相机原因有几点第一室内走廊和实验室区域大量存在白墙和重复纹理纯视觉特征点要么太少要么难匹配稳定第二视觉SLAM的深度估计在光照变化时容易产生漂移需要更多计算资源做特征提取和回环描述子第三导航最终需要的是准确的障碍物几何边界激光点云直接给出距离而视觉还得做逆深度估计和滤波。当然视觉也有它的价值——识别门牌、区分人和静态障碍物时非常有用。所以在lingbot-map里视觉被放在语义层不参与几何建图的主流程。3. 建图模块落地实录从点云到可导航地图的完整链路3.1 坐标系统的设计先分清map、odom和机器人本体建图很容易在坐标系上栽跟头所以第一步就得定义清楚。lingbot-map使用四个坐标系map全局地图坐标系坐标原点固定在世界中某处一旦建立不再变化。odom里程计坐标系以机器人上电时刻的位置为原点之后随着编码器和IMU积分不断累积漂移。base_footprint机器人在地面上的投影中心是控制算法计算速度的参考系。laser激光雷达的安装坐标系相对base_footprint一般是一个固定的静态变换。map和odom为什么要拆开因为定位模块输出的位姿负责修正累计漂移而里程计只管“短时间内的相对增量”。如果只有map没有odom定位算法每次修正都会带来跳变底盘的平滑控制会很难受。拆分后map→odom可以缓慢变化来吸收修正量odom→base_footprint则保持连续平滑。这套TF树结构是导航系统的地基我见过很多诡异的“机器人原地转圈”问题最后都查出来是某个坐标系变换发反了或者频率太低。3.2 点云预处理体素滤波、离群点剔除与运动畸变矫正激光雷达直接输出的点云不能直接用来匹配。原始点云在室内环境下每帧可能包含数千个点其中既有玻璃反射产生的飞点也有机器人自身部件反光的噪点直接进匹配算法不仅慢还可能把匹配结果带偏。预处理我做三步第一步体素滤波——把空间划分成2厘米的小格子每个格子只保留一个重心点这一步能把单帧点位降到三分之一左右同时不损失地图精度第二步半径离群点剔除——如果一个点周围规定半径内的邻居数量少于阈值就认为是孤立的飞点删掉第三步范围裁剪——把距离机器人过近如小于20厘米和超过雷达有效测距如大于15米的点直接丢弃防止自反射和远处稀疏噪声干扰匹配。运动畸变矫正很容易被人忽略但恰恰是它决定了地图是否重影。单线雷达完成一圈扫描通常需要几十毫秒到上百毫秒在这段时间里机器人已经在移动这一帧100多个点其实是在不同时刻、不同位置测量得到的。把它们全部当成同一个时刻的点地图边缘就会出现拖影。矫正做法记录每个点的精确时间戳用IMU加里程计插值出该时刻机器人位姿再把点变换到扫描起始时刻的坐标系下。做完这步之后重影问题基本消失地图线条由“20厘米宽的虚影”变成了“不超过5厘米的实线”。3.3 里程计标定不想地图歪成“平行四边形”就必须做我强烈建议大家把里程计标定放在所有建图工作之前。关于轮式机器人里程计有两个参数会直接影响建图质量轮子实际直径和编码器理论直径的比例以及两个驱动轮之间的实际轮距。前者错了机器人直行时编码器算出的距离和真实距离对不上地图整体会被均匀缩放后者错了每次转弯都会产生一个固定的角度误差地图就会逐渐朝一侧弯过去。我自己用的标定方法很简单。先让机器人分别以0.2、0.4、0.6米每秒的速度直线跑3米用卷尺量真实移动距离和编码器数据对比算出平均比例系数。然后让机器人原地转10圈对比编码器角度和IMU角度的偏差修正轮距参数。最后再绕一个固定半径的圆跑一圈看轨迹首尾是否能闭合。标定前机器人走5米就开始偏转个90度的弯之后误差能到十几厘米标定后同样路径往返的重复误差在3厘米以内。这个精度虽然还不算顶级但对室内导航已经足够了。3.4 建图流程与地图后处理不是“扫完就保存”那么简单建图的完整流程我总结成了下面几步每一步都有值得注意的细节启动底盘驱动和激光雷达驱动检查TF树是否完整、话题频率是否稳定。启动IMU和里程计的融合节点确认融合后的里程计话题输出正常。启动SLAM节点在可视化工具里确认当前帧点云能和地图逐渐对得上。手动遥控机器人走遍目标区域。第一次扫图时优先跑一个全场大回环让回环检测尽早激活把整个地图的累计误差一次性修正回来。保存地图会得到两个主要文件栅格图像文件和配置文件。配置里包含分辨率、原点位置、占用阈值等参数。对栅格图像做后处理用图像工具清理孤立噪点去掉墙外的杂散点必要时手动把一些不期望进入的门洞补上。地图后处理常被当成可选项但我实际体验下来它非常重要。原始栅格地图里墙边缘往往有一圈“半占用”的过渡像素死角里的点尘也会造成局部狭小区域识别异常。我会先在图像编辑工具里对地图做一次孤立噪点清除再关闭一些不必要的开口最后才把地图交给导航模块使用。4. 定位与路径规划把地图真正“用起来”的工程细节4.1 蒙特卡洛定位的两个关键参数粒子数与观测模型构建好地图之后地图本身是静态的机器人需要实时回答“我在哪”。lingbot-map用的是自适应蒙特卡洛定位原理不复杂在地图上随机撒一大批“粒子”每个粒子代表一个可能的位姿机器人每移动一步所有粒子跟着运动模型预测一步激光雷达每扫一圈根据观测模型给每个粒子打分越像当前扫描的粒子权重越高最后按权重重采样粒子逐渐聚拢到真实位姿附近。这条链路里有两个参数最值得调。第一是粒子总数。环境特征丰富时2000到3000个粒子就足够环境存在高度对称的区域时比如一个大厅或者一段重复的走廊粒子数建议翻倍到5000甚至更多否则粒子群会被卡在错误的对称模态里。第二是观测模型的选择。对激光数据建模时要综合考虑命中点、意外障碍、随机测量误差和传感器最大量程四类情况每类给一个合理的概率权重。如果权重设置太极端定位会对墙角边边角角的微小差距过分敏感反而容易抖来抖去。4.2 “绑架问题”与初始位姿机器人被搬走之后怎么办定位系统里有一个特殊的经典问题——当机器人没有正常下电而是被人直接搬到一个新位置时所有粒子仍然停留在原来的位置激光扫描和新环境对不上系统就瞬间“失聪”了。这个问题在真实使用中不是罕见情况实验室里经常有人随手把机器人推到别处。处理绑架问题有两条路。一条是被动恢复定位器检测到大量粒子的平均得分骤降时自动在整张地图范围内重新撒粒子重新搜索位姿。另一条是主动重定位提供一个“手动给初始位姿”的接口操作者在可视化界面点一下机器人大概在哪、面朝哪个方向系统就在这个附近撒高密度粒子快速收敛。我的经验是两条都要留被动恢复处理突然搬动主动重定位处理长时间断电后再开机的情形。4.3 路径规划全局规划与局部规划是怎么配合的定位恢复后路径规划开始工作。我先说全局部分它把栅格地图转换成一棵带代价的图每个格子是节点相邻格子的移动有通行代价。常用算法是A*或Dijkstra变体在已知静态地图上找到一条从当前位置到目标点的路径。这条路径通常是抽象、鸟瞰层面的一条折线它不关心底盘能不能转得过来、加速来不来得及。局部规划解决的是“具体怎么走”的问题。机器人执行全局路径时实时传感器会不断发现新障碍——一个人走过来了、一把椅子被挪到了过道中间。局部规划器以全局路径为参考在机器人周围一小块区域内搜索一条满足运动学约束的轨迹输出线速度和角速度。我对比过DWA和TEB两类规划器最后默认用的是TEB变体。它能显式优化“到达时间最短”和“避免来回打摆子”过窄门时的表现比DWA好但代价是参数多、调起来费劲——最大的坑是速度上限给高了之后机器人会在窄通道前反复冲过去又急刹回来需要把加速度约束和代价权重一起降低才能稳住。4.4 代价地图的三层结构为什么明明有地图还是会“撞上”空气导航系统里还有一张和静态地图不一样的图代价地图。它分为三层看待静态层直接读取栅格地图标记已知障碍障碍物层接收实时激光和深度数据刻画动态障碍膨胀层把所有障碍向外“吹”一圈给机器人留出安全距离。膨胀半径怎么设非常考验经验。太小了机器人会贴着墙走稍微有点定位误差就会刮擦太大了窄通道会被整条涂掉机器人直接判“此路不通”。我的做法是把膨胀半径设为“机器人几何半径加5厘米”然后叠加一个额外的内切半径判断。做这些判断时别忘了激光雷达本身安装位置不一定在机器人正中心左右视野范围也不同代价地图的传感器布置最好按实际安装位姿配置。5. 实测排错记录lingbot-map开发过程中踩过的五个坑5.1 地图重影和走廊双层边根因是里程计标定不是SLAM参数现象建图完成后直走廊在地图上呈现明显的双层边界宽度比真实宽度多出约20厘米。排查链路我先停掉SLAM直接让机器人沿墙直线往返20米把编码器里程计画出来的轨迹和真实路径对比发现直行距离偏大了约8%。接着检查雷达运动畸变是否矫正到位——停在一个位置原地扫地图边界也是干净的所以排除畸变问题。最后定位到轮径换算系数没校准编码器按默认轮径计算但实际轮胎气压和磨损导致每个轮子的滚动半径已经变了。修复按3.3节的办法重新标定轮径系数和轮距更新到底盘驱动配置里再重新建图。结果走廊双层边界消失地图宽度误差在3厘米以内。5.2 对称区域定位左右横跳粒子群陷在错误模态里现象机器人进入一个矩形展览大厅后定位显示位置在真实点和大厅另一侧对称点之间来回跳路径规划被带着画了好几条“对角线”。排查链路先看粒子分布热力图发现粒子确实聚集在两个对称位置然后调低观测模型中“沿墙射程匹配”的权重改大“点云命中精度”的权重再看粒子收敛情况。同时检查了地图本身发现大厅里那张地图保存时把进门处的两块墙画成了完全对称的装饰性柱子等于地图自己提供了两个高度相似的解释。修复一是在地图里保留了一些贴墙的静态提示物几个箱子、一段矮柜作为非对称特征二是把粒子数从2000提到5000三是重定位时默认优先保留“更靠近上一次有效位置”的粒子。效果横跳频率降到几乎没有。5.3 地图导入后尺寸和实体差30厘米yaml中的分辨率搞错了现象在仿真环境里导航一切正常真机实测时机器人总在离墙还有30厘米的位置就停下来或者明明地图上显示前方有门现实中却被一块空地挡住。排查链路我打印了地图文件里的分辨率和尺寸字段又在可视化工具里量了一段已知长度墙面的像素长度。发现分辨率字段被设置成了和图像像素尺寸不匹配的组合——栅格图是2500x2000分辨率填的是0.05米/像素但实际保存时图像经过了压缩缩放导致每像素对应的真实距离比配置文件里写的更大。修复统一地图保存和加载时的分辨率直接用建图工具导出的原始尺寸不再用图像编辑软件缩放。同时在地图校准环节加了一段自动测试加载地图后往地图上两个已知距离的特征点发布目标点验证到达误差。5.4 CPU占用持续在90%以上SLAM和导航抢了底盘的算力现象系统跑起来后板载CPU占用率长期高于90%底盘控制指令出现周期性延迟表现为机器人走走停停。排查链路我用性能分析工具把各节点CPU占用率拉出来发现点云预处理、SLAM匹配、代价地图更新和AMCL粒子更新四个节点占了大头。进一步看数据体素滤波的格子设得太小一帧点云处理耗时就接近30毫秒代价地图的障碍物层更新频率设成了10Hz每次都要处理全量点云。修复体素滤波格子从2厘米放宽到4厘米地图精度影响不大激光点云距机器人15米以外的点直接丢弃代价地图障碍层更新频率降到5HzAMCL粒子数从8000降到3500。优化后CPU占用降到45%左右底盘响应恢复稳定。5.5 导航规划永远绕不开一扇门膨胀把门缝堵死了现象地图上明明显示一扇0.8米宽的门机器人却总是在门口停下报“路径不可达”。排查链路我把代价地图单独显示出来发现门缝处整条都被涂成了障碍颜色。检查膨胀参数后发现机器人半径设为0.35米膨胀外层又额外加了0.4米加起来已经超过了门洞净宽的一半两侧膨胀区域直接重合中间可通行区域被算成了零宽度。雷达扫描门框时还有一个折角点云在转角处略微散开进一步压缩了可用通道。修复缩小外层膨胀半径让内层膨胀只负责安全、外层只做减速提醒同时允许代价地图对“窄通道”单独标记降低通过速度但不直接判死。经过调整0.8米的门可以正常通过但0.55米以下的门还是放弃——不是技术上绝对不能过而是此时机器人过门时的姿态余量太小一旦定位误差有波动就会擦到门框不值得冒险。最后再分享几条经验lingbot-map从第一版“能建图”到现在“能稳定巡检”中间走了不少弯路。如果让我总结最想提醒后来人的事情有这么几条第一先把里程计标定做扎实再谈建图。这是整个系统里最便宜、最容易被忽视、却对结果影响最大的一步。标定做不好后面所有SLAM参数都是在给错误数据擦屁股。第二第一次建图一定要规划一个大回环宁可多绕一段路也要让回环检测跑起来。没有回环修正的地图走小圈时看起来没问题整个场景连起来之后误差会非常夸张。第三任何时候改地图都要重新确认分辨率和原点。地图文件里的几个数字决定了导航的一切就算只是用图像软件剪裁了一下尺寸也要回到建图工具里重新导出。第四保留下每个环节的可视化开关。点云、匹配得分、粒子分布、代价地图、全局路径、机器人足迹全部做成可以独立打开和关闭的图层。出问题的时候能最快告诉你“故障出在哪一层”的不是日志而是这些图层。接下来我准备给lingbot-map加上多楼层地图自动切换和动态物体滤除的能力当前这套架构把SHMSLAM-HNI-Map分层拆开了加新功能只需要替换对应模块不需要改动底盘和规划接口。这个项目还会继续迭代下去。