PCL之欧式聚类

PCL 欧式聚类原理详解

1. 欧式聚类是什么

在 PCL 中,欧式聚类通常指:

cpp 复制代码
pcl::EuclideanClusterExtraction<pcl::PointXYZ>

它的作用是:

根据点与点之间的空间距离,把点云划分成若干个独立的点集。

简单来说,如果一堆点在空间上靠得比较近,就认为它们属于同一个物体;如果两堆点之间距离比较远,就认为它们属于不同物体。

例如点云中有三个彼此分开的物体:

text 复制代码
物体 A        物体 B        物体 C
ooooo        ooooo        ooooo
ooooo        ooooo        ooooo

欧式聚类之后,会得到三个聚类:

text 复制代码
cluster 1 = 物体 A 的点
cluster 2 = 物体 B 的点
cluster 3 = 物体 C 的点

2. 欧式聚类的基本思想

欧式聚类的基本思想是:

如果两个点之间的欧式距离小于某个阈值,就认为它们在空间上是邻近的。

对于三维点:

cpp 复制代码
p_i = (x_i, y_i, z_i)
p_j = (x_j, y_j, z_j)

它们之间的欧式距离为:

d(pi,pj)=(xi−xj)2+(yi−yj)2+(zi−zj)2 d(p_i, p_j) = \sqrt{ (x_i - x_j)^2 + (y_i - y_j)^2 + (z_i - z_j)^2 } d(pi,pj)=(xi−xj)2+(yi−yj)2+(zi−zj)2

如果:

d(pi,pj)<r d(p_i, p_j) < r d(pi,pj)<r

则认为点 pip_ipi 和点 pjp_jpj 是邻近点。

其中:

cpp 复制代码
r = cluster_tolerance

也就是 PCL 中的:

cpp 复制代码
ec.setClusterTolerance(r);

这个参数是欧式聚类中最重要的参数。


3. 欧式聚类的直观理解

可以把每个点想象成一个小球,球的半径为:

cpp 复制代码
cluster_tolerance

如果两个点之间的距离小于这个阈值,就认为这两个点可以连接起来。

例如:

text 复制代码
p1 ---- p2 ---- p3 ---- p4

即使 p1p_1p1 和 p4p_4p4 的距离比较远,只要中间存在连续的邻近关系:

text 复制代码
p1 和 p2 距离较近
p2 和 p3 距离较近
p3 和 p4 距离较近

那么 p1p_1p1、p2p_2p2、p3p_3p3、p4p_4p4 都会被划分到同一个聚类中。

所以欧式聚类判断的不是:

一个聚类内部所有点两两之间都必须距离很近。

而是:

只要点与点之间能够通过邻近关系连通,就属于同一个聚类。


4. 欧式聚类的本质:图的连通域搜索

欧式聚类本质上可以看成图的连通域搜索。

可以把点云看成一个图:

text 复制代码
每一个点:图中的一个节点
点与点之间距离小于阈值:两个节点之间存在一条边

如果:

d(pi,pj)<r d(p_i, p_j) < r d(pi,pj)<r

那么可以认为 pip_ipi 和 pjp_jpj 之间存在一条连接边。

这样,点云就可以被转换成一个图结构。

例如:

text 复制代码
p1 --- p2 --- p3        p4 --- p5 --- p6

这个图中有两个连通区域:

text 复制代码
连通区域 1:p1, p2, p3
连通区域 2:p4, p5, p6

因此,欧式聚类的本质是:

基于距离邻接关系的连通域提取。


5. 为什么需要 KdTree

如果要判断某个点附近有哪些邻近点,最直接的方法是暴力搜索。

假设点云中有 NNN 个点,对某个点 pip_ipi,暴力搜索过程如下:

cpp 复制代码
for 每一个点 pj:
    计算 pi 和 pj 的距离
    if distance(pi, pj) < r:
        pj 是 pi 的邻近点

一次搜索需要遍历所有点,复杂度约为:

O(N) O(N) O(N)

而欧式聚类中,几乎每个点都需要查找邻居点,因此整体复杂度可能接近:

O(N2) O(N^2) O(N2)

当点云数量很大时,这种方式非常慢。

所以 PCL 中一般使用 KdTree 来加速邻域搜索:

cpp 复制代码
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(
    new pcl::search::KdTree<pcl::PointXYZ>
);

tree->setInputCloud(cloud);

KdTree 的作用是:

快速找到某个点半径 rrr 内的所有邻近点。

在 PCL 中常见的搜索方式是:

cpp 复制代码
tree->radiusSearch(point, radius, indices, distances);

6. 欧式聚类的整体流程

PCL 欧式聚类的整体流程可以概括为:

text 复制代码
1. 输入点云
2. 建立 KdTree 空间搜索结构
3. 初始化所有点为未访问状态
4. 从一个未访问点开始,作为新的聚类种子点
5. 搜索该点半径 r 内的邻近点
6. 将未访问过的邻近点加入当前聚类
7. 继续对新加入的点进行邻域搜索
8. 不断扩展,直到当前聚类不能继续增长
9. 保存当前聚类
10. 继续寻找下一个未访问点
11. 直到所有点都被处理完成

7. 算法流程详解

7.1 初始化访问标记

首先,为点云中的每个点设置一个访问标记。

可以理解为:

cpp 复制代码
processed[i] = false;

其中:

text 复制代码
processed[i] = false 表示第 i 个点还没有被处理
processed[i] = true  表示第 i 个点已经被归入某个聚类

7.2 选择一个未访问点作为种子点

遍历点云,找到一个还没有被访问的点 pip_ipi。

将它作为一个新聚类的起始点:

text 复制代码
当前聚类 cluster = {p_i}

同时将它标记为已访问:

cpp 复制代码
processed[i] = true;

7.3 搜索种子点的邻近点

使用 KdTree 搜索点 pip_ipi 周围半径 rrr 内的邻近点。

也就是寻找所有满足:

d(pi,pj)<r d(p_i, p_j) < r d(pi,pj)<r

的点 pjp_jpj。

在 PCL 中,对应操作类似:

cpp 复制代码
tree->radiusSearch(p_i, r, neighbor_indices, neighbor_distances);

搜索得到的结果是:

text 复制代码
neighbor_indices = 半径 r 内邻近点的索引
neighbor_distances = 邻近点到查询点的距离平方

注意,PCL 中返回的通常是距离平方,而不是距离本身。


7.4 将邻近点加入当前聚类

对于搜索得到的每一个邻近点,如果它还没有被访问过,则执行:

text 复制代码
1. 标记为已访问
2. 加入当前 cluster
3. 作为新的种子点继续搜索

也就是说,聚类会不断向外扩展。

例如:

text 复制代码
p0 的邻近点有:p1, p2
p1 的邻近点有:p3, p4
p2 的邻近点有:p5

最终得到:

text 复制代码
cluster = {p0, p1, p2, p3, p4, p5}

7.5 使用 BFS 或 DFS 扩展聚类

欧式聚类的扩展过程可以用 BFS 或 DFS 实现。

以 BFS 为例,可以使用一个队列:

cpp 复制代码
std::queue<int> queue;

流程如下:

text 复制代码
1. 将种子点放入队列
2. 当队列不为空时:
   1. 取出队首点
   2. 搜索该点半径 r 内的邻近点
   3. 将未访问过的邻近点加入队列
   4. 将这些点加入当前聚类

这样可以保证所有与种子点连通的点都会被找到。


8. 欧式聚类伪代码

欧式聚类的伪代码如下:

cpp 复制代码
for 每一个点 i:
    if processed[i] == true:
        continue;

    创建一个新的 cluster;
    创建一个队列 queue;

    将 i 加入 queue;
    processed[i] = true;

    while queue 不为空:
        current = queue.front();
        queue.pop();

        将 current 加入 cluster;

        使用 KdTree 搜索 current 半径 r 内的邻近点 neighbors;

        for 每一个 neighbor:
            if processed[neighbor] == false:
                processed[neighbor] = true;
                queue.push(neighbor);

    if cluster.size() 满足范围:
        保存 cluster;

其中,聚类点数范围由下面两个参数控制:

cpp 复制代码
ec.setMinClusterSize(min_size);
ec.setMaxClusterSize(max_size);

9. PCL 中的典型代码

下面是 PCL 中使用欧式聚类的典型代码:

cpp 复制代码
#include <iostream>
#include <vector>

#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/search/kdtree.h>
#include <pcl/segmentation/extract_clusters.h>

void euclideanCluster(
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud)
{
    if (!cloud || cloud->empty())
    {
        std::cout << "Input cloud is empty." << std::endl;
        return;
    }

    // 1. 创建 KdTree 搜索结构
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(
        new pcl::search::KdTree<pcl::PointXYZ>
    );

    tree->setInputCloud(cloud);

    // 2. 保存聚类结果
    std::vector<pcl::PointIndices> cluster_indices;

    // 3. 创建欧式聚类对象
    pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;

    // 距离阈值,单位与点云坐标单位一致
    ec.setClusterTolerance(2.0);

    // 最小聚类点数
    ec.setMinClusterSize(50);

    // 最大聚类点数
    ec.setMaxClusterSize(100000);

    // 设置搜索方法
    ec.setSearchMethod(tree);

    // 设置输入点云
    ec.setInputCloud(cloud);

    // 执行聚类
    ec.extract(cluster_indices);

    // 4. 根据索引提取每一个聚类
    int cluster_id = 0;

    for (const auto& indices : cluster_indices)
    {
        pcl::PointCloud<pcl::PointXYZ>::Ptr cluster(
            new pcl::PointCloud<pcl::PointXYZ>
        );

        for (int index : indices.indices)
        {
            cluster->push_back((*cloud)[index]);
        }

        cluster->width = static_cast<uint32_t>(cluster->size());
        cluster->height = 1;
        cluster->is_dense = true;

        std::cout << "cluster " << cluster_id
                  << " size = " << cluster->size()
                  << std::endl;

        cluster_id++;
    }
}

10. 关键参数解释

PCL 欧式聚类中最关键的三个参数是:

cpp 复制代码
ec.setClusterTolerance(...);
ec.setMinClusterSize(...);
ec.setMaxClusterSize(...);

11. cluster_tolerance 参数

cpp 复制代码
ec.setClusterTolerance(2.0);

这个参数表示聚类时的距离阈值。

如果两个点之间的距离小于该阈值,就认为它们可以连接在一起。

也就是:

d(pi,pj)<r d(p_i, p_j) < r d(pi,pj)<r

其中:

text 复制代码
r = cluster_tolerance

如果点云单位是毫米,那么:

cpp 复制代码
ec.setClusterTolerance(2.0);

表示距离阈值为 2mm2mm2mm。

如果点云单位是米,那么:

cpp 复制代码
ec.setClusterTolerance(0.002);

表示距离阈值为 2mm2mm2mm。

11.1 cluster_tolerance 太小

如果 cluster_tolerance 设置得太小,一个完整物体可能会被分裂成多个小聚类。

例如:

text 复制代码
原本应该是一个物体:

oooooooooooooooo

由于阈值太小,可能被分成:

oooo   oooo   oooo   oooo

11.2 cluster_tolerance 太大

如果 cluster_tolerance 设置得太大,两个本来分开的物体可能会被粘在一起。

例如:

text 复制代码
物体 A        物体 B

oooooo        oooooo

如果距离阈值过大,可能得到:

text 复制代码
cluster = 物体 A + 物体 B

因此,cluster_tolerance 是欧式聚类中最需要重点调试的参数。


12. MinClusterSize 参数

cpp 复制代码
ec.setMinClusterSize(50);

这个参数表示一个有效聚类至少需要包含多少个点。

如果一个聚类的点数小于该值,就会被过滤掉。

它常用于去除:

text 复制代码
孤立噪声点
小碎片
反光产生的少量异常点
无效小区域

例如:

cpp 复制代码
ec.setMinClusterSize(50);

表示少于 50 个点的聚类不会被保留。


13. MaxClusterSize 参数

cpp 复制代码
ec.setMaxClusterSize(100000);

这个参数表示一个有效聚类最多允许包含多少个点。

如果某个聚类点数超过该值,也会被过滤掉。

它常用于去除:

text 复制代码
背景大平面
异常大区域
未去除的桌面或墙面

不过在很多工程场景中,如果前面已经做了 ROI 裁剪、平面分割或背景去除,MaxClusterSize 可以设置得相对大一些。


14. 为什么欧式聚类前通常要预处理

欧式聚类只根据距离进行分组,它并不知道哪些点是目标,哪些点是背景。

如果目标物体和背景点云在空间上连接在一起,那么欧式聚类可能会把它们分到同一个 cluster 中。

例如:

text 复制代码
物体点云与桌面点云接触

如果不先去掉桌面,欧式聚类可能得到:

text 复制代码
cluster = 物体 + 桌面

因此,常见的处理流程是:

text 复制代码
原始点云
  ↓
去除无效点
  ↓
直通滤波 PassThrough
  ↓
体素滤波 VoxelGrid
  ↓
平面分割 RANSAC
  ↓
去除背景平面
  ↓
欧式聚类
  ↓
得到目标点云

15. 欧式聚类和 RANSAC 的区别

欧式聚类和 RANSAC 经常一起使用,但它们解决的问题不同。

15.1 RANSAC

RANSAC 是一种模型拟合方法。

它关心的是:

哪些点满足某个几何模型?

例如:

text 复制代码
直线
平面
圆
圆柱

在 PCL 中常见模型包括:

cpp 复制代码
pcl::SACMODEL_LINE
pcl::SACMODEL_PLANE
pcl::SACMODEL_CIRCLE2D
pcl::SACMODEL_CYLINDER

15.2 欧式聚类

欧式聚类是一种空间连通区域分割方法。

它关心的是:

哪些点在空间距离上连在一起?

它不关心点云是否满足某种几何形状。


15.3 二者区别总结

text 复制代码
RANSAC:
    根据几何模型找点,例如找平面、直线、圆。

欧式聚类:
    根据空间距离找点,例如找一块连续的点云区域。

在实际工程中,经常是:

text 复制代码
先用 RANSAC 去掉背景平面
再用欧式聚类提取独立目标
最后对目标 cluster 做直线、圆、平面等拟合

16. 欧式聚类的优点

欧式聚类的优点包括:

text 复制代码
1. 原理简单,容易理解
2. 参数较少,主要调 cluster_tolerance
3. 对空间分离明显的物体效果好
4. 配合 KdTree 后速度较快
5. 适合点云目标分割、噪声去除、独立区域提取

17. 欧式聚类的局限

欧式聚类也有一些明显局限。

17.1 对距离阈值敏感

如果距离阈值设置不合适,会出现:

text 复制代码
阈值太小:一个物体被分裂
阈值太大:多个物体被合并

17.2 不适合密度变化很大的点云

欧式聚类使用固定距离阈值。

如果点云密度变化很大,例如:

text 复制代码
有的地方点很密
有的地方点很稀

那么固定的 cluster_tolerance 可能不适合所有区域。


17.3 不能理解物体形状

欧式聚类只根据距离判断连通性。

如果两个不同物体距离很近,甚至接触在一起,那么它们可能会被划分为同一个聚类。


18. 参数调试建议

18.1 先确认点云单位

首先要确认点云坐标单位是米还是毫米。

如果点云单位是毫米,可以从下面参数开始尝试:

cpp 复制代码
ec.setClusterTolerance(1.0);
ec.setClusterTolerance(2.0);
ec.setClusterTolerance(5.0);

如果点云单位是米,可以从下面参数开始尝试:

cpp 复制代码
ec.setClusterTolerance(0.001);
ec.setClusterTolerance(0.002);
ec.setClusterTolerance(0.005);

18.2 根据点间距设置阈值

假设点云的平均点间距为:

davg d_{avg} davg

那么可以让聚类距离阈值从下面范围开始尝试:

r=2davg∼4davg r = 2d_{avg} \sim 4d_{avg} r=2davg∼4davg

其中:

  • rrr 是 cluster_tolerance
  • davgd_{avg}davg 是点云平均点间距。

例如,若平均点间距约为 0.5mm0.5mm0.5mm,可以先尝试:

cpp 复制代码
ec.setClusterTolerance(1.0);
ec.setClusterTolerance(1.5);
ec.setClusterTolerance(2.0);

18.3 根据目标间距限制阈值

假设两个不同目标之间的最小间距为:

dobject d_{object} dobject

为了避免两个目标被合并,一般应满足:

r<dobject r < d_{object} r<dobject

如果希望更加稳妥,可以取:

r≤0.5dobject r \le 0.5d_{object} r≤0.5dobject


18.4 根据噪声规模设置 MinClusterSize

如果噪声点通常只有几个点到几十个点,可以设置:

cpp 复制代码
ec.setMinClusterSize(30);

如果只关心较大的目标区域,可以设置:

cpp 复制代码
ec.setMinClusterSize(100);

如果处理的是线扫轮廓点,单个有效区域点数可能本来就不多,那么 MinClusterSize 不宜设置过大。


19. 选择最大聚类的工程代码

实际工程中,很多时候只需要保留最大的 cluster。

例如在去除小噪声、保留主体点云时,可以使用下面代码:

cpp 复制代码
#include <iostream>
#include <algorithm>
#include <vector>

#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/search/kdtree.h>
#include <pcl/segmentation/extract_clusters.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr getLargestCluster(
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud,
    double tolerance,
    int min_size,
    int max_size)
{
    pcl::PointCloud<pcl::PointXYZ>::Ptr result(
        new pcl::PointCloud<pcl::PointXYZ>
    );

    if (!cloud || cloud->empty())
    {
        return result;
    }

    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(
        new pcl::search::KdTree<pcl::PointXYZ>
    );

    tree->setInputCloud(cloud);

    std::vector<pcl::PointIndices> cluster_indices;

    pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
    ec.setClusterTolerance(tolerance);
    ec.setMinClusterSize(min_size);
    ec.setMaxClusterSize(max_size);
    ec.setSearchMethod(tree);
    ec.setInputCloud(cloud);
    ec.extract(cluster_indices);

    if (cluster_indices.empty())
    {
        return result;
    }

    auto max_iter = std::max_element(
        cluster_indices.begin(),
        cluster_indices.end(),
        [](const pcl::PointIndices& a, const pcl::PointIndices& b)
        {
            return a.indices.size() < b.indices.size();
        }
    );

    for (int index : max_iter->indices)
    {
        result->push_back((*cloud)[index]);
    }

    result->width = static_cast<uint32_t>(result->size());
    result->height = 1;
    result->is_dense = true;

    return result;
}

调用方式示例:

cpp 复制代码
auto main_cluster = getLargestCluster(
    cloud,
    2.0,
    50,
    100000
);

如果点云单位是米,而希望距离阈值为 2mm2mm2mm,则应该写成:

cpp 复制代码
auto main_cluster = getLargestCluster(
    cloud,
    0.002,
    50,
    100000
);

20. 欧式聚类在点云处理中的典型位置

在点云工程处理中,欧式聚类通常不会直接作用在原始点云上,而是放在预处理之后。

典型流程如下:

text 复制代码
原始点云
  ↓
去除无效点,例如 (0, 0, 0)
  ↓
ROI 裁剪
  ↓
滤波去噪
  ↓
欧式聚类
  ↓
选择目标 cluster
  ↓
后续几何拟合

例如对于管材轮廓、标定板点云或焊缝点云,可以使用欧式聚类完成:

text 复制代码
1. 去除离散噪声点
2. 提取主体点云
3. 分离多个独立区域
4. 为后续直线拟合、平面拟合、圆拟合提供干净数据

21. 欧式聚类总结

PCL 欧式聚类的本质是:

使用 KdTree 快速查找每个点半径范围内的邻居点,然后通过 BFS 或 DFS 找出空间上的连通区域。

它的核心判断条件是:

d(pi,pj)<r d(p_i, p_j) < r d(pi,pj)<r

其中:

  • pip_ipi 和 pjp_jpj 是两个点;
  • d(pi,pj)d(p_i, p_j)d(pi,pj) 是两个点之间的欧式距离;
  • rrr 是距离阈值,也就是 cluster_tolerance

三个关键参数是:

cpp 复制代码
ec.setClusterTolerance(r);
ec.setMinClusterSize(min_size);
ec.setMaxClusterSize(max_size);

可以这样理解:

text 复制代码
cluster_tolerance:
    决定点与点之间能不能连接。

MinClusterSize:
    决定小于多少点的聚类被认为是噪声。

MaxClusterSize:
    决定大于多少点的聚类被认为是异常大区域。

一句话总结:

欧式聚类就是基于距离阈值的点云连通区域提取算法,适合分割空间上相互分离的点云目标。

相关推荐
嘉子的秃头日记1 小时前
TRO 2026|轮椅也能“猜到”用户想往哪走?
大数据·人工智能·机器学习
Hali_Botebie2 小时前
非共轭先验(Non-conjugate Prior)和共轭先验(Conjugate Prior)
人工智能·机器学习
xrgs_shz3 小时前
基于K-Means聚类分析的鸢尾花分类
人工智能·机器学习
卡梅德生物科技小能手3 小时前
卡梅德生物科普:MAPT(微管相关蛋白Tau)
人工智能·经验分享·机器学习
AI数据皮皮侠4 小时前
全国高考报名、录取数据(1977-2026)
大数据·数据库·人工智能·python·机器学习·高考
码路星球4 小时前
我把 K-Means 的每一步都画了出来:分配、更新、收敛,外加几个工程踩坑
机器学习
踩着两条虫4 小时前
VTJ.PRO v2.4.2 私有化部署与升级实操指南
前端·人工智能·低代码·架构·数据挖掘
春日见4 小时前
决策规划控制面经汇总
人工智能·深度学习·算法·机器学习·自动驾驶
逻辑君5 小时前
Foresight研究报告【20260023】
人工智能·深度学习·机器学习·数学建模
Litluecat5 小时前
配合多角色提示语,学习AI漫剧(刚开始学)
人工智能·学习·机器学习·ai·提示词·漫剧