点云预处理:从原始数据到可用资产

拿到一坨原始点云,别急着往上怼配准算法。我见过太多新手直接拿原始数据跑 ICP,结果要么跑飞,要么卡死。点云预处理,说白了就是把传感器采回来的「毛坯房」装修成能住人的「精装房」。这一步做不好,后面全是白搭。

今天咱们就聊透这五个核心操作:体素滤波、统计滤波、半径滤波、直通滤波,还有法向量估计。每个我都踩过坑,咱们边讲边聊。

点云预处理流程 原始点云 直通滤波 体素降采样 统计滤波 半径滤波 法向量估计 干净点云 顺序不是死的,但建议先直通 再降采样,最后去噪+法向量

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

激光扫描仪一开,动辄几百万个点。你想想看,一个平面有1000个点和有100个点,对配准精度影响其实不大,但计算量差了一个数量级。体素滤波就是干这个的——在保持形状的前提下大幅减少点数。

原理很简单:把空间切成一个个小立方体(体素),每个体素里只保留一个重心点。体素边长越大,降采样越狠。

关键参数:leaf_size

这个值决定了体素边长。我个人的经验是:对室内场景设 0.01m~0.02m,室外大场景设 0.05m~0.1m。设太小没效果,设太大把特征都磨平了。

import open3d as o3d

pcd = o3d.io.read_point_cloud("raw_scan.ply")
print(f"原始点数: {len(pcd.points)}")

# 体素滤波,leaf_size=0.02m
voxel_pcd = pcd.voxel_down_sample(voxel_size=0.02)
print(f"降采样后: {len(voxel_pcd.points)}")

我的小技巧:降采样后的点云记得保存一份。调试配准参数时,先用降采样后的数据跑,参数调好了再用全量数据做最终结果。能省你大把时间。

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

原始点云里总有些莫名其妙的离群点——可能是飞鸟、可能是多路径反射、也可能是传感器本身的噪声。统计滤波就是用来收拾这些「不守规矩」的点的。

它的逻辑是:每个点周围找 k 个邻居,算平均距离。如果这个距离比全局均值大出好几个标准差,那这个点就是离群点,干掉它。

参数 含义 推荐值
k 邻居点数 20~50
std_ratio 标准差倍数阈值 1.0~2.0
# 统计滤波去噪
cl, ind = voxel_pcd.remove_statistical_outlier(
    nb_neighbors=20, 
    std_ratio=2.0
)
clean_pcd = voxel_pcd.select_by_index(ind)
print(f"去噪后点数: {len(clean_pcd.points)}")

我曾经踩过的坑:std_ratio 设太小(比如 0.5),会把正常的地面点也干掉,点云变得坑坑洼洼。设太大(比如 3.0),离群点又清不干净。我一般从 2.0 开始试,看效果再调。

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

统计滤波看的是「距离分布」,半径滤波看的是「密度」。每个点周围画个球,如果球里的邻居数少于某个阈值,就认为这个点是孤立的,删掉。

什么时候用半径滤波?我个人习惯是:当点云密度不均匀时,半径滤波比统计滤波更稳。比如车载激光雷达扫到的地面点很密,远处物体点很疏——统计滤波容易把远处的正常点误删,半径滤波反而能保留它们。

# 半径滤波去噪
cl, ind = clean_pcd.remove_radius_outlier(
    nb_points=6, 
    radius=0.05
)
final_pcd = clean_pcd.select_by_index(ind)
滤波方式 适用场景 缺点
统计滤波 均匀密度、全局离群点 密度不均时误删
半径滤波 密度不均、局部孤立点 参数对半径敏感

我的建议:两个滤波可以串着用。先统计滤波清掉大离群点,再半径滤波清掉小孤立点。但别过度滤波,点云太稀疏了配准精度反而下降。

4. 直通滤波:一刀切掉不要的区域

直通滤波是预处理的第一步。说白了就是:设定一个坐标轴的范围,范围外的点全部砍掉。

比如你只关心地面以上 2 米内的物体,那就设 z 轴范围 [0, 2]。或者你只想要 x 方向 10~20 米之间的点,那就切一刀。

# 直通滤波:只保留 z 在 0~2m 之间的点
z_mask = (np.asarray(final_pcd.points)[:, 2] > 0) & \
         (np.asarray(final_pcd.points)[:, 2] < 2.0)
filtered_pcd = final_pcd.select_by_index(np.where(z_mask)[0])

为什么先做直通滤波?

因为体素滤波和统计滤波都要遍历所有点。先把无关区域切掉,数据量小了,后面操作就快。我一般拿到点云第一件事就是切 ROI(感兴趣区域)。

5. 点云法向量估计:给点云「指明方向」

法向量是点云最重要的局部特征之一。配准、分割、曲面重建,哪哪都离不开它。法向量估计的核心思路:对每个点,用它的 k 个邻居拟合一个平面,平面的法线就是该点的法向量。

嗯,这里要注意:法向量有方向歧义——同一个平面,法线可以朝上也可以朝下。所以估计完还要做「法向量定向」,让所有法线朝向一致(通常朝向视点方向)。

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

# 定向到视点(默认是原点)
filtered_pcd.orient_normals_towards_camera_location(
    camera_location=np.array([0, 0, 0])
)
参数 含义 影响
radius 搜索半径 半径越大,法向量越平滑
max_nn 最大邻居数 限制计算量,防止过密区域卡死

我曾经翻车的一次经历:在边缘和尖锐棱角处,法向量估计会「打架」。比如桌子的棱边,左边邻居拟合的法线朝左,右边邻居拟合的法线朝右,结果棱边上的法向量忽左忽右。后来我加了双边滤波做平滑,才把这个问题压下去。后面章节会细讲。

小结一下

预处理这五步,顺序很重要。我个人的标准流程是:

  1. 直通滤波——先切 ROI,减少数据量
  2. 体素降采样——均匀稀疏化,保持形状
  3. 统计滤波——清掉大离群点
  4. 半径滤波——补刀小孤立点
  5. 法向量估计——为配准做准备

当然,具体场景可以灵活调整。比如点云已经很干净了,滤波可以跳过。但法向量估计基本是必做的——没有法向量,很多配准算法根本跑不起来。