点云预处理:直通滤波、体素滤波与统计滤波

做激光雷达障碍物检测,有个环节我特别想跟你聊聊——点云预处理。

很多人一上来就急着跑聚类、做识别,结果数据又大又乱,算法跑得慢不说,还老出假阳性。我刚开始做这个方向时也吃过这个亏。后来才明白:预处理做得好,后面事半功倍;预处理偷了懒,后面全是坑。

今天咱们就掰开揉碎,把三种最常用的滤波方法讲透:直通滤波、体素滤波、统计滤波。这三种方法,说白了就是解决三个问题——裁掉没用的、减少数据量、去掉噪声点。

核心观点:点云预处理不是可选项,而是必选项。原始点云数据通常包含大量无关区域、冗余点和噪声,直接处理会导致计算爆炸和结果不可靠。

点云预处理三大滤波方法 原始点云 直通滤波 PassThrough Filter 体素滤波 VoxelGrid Filter 统计滤波 StatisticalOutlierRemoval 裁剪空间范围 降采样 + 结构化 去除离群噪声点 预处理后的干净点云 → 后续聚类/识别

一、直通滤波:先砍掉没用的区域

直通滤波,名字听着挺唬人,其实逻辑特别简单——按坐标轴切一刀。

你想想看,激光雷达扫描一圈,能扫到几百米外的建筑、天上的飞鸟、甚至地面上的小石子。但咱们做障碍物检测,关心的可能只是车前30米、左右各5米的范围。那远处的点留着干嘛?除了增加计算量,没别的好处。

我在做园区无人车项目时,雷达装在车顶,能扫到旁边楼房的窗户。那些点对障碍物检测完全没用,反而让聚类算法把楼房的轮廓也当成障碍物。后来加了直通滤波,只保留前方20米、左右4米、高度0.2到2米的范围,数据量直接砍掉60%,检测准确率反而上去了。

我的习惯:直通滤波的参数设置,我一般先看一遍原始点云的可视化,大概知道目标区域在哪,再动手设阈值。别闭着眼睛设,容易把有用的点也切掉。

直通滤波的代码实现,PCL里封装得特别好:

// C++ PCL 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");        // 沿Z轴(高度)滤波
pass.setFilterLimits(0.2, 2.0);      // 保留0.2米到2.0米之间的点
// pass.setFilterLimitsNegative(true); // 设为true则保留范围外的点
pass.filter(*cloud_filtered);

Python版本用Open3D也很简洁:

# Python Open3D 直通滤波
import open3d as o3d

pcd = o3d.io.read_point_cloud("scan.pcd")
# 沿X轴滤波,保留-10到10米范围
pcd_cropped = pcd.crop(o3d.geometry.AxisAlignedBoundingBox(
    min_bound=(-10, -100, -100),
    max_bound=(10, 100, 100)
))

这里有个细节要注意:滤波轴的选择。如果雷达装在车顶,Z轴滤波能去掉地面和头顶的点;如果关心的是前方障碍物,X轴滤波更合适。我一般会组合使用,先切Z轴去掉地面和天空,再切X轴限制探测距离。

警告:直通滤波的阈值设置要留有余量。比如障碍物高度可能是2.1米,你设成2.0米就把车顶的障碍物切掉了。我习惯留10%-20%的余量。

二、体素滤波:给点云「减肥」

直通滤波砍掉了区域,但剩下的点还是太多。一台16线激光雷达,一秒钟能产生30万个点。这么多点全留着做聚类,CPU直接冒烟。

体素滤波就是用来解决这个问题的。它的思路很巧妙:把空间切成一个个小立方体(体素),每个立方体里只保留一个代表点。说白了,就是用一个小格子去「概括」一片区域。

我记得有一次做高速场景的障碍物检测,原始点云有80多万个点,聚类算法跑了2秒多一帧,根本没法实时。用了体素滤波,叶子大小设成0.1米,点云压缩到8万个点,聚类时间降到50毫秒,检测效果几乎没有下降。

体素滤波的核心参数就一个——叶子大小(leaf size)。这个值设得越大,降采样越狠,但细节丢失也越多。

叶子大小 适用场景 效果
0.05m (5cm) 近距离高精度检测 保留细节,降采样约50%
0.1m (10cm) 一般障碍物检测 平衡精度与速度
0.2m (20cm) 远距离/快速预览 大幅降采样,细节丢失较多
0.5m (50cm) 地图构建/粗略定位 极度压缩,仅保留轮廓

代码实现也很直接:

// C++ PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setLeafSize(0.1f, 0.1f, 0.1f);  // 10cm立方体
sor.filter(*cloud_filtered);
# Python Open3D 体素滤波
voxel_down_pcd = pcd.voxel_down_sample(voxel_size=0.1)

避坑指南:体素滤波后的点云,每个点的坐标是体素内所有点的平均值。这意味着原来在体素边界的点会被「拉」到中心位置。如果你做的是高精度定位,这个误差需要考虑。我曾经因为这个原因,在车道线检测时发现点云位置偏了5厘米,排查了半天才发现是体素滤波搞的鬼。

三、统计滤波:把「野点」揪出来

激光雷达不是完美的。有时候会扫到空气中的灰尘、飞虫,或者因为多路径反射产生一些「幽灵点」。这些点离主体点云很远,数量少但位置诡异,对障碍物检测的干扰特别大。

统计滤波就是专门干这个的。它的原理很直观:计算每个点周围邻居的平均距离,如果某个点离邻居太远,就认为它是离群点,直接扔掉。

我做过一个测试:在雨雾天气采集的点云,统计滤波能去掉大约5%-8%的噪声点,而真正障碍物的点几乎不受影响。这招在恶劣天气下特别好用。

统计滤波有两个参数需要调:

  • 邻居点数(k):计算距离时考虑多少个近邻点。一般设20-50。
  • 标准差倍数(stddev):距离超过平均距离多少倍就算离群点。一般设1.0-2.0。

这两个参数怎么配合?我个人的经验是:先固定k=30,调stddev。stddev设得越小,滤波越激进,但可能误删正常点。设得太大,噪声点又滤不干净。我一般从1.5开始试,看效果再微调。

// C++ PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(30);          // 考虑30个近邻点
sor.setStddevMulThresh(1.5); // 标准差倍数阈值
sor.filter(*cloud_filtered);
# Python Open3D 统计滤波
cl, ind = pcd.remove_statistical_outlier(nb_neighbors=30, std_ratio=1.5)
pcd_clean = pcd.select_by_index(ind)

注意:统计滤波对稀疏点云效果不好。如果点云本身就很稀疏(比如远距离的点),每个点的邻居都很少,统计滤波会把正常的远距离点也当成离群点删掉。我一般在体素滤波之后再做统计滤波,这时候点云密度比较均匀,效果最好。

三种滤波的配合使用顺序

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

  1. 先直通滤波:砍掉无关区域,减少后续计算量
  2. 再体素滤波:降采样,让点云密度均匀
  3. 最后统计滤波:去掉噪声和离群点

这个顺序是有讲究的。如果先做统计滤波再做体素滤波,统计滤波时点云密度不均匀,参数不好调。如果先做体素滤波再做直通滤波,体素滤波在无关区域也浪费了计算资源。

一句话总结:直通滤波管范围,体素滤波管数量,统计滤波管质量。三步走完,点云就干净了,后面的聚类和识别才能跑得又快又准。

好了,这三种滤波方法就聊到这儿。代码我都贴出来了,建议你拿自己的数据跑一遍,看看每一步之后点云变成了什么样。亲手调一遍参数,比看十遍文章都管用。

可可精品资料库