3. 点云滤波与降噪:统计滤波、半径滤波与体素滤波
点云数据这东西,说白了就是一堆三维坐标点。但现实世界没那么干净——激光扫描仪会打到玻璃上产生飞点,深度相机在边缘处会冒出噪点,甚至风吹起的灰尘都能被记录下来。我刚开始做点云处理那会儿,拿到的数据经常是「满天星」,不处理根本没法用。
所以滤波和降噪,是点云处理的第一步,也是最重要的一步。今天咱们就聊三种最常用的方法:统计滤波、半径滤波和体素滤波。
核心思路:滤波不是「删数据」,而是「保留可信的数据」。三种方法各有侧重,实际项目中往往组合使用。
3.1 统计滤波(Statistical Outlier Removal)
统计滤波的思路很直观:每个点周围应该有一定数量的邻居。如果一个点离它的邻居们太远,那它大概率是个噪点。
具体做法是这样的:
- 对每个点,计算它到所有 k 个最近邻的平均距离。
- 所有点的平均距离会形成一个分布(近似高斯分布)。
- 设定一个标准差倍数 threshold(比如 1.0 或 2.0)。
- 平均距离超过
μ + threshold × σ的点,被判定为离群点,剔除。
我在项目中遇到过这样的情况:用地面激光扫描仪采集古建筑数据时,总有一些飘在空中的点——那是飞虫或者灰尘。统计滤波一上,干净利落。但要注意,如果场景本身就有稀疏结构(比如电线杆),别把人家正常点也给滤掉了。
参数调优建议:
- k(邻域点数):一般取 20~50。点云越密,k 可以越大。
- std_mult(标准差倍数):1.0 比较激进,2.0 比较保守。我习惯先试 1.5。
// C++ PCL 示例:统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(30); // 邻域点数 k = 30
sor.setStddevMulThresh(1.5); // 标准差倍数 1.5
sor.filter(*cloud_filtered);
注意:统计滤波假设点云密度相对均匀。如果点云密度变化极大(比如近处密集、远处稀疏),建议先分区域处理,或者改用半径滤波。
3.2 半径滤波(Radius Outlier Removal)
半径滤波的逻辑更简单粗暴:以每个点为中心,画一个半径为 r 的球。球里面的邻居数如果少于某个阈值 min_pts,就把这个点干掉。
你想想看,一个孤立噪点周围通常没几个点,而正常点周围应该有不少邻居。这就是半径滤波的直觉。
| 特性 | 统计滤波 | 半径滤波 |
|---|---|---|
| 判断依据 | 距离分布的统计量 | 固定半径内的点数 |
| 对密度变化 | 敏感 | 不敏感(半径固定) |
| 参数数量 | 2 个(k, std_mult) | 2 个(r, min_pts) |
| 典型场景 | 均匀密度 + 稀疏离群点 | 密度不均 + 孤立小簇 |
我曾经在一个工厂自动化项目里用过半径滤波。当时点云来自多个角度拼接,边缘处总有一些「飘」出来的点。统计滤波效果一般,因为边缘点密度本身就不均匀。换成半径滤波,设半径 0.05 米、最少 3 个点,问题就解决了。
// C++ PCL 示例:半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.05); // 搜索半径 0.05 米
ror.setMinNeighborsInRadius(3); // 最少 3 个邻居
ror.filter(*cloud_filtered);
经验之谈:半径滤波的参数和点云的单位有关。如果你的点云单位是毫米,半径设 50 就相当于 5 厘米。记得先确认单位!
3.3 体素滤波(Voxel DownSample)
体素滤波和前两个不太一样——它不只是去噪,更主要的是降采样。但为什么把它放在滤波章节?因为体素滤波在降采样的同时,天然有平滑噪声的效果。
原理很简单:
- 把三维空间划分成一个个小立方体(体素),边长由你指定。
- 每个体素内,计算所有点的重心(或中心),用这个点代表整个体素。
- 原始点云被大幅精简,同时局部噪声被平均掉了。
我记得有一次处理一个 2000 万点的户外场景,直接做配准,内存直接爆了。体素滤波设成 0.1 米的体素大小,点云降到 50 万点,配准速度提升了 40 倍,精度几乎没损失。
体素大小怎么选?
- 0.01 米(1cm):保留精细结构,适合室内场景。
- 0.05 米(5cm):平衡精度与效率,适合大多数场景。
- 0.1 米(10cm):快速预览或粗配准,适合大场景。
// C++ PCL 示例:体素滤波
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud);
vg.setLeafSize(0.05f, 0.05f, 0.05f); // 体素边长 5cm
vg.filter(*cloud_filtered);
注意:体素滤波会丢失细节。如果你的应用需要精确的几何特征(比如边缘检测),体素大小别设太大。另外,体素滤波不保证保留原始点的精确位置——它生成的是重心点。
3.4 三种滤波的对比与组合策略
实际项目中,很少只用一种滤波。我个人的习惯流程是这样的:
- 先体素滤波:把数据量降下来,同时初步平滑噪声。
- 再统计滤波或半径滤波:根据场景特点,剔除离群点。
- 最后再检查:可视化看一眼,确认没有误删关键点。
| 场景 | 推荐组合 | 说明 |
|---|---|---|
| 室内小物体扫描 | 体素(0.01) + 统计滤波(k=20, std=1.5) | 保留细节,剔除飞点 |
| 户外大场景 | 体素(0.1) + 半径滤波(r=0.2, min=5) | 大幅降采样,剔除孤立点 |
| 机器人导航 | 半径滤波(r=0.05, min=3) | 实时性要求高,不降采样 |
| 高精度配准 | 统计滤波(k=50, std=2.0) | 保守滤波,保留更多点 |
嗯,这里要注意一点:滤波不是越狠越好。我见过有人把体素设到 0.5 米,结果一堵墙变成几个点,配准直接失败。滤波的目的是「去伪存真」,不是「削足适履」。
一句话总结:统计滤波看距离分布,半径滤波看邻居数量,体素滤波看空间网格。三者各有所长,组合使用效果最佳。