4、点云预处理(上):体素滤波降采样、统计滤波去噪、半径滤波去噪

各位同学,咱们今天聊聊点云预处理。说实话,很多刚入行的朋友拿到点云数据,上来就急着做分割、做识别。我当年也犯过这个错——结果呢?数据量太大,算法跑不动;噪声太多,分割结果一塌糊涂。后来我学乖了:预处理做得好,后面事半功倍。

点云预处理说白了就两件事:降采样去噪。降采样是为了减少数据量,去噪是为了提高数据质量。今天咱们先讲三个最常用的方法:体素滤波、统计滤波、半径滤波。

核心思路:原始点云数据往往包含几十万甚至上百万个点,直接处理计算量太大。而且传感器采集时难免引入噪声。预处理就是要在保留几何结构的前提下,把数据量降下来、把噪声去掉。

点云预处理(上) 体素滤波降采样 三维网格体素化 每个体素保留一个重心点 统计滤波去噪 基于邻域距离统计 去除离群稀疏点 半径滤波去噪 固定半径内点数阈值 孤立点剔除 目标:减少数据量 + 去除噪声 → 提高后续处理效率与精度 自动驾驶 | 机器人导航 | 三维重建 | 工业检测

4.1 体素滤波降采样

体素滤波,名字听着高大上,其实原理特别简单。你想象一下,把三维空间切成一个个小立方体,就像把一块豆腐切成骰子块。每个小立方体就是一个体素(Voxel)。

具体怎么做呢?对于每个体素内部的所有点,只保留一个点——通常是这个体素内所有点的重心(质心)。这样一来,原本密密麻麻的点云就变得稀疏了,但整体的几何形状基本不变。

我的经验:体素大小的选择很关键。设得太小,降采样效果不明显;设得太大,会丢失细节。我个人习惯先设一个0.05m(5厘米)试试,然后根据效果调整。在自动驾驶场景中,0.1m到0.2m的体素尺寸比较常见。

来看代码实现,我用的是PCL(Point Cloud Library):

// C++ PCL 体素滤波示例
#include <pcl/filters/voxel_grid.h>

pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud_input);
// 设置体素尺寸:0.1m x 0.1m x 0.1m
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*cloud_filtered);

std::cout << "降采样前: " << cloud_input->size() << " 个点" << std::endl;
std::cout << "降采样后: " << cloud_filtered->size() << " 个点" << std::endl;

运行这段代码,你会发现点数量可能从几十万降到几万。嗯,效果立竿见影。

注意:体素滤波会改变点的精确位置(因为用的是重心)。如果你的应用对点云位置精度要求极高(比如高精度测绘),需要谨慎使用。我曾经在一个桥梁检测项目中,因为体素设得太大,把一条细微的裂缝给"滤"没了……从那以后,我每次都会先可视化检查一下降采样结果。

4.2 统计滤波去噪

统计滤波,说白了就是"看邻居"。对于点云中的每个点,计算它到所有邻居点的平均距离。如果这个平均距离明显大于全局的平均水平,那这个点就很可能是噪声。

为什么会这样?你想想看,正常的物体表面,点与点之间分布比较均匀,距离差不多。但噪声点往往是孤立的,它周围没什么点,所以平均距离会偏大。

具体步骤是这样的:

  1. 对每个点,找到它的k个最近邻(k由你设定)
  2. 计算该点到这k个邻居的平均距离
  3. 计算所有点平均距离的均值和标准差
  4. 设定一个阈值(通常是均值 + n倍标准差),超过阈值的点被判定为噪声

看代码:

// PCL 统计滤波
#include <pcl/filters/statistical_outlier_removal.h>

pcl::StatisticalOutlierRemoval<pcl::PointXYZ> stat_filter;
stat_filter.setInputCloud(cloud_input);
// 设置邻居点个数
stat_filter.setMeanK(50);
// 设置标准差倍数阈值(常用1.0~2.0)
stat_filter.setStddevMulThresh(1.5);

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
stat_filter.filter(*cloud_filtered);

关键参数:

  • setMeanK(k):邻居点数量。k越大,对局部密度的估计越平滑,但计算越慢。我一般设50~100。
  • setStddevMulThresh(n):标准差倍数。n越小,剔除的点越多(更严格)。1.0~2.0是比较常用的范围。

我记得有一次处理激光雷达采集的户外点云,里面有很多飞鸟造成的噪点。用统计滤波,设k=30、阈值1.2,一下就把那些飞鸟点全去掉了,而地面和建筑物的点完好无损。这就是统计滤波的厉害之处——它自适应于点云的局部密度。

4.3 半径滤波去噪

半径滤波的思路更直接:以每个点为中心,画一个半径为r的球。如果球内的邻居点数量少于某个阈值,就把这个点删掉。

说白了就是:你周围连几个邻居都没有,那你八成是个孤立的噪声点,走你!

这个方法特别适合去除那种稀疏的、孤立的离群点。比如传感器偶尔产生的"飞点"——莫名其妙出现在半空中的一个点,周围啥也没有。

// PCL 半径滤波
#include <pcl/filters/radius_outlier_removal.h>

pcl::RadiusOutlierRemoval<pcl::PointXYZ> radius_filter;
radius_filter.setInputCloud(cloud_input);
// 设置搜索半径(单位:米)
radius_filter.setRadiusSearch(0.1);
// 设置半径内最少点数
radius_filter.setMinNeighborsInRadius(5);

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
radius_filter.filter(*cloud_filtered);

避坑指南:我曾经在一个室内点云项目中,半径设了0.05m,最少点数设了10。结果发现墙角处的点被大量误删——因为墙角处点云密度本来就低。后来我把半径调到0.08m,最少点数降到5,效果就好多了。所以参数一定要根据实际点云密度来调,别死记硬背。

4.4 三种方法怎么选?

我整理了一个对比表,方便你快速决策:

方法 主要用途 优点 缺点 适用场景
体素滤波 降采样 速度快,均匀降采样 改变点位置,可能丢失细节 数据量过大时的首选
统计滤波 去噪 自适应密度,效果好 计算稍慢,需调参 均匀噪声、离群点
半径滤波 去噪 原理简单,参数直观 对密度变化敏感 稀疏孤立噪声

实际项目中,我通常的组合拳是:先体素滤波降采样,再统计滤波去噪。体素滤波把数据量降下来,后面的统计滤波跑得更快。如果发现还有零星的孤立噪点,再补一个半径滤波收尾。

总结一下今天的重点:

  • 体素滤波:切格子,留重心,降采样
  • 统计滤波:看邻居距离,剔除离群点
  • 半径滤波:数邻居个数,剔除孤立点
  • 预处理顺序:先降采样,再去噪

好了,今天就到这儿。这三种方法你回去一定要动手试试,参数调一调,看看效果变化。只有亲手调过参数,才能真正理解它们的脾气秉性。

专注资料整理