2、点云预处理(上):体素滤波降采样、直通滤波、统计滤波去噪
各位同学,欢迎来到点云预处理的第一讲。
说实话,在自动驾驶感知这条路上摸爬滚打这么多年,我最大的体会就是:原始点云数据,基本没法直接用。你想想看,一帧64线激光雷达的数据,动辄十几万个点,里面还夹杂着各种噪声、离群点、地面反射杂波。你要是直接把这么一堆原始数据扔给检测网络,别说实时性了,内存都能给你撑爆。
所以,预处理这一步,看似基础,实则决定了整个感知管线的上限。今天我们就来聊聊三个最常用的预处理手段:体素滤波降采样、直通滤波、统计滤波去噪。
核心观点:预处理不是简单的“过滤”,而是有策略的“信息浓缩”。去掉冗余,保留关键,这才是高手和新手的区别。
2.1 体素滤波降采样:给点云“瘦身”
体素滤波,说白了就是给点云空间画一个三维网格。每个小立方体(体素)里,只保留一个代表点。这个代表点通常是体素内所有点的重心,或者直接取中心点。
为什么需要它? 我举个例子你就明白了。有一次我在做高速公路场景的感知,一帧64线激光雷达的数据量大概在13万点左右。直接跑检测模型,帧率只有8帧,根本没法用。后来加了体素滤波,把体素大小设为0.2米,点云瞬间降到2万点,帧率直接飙到25帧。检测精度呢?只掉了不到1%。
这就是体素滤波的魅力——用极小的精度代价,换取巨大的性能提升。
来看代码实现(基于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)}")
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])
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)
2.4 三种滤波的配合使用策略
这三种滤波不是孤立使用的。我一般按照这个顺序来:
- 第一步:直通滤波——快速切出ROI,剔除远距离和无效区域
- 第二步:统计滤波——去除离群点和噪声
- 第三步:体素滤波——最后降采样,控制点云密度
为什么把体素滤波放在最后?因为统计滤波需要计算邻居距离,如果点云太密,计算量会很大。先做直通和统计,把无关点和噪声去掉,再做体素降采样,这样效率最高。
总结一下:
- 体素滤波:降采样,减少数据量,保留几何结构
- 直通滤波:按坐标轴切出感兴趣区域
- 统计滤波:基于邻域统计剔除离群点
三者配合,基本能处理90%以上的点云预处理需求。
好了,这一讲的内容就到这里。这三种滤波方法,看起来简单,但用好了真的能省很多事。下一讲我们会继续聊点云预处理的另外几个重要话题——地面滤波和点云分割。到时候见。