4、点云滤波:体素滤波、统计滤波、半径滤波
点云滤波,说白了就是给点云数据「洗个澡」。
我刚开始做激光雷达项目时,拿到一帧原始点云,密密麻麻几百万个点,心里还挺激动。结果一跑算法,电脑直接卡死。后来才明白——原始点云里大量是噪声、离群点、冗余点。不处理干净,后面什么配准、分割、识别都别想好好干。
今天咱们就聊三种最常用的滤波方法:体素滤波、统计滤波、半径滤波。这三种方法,我几乎在每个项目里都会用到,各有各的脾气。
4.1 体素滤波(VoxelGrid)——给点云「瘦身」
体素滤波,说白了就是降采样。把空间切成一个个小立方体(体素),每个立方体里只保留一个点(通常是重心)。
为什么要这么做?
你想想看,一帧64线激光雷达的数据,动不动就几十万甚至上百万个点。这么多点,很多是冗余的——比如一面墙,你只需要几个点就能描述它的位置,没必要几万个点堆在那儿。体素滤波就是帮你「精简队伍」。
核心思想:用三维网格(体素)对点云空间进行划分,每个网格内只保留一个代表点。
参数怎么调?
体素滤波就一个关键参数:体素大小(leaf size)。
- 体素越大,降采样越狠,点越少,但细节丢失也越多
- 体素越小,保留的点越多,计算量也越大
我个人习惯:对于室内场景(比如房间、走廊),体素大小设在0.05m~0.1m;对于室外大场景(比如街道、园区),可以放宽到0.2m~0.5m。
我的经验:体素滤波最大的好处是「均匀降采样」。它不会像随机采样那样,有的地方点密、有的地方点稀。体素滤波能保证整个空间都有点,不会出现空洞。
代码示例(PCL)
// 创建体素滤波对象
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud_input);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长0.1米
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 统计滤波(StatisticalOutlierRemoval)——揪出「孤魂野鬼」
统计滤波,专门用来干掉离群点。
什么叫离群点?就是那些孤零零飘在空中的点。比如激光打到玻璃上产生的反射噪点,或者远处飘来的灰尘点。这些点数量不多,但特别影响后续处理——配准时它们会拉偏结果,分割时它们会干扰聚类。
统计滤波的原理很简单:
- 对每个点,计算它到所有邻居点的平均距离
- 假设这些平均距离服从高斯分布
- 把那些平均距离超出「均值±k倍标准差」的点,判定为离群点,直接干掉
核心思想:点云中大部分点分布是均匀的,离群点往往「不合群」——它们到邻居的距离明显偏大。
两个关键参数
| 参数 | 含义 | 推荐值 |
|---|---|---|
| 邻居点数(k) | 计算平均距离时,考虑多少个最近邻 | 20~50 |
| 标准差倍数(std_mul) | 判定阈值,越大越宽松 | 1.0~3.0 |
我曾经在一个项目中,点云里混进了大量「飞点」——就是激光打到空中漂浮物上产生的噪点。统计滤波一跑,干净利落。但要注意:如果场景本身点云就很稀疏(比如远距离点),统计滤波可能会把正常点也误删掉。这时候需要把标准差倍数调大一些。
代码示例(PCL)
// 创建统计滤波对象
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor_filter;
sor_filter.setInputCloud(cloud_input);
sor_filter.setMeanK(30); // 考虑30个邻居
sor_filter.setStddevMulThresh(2.0); // 2倍标准差
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
sor_filter.filter(*cloud_filtered);
避坑指南:统计滤波对「孤立小簇」效果很好,但对「大片噪点区域」效果有限。比如一大片雾状噪点,每个点周围都有很多邻居,统计滤波就识别不出来了。这种情况我一般先用半径滤波打头阵。
4.3 半径滤波(RadiusOutlierRemoval)——「圈子文化」
半径滤波,比统计滤波更「简单粗暴」。
它的逻辑是:给每个点画一个半径为r的圆(三维里是球),数一数这个圆里有多少个邻居点。如果邻居数少于某个阈值,就判定为离群点,删掉。
说白了,就是「朋友太少的人,不配留在点云里」。
核心思想:在指定半径内,邻居数量不足的点视为离群点。
两个参数
| 参数 | 含义 | 推荐值 |
|---|---|---|
| 搜索半径(r) | 画多大的圈 | 0.1~0.5米(视场景而定) |
| 最少邻居数(min_pts) | 圈里至少要有几个点 | 3~10 |
我记得有一次做隧道点云处理,隧道壁上有很多施工留下的金属反光点,这些点周围邻居很少,半径滤波一过,全给清掉了。而隧道壁本身的点因为密集,全部保留。效果非常直观。
代码示例(PCL)
// 创建半径滤波对象
pcl::RadiusOutlierRemoval<pcl::PointXYZ> radius_filter;
radius_filter.setInputCloud(cloud_input);
radius_filter.setRadiusSearch(0.2); // 搜索半径0.2米
radius_filter.setMinNeighborsInRadius(5); // 至少5个邻居
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
radius_filter.filter(*cloud_filtered);
注意:半径滤波对参数非常敏感。半径设大了,会把正常点也删掉;半径设小了,离群点又删不干净。我建议先用统计滤波粗筛一遍,再用半径滤波精修,效果最好。
4.4 三种滤波方法对比
这三种方法各有各的适用场景,我整理了一张表,方便你对比:
| 方法 | 主要用途 | 优点 | 缺点 |
|---|---|---|---|
| 体素滤波 | 降采样、减少点数 | 均匀降采样,速度快 | 改变点坐标,丢失细节 |
| 统计滤波 | 去除稀疏离群点 | 自适应阈值,效果好 | 对密集噪点无效 |
| 半径滤波 | 去除孤立小簇 | 简单直观,参数少 | 对参数敏感 |
4.5 实际项目中的滤波流程
我一般会这样组合使用:
- 第一步:体素滤波——先把点云降采样到可处理的数量(比如从100万降到10万)
- 第二步:统计滤波——干掉那些飘在空中的稀疏噪点
- 第三步:半径滤波——再清理一下残留的孤立小簇
这个流程我用了好几年,基本能应对90%以上的场景。当然,如果你的点云质量特别好(比如室内静态扫描),直接上体素滤波就够了。
我的建议:不要一上来就调参数。先跑一遍默认参数,看看效果,再根据结果微调。我见过太多人花半小时调参数,结果发现是数据本身有问题。
4.6 本章知识体系
下面这张图,帮你理清三种滤波方法的核心逻辑:
嗯,以上就是三种滤波方法的全部内容。说实话,滤波这件事,理论不难,难的是根据实际数据调参数。多试几次,你就能找到感觉。
公众号:蓝海资料掘金营,微信deep3321