4. 点云预处理与滤波:体素滤波、直通滤波、统计滤波,去除离群点与降采样
点云数据,说白了就是一堆三维坐标点的集合。但现实世界里的点云,远没有教科书上那么干净。激光雷达扫出来的数据,夹杂着噪声、飞点、还有各种莫名其妙的离群值。如果你直接把原始点云丢给后面的障碍物检测模块,那结果基本没法看。
我刚开始做无人机避障那会儿,就吃过这个亏。有一次试飞,飞机对着一个电线杆直直撞过去,我百思不得其解。后来回放数据才发现,点云里有个孤立的噪点被误判成了可穿越的空隙。嗯,从那以后,我再也不敢轻视预处理这一步了。
今天我们就来聊聊点云预处理的三个核心手段:体素滤波、直通滤波、统计滤波。它们各自解决不同的问题,组合起来用效果最好。
4.1 体素滤波:降采样的利器
体素滤波,说白了就是用一个个小立方体(体素)把空间网格化。每个体素内只保留一个代表点,通常是重心或中心点。
为什么需要它?
- 原始点云太密了。比如Velodyne 64线雷达,一秒钟能扫出130万个点。你想想看,无人机上的嵌入式电脑哪扛得住?
- 降采样后,计算量大幅下降,但几何结构基本保留。
核心参数:体素大小
- 体素越大,降采样越狠,点云越稀疏。
- 体素太小,降采样效果不明显。
- 我个人习惯:室内场景用0.05m,室外开阔场景用0.1m~0.2m。
重要提醒:体素滤波会丢失细节。如果你要识别细小的障碍物(比如电线、树枝),体素尺寸别超过0.1m。
// C++ 示例:PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud);
vg.setLeafSize(0.1f, 0.1f, 0.1f); // 体素边长10cm
vg.filter(*cloud_filtered);
4.2 直通滤波:划定感兴趣区域
直通滤波,名字听着玄乎,其实就是切一刀。沿着某个坐标轴,只保留指定范围内的点。
典型应用场景:
- 无人机飞行高度5米,那10米以上的点云基本是噪声,直接切掉。
- 只关注前方30米内的障碍物,远处的点云不要。
- 地面点云干扰太大,把z轴负半轴的点滤掉。
我在项目中遇到过一个问题:无人机在树林里飞行,雷达扫到头顶的树叶,导致高度估计频繁跳变。后来加了直通滤波,把高度限制在无人机上下2米范围内,问题就解决了。
小技巧:直通滤波通常放在预处理的第一步。先切出感兴趣区域,后面的计算量就小多了。
// C++ 示例:PCL 直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z"); // 沿z轴滤波
pass.setFilterLimits(0.0, 5.0); // 保留0~5米范围
pass.filter(*cloud_filtered);
4.3 统计滤波:揪出离群点
统计滤波的原理很简单:计算每个点与其k个近邻的平均距离。如果这个距离明显大于整体平均水平,那这个点很可能是离群点,干掉它。
为什么会有离群点?
- 激光打到玻璃、水面等镜面物体上,产生反射噪点。
- 灰尘、雨滴、飞虫造成的瞬时噪点。
- 传感器本身的测量误差。
两个关键参数:
| 参数 | 含义 | 推荐值 |
|---|---|---|
| k(近邻数) | 计算平均距离时考虑的邻居数量 | 20~50 |
| std_dev(标准差倍数) | 判定阈值,超过均值+std_dev*标准差即视为离群点 | 1.0~2.0 |
注意:std_dev设得太小,会把正常点也滤掉;设得太大,离群点滤不干净。我曾经在雨雾天气测试,把std_dev调到3.0才勉强够用。环境不同,参数要灵活调整。
// C++ 示例:PCL 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50); // 近邻数50
sor.setStddevMulThresh(1.0); // 标准差倍数1.0
sor.filter(*cloud_filtered);
4.4 三种滤波的配合策略
在实际项目中,我通常按这个顺序处理点云:
- 直通滤波:先切出感兴趣区域,减少数据量。
- 统计滤波:去除离群噪点,净化数据。
- 体素滤波:最后降采样,控制点云密度。
为什么会是这个顺序?你想想看,如果先做体素滤波,离群点会被平均到体素里,反而更难识别。先做统计滤波,离群点孤立存在,一抓一个准。
4.5 实战中的避坑指南
讲几个我踩过的坑,希望对你有帮助。
坑一:体素滤波后点云变稀疏,障碍物检测不到
有一次我在测试无人机避障,体素尺寸设了0.2m。结果一根5cm粗的树枝,体素里只留下一个点,障碍物检测模块直接忽略了它。后来我把体素尺寸改到0.05m,问题解决。
坑二:统计滤波把墙上的点也滤掉了
室内场景中,墙面本身很平整,每个点的近邻距离都很小。但如果你把std_dev设得太小(比如0.5),墙面边缘的点会被误判为离群点。我建议室内场景用1.5~2.0,室外场景用1.0~1.5。
坑三:直通滤波切得太狠,丢失关键信息
无人机在斜坡上飞行时,如果你用固定的z轴范围做直通滤波,可能会把坡面上的障碍物点切掉。我的做法是:结合无人机当前的姿态,动态调整滤波范围。
核心原则:滤波参数没有银弹。每个场景都要单独调参。我习惯在代码里留一个参数配置文件,现场试飞时边调边看效果。
4.6 代码实战:完整的预处理流程
下面是一个完整的预处理函数,把三种滤波串起来。代码不难,但很实用。
#include <pcl/point_types.h>
#include <pcl/filters/passthrough.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/filters/voxel_grid.h>
pcl::PointCloud<pcl::PointXYZ>::Ptr preprocessCloud(
pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud) {
// 1. 直通滤波:只保留前方30米,高度0~5米
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(input_cloud);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 30.0);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_pass(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_pass);
pass.setInputCloud(cloud_pass);
pass.setFilterFieldName("z");
pass.setFilterLimits(0.0, 5.0);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_pass_z(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_pass_z);
// 2. 统计滤波:去除离群点
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_pass_z);
sor.setMeanK(30);
sor.setStddevMulThresh(1.5);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_sor(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_sor);
// 3. 体素滤波:降采样
pcl::VoxelGrid<pcl::PointXYZ> vg;
vg.setInputCloud(cloud_sor);
vg.setLeafSize(0.1f, 0.1f, 0.1f);
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_final(new pcl::PointCloud<pcl::PointXYZ>);
vg.filter(*cloud_final);
return cloud_final;
}
这段代码我用了好几年,基本框架没变过。唯一会改的就是参数——不同传感器、不同场景,参数差异很大。
好了,点云预处理就聊到这儿。记住一句话:预处理做得好,后面障碍物检测就成功了一半。别嫌这步麻烦,它值得你花时间。