4. 统计滤波原理:邻域搜索与高斯分布假设、均值与标准差计算、离群点剔除策略
各位同学好,今天我们来聊聊统计滤波。说实话,在点云处理的各种滤波方法里,统计滤波是我个人用得最多的一种。为什么?因为它简单、有效,而且背后有扎实的数学支撑。
你想想看,激光雷达扫出来的点云,总会有那么些“野点”——明明是个平整的墙面,突然飘出来一个孤零零的点。这种离群点对定位算法来说简直是灾难。我曾在一次园区无人车项目中,就因为几个离群点导致ICP匹配失败,车子差点怼上路沿。从那以后,我对统计滤波就格外重视。
4.1 核心思想:每个点都不是孤岛
统计滤波的基本假设其实很朴素:真实世界中的物体表面是连续的,每个点周围都应该有足够多的邻居。如果一个点周围空空荡荡,那它大概率是个噪声点。
具体来说,我们假设每个点到它k个最近邻的距离服从高斯分布。为什么是高斯分布?因为在实际场景中,激光雷达的测量噪声、物体表面的微小起伏,这些因素叠加起来,距离分布往往呈现钟形曲线。当然,这不是绝对的,但工程上够用了。
统计滤波的两步走:
- 对每个点,计算它到k个最近邻的平均距离
- 如果这个平均距离超过全局均值+若干倍标准差,就判定为离群点
4.2 邻域搜索:KD-Tree是标配
要计算每个点的邻居,最直接的办法是暴力搜索——每个点跟其他所有点算距离。但点云动辄几十万个点,O(n²)的复杂度谁也扛不住。
我建议用KD-Tree来做邻域搜索。这是一种空间分割数据结构,能把搜索复杂度降到O(log n)。
KD-Tree的原理不复杂:
- 每次选一个维度(比如x轴),找中位数作为切分点
- 左边放小于中位数的点,右边放大于中位数的点
- 递归切分,直到每个叶子节点包含的点数少于阈值
搜索时,从根节点往下走,利用空间剪枝快速排除不可能的分支。嗯,这里要注意:KD-Tree在高维空间性能会下降,但三维点云完全够用。
下面我用PCL库演示一下KD-Tree的构建和近邻搜索:
#include <pcl/kdtree/kdtree_flann.h>
// 假设cloud是输入点云
pcl::KdTreeFLANN<pcl::PointXYZ> kdtree;
kdtree.setInputCloud(cloud);
// 搜索第0个点的10个最近邻
pcl::PointXYZ searchPoint = cloud->points[0];
std::vector<int> pointIdxNKNSearch(10);
std::vector<float> pointNKNSquaredDistance(10);
if (kdtree.nearestKSearch(searchPoint, 10, pointIdxNKNSearch, pointNKNSquaredDistance) > 0) {
for (size_t i = 0; i < pointIdxNKNSearch.size(); ++i) {
// pointIdxNKNSearch[i] 就是邻居索引
// pointNKNSquaredDistance[i] 是平方距离
}
}
4.3 均值与标准差计算
有了KD-Tree,我们就可以遍历每个点,计算它到k个最近邻的平均距离。假设点云有N个点,我们会得到N个平均距离值:d₁, d₂, ..., dₙ。
接下来算全局均值和标准差:
// 伪代码
float sum = 0.0;
for (int i = 0; i < N; i++) {
sum += distances[i];
}
float mean = sum / N;
float sq_sum = 0.0;
for (int i = 0; i < N; i++) {
sq_sum += (distances[i] - mean) * (distances[i] - mean);
}
float stddev = sqrt(sq_sum / N);
这里有个细节:标准差用总体标准差还是样本标准差?我个人习惯用总体标准差(除以N),因为点云数据量足够大,两者差别微乎其微。
经验之谈:k值的选择很关键。k太小,统计结果不稳定;k太大,计算量上去了,而且可能把边缘点误判为离群点。我一般取k=20~50,具体看点云密度。稀疏点云用20,稠密点云用50。
4.4 离群点剔除策略
有了均值和标准差,离群点判定就简单了:
判定条件:如果某个点的平均邻域距离 > mean + stddev_mult × stddev,则剔除。
这里的stddev_mult是一个可调参数,通常取1.0~3.0。数值越大,滤波越宽松,保留的点越多。
我曾经在一个隧道场景中吃过亏:隧道壁很平整,但激光雷达在远处会产生一些稀疏点。我用了默认的1.0倍标准差,结果把隧道深处的点全滤掉了,导致定位退化。后来我把倍数调到2.5,效果就好了很多。
避坑指南:
- 统计滤波假设点云密度均匀。如果场景中既有密集区又有稀疏区(比如室内墙角vs空旷走廊),建议分区域处理
- 不要对地面点云做统计滤波——地面点天然分布稀疏,容易被误删
- 滤波参数(k和stddev_mult)最好通过实验标定,不要拍脑袋
4.5 完整流程与可视化
下面这张图展示了统计滤波的完整流程:
最后,我用PCL的现成接口演示一下完整调用:
#include <pcl/filters/statistical_outlier_removal.h>
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(30); // k近邻数
sor.setStddevMulThresh(2.0); // 标准差倍数
sor.filter(*cloud_filtered);
// 被剔除的点也可以拿到
sor.setNegative(true);
sor.filter(*cloud_outliers);
你看,核心代码就这么几行。但真正用好它,需要理解背后的原理和参数含义。我建议你在自己的数据集上多试试不同的k值和标准差倍数,感受一下参数变化对结果的影响。
好了,统计滤波的原理就讲到这里。记住一句话:离群点不是敌人,但盲目保留它们一定会坑你。下一节我们会聊体素滤波,那是另一种完全不同的思路。
本章要点回顾:
- 统计滤波基于高斯分布假设,认为每个点的邻域距离服从正态分布
- KD-Tree是高效邻域搜索的标配,复杂度O(log n)
- 离群点判定:平均邻域距离 > 全局均值 + α × 标准差
- 参数k和α需要根据场景调优,没有万能参数
可可精品资料库