4、点云聚类算法(上):欧几里得聚类、DBSCAN聚类原理、KD-Tree加速、参数调优经验

点云聚类,说白了就是把一堆乱七八糟的点,按照空间分布“物以类聚”。你想想看,激光雷达扫出来的点云,少则几万,多则上百万个点。如果不做聚类,你根本不知道哪个点是车、哪个点是行人、哪个点是路边的垃圾桶。

我个人习惯把聚类看作是感知系统的“第一道筛子”。筛得好,后面的目标跟踪、轨迹预测都轻松;筛不好,后面全是坑。今天咱们就聊聊两种最常用的聚类方法:欧几里得聚类和DBSCAN,顺便把KD-Tree加速和参数调优的经验也一并交代了。

点云聚类知识体系(上) 点云聚类 欧几里得聚类 DBSCAN KD-Tree加速 距离阈值 最小点数 BFS/DFS搜索 Eps邻域 MinPts 核心/边界/噪声点 空间分割 最近邻搜索 O(logN)加速

4.1 欧几里得聚类:最直观的“邻居抱团”

欧几里得聚类的思想特别简单:如果两个点之间的距离小于某个阈值,它们就属于同一个簇。然后不断往外扩散,直到把所有连通的点都找出来。

我在项目中遇到过一个问题:用欧几里得聚类处理高速公路场景,结果一辆大卡车被分成了七八块。为什么?因为卡车侧面太平了,点与点之间的距离稍微大一点,算法就认为它们“不连通”了。后来我把距离阈值调大了一点,才把卡车完整地聚出来。

核心步骤:

  1. 从点云中任选一个未处理的点作为种子
  2. 用KD-Tree搜索该点半径r内的所有邻居
  3. 将邻居加入当前簇,并标记为已处理
  4. 对簇内每个新点重复步骤2-3,直到没有新点加入
  5. 如果簇内点数大于最小点数阈值,保留该簇;否则视为噪声丢弃

说白了,这就是一个广度优先搜索(BFS)的过程。你想想看,每个点都在“拉帮结派”,距离近的就成一伙。

4.2 DBSCAN:密度说了算

DBSCAN和欧几里得聚类最大的区别在于:它不只看距离,还看密度。一个点周围如果足够“热闹”(邻居多),它就是核心点;如果只是偶尔有几个邻居,那就是边界点;如果孤零零的,那就是噪声点。

我记得有一次做园区低速场景的聚类,欧几里得聚类怎么调都搞不定稀疏的行人点云。换DBSCAN一试,效果立竿见影。为什么?因为行人点云稀疏但密度相对均匀,DBSCAN对这种场景天生友好。

DBSCAN的三个关键概念:

  • 核心点:在半径Eps内至少有MinPts个点
  • 边界点:在半径Eps内少于MinPts个点,但属于某个核心点的邻居
  • 噪声点:既不是核心点也不是边界点

DBSCAN的好处是不用预先指定聚类数量,而且能自动识别噪声。但它的缺点也很明显——对参数敏感。Eps和MinPts稍微变一变,结果可能天差地别。

4.3 KD-Tree加速:别让搜索拖后腿

你想想看,如果不用KD-Tree,每次找邻居都要遍历所有点,复杂度是O(N²)。对于10万个点的点云,那就是100亿次距离计算——你的CPU会哭的。

KD-Tree的原理很简单:把三维空间用二叉树的形式分割。每次找邻居时,只需要搜索树的一部分分支,复杂度降到O(N log N)。

我曾经在一个项目里踩过坑:直接用暴力搜索做欧几里得聚类,点云只有2万个点,结果一帧处理了3秒钟。换成KD-Tree之后,同样的数据只用了50毫秒。嗯,这里要注意,KD-Tree的构建本身也有开销,但对于静态场景或者帧间变化不大的情况,可以复用上一帧的树结构。

// 伪代码:KD-Tree加速的欧几里得聚类
function euclideanCluster(points, radius, minPoints):
    kdTree = buildKDTree(points)  // 构建KD-Tree
    visited = [false] * len(points)
    clusters = []
    
    for i in range(len(points)):
        if visited[i]: continue
        queue = [i]
        cluster = []
        visited[i] = true
        
        while queue:
            idx = queue.pop(0)
            cluster.append(idx)
            // KD-Tree快速搜索半径内的邻居
            neighbors = kdTree.radiusSearch(points[idx], radius)
            for n in neighbors:
                if not visited[n]:
                    visited[n] = true
                    queue.append(n)
        
        if len(cluster) >= minPoints:
            clusters.append(cluster)
    
    return clusters

4.4 参数调优经验:纸上得来终觉浅

参数调优这件事,说实话没有银弹。但我可以分享一些实战中总结出来的经验。

参数 典型范围 调优建议
欧几里得距离阈值 0.3m - 1.5m 高速场景偏大(0.8-1.2m),低速场景偏小(0.3-0.6m)
DBSCAN的Eps 0.2m - 1.0m 用k-distance图找拐点,我一般取排序后第MinPts个距离的均值
DBSCAN的MinPts 3 - 10 点云密度高取大值,密度低取小值。行人场景我常用4-5
最小聚类点数 3 - 20 用于过滤噪声,太小会保留孤立点,太大会漏掉小目标

避坑指南:

我曾经在雨雪天气吃过亏。雨滴和雪花在点云中形成大量稀疏的噪声点,如果不做预处理直接聚类,DBSCAN会把它们当成小目标。后来我加了一步“体素滤波+离群点移除”,才把这个问题解决掉。

另外,我个人习惯在调参时先做可视化。把聚类结果用不同颜色画出来,一眼就能看出参数合不合理。别光盯着数字看,视觉反馈往往更直观。

还有一个技巧:对于不同距离的物体,可以用不同的距离阈值。比如近处用0.5m,远处用1.0m。因为激光雷达的点云密度是随距离衰减的,远处点更稀疏,阈值自然要大一些。

我的调参流程:

  1. 先用默认参数跑一遍,看聚类效果
  2. 观察过分割(一个物体被分成多块)还是欠分割(多个物体被合成一块)
  3. 过分割就增大距离阈值或Eps,欠分割就减小
  4. 反复迭代3-5次,基本能找到一个不错的平衡点

好了,关于欧几里得聚类、DBSCAN和KD-Tree加速的核心内容就这些。下一节我们会聊到更高级的聚类方法,比如基于图割的聚类和基于深度学习的点云分割。但今天讲的这些基础方法,在实际工程中依然是最常用的——别小看它们。

可可精品资料库