2. 无人机系统基础

各位同事,今天我们来聊聊环境建模。这是航线规划的基石,说白了就是回答一个问题:无人机眼中的世界长什么样?

我见过太多团队,算法写得天花乱坠,结果一上真机就撞山。为什么?环境模型没建对。你给一个2.5D的模型,它当然不知道头顶有根电线。所以这一章,我们把这几个核心概念掰开揉碎了讲清楚。

2.5D 与 3D 环境建模:差的不只是一个维度

很多人觉得2.5D就是2D加了个高度值,其实没那么简单。我习惯这么区分:

  • 2.5D:每个(x, y)坐标对应唯一一个高度z。说白了,它是一张高度图。你从正上方看下去,能看到地形起伏,但看不到悬崖下的洞穴、桥梁下方的空间。
  • 3D:每个(x, y, z)坐标都可以独立存在。同一个(x, y)位置,可以有多个z值——比如一棵树,树冠在z=10,树干在z=2,中间是空的。

核心区别一句话:2.5D是“一层皮”,3D是“一个体”。

我在做电力巡线项目时吃过这个亏。当时用2.5D模型规划航线,无人机沿着山谷飞,一切正常。结果到了铁塔附近,塔顶的横担和导线在2.5D里被“压平”了——因为那个(x, y)位置只能存一个高度。飞机直接撞上了导线。嗯,从那以后,但凡涉及杆塔、桥梁、树木的场景,我坚决上3D模型。

对比项 2.5D 模型 3D 模型
数据结构 栅格 + 高度值 体素 / 点云 / 网格
悬空物体 不支持 支持
计算量 低 高(通常高1~2个数量级)
典型场景 平原、丘陵、大范围测绘 城市峡谷、矿区、森林、电力
传感器要求 单目 / 激光测距 激光雷达 / 双目 / 深度相机

我的建议:如果飞行区域最高物体高度不超过地面起伏的20%,且没有悬空障碍物,2.5D够用。否则,老老实实上3D。省下来的算力,不够赔一架飞机。

栅格地图构建:把世界切成小方块

栅格地图,是最直观的环境表达方式。你把空间划分成一个个小格子,每个格子存一个值——0表示空闲,1表示占用,0.5表示未知。

你想想看,这就像在CAD里画网格。每个格子的大小,决定了地图的精度和内存消耗。我一般遵循一个经验法则:栅格分辨率 = 无人机安全半径的 1/3 到 1/5。比如你的四旋翼半径0.5米,栅格设0.1~0.15米就够。再细,内存爆炸,规划还慢。

构建过程其实不复杂:

  1. 传感器数据输入:激光雷达点云或深度图
  2. 坐标变换:把传感器坐标系下的点,转到世界坐标系
  3. 栅格映射:计算每个点落在哪个栅格里
  4. 概率更新:用贝叶斯公式更新栅格的占用概率

这里有个坑,我踩过。传感器有噪声,一个点可能落在栅格边界附近。如果你硬性归到某个格子,会导致边缘锯齿严重。我后来改用反距离加权的方式——一个点同时影响周围4个栅格,权重按距离分配。效果好了很多。

我曾经犯过的错:在动态环境中用固定阈值判断栅格占用。比如设阈值0.7,大于就算占用。结果树叶被风吹动,一会儿占用一会儿空闲,航线规划直接震荡。后来我加了时间衰减因子——很久没观测到的栅格,概率慢慢回归0.5(未知)。这才稳住。

八叉树地图原理:用树结构省内存

栅格地图有个致命问题:内存。一个10m×10m×5m的空间,用0.1m分辨率,就是50万个格子。如果扩展到1km范围,内存直接上GB。怎么办?八叉树(OctoMap)就是来救场的。

八叉树的思路很聪明:大块区域如果全是空闲或全是占用,就不细分。只有边界区域才递归细分到最小粒度。

我画了一张图,帮你理解这个结构:

八叉树地图原理示意图 根节点(整个空间) 子节点1 子节点2 子节点3(占用) 子节点4 子节点5(混合) 细分1 细分2 细分3 细分4 图例说明: 绿色节点 = 完全空闲,停止细分 红色节点 = 完全占用,停止细分 橙色节点 = 混合区域,继续递归细分 紫色节点 = 细分后的子节点(达到最小分辨率)

八叉树的核心操作就三个:

  • 插入:从根节点往下走,找到对应子节点,更新占用概率。如果子节点不存在,就创建。
  • 查询:给定一个3D坐标,从根节点递归查找,返回该位置是空闲、占用还是未知。
  • 剪枝:如果某个节点的所有子节点状态一致(全空闲或全占用),就把子节点删掉,父节点直接代表整个区域。这是省内存的关键。

我给你们看一段伪代码,理解一下插入逻辑:

function insertPoint(node, point, probability):
    if node.isLeaf():
        node.updateProbability(probability)
        if node.isMixed():
            node.subdivide()  // 分裂成8个子节点
        return
    
    childIndex = computeChildIndex(point, node.center)
    if node.children[childIndex] == null:
        node.children[childIndex] = new OctreeNode()
    insertPoint(node.children[childIndex], point, probability)
    
    // 插入后尝试剪枝
    if allChildrenSameState(node):
        node.prune()

实际项目中的参数:我一般设最大深度16层。对于0.1m分辨率,16层能覆盖约6.5km³的空间——够用了。深度每加1层,内存节省约30%,但查询时间增加约15%。平衡点自己把握。

三种模型怎么选?我的决策框架

说了这么多,到底用哪个?我个人的决策流程是这样的:

  1. 先看场景:有没有悬空障碍物?有 → 3D;没有 → 2.5D
  2. 再看范围:超过100m×100m?是 → 八叉树;否 → 普通栅格
  3. 最后看算力:机载电脑是STM32还是Jetson?前者老老实实2.5D+栅格,后者可以上3D+八叉树

嗯,这套框架帮我省了不少事。有一次在矿区做项目,环境复杂,但客户机载电脑性能有限。我折中用了2.5D栅格 + 局部3D八叉树——全局用2.5D做粗略规划,到障碍物附近切换到局部3D做精细避障。效果不错,内存占用控制在50MB以内。

注意:八叉树不是银弹。在高度动态的环境(比如人群密集区),频繁的插入和剪枝会导致CPU负载飙升。我遇到过八叉树更新频率超过20Hz时,树结构重组占用了40%的CPU。后来我加了更新队列,把传感器数据攒够一批再批量更新,才降下来。

好了,环境建模这块就聊到这儿。记住一句话:模型的选择,本质是精度、内存、算力三者的权衡。没有最好的模型,只有最适合你场景的模型。