4. 点云预处理(上)

各位同学,欢迎来到实战教程的第一章。

说实话,做融合定位这么多年,我踩过最大的坑就是——原始点云直接往里怼。你想想看,一帧激光雷达扫下来,几十万个点,里面还有各种噪声、离群点、甚至飞点。直接拿去做配准或者融合,结果可想而知。

所以,点云预处理不是可选项,是必选项。今天我们就来聊聊滤波和降采样这两件事。

核心观点:预处理做得好,定位精度差不了。预处理偷懒,后面全白干。

点云预处理知识体系 原始点云 滤波阶段(去除噪声) 直通滤波(PassThrough) 体素滤波(VoxelGrid) 统计滤波(StatisticalOutlier) 降采样阶段(减少点数) VoxelGrid 降采样 Farthest Point Sampling

4.1 为什么要做点云滤波?

先说说我的亲身经历。有一次在室内走廊做定位实验,激光雷达扫出来的点云里,莫名其妙多了几个离得很远的点。一查,是玻璃窗反射造成的。这些点如果不处理,配准时会把整个位姿带偏。

点云滤波要解决三个问题:

  • 去除噪声——传感器本身的热噪声、环境反射造成的飞点
  • 限制范围——只保留我们关心的区域,比如前方20米内
  • 剔除离群点——那些孤零零飘在空中的点,基本是误检

我的经验:滤波参数不要死记硬背。不同场景、不同激光雷达,参数差异很大。我一般先在Rviz里可视化看一下效果,再定参数。

4.2 三种常用滤波方法

4.2.1 直通滤波(PassThrough Filter)

直通滤波,说白了就是切一刀。你设定一个范围,在这个范围之外的点全部扔掉。

比如,我只关心前方10米内的障碍物,那就把x轴范围设在0到10米。或者我只关心地面以上2米内的物体,那就把z轴范围设在-0.5到2.0米。

// C++ 示例:直通滤波
pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");        // 沿z轴滤波
pass.setFilterLimits(-0.5, 2.0);     // 保留z在-0.5到2.0之间的点
pass.filter(*cloud_filtered);

注意:直通滤波会改变点云的组织结构。如果你用的是有序点云(organized point cloud),滤波后可能变成无序的。我曾经因为这个原因,导致后续的深度图生成直接崩了。

4.2.2 体素滤波(VoxelGrid Filter)

体素滤波这个名字听起来高大上,其实原理很简单。你把三维空间切成一个个小立方体(体素),每个立方体里只保留一个点——通常是所有点的重心。

这样做有两个好处:一是降采样,二是平滑噪声。

// C++ 示例:体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.1f, 0.1f, 0.1f);  // 体素边长10cm
voxel.filter(*cloud_filtered);

体素大小怎么选?我个人习惯:

  • 室内场景:0.05m - 0.1m
  • 室外场景:0.1m - 0.3m
  • 远距离物体:可以适当放大,反正远距离点本来就稀疏

4.2.3 统计滤波(Statistical Outlier Removal)

统计滤波是专门对付离群点的。它的思路是:计算每个点到它k个最近邻的平均距离。如果这个距离明显大于平均值,那这个点大概率是离群点。

// C++ 示例:统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);              // 考虑50个近邻
sor.setStddevMulThresh(1.0);   // 标准差倍数阈值
sor.filter(*cloud_filtered);

关键参数:

  • setMeanK:近邻数量。越大,滤波越平滑,但计算越慢
  • setStddevMulThresh:阈值。越小,剔除的点越多

我曾经在户外场景用过一次统计滤波,阈值设得太小(0.5),结果把墙上的点都当成离群点给剔了。嗯,那面墙看起来就像被狗啃过一样。

4.3 点云降采样

滤波之后,点云干净了,但点数可能还是很多。一帧64线激光雷达的点云,动辄十几万个点。实时处理的话,CPU扛不住。

降采样就是要在保持几何结构的前提下,减少点的数量。

4.3.1 VoxelGrid 降采样

刚才提到的体素滤波,其实也是一种降采样。但这里要区分一下:

  • 体素滤波:每个体素内取重心,有平滑效果
  • 体素降采样:每个体素内随机取一个点,或者取离体素中心最近的点

我个人更推荐用体素滤波来做降采样,因为取重心能保留更多几何信息。

4.3.2 Farthest Point Sampling(FPS)

FPS 是一种更高级的降采样方法。它的思路是:每次选一个离已选点集最远的点加入。

这样做的好处是,采样点能均匀覆盖整个点云,不会出现局部密集、局部稀疏的情况。

// Python 示例:Farthest Point Sampling
def farthest_point_sample(point_cloud, n_samples):
    """
    point_cloud: (N, 3) 的点云
    n_samples: 要采样的点数
    """
    N, D = point_cloud.shape
    centroids = np.zeros((n_samples,))
    distance = np.ones((N,)) * 1e10
    
    farthest = np.random.randint(0, N)
    for i in range(n_samples):
        centroids[i] = farthest
        centroid = point_cloud[farthest, :].reshape(1, 3)
        dist = np.sum((point_cloud - centroid) ** 2, axis=1)
        distance = np.minimum(distance, dist)
        farthest = np.argmax(distance)
    
    return point_cloud[centroids.astype(np.int32)]

什么时候用FPS?当你的点云分布很不均匀时,比如近处点很密、远处点很疏。FPS能保证远处那些稀疏的点不会被丢掉。我在做长距离走廊定位时,就靠FPS保住了远处的特征点。

4.4 代码实战:完整的预处理流程

下面是一个完整的预处理流程,把滤波和降采样串起来:

# Python + Open3D 实现
import open3d as o3d
import numpy as np

def preprocess_point_cloud(pcd, config):
    """
    点云预处理流水线
    config: 配置字典
    """
    # 1. 直通滤波 - 只保留前方20米,高度-1到3米
    bbox = o3d.geometry.AxisAlignedBoundingBox(
        min_bound=(-20, -20, -1),
        max_bound=(20, 20, 3)
    )
    pcd = pcd.crop(bbox)
    
    # 2. 统计滤波 - 剔除离群点
    cl, ind = pcd.remove_statistical_outlier(
        nb_neighbors=20,
        std_ratio=2.0
    )
    pcd = pcd.select_by_index(ind)
    
    # 3. 体素降采样 - 体素大小0.1m
    pcd = pcd.voxel_down_sample(voxel_size=0.1)
    
    return pcd

# 使用示例
pcd = o3d.io.read_point_cloud("scan.pcd")
processed_pcd = preprocess_point_cloud(pcd, {})
o3d.visualization.draw_geometries([processed_pcd])

性能提醒:统计滤波的nb_neighbors不要设太大。我试过设到100,一帧点云处理了3秒,这在实时系统里是不可接受的。一般20-50就够用了。

4.5 参数调优建议

场景 体素大小 统计滤波近邻数 统计滤波阈值 推荐降采样方法
室内小场景 0.05m 20 1.5 VoxelGrid
室外开阔 0.2m 30 2.0 VoxelGrid
长走廊/隧道 0.1m 50 1.0 FPS
高速移动场景 0.15m 20 2.5 VoxelGrid

这些参数不是绝对的。我建议你拿到新数据后,先可视化看看效果,再微调。毕竟,每个激光雷达的脾气都不一样。

本章小结:

  • 直通滤波:切掉不关心的区域,简单粗暴
  • 体素滤波:降采样+平滑,最常用的方法
  • 统计滤波:专门对付离群点,参数要小心调
  • FPS:均匀采样,适合分布不均的场景
  • 预处理流程:直通→统计→降采样,这个顺序我用了好几年