3. 点云滤波与降噪

点云数据这东西,说白了就是一堆三维坐标点。但现实世界没那么干净——激光扫描仪会打到玻璃上产生飞点,深度相机在边缘处会冒出噪点,甚至风吹起的灰尘都能被记录下来。我刚开始做点云处理那会儿,拿到的数据经常是「满天星」,不处理根本没法用。

所以滤波和降噪,是点云处理的第一步,也是最重要的一步。今天咱们就聊三种最常用的方法:统计滤波、半径滤波和体素滤波。

核心思路:滤波不是「删数据」,而是「保留可信的数据」。三种方法各有侧重,实际项目中往往组合使用。

点云滤波与降噪知识体系 原始点云 统计滤波 (SOR) 半径滤波 (ROR) 体素滤波 (Voxel) 原理:统计邻域距离分布 剔除偏离均值 ±kσ 的点 适合:稀疏离群点 原理:邻域内点数阈值 半径内点数 < 阈值则剔除 适合:孤立小簇 原理:体素网格重心采样 降采样 + 平滑噪声 适合:大规模数据精简

3.1 统计滤波(Statistical Outlier Removal)

统计滤波的思路很直观:每个点周围应该有一定数量的邻居。如果一个点离它的邻居们太远,那它大概率是个噪点。

具体做法是这样的:

  1. 对每个点,计算它到所有 k 个最近邻的平均距离。
  2. 所有点的平均距离会形成一个分布(近似高斯分布)。
  3. 设定一个标准差倍数 threshold(比如 1.0 或 2.0)。
  4. 平均距离超过 μ + threshold × σ 的点,被判定为离群点,剔除。

我在项目中遇到过这样的情况:用地面激光扫描仪采集古建筑数据时,总有一些飘在空中的点——那是飞虫或者灰尘。统计滤波一上,干净利落。但要注意,如果场景本身就有稀疏结构(比如电线杆),别把人家正常点也给滤掉了。

参数调优建议:

  • k(邻域点数):一般取 20~50。点云越密,k 可以越大。
  • std_mult(标准差倍数):1.0 比较激进,2.0 比较保守。我习惯先试 1.5。
// C++ PCL 示例:统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(30);          // 邻域点数 k = 30
sor.setStddevMulThresh(1.5); // 标准差倍数 1.5
sor.filter(*cloud_filtered);

注意:统计滤波假设点云密度相对均匀。如果点云密度变化极大(比如近处密集、远处稀疏),建议先分区域处理,或者改用半径滤波。

3.2 半径滤波(Radius Outlier Removal)

半径滤波的逻辑更简单粗暴:以每个点为中心,画一个半径为 r 的球。球里面的邻居数如果少于某个阈值 min_pts,就把这个点干掉。

你想想看,一个孤立噪点周围通常没几个点,而正常点周围应该有不少邻居。这就是半径滤波的直觉。

特性 统计滤波 半径滤波
判断依据 距离分布的统计量 固定半径内的点数
对密度变化 敏感 不敏感(半径固定)
参数数量 2 个(k, std_mult) 2 个(r, min_pts)
典型场景 均匀密度 + 稀疏离群点 密度不均 + 孤立小簇

我曾经在一个工厂自动化项目里用过半径滤波。当时点云来自多个角度拼接,边缘处总有一些「飘」出来的点。统计滤波效果一般,因为边缘点密度本身就不均匀。换成半径滤波,设半径 0.05 米、最少 3 个点,问题就解决了。

// C++ PCL 示例:半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.05);   // 搜索半径 0.05 米
ror.setMinNeighborsInRadius(3); // 最少 3 个邻居
ror.filter(*cloud_filtered);

经验之谈:半径滤波的参数和点云的单位有关。如果你的点云单位是毫米,半径设 50 就相当于 5 厘米。记得先确认单位!

3.3 体素滤波(Voxel DownSample)

体素滤波和前两个不太一样——它不只是去噪,更主要的是降采样。但为什么把它放在滤波章节?因为体素滤波在降采样的同时,天然有平滑噪声的效果。

原理很简单:

  1. 把三维空间划分成一个个小立方体(体素),边长由你指定。
  2. 每个体素内,计算所有点的重心(或中心),用这个点代表整个体素。
  3. 原始点云被大幅精简,同时局部噪声被平均掉了。

我记得有一次处理一个 2000 万点的户外场景,直接做配准,内存直接爆了。体素滤波设成 0.1 米的体素大小,点云降到 50 万点,配准速度提升了 40 倍,精度几乎没损失。

体素大小怎么选?

  • 0.01 米(1cm):保留精细结构,适合室内场景。
  • 0.05 米(5cm):平衡精度与效率,适合大多数场景。
  • 0.1 米(10cm):快速预览或粗配准,适合大场景。
// C++ PCL 示例:体素滤波
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud);
vg.setLeafSize(0.05f, 0.05f, 0.05f); // 体素边长 5cm
vg.filter(*cloud_filtered);

注意:体素滤波会丢失细节。如果你的应用需要精确的几何特征(比如边缘检测),体素大小别设太大。另外,体素滤波不保证保留原始点的精确位置——它生成的是重心点。

3.4 三种滤波的对比与组合策略

实际项目中,很少只用一种滤波。我个人的习惯流程是这样的:

  1. 先体素滤波:把数据量降下来,同时初步平滑噪声。
  2. 再统计滤波或半径滤波:根据场景特点,剔除离群点。
  3. 最后再检查:可视化看一眼,确认没有误删关键点。
场景 推荐组合 说明
室内小物体扫描 体素(0.01) + 统计滤波(k=20, std=1.5) 保留细节,剔除飞点
户外大场景 体素(0.1) + 半径滤波(r=0.2, min=5) 大幅降采样,剔除孤立点
机器人导航 半径滤波(r=0.05, min=3) 实时性要求高,不降采样
高精度配准 统计滤波(k=50, std=2.0) 保守滤波,保留更多点

嗯,这里要注意一点:滤波不是越狠越好。我见过有人把体素设到 0.5 米,结果一堵墙变成几个点,配准直接失败。滤波的目的是「去伪存真」,不是「削足适履」。

一句话总结:统计滤波看距离分布,半径滤波看邻居数量,体素滤波看空间网格。三者各有所长,组合使用效果最佳。