Skip to content

三维点云处理(三):点云分割与聚类实战

点云聚类(Clustering)是将无序点云按空间分布和几何特征划分为若干个独立区域(或物体)的过程。在三维点云处理中,它是目标检测、障碍物分类和场景识别的前置核心步骤。

本篇抛开复杂的公式推导,从工程师视角对比 5 种主流聚类算法,并给出基于 DBSCAN 密度聚类的实战代码,同时纠正 Open3D 在处理 DBSCAN 噪声时常见的程序索引 Bug。


1. 5 种主流聚类算法的直观对比与决策指南

算法核心思路优点缺点适用应用场景
K-Means随机选 个中心,按距离划分簇,迭代更新中心。速度快,实现极简。必须提前指定 值;对噪声敏感;只适用于规则的球状分布。均匀分布点云的初级快速切分。
DBSCAN从种子点出发,利用邻域半径 eps 内的最少点数 min_points 沿高密度区域漫延拓展。不需要指定簇数;能处理任何复杂的奇特形状;天然过滤噪声点。大场景下构建检索树耗时;对密度分布极不均匀的数据效果差。最常用的激光雷达(LiDAR)地面障碍物、车辆、行人聚类检测。
GMM假定数据由多个高斯分布混合而成,计算每个点属于各分布的概率。属于“软聚类”,给出概率值;适用于椭球体分布。计算开销大,容易陷入局部极值。统计学建模,用于有重叠区域的片区过渡划分。
MeanShift利用核密度梯度方向,每个点向密度最高的方向(均值漂移)移动,直到收敛。不需要指定簇数;能自适应中心位置。计算复杂度高(),点数多时极慢。小规模点云或特征空间分析。
谱聚类基于图论,计算点与点之间的相似度矩阵,通过特征值分解进行降维后聚类。适用于非常复杂的非凸空间分布。构建相似度矩阵和特征分解非常消耗内存和计算时间。少量关键点的高精度特定细分。

1.2 欧氏聚类 (Euclidean Clustering) 与 DBSCAN 的技术区别

① 欧氏聚类 (距离阈值 r)桥接错误:噪点连通了两个不同物体② DBSCAN 聚类 (半径 eps + 最小点数 min_pts=4)噪点 (-1)成功隔离:噪点密度低,无法传播连通

在点云分割领域,除了 DBSCAN,欧氏距离聚类 (Euclidean Clustering) 也是极其常用的一种算法。两者的核心差异在于对密度的敏感度:

  • 欧氏聚类:利用 KD-Tree 加速近邻检索,通过简单的宽度优先搜索 (BFS) 或深度优先搜索 (DFS),将距离小于设定阈值 的所有点直接划分为同一个簇。
    • 致命局限极易发生“桥接”错误。若两个物体靠得很近,只要中间存在一个细微的噪声点将它们连通,欧氏聚类就会将这两个完全不同的物体强行合并为一个簇。
  • DBSCAN:在距离阈值 eps 的基础上,增加了密度约束 min_points。只有当一个点周围邻域内的点数超过 min_points 时,它才算作“核心点”并允许向外扩张。
    • 优势:即使有零星的噪点连接,只要噪点区域密度达不到门槛,DBSCAN 就不会跨过噪点,从而避免了“桥接”错误,且能自动提取出真正的离群噪声(噪点标签为 -1)。

1.3 地面过滤 (Ground Filtering):聚类前置的“生死步骤”

在自动驾驶(LiDAR 扫描)和地面移动机器人中,雷达数据有一半以上的点属于地面。在进行障碍物聚类前,必须先彻底滤除地面点云。 如果漏掉地面过滤,地面会像一个巨大的“超级连接桥”,将所有的汽车、建筑物、电线杆以及行人和地面连通在一起,使得聚类算法将整个世界聚成一个单一的超大杂乱簇。

常见地面过滤方案:

  1. RANSAC 地面平面拟合
    • 原理:由于大范围路面宏观上可视为一个平面,可提取 Z 轴较低的区域,利用 RANSAC 迭代求解出全局地面平面参数 ,将平面内(局内点)判定为地面并剔除。
    • 局限:无法应对长距离下具有坡度起伏或弯折的复杂路面。
  2. Patchwork / Patchwork++ (区域级高度估计)
    • 原理:将三维点云在 2D 鸟瞰图 (BEV) 空间划分为多个同心圆环(Concentric Rings)和径向扇区。在每个局部小区域内,利用 PCA(最小特征值对应的法向量)自适应拟合局部切平面,实现对起伏地形的精准地面剔除。这是目前 SLAM 和自动驾驶领域的主流开源算法。
  3. 栅格高程图滤波 (Elevation Map Filter)
    • 原理:将空间投影为 2D 像素栅格。在每个栅格 Cell 内统计点的最大高度 和最小高度 。如果高度差 小于设定阈值,且 处于路面高度范围内,则判定该格内所有点为地面。

1.4 工业级激光雷达聚类优化:自适应邻域半径 (Adaptive Eps)

对于激光雷达(LiDAR)而言,其点云分布遵循**“近密远疏”**的物理特性——雷达附近的点密集得连成一片,而 50 米开外的点稀疏得零星见底。

如果 DBSCAN 使用固定的 eps 半径:

  • 如果 eps 设定太小(如 0.2 米):雷达远处的障碍物(由于物理点距已大于 0.2 米)会被切碎为无数个碎片,甚至直接被判定为噪声丢弃。
  • 如果 eps 设定太大(如 1.0 米):雷达近处挨得较近的多个行人,会被直接融合成一整个大物体。

解决方案:自适应多区域聚类 (Multi-Zone Clustering)

在激光雷达工程实践中,我们通常根据点到雷达中心点的距离 ,将场景划分为不同的环状区域(如 0-15m,15-30m,30m+),每个区域采用不同比例缩放的 eps 阈值分别进行 DBSCAN 聚类:

或者利用激光雷达线束投影(Range Image):将三维点云投影为二维深度图(Range Image),在像素空间内根据角度分辨率直接计算相邻光束的弧长作为邻域半径,从而在根源上消除距离对密度聚类的负面影响。


2. DBSCAN 噪声标签 -1 引起的索引越界 Bug 修正

2.1 常见代码缺陷

在 Open3D 中,DBSCAN 聚类函数:

python
labels = pcd.cluster_dbscan(eps=0.02, min_points=10)

返回一个包含每个点所属簇 ID 的列表。DBSCAN 的一大特征是它能识别噪声点(即不属于任何簇的孤立噪点),并将这些噪点的 label 标记为 -1

很多开发者为了快速给不同的簇涂上彩虹色,会写出以下代码:

python
# 错误做法:直接使用 labels 索引颜色数组
colors = plt.get_cmap("tab20")(labels)  # labels 中包含 -1,直接索引导致数组越界或逻辑混乱
pcd.colors = o3d.utility.Vector3dVector(colors[:, :3])

这会导致 -1 索引到颜色数组的最后一个元素,使得“噪声点”和“最后一个聚类簇”染成了相同的颜色,或者在某些固定大小的颜色数组中直接触发 IndexError 越界崩溃。

2.2 正确处理逻辑

物理上,我们应当把所有的噪声点(标签为 -1 的点)单独过滤出来,并染成黑色灰色以示区分,只对正常的聚类簇进行彩色索引渲染:

python
# 1. 计算最大标签数,定义调色板
max_label = labels.max()
colors = plt.get_cmap("tab20")(labels / (max_label if max_label > 0 else 1))

# 2. 强制将标签为 -1 的噪点颜色指定为黑色 [0.0, 0.0, 0.0]
colors[labels < 0] = 0.0

# 3. 写入点云对象
pcd.colors = o3d.utility.Vector3dVector(colors[:, :3])

3. Open3D 实战:DBSCAN 点云聚类完整代码

下面使用 DBSCAN 算法对点云进行空间聚类,自动划分出独立物体并渲染彩色:

python
import matplotlib.pyplot as plt
import numpy as np
import open3d as o3d

# 1. 读入原始点云 (示例使用多硬币/杂乱球体拼接的场景,或自定义 PLY)
# 这里随机生成 3 个不同空间位置的球体来模拟 3 个独立障碍物
pcd1 = o3d.geometry.TriangleMesh.create_sphere(radius=0.3).sample_points_uniformly(1500)
pcd1.translate([1.0, 0.0, 0.0])

pcd2 = o3d.geometry.TriangleMesh.create_sphere(radius=0.4).sample_points_uniformly(2000)
pcd2.translate([0.0, 1.2, 0.0])

pcd3 = o3d.geometry.TriangleMesh.create_sphere(radius=0.2).sample_points_uniformly(1000)
pcd3.translate([-1.0, -0.5, 0.5])

# 合并点云,并加入一些随机背景孤立噪声点
noise_points = np.random.uniform(low=-2.0, high=2.0, size=(100, 3))
pcd_noise = o3d.geometry.PointCloud()
pcd_noise.points = o3d.utility.Vector3dVector(noise_points)

pcd = pcd1 + pcd2 + pcd3 + pcd_noise
print(f"场景总点数 (包含噪声): {len(pcd.points)}")

# ==================== 步骤 A: DBSCAN 聚类核心参数 ====================
# eps: 邻域搜索半径(米)。过大导致物体连在一起,过小导致物体碎开。
# min_points: 构成一个簇所需的最小点数门槛。
eps = 0.25
min_points = 15

# 执行聚类
# labels 长度等于点数,存储每个点所属的分类标签 (0, 1, 2...) 或噪声 (-1)
labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_points, print_progress=True))

# 统计信息
max_label = labels.max()
num_clusters = max_label + 1
print(f"\n聚类完成!自动检测到独立物体(簇)数量: {num_clusters}")
print(f"噪声点数量: {np.sum(labels == -1)}")

# ==================== 步骤 B:  修正着色逻辑 (噪点指定为黑色) ====================
# 获取调色板
cmap = plt.get_cmap("tab20")

# 对普通簇的标签归一化 [0, 1] 方便 cmap 映射
normalized_labels = labels / (max_label if max_label > 0 else 1)
colors = cmap(normalized_labels)[:, :3] # 舍弃 Alpha 通道

# 核心修正:利用布尔掩码,将所有 label == -1 的噪声点涂成纯黑色
colors[labels == -1] = [0.0, 0.0, 0.0]

# 将颜色赋值回点云
pcd.colors = o3d.utility.Vector3dVector(colors)

# ==================== 步骤 C: 可视化结果 ====================
# 运行后会看到 3 个彩色的球状聚类物体,以及散落在空间中的黑色杂乱噪声点
o3d.visualization.draw_geometries(
    [pcd],
    window_name="DBSCAN Point Cloud Clustering"
)

基于 VitePress 强力驱动 | 记录技术与生活