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

各位同学,咱们今天聊聊点云预处理。说实话,很多刚入行的朋友拿到点云数据,上来就急着做分割聚类,结果效果一塌糊涂。我当年也犯过这个错——有一次项目赶进度,直接拿原始点云跑算法,结果聚类出来一堆乱七八糟的东西,排查了半天才发现是噪声点惹的祸。

点云预处理,说白了就是给数据「洗个澡」。原始点云通常有几个问题:数据量太大、噪声点多、无关区域干扰。咱们这节课就针对这四个问题,一个一个解决。

2.1 体素滤波降采样

先说说降采样。你想想看,一个64线激光雷达,一秒钟能产生几十万个点。这么多点直接处理,计算量太大了。我见过有人用原始点云跑分割,一帧数据要算好几秒,这哪行?

体素滤波的思路很简单:把空间划分成一个个小立方体(体素),每个体素里只保留一个点。这个点通常是体素内所有点的重心,或者直接取中心点。

核心参数:体素大小

体素大小直接决定降采样后的点云密度。我个人的习惯是:

  • 室外场景:0.1m - 0.2m
  • 室内场景:0.05m - 0.1m
  • 精细建模:0.02m - 0.05m

体素设得太小,降采样效果不明显;设得太大,会丢失细节。我记得有一次做道路检测,体素设了0.5m,结果路沿石的特征全没了,分割出来的道路边界歪歪扭扭的。

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

小技巧:体素滤波后的点云,点的位置是体素内所有点的平均值,不是原始点。如果你需要保留原始点的位置,可以用随机采样代替体素滤波。

2.2 统计滤波去噪

统计滤波是去噪的利器。它的原理是:计算每个点与其k个近邻点的平均距离,如果这个距离超过某个阈值,就认为是离群点。

为什么会这样?因为正常点云中,每个点周围都有很多邻居,平均距离比较小。而噪声点往往是孤立的,周围没什么点,平均距离就很大。

我曾经在一个工厂项目里遇到过这种情况:激光雷达扫描时,有些灰尘和飞虫被误采为点云。这些点数量不多,但位置随机,用统计滤波一下子就滤掉了。

两个关键参数:

  • k(近邻点数):一般取20-50。点云越密,k可以越大。
  • std_dev(标准差倍数):一般取1.0-3.0。值越小,滤掉的点越多。
// C++ 示例:PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setMeanK(50);           // 近邻点数
sor.setStddevMulThresh(1.0); // 标准差倍数
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_denoised(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_denoised);

注意:统计滤波可能会误删边缘点。比如物体边缘的点,邻居少,平均距离大,容易被当成噪声。如果你要保留边缘细节,std_dev可以设大一点,比如2.0或3.0。

2.3 半径滤波去噪

半径滤波和统计滤波有点像,但思路更直接:以每个点为中心,画一个半径为r的球,如果球内的点数少于某个阈值,就删掉这个点。

说白了,就是「朋友太少就滚蛋」。这个方法的优点是参数直观,好理解。缺点是对点云密度变化敏感——密度大的地方,半径内点就多;密度小的地方,半径内点就少。

我建议在点云密度比较均匀的场景下用半径滤波。比如室内结构化场景,点云分布相对均匀,效果就很好。

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

经验之谈:半径滤波和统计滤波可以配合使用。先用统计滤波去掉大范围的离群点,再用半径滤波清理小范围的孤立点。我一般先做统计滤波(std_dev=1.5),再做半径滤波(r=0.1m, min=3)。

2.4 直通滤波

直通滤波是最简单的滤波方法,没有之一。它的作用就是切掉你不需要的区域。

比如你只关心前方50米内的障碍物,那就把x轴方向超过50米的点全部删掉。再比如你只关心地面以上的物体,那就把z轴小于0的点删掉。

直通滤波在预处理中非常实用。我记得有一次做停车场检测,激光雷达装在车顶,采集到的点云包含了车顶本身。用直通滤波把z轴小于1.5米的点删掉,车顶就没了,剩下的全是周围环境。

// C++ 示例:PCL 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_radius);
pass.setFilterFieldName("z");      // 沿z轴滤波
pass.setFilterLimits(0.0, 2.0);    // 保留0到2米范围
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_passed(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_passed);

常用滤波范围:

应用场景 滤波轴 范围
道路检测 z轴 -0.5m ~ 3.0m
行人检测 z轴 0.0m ~ 2.0m
前方障碍物 x轴 0.0m ~ 50.0m
侧方车辆 y轴 -10.0m ~ 10.0m

避坑指南:直通滤波的边界是硬切割。如果你在边界处有重要物体,可能会被切掉一半。我曾经在项目里把一辆车的尾部切掉了,导致聚类时少了一个障碍物。建议在边界处留一点余量。

2.5 四种滤波的选用策略

好了,四种滤波方法都讲完了。你可能会问:到底该用哪个?

我的建议是:

  • 数据量太大?先做体素滤波降采样。
  • 有离群噪声点?用统计滤波或半径滤波。
  • 只关心某个区域?用直通滤波切掉无关区域。

实际项目中,我通常的预处理流程是:

  1. 直通滤波:切掉明显无关的区域(比如天空、地面以下)
  2. 体素滤波:降采样到可处理的数据量
  3. 统计滤波:去掉大噪声点
  4. 半径滤波:清理小噪声点

嗯,这个流程我用了好几年,效果一直很稳定。当然,具体参数要根据你的传感器和场景来调。没有万能参数,只有不断试错。

下一节课,咱们就拿着预处理好的点云,开始做分割和聚类了。到时候你会发现,预处理做得好,后面的工作轻松一大半。