4. 统计滤波原理:邻域搜索与高斯分布假设、均值与标准差计算、离群点剔除策略

各位同学好,今天我们来聊聊统计滤波。说实话,在点云处理的各种滤波方法里,统计滤波是我个人用得最多的一种。为什么?因为它简单、有效,而且背后有扎实的数学支撑。

你想想看,激光雷达扫出来的点云,总会有那么些“野点”——明明是个平整的墙面,突然飘出来一个孤零零的点。这种离群点对定位算法来说简直是灾难。我曾在一次园区无人车项目中,就因为几个离群点导致ICP匹配失败,车子差点怼上路沿。从那以后,我对统计滤波就格外重视。

4.1 核心思想:每个点都不是孤岛

统计滤波的基本假设其实很朴素:真实世界中的物体表面是连续的,每个点周围都应该有足够多的邻居。如果一个点周围空空荡荡,那它大概率是个噪声点。

具体来说,我们假设每个点到它k个最近邻的距离服从高斯分布。为什么是高斯分布?因为在实际场景中,激光雷达的测量噪声、物体表面的微小起伏,这些因素叠加起来,距离分布往往呈现钟形曲线。当然,这不是绝对的,但工程上够用了。

统计滤波的两步走:

  1. 对每个点,计算它到k个最近邻的平均距离
  2. 如果这个平均距离超过全局均值+若干倍标准差,就判定为离群点

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 完整流程与可视化

下面这张图展示了统计滤波的完整流程:

输入原始点云 构建KD-Tree 空间索引建立 k近邻搜索 计算平均距离 统计全局 均值与标准差 离群点判定 d_i > μ + α·σ ? 剔除(离群点) 保留(内点) 输出滤波后点云

最后,我用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和α需要根据场景调优,没有万能参数

可可精品资料库