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:
决定大于多少点的聚类被认为是异常大区域。
一句话总结:
欧式聚类就是基于距离阈值的点云连通区域提取算法,适合分割空间上相互分离的点云目标。