3. 点云滤波与降噪:体素滤波、统计滤波、半径滤波

激光雷达扫出来的点云,说白了就是一堆带噪声的离散点。你想想看,传感器本身有测量误差,环境里还有飞溅的灰尘、飘落的树叶、甚至飞过的昆虫。这些乱七八糟的点如果不处理,后面的配准、分割、识别全都会跑偏。

我刚开始做点云处理时,就吃过这个亏。有一次在矿区做建图,点云里全是扬尘造成的悬浮点,结果地图建出来像一团棉花糖。嗯,从那以后我学乖了——滤波是点云处理的第一步,也是最关键的一步。

这一章我们聊三种最常用的滤波方法:体素滤波、统计滤波、半径滤波。它们解决的问题不同,适用场景也不同。我会把原理、参数调优、以及我踩过的坑都讲清楚。

点云滤波与降噪知识体系 点云滤波与降噪 体素滤波(Voxel Grid) 统计滤波(SOR) 半径滤波(ROR) 核心:体素内取重心/中心 作用:降采样 + 去噪 关键参数:体素大小 核心:邻域距离统计分布 作用:去除离群孤立点 关键参数:K近邻 + 标准差倍数 核心:半径内邻点计数 作用:去除稀疏孤立点 关键参数:搜索半径 + 阈值点数 实际工程中:体素滤波打底 → 统计/半径滤波精修

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和倍数)
半径滤波 邻域点数计数 去稀疏孤立点 低 中等(半径和阈值)

实际工程中,我很少只用一种滤波。典型的流水线是:

  1. 体素滤波打底:把点云密度降到可控范围,同时去掉体素内的微小噪声。
  2. 统计滤波或半径滤波精修:根据场景选一个。场景均匀用半径滤波,场景复杂用统计滤波。
  3. 可选:再做一次体素滤波:如果第二步引入了不均匀的密度,可以用小体素再平滑一次。

一句话总结:体素滤波管「量」和「噪」,统计滤波管「孤」,半径滤波管「稀」。三兄弟配合好,点云干净又利索。

嗯,这一章的内容就这些。三种滤波方法都不复杂,但参数调优需要经验。我建议你拿自己的数据跑一遍,看看不同参数下点云的变化。只有亲手调过参数,才能真正理解每个参数的意义。