2、点云预处理(上):体素滤波降采样、直通滤波、统计滤波去噪

各位同学,欢迎来到点云预处理的第一讲。

说实话,在自动驾驶感知这条路上摸爬滚打这么多年,我最大的体会就是:原始点云数据,基本没法直接用。你想想看,一帧64线激光雷达的数据,动辄十几万个点,里面还夹杂着各种噪声、离群点、地面反射杂波。你要是直接把这么一堆原始数据扔给检测网络,别说实时性了,内存都能给你撑爆。

所以,预处理这一步,看似基础,实则决定了整个感知管线的上限。今天我们就来聊聊三个最常用的预处理手段:体素滤波降采样、直通滤波、统计滤波去噪

核心观点:预处理不是简单的“过滤”,而是有策略的“信息浓缩”。去掉冗余,保留关键,这才是高手和新手的区别。

点云预处理(上) 体素滤波降采样 用立方体网格“浓缩”点云 直通滤波 按坐标轴“切”出ROI 统计滤波去噪 基于邻域统计剔除离群点 目标:减少数据量 + 保留几何结构 + 去除噪声

2.1 体素滤波降采样:给点云“瘦身”

体素滤波,说白了就是给点云空间画一个三维网格。每个小立方体(体素)里,只保留一个代表点。这个代表点通常是体素内所有点的重心,或者直接取中心点。

为什么需要它? 我举个例子你就明白了。有一次我在做高速公路场景的感知,一帧64线激光雷达的数据量大概在13万点左右。直接跑检测模型,帧率只有8帧,根本没法用。后来加了体素滤波,把体素大小设为0.2米,点云瞬间降到2万点,帧率直接飙到25帧。检测精度呢?只掉了不到1%。

这就是体素滤波的魅力——用极小的精度代价,换取巨大的性能提升

我的经验:体素大小的选择很关键。0.1米适合近距离密集场景,0.3米以上适合远距离稀疏场景。我个人习惯在城区场景用0.15米,高速场景用0.25米。你可以根据实际场景微调。

来看代码实现(基于PCL):

// C++ PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud_input);
voxel_filter.setLeafSize(0.2f, 0.2f, 0.2f);  // 体素边长0.2米
voxel_filter.filter(*cloud_filtered);

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

Python版本(Open3D)也很简洁:

import open3d as o3d

pcd = o3d.io.read_point_cloud("lidar_frame.pcd")
down_pcd = pcd.voxel_down_sample(voxel_size=0.2)
print(f"原始点数: {len(pcd.points)}, 降采样后: {len(down_pcd.points)}")
注意:体素滤波会丢失微小的几何细节。如果你在做高精度地图或者需要识别细小物体(比如路沿、锥桶),体素大小不要超过0.1米。我曾经因为体素设得太大,导致一个30厘米高的锥桶在点云里完全消失了,后来排查了半天才发现是滤波参数的问题。

2.2 直通滤波:快速切出你的ROI

直通滤波,名字听着高大上,其实原理特别简单——就是沿着坐标轴切一刀。

你想啊,自动驾驶场景里,我们关心的是前方50米、左右各20米、高度在-2米到3米之间的区域。超出这个范围的点,要么是远处的噪声,要么是天空或者地面以下的无效点。直接砍掉,干净利落。

为什么叫“直通”? 因为它只在一个维度上做判断。比如只保留X轴在0到50米之间的点,其他轴不管。这就是“直通”的含义。

我刚开始做感知的时候,总觉得直通滤波太简单,不屑于用。后来发现,简单的东西往往最有效。在预处理管线里,直通滤波通常是第一步,因为它能快速剔除大量无关点,为后续处理减负。

代码示例:

// PCL 直通滤波 - 只保留前方50米内的点
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_input);
pass.setFilterFieldName("x");          // 沿X轴滤波
pass.setFilterLimits(0.0, 50.0);       // 保留0~50米
pass.setFilterLimitsNegative(false);   // false表示保留区间内
pass.filter(*cloud_filtered);

Python版本:

import numpy as np

points = np.asarray(pcd.points)
# 保留X轴0~50米,Y轴-20~20米,Z轴-2~3米
mask = (points[:, 0] >= 0) & (points[:, 0] <= 50) & \
       (points[:, 1] >= -20) & (points[:, 1] <= 20) & \
       (points[:, 2] >= -2) & (points[:, 2] <= 3)
filtered_pcd = pcd.select_by_index(np.where(mask)[0])
避坑指南:直通滤波的阈值设置要结合激光雷达的安装位置。比如激光雷达装在车顶1.8米处,那么Z轴下限可以设为-2.0米(考虑路面起伏),上限设为2.5米(基本覆盖行人高度)。我曾经见过有人把Z轴上限设到10米,结果把路边的树冠全保留了,白白增加了计算量。

2.3 统计滤波去噪:揪出那些“孤魂野鬼”

体素滤波和直通滤波处理完,点云已经干净了不少。但还有一个问题——离群点

什么是离群点?就是那些孤零零飘在空中的点。可能是激光打到飞鸟身上,可能是多路径反射造成的鬼影,也可能是传感器本身的噪声。这些点数量不多,但危害极大——检测算法很容易把它们误识别成障碍物。

统计滤波的原理很直观:对每个点,计算它到最近K个邻居的平均距离。如果这个平均距离太大,说明它周围很空旷,大概率是离群点,直接干掉。

嗯,这里要注意,K值的选择很关键。K太小,容易把正常点误删;K太大,计算量上去了,而且可能把边缘点也删掉。

我个人的经验是:K取20~30,标准差倍数取1.0~2.0。具体来说,先计算所有点平均距离的均值和标准差,然后设定一个阈值:均值 + 倍数 × 标准差。超过这个阈值的,就是离群点。

来看代码:

// PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_input);
sor.setMeanK(20);              // 每个点考虑20个最近邻
sor.setStddevMulThresh(1.5);   // 标准差倍数阈值
sor.filter(*cloud_filtered);

Python版本:

# Open3D 统计滤波
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=1.5)
inlier_pcd = pcd.select_by_index(ind)
outlier_pcd = pcd.select_by_index(ind, invert=True)
我曾经踩过的坑:有一次在雨雾天气测试,统计滤波把大量正常点都删掉了。后来发现,雨雾天气下点云整体变得稀疏,每个点的邻居距离都偏大,导致正常点也被判定为离群点。解决方案是:在恶劣天气下,适当增大标准差倍数(比如从1.5调到2.5),或者干脆关闭统计滤波,改用基于深度学习的去噪方法。

2.4 三种滤波的配合使用策略

这三种滤波不是孤立使用的。我一般按照这个顺序来:

  1. 第一步:直通滤波——快速切出ROI,剔除远距离和无效区域
  2. 第二步:统计滤波——去除离群点和噪声
  3. 第三步:体素滤波——最后降采样,控制点云密度

为什么把体素滤波放在最后?因为统计滤波需要计算邻居距离,如果点云太密,计算量会很大。先做直通和统计,把无关点和噪声去掉,再做体素降采样,这样效率最高。

总结一下:

  • 体素滤波:降采样,减少数据量,保留几何结构
  • 直通滤波:按坐标轴切出感兴趣区域
  • 统计滤波:基于邻域统计剔除离群点

三者配合,基本能处理90%以上的点云预处理需求。

好了,这一讲的内容就到这里。这三种滤波方法,看起来简单,但用好了真的能省很多事。下一讲我们会继续聊点云预处理的另外几个重要话题——地面滤波和点云分割。到时候见。


专注资料整理