4、统计滤波原理:邻域搜索、高斯分布假设、标准差阈值设定

统计滤波,说白了就是帮我们剔除点云中的「离群点」。

我在项目里遇到过好几次——激光扫描时,偶尔会蹦出几个莫名其妙的点,离主体老远。这些点不处理掉,后面的配准、建图全得崩。嗯,统计滤波就是干这个的。

4.1 核心思想:每个点都有「邻居」

统计滤波的基本假设很简单:一个正常的点,周围应该有不少邻居。如果一个点孤零零的,周围没几个点,那它大概率是噪声。

你想想看,激光打在墙面上,会反射回来密密麻麻的点。但如果是空气中的灰尘、飞虫,或者传感器本身的误触发,产生的点就是稀疏的、孤立的。

统计滤波的两步走:

  1. 对每个点,计算它到所有邻居点的平均距离
  2. 如果这个平均距离太大,就判定为离群点,删掉

4.2 邻域搜索:怎么找邻居?

找邻居,最直接的办法就是K近邻搜索。我习惯用K=50,当然这个值可以调。

具体做法:

  • 对点云中的每个点Pi,用KD-Tree找到它最近的K个点
  • 计算Pi到这K个点的欧氏距离
  • 取这些距离的平均值,记为di

为什么要用KD-Tree?因为暴力搜索太慢了。点云动辄几十万个点,每个点都去遍历所有其他点,复杂度是O(n²),根本跑不动。KD-Tree能把搜索降到O(n log n),这才是工程可用的方案。

我的经验:K值别设太大。K=20~50通常够用。K太大,会把远处的点也算进来,反而模糊了局部特征。K太小,统计结果不稳定。

4.3 高斯分布假设:距离的统计规律

好,现在我们有了每个点的平均距离di。接下来做什么?

统计滤波假设:所有点的平均距离di服从高斯分布。

为什么会这样?因为正常点云中,大部分点的局部密度是均匀的。墙面上的点,每个点到邻居的距离都差不多。只有离群点,它的di会明显偏大。

于是我们计算整个点云的:

  • 均值 μ:所有di的平均值
  • 标准差 σ:所有di的标准差

有了μ和σ,我们就知道「正常范围」在哪里了。

数学表达:

μ = (1/N) * Σ dᵢ
σ = sqrt((1/N) * Σ (dᵢ - μ)²)

4.4 标准差阈值设定:砍掉尾巴

接下来就是关键一步——设定阈值。

统计滤波的判定规则是:

如果 dᵢ > μ + std_mult * σ,则判定为离群点

这里的std_mult就是标准差倍数,也叫阈值系数。我常用的值是1.0到2.0之间。

std_mult 取值 效果 适用场景
0.5 非常严格,只保留最密集的点 高精度建模,噪声较多
1.0 适中,剔除明显离群点 大多数场景,我推荐这个
2.0 宽松,只剔除极端离群点 点云稀疏,或需要保留边缘
3.0 非常宽松 几乎不滤波,仅去除飞点

注意:阈值设得太小,会把正常的边缘点也删掉。边缘点的邻居本来就少,平均距离自然偏大。我曾经在扫描建筑物墙角时吃过这个亏——墙角点全被滤掉了,模型缺了一大块。

4.5 完整流程:一张图看懂

下面我用一张流程图把整个逻辑串起来:

统计滤波完整流程 输入原始点云 构建KD-Tree 为每个点搜索K个最近邻 计算每个点的平均距离 dᵢ 到K个邻居的欧氏距离取均值 计算全局均值 μ 和标准差 σ 假设 dᵢ 服从高斯分布 dᵢ > μ + std_mult·σ ? 是 标记为离群点 否 保留为内点 输出滤波后点云

4.6 PCL代码实现

在PCL里,统计滤波已经封装好了。我直接贴一段我常用的代码:

#include <pcl/filters/statistical_outlier_removal.h>

// 输入点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// ... 加载点云数据 ...

// 创建滤波器对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);

// 设置参数
sor.setMeanK(50);          // K近邻数量
sor.setStddevMulThresh(1.0); // 标准差倍数阈值

// 执行滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_filtered);

std::cout << "滤波前点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;

我的习惯:先用std_mult=1.0试跑一次,看看滤掉了多少点。如果滤掉超过5%,我会调大到1.5。如果滤掉不到1%,我会调小到0.8。这个比例没有绝对标准,取决于你的点云质量和应用需求。

4.7 避坑指南

我曾经在室外大场景点云上吃过亏。场景里有树、有建筑、有地面,密度差异很大。树冠部分的点很稀疏,建筑墙面很密集。如果用全局的μ和σ,树冠的点全被当成离群点删掉了。

怎么办?有两个思路:

  • 分块滤波:把点云切成小块,每块单独计算μ和σ
  • 自适应阈值:根据局部密度动态调整std_mult

不过这些是进阶内容了。对于大多数室内场景、机器人SLAM场景,全局统计滤波已经够用。

4.8 小结

统计滤波的核心就三件事:

  • 邻域搜索——用KD-Tree找K个邻居
  • 高斯分布假设——认为平均距离服从正态分布
  • 阈值判定——超过μ + std_mult·σ的就干掉

简单、高效、实用。这就是我为什么在项目中一直用它。

可可精品资料库