4、环境感知与障碍物表征:点云预处理、检测与建图

各位同学,欢迎来到第四章。这一章我们聊点硬核的——无人机怎么“看”懂这个世界。

说白了,无人机飞得好不好,一半看控制算法,另一半就看它能不能准确感知周围环境。我这些年调试过的炸机案例,十有八九都是感知环节出了岔子。嗯,这一章我们就把环境感知这条链路彻底打通。

核心逻辑:原始点云 → 预处理(滤波+降采样) → 障碍物检测(聚类) → 障碍物表征(包围盒/栅格)

原始点云 LiDAR / 深度相机 预处理 • 直通滤波 • 体素降采样 • 统计滤波 障碍物检测 • DBSCAN 聚类 • 欧式聚类 • 区域生长 障碍物表征 • AABB 包围盒 • OBB 包围盒 • 占据栅格地图 • 八叉树地图 环境感知与障碍物表征完整流程

4.1 点云预处理:先把数据洗干净

激光雷达扫出来的原始点云,说实话,挺脏的。有噪点、有离群点、还有大量冗余数据。你想想看,一帧64线激光雷达的数据量动辄十几万个点,直接丢给聚类算法,算力根本扛不住。

我个人习惯,预处理分三步走:

4.1.1 直通滤波(PassThrough Filter)

先砍掉不关心的区域。比如无人机飞行高度在10米以内,那10米以上的点云直接扔掉。还有太远的点(超过30米)精度已经不行了,也砍掉。

// C++ 伪代码 - 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(0.0, 10.0);  // 保留高度0~10米
pass.filter(*cloud_filtered);

我的经验:直通滤波的参数别设太死。我在项目中遇到过,无人机稍微倾斜一下,原本在范围内的点就飞出去了。建议留10%~20%的余量。

4.1.2 体素降采样(VoxelGrid Downsampling)

这是降采样最常用的方法。把空间划分成小立方体(体素),每个体素内只保留一个重心点。说白了就是用分辨率换效率。

体素大小 点云数量(原始10万点) 处理耗时 适用场景
0.05m ~8万 近距离精细避障
0.10m ~3万 中等 通用避障
0.20m ~1万 远距离粗略感知

我一般用0.1m的体素。再小的话,嵌入式平台跑不动。再大的话,细小的障碍物(比如电线)就丢了。

4.1.3 统计滤波(Statistical Outlier Removal)

这个用来干掉离群噪点。原理很简单:计算每个点周围K个邻居的平均距离,如果距离分布偏离均值太远,就判定为噪点。

// 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setMeanK(50);          // 邻居数量
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.filter(*cloud_clean);

注意:标准差倍数设得太小(比如0.5),会把正常的边缘点也干掉。我曾经吃过这个亏,导致无人机把一棵大树误判成可通行区域,差点撞上去。建议从1.0开始调。

4.2 障碍物检测与聚类:把点云变成物体

预处理完了,接下来要回答一个问题:哪些点属于同一个障碍物?

这里我重点讲DBSCAN,因为它不需要预先知道有多少个障碍物,而且能处理任意形状的簇。这在野外飞行时特别实用——你永远不知道前面是一棵树还是一根电线杆。

4.2.1 DBSCAN 核心参数

  • Eps(邻域半径):多大范围内算邻居。我一般设0.3~0.5米,视激光雷达分辨率而定。
  • MinPts(最小点数):一个簇至少要有多少个点。设太小会把噪点也当成障碍物,设太大又会漏掉小障碍物。
// DBSCAN 聚类伪代码
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.3);    // 相当于Eps
ec.setMinClusterSize(50);       // MinPts
ec.setMaxClusterSize(100000);
ec.setInputCloud(cloud_clean);
ec.extract(cluster_indices);

避坑指南:我曾经在树林里测试,DBSCAN把相邻的两棵树聚成了一个簇。原因是Eps设得太大。后来我改成自适应Eps——根据点云距离动态调整,效果好了很多。

4.2.2 其他聚类方法

除了DBSCAN,还有欧式聚类和区域生长。欧式聚类说白了就是DBSCAN的简化版,只考虑欧氏距离。区域生长则适合平面提取,比如地面和墙面。

4.3 障碍物包围盒拟合:给障碍物画框框

聚类完成后,每个障碍物都是一堆散点。但路径规划算法需要的是规则的几何形状。所以我们要给每个障碍物拟合一个包围盒。

4.3.1 AABB(轴对齐包围盒)

最简单,就是找点云在X、Y、Z方向上的最大值和最小值。计算快,但包裹不紧,尤其对于斜着的物体。

// AABB 计算
pcl::PointXYZ min_pt, max_pt;
pcl::getMinMax3D(*cluster_cloud, min_pt, max_pt);

4.3.2 OBB(有向包围盒)

OBB会跟着物体的方向旋转,包裹更紧。但计算量也大一些,要用主成分分析(PCA)求主轴方向。

我个人习惯:对于快速移动的无人机,用AABB就够了。如果是精细操作(比如穿过狭窄缝隙),必须用OBB。

小技巧:OBB算出来之后,记得把包围盒膨胀一圈(比如扩大10%)。这是给控制留安全余量。我见过太多人算得刚刚好,结果无人机一抖就蹭上了。

4.4 占据栅格地图构建:把世界变成格子

最后一步,把障碍物信息映射到栅格地图上。每个格子有三种状态:占据、空闲、未知。

为什么不用直接的点云?因为栅格地图天然适合路径规划——A*、Dijkstra这些算法都是在栅格上跑的。

4.4.1 栅格地图更新逻辑

  • 激光束穿过的格子 → 标记为空闲
  • 激光束终点(障碍物表面) → 标记为占据
  • 没有激光束经过的格子 → 保持未知
// 栅格地图更新伪代码
for each laser_scan:
    for each point in laser_scan:
        // 射线穿过的格子
        cells = bresenham(origin, point)
        for cell in cells:
            grid[cell].log_odds -= free_factor
        
        // 终点格子
        grid[point].log_odds += occupied_factor

重要:栅格分辨率的选择很关键。0.1米的分辨率适合室内,0.2~0.5米适合室外。分辨率越高,地图越精细,但内存和计算量也越大。一块100m×100m的区域,0.1米分辨率就是100万个格子,每个格子还要存概率值——内存一下子就上去了。

4.4.2 八叉树地图(OctoMap)

如果觉得栅格地图太占内存,可以用八叉树。它只对存在障碍物的区域进行细分,空白区域用大格子表示。说白了就是自适应分辨率。

我在一个机载计算资源有限的飞控上,就是用OctoMap替代了传统栅格地图,内存占用从200MB降到了15MB。嗯,这个差距还是很明显的。


好了,这一章的内容就到这里。环境感知是无人机避障的「眼睛」,预处理、聚类、包围盒、栅格地图,每一步都有坑,但也都有对应的解法。希望各位在实际项目中,能把这些方法用起来,少走我当年走过的弯路。