一、点云预处理技术:让原始数据变得“可用”

做激光雷达点云深度学习,有个残酷的现实我得先告诉你:原始点云数据,基本没法直接用

你想想看,一帧64线激光雷达的数据,动辄十几万个点。这里面有噪声、有离群点、有地面、有天空,甚至还有雨滴和飞虫反射回来的假点。如果直接把这种“脏数据”扔进网络,模型大概率会学歪。

所以,预处理这一步,说白了就是给数据“洗澡”。我做了这么多年点云项目,可以负责任地说:预处理做得好,模型训练事半功倍;预处理偷懒,后面调参调到哭。

今天我们就来聊聊点云预处理的几个核心操作:降采样、去噪、滤波、法向量估计。嗯,一个一个来。

核心观点:预处理不是“可选项”,而是“必选项”。它直接决定了后续特征提取和模型收敛的质量。

1.1 体素滤波降采样:用“格子”来压缩数据

体素滤波,英文叫Voxel Grid Filter。原理特别简单:把空间切成一个个小立方体(体素),每个体素里只保留一个点

这个点怎么选?通常是体素内所有点的重心,或者直接取中心点。我个人习惯用重心,因为能保留更多局部形状信息。

为什么要降采样?原因有两个:

  • 计算量太大——十几万个点,网络根本吃不消
  • 冗余太多——同一平面上的点,其实信息高度重复

体素大小怎么设?我一般设0.05米到0.1米。太小了降采样效果不明显,太大了会把细节磨平。我在做自动驾驶路沿检测时,试过0.2米的体素,结果路沿直接变成了一坨“毛边”,根本没法用。

import open3d as o3d

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

# 体素滤波,体素大小0.05m
down_pcd = pcd.voxel_down_sample(voxel_size=0.05)

print(f"原始点数: {len(pcd.points)}")
print(f"降采样后: {len(down_pcd.points)}")

我的小技巧:体素大小最好根据场景动态调整。比如城区道路用0.05m,高速场景可以放宽到0.1m。别死板套一个参数。

1.2 统计滤波去噪:干掉“孤魂野鬼”

统计滤波,英文Statistical Outlier Removal。它的逻辑是:如果一个点周围邻居太少,那它大概率是噪声

具体做法是:对每个点,计算它到k个最近邻的平均距离。如果这个距离超过全局均值+若干倍标准差,就判定为离群点,直接删掉。

我遇到过最典型的场景:下雨天采集的数据,雨滴反射回来的点就是典型的“孤魂野鬼”。统计滤波一过,干干净净。

参数怎么调?两个关键值:

  • k(邻居数):一般设20-50。太小了容易误删,太大了去噪效果变差
  • std_ratio(标准差倍数):一般设1.0-2.0。值越小,去噪越狠
# 统计滤波去噪
cl, ind = down_pcd.remove_statistical_outlier(
    nb_neighbors=20, 
    std_ratio=2.0
)
clean_pcd = down_pcd.select_by_index(ind)

print(f"去噪前: {len(down_pcd.points)}")
print(f"去噪后: {len(clean_pcd.points)}")

注意:统计滤波对稀疏场景不友好。比如远距离的点,本身邻居就少,容易被误删。我曾经在高速场景吃过这个亏,后来改用半径滤波才解决。

1.3 半径滤波去噪:另一种“社交距离”判断

半径滤波和统计滤波思路类似,但判断标准不同:如果一个点周围指定半径内的邻居数少于阈值,就删掉

说白了,统计滤波看的是“距离分布”,半径滤波看的是“绝对密度”。

什么时候用半径滤波?我个人的经验是:当场景密度不均匀时,半径滤波更靠谱。比如近处点很密、远处点很稀的场景,统计滤波容易把远处的有效点也删掉,但半径滤波只要设好半径和最小点数,就能保留下来。

# 半径滤波去噪
cl, ind = clean_pcd.remove_radius_outlier(
    nb_points=16, 
    radius=0.05
)
final_pcd = clean_pcd.select_by_index(ind)

避坑指南:我曾经在隧道场景里用半径滤波,结果隧道壁上的点被大量误删。后来发现是半径设得太小。记住:半径要大于点云的平均点间距,否则等于没滤波。

1.4 直通滤波:快速“切”出感兴趣区域

直通滤波,PassThrough Filter,是预处理里最简单、最暴力的操作。直接按坐标轴范围切一刀

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

为什么需要直通滤波?因为很多场景下,我们只对特定区域感兴趣。比如自动驾驶,车顶的树叶、远处的天空,这些点对模型训练毫无意义,反而会引入噪声。

import numpy as np

# 手动实现直通滤波
points = np.asarray(final_pcd.points)

# 只保留前方30米,高度-0.5到2.0米
mask = (points[:, 0] > 0) & (points[:, 0] < 30) & \
       (points[:, 2] > -0.5) & (points[:, 2] < 2.0)

filtered_points = points[mask]

经验之谈:直通滤波一定要放在预处理的第一步。先把无关区域切掉,后面的降采样和去噪计算量能减少一半以上。

1.5 点云法向量估计:给每个点“定方向”

法向量,就是点云表面每个点的朝向。它有什么用?法向量是很多高级特征的基础

比如:

  • 地面分割——地面点的法向量基本朝上
  • 边缘检测——法向量突变的地方就是边缘
  • 点云配准——法向量能帮助找到对应点

法向量怎么算?最常用的方法是PCA(主成分分析)。对每个点,找它周围的k个邻居,然后计算这些点的协方差矩阵,最小特征值对应的特征向量就是法向量。

嗯,这里要注意:法向量有方向歧义性。同一个平面,法向量可以朝上也可以朝下。所以一般需要做“法向量定向”,让所有法向量都指向同一个方向(比如朝向视点)。

# 计算法向量
final_pcd.estimate_normals(
    search_param=o3d.geometry.KDTreeSearchParamHybrid(
        radius=0.1, max_nn=30
    )
)

# 查看法向量
normals = np.asarray(final_pcd.normals)
print(f"法向量形状: {normals.shape}")

我的习惯:法向量估计的搜索半径,一般设为体素大小的2-3倍。比如体素0.05m,搜索半径就设0.1-0.15m。这样既能保证邻居数量,又不会跨过物体边界。

1.6 特征计算:从几何到语义的“桥梁”

有了法向量,我们就可以计算更丰富的几何特征了。常见的点云特征包括:

特征名称 含义 典型应用
曲率 表面弯曲程度 角点检测、平面分割
粗糙度 局部表面起伏程度 地面与非地面分类
FPFH 快速点特征直方图 点云配准、物体识别
SHOT 方向直方图特征 3D物体识别

这些特征说白了,就是把点云的“几何形状”翻译成“数字向量”,让深度学习模型能理解。

我个人最常用的是曲率和粗糙度。为什么?因为计算快,而且对大部分任务都够用。FPFH虽然更鲁棒,但计算量太大,实时场景基本扛不住。

# 计算曲率(通过PCA特征值)
from sklearn.decomposition import PCA

def compute_curvature(points, k=30):
    curvatures = []
    for p in points:
        # 找k个最近邻
        # 计算协方差矩阵
        # 特征值分解
        # 曲率 = 最小特征值 / 特征值之和
        pass  # 具体实现略
    return curvatures

总结一下:预处理不是“做完就扔”的步骤。它直接决定了后续模型能学到什么。体素滤波解决数据量问题,统计/半径滤波解决噪声问题,直通滤波解决范围问题,法向量和特征计算解决“语义鸿沟”问题。每一步都踩过坑,才敢说这些经验。

点云预处理技术体系 原始点云数据 第一步:直通滤波(切出ROI区域) 第二步:体素滤波降采样 统计滤波去噪 半径滤波去噪 第四步:法向量估计 + 几何特征计算 高质量训练数据

专注资料整理