第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 特征地图:用特征点说话

特征地图,说白了就是「找地标」。你想想看,你回家的时候怎么认路?是不是看「那个红色的大楼」、「拐角处的便利店」?机器人也一样,它通过识别环境中的特征点来定位。

常见的特征包括:

  • 角点:墙角、桌角这些有明显角度变化的地方
  • 边缘:门框、窗户边框
  • 纹理:地毯花纹、墙面纹理
  • 平面:墙壁、地板

特征地图的构建流程大概是这样的:

  1. 传感器采集数据(激光雷达或摄像头)
  2. 提取特征点(比如用ORB、SIFT算法)
  3. 特征匹配(把当前帧的特征和地图中的特征对上)
  4. 位姿估计(根据匹配结果算出机器人的位置)
  5. 地图更新(把新的特征点加入地图)

我记得有一次做视觉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++实现一个高性能的建图模块。到时候咱们手写代码,把理论落地。

记住:地图是机器人的眼睛,选对地图,你的扫地机就成功了一半。