4. 点云数据预处理

点云数据,说白了就是一堆三维坐标点的集合。但激光雷达扫出来的原始点云,往往又密又乱,还带着各种噪声。我刚开始做SLAM时,拿着原始点云直接跑匹配,结果定位精度惨不忍睹。后来才明白——预处理做不好,后面全是白忙活。

这一章,我们就来聊聊点云预处理的三个核心操作:体素滤波、直通滤波和半径滤波。同时,我会带你上手PCL库,这是点云处理领域最常用的工具库,没有之一。

点云预处理知识体系 原始点云 体素滤波 降采样,控制点密度 直通滤波 裁剪空间范围 半径滤波 去除离群噪声点 预处理后点云 特征提取 → 配准 → 建图定位 PCL库实现

4.1 为什么需要点云预处理?

你想想看,一台16线激光雷达,每秒能产生30万个点。如果直接拿这些点去算匹配,计算量巨大,而且很多点其实是没用的。比如,远处的点稀疏且噪声大,地面上的点对定位帮助有限,还有一些孤立的噪点会严重干扰特征提取。

预处理的目的就三个:减量、去噪、提纯。说白了,就是把原始点云里那些「没用」或「有害」的点去掉,留下真正有价值的信息。

核心原则:预处理不是越狠越好。过度滤波会丢失细节,影响后续匹配精度。我一般会先保守地设参数,然后根据效果逐步调整。

4.2 PCL库快速上手

PCL(Point Cloud Library)是点云处理的事实标准。它封装了从滤波、特征提取到配准、分割的全套算法。我个人习惯用PCL 1.8以上版本,配合ROS Melodic或Noetic都很稳定。

先看一个最基础的PCL程序框架:

#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/passthrough.h>
#include <pcl/filters/radius_outlier_removal.h>

// 定义点云类型
typedef pcl::PointXYZ PointT;
typedef pcl::PointCloud<PointT> PointCloud;

int main(int argc, char** argv) {
    // 创建点云对象
    PointCloud::Ptr cloud(new PointCloud);
    PointCloud::Ptr cloud_filtered(new PointCloud);
    
    // 加载点云(假设已有数据)
    // cloud = ... 
    
    // 这里放滤波代码
    
    return 0;
}

嗯,这里要注意:PCL里大量使用智能指针(::Ptr),这是为了避免内存泄漏。刚开始用的时候可能会不习惯,但用多了你会发现这设计确实好。

4.3 体素滤波:给点云「瘦身」

体素滤波的原理很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个重心点。这样,原本密集的点云就变得稀疏了,但整体形状保持不变。

我在项目中遇到过这样的情况:用32线雷达扫描一个房间,原始点云有80万个点。体素滤波后,降到5万个点,但墙角和桌面的轮廓依然清晰。匹配速度提升了10倍以上。

代码实现:

// 体素滤波
pcl::VoxelGrid<PointT> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.05f, 0.05f, 0.05f);  // 体素边长5cm
voxel.filter(*cloud_filtered);

std::cout << "滤波前点数: " << cloud->size() << std::endl;
std::cout << "滤波后点数: " << cloud_filtered->size() << std::endl;
体素大小 适用场景 效果
0.01m (1cm) 高精度室内建图 几乎不降采样,保留细节
0.05m (5cm) 一般SLAM场景 推荐值,平衡精度与速度
0.10m (10cm) 大场景快速建图 点云稀疏,可能丢失小物体
0.20m (20cm) 室外远距离 仅保留主要结构

我的经验:体素大小不要超过你关注的最小物体尺寸的一半。比如你要识别路沿,路沿宽度约15cm,那体素最好设在7cm以内。

4.4 直通滤波:裁剪空间范围

直通滤波,说白了就是「切一刀」。你指定一个坐标轴的范围,落在范围外的点全部砍掉。这在去除远处噪点、裁剪感兴趣区域时非常有用。

比如,激光雷达装在车顶,我们只关心前方20米内的障碍物,那就可以把Z轴(高度)和X轴(前方距离)限制一下:

// 直通滤波 - 限制Z轴范围(去除地面和过高点)
pcl::PassThrough<PointT> pass;
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("z");
pass.setFilterLimits(-0.5, 2.0);  // 保留高度-0.5m到2.0m之间的点
pass.filter(*cloud_filtered);

// 直通滤波 - 限制X轴范围(只看前方)
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 20.0);  // 保留前方0到20米
pass.filter(*cloud_filtered);

我曾经犯过一个错误:在直通滤波时,忘了设置setFilterLimitsNegative(false),结果把想要的点全滤掉了,保留了外面的点。调试了半天才发现。嗯,这个坑你们别踩。

注意:直通滤波默认保留范围内的点。如果想保留范围外的点,需要调用 pass.setFilterLimitsNegative(true)。另外,多次直通滤波可以组合使用,实现任意形状的裁剪。

4.5 半径滤波:清除离群噪点

激光雷达扫到玻璃、水面或者远距离物体时,会产生一些孤立的噪点。这些点周围没有其他点支撑,一看就是「孤儿」。半径滤波就是专门收拾这些孤儿的。

它的逻辑是:对每个点,检查它周围指定半径内有多少个邻居。如果邻居数量少于阈值,就判定为离群点,直接删除。

// 半径滤波
pcl::RadiusOutlierRemoval<PointT> rad;
rad.setInputCloud(cloud_filtered);
rad.setRadiusSearch(0.1);      // 搜索半径10cm
rad.setMinNeighborsInRadius(3); // 至少要有3个邻居
rad.filter(*cloud_filtered);

参数怎么调?我一般这样试:

  • 半径:设为体素大小的2-3倍。体素5cm,半径就设10-15cm。
  • 最小邻居数:室内场景设2-3,室外稀疏场景设1-2。

你想想看,如果半径设太大、邻居数设太高,那正常的点也可能被误删。如果设太小,噪点又清不干净。这需要根据实际点云密度来微调。

4.6 三种滤波的组合策略

在实际项目中,我通常按这个顺序处理:

  1. 先直通滤波:快速裁剪掉不关心的区域,减少后续计算量。
  2. 再体素滤波:降采样,控制点云密度。
  3. 最后半径滤波:清除降采样后可能出现的离群点。

为什么是这个顺序?因为体素滤波会改变点的分布,如果先做半径滤波,体素化后可能又产生新的离群点。先降采样再清噪,效果更干净。

完整流程代码:

// 1. 直通滤波 - 裁剪空间
pcl::PassThrough<PointT> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.0, 3.0);
pass.filter(*cloud_tmp);

// 2. 体素滤波 - 降采样
pcl::VoxelGrid<PointT> voxel;
voxel.setInputCloud(cloud_tmp);
voxel.setLeafSize(0.05f, 0.05f, 0.05f);
voxel.filter(*cloud_tmp);

// 3. 半径滤波 - 去噪
pcl::RadiusOutlierRemoval<PointT> rad;
rad.setInputCloud(cloud_tmp);
rad.setRadiusSearch(0.15);
rad.setMinNeighborsInRadius(3);
rad.filter(*cloud_filtered);

4.7 避坑指南

做点云预处理这几年,我踩过不少坑。挑几个典型的说说:

  • 体素滤波后点云变「方块感」:体素设太大就会这样。如果发现建图结果出现锯齿状边缘,把体素缩小一半试试。
  • 直通滤波把目标切没了:有一次我想滤掉地面,Z轴下限设了0.0,结果车停在坡上,半个车身被切掉了。后来我改成动态阈值,根据IMU的俯仰角调整。
  • 半径滤波把墙上的点删光了:墙面点本来就很稀疏,半径设太小、邻居数设太高,墙就没了。对于墙面这种平面结构,建议用统计滤波替代半径滤波。

最后说一句:PCL的滤波函数都是线程安全的,但如果你在ROS的回调函数里频繁创建和销毁滤波器对象,会有性能开销。我习惯把滤波器对象声明为类成员,初始化一次,反复使用。

好了,点云预处理就聊到这里。这三种滤波是SLAM预处理阶段最基础也最实用的操作。掌握了它们,你就有了处理原始点云的第一把利器。