4. 点云预处理:体素滤波降采样、统计滤波去噪、半径滤波去噪、直通滤波
点云数据拿到手,第一件事不是急着做配准或分割。你得先看看这堆点「干不干净」。
我早年做过一个仓储抓取项目,激光雷达扫出来的点云直接送进分割模块,结果机器人死活抓不准货架上的小件。折腾了两天,最后发现是点云里有一堆漂浮的噪声点——说白了,预处理没做好,后面全是白费功夫。
这一章,我们就来聊聊点云预处理的四个基本功:体素滤波、统计滤波、半径滤波和直通滤波。它们各自解决不同的问题,组合起来用,效果最好。
4.1 体素滤波降采样
体素滤波,说白了就是用一个小立方体(体素)把空间网格化。每个网格里只保留一个点——通常是重心或中心点。
为什么要做这个?你想想看,一台高分辨率激光雷达扫一圈,动辄几十万甚至上百万个点。这么多点直接处理,计算量太大了。体素滤波能在保持物体形状的前提下,把点数降下来。
来看一段 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 宽松一些,保留更多边缘点。
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);
4.5 四种滤波的串联流程
在实际项目中,这四种滤波很少单独使用。我总结了一套通用的串联流程:
- 直通滤波:先砍掉 ROI 之外的区域,减少数据量。
- 体素滤波:降采样,进一步压缩点数。
- 统计滤波:去掉大范围的离群噪声点。
- 半径滤波:清理剩余的稀疏飞点。
这套流程我用了好几年,在仓储、工业抓取、服务机器人等多个场景都验证过。当然,具体参数要根据你的传感器和场景来调。