4. 点云降采样

任务目标

实现均匀采样算法,从输入点云中均匀抽取固定数量的点,使采样结果在空间分布上保持均匀,避免局部点云过密或过疏。

核心原理

均匀采样的关键是将三维空间划分为体素网格,每个体素内最多保留一个点。通过遍历所有点并记录其所在体素,仅保留每个体素中第一个遇到的点,即可实现空间均匀分布。该方法不依赖点云密度,采样结果数量由体素尺寸决定,而非固定数量。

完整实现


import numpy as np

def uniform_sample(points, voxel_size):
    """
    均匀采样:将空间划分为体素网格,每个体素保留一个点
    
    参数:
        points: (N, 3) float array, 输入点云坐标
        voxel_size: float, 体素边长
    
    返回:
        sampled: (M, 3) float array, 采样后的点云 (M ≤ N)
    """
    if voxel_size <= 0:
        raise ValueError("voxel_size 必须为正数")
    if points.shape[0] == 0:
        return points.copy()
    
    # 计算每个点所属的体素索引
    voxel_indices = np.floor(points / voxel_size).astype(np.int64)
    
    # 使用字典记录每个体素中第一个出现的点
    seen = {}
    sampled_list = []
    
    for i in range(points.shape[0]):
        key = tuple(voxel_indices[i])
        if key not in seen:
            seen[key] = True
            sampled_list.append(points[i])
    
    return np.array(sampled_list)

def uniform_sample_fixed_count(points, target_count, max_iter=20):
    """
    均匀采样到指定数量:通过二分搜索体素尺寸,使采样点数接近目标值
    
    参数:
        points: (N, 3) float array, 输入点云
        target_count: int, 目标采样点数
        max_iter: int, 最大迭代次数
    
    返回:
        sampled: (M, 3) float array, 采样结果 (M ≈ target_count)
        voxel_size: float, 最终使用的体素尺寸
    """
    if target_count <= 0:
        raise ValueError("target_count 必须为正数")
    if points.shape[0] <= target_count:
        return points.copy(), 0.0
    
    # 计算点云包围盒
    min_bound = points.min(axis=0)
    max_bound = points.max(axis=0)
    diag = np.linalg.norm(max_bound - min_bound)
    
    # 二分搜索体素尺寸
    lo, hi = diag / 1000, diag
    best_voxel = hi
    best_count = 1
    
    for _ in range(max_iter):
        mid = (lo + hi) / 2
        sampled = uniform_sample(points, mid)
        count = sampled.shape[0]
        
        if count > target_count:
            lo = mid          # 体素太大,采样点太少 → 缩小体素
        else:
            hi = mid          # 体素太小,采样点太多 → 增大体素
            if abs(count - target_count) < abs(best_count - target_count):
                best_voxel = mid
                best_count = count
    
    # 用最优体素尺寸做最终采样
    sampled = uniform_sample(points, best_voxel)
    return sampled, best_voxel

运行与验证


# 生成测试数据:两个密度不同的区域
np.random.seed(42)
dense = np.random.uniform([0, 0, 0], [1, 1, 1], size=(5000, 3))
sparse = np.random.uniform([2, 2, 2], [3, 3, 3], size=(500, 3))
points = np.vstack([dense, sparse])

# 执行均匀采样
sampled, voxel = uniform_sample_fixed_count(points, target_count=1000)
print(f"输入点数: {points.shape[0]}")
print(f"采样点数: {sampled.shape[0]}")
print(f"体素尺寸: {voxel:.4f}")

# 验证空间均匀性:统计两个区域的采样点比例
dense_region = ((sampled[:, 0] < 1.5) & (sampled[:, 1] < 1.5) & (sampled[:, 2] < 1.5)).sum()
sparse_region = sampled.shape[0] - dense_region
print(f"密集区采样点数: {dense_region}")
print(f"稀疏区采样点数: {sparse_region}")
print(f"原始比例(密集:稀疏) = 5000:500 = 10:1")
print(f"采样比例 = {dense_region}:{sparse_region}")

通过条件

  • 采样点数接近目标值(偏差在±5%以内)
  • 密集区与稀疏区的采样点比例明显小于原始10:1,表明空间分布更均匀
  • 采样结果中不存在重复点(每个体素仅保留一个点)

关键说明

均匀采样与体素下采样(Voxel Grid)的区别在于:体素下采样用体素内所有点的重心代替该体素,而均匀采样直接保留体素内第一个原始点。前者会生成新点,后者保留原始点坐标。当需要固定输出数量时,使用二分搜索自动确定体素尺寸,避免手动调参。

原始点云 (5500点) 密集区 (5000点) 稀疏区 (500点) 均匀采样结果 (~1000点) 每个体素保留1个点 → 空间均匀