4. 点云预处理:点云滤波与离群点移除实战
点云数据进来之后,第一件事不是急着做匹配或者建图。我见过不少新手,拿到激光雷达数据就直接扔给后端,结果地图里全是飞点、噪点,路径规划根本没法用。说白了,原始点云就是一堆带噪声的坐标点,你不把它洗干净,后面每一步都会出问题。
这一章,我们就来聊聊点云预处理中最核心的三个滤波操作:直通滤波、体素滤波、统计滤波,以及离群点移除的实战方法。我会结合自己踩过的坑,把每个滤波的原理、参数调优、代码实现都讲透。
核心观点:点云预处理不是可选项,而是必选项。一个干净的点云,能让后续的配准、建图、路径规划效率提升 50% 以上。
4.1 直通滤波:快速裁剪无效区域
直通滤波是所有滤波里最直观的。它的逻辑很简单:设定一个坐标轴范围,只保留落在范围内的点,范围外的全部扔掉。
举个例子。你装了一颗 32 线激光雷达在车顶,水平视野 360°,垂直视野 40°。但你的机器人只在 10 米范围内活动,那 50 米外的点对你来说就是噪声。用直通滤波,把 x、y、z 三个方向的范围一设,远处的点就全过滤掉了。
我个人习惯在预处理的第一步就做直通滤波。为什么?因为数据量能瞬间降下来。一颗 32 线雷达每秒产生几十万个点,你先把 20 米外的点砍掉,后面体素滤波和统计滤波的计算量就小多了。
实战技巧:直通滤波的阈值不要设得太紧。比如你的机器人最大运动范围是 10 米,我建议设到 12 米,留一点余量。太紧的话,边缘区域的有效点也可能被误删。
来看一段 PCL 的代码实现:
// 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_input);
pass.setFilterFieldName("z"); // 沿 Z 轴滤波
pass.setFilterLimits(0.0, 2.0); // 保留 0~2 米范围
// pass.setFilterLimitsNegative(true); // 如果设为 true,则保留范围外的点
pass.filter(*cloud_filtered);
这里有个坑。我记得有一次做室外项目,地面有点坡度,我按平地设了 z 轴范围 0~1.5 米,结果坡上的点全被滤掉了,导致建图出现空洞。后来我改成先做一次平面拟合,再根据拟合结果动态调整 z 轴范围,问题才解决。
4.2 体素滤波:降采样不减特征
体素滤波,说白了就是把空间切成一个个小立方体(体素),每个体素里只保留一个代表点。这个代表点通常是体素内所有点的重心,或者离重心最近的点。
你想想看,一个 10 米×10 米的区域,如果体素大小设为 0.1 米,那就有 100×100×100 = 100 万个格子。但实际点云密度远没这么高,体素滤波能帮你把冗余的点压缩掉,同时保持物体的几何形状。
体素大小怎么选?我一般遵循这个原则:
| 应用场景 | 推荐体素大小 | 说明 |
|---|---|---|
| 室内小场景(10m 以内) | 0.05 ~ 0.1 m | 保留细节,适合建图 |
| 室外中等场景(50m 以内) | 0.1 ~ 0.2 m | 平衡精度与效率 |
| 大场景(100m 以上) | 0.2 ~ 0.5 m | 快速降采样,用于导航 |
代码实现也很直接:
// 体素滤波降采样
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud_filtered);
voxel.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长 0.1 米
voxel.filter(*cloud_downsampled);
注意:体素滤波会改变点的精确位置。如果你做的是高精度建图,体素大小不要超过 0.05 米,否则墙角的棱角会被磨平。我曾在一次高精度定位项目中吃过这个亏,后来改成了 0.03 米才满足要求。
4.3 统计滤波:揪出离群点
统计滤波的原理很有意思。它假设每个点的邻域内,点到邻域中心的距离服从高斯分布。如果一个点离它的邻居们太远,那它大概率是个离群点。
具体做法是:对每个点,计算它到 k 个最近邻的平均距离。然后算所有平均距离的均值和标准差。如果某个点的平均距离大于「均值 + 标准差 × 倍数」,就把它标记为离群点并移除。
这里有两个参数需要调:
- k(邻域点数):一般取 10~50。点云越密,k 可以越大。
- std_dev(标准差倍数):一般取 1.0~3.0。倍数越小,过滤越狠。
我曾经在一个果园项目里被这个参数坑过。果园里有很多细枝和树叶,点云本来就稀疏,我设了 std_dev=1.0,结果把很多树枝上的有效点也当成离群点删掉了。后来我把 std_dev 调到 2.5,才保住了那些稀疏但有用的点。
代码实现:
// 统计滤波移除离群点
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_downsampled);
sor.setMeanK(20); // 邻域点数
sor.setStddevMulThresh(2.0); // 标准差倍数
sor.filter(*cloud_clean);
经验之谈:统计滤波对稀疏点云效果一般。如果你的点云本身就很稀疏(比如 16 线雷达远距离数据),建议先用体素滤波把点云密度提上来,再做统计滤波。或者改用半径滤波(RadiusOutlierRemoval),效果更稳定。
4.4 三种滤波的串联实战
在实际项目中,我很少只用一种滤波。通常的流程是:
- 直通滤波:先砍掉远距离和无效区域,减少数据量。
- 体素滤波:降采样,保持形状特征,进一步压缩数据。
- 统计滤波:最后清理离群噪点,得到干净点云。
这个顺序是有讲究的。直通滤波先做,能减少后续滤波的计算量。体素滤波在中间,能把点云密度拉均匀,让统计滤波的邻域统计更准确。统计滤波放最后,因为它的计算量最大,而且对点云密度敏感。
来看一个完整的串联代码:
// 完整预处理流程
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_temp(new pcl::PointCloud<pcl::PointXYZ>);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_clean(new pcl::PointCloud<pcl::PointXYZ>);
// 1. 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(0.0, 3.0);
pass.filter(*cloud_temp);
// 2. 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud_temp);
voxel.setLeafSize(0.1f, 0.1f, 0.1f);
voxel.filter(*cloud_temp);
// 3. 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_temp);
sor.setMeanK(20);
sor.setStddevMulThresh(2.0);
sor.filter(*cloud_clean);
调参建议:不要一次性把所有参数都调好。我习惯先调直通滤波,看效果;再加体素滤波,看降采样后的点云形状是否完整;最后调统计滤波,看离群点是否被清除。每一步都可视化检查,避免参数叠加后出现意外。
4.5 离群点移除的进阶技巧
除了统计滤波,PCL 还提供了几种离群点移除方法。我简单列一下它们的区别:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| StatisticalOutlierRemoval | 基于邻域距离的统计分布 | 均匀密度点云,通用场景 |
| RadiusOutlierRemoval | 半径内点数少于阈值则移除 | 稀疏点云,远距离数据 |
| ConditionalRemoval | 自定义条件(如强度、高度) | 需要按属性过滤的场景 |
我个人在室外项目中更喜欢用 RadiusOutlierRemoval。因为室外点云密度变化大,统计滤波的假设(高斯分布)不一定成立。半径滤波更直接:你设一个半径,半径内点数不够就删掉。简单粗暴,但有效。
代码示例:
// 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> rad;
rad.setInputCloud(cloud_temp);
rad.setRadiusSearch(0.2); // 搜索半径 0.2 米
rad.setMinNeighborsInRadius(5); // 至少 5 个邻居
rad.filter(*cloud_clean);
避坑提醒:半径滤波的半径不能设得太小。如果半径小于点云的平均点间距,那所有点都会被当成离群点删掉。我一般先算一下点云的平均密度,再设半径。比如平均点间距 0.05 米,半径就设 0.15~0.2 米,留 3~4 倍余量。
好了,点云预处理这块就聊到这儿。直通滤波、体素滤波、统计滤波,这三种方法基本能覆盖 90% 的日常需求。记住一个原则:先粗后细,先快后慢。先把数据量降下来,再做精细滤波。这样既保证了效率,又保证了质量。