点云预处理:让原始数据变得“干净”

大家好,我是你们的讲师。今天我们来聊聊点云预处理。说实话,这是整个激光雷达感知算法里最“脏”最累的活,但也是最重要的一步。

你想想看,激光雷达扫出来的原始点云,那叫一个“原生态”——远处有飘着的噪点,近处有地面反射的杂波,还有各种你根本不想看到的背景物体。我刚开始做这个方向时,就吃过这个亏:直接把原始点云丢进检测网络,结果模型训练出来跟个“瞎子”似的,啥都认不准。

所以,预处理就是给点云“洗澡”。洗得干净,后面的算法才能跑得顺。今天我们要讲四种最常用的滤波方法:直通滤波、体素滤波、统计滤波和半径滤波。它们各有各的脾气,也各有各的用武之地。

核心观点:预处理不是“可选项”,而是“必选项”。跳过这一步,后面的算法精度会大打折扣。

点云预处理 直通滤波 体素滤波 统计滤波 半径滤波 应用场景 去除背景 → 降采样 → 去噪点 → 保留有效点云 直通滤波 → 体素滤波 → 统计滤波 → 半径滤波(按处理顺序)

直通滤波:最简单的“切蛋糕”

直通滤波,说白了就是“切一刀”。你设定一个范围,比如 x 轴从 -50 米到 50 米,那么在这个范围之外的点,全部砍掉。

我个人的习惯是,拿到点云数据后,第一件事就是做直通滤波。为什么?因为激光雷达的视野虽然广,但真正有用的区域往往就在车前方几十米。远处的点不仅稀疏,而且噪声大,留着反而干扰算法。

小技巧:直通滤波的参数设置要结合你的应用场景。高速场景可以设远一点(100米),城区场景设近一点(50米)就够了。

// C++ 示例:PCL 直通滤波
#include <pcl/filters/passthrough.h>

pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");  // 沿 Z 轴滤波
pass.setFilterLimits(-1.5, 2.0);  // 保留 -1.5 到 2.0 米之间的点
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_filtered);

你看,代码就这么几行。但别小看它。我曾经在一个项目中,就因为没做直通滤波,导致后面的地面分割算法把远处的树梢当成了地面点,结果整个路径规划都乱了套。

体素滤波:给点云“瘦身”

体素滤波,也叫体素网格滤波。它的思路很简单:把空间划分成一个个小立方体(体素),每个体素里只保留一个点(通常是重心点)。

为什么要做这个?因为激光雷达一帧数据可能有几十万个点,直接处理太慢了。我做过测试,一个 64 线激光雷达的原始点云,不做降采样的话,在嵌入式设备上跑检测算法,帧率能掉到 5 FPS 以下。这哪行啊?

体素大小 点云数量(约) 处理时间 精度影响
0.1m 80,000 几乎无影响
0.2m 30,000 中等 轻微
0.5m 8,000 明显

注意:体素大小不是越小越好。太小了降采样效果不明显,太大了会丢失细节。我一般从 0.2m 开始试,根据实际效果调整。

// C++ 示例:PCL 体素滤波
#include <pcl/filters/voxel_grid.h>

pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.2f, 0.2f, 0.2f);  // 体素边长 0.2 米
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
voxel.filter(*cloud_filtered);

嗯,这里要注意一点:体素滤波会改变点的精确位置。如果你做的是高精度定位,那就要慎重了。但对于目标检测来说,这个精度损失完全可以接受。

统计滤波:揪出“离群点”

统计滤波,英文叫 Statistical Outlier Removal。它的原理是基于一个观察:在点云中,正常点周围的邻居多,而噪声点周围的邻居少。

具体做法是:对每个点,计算它到 k 个最近邻的平均距离。如果这个平均距离超出了全局均值的一定标准差范围,就把它标记为离群点,删掉。

我遇到过最典型的场景是:激光雷达在雨天工作时,雨滴会反射出很多孤立的噪点。这些点用肉眼都看得出来,但算法可不会自动忽略它们。统计滤波就是专门干这个的。

// C++ 示例:PCL 统计滤波
#include <pcl/filters/statistical_outlier_removal.h>

pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);          // 考虑 50 个最近邻
sor.setStddevMulThresh(1.0);  // 标准差倍数阈值
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_filtered);

参数调优建议:setMeanK 一般取 20-50,setStddevMulThresh 取 1.0-2.0。阈值越小,删除的点越多。我建议先从保守的参数开始,逐步收紧。

半径滤波:另一种“社交距离”判断

半径滤波和统计滤波有点像,但判断逻辑不同。它看的是:在某个半径范围内,点的数量是否达到阈值。如果不够,就认为这个点是孤立的,删掉。

说白了,统计滤波看的是“平均距离”,半径滤波看的是“邻居数量”。两者各有优劣。我个人更喜欢用半径滤波,因为它的物理意义更直观——你设定一个半径,比如 0.5 米,然后说“在这个圈子里至少要有 3 个点”,否则就删掉。

// C++ 示例:PCL 半径滤波
#include <pcl/filters/radius_outlier_removal.h>

pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.5);     // 搜索半径 0.5 米
ror.setMinNeighborsInRadius(3);  // 最少 3 个邻居
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
ror.filter(*cloud_filtered);

经验之谈:半径滤波对稀疏点云效果不太好。如果点云本身就很稀疏,建议用统计滤波代替。我曾经在高速场景下用半径滤波,结果把远处的车辆点云也误删了,后来改用了统计滤波才解决问题。

四种滤波的“组合拳”

在实际项目中,很少只用一种滤波。我通常的流程是:

  1. 先直通滤波:砍掉远处的无用点,缩小处理范围
  2. 再体素滤波:降采样,减少计算量
  3. 最后统计/半径滤波:去除噪声点

这个顺序是有讲究的。先做直通滤波可以减少后续处理的数据量,体素滤波放在统计滤波之前可以避免把稀疏区域的点误判为噪声。你想想看,如果先做统计滤波,体素滤波又把点给合并了,那统计滤波的效果不就白费了吗?

好了,这就是点云预处理的四种核心方法。代码都很简单,但参数调优需要经验积累。我建议你多拿几组不同场景的数据试试,找到最适合你项目的参数组合。


专注资料整理