4、点云预处理:体素化降采样、均匀采样、随机采样、最远点采样(FPS)

各位同学,咱们今天聊聊点云预处理里的采样方法。说实话,我刚开始做三维扫描那会儿,拿到一个点云数据就直接往上怼算法,结果经常跑半天不出结果,或者内存直接爆掉。后来才明白——预处理这一步,省不了

点云数据量通常很大,动辄几十万甚至上百万个点。直接处理,计算量吃不消。所以我们需要采样,说白了就是用更少的点,保留尽可能多的信息。今天讲四种主流方法:体素化降采样、均匀采样、随机采样、最远点采样(FPS)。

核心思想:采样不是随便扔点,而是要在降低数据量的同时,尽量保持原始点云的几何结构和分布特征。

点云预处理采样方法 体素化降采样 均匀网格 + 重心/中心 均匀采样 固定间隔抽取 随机采样 概率抽取 最远点采样 (FPS) 迭代选取最远点 速度:随机 > 均匀 > 体素 > FPS 质量:FPS > 体素 > 均匀 > 随机

4.1 体素化降采样(Voxel Grid Downsampling)

这个方法我用的最多。它的思路很简单:把三维空间切成一个个小立方体(体素),每个体素里只保留一个点。

具体怎么做呢?你设定一个体素大小,比如 0.01m。然后算法会把点云划分到对应的体素网格里。每个体素内,你可以取所有点的重心,或者取离体素中心最近的点。

我个人习惯:取重心比取中心点更稳定。重心能反映体素内点的实际分布,而中心点可能选到边缘的离群点。

import open3d as o3d

# 读取点云
pcd = o3d.io.read_point_cloud("scan.ply")

# 体素化降采样,体素大小 0.02m
downsampled = pcd.voxel_down_sample(voxel_size=0.02)

# 可视化对比
o3d.visualization.draw_geometries([downsampled])

优点:速度快,能均匀降低密度,对噪声有一定抑制作用。
缺点:体素大小不好调。调大了丢失细节,调小了降采样效果不明显。

我曾经踩过的坑:体素大小设成 0.1m,结果一个拳头大小的物体只剩几个点,形状完全没了。后来我学乖了——先看点云的平均点间距,体素大小设为平均间距的 2-3 倍比较稳妥。

4.2 均匀采样(Uniform Sampling)

均匀采样,说白了就是每隔固定步长取一个点。比如每 5 个点取一个,或者每 10 个点取一个。

这个方法在点云有序排列时很好用。但问题是——真实扫描的点云往往不是均匀分布的。靠近扫描仪的地方点密,远处点稀。均匀采样会破坏这种密度信息

import numpy as np

def uniform_sample(points, step=5):
    """均匀采样:每隔 step 个点取一个"""
    return points[::step]

# 使用示例
points = np.asarray(pcd.points)
sampled = uniform_sample(points, step=10)

优点:实现简单,计算量极小。
缺点:对点云顺序敏感,容易丢失稀疏区域的点。

适用场景:点云已经过预处理、分布比较均匀时,均匀采样是个轻量级的好选择。但如果是原始扫描数据,我建议慎用。

4.3 随机采样(Random Sampling)

随机采样,就是随机从点云里抽出一部分点。每个点被选中的概率相等。

你想想看,这方法简单粗暴,但有个问题——随机性太强。可能把关键特征点全漏掉,也可能在密集区域重复采样。

import random

def random_sample(points, num_samples):
    """随机采样:从 points 中随机抽取 num_samples 个点"""
    if num_samples >= len(points):
        return points
    indices = random.sample(range(len(points)), num_samples)
    return points[indices]

# 使用示例
sampled = random_sample(points, 5000)

优点:实现最简单,速度最快。
缺点:不稳定,多次采样结果差异大,容易丢失结构信息。

我建议:随机采样适合做快速预览,或者作为其他算法的初始化步骤。但别指望它保留精细结构。

4.4 最远点采样(Farthest Point Sampling, FPS)

终于讲到 FPS 了。这个方法在深度学习里很常见,比如 PointNet++ 就用它来做下采样。

FPS 的思路是:每次选一个离已选点集最远的点。这样能保证采样点均匀覆盖整个点云空间。

具体步骤:

  1. 随机选一个初始点。
  2. 计算所有点到已选点集的距离,取最小值。
  3. 选距离最小值最大的那个点加入已选点集。
  4. 重复步骤 2-3,直到选够点数。
import numpy as np

def fps(points, num_samples):
    """最远点采样"""
    n = points.shape[0]
    if num_samples >= n:
        return points
    
    # 随机选第一个点
    selected = [np.random.randint(n)]
    distances = np.full(n, np.inf)
    
    for _ in range(1, num_samples):
        # 更新距离
        last = selected[-1]
        dist = np.linalg.norm(points - points[last], axis=1)
        distances = np.minimum(distances, dist)
        
        # 选最远点
        farthest = np.argmax(distances)
        selected.append(farthest)
    
    return points[selected]

# 使用示例
sampled = fps(points, 5000)

优点:采样点分布均匀,能很好保留点云的整体结构。
缺点:计算量大,复杂度 O(n*m),n 是原始点数,m 是采样点数。

我曾经遇到的情况:对一个 50 万点的点云做 FPS 采样到 1 万点,跑了将近 10 秒。后来我改用 KD-Tree 加速距离计算,时间降到了 2 秒以内。所以,大数据量时记得优化一下。

4.5 四种方法对比

方法 速度 均匀性 结构保持 适用场景
体素化降采样 较好 通用预处理
均匀采样 极快 有序点云
随机采样 极快 快速预览
最远点采样 极好 极好 深度学习、精细处理

总结一下我的经验:

  • 日常预处理,体素化降采样是首选,平衡了速度和效果。
  • 如果要做深度学习训练,FPS 是标配,虽然慢但值得。
  • 随机采样和均匀采样,我一般只在调试阶段用,或者数据量实在太大时做快速降维。

嗯,四种方法就讲到这里。每种方法都有自己的脾气,选对了事半功倍。下次你拿到一个点云,不妨先问问自己:我要保留什么?速度优先还是质量优先? 想清楚了再选方法。


专注资料整理