3. 点云预处理:体素滤波降采样、统计滤波去噪、半径滤波去噪

点云数据拿到手,第一件事是什么?

不是急着做配准,也不是急着做分割。我个人的习惯是——先做预处理。

为什么?你想想看,激光雷达扫出来的原始点云,动辄几十万甚至上百万个点。这里面有大量的冗余数据,还有各种噪声点、离群点。直接拿这些数据去跑算法,不仅慢,而且结果往往一塌糊涂。

说白了,预处理就是给点云「洗个澡」,把脏东西洗掉,把多余的水分拧干。今天我们就聊三个最常用的预处理手段:体素滤波、统计滤波、半径滤波。

核心逻辑:降采样解决「点太多」的问题,去噪解决「点太脏」的问题。两者缺一不可。

点云预处理三大核心操作 原始点云(百万级) 体素滤波降采样(减少点数) 统计滤波去噪(去除稀疏离群点) 半径滤波去噪(去除孤立点) 干净、轻量的点云

3.1 体素滤波降采样

体素滤波,说白了就是把三维空间切成一个个小立方体(体素),每个立方体里只保留一个代表点。

我刚开始做点云处理时,总想着保留所有点,生怕丢了信息。后来发现,相邻的点其实长得差不多,保留一个就够了。体素滤波就是干这个的。

工作原理:

  1. 设定体素大小(比如 0.1m × 0.1m × 0.1m)
  2. 把点云划分到各个体素中
  3. 每个体素内计算所有点的重心(或中心),作为代表点

我的经验:体素大小怎么选?室内场景我一般用 0.05m,室外场景用 0.1m-0.2m。太小了降采样效果不明显,太大了会丢失细节。我曾经在一个隧道项目里用了 0.5m 的体素,结果把墙面的细节全抹平了,后来改回 0.15m 才解决问题。

import open3d as o3d

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

# 体素滤波降采样,体素大小 0.1m
voxel_size = 0.1
down_pcd = pcd.voxel_down_sample(voxel_size)

print(f"原始点数: {len(pcd.points)}")
print(f"降采样后点数: {len(down_pcd.points)}")
# 输出示例:原始点数: 1,200,000 → 降采样后点数: 85,000
体素大小 适用场景 点数减少比例
0.05m 室内精细建模 约 70%-80%
0.1m 通用场景 约 85%-95%
0.2m 室外大场景 约 95%-99%

注意:体素滤波会改变点的精确位置(因为用了重心)。如果你的应用对几何精度要求极高(比如工业测量),可以考虑用体素中心代替重心,或者改用随机降采样。

3.2 统计滤波去噪

降采样之后,点云里可能还有一些「野点」——就是那些飘在空中的孤立噪声点。统计滤波就是专门收拾它们的。

核心思想:每个点周围都有邻居。如果一个点离它的邻居太远了,那它大概率是噪声。

具体做法是:

  1. 对每个点,计算它到所有 k 个最近邻的平均距离
  2. 所有点的平均距离会形成一个分布(近似高斯分布)
  3. 设定一个标准差倍数阈值,超过这个阈值的点就被判定为离群点

嗯,这里要注意:k 值(邻居数)和 std_ratio(标准差倍数)是两个关键参数。

import open3d as o3d

# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
    nb_neighbors=20,      # 考虑20个最近邻
    std_ratio=2.0         # 标准差倍数阈值
)

# 提取内点(干净点云)
clean_pcd = down_pcd.select_by_index(ind)

# 提取外点(噪声点云)
outlier_pcd = down_pcd.select_by_index(ind, invert=True)

print(f"内点数量: {len(clean_pcd.points)}")
print(f"噪声点数量: {len(outlier_pcd.points)}")

避坑指南:我曾经在一个户外 LiDAR 数据集上把 std_ratio 设成了 1.0,结果把很多正常的墙面点也删掉了。后来发现,户外场景点云分布比较稀疏,标准差倍数设到 2.0-3.0 更合适。室内场景可以设小一点,1.5-2.0 就够用。

3.3 半径滤波去噪

统计滤波好用,但它有个小毛病——它假设点云密度是均匀的。如果场景里有的地方密、有的地方稀,统计滤波可能会误删。

这时候,半径滤波就派上用场了。

半径滤波的逻辑更直接:以每个点为中心,画一个半径为 r 的球。如果球内的邻居数少于某个阈值,就把这个点删掉。

说白了,就是「你周围没人陪你,那你就是孤魂野鬼,删掉」。

import open3d as o3d

# 半径滤波去噪
clean_pcd_radius, ind = clean_pcd.remove_radius_outlier(
    nb_points=6,          # 半径内至少要有6个点
    radius=0.5            # 搜索半径 0.5m
)

outlier_radius = clean_pcd.select_by_index(ind, invert=True)

print(f"半径滤波后点数: {len(clean_pcd_radius.points)}")
print(f"被删除的孤立点: {len(outlier_radius.points)}")
滤波方法 核心参数 适用场景 优点 缺点
统计滤波 k, std_ratio 密度均匀的场景 自适应强,参数好调 密度不均时可能误删
半径滤波 r, nb_points 密度变化大的场景 物理意义明确,直观 参数需要根据场景调整

我的建议:实际项目中,我通常把两个滤波串联使用。先做统计滤波去掉明显的离群点,再做半径滤波清理残留的孤立点。顺序不要搞反——先粗筛再精筛,效果最好。

3.4 完整预处理流程

把上面三个步骤串起来,就是一个完整的预处理管线:

import open3d as o3d

def preprocess_point_cloud(pcd_path, voxel_size=0.1,
                           nb_neighbors=20, std_ratio=2.0,
                           radius=0.5, min_points=6):
    """
    点云预处理完整流程
    """
    # 1. 读取原始点云
    pcd = o3d.io.read_point_cloud(pcd_path)
    print(f"原始点数: {len(pcd.points)}")

    # 2. 体素滤波降采样
    pcd_down = pcd.voxel_down_sample(voxel_size)
    print(f"降采样后: {len(pcd_down.points)}")

    # 3. 统计滤波去噪
    cl, ind = pcd_down.remove_statistical_outlier(
        nb_neighbors=nb_neighbors, std_ratio=std_ratio
    )
    pcd_stat = pcd_down.select_by_index(ind)

    # 4. 半径滤波去噪
    pcd_final, ind = pcd_stat.remove_radius_outlier(
        nb_points=min_points, radius=radius
    )

    print(f"最终点数: {len(pcd_final.points)}")
    return pcd_final

# 使用示例
clean_cloud = preprocess_point_cloud("raw_cloud.pcd")

小技巧:如果你不确定参数怎么设,可以先可视化一下点云,看看噪声的分布情况。Open3D 的 draw_geometries() 函数可以帮你快速预览。我每次调参前都会先看一眼数据,心里有个底再动手。

好了,这就是点云预处理的三个核心操作。体素滤波负责「减量」,统计滤波和半径滤波负责「提质」。三者配合,才能得到干净、轻量的点云数据,为后续的配准、分割、识别打好基础。

记住一句话:预处理做得好,后面少加班。


公众号:蓝海资料掘金营,微信deep3321