4. 统计滤波(Statistical Outlier Removal):离群点检测原理、均值与标准差计算、PCL中的SOR实现
各位同学,欢迎来到《点云滤波算法深度拆解》的第四讲。
今天我们来聊聊一个非常实用、也是我项目里用得最多的滤波方法——统计滤波(Statistical Outlier Removal,简称SOR)。
你想想看,激光雷达扫出来的点云,是不是经常有一些“飘”在空中的孤立点?或者离主体目标很远、明显是噪声的点?这些点我们称之为离群点。它们是怎么来的呢?可能是传感器本身的测量误差,也可能是环境中的灰尘、飞溅物,甚至是被风吹动的树叶。如果不处理掉,后续的配准、分割、识别都会受影响。
那怎么把这些“坏点”揪出来呢?统计滤波就是干这个的。
核心思想:离群点往往“不合群”——它们与周围邻居的距离,明显大于正常点之间的平均距离。
4.1 离群点检测原理:从“社交距离”说起
我习惯用一个比喻来理解统计滤波:想象你在一个拥挤的派对上。
- 正常点:大家三三两两聚在一起聊天,人与人之间的距离比较均匀,不会太远也不会太近。
- 离群点:有一个人独自站在角落里,离所有人都很远。这个人就是“离群点”。
统计滤波的原理,就是计算每个点与其k个最近邻居的平均距离。然后,我们看看这个平均距离是否“异常”。如果它比全局平均距离大很多,那它很可能就是离群点。
为什么会这样?因为正常点周围总有其他点,它们的邻居距离分布是相对集中的。而离群点周围空空荡荡,邻居距离自然就大。
我个人经验:我在做自动驾驶路侧激光雷达数据处理时,经常遇到一些由于多路径反射产生的“鬼影点”。这些点位置很诡异,但用统计滤波一滤,基本都能干掉。SOR对稀疏的、孤立的噪声点效果特别好。
4.2 均值与标准差计算:数学其实很简单
好了,原理讲完了,我们来看看具体怎么算。说白了,就是两步走:
第一步:计算每个点的邻居平均距离
对于点云中的每一个点 \( p_i \),我们找到它的k个最近邻居(k是用户设定的参数)。然后计算 \( p_i \) 到这k个邻居的欧氏距离,再取平均值,记为 \( d_i \)。
公式很简单:
d_i = (1/k) * Σ (distance(p_i, neighbor_j)) for j = 1 to k
第二步:计算全局均值和标准差,设定阈值
现在,我们有了所有点的 \( d_i \) 值。接下来,计算这些 \( d_i \) 的全局均值 \( \mu \) 和标准差 \( \sigma \)。
公式:
μ = (1/N) * Σ d_i
σ = sqrt( (1/N) * Σ (d_i - μ)^2 )
其中N是点云中点的总数。
然后,我们设定一个阈值:
threshold = μ + std_mult * σ
这里的 std_mult 是一个倍数,通常取1.0到3.0之间。如果某个点的 \( d_i \) 大于这个阈值,我们就把它标记为离群点,移除掉。
关键点: std_mult 越大,滤波越宽松,保留的点越多;std_mult 越小,滤波越严格,移除的点越多。我一般从1.5开始试,效果不好再调。
4.3 PCL中的SOR实现:代码实战
理论说完了,咱们直接上代码。PCL(Point Cloud Library)里已经把统计滤波封装好了,用起来非常方便。
我记得我第一次用PCL的SOR时,还犯了个低级错误——忘了设置搜索半径。结果滤波完点云全没了,哈哈。后来才意识到,setMeanK 和 setStddevMulThresh 这两个参数必须配好。
下面是一个完整的示例代码:
#include <pcl/point_types.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/io/pcd_io.h>
int main()
{
// 1. 加载点云
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
pcl::io::loadPCDFile("input_cloud.pcd", *cloud);
std::cout << "原始点云点数: " << cloud->size() << std::endl;
// 2. 创建统计滤波对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
// 3. 设置参数
sor.setMeanK(50); // k近邻搜索的邻居数量,我常用50
sor.setStddevMulThresh(1.5); // 标准差倍数,1.5是比较常用的值
// 4. 执行滤波
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_filtered);
std::cout << "滤波后点云点数: " << cloud_filtered->size() << std::endl;
// 5. 保存结果
pcl::io::savePCDFile("output_filtered.pcd", *cloud_filtered);
// 6. (可选)获取被移除的离群点
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_outliers(new pcl::PointCloud<pcl::PointXYZ>);
sor.setNegative(true); // 设置为true,则提取被移除的点
sor.filter(*cloud_outliers);
std::cout << "移除的离群点数: " << cloud_outliers->size() << std::endl;
return 0;
}
参数调优建议
| 参数 | 作用 | 我的建议 |
|---|---|---|
setMeanK |
邻居数量k | 点云密度大时取大值(如100),密度小时取小值(如20)。我一般从50开始试。 |
setStddevMulThresh |
标准差倍数 | 噪声严重时取小值(如1.0),噪声轻微时取大值(如2.5)。1.5是个不错的起点。 |
避坑指南:我曾经在一个项目中,点云密度极不均匀——近处很密,远处很稀。直接用全局的均值和标准差,结果远处的点全被当成离群点删掉了。后来我改用体素滤波降采样先让密度均匀化,再用SOR,效果就好了很多。所以,SOR对点云密度均匀性有一定要求,使用时要注意。
4.4 知识体系图:统计滤波的核心逻辑
下面我用一张SVG图来总结统计滤波的完整流程,方便你理解整个逻辑链条。
4.5 总结与个人心得
统计滤波,说白了就是利用“距离的统计分布”来识别异常点。它简单、高效、可解释性强,是点云预处理中不可或缺的一环。
我在实际项目中,通常会把SOR放在体素滤波之后,因为体素滤波会让点云密度更均匀,SOR的效果会更好。另外,如果你处理的点云有大量离群点,可以先用SOR粗滤一遍,再用半径滤波(Radius Outlier Removal)精调,两者配合效果更佳。
嗯,关于统计滤波,今天就讲到这里。记住两个关键参数:setMeanK 和 setStddevMulThresh,多试几次,你就能找到最适合你数据的组合。
一个小技巧:如果你不确定参数怎么设,可以先可视化一下点云中每个点的邻居平均距离分布直方图。如果分布是单峰且对称的,说明点云质量不错;如果出现长尾,那尾巴上的点就是潜在的离群点。这个直方图能帮你直观地选择阈值。