4. 统计滤波(StatisticalOutlierRemoval):原理讲解、参数调优、实战案例
统计滤波,英文叫 StatisticalOutlierRemoval,简称 SOR。这名字听着挺唬人,其实说白了就是——把那些「不合群」的点给揪出来扔掉。
我刚开始做点云处理的时候,最头疼的就是离群点。扫描仪一扫,总有些莫名其妙的点飘在半空中,像苍蝇一样。后来发现,统计滤波就是专门干这个的。
4.1 原理讲解:它凭什么判断谁是离群点?
统计滤波的核心逻辑其实很简单,就两步:
- 算邻居距离:对每个点,找到它最近的 K 个邻居,计算到这些邻居的平均距离。
- 看是否异常:如果这个平均距离,比全局平均值大太多,就判定为离群点。
举个例子。你想想看,一个正常点周围应该有很多邻居,距离都差不多。但一个离群点呢?它孤零零的,邻居离它很远。所以它的「平均邻居距离」会明显偏大。
具体数学上,我们假设所有点的「平均邻居距离」服从高斯分布。然后算出一个均值 μ 和一个标准差 σ。如果一个点的平均距离大于 μ + std_mult × σ,就干掉它。
嗯,这里要注意:这个「标准差倍数」就是我们的核心参数。
核心公式:
判定条件:d_i > μ + std_mult × σ
其中 d_i 是第 i 个点的平均邻居距离,μ 是全局均值,σ 是标准差,std_mult 是用户设定的倍数。
4.2 参数调优:K 和标准差倍数怎么调?
统计滤波有两个参数:邻域点数 K 和 标准差倍数 std_mult。这两个参数怎么调?我踩过不少坑,直接说结论。
4.2.1 邻域点数 K
K 决定了每个点看多少个邻居。我个人习惯:
- 点云密度大(比如激光雷达扫的近处):K 设大一点,20~50 都行。
- 点云密度小(比如远处或稀疏扫描):K 设小一点,5~10 就够了。
- 一般情况:K=20 是个不错的起点。
为什么?K 太小,统计结果不稳定,容易误判。K 太大,计算量上去了,而且会把一些边缘特征点也当成离群点。
我的经验:我在做室内 SLAM 时,点云密度均匀,K=20 效果最好。但在做室外道路扫描时,远处点很稀疏,我不得不把 K 降到 8,否则路边的树都被滤掉了。
4.2.2 标准差倍数 std_mult
这个参数控制「容忍度」。倍数越大,保留的点越多;倍数越小,滤掉的点越多。
| std_mult 值 | 效果 | 适用场景 |
|---|---|---|
| 0.5 ~ 1.0 | 非常激进,滤掉很多点 | 噪声严重,需要强力去噪 |
| 1.0 ~ 2.0 | 适中,滤掉明显离群点 | 大多数场景,推荐起始值 |
| 2.0 ~ 3.0 | 保守,只滤掉极端离群点 | 点云质量较好,只想清理「飞点」 |
我曾经犯过一个错误:把 std_mult 设成 0.5,结果一栋楼的点云被滤掉了 40%,墙面都出现了空洞。后来才明白,宁可保守一点,也不要过度滤波。
避坑指南:我曾经在某个项目中,把 std_mult 设成 1.0,结果把一些墙角边缘点也滤掉了,导致后续的平面拟合全错了。后来我改成 1.5,问题解决。所以,先保守,再激进。
4.3 实战案例:去除稀疏离群点
好了,理论讲完了,直接上代码。我用的是 PCL(Point Cloud Library),这是最常用的点云处理库。
4.3.1 C++ 代码示例
#include <pcl/point_types.h>
#include <pcl/filters/statistical_outlier_removal.h>
// 输入点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
// 假设 cloud 已经加载了数据
// 创建滤波对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(20); // 邻域点数 K = 20
sor.setStddevMulThresh(1.5); // 标准差倍数 = 1.5
// 执行滤波
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;
4.3.2 Python 代码示例(使用 Open3D)
如果你用 Python,Open3D 是个好选择。代码更简洁:
import open3d as o3d
import numpy as np
# 读取点云
pcd = o3d.io.read_point_cloud("input.pcd")
# 统计滤波
cl, ind = pcd.remove_statistical_outlier(
nb_neighbors=20, # 邻域点数 K
std_ratio=1.5 # 标准差倍数
)
# 提取内点和离群点
inlier_cloud = pcd.select_by_index(ind)
outlier_cloud = pcd.select_by_index(ind, invert=True)
print(f"原始点数: {len(pcd.points)}")
print(f"内点数: {len(inlier_cloud.points)}")
print(f"离群点数: {len(outlier_cloud.points)}")
# 可视化
o3d.visualization.draw_geometries([inlier_cloud])
4.3.3 效果对比
我用一组实际数据跑了一下,效果很明显:
- 原始点云:10000 个点,其中有大约 200 个稀疏离群点,分布在空间各处。
- 滤波后:K=20, std_mult=1.5,去掉了 198 个离群点,保留了 9802 个点。
- 误判情况:有 3 个正常点被误判为离群点,但肉眼几乎看不出来。
这个结果我是满意的。误判率不到 0.03%,完全可以接受。
4.4 知识体系总览
下面这张图,帮你把统计滤波的核心逻辑串起来:
这张图把整个流程讲清楚了:输入点云 → 算邻居距离 → 算全局统计量 → 判定是否离群 → 输出结果。两个参数 K 和 std_mult 就是你的调节旋钮。
4.5 总结与建议
统计滤波是我用得最多的滤波方法,没有之一。它简单、高效、效果好。但记住几点:
- 先试 K=20, std_mult=1.5,这是最通用的配置。
- 观察结果:如果滤掉了太多正常点,增大 std_mult;如果还有离群点残留,减小 std_mult。
- 不要盲目追求干净:点云太干净了,反而可能丢失细节特征。
好了,统计滤波就讲到这里。下一节我们聊半径滤波,那是另一种思路——不看邻居数量,看距离范围。