3. 环境建模:栅格地图、ESDF、Voxel Map,动态障碍物表示,语义地图融合

环境建模,说白了就是让机器人知道「我在哪,周围有什么」。

我刚开始做机器人那会儿,觉得SLAM建个图就完事了。后来做运动规划才发现——地图怎么存、怎么查、怎么更新,直接决定了你的规划器能不能跑起来。今天咱们就把这几种主流的地图表示方式掰开揉碎了讲。

3.1 栅格地图(Occupancy Grid Map)

这是最基础、最直观的表示方式。把空间切成一个个小格子,每个格子存一个概率值——0表示空闲,1表示占据,0.5表示未知。

核心数据结构

// 一个简单的栅格地图实现
struct GridMap {
    double resolution;      // 分辨率,比如0.05m
    int width, height;      // 栅格数量
    double origin_x, origin_y; // 地图原点(世界坐标系)
    std::vector<double> data;  // 存储占据概率
};

// 查询某个世界坐标对应的栅格值
double getOccupancy(GridMap& map, double wx, double wy) {
    int ix = (int)((wx - map.origin_x) / map.resolution);
    int iy = (int)((wy - map.origin_y) / map.resolution);
    if (ix < 0 || ix >= map.width || iy < 0 || iy >= map.height)
        return -1.0; // 未知区域
    return map.data[iy * map.width + ix];
}

我踩过的坑

分辨率的选择是个大问题。我早期做过一个扫地机器人项目,为了省内存把分辨率设成0.2m。结果呢?椅子腿之间的空隙全被忽略了,机器人老往缝里钻,卡得死死的。后来改成0.05m,内存翻了好几倍,但规划出来的路径终于能走了。

经验之谈:室内移动机器人,0.05m是黄金分辨率。室外可以放宽到0.1-0.2m。别为了省内存牺牲规划质量。

3.2 ESDF(欧几里得符号距离场)

栅格地图有个硬伤——它只告诉你「这格有没有东西」,但没告诉你「离障碍物有多远」。规划路径时,我们需要知道机器人离墙还有多远,才能安全通过。

ESDF就是干这个的。每个栅格存一个距离值:正数表示到最近障碍物的距离,负数表示在障碍物内部。

为什么ESDF对规划这么重要?

  • 梯度信息:可以计算障碍物的方向,引导路径平滑绕开
  • 碰撞检测:直接查距离值,不用遍历所有障碍物
  • 优化目标:把「远离障碍物」变成数学上的代价函数

快速构建ESDF

// 从栅格地图构建ESDF(简化版)
void buildESDF(const GridMap& occupancy, ESDFMap& esdf) {
    // 第一步:初始化,障碍物内设为0,空闲设为无穷大
    for (int i = 0; i < esdf.size(); i++) {
        if (occupancy.data[i] > 0.5)  // 障碍物
            esdf.data[i] = 0.0;
        else
            esdf.data[i] = INFINITY;
    }
    
    // 第二步:双向距离传播(这里用简单的BFS示意)
    // 实际工程中会用更高效的算法,比如Fast Marching Method
    std::queue<int> q;
    for (int i = 0; i < esdf.size(); i++) {
        if (esdf.data[i] == 0.0) q.push(i);
    }
    
    while (!q.empty()) {
        int idx = q.front(); q.pop();
        // 检查四个邻居
        for each neighbor n of idx {
            double new_dist = esdf.data[idx] + resolution;
            if (new_dist < esdf.data[n]) {
                esdf.data[n] = new_dist;
                q.push(n);
            }
        }
    }
}
注意:ESDF的更新频率很关键。动态环境下,障碍物移动了,距离场必须重新计算。我见过有人每帧都全图重建,结果CPU直接拉满。建议用增量更新算法,只更新变化区域。

3.3 Voxel Map(体素地图)

二维栅格不够用怎么办?无人机、机械臂这些需要在三维空间里规划的,就得用体素地图了。

体素就是三维版的像素。每个小立方体存占据信息。但三维地图有个大问题——内存爆炸。一个100m×100m×10m的空间,0.1m分辨率,就是1亿个体素。每个体素存一个float,400MB就没了。

常用的优化方案

方案 原理 适用场景
八叉树(OctoMap) 递归分割,空白区域用大节点表示 静态环境,内存敏感
哈希表(Voxel Hashing) 只存储被观测到的体素 动态环境,实时重建
TSDF(截断符号距离场) 只存储表面附近的距离值 RGB-D重建,精度要求高

我个人比较喜欢用八叉树。为什么呢?因为大部分空间其实是空的。一个房间,墙和家具只占不到10%的体积。八叉树天然就能压缩这些空白区域,内存能省90%以上。

3.4 动态障碍物表示

静态地图好办,建一次用一年。但现实世界哪有那么安静?行人、车辆、其他机器人,这些都是会动的。

动态障碍物的几种表示方式

  1. 速度障碍物(VO):把动态障碍物映射到速度空间,判断哪些速度会导致碰撞。常用于多机器人避碰。
  2. 时空障碍物:在时间维度上扩展障碍物。比如一个行人以1m/s的速度走过,那他在未来2秒内会占据一条「时空走廊」。
  3. 概率预测:用卡尔曼滤波或粒子滤波预测障碍物未来位置,存成概率分布。

我实际项目中的做法

// 动态障碍物结构体
struct DynamicObstacle {
    int id;                    // 唯一标识
    Vector3 position;          // 当前位置
    Vector3 velocity;          // 当前速度
    double radius;             // 包围球半径
    double confidence;         // 跟踪置信度
    std::deque<Vector3> history; // 历史轨迹(用于预测)
    
    // 预测未来t秒后的位置
    Vector3 predict(double dt) {
        // 简单线性预测,实际可以用更复杂的模型
        return position + velocity * dt;
    }
};
核心原则:动态障碍物的表示要「够用就好」。别为了精确预测搞个深度学习模型,结果推理时间比规划时间还长。简单的恒速模型+卡尔曼滤波,在大多数场景下已经够用了。

3.5 语义地图融合

几何信息告诉你「这里有东西」,语义信息告诉你「这是什么东西」。把两者结合起来,就是语义地图。

为什么要做语义融合?举个例子:

  • 一个「门」和一个「墙」,在几何上都是障碍物。但规划时,门是可以打开的,墙不行。
  • 一个「桌子」和一个「椅子」,桌子下面可以钻过去,椅子下面不行(太矮)。
  • 「人行道」和「机动车道」,机器人应该走哪条?

语义地图的存储结构

// 语义体素
struct SemanticVoxel {
    uint8_t occupancy;         // 占据概率 0-255
    uint8_t semantic_class;    // 语义类别:0=未知, 1=地面, 2=墙壁, 3=门, 4=桌子...
    uint8_t traversability;    // 可通行性:0=不可通行, 1=谨慎通行, 2=自由通行
    uint16_t instance_id;      // 实例ID,区分不同的门、不同的桌子
};

// 语义地图查询
Traversability getTraversability(SemanticMap& map, Vector3 pos) {
    SemanticVoxel voxel = map.query(pos);
    if (voxel.occupancy > 200) return BLOCKED;  // 高概率占据
    if (voxel.semantic_class == DOOR && isDoorOpen(voxel.instance_id))
        return FREE;  // 门是开的,可以走
    return static_cast<Traversability>(voxel.traversability);
}

融合的难点

语义融合最大的坑在于——语义标签会出错。我做过一个项目,视觉模型把「玻璃门」识别成了「墙壁」,结果机器人对着玻璃门一头撞上去。还好速度慢,不然就出事故了。

后来我加了一个规则:如果语义标签和几何信息冲突(比如视觉说是墙,但激光雷达打过去是空的),就降低这个语义标签的置信度,优先相信几何数据。

我的建议:语义地图不要单独用。把它作为几何地图的「补充信息层」。规划时先查几何地图保证安全,再查语义地图优化路径。安全第一,智能第二。

3.6 几种地图的选型指南

场景 推荐方案 理由
室内扫地机器人 栅格地图 + ESDF 二维够用,ESDF保证安全距离
无人机导航 八叉树 + TSDF 三维空间,内存敏感
仓储AGV 栅格地图 + 语义层 环境结构化,语义信息丰富
人机协作 体素哈希 + 动态障碍物 需要快速更新,处理移动的人

嗯,环境建模这块就聊到这儿。说白了,没有最好的地图,只有最合适的。你想想看,你的机器人要在什么环境下跑?需要多高的精度?内存和算力有多少?想清楚这些,选型就简单了。

下一章咱们聊聊路径搜索——有了地图,怎么找到一条好路?