第1章:地图表示方法——扫地机眼中的世界
大家好,我是你们的老朋友。今天咱们聊聊扫地机地图表示这件事。
你想想看,一台扫地机要在你家跑来跑去,它得先知道「我在哪」、「要去哪」、「怎么走」。这三个问题,说白了都离不开地图。我做了这么多年SLAM,发现很多新手一上来就扎进算法里,结果连地图怎么存的都没搞明白。嗯,今天咱们就把这个基础打牢。
1.1 栅格地图:最直观的表示方式
栅格地图,我个人习惯叫它「格子地图」。就是把环境切成一个个小方格,每个格子要么是障碍物,要么是空地。
举个例子,你家客厅大概4米×5米。如果每个格子是10厘米×10厘米,那地图就是40×50=2000个格子。每个格子存一个值:0表示空地,1表示障碍物,0.5表示未知。
核心要点:栅格地图的精度取决于格子大小。格子越小,地图越精细,但内存占用也越大。
我在项目中遇到过一个问题:有次做商用清洁机器人,客户要求识别地上的小纸片。我把格子设到2厘米,结果地图文件大到几百兆,机器人跑起来卡得要命。后来我学乖了——根据任务需求动态调整分辨率。
栅格地图的代码实现其实很简单:
class GridMap:
def __init__(self, width, height, resolution):
self.width = width # 地图宽度(格子数)
self.height = height # 地图高度(格子数)
self.resolution = resolution # 每个格子的实际尺寸(米)
self.data = [[0.5] * width for _ in range(height)] # 0.5表示未知
def set_obstacle(self, x, y):
"""设置障碍物"""
grid_x = int(x / self.resolution)
grid_y = int(y / self.resolution)
if 0 <= grid_x < self.width and 0 <= grid_y < self.height:
self.data[grid_y][grid_x] = 1.0
def is_free(self, x, y):
"""判断是否可通行"""
grid_x = int(x / self.resolution)
grid_y = int(y / self.resolution)
return self.data[grid_y][grid_x] < 0.5
避坑指南:我曾经犯过一个低级错误——直接用像素坐标当世界坐标。结果机器人明明看到墙在前面,却一头撞上去。记住:栅格坐标和世界坐标之间要有个转换关系,别搞混了。
1.2 拓扑地图:只关心连通关系
拓扑地图就更有意思了。它不关心具体尺寸,只关心「哪些地方是连通的」。你想想地铁线路图——它不会精确标注站与站之间的距离,但你能清楚地知道怎么换乘。
拓扑地图用节点和边来表示。节点代表关键位置(比如房间门口、走廊拐角),边代表连通路径。我建议在以下场景使用拓扑地图:
- 大范围环境:比如整层办公楼,用栅格地图内存爆炸,拓扑地图就轻巧多了
- 任务导向:机器人只需要知道「从A点到B点」,不需要精确的几何信息
- 快速规划:拓扑地图的路径搜索比栅格地图快一个数量级
拓扑地图的构建原理其实不复杂:
class TopoMap:
def __init__(self):
self.nodes = {} # 节点字典:{id: (x, y)}
self.edges = [] # 边列表:[(id1, id2, cost)]
def add_node(self, node_id, x, y):
self.nodes[node_id] = (x, y)
def add_edge(self, id1, id2, cost=1.0):
self.edges.append((id1, id2, cost))
def find_path(self, start_id, end_id):
"""使用BFS或Dijkstra找路径"""
# 这里省略具体实现
pass
注意:拓扑地图的精度取决于节点选取。节点太少,路径可能不准确;节点太多,又失去了拓扑地图的优势。我一般会结合栅格地图先做一次粗规划,再提取关键节点构建拓扑图。
1.3 特征地图:用特征点说话
特征地图,说白了就是「找地标」。你想想看,你回家的时候怎么认路?是不是看「那个红色的大楼」、「拐角处的便利店」?机器人也一样,它通过识别环境中的特征点来定位。
常见的特征包括:
- 角点:墙角、桌角这些有明显角度变化的地方
- 边缘:门框、窗户边框
- 纹理:地毯花纹、墙面纹理
- 平面:墙壁、地板
特征地图的构建流程大概是这样的:
- 传感器采集数据(激光雷达或摄像头)
- 提取特征点(比如用ORB、SIFT算法)
- 特征匹配(把当前帧的特征和地图中的特征对上)
- 位姿估计(根据匹配结果算出机器人的位置)
- 地图更新(把新的特征点加入地图)
我记得有一次做视觉SLAM项目,在走廊里跑得好好的,一到空旷大厅就丢定位。为什么?因为大厅里没有明显的特征点!后来我加了天花板上的灯作为特征,问题就解决了。所以特征地图对环境要求比较高,太空旷或者太重复的环境都不好使。
1.4 占据栅格地图:概率视角下的世界
占据栅格地图,是栅格地图的升级版。它不直接说「这个格子是障碍物」,而是说「这个格子是障碍物的概率是80%」。为什么要这么做?因为传感器的数据是有噪声的。
你想想看,激光雷达测距可能有几厘米的误差,超声波传感器误差更大。如果直接根据一次测量就判定障碍物,那地图会变得乱七八糟。
占据栅格地图的核心公式是:
# 对数几率形式的更新公式
# l(m_i | z_{1:t}) = l(m_i | z_{1:t-1}) + l(m_i | z_t) - l(m_i)
# 其中 l 表示对数几率(log-odds)
# 概率 p 和对数几率 l 的转换关系:
# l = log(p / (1-p))
# p = 1 / (1 + exp(-l))
代码实现上,我们维护每个格子的对数几率值,每次观测后更新:
class OccupancyGridMap:
def __init__(self, width, height, resolution):
self.width = width
self.height = height
self.resolution = resolution
# 初始化为0,对应概率0.5(完全未知)
self.log_odds = [[0.0] * width for _ in range(height)]
def update_cell(self, grid_x, grid_y, measurement):
"""
measurement: True表示检测到障碍物,False表示检测到空闲
"""
# 传感器模型参数
l_occ = 0.8 # 观测到障碍物的对数几率增量
l_free = -0.4 # 观测到空闲的对数几率增量
if measurement:
self.log_odds[grid_y][grid_x] += l_occ
else:
self.log_odds[grid_y][grid_x] += l_free
# 限制范围,防止溢出
self.log_odds[grid_y][grid_x] = max(-5.0, min(5.0, self.log_odds[grid_y][grid_x]))
def get_probability(self, grid_x, grid_y):
"""获取某个格子的占据概率"""
l = self.log_odds[grid_y][grid_x]
return 1.0 / (1.0 + math.exp(-l))
关键点:占据栅格地图的更新是增量式的。每次传感器数据来了,只更新被射线穿过的格子。这样计算量很小,适合实时运行。
1.5 四种地图的对比与选择
说了这么多,到底该用哪种地图?我给大家整理了一个对比表:
| 地图类型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 栅格地图 | 直观、易于实现、支持精确导航 | 内存占用大、大场景效率低 | 小范围室内环境(家庭、办公室) |
| 拓扑地图 | 轻量、搜索快、适合大场景 | 精度低、无法处理复杂障碍物 | 多楼层、大范围巡检 |
| 特征地图 | 鲁棒性好、支持重定位 | 依赖特征提取、空旷环境失效 | 视觉SLAM、动态环境 |
| 占据栅格地图 | 处理噪声、支持动态更新 | 计算量稍大、需要传感器模型 | 激光SLAM、实时建图 |
我个人在实际项目中,通常这样搭配:
- 建图阶段:用占据栅格地图,因为它能处理传感器噪声
- 导航阶段:用栅格地图做局部规划,拓扑地图做全局规划
- 重定位阶段:用特征地图,快速找回位置
我的经验:别想着只用一种地图搞定所有问题。我做过一个项目,一开始只用栅格地图,结果2000平米的厂房建图花了半小时。后来改成「栅格+拓扑」混合地图,建图时间缩短到5分钟,导航精度一点没降。
1.6 小结
今天咱们聊了四种地图表示方法。栅格地图最直观,拓扑地图最轻量,特征地图最鲁棒,占据栅格地图最实用。每种方法都有自己的脾气,关键是根据你的场景选对工具。
下一章,我会带大家深入占据栅格地图的构建细节,包括传感器模型、射线追踪算法、以及如何用C++实现一个高性能的建图模块。到时候咱们手写代码,把理论落地。
记住:地图是机器人的眼睛,选对地图,你的扫地机就成功了一半。