4. 点云预处理与滤波:体素滤波、直通滤波、统计滤波,去除离群点与降采样

点云数据,说白了就是一堆三维坐标点的集合。但现实世界里的点云,远没有教科书上那么干净。激光雷达扫出来的数据,夹杂着噪声、飞点、还有各种莫名其妙的离群值。如果你直接把原始点云丢给后面的障碍物检测模块,那结果基本没法看。

我刚开始做无人机避障那会儿,就吃过这个亏。有一次试飞,飞机对着一个电线杆直直撞过去,我百思不得其解。后来回放数据才发现,点云里有个孤立的噪点被误判成了可穿越的空隙。嗯,从那以后,我再也不敢轻视预处理这一步了。

今天我们就来聊聊点云预处理的三个核心手段:体素滤波直通滤波统计滤波。它们各自解决不同的问题,组合起来用效果最好。

4.1 体素滤波:降采样的利器

体素滤波,说白了就是用一个个小立方体(体素)把空间网格化。每个体素内只保留一个代表点,通常是重心或中心点。

为什么需要它?

  • 原始点云太密了。比如Velodyne 64线雷达,一秒钟能扫出130万个点。你想想看,无人机上的嵌入式电脑哪扛得住?
  • 降采样后,计算量大幅下降,但几何结构基本保留。

核心参数:体素大小

  • 体素越大,降采样越狠,点云越稀疏。
  • 体素太小,降采样效果不明显。
  • 我个人习惯:室内场景用0.05m,室外开阔场景用0.1m~0.2m。

重要提醒:体素滤波会丢失细节。如果你要识别细小的障碍物(比如电线、树枝),体素尺寸别超过0.1m。

// C++ 示例:PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud);
vg.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长10cm
vg.filter(*cloud_filtered);

4.2 直通滤波:划定感兴趣区域

直通滤波,名字听着玄乎,其实就是切一刀。沿着某个坐标轴,只保留指定范围内的点。

典型应用场景:

  • 无人机飞行高度5米,那10米以上的点云基本是噪声,直接切掉。
  • 只关注前方30米内的障碍物,远处的点云不要。
  • 地面点云干扰太大,把z轴负半轴的点滤掉。

我在项目中遇到过一个问题:无人机在树林里飞行,雷达扫到头顶的树叶,导致高度估计频繁跳变。后来加了直通滤波,把高度限制在无人机上下2米范围内,问题就解决了。

小技巧:直通滤波通常放在预处理的第一步。先切出感兴趣区域,后面的计算量就小多了。

// C++ 示例:PCL 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");      // 沿z轴滤波
pass.setFilterLimits(0.0, 5.0);    // 保留0~5米范围
pass.filter(*cloud_filtered);

4.3 统计滤波:揪出离群点

统计滤波的原理很简单:计算每个点与其k个近邻的平均距离。如果这个距离明显大于整体平均水平,那这个点很可能是离群点,干掉它。

为什么会有离群点?

  • 激光打到玻璃、水面等镜面物体上,产生反射噪点。
  • 灰尘、雨滴、飞虫造成的瞬时噪点。
  • 传感器本身的测量误差。

两个关键参数:

参数 含义 推荐值
k(近邻数) 计算平均距离时考虑的邻居数量 20~50
std_dev(标准差倍数) 判定阈值,超过均值+std_dev*标准差即视为离群点 1.0~2.0

注意:std_dev设得太小,会把正常点也滤掉;设得太大,离群点滤不干净。我曾经在雨雾天气测试,把std_dev调到3.0才勉强够用。环境不同,参数要灵活调整。

// C++ 示例:PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);          // 近邻数50
sor.setStddevMulThresh(1.0); // 标准差倍数1.0
sor.filter(*cloud_filtered);

4.4 三种滤波的配合策略

在实际项目中,我通常按这个顺序处理点云:

  1. 直通滤波:先切出感兴趣区域,减少数据量。
  2. 统计滤波:去除离群噪点,净化数据。
  3. 体素滤波:最后降采样,控制点云密度。

为什么会是这个顺序?你想想看,如果先做体素滤波,离群点会被平均到体素里,反而更难识别。先做统计滤波,离群点孤立存在,一抓一个准。

原始点云 直通滤波 统计滤波 体素滤波 预处理流水线:先切区域 → 去噪点 → 再降采样

4.5 实战中的避坑指南

讲几个我踩过的坑,希望对你有帮助。

坑一:体素滤波后点云变稀疏,障碍物检测不到

有一次我在测试无人机避障,体素尺寸设了0.2m。结果一根5cm粗的树枝,体素里只留下一个点,障碍物检测模块直接忽略了它。后来我把体素尺寸改到0.05m,问题解决。

坑二:统计滤波把墙上的点也滤掉了

室内场景中,墙面本身很平整,每个点的近邻距离都很小。但如果你把std_dev设得太小(比如0.5),墙面边缘的点会被误判为离群点。我建议室内场景用1.5~2.0,室外场景用1.0~1.5。

坑三:直通滤波切得太狠,丢失关键信息

无人机在斜坡上飞行时,如果你用固定的z轴范围做直通滤波,可能会把坡面上的障碍物点切掉。我的做法是:结合无人机当前的姿态,动态调整滤波范围。

核心原则:滤波参数没有银弹。每个场景都要单独调参。我习惯在代码里留一个参数配置文件,现场试飞时边调边看效果。

4.6 代码实战:完整的预处理流程

下面是一个完整的预处理函数,把三种滤波串起来。代码不难,但很实用。

#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/filters/voxel_grid.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr preprocessCloud(
    pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud) {
    
    // 1. 直通滤波:只保留前方30米,高度0~5米
    pcl::PassThrough<pcl::PointXYZ> pass;
    pass.setInputCloud(input_cloud);
    pass.setFilterFieldName("x");
    pass.setFilterLimits(0.0, 30.0);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_pass(new pcl::PointCloud<pcl::PointXYZ>);
    pass.filter(*cloud_pass);
    
    pass.setInputCloud(cloud_pass);
    pass.setFilterFieldName("z");
    pass.setFilterLimits(0.0, 5.0);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_pass_z(new pcl::PointCloud<pcl::PointXYZ>);
    pass.filter(*cloud_pass_z);
    
    // 2. 统计滤波:去除离群点
    pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
    sor.setInputCloud(cloud_pass_z);
    sor.setMeanK(30);
    sor.setStddevMulThresh(1.5);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_sor(new pcl::PointCloud<pcl::PointXYZ>);
    sor.filter(*cloud_sor);
    
    // 3. 体素滤波:降采样
    pcl::VoxelGrid<pcl::PointXYZ> vg;
    vg.setInputCloud(cloud_sor);
    vg.setLeafSize(0.1f, 0.1f, 0.1f);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_final(new pcl::PointCloud<pcl::PointXYZ>);
    vg.filter(*cloud_final);
    
    return cloud_final;
}

这段代码我用了好几年,基本框架没变过。唯一会改的就是参数——不同传感器、不同场景,参数差异很大。

好了,点云预处理就聊到这儿。记住一句话:预处理做得好,后面障碍物检测就成功了一半。别嫌这步麻烦,它值得你花时间。

专注资料整理