4、统计滤波原理:邻域搜索、高斯分布假设、标准差阈值设定
统计滤波,说白了就是帮我们剔除点云中的「离群点」。
我在项目里遇到过好几次——激光扫描时,偶尔会蹦出几个莫名其妙的点,离主体老远。这些点不处理掉,后面的配准、建图全得崩。嗯,统计滤波就是干这个的。
4.1 核心思想:每个点都有「邻居」
统计滤波的基本假设很简单:一个正常的点,周围应该有不少邻居。如果一个点孤零零的,周围没几个点,那它大概率是噪声。
你想想看,激光打在墙面上,会反射回来密密麻麻的点。但如果是空气中的灰尘、飞虫,或者传感器本身的误触发,产生的点就是稀疏的、孤立的。
统计滤波的两步走:
- 对每个点,计算它到所有邻居点的平均距离
- 如果这个平均距离太大,就判定为离群点,删掉
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 完整流程:一张图看懂
下面我用一张流程图把整个逻辑串起来:
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·σ的就干掉
简单、高效、实用。这就是我为什么在项目中一直用它。