一个多月前,我们团队在给某商用清洁机器人做自主导航改造。老方案是贴着反光板和磁条走,现场的施工队得拿卷尺量好位置,一段一段贴,机器人稍微走偏一点就得回去重新校。做维护的同事抱怨了好几次,说这哪是智能清洁,是给地板装铁轨。于是我们把目光转向激光SLAM,项目代号就叫lingbot-map,核心目标很朴素:在商超、写字楼这类室内场景下,用一套低成本硬件组合,靠机器人自己把环境构建成可以反复使用的地图。这篇文章就是把这个项目的选型过程、参数调优、踩坑记录和横跨建图到导航的衔接细节完整梳理一遍。内容更适合正在做室内移动机器人、刚接触ROS建图、或者准备从"能跑demo"跨到"图真正能用"阶段的同学参考。
1. 这个项目到底要做什么:从"贴条认路"到"自建地图"
1.1 为什么老方案做不下去了
先说说背景。我们当时做的是一款室内清洁机器人,原来的定位方案是反光板+磁条:现场布置好反光贴纸,车顶的传感器扫到反射信号后算出相对位置。这个方案在单一封闭空间里还算稳定,但一进商用环境就很痛苦。
- 现场布置成本太高。一个小楼层,光是贴磁条和布置反光板就花了三天,开业后商场一改柜台,所有标定位置都要跟着动。
- 机器人"感知"不到环境,只知道自己在一堆标签中间。清洁路线是人提前画好的,车子出了路线就抓瞎。
- 客户临时换场地,整套部署流程从头再来一遍,公司项目经理每次提起都要叹气。
问题的本质是:机器人没有对环境本身的建模能力,它的"地图"是人维护的,不是它自己学出来的。所以lingbot-map立项时,第一目标就是让机器人在新场地里自己走一遍,交出一张能用于导航的地图,之后换场地只要重新跑一遍建图流程就行。
1.2 lingbot-map 的能力定位
这个项目不是一个万能SLAM平台,定位很聚焦:用单线激光雷达做2D室内栅格地图构建。它要解决的是三件事:
第一,把环境转换成一张二维栅格地图,墙是墙、障碍物是障碍物、可通行区域是白、未知区域是灰。第二,这套地图要能和下游导航栈无缝衔接,存下来的地图能被加载、被定位、被路径规划使用。第三,面对多楼层场地时,能分层管理地图,而不是把所有楼层的结构硬拼进一张图里。
说白了,lingbot-map就是整个自主导航链路里的"地图生产环节"。它不负责避障,不负责路径规划,只负责把环境变成一张可靠的、机器可读的底图。这个边界很重要,团队里一开始有人想把定位、建图、导航全塞进来,我坚持拆开,因为后面你会发现,每一步单独调优都比大杂烩容易得多。
1.3 硬件基线与团队底子
硬件方案没有追逐高配,都是够用就行的思路:
| 部件 | 选型 | 说明 |
|---|---|---|
| 激光雷达 | 单线雷达,10Hz扫描频率,30米量程 | 对商超场景足够,远墙也能扫到轮廓 |
| 底盘 | 双轮差速底盘,带轮式里程计 | 差速模型简单,里程计数据用于前端匹配 |
| IMU | 可选,初期没装,中后期才加上 | 在对称走廊场景里效果明显,后面会细说 |
| 工控机 | x86平台,四核i5级别,8GB内存 | Cartographer实时跑起来无压力 |
团队里几个同事的ROS基础参差不齐,有写过发布订阅的,有以前只调过导航包的。但大家都清楚一点:建图这活儿,七分在数据采集和参数调优,三分在代码本身。所以语言层面用的就是ROS + C++,没搞什么花活。
2. 方案选型:为什么跳过普通2D SLAM,直接上带回环的框架
2.1 三种方案的对比直觉
提到2D SLAM,绕不开三个名字:gmapping、hector_slam、Cartographer。网上对比文章很多,但我这里想从"商用落地"的角度讲,和跑demo是完全不同的标准。
| 方案 | 回环检测 | 依赖里程计 | 地图一致性 | 调参成本 | 适用场景 |
|---|---|---|---|---|---|
| gmapping | 无 | 强依赖 | 中低 | 低 | 小房间、快速验证 |
| hector_slam | 无 | 不依赖 | 中 | 中 | 手持设备、无轮式底盘 |
| Cartographer | 有(子图+闭环约束) | 中等 | 高 | 高 | 中大场景、商用部署 |
gmapping 的实现思路本质上靠的是粒子滤波,每个粒子保存一条轨迹假设,然后不断用激光观测去加权。它的问题不是不能用,而是没有显式的回环检测机制。在一个 30 米走廊里走一个来回,开始和结束时的误差会悄悄累积,回到原点时地图可能会出现几厘米甚至几十厘米的错位。
hector_slam 的思路是基于扫描匹配的直接优化,不依赖里程计,但它对激光数据质量要求极高,而且对轮式机器人的运动模型适配不好。我们这种差速底盘在瓷砖地面上偶尔会打滑,hector 很容易直接把漂移吃进去。
2.2 为什么"无回环"是硬伤
我最早用 gmapping 在小房间里跑,效果挺惊喜:一个20平的测试间,地图边缘光滑,门的位置清清楚楚。后来换到真实商超的一个约40m×30m楼层,gmapping 就露馅了。场地是个环形动线,机器人在里头转一大圈回到起点,地图边缘出现了一分为二的"重影"——同一堵墙,前半段和后半段各画了一遍,中间错开了约15厘米。
这就是没有回环检测的典型症状。回环检测的意思打个比方:你闭着眼睛在房间里绕一圈,摸过桌子和床,绕回门口时睁开眼,发现门就在面前,这时候你会意识到"哦,刚才摸过的地方其实都在这一间屋里",把之前零散的记忆一下子对齐了。gmapping 缺的正是这个动作——它一直在滚动累计地图,却没有一个步骤把"当前看到的门"和"记忆里那道门"做全局关联。
所以选型结论很明确:只要场地面积超过一两百平米,或者环境存在明显的环路动线,就必须选带回环检测的框架。Cartographer 靠的是"子图 + 闭环约束"这套机制:它先把连续激光帧分成若干个子图,再用局部匹配把每帧插进附近子图,每当机器人重新走过陌生中相似位置时,全局优化器就把所有子图之间的误差一起纠正。这套机制用在商超这种场景里,地图一致性明显比 gmapping 高一个档次。
2.3 关于算力与成本的现实考量
有人担心 Cartographer 很吃算力,我们实际跑下来,在中端工控机上CPU占用大约在30%-50%之间浮动,内存约2GB左右。对商用机器人来说完全能接受,毕竟建图不是常态任务,部署当天跑一次,后续每天复用同一张地图。
还有一个现实问题:方案成本。整套建图硬件成本相比反光板方案并没有增加太多,单线雷达属于成熟器件,而且后续还能用于实时避障,等于一份硬件两份用途。这也是我坚持选2D而不是直接用3D激光或视觉SLAM的原因——商用2D激光方案成熟、稳定、好维护,视觉方案在商场玻璃幕墙、反光地砖面前的鲁棒性还不足以让人放心。
3. 建图前的三个准备动作:标定里程计、装好雷达、定运动策略
3.1 里程计标定:先让"轮子会数数"
很多人建图失败不是算法不行,是喂给算法的里程计数据本身就是歪的。轮式里程计的核心是编码器,它的原始读数通常会因为轮径误差、底盘装配误差而存在一个固定比例偏差。
我的标定方法很土但很有效:在地面上画一条10米长的直线,让机器人以0.3m/s的速度匀速直行,记录里程计读数。比如走了10米,里程计报的是9.82米,那修正系数就是 10 ÷ 9.82 ≈ 1.0183。把这个系数乘到底盘的速度控制量或里程计发布节点里,反复测三次,直到误差在1%以内。
这类标定听着简单,但多数项目都不做,最后把锅甩给SLAM算法,实在冤枉。另外还要注意左右轮的一致性:让机器人原地旋转90度,看看实际转了92度还是88度,差别大了就检查左右轮修正系数是否一致。左右轮系数不一致时,机器人走直线会画弧,地图后期会出现整体扭曲。
3.2 激光雷达的安装与检查
雷达安装有两条容易被忽略的原则:
第一条,固定必须刚性强。支架不能有弹性,车身一震动,雷达就跟着微微晃动,扫描数据会引入周期性噪声,SLAM前端匹配时很难收敛。我们最初用的3D打印支架,跑了几分钟地图边缘就开始抖动,换成铝合金支架之后问题立刻消失。
第二条,安装高度要避开"动态噪声层"。室内环境里,行人腿部、扫地机器人、宠物都在较低高度区域活动,如果雷达装得离地太近,建图时会把这些动目标一起扫进去。我们后来把雷达装在底盘前上方约30cm高度,既能看到墙体踢脚线和柱子底部,又避免了一些地面上的小杂物干扰。
装好后打开可视化工具看一眼扫描帧,如果发现墙面的激光点像抖动的杂乱线,先查机械固定和电机驱动,别急着调SLAM参数。
3.3 建图运动策略:为什么不能猛糙
建图时的机器人运动路径,直接影响回环检测的成功率和地图质量。我们在项目初期让机器人按照正常清洁路径跑:折返、加速、原地掉头,效果很差。后来改成"建图专用运动策略",效果立竿见影:
- 直线速度控制在 0.5 m/s 以内,转弯角速度控制在 0.25 rad/s 以内,保证激光帧间距合理。
- 尽量走大回环,不走"来回蹭"。比如整个楼层绕一圈,每隔一段距离再穿插一个横向通道,让前后路径形成交叉,这样能创造大量回环约束。
- 同一个区域不要反复原地旋转,旋转会让匹配退化为打转,时间长了子图内部出现累积误差。
- 在特征稀少的长走廊或大厅里,可以有意识地贴着墙走一段,给激光雷达充足的近场特征。
这套策略本质上是给SLAM"喂"更高质量的数据。道理很简单:建图算法再强,信息不够就是不够,特征是闭环的锚点,你散步时记路也会看路牌和拐角。
4. 建图流程与参数调优:从能跑通到图能用
4.1 启动建图的完整命令链
我们基于开源SLAM框架,在ROS里封装了lingbot-map的建图启动层。核心流程是先驱动激光和里程计,然后启动SLAM节点,最后用 map_server 保存地图。
# 终端1:启动激光雷达驱动 roslaunch robot_bringup lidar.launch # 终端2:启动底盘与里程计 roslaunch robot_bringup base_controller.launch # 终端3:启动Cartographer建图 roslaunch lingbot_map cartographer_2d.launch # 建图完成后保存 rosrun map_server map_saver -f final_map保存后会生成final_map.pgm和final_map.yaml。前者是栅格图像,后者记录分辨率、原点、占用阈值这些元信息。导航时加载的就是这两个文件,所以地图文件要妥善归档,我们后面还会讲版本管理。
4.2 几个必须调的核心参数
Cartographer的配置主要在 lua 文件里。很多人第一次打开这个文件会头皮发麻,其实真正需要关心的参数没那么多,关键是明白每个参数在控制什么。
下面是我在项目中实际用下来比较稳定的一套关键配置:
include "map_builder.lua" include "trajectory_builder.lua" options = { map_builder = MAP_BUILDER, trajectory_builder = TRAJECTORY_BUILDER, map_frame = "map", tracking_frame = "base_link", odom_frame = "odom", provide_odom_frame = true, publish_frame_projected_to_2d = false, use_pose_estimated_by_pose_subscriber = false, num_laser_scans = 1, num_submap_scans = 1, } MAP_BUILDER.use_trajectory_builder_2d = true MAP_BUILDER.num_background_threads = 4 TRAJECTORY_BUILDER_2D.submaps.num_range_data = 90 TRAJECTORY_BUILDER_2D.use_imu_data = false TRAJECTORY_BUILDER_2D.use_online_correlative_scan_matching = true TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.linear_search_window = 0.5 TRAJECTORY_BUILDER_2D.real_time_correlative_scan_matcher.angular_search_window = 0.4 TRAJECTORY_BUILDER_2D.ceres_scan_matcher.translation_weight = 8.0 TRAJECTORY_BUILDER_2D.ceres_scan_matcher.rotation_weight = 2.0 MAP_BUILDER.constraint_builder.max_constraint_distance = 2.0几个重点解释一下:
num_range_data = 90表示积累90帧激光数据才生成一个新的子图。数值越小,子图生成越快、局部匹配越及时,但也意味着子图数量多、全局优化负担大。我们用的10Hz雷达,90帧相当于9秒一个子图,在商超场景够用。
linear_search_window和angular_search_window是实时扫描匹配器的搜索范围。搜索窗越大,越容易找到正确的匹配位置,但计算量成倍增加。一般先给默认值跑一遍,如果轨迹频繁跳变再适当增大,不用开太大。
translation_weight和rotation_weight控制里程计和激光匹配的信任权重。我们保留了较大的平移权重,因为差速底盘的直线里程计比较可靠;但如果你的底盘打滑严重,这个值要适当降低。
4.3 回环检测的"甜点区"
回环检测是Cartographer最有价值的部分,也是最需要现场感受的部分。我把它理解成一个"蒙眼回家"任务:算法不知道你出发时什么样,只有当它发现当前扫描和之前某个子图高度相似时,才会重置之间积累的误差。
实际操作中,我总结出了一个"甜点区"规律:机器人绕一个大回环,在即将回到起点的那段路程里,如果雷达能看到出发时扫过的墙面,闭环通常在几秒内就能发生。如果绕了很久还不闭环,先别急着改回环参数,而是复盘路径——是不是始终跟出发位置保持着两三米的距离?是不是被一排货架挡住了视线?
有一次我们在商超里绕了一大圈,回到起点时闭环一直不触发。后来发现通道中间堆了一排临时促销货架,把雷达视野挡住了一部分,去程能看到的那面墙,返程时被货架遮了个严实。把货架临时挪开,机器人走完最后一小段路径,闭环立刻建立,地图偏差瞬间被拉平。
5. 实测踩坑记录:一条30米走廊差点毁掉整张图
5.1 场景一:对称走廊的"复制粘贴"错位
这是我们项目里遇到的最棘手的建图问题。场地有一条30多米长的商业走廊,两侧是玻璃橱窗和统一规格的门头,特征密集但是几乎完全对称。机器人从走廊这头走到那头,地图上出现了一个很隐蔽的错误:走廊的两侧墙体保持了平行,但整体宽度被压缩了好几厘米,走廊尽头的柱子位置跟真实位置偏了大半米。
排查过程比直接看现象复杂得多。我先把rosbag录下来离线回放,用不同参数组合重建地图,最终发现是回环检测在这里犯了"胆小"的毛病:算法看到走廊重复纹理,时不时有点"疑神疑鬼",既想匹配又怕匹配错,最后在几何层面做出一个折中解,把墙面拉近了。
解决办法分两步。第一步,给机器人增加一个IMU,重力方向和航向角的变化能提供额外的姿态约束,减小对称环境的误匹配概率。第二步,建图时把运动策略调得更"磨蹭"一些,在走廊尽头做一个明显的回转动作,并且在墙上不起眼的位置临时挂几张黑白棋盘纸,人为制造视觉特征锚点。重新建图后,走廊宽度误差从几厘米降到了1厘米以内。
这个坑给我的教训是:特征稀少的地方容易漂移,特征密集但过于相似的地方也会让算法"看走眼",工程上往往需要引入额外的物理约束(IMU)或人工标记来打破对称性。
5.2 场景二:动态人群导致的鬼影
商场白天的人流量非常可怕。建图时旁边有人走动、经过、站立,激光雷达都会把这些动态点扫进地图,于是地图上出现各种"鬼影":椅子腿、人形斑块、甚至是短暂停留的清洁车。
Cartographer在局部匹配时对这些动态点有一定的容忍度,但那只是"不产生明显路径跳变"的程度,不代表地图里不会留下痕迹。我们的做法是把建图时间安排到商场还没营业的时候,这一点听起来像废话,但真的很重要。如果必须在营业期间建图,激光雷达内部有滤波功能可以把单次扫描中的孤立点剔除,同时尽量避开自动扶梯和主通道。
还有一种动态源是玻璃门和落地玻璃。激光打上去基本是穿过的,但斜着打会产生镜面反射假点,地图上会拉出奇怪的斜线。处理方式是建图前先把玻璃门固定在打开状态,或者用胶带贴个明显标记。玻璃区域在地图里不参与回环检测也没关系,导航时预留一点安全膨胀半径就好。
5.3 场景三:电梯厅的"短环欺骗"
电梯厅是另一个容易出问题的位置。电梯厅空间小、墙面相似、环路短,机器人进去转一圈,很短的时间内又回到同一片空间,回环检测很容易"误报喜"——它以为检测到了一个大闭环,实际上只是绕着电梯厅转了一圈,全局优化会把整条轨迹往错误方向拽一下。
排查这种问题的典型特征是:地图局部看起来没问题,但几道门的位置成对偏移,而且偏移的方向很一致,像整体被"推"了一下。我们当时的处理方法是调整回环约束的最低分数阈值,把过于相似但置信度不高的候选对过滤掉。同时在建图路径上避免在电梯厅里做长时间原地旋转,进电梯厅后快速通过,把更多有效时间用在需要特征的通道上。
这个坑告诉我们:回环检测不是次数越多越好,错误闭环的危害远大于少一个闭环。调参时宁可保守一点,让算法"宁缺毋滥"。
5.4 排查问题的通用姿势:离线bag回放
这三个坑的排查都离不开一个习惯:建图过程全程录制rosbag。录bag的好处是,环境变化了也没关系,数据已经固化了,你能在办公室里反复试不同参数、不同算法版本,不用每次都把机器人推回现场。
# 录制所有与建图相关的数据 rosbag record -O site_a_bag \ /scan \ /odom \ /imu/data \ /tf \ /tf_static排查建议:一旦地图出现异常,先不要急着改现场,把bag回放,换参数重建。如果你是刚开始接触SLAM,我建议任何一次正式建图前都先录一个短bag,确认数据链路完整。数据有问题,算法是救不回来的;数据没问题,问题就大概率出在参数或路径规划上。
6. 从建图到导航的衔接:坐标、地图清理与AMCL参数
6.1 保存地图与杂点清理
地图建好后不能拿起来直接用,原始栅格图里往往会有一些残留噪点。保存完final_map.pgm后,我用图像工具过一遍,把孤立的单点像素清掉,不然导航时这些点会被膨胀成"伪障碍物",路径规划器会绕着看不见的鬼影走。
清理时要小心,只删孤立的噪点,不碰连续的墙体轮廓。如果地图边缘有大量的"毛刺",可以在保存前用栅格后处理程序做一次膨胀再腐蚀,这能把断开的小碎片连起来,同时不影响整体几何。我们在lingbot-map里封装了一个后处理脚本,读入pgm和yaml,输出清理过的地图,再人工二次确认。
6.2 坐标关系:为什么导航要用map坐标系
在地图保存与加载这个阶段,最常见的困惑是:地图里的原点在哪里?机器人怎么知道自己在哪?
这里必须把三套坐标系理清。map是全局地图坐标系,一旦建图完成就固定不动;odom是里程计累计的坐标系,会随时间漂移;base_link是机器人本体的坐标系。SLAM内部维护的是map到odom之间的变换关系,而底层里程计只负责odom到base_link。
导航定位时,AMCL做的事情就是估计机器人在map坐标系下的位姿,并不断用激光数据修正估计。理解这个链路很重要,因为如果直接把odom当作定位参考,里程计漂移会逐渐把机器人的位置带跑偏,地图再好也没用。
6.3 AMCL初始化:定位不能靠运气
AMCL送代初始状态极其重要。我们把建图完成的机器人在场地入口摆好,设置一个带有一定方差的初始位姿,粒子数控制在8000左右。如果初始位姿完全未知,粒子会遍布全图,收敛时间大幅拉长,很容易收敛到错误位置。
AMCL参数里建议关注这几项:
min_particles和max_particles:建议5000到10000之间,商超场景不算巨大,8000足够。update_min_a和update_min_d:控制每次更新前机器人最少要移动多长距离/角度,避免原地不动时粒子反复退化。laser_max_range:限制雷达参与匹配的最大距离,太长会把远处的噪声也算进来。
定位效果不好的时候,还要回头检查地图本身是否干净。如果地图里有多余的临时障碍物,AMCL扫描匹配时会产生大量错误观测,粒子集就会发散。所以我们有一条内部规则:地图跟现场有差异时,先在地图层面解决问题,再考虑加粒子数。
7. 多楼层地图的组织方式:一层一图,垂直切换
7.1 多地图与"垂直切换"
商用场地大概率是多楼层的。有人问能不能用3D SLAM把整栋楼建一张大图,从技术和成本上讲都不划算。我们最终选择:一层一图,垂直切换。
每一层跑一趟建图流程,产出一张独立的2D地图文件,命名带上楼层编号:site_a_b1.yaml、site_a_f1.yaml、site_a_f2.yaml。机器人进入电梯或楼梯间时,先在地图管理模块里切换到对应楼层的地图,再通过电梯口的定位辅助装置(比如二维码或反光板)给AMCL一个初始位置,剩下的事情交给粒子滤波去收敛。
这样做的理由是,2D地图天然不适合表达跨楼层的垂直关系,强行拼图必然导致同一平面坐标上出现多层墙体重叠,导航和定位都会崩。
7.2 电梯口与坡道边界的特殊处理
电梯口是楼层切换的咽喉,也是多地图管理的难点。电梯内部空间狭小,激光扫描帧很容易被电梯门和轿厢壁反射,建图时经常会在这里留下奇怪的伪轮廓。
处理方法是建图时把电梯门完全打开、轿厢停在当前楼层,让激光能扫到电梯轿厢内部,这样至少在画面上能识别出"电梯口是一个可进入的空间"。但地图边界在电梯门附近要做人工裁剪,留下一小段过渡区,避免导航时机器人把电梯井当成可通行区域。
对坡道或缓坡,2D建图会直接把坡道压扁成平面。如果坡道角度很小(5度以内),影响不大;如果坡度明显,建议在坡道两侧设置禁行区,让机器人在切换楼层时走电梯而不是坡道。
7.3 地图版本管理与更新流程
多楼层意味着多份地图,多份地图意味着必须有人管版本。我们的做法很简单:建图、评审、锁版、发布。
每一次正式建图后,先由一个人评审地图:墙体是否连续、走廊是否对齐、门的位置是否和现场一致、有没有明显鬼影。通过后把地图文件复制到统一目录,文件名带日期和版本号。环境大改(比如商场翻新)之后再重扫对应楼层,替换旧版。
地图更新这个事要提个醒:2D建图一般不支持"局部热更新",某个角落变了,整层地图最好重扫一遍再来。这和做导航实时避障是两码事,别想着在线动态改地图,传统方案里会引入很多不确定的副作用,我们试过,最后都老老实实重扫。
8. 项目收尾后的几点个人体会
lingbot-map这个项目前后折腾了一个多月,最深的感受是:SLAM建图的难点不在"调用算法",而在"让算法在真实环境里稳定地工作"。我们花在参数调试、路径策略、环境准备上的时间,远远多于写代码的时间。
如果让我给后来者一个建议,我会说:先花一天把里程计标定干净,再花一天录制高质量bag,之后所有参数调试都能在办公室完成,效率会高很多。另一个建议是建图时多绕大圈、多制造回环,宁可路径看起来"笨拙",也要保证每个空间都被充分遍历。地图质量不过关,后面AMCL、导航规划都会跟着遭罪,返工成本极高。
目前lingbot-map已经在我们自己的几个测试场地上稳定跑起来了,下一步打算把多楼层地图切换做得更顺滑,再把局部场景重扫和地图自动拼接加进去。这篇记录里的经验,希望能让你在建图的路少踩几个坑。