3. 点云滤波与降噪:体素滤波、统计滤波、半径滤波
激光雷达扫出来的点云,说白了就是一堆带噪声的离散点。你想想看,传感器本身有测量误差,环境里还有飞溅的灰尘、飘落的树叶、甚至飞过的昆虫。这些乱七八糟的点如果不处理,后面的配准、分割、识别全都会跑偏。
我刚开始做点云处理时,就吃过这个亏。有一次在矿区做建图,点云里全是扬尘造成的悬浮点,结果地图建出来像一团棉花糖。嗯,从那以后我学乖了——滤波是点云处理的第一步,也是最关键的一步。
这一章我们聊三种最常用的滤波方法:体素滤波、统计滤波、半径滤波。它们解决的问题不同,适用场景也不同。我会把原理、参数调优、以及我踩过的坑都讲清楚。
3.1 体素滤波(Voxel Grid)
体素滤波的原理其实很简单。你把三维空间切成一个个小立方体,每个立方体就是一个体素。然后每个体素里只保留一个点——要么是体素内所有点的重心,要么是体素的中心点。
这样做的好处有两个:一是大幅降低点云密度,二是平滑掉局部噪声。我个人的习惯是,拿到一帧原始点云,第一件事就是做体素滤波。64线激光雷达一帧可能有十几万个点,体素滤波后降到一两万个,后面的处理速度能快一个数量级。
核心要点:
- 体素大小:这是唯一的关键参数。设大了点云变稀疏,细节丢失;设小了降采样效果不明显。一般从0.05m开始试,根据场景调整。
- 重心 vs 中心:取重心更精确,但计算量大一点;取中心速度快,但会引入微小偏移。我一般用重心。
- 适用场景:所有点云预处理的第一步,尤其适合大规模点云。
代码实现也很直接。PCL里已经封装好了,几行就能搞定:
// C++ PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长0.1米
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*filtered_cloud);
我的经验:体素大小不要设成固定值。室内场景0.05m够用,室外开阔场景0.2m也行。我一般在配置文件中做成可调参数,上线前用几帧数据扫一遍,选一个折中值。
3.2 统计滤波(Statistical Outlier Removal)
统计滤波的思路是:离群点周围的点很少,正常点周围的点很多。它计算每个点到它K个最近邻的平均距离,然后看这个距离是否符合整体分布。如果某个点的平均距离偏离均值太多(超过n倍标准差),就把它干掉。
为什么会这样?你想想看,一个正常的墙面点,周围密密麻麻全是点。但一个飞溅的噪声点,周围可能空荡荡的,离它最近的几个点都在老远的地方。统计滤波就是利用这个差异来识别噪声。
我记得有一次做高速公路的点云处理,路面上有大量轮胎带起来的碎石点。体素滤波去不掉这些点,因为它们本身是真实存在的物体。但统计滤波一跑,这些稀疏的碎石点全被识别出来了——因为它们周围的点密度远低于路面。
避坑指南:我曾经把标准差倍数设得太小(比如0.5),结果把墙角的点也误删了,导致建图出现空洞。后来我总结了一个经验:先设大一点(比如3.0),看效果再逐步调小。另外K值一般取20-50,太小了统计不稳定,太大了计算量暴涨。
参数对比表:
| 参数 | 推荐范围 | 偏小的影响 | 偏大的影响 |
|---|---|---|---|
| K(近邻数) | 20 - 50 | 统计不稳定,误删多 | 计算量大,边缘点被删 |
| StdDevMul(标准差倍数) | 1.0 - 3.0 | 保留噪声多 | 误删正常点 |
// C++ PCL 统计滤波示例
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(30); // 每个点取30个近邻
sor.setStddevMulThresh(2.0); // 2倍标准差
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*filtered_cloud);
3.3 半径滤波(Radius Outlier Removal)
半径滤波的逻辑更直白:以每个点为中心画一个球,球里点数少于阈值就删掉。说白了就是「你周围朋友太少,说明你不合群,走你」。
它和统计滤波的区别在哪?统计滤波看的是距离分布,半径滤波看的是数量。统计滤波能识别出那些虽然周围有点但距离偏大的点,半径滤波只管「球里有没有足够多的点」。
我个人习惯在以下场景用半径滤波:
- 点云密度比较均匀的场景(比如室内)
- 需要保留边缘细节的时候(半径滤波对边缘的误删比统计滤波少)
- 快速过滤明显离群点(计算量比统计滤波小)
// C++ PCL 半径滤波示例
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.2); // 搜索半径0.2米
ror.setMinNeighborsInRadius(5); // 至少5个邻点
pcl::PointCloud<pcl::PointXYZ>::Ptr filtered_cloud(new pcl::PointCloud<pcl::PointXYZ>);
ror.filter(*filtered_cloud);
实战技巧:半径滤波的参数和点云密度强相关。如果激光雷达是32线的,半径可以设大一点(0.3-0.5m);如果是64线或128线,密度高,半径可以设小一点(0.1-0.2m)。我一般先统计一下点云的平均点间距,然后设半径为平均间距的3-5倍。
3.4 三种滤波的对比与选型
三种滤波各有各的脾气。我整理了一个对比表,方便你快速选型:
| 滤波方法 | 核心思想 | 主要用途 | 计算开销 | 参数敏感性 |
|---|---|---|---|---|
| 体素滤波 | 体素内降采样 | 降密度、去噪 | 低 | 中等(体素大小) |
| 统计滤波 | 距离统计分布 | 去离群点 | 中 | 高(K和倍数) |
| 半径滤波 | 邻域点数计数 | 去稀疏孤立点 | 低 | 中等(半径和阈值) |
实际工程中,我很少只用一种滤波。典型的流水线是:
- 体素滤波打底:把点云密度降到可控范围,同时去掉体素内的微小噪声。
- 统计滤波或半径滤波精修:根据场景选一个。场景均匀用半径滤波,场景复杂用统计滤波。
- 可选:再做一次体素滤波:如果第二步引入了不均匀的密度,可以用小体素再平滑一次。
一句话总结:体素滤波管「量」和「噪」,统计滤波管「孤」,半径滤波管「稀」。三兄弟配合好,点云干净又利索。
嗯,这一章的内容就这些。三种滤波方法都不复杂,但参数调优需要经验。我建议你拿自己的数据跑一遍,看看不同参数下点云的变化。只有亲手调过参数,才能真正理解每个参数的意义。