点云预处理:从原始数据到可用特征

大家好,我是你们的老朋友。今天我们来聊聊点云预处理。说实话,很多刚入行的朋友拿到点云数据就直接往上怼分割算法,结果效果惨不忍睹。我当年也犯过这个错——有一次项目赶进度,跳过预处理直接跑分割,结果出来的结果连我自己都看不下去。后来我学乖了:预处理做得好,后续工作省一半力气。

点云预处理说白了就是给原始数据「洗个澡」。激光雷达扫出来的点云,里面啥都有:噪声点、离群点、冗余点,还有各种乱七八糟的干扰。我们要做的,就是把这些「脏东西」清理干净,同时把有用的特征提取出来。

点云预处理知识体系 原始点云数据 降采样 体素滤波 均匀采样 去噪 统计滤波 半径滤波 特征提取 直通滤波 法向量估计 干净、轻量、带特征的点云 降采样 去噪 特征提取

1. 体素滤波降采样:给点云「减肥」

先说说体素滤波。你想想看,激光雷达一秒钟能扫几十万个点,这么多点全拿来算,你的显卡受得了吗?反正我的笔记本曾经因为点云太密直接蓝屏了...所以第一步,降采样。

体素滤波的原理其实很简单:把空间切成一个个小立方体(体素),每个立方体里只保留一个点。这个点可以是重心,也可以是中心点。我个人习惯用重心,因为能更好地保留原始形状。

核心参数:体素大小(leaf size)。这个值设得越大,点云越稀疏。一般我设0.05-0.1米,具体看你的场景。
import open3d as o3d

# 读取点云
pcd = o3d.io.read_point_cloud("scene.pcd")
print(f"原始点数:{len(pcd.points)}")

# 体素滤波降采样
voxel_size = 0.05  # 5厘米体素
down_pcd = pcd.voxel_down_sample(voxel_size)
print(f"降采样后点数:{len(down_pcd.points)}")

# 可视化对比
o3d.visualization.draw_geometries([pcd], window_name="原始点云")
o3d.visualization.draw_geometries([down_pcd], window_name="降采样后")
我的小技巧:体素大小不要一刀切。如果场景里有小物体(比如杯子),体素设太大就把细节抹掉了。我一般先试0.03,看效果再调。

2. 统计滤波去噪:干掉「飘在空中的点」

降采样之后,接下来就是去噪。统计滤波是我最常用的去噪方法之一。它干的事情很简单:每个点周围找K个邻居,算平均距离。如果这个距离太大,说明它是离群点,干掉它。

为什么会这样?因为真实物体表面的点,邻居距离都比较均匀。而那些噪声点——比如飞鸟、扬尘——它们周围空荡荡的,距离一下子就大了。

# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
    nb_neighbors=20,      # 邻居点数
    std_ratio=2.0         # 标准差倍数阈值
)
clean_pcd = down_pcd.select_by_index(ind)

# 被剔除的噪声点
noise_pcd = down_pcd.select_by_index(ind, invert=True)

print(f"保留的点数:{len(clean_pcd.points)}")
print(f"剔除的噪声点数:{len(noise_pcd.points)}")
注意:std_ratio设太小会把正常点当噪声删掉。我曾经在一个项目里设了1.0,结果把墙角的点全删了,分割出来的墙缺了一大块...后来改成2.0就正常了。

3. 半径滤波去噪:另一种思路

半径滤波和统计滤波思路不同。它是在每个点周围画个球,如果球里的邻居太少,就认为这个点是孤立的,删掉。

什么时候用半径滤波?我个人的经验是:当你的点云密度不均匀时,半径滤波比统计滤波更靠谱。比如室内场景,近处点密、远处点稀,统计滤波容易把远处的正常点误删。

# 半径滤波去噪
clean_pcd_r = clean_pcd.remove_radius_outlier(
    nb_points=16,         # 半径内至少需要的点数
    radius=0.05           # 搜索半径(米)
)[0]
对比总结:
方法 适用场景 关键参数
统计滤波 密度均匀的场景 nb_neighbors, std_ratio
半径滤波 密度不均匀的场景 nb_points, radius

4. 直通滤波:快速「切」出感兴趣区域

直通滤波,说白了就是「切一刀」。你只想要某个范围内的点,比如地面以上1米到2米之间的区域,那就设个范围,把外面的全扔掉。

这个操作在自动驾驶里特别常用。比如我只想看行人,那就把地面以下和头顶以上的点全切掉。速度快,效果好。

import numpy as np

# 将点云转为numpy数组
points = np.asarray(clean_pcd.points)

# 直通滤波:只保留Z轴在0.5到2.0米之间的点
z_min, z_max = 0.5, 2.0
mask = (points[:, 2] >= z_min) & (points[:, 2] <= z_max)
filtered_points = points[mask]

# 创建新的点云对象
filtered_pcd = o3d.geometry.PointCloud()
filtered_pcd.points = o3d.utility.Vector3dVector(filtered_points)

print(f"直通滤波后点数:{len(filtered_pcd.points)}")
经验之谈:直通滤波可以组合使用。比如先切Z轴,再切X轴,就能快速提取出「前方10米、高度1.5米」这个区域。我经常用它来做ROI提取。

5. 点云法向量估计:给点云「长毛」

最后一步,法向量估计。法向量是什么?就是每个点所在表面的朝向。有了法向量,你才能做分割、识别、配准这些高级操作。

法向量估计的原理:找每个点的K个邻居,拟合一个平面,平面的法线就是法向量。嗯,这里要注意:法向量的方向是模糊的,需要做定向处理。

# 法向量估计
filtered_pcd.estimate_normals(
    search_param=o3d.geometry.KDTreeSearchParamHybrid(
        radius=0.1,       # 搜索半径
        max_nn=30         # 最大邻居数
    )
)

# 可视化法向量
o3d.visualization.draw_geometries(
    [filtered_pcd],
    point_show_normal=True,
    window_name="法向量可视化"
)

# 获取法向量数据
normals = np.asarray(filtered_pcd.normals)
print(f"法向量形状:{normals.shape}")  # (N, 3)
避坑指南:法向量估计对点云密度很敏感。太稀疏的地方,拟合出来的平面不靠谱。我建议在法向量估计之前,先做一次体素滤波让密度均匀化。另外,边缘点的法向量往往不准,后续处理时要注意。

完整预处理流程

好了,上面这些步骤串起来,就是一个完整的预处理管线。我一般按这个顺序来:

  1. 体素滤波降采样 — 先减肥,减少计算量
  2. 统计滤波去噪 — 干掉明显的离群点
  3. 半径滤波去噪 — 补刀,处理密度不均的情况
  4. 直通滤波 — 切出感兴趣区域
  5. 法向量估计 — 提取几何特征
def preprocess_point_cloud(pcd, voxel_size=0.05):
    """完整的点云预处理管线"""
    # 1. 降采样
    pcd_down = pcd.voxel_down_sample(voxel_size)
    
    # 2. 统计滤波
    pcd_clean, _ = pcd_down.remove_statistical_outlier(20, 2.0)
    
    # 3. 半径滤波
    pcd_clean = pcd_clean.remove_radius_outlier(16, 0.05)[0]
    
    # 4. 直通滤波(Z轴)
    points = np.asarray(pcd_clean.points)
    mask = (points[:, 2] >= 0.3) & (points[:, 2] <= 3.0)
    pcd_filtered = o3d.geometry.PointCloud()
    pcd_filtered.points = o3d.utility.Vector3dVector(points[mask])
    
    # 5. 法向量估计
    pcd_filtered.estimate_normals(
        search_param=o3d.geometry.KDTreeSearchParamHybrid(0.1, 30)
    )
    
    return pcd_filtered

# 一键预处理
processed_pcd = preprocess_point_cloud(pcd)
print(f"预处理完成,最终点数:{len(processed_pcd.points)}")

说实话,这套流程我用了好几年,在室内场景、室外场景、机器人导航这些项目里都验证过。当然,具体参数要根据你的数据微调。比如激光雷达型号不同,点云密度差很多,体素大小和滤波参数都得跟着变。

嗯,今天就先聊到这儿。预处理这块看似基础,但真的决定了后续算法的上限。下次你们跑分割模型效果不好,先别急着调网络结构,回头看看预处理有没有做好——我敢打赌,一半的问题出在这里。

一句话总结:预处理做得好,点云分割成功一半。

专注资料整理