2. 机器人眼中的世界:传感器与栅格地图

上一篇:1. SLAM是什么:盲人摸象与边走边画 | 下一篇:3. 粒子滤波:一千个分身猜位置

gmapping 只吃两种数据:激光(”我看到什么”)和里程计(”我动了多少”)。这一篇讲清楚这两种数据长什么样、各有什么脾气,以及最终要画出的栅格地图是什么。


一、激光雷达:机器人的一圈”声呐”

1.1 数据长什么样

2D 激光雷达(如 RPLIDAR、思岚 S2)水平旋转,每转一圈打出几百束激光,返回一个距离数组

1
2
3
angles: [-90°, -89.5°, ..., +89.5°]   (均匀分布 360°/N 份)
ranges: [2.13, 2.10, 1.05, inf, ..., 1.87]
↑前方墙 ↑桌腿 ↑没打到东西(超量程)

一帧激光 = N 个 (角度, 距离) 对,从雷达中心向外呈放射状的一圈点。

1.2 关键参数

参数 含义 对 gmapping 的影响
频率 每秒扫几圈(典型 5~12 Hz) gmapping 依赖里程计先验,5Hz 也够用(Hector 需要 40Hz+)
角分辨率 每束激光隔几度(典型 0.25°~1°) 越密越利于扫描匹配
量程 最远测多远(典型 8~30 m) maxUrange 参数通常设为量程的 80%

1.3 激光的脾气:准,但”不知道自己是谁”

  • ✅ 测距很准(厘米级),不受光照影响;
  • ❌ 只告诉你”这个方向 X 米处有个东西”,不告诉你那是什么–两段长得一样的走廊,激光分不出来(这正是回环检测难的原因);
  • ❌ 玻璃、镜面会”穿过去”或产生鬼影。

二、里程计:从轮子转数猜走了多远

2.1 原理

轮子电机带编码器:转一圈 = 走 2πR 米。左右轮转数差 -> 转了多少角度。里程计把这两者积分成位姿变化 $\Delta x, \Delta y, \Delta \theta$。

2.2 里程计的致命伤:误差累积

每一步的小误差(轮子打滑、轮径不准、地面不平)全部累加

1
2
3
4
5
6
flowchart LR
A["真實轨迹"] --- B["里程计轨迹"]
subgraph drift["走 100 米后"]
A1["✅ 实际在 A 点"]
B1["❌ 里程计以为在 B 点<br/>(可能偏了好几米)"]
end

但短时间内的”相对变化”很可靠。gmapping 的用法正是取其长避其短:用里程计提供短时位姿先验(”我这 0.1 秒大概动了这么点”),再用激光来修正长期漂移。这也解释了为什么 gmapping 对雷达频率要求低:两帧激光之间,里程计能大致告诉你机器人怎么动的。

注:Hector SLAM 不用里程计(为救灾地形设计,轮子可能悬空),所以它必须用高频激光硬扛;Cartographer 为手持设备设计,同样没里程计。gmapping 是”有轮子机器人”的菜


三、栅格地图:把世界切成小格子

3.1 一张”灰度图”

gmapping 输出的地图,本质是一张 pgm 灰度图(ROS 里叫 /map,消息类型 nav_msgs/OccupancyGrid):

颜色 占用概率 含义
⬛ 黑 ≈ 1 障碍物(有东西,撞不得)
⬜ 白 ≈ 0 空闲(激光穿过去了,没东西)
🟫 灰 0.5 未知(从没扫到过这里)

把环境切成边长 delta(分辨率,典型 5cm)的小格子,每个格子存一个”这里被占据的概率”。这就是 gmapping 每个粒子随身携带的地图

3.2 关键问题:为什么激光能”点亮”格子?

一束激光从雷达到命中点,划过一条线段,信息量巨大:

1
2
3
4
flowchart LR
L["🤖雷达"] -.->|"激光穿过<br/>这些格子 = 空闲"| H["⬛命中点<br/>这个格子 = 占用"]
style L fill:#e8f0fe,stroke:#1a73e8
style H fill:#1e1e1e,color:#fff
  • 穿过没被挡 -> 沿途所有格子大概率是的;
  • 终点被挡住 -> 命中格子大概率是占用的。

这个”从激光射线反推格子占用概率”的经典模型叫 Bresenham 光线投射(第 8 篇实现时亲手写)。gmapping 底层用的改进版叫 Gmapping-Style Scan Matcher + 每粒子地图更新,本质相同。

3.3 地图怎么更新:还是贝叶斯

某个格子 $c$ 收到新激光证据后的更新,人话版:

新置信度 = 老置信度 × (这束激光说它占用/空闲的可能性)

情况 格子概率变化
激光穿过该格子 占用概率 ↓(乘一个 <1 的系数,如 0.4)
激光正好打在该格子 占用概率 ↑(乘一个 >1 的系数,如 0.6+0.7)
这次没涉及该格子 不变

多次观测反复修正,灰格子逐渐”变黑”或”变白” – 这就是你在 RViz 里看到地图一点点长出来的过程。

3.4 ROS 里的地图细节

  • /map 话题采用 latched(锁存) 发布:地图不更新时不重发,新订阅者也能立刻收到最后一次的地图(地图变化慢,省带宽);
  • 地图原点、分辨率、尺寸由 map_metadata 描述;
  • 每个粒子的地图在内存里,只有最优粒子的地图才会发布出来。

四、三个坐标系的”套娃”(TF 树)

gmapping 输出的定位结果,在 ROS 里表现为一条 TF 变换链:

1
2
3
4
5
6
flowchart LR
MAP["map 坐标系<br/>(全局地图, 不漂移)"] -->|"[map->odom]<br/>gmapping 负责修正"| ODOM["odom 坐标系<br/>(里程计积分, 平滑但漂移)"]
ODOM -->|"[odom->base_link]<br/>里程计直接发布"| BASE["base_link<br/>(机器人本体)"]
BASE -->|"[base->laser]<br/>静态, 装在哪就在哪"| LASER["laser 坐标系"]
style MAP fill:#e6f4ea,stroke:#188038
style ODOM fill:#fef7e0,stroke:#b06000

为什么要套娃? 分工明确:

  • odom -> base_link:里程计发布,短期平滑(适合控制),但长期漂移;
  • map -> odom:gmapping 发布,本质是修正量(”里程计世界和真实地图世界差了多少”),跳变但不漂移。

gmapping 每帧做的事:拿最优粒子的位姿(地图坐标)和里程计位姿求差,更新 map -> odom(第 6 篇源码里对应 map_to_odom_ 那三行)。


五、本篇小结

角色 长处 短处 gmapping 怎么用
激光雷达 测距准、不受光照 不识别物体、无差异场景 提供观测,修正漂移
里程计 短时相对运动准 误差累积 提供位姿先验,降低对雷达频率要求
栅格地图 直观、可导航 每粒子一张 -> 内存大户 每个粒子随身携带一份

📚 参考:源代码解析(栅格地图与 /map latched 机制)、ROS1系列(话题与参数)