4. 点云预处理:体素滤波降采样、统计滤波去噪、半径滤波去噪、直通滤波

点云数据拿到手,第一件事不是急着做配准或分割。你得先看看这堆点「干不干净」。

我早年做过一个仓储抓取项目,激光雷达扫出来的点云直接送进分割模块,结果机器人死活抓不准货架上的小件。折腾了两天,最后发现是点云里有一堆漂浮的噪声点——说白了,预处理没做好,后面全是白费功夫。

这一章,我们就来聊聊点云预处理的四个基本功:体素滤波、统计滤波、半径滤波和直通滤波。它们各自解决不同的问题,组合起来用,效果最好。

核心观点:预处理不是可选项,是必选项。跳过这一步,你的算法大概率会在真实场景中翻车。
点云预处理四大滤波方法 原始点云 体素滤波 降采样 统计滤波 去离群点 半径滤波 去稀疏点 直通滤波 范围裁剪 干净点云 实际项目中,通常按 直通 → 体素 → 统计/半径 的顺序串联使用

4.1 体素滤波降采样

体素滤波,说白了就是用一个小立方体(体素)把空间网格化。每个网格里只保留一个点——通常是重心或中心点。

为什么要做这个?你想想看,一台高分辨率激光雷达扫一圈,动辄几十万甚至上百万个点。这么多点直接处理,计算量太大了。体素滤波能在保持物体形状的前提下,把点数降下来。

经验参数:我一般把体素大小设在 0.01m ~ 0.05m 之间。室内场景用 0.02m,室外大场景可以放宽到 0.05m。太小了降采样效果不明显,太大了会丢失细节。

来看一段 PCL 的实现代码:

// C++ PCL 体素滤波示例
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.02f, 0.02f, 0.02f); // 体素边长 2cm
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
voxel_filter.filter(*cloud_filtered);

std::cout << "原始点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;

输出结果大概是这样:

指标 数值
原始点数 1,024,000
体素大小 0.02 m
滤波后点数 ~85,000
压缩比 约 12:1

压缩了十几倍,但物体的轮廓基本没变。这就是体素滤波的价值。

注意:体素滤波会改变点的精确位置。如果你需要做高精度测量(比如抓取精密零件),体素大小别设太大,否则位置误差会累积。

4.2 统计滤波去噪

统计滤波的思路很直观:每个点周围都有一些邻居。如果某个点离它的邻居太远,那它大概率是个噪声点。

具体做法是:计算每个点到它 k 个最近邻的平均距离。这些平均距离会形成一个高斯分布。我们把落在均值 ± n 倍标准差范围之外的点剔除掉。

我在项目中遇到过一种情况:扫地机器人上的激光雷达偶尔会扫到天花板上的灰尘颗粒,产生一些飘浮的孤立点。统计滤波一跑,这些点全没了。

// C++ PCL 统计滤波示例
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setMeanK(50);          // 邻居数量
sor.setStddevMulThresh(1.0); // 标准差倍数阈值
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_sor(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_sor);

参数怎么调?我个人的习惯是:

  • setMeanK:一般取 20~100。点云密度高就取大一点,密度低就取小一点。
  • setStddevMulThresh:1.0 比较严格,能去掉大部分离群点;1.5 宽松一些,保留更多边缘点。
避坑指南:我曾经把标准差倍数设成 0.5,结果把物体边缘的正常点也删掉了,导致抓取时位姿估计失败。后来改成 1.0,问题解决。

4.3 半径滤波去噪

半径滤波和统计滤波有点像,但逻辑更简单:以每个点为中心画一个球,如果球内的邻居数量少于某个阈值,就把这个点删掉。

什么时候用半径滤波?当你的场景里有稀疏的飞点或者孤立的噪声点时,半径滤波比统计滤波更直接。

// C++ PCL 半径滤波示例
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud_sor);
ror.setRadiusSearch(0.05);   // 搜索半径 5cm
ror.setMinNeighborsInRadius(6); // 最少邻居数
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_ror(new pcl::PointCloud<pcl::PointXYZ>);
ror.filter(*cloud_ror);

两种滤波的对比:

特性 统计滤波 半径滤波
判断依据 平均距离的统计分布 固定半径内的点数
适用场景 均匀噪声、离群点 稀疏飞点、孤立噪声
参数敏感性 对标准差倍数敏感 对半径大小敏感
计算速度 稍慢(需计算统计量) 较快

我个人习惯先做统计滤波,再做半径滤波。前者去掉大范围的离群点,后者清理剩下的零星噪声。

4.4 直通滤波

直通滤波是最简单的一种——直接按坐标轴范围裁剪点云。

比如,你的机器人工作空间是地面以上 0.5m 到 1.5m,那把 z 轴范围设成 [0.5, 1.5],之外的统统砍掉。这样能瞬间减少大量无关点。

// C++ PCL 直通滤波示例
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_ror);
pass.setFilterFieldName("z");      // 沿 z 轴裁剪
pass.setFilterLimits(0.5, 1.5);    // 保留 0.5m ~ 1.5m 范围
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_pass(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_pass);
实用技巧:直通滤波通常放在预处理的第一步。先把无关区域砍掉,后续的体素滤波和统计滤波计算量会小很多。我做过一个项目,先用直通滤波把 5m 外的点全删了,后续处理速度提升了 3 倍。

4.5 四种滤波的串联流程

在实际项目中,这四种滤波很少单独使用。我总结了一套通用的串联流程:

  1. 直通滤波:先砍掉 ROI 之外的区域,减少数据量。
  2. 体素滤波:降采样,进一步压缩点数。
  3. 统计滤波:去掉大范围的离群噪声点。
  4. 半径滤波:清理剩余的稀疏飞点。

这套流程我用了好几年,在仓储、工业抓取、服务机器人等多个场景都验证过。当然,具体参数要根据你的传感器和场景来调。

一句话总结:直通滤波砍范围,体素滤波降密度,统计滤波去离群,半径滤波清飞点。四步走完,点云就干净了。