点云滤波入门:滤波的意义、体素滤波、直通滤波、统计滤波

大家好,我是你们这趟点云之旅的向导。今天咱们聊聊点云处理里最基础、也最绕不开的一步——滤波。

你想想看,激光雷达扫出来的原始点云,那叫一个“脏”。噪点、离群点、冗余点,啥都有。我刚开始做感知的时候,拿到一帧64线激光雷达的数据,几百万个点直接往算法里灌,结果程序直接卡死。嗯,从那以后我就明白了——滤波不是可选项,是必选项。

滤波的核心目标就三个:

  • 降采样:减少点数,提升计算效率
  • 去噪:剔除无效噪点,提升数据质量
  • 提取ROI:只保留感兴趣区域,减少干扰

说白了,滤波就是给点云“洗澡”。洗掉脏东西,留下干净有用的部分。今天咱们重点聊三种最常用的滤波方法:体素滤波、直通滤波、统计滤波。

点云滤波知识体系 点云滤波 体素滤波 降采样 · 体素化 直通滤波 范围裁剪 · ROI提取 统计滤波 去离群点 · 统计分布 三种方法各有侧重,实际项目中常组合使用

一、体素滤波(Voxel Grid)—— 点云的“像素化”

体素滤波,我个人觉得是点云降采样里最优雅的方法。它的思路很简单:把三维空间切成一个个小立方体(体素),每个体素里只保留一个代表点。

你可以把它想象成把一张高清照片缩小成马赛克图。每个马赛克块里,只取一个颜色值。体素滤波也是这个道理。

我的经验之谈:体素大小怎么选?我一般从0.1m开始试。对于64线激光雷达,0.1m体素能把几十万点降到一两万点,同时保留足够的环境细节。如果场景简单,0.2m也行。但别超过0.5m,否则远处的行人可能就“消失”了。

来看代码实现,我用的是PCL库,这是工业界最常用的点云库:

#include <pcl/filters/voxel_grid.h>

pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
// 设置体素边长,单位:米
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*filtered_cloud);

std::cout << "原始点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << filtered_cloud->size() << std::endl;

你看,核心就三行代码。但这里有个坑——体素滤波会改变点的精确位置。每个体素里的代表点,通常是体素内所有点的重心。这意味着你丢失了原始点的精确坐标。

注意:如果你的下游任务对点云精度要求极高(比如高精地图构建),体素滤波要慎用。我曾在做车道线检测时吃过这个亏——体素滤波后,车道线边缘变得模糊,检测精度直接掉了5个点。

二、直通滤波(PassThrough)—— 简单粗暴的“切蛋糕”

直通滤波,说白了就是按坐标轴切一刀。你指定一个范围,在这个范围之外的点,全部扔掉。

举个例子。自动驾驶场景里,我们只关心前方50米内的障碍物。那就可以用直通滤波,把X轴(车辆前进方向)上大于50米的点全部滤掉。

我习惯在预处理的第一步就用直通滤波。为什么呢?因为原始点云里,远处的点又稀疏又噪,留着只会拖慢后续处理。先切一刀,数据量能减少30%-50%。

#include <pcl/filters/passthrough.h>

pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
// 只保留Z轴(高度)在 -1.0 到 2.0 米之间的点
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.0, 2.0);
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*filtered_cloud);

这里有个小技巧:可以串联多个直通滤波。比如先切X轴,再切Y轴,最后切Z轴,就能快速提取出一个长方体区域内的点云。我经常用这个方法来提取“车辆前方感兴趣区域”。

避坑指南:我曾经犯过一个低级错误——直通滤波的坐标轴搞反了。激光雷达的坐标系,X轴朝前,Y轴朝左,Z轴朝上。但不同厂家的雷达坐标系可能不同。拿到数据后,先可视化看一眼,确认坐标轴方向再动手。

三、统计滤波(StatisticalOutlierRemoval)—— 揪出“孤魂野鬼”

统计滤波,专门对付离群点。什么是离群点?就是那些周围没几个邻居的“孤魂野鬼”。

它的原理很直观:对每个点,计算它到最近K个邻居的平均距离。如果这个平均距离太大,就认为它是离群点,扔掉。

为什么会这样?因为正常点云(比如地面、墙面)的点分布密集,每个点周围都有不少邻居。而噪点(比如空气中的灰尘、传感器误检)往往是孤立的,周围没几个点。

#include <pcl/filters/statistical_outlier_removal.h>

pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
// 邻居点数:每个点考虑最近的50个邻居
sor.setMeanK(50);
// 标准差倍数:超过均值+1.0倍标准差的点视为离群点
sor.setStddevMulThresh(1.0);
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*filtered_cloud);

两个参数怎么调?我一般这样试:

参数 作用 推荐值 调参方向
MeanK 邻居点数 20-100 点云越密,值越大
StddevMulThresh 标准差倍数 0.5-3.0 值越小,滤掉的点越多

注意:统计滤波不是万能的。如果离群点恰好聚在一起(比如远处的一小片树叶),统计滤波可能识别不出来。另外,参数调得太狠,会把边缘的物体(比如行人手臂)也当成离群点滤掉。我建议先保守一点,用1.5倍标准差,看看效果再微调。

三种滤波怎么选?

实际项目中,这三种滤波我通常会组合使用:

  1. 第一步:直通滤波——快速裁剪出感兴趣区域,减少数据量
  2. 第二步:统计滤波——剔除离群噪点,提升数据质量
  3. 第三步:体素滤波——降采样到合适密度,为后续处理做准备

这个流程我用了好几年,稳定可靠。当然,具体顺序可以灵活调整。比如如果你的场景噪点特别多,可以先把统计滤波提前。

好了,今天的内容就到这里。滤波是点云处理的“基本功”,看似简单,但调参和组合使用上有很多门道。希望今天的分享能帮你少走一些弯路。

可可精品资料库