从”照着配置跑通 move_base demo”升级为”理解建图/定位/规划/代价地图每层的数学原理,知道参数为什么这么设、导航失败往哪查”。激光雷达数据模型、gmapping 与 cartographer 的取舍、AMCL 粒子滤波、代价地图三层结构与衰减公式、A*/DWA/TEB 的算法本质、move_base 状态机与恢复行为——最后给出可量化的调参顺序与评估指标。
一、激光雷达数据:LaserScan 的几何与噪声模型
LaserScan 的结构
1 | # 典型 2D 雷达(如 rplidar A1):360°、每圈 720 点 |
几何要点:第 i 个点的角度是 angle_min + i * angle_increment,距离是 ranges[i],则该点在雷达系下的坐标:
$$x_i = r_i \cos(\theta_i), \quad y_i = r_i \sin(\theta_i)$$
运动畸变(关键坑):激光是逐点采样的——一帧 720 点不是同一时刻测的,而是雷达旋转一圈(100ms)内连续测的。车在转圈/加速时,一帧内车已经移动了几厘米到几十厘米,直接按”所有点都在帧起始时刻”转换,点云就扭曲了。影响:
- 建图/定位对运动畸变敏感(gmapping 默认不处理,靠里程计好 + 车速慢来兜底)
- cartographer 在 ScanMatcher 里显式做畸变补偿(用 IMU/里程计把每个点外推到统一时刻)——这是它精度高于 gmapping 的原因之一
- 工程对策:建图时控制车速(< 0.5 m/s)、减少急转弯;导航时把 scan 时间戳与里程计对齐
噪声模型
雷达测距噪声近似高斯 + 少量离群点(镜面、玻璃、吸光材料返回 inf/NaN):
- 无效点处理:
ranges[i]为 inf/NaN 时,代价地图该方向不产生障碍也不清除——别把 NaN 当 0(0 会被当成”障碍就在脸上”,代价地图直接红一圈,导航原地卡死) - 角度分辨率:720 点/圈在 5m 处相邻点间距 ≈ $5 \times 2\pi/720 \approx 4.4$cm——远距离小障碍(桌腿)可能正好落在两条射线之间被漏掉,这是 2D 雷达的物理极限,膨胀半径设大一点就是在补偿它
二、建图:gmapping 还是 cartographer
gmapping:RBPF 粒子滤波
gmapping 的核心是带权重的粒子群:每个粒子携带一条机器人轨迹假设 + 对应的栅格地图,里程计预测粒子分布、激光匹配更新权重、重采样淘汰低权重粒子。三个工程结论:
- gmapping 的好坏上限 = 里程计的好坏。粒子滤波的提议分布依赖里程计,里程计漂了地图就糊。麦轮底盘打滑(见运动学篇)建图必糊——建图前先确认里程计(推车走直线,
rostopic echo /odom看位置是否线性增长) - 粒子数 = 精度 vs 算力。默认 30 个粒子,长廊/对称环境加到 80~100 才不飘;但粒子越多 CPU 越满(建图时雷达帧率会掉)。调法:地图出现”重影/错层”就加倍粒子数
- 退化场景:长廊(激光前后对称,匹配多解)、空旷场地(无特征,粒子权重全平)——gmapping 在这两类场景必翻车,不是参数问题,是算法本质
cartographer:子图 + 图优化 + 回环
cartographer 用局部子图(submap)+ 全局图优化:前端把连续帧匹配进当前子图(scan-to-submap,带畸变补偿),后端在检测到回环时把子图之间的位姿约束丢进稀疏图优化(数学本质见《SLAM 与视觉基础》篇的图优化部分)一次性拉平漂移。为什么大场景选它:
- 误差不是纯累积的:子图内误差被持续优化,回环闭合后全局误差被拉回
- 长廊也能建(图优化 + 子图间约束比纯粒子滤波稳),但对称场景回环仍会误闭合——需要
--config里调小回环阈值或建图时避免走重复对称路径
建图质量怎么评估
不看”图漂不漂亮”,看三个量化指标:
- 回环闭合:绕一圈回到起点,地图上起点位置的特征(墙角、门框)与轨迹起点重合,偏差 < 10cm 合格
- 对称度:房间的墙应该是直的——把地图导到图片里看墙角是不是 90°、门框宽度和实测是否一致
- 重影检查:同一面墙在图上出现两条平行线 = 轨迹漂移(粒子/回环没兜住)
1 | # 建图命令速查(gmapping) |
保存的 map.yaml 里 resolution: 0.05(5cm/像素)、origin 是地图左下角在世界系的位置——origin 错了导航会整体偏移,加载后先在 RViz 里对比激光与地图是否对齐再放机器人。
三、定位:AMCL 粒子滤波
AMCL(自适应蒙特卡洛定位)与 gmapping 同源——粒子滤波,但地图已知,只估计位姿:
- 初始:粒子在初始位姿附近撒开(或全图均匀撒——
initial_pose不给就是全局定位) - 预测:按里程计运动模型移动粒子
- 更新:每个粒子按”该位姿下激光与地图的匹配程度”加权
- 重采样:低权重粒子被高权重粒子替换,粒子群收敛到真实位姿
工程要点:
initial_pose必须给(RViz 2D Pose Estimate 点一下):全局定位要几秒到几十秒收敛,比赛/产线等不起;给了初始位姿秒级收敛- **
kidnapped robot problem**:机器人被搬走(被抬起来换地方),粒子群还在原地——AMCL 的”自适应”体现在粒子数会根据定位质量自动增减,但搬走这种极端情况要手动重新初始化(RViz 重新给初始位姿) - 参数三件套:
min_particles/max_particles(默认 100/5000,算力够就调大)、update_min_d/update_min_a(里程计走多远才做一次激光匹配,调大省 CPU 但响应慢)、laser_max_range(超过该距离的激光点丢弃——空旷场地用它避免”远处墙的匹配噪声”)
1 | amcl: |
定位质量的量化检查:RViz 里开 ParticleCloud,粒子云应收敛成一团贴在地图上;导航时粒子云若发散成几团 = 定位丢失,先别导航,查地图/里程计。
四、代价地图:三层结构与代价衰减公式
三层各司其职
| 层 | 数据来源 | 作用 |
|---|---|---|
| static_layer | map_server 静态地图 | 已知障碍(墙) |
| obstacle_layer | 实时传感器(LaserScan) | 动态障碍 + 清障(raytrace) |
| inflation_layer | 前两层结果 | 把障碍膨胀出”禁区”(cost 梯度) |
marking 与 clearing 的语义(obstacle_layer 的灵魂):
marking: true:激光打到的点(obstacle_range内)标记为障碍clearing: true:激光穿过的区域(从雷达到障碍点之间,raytrace_range内)清除为自由——这保证移动障碍消失后,代价地图能自动把残留的障碍擦掉- **必须
raytrace_range > obstacle_range**:只标记不清除,车走了障碍”影子”留在图上,导航会绕不存在的墙。典型配置:obstacle 2.5m、raytrace 3.0m
1 | # costmap_common_params.yaml |
膨胀代价的衰减公式(inflation 的数学)
inflation 层对每个障碍栅格周围施加代价,随距离指数衰减:
$$\text{cost}(d) = 252 \cdot e^{-\alpha (d - d_{inscribed})}, \quad \alpha = \frac{\text{cost_scaling_factor}}{\text{inflation_radius} - d_{inscribed}}$$
- 障碍栅格本身代价 254(致命 lethal);footprint 内切圆半径(inscribed)以内代价 253(内切碰撞);外切圆半径(circumscribed)以外代价 < 253,规划器可以”擦边过”
cost_scaling_factor越小衰减越慢:2.58 是”温和推开”(路径平滑绕行),5.0 是”贴墙走”(窄通道能过但风险高),0.5 是”极远就躲”(空旷场景安全,窄门过不去)inflation_radius必须 ≥ 机器人内切半径,否则规划的路径会要求机器人穿过自己过不去的缝(贴着障碍走,实际会撞)。底盘 0.5m 半宽 → 膨胀半径至少 0.55~0.6
全局 vs 局部代价地图
| global_costmap | local_costmap | |
|---|---|---|
| 坐标系 | map | odom |
| 更新 | 静态地图为主,导航前构建 | 滚动窗口,随机器人移动 |
| 数据 | 静态层 + 障碍 + 膨胀 | 障碍 + 膨胀(无静态层) |
| 用途 | 全局路径规划 | 局部避障 + 速度约束 |
rolling_window: true + 小尺寸(5m×5m)让局部地图始终以机器人为中心,代价是窗口外无信息——所以局部规划器只负责”眼前几米”,全局路径由 global planner 负责。局部代价地图分辨率 0.05m 时 5m 窗口 = 100×100 栅格,20Hz 更新完全跑得动。
五、路径规划:A*、DWA、TEB 的算法本质
A*:启发函数必须 admissible
A* 在 Dijkstra 基础上加启发项:f(n) = g(n) + h(n),其中 h(n) 是 n 到目标的估计代价。最优性条件:h(n) 不能高估真实代价(admissible)——2D 栅格里欧氏距离永远 ≤ 实际路径长度,所以 A* 保证最优;曼哈顿距离在允许斜走的 8 邻域里会高估,可能给出次优路径。工程结论:global_planner 用 AStar 时保持默认欧氏启发,别手改。
1 | # global_planner 参数 |
default_tolerance 是高频坑:目标点设在地图障碍上(或贴墙),A* 找不到路径直接失败报 “Failed to get a plan”——给 0.2~0.5m 容差让它停到目标附近可达点。
DWA:速度空间采样 + 轨迹评价
DWA(动态窗口法)的核心:在机器人当前可达的速度空间里采样(考虑加速度极限的动态窗口),对每个采样速度预测一段轨迹(运动学前推),用评价函数打分,选最高分执行:
$$J = \alpha \cdot \text{heading}(v,\omega) + \beta \cdot \text{dist}(v,\omega) + \gamma \cdot \text{vel}(v,\omega)$$
heading:轨迹末端朝向与目标方向的夹角(越小越好)dist:轨迹上离最近障碍的距离(越远越好,撞障碍的直接淘汰)vel:速度大小(鼓励快跑)- 三个权重就是行为风格:
α大 → 直奔目标(可能绕不过障碍)、β大 → 保守绕行(可能绕远路)、γ大 → 飙车
1 | # base_local_planner (DWA) |
运动学约束在这里体现:acc_lim 限制加速度(防打滑,呼应麦轮篇的 S 曲线)、max_vel_theta 限制转弯速率——这些值必须 ≤ 底盘实际能力,否则 DWA 预测的轨迹底盘执行不了,表现是”规划器一直重新规划、车走走停停”。
TEB:时间弹性带
TEB(Timed Elastic Band)把路径看成一条带时间戳的弹性带,优化目标是最小化”到目标时间”同时满足约束(避障、运动学、动力学):
- 相比 DWA 只规划 1.7 秒,TEB 规划整条局部路径,提前看到弯道减速,窄道通行能力更强
- 参数核心:
min_obstacle_dist(离障碍最近距离)、weight_obstacle(避障权重)、dt_ref(相邻位姿时间间隔,调小 = 轨迹更精细但更费算力) - 坑:TEB 参数多且相互耦合,调不好比 DWA 更容易”原地抖动”——先用 DWA 跑通,再用 TEB 优化,别一上来就 TEB
六、move_base 状态机与恢复行为
move_base 内部是一个状态机:全局规划 → 局部规划执行 → 失败 → 恢复行为 → 重试。三个默认恢复行为(recovery behaviors):
conservative_reset:清除代价地图上距离机器人较远的障碍(可能是传感器误报/动态障碍残留)rotate_recovery:原地旋转 360° 重新扫描,刷新局部代价地图(车在窄道里会原地转圈,就是这个)aggressive_reset:清除整个代价地图上的障碍层重新来过
1 | # 三个恢复行为全关 = 失败直接放弃,别这么干 |
导航失败的标准排查路径(按此顺序,90% 的问题在前两层):
1 | 1. TF 树对吗? → view_frames:odom→base_link→laser 是否连续更新 |
最后一条最坑:导航栈一切正常、/cmd_vel 频率正确,车就是不动——查底盘。导航调试一半的时间花在”导航没问题,是执行层的问题”上。
七、调参实战:顺序与量化评估
调参顺序(每步验证再进下一步)
- TF:树结构 + 更新频率(10Hz 以上)
- 静态地图:加载后激光与地图偏差 < 5cm
- AMCL 定位:粒子收敛、定位误差 < 10cm(推车转一圈看粒子云)
- 膨胀层:膨胀半径 ≥ 内切半径,窄道能过、贴墙不撞
- 全局规划:路径合理(不穿墙、不绕远、终点可达)
- 局部规划:跟踪平滑、不抖动、窄道不卡
- 速度限制:加减速匹配底盘能力,不打滑
量化评估指标
| 指标 | 定义 | 合格线 |
|---|---|---|
| 路径长度比 | 实际行驶距离 / 最短可行距离 | < 1.3 |
| 到达时间 | 起点到目标耗时 | 与速度上限的理论下限比 < 2x |
| 绕路率 | 明显绕行(如绕 3 倍远)的次数 | 0 |
| 成功率 | 同一任务重复 10 次成功次数 | ≥ 9/10 |
| 贴墙距离 | 行驶中与障碍最近距离 | > 膨胀半径的 80% |
调参要有对照组:改一个参数跑同一组任务(固定起点终点),记录上面 5 个指标再对比。**”感觉顺畅了”不算数**——这是把导航从”能跑”做到”可靠”的唯一路径,与 PID 篇的 MAE/ITAE 量化评估是同一个方法论。
踩坑清单
- NaN/inf 当有效距离:
ranges[i]无效值会导致代价地图”脸上有墙”,先过滤再进 costmap - 运动畸变不管:建图时快开快转,地图糊;控制车速 + 用带畸变补偿的 cartographer
- raytrace_range ≤ obstacle_range:障碍影子残留,导航绕不存在的墙;永远 raytrace > obstacle
- 膨胀半径小于内切半径:规划路径要求机器人钻自己过不去的缝
- 目标点在地图上(default_tolerance=0):A* 无解报失败;给 0.2~0.5m 容差
- acc_lim 超过底盘能力:规划器预测轨迹执行不了,走走停停
- 导航不动先查底盘:cmd_vel 正常车不动 = 执行层问题,别在导航参数里瞎调
- 里程计差就建图:麦轮打滑/里程计漂移,gmapping 必糊;先修里程计
- AMCL 不给初始位姿:全局定位几十秒还可能收敛到错误位置;RViz 手动初始化
- 调参无对照组:改参数不记录指标 = 玄学调参;用路径长度比/成功率量化