PCL点云欧式聚类分割原理与参数调优实战
发布时间:2026/10/14 13:32:41 锦皓数字建站

1. 欧式聚类是什么为什么我建议你从它开始学分割点云分割一直是三维视觉里绕不开的坎。无论是机器人抓取、AGV避障还是室内场景重建第一步往往不是做识别而是先把一堆密密麻麻的三维点“切开”——把属于同一个物体的点归拢到一起把背景和杂点剔掉。在 PCLPoint Cloud Library里入门门槛最低、使用频率最高、调参最直观的分割方法就是欧式聚类分割Euclidean Cluster Extraction。这玩意儿到底干了件什么事说白了就是一句话挨个检查每个点看它周围一定距离范围内有没有“邻居”。如果有点离它足够近就归到同一个组里如果周围空荡荡的就认为它是独立的一簇。这个过程不断重复直到所有点都被遍历完最终得到一堆“簇”每一簇通常就对应着一个潜在的目标物体。它的适用场景非常明确目标物体在空间上是彼此分离的。比如地上散落的零部件、货架上的盒子、道路上的行人或车辆。只要物体之间没有物理接触欧式聚类就能把它们干净利落地分开。而如果两个物体靠在一起、粘连严重那这个方法就会力不从心这时候要考虑区域生长分割或者模型拟合。为什么我建议初学者从这个方法入手因为它的原理足够简单不涉及复杂的数学推导。你不需要理解概率图模型不需要背特征描述子的公式只需要掌握两个核心概念KdTree 空间搜索和距离阈值判断。而且 PCL 已经把这些封装成了现成接口十几行代码就能出一个能看到效果的可视化程序。等你把欧式聚类的调参逻辑吃透了再去看超体聚类、RANSAC 拟合这些进阶方法思路会顺很多。2. 原理拆解KdTree 加速搜索与区域生长逻辑2.1 从暴力搜索到 KdTree为什么必须用空间索引欧式聚类的朴素实现思路很直接随便取一个点然后遍历点云里所有其他点看谁跟它的距离小于阈值把这些点划为一类再对类里的每个点重复这个过程。听起来不难但问题是点云动不动就是几十万、上百万个点。假设有 10 万个点每个点都要跟全部点比一次距离那就是 100 亿次距离计算。这个计算量别说是实时处理离线跑起来都让人着急。PCL 的解决方案是 KdTree。KdTree 是一种二叉树结构它把三维空间按维度轮流切分构建成一颗搜索树。查询一个点的近邻时不需要扫描全部点而是沿着树的分支快速定位到目标区域附近再在局部范围内搜索。这个过程把查找复杂度从 O(n) 降到了 O(log n) 量级。10 万个点的点云用 KdTree 做近邻搜索速度能提升几个数量级。提示欧式聚类里面涉及到大量“查找某点的邻居”操作这个操作效率直接决定了整个算法的耗时。PCL 里做近邻搜索优先用 pcl::search::KdTree不要图省事用暴力遍历。2.2 区域生长的具体步骤种子点扩散是怎么跑的欧式聚类的核心逻辑可以拆成下面几步创建一个空的点云列表用来存放待处理的点再创建一个空的簇列表用来存放最终分割结果。从输入点云中取一个没被处理过的点作为种子点放进待处理列表。从待处理列表中取一个点用 KdTree 搜索它半径 setClusterTolerance 范围内的所有邻居点。遍历这些邻居点如果某个邻居点还没被处理过就把它标记为已处理并加入待处理列表同时加入当前簇。重复第 3、4 步直到待处理列表为空说明当前簇已经“长大”完了。检查当前簇的点数如果符合最小点数、最大点数限制就把这个簇保存下来。回到第 2 步取下一个未处理的点开启一个新簇。这个过程本质上就是图论里的连通区域标记——每个点是一个节点距离小于阈值的点之间连一条边最终形成的连通分量就是我们要的簇。理解这一点很重要后面调参的时候你会发现聚类容差Cluster Tolerance本质上就是在控制“连边”的判定标准。2.3 欧氏距离这个判据的局限什么时候它不灵欧式聚类用的是最朴素的欧氏距离判据这里面有几个天然的局限。第一个问题是它对“密度不均匀”的点云很敏感。同一个物体近处扫描的点密远处扫描的点稀。如果你用固定的距离阈值那么近处可能把两个本应分开的物体连在一起因为点密度高间距小远处又可能把一个物体拆成好几块因为点间距大超过阈值。处理这种情况要么先把点云做体素滤波均匀化要么根据距离自适应调整阈值但这会增加复杂度。第二个问题是它完全没用到点云的几何信息。法向量、曲率、颜色这些信息在聚类过程中被忽略了。这就意味着即使两个物体形状差异巨大只要它们在空间上靠得近欧式聚类就没法区分它们。比如一个球和一个立方体紧挨着放距离小于阈值它俩就会被算成同一个簇。第三个问题是它处理不了“相互穿插”的点云。两个物体中间有一片点混在一起谁是哪个物体的算法分不清楚。这时候就得靠模型拟合或者更高级的分割方法。所以欧式聚类适合的场景画像很清晰物体分离、背景干净、点密度相对均匀。如果实际数据不是这样先做预处理或者换个方法别硬刚。3. 三个核心参数聚类容差、最小点数、最大点数怎么定3.1 setClusterTolerance整个算法最敏感的一个参数聚类容差也就是搜索半径是我每次调参时第一个动的参数。它决定了“多远算邻居”。这个值设小了一个物体会被拆成好几块设大了不同物体又会被粘连在一起。这个值到底该设多大没有放之四海皆准的答案因为它取决于你点云的单位和尺度。PCL 默认点云坐标单位是米所以如果你扫描的是一个身高 1.8 米的人聚类容差设 0.02 可能是合适的如果你处理的是无人机航拍地形点间距都是米级那容差就得设到几米。判断方法很简单先看看你的点云平均点间距是多少容差取点间距的 2 到 5 倍通常比较稳妥。具体操作时我习惯先拿一帧点云做可视化用 PCL Visualizer 或者 CloudCompare 量一下目标物体内部点的平均间距再量一下两个相邻物体的最小间距。容差要大于物体内部点间距小于物体间的最小间距。如果这两个值量出来非常接近说明你的数据不太适合欧式聚类需要先想想怎么增加物体间距或者降低内部点间距。3.2 setMinClusterSize 和 setMaxClusterSize过滤噪点和巨型簇最小点数和最大点数这两个参数是用来过滤无效簇的。最小点数的作用是去掉那些由少量噪声点组成的小簇最大点数的作用是防止某些大簇把不相干的点全部吞进去。最小点数怎么设如果你的点云经过了体素滤波那么可以大概估算一下一个最小目标物大概对应多少个点。比如你的体素边长为 0.01 米一个 0.1 米见方的小零件大约包含 10×10×10 1000 个体素点。那么最小点数可以设到 100 到 300 之间留出安全余量。设太小了一个噪点就能形成一簇输出里全是碎片设太大了小目标会被直接忽略掉。最大点数主要是为了防止意外情况的。比如一帧点云里有一面墙你没有提前做平面分割那么这面墙很可能被当成一个巨大簇输出来。设一个最大点数可以及时止损比如 50000超过这个数说明这个簇大概率不是你要的单个目标而是没分割干净的背景。还有一种更稳妥的思路先用 RANSAC 或者其他方法把地面、墙面这些大平面滤掉再跑欧式聚类这样最大点数设不设都无所谓。3.3 参数设定的参考表与实际调试路径为了让大家有个直观的参照我整理了一份我一贯使用的参数设定思路参数推荐调试方法参考值单位米聚类容差先量点间距取 2~5 倍点间距作为初始值0.02 ~ 0.05最小点数估算最小目标物对应的点数留有余量50 ~ 500最大点数结合场景内最大单体目标估算或直接设 50000 上限50000 ~ 100000实际调试时我很少一次性把三个参数全部定好。我的顺序是先把最小点数设得很低比如 10最大点数控掉只调聚类容差——观察分割结果的过分割和欠分割情况。容差调得差不多之后再把最小点数逐渐往上抬直到不再出现碎片簇。最后如果发现某个簇特别巨大、明显不符合目标物尺寸再考虑设最大点数。4. 实操落地从 PCL 代码到可视化验证4.1 环境准备与 CMakeLists 配置下面进入动手环节。假设你已经装好了 PCL 1.10 以上版本我以 C 为例写完整流程。命令行工具方式pcl_segment_differences 之类不适合做精细调参我不推荐。先建一个工程目录然后写 CMakeLists.txtcmake_minimum_required(VERSION 3.10) project(euclidean_cluster_demo) set(CMAKE_BUILD_TYPE Release) find_package(PCL 1.10 REQUIRED COMPONENTS common io filters segmentation search visualization) include_directories(${PCL_INCLUDE_DIRS}) link_directories(${PCL_LIBRARY_DIRS}) add_definitions(${PCL_DEFINITIONS}) add_executable(euclidean_cluster euclidean_cluster.cpp) target_link_libraries(euclidean_cluster ${PCL_LIBRARIES})4.2 核心代码滤波、分割、提取、可视化一条龙然后是主程序。下面这段代码参考了我常用的写法包含了体素滤波、平面分割、欧式聚类和结果可视化中间加了注释方便对照#include pcl/io/pcd_io.h #include pcl/point_types.h #include pcl/filters/voxel_grid.h #include pcl/filters/extract_indices.h #include pcl/segmentation/sac_segmentation.h #include pcl/segmentation/extract_clusters.h #include pcl/search/kdtree.h #include pcl/visualization/pcl_visualizer.h int main(int argc, char** argv) { if (argc ! 2) { std::cerr 用法: argv[0] input.pcd std::endl; return -1; } // 1. 读取点云 pcl::PointCloudpcl::PointXYZ::Ptr cloud(new pcl::PointCloudpcl::PointXYZ()); if (pcl::io::loadPCDFile(argv[1], *cloud) -1) { PCL_ERROR(读取PCD文件失败\n); return -1; } std::cout 输入点云点数: cloud-size() std::endl; // 2. 体素滤波降采样 均匀化 pcl::PointCloudpcl::PointXYZ::Ptr filtered(new pcl::PointCloudpcl::PointXYZ()); pcl::VoxelGridpcl::PointXYZ voxel; voxel.setInputCloud(cloud); voxel.setLeafSize(0.01f, 0.01f, 0.01f); voxel.filter(*filtered); std::cout 体素滤波后点数: filtered-size() std::endl; // 3. 用RANSAC分割出最大的平面通常是地面或墙面 pcl::SACSegmentationpcl::PointXYZ seg; pcl::PointIndices::Ptr inliers(new pcl::PointIndices()); pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients()); seg.setOptimizeCoefficients(true); seg.setModelType(pcl::SACMODEL_PLANE); seg.setMethodType(pcl::SAC_RANSAC); seg.setDistanceThreshold(0.02); seg.setInputCloud(filtered); seg.segment(*inliers, *coefficients); std::cout 检测到的平面点数: inliers-indices.size() std::endl; // 4. 提取非平面点云作为聚类的输入 pcl::ExtractIndicespcl::PointXYZ extract; pcl::PointCloudpcl::PointXYZ::Ptr objects(new pcl::PointCloudpcl::PointXYZ()); extract.setInputCloud(filtered); extract.setIndices(inliers); extract.setNegative(true); extract.filter(*objects); // 5. 欧式聚类分割 pcl::search::KdTreepcl::PointXYZ::Ptr tree(new pcl::search::KdTreepcl::PointXYZ()); tree-setInputCloud(objects); std::vectorpcl::PointIndices cluster_indices; pcl::EuclideanClusterExtractionpcl::PointXYZ ec; ec.setClusterTolerance(0.03); // 聚类容差单位米 ec.setMinClusterSize(100); // 最小簇点数 ec.setMaxClusterSize(50000); // 最大簇点数 ec.setSearchMethod(tree); ec.setInputCloud(objects); ec.extract(cluster_indices); // 6. 输出每个簇的点数并把结果存入一个向量方便可视化 std::vectorpcl::PointCloudpcl::PointXYZ::Ptr clusters; int cluster_id 0; for (const auto indices : cluster_indices) { pcl::PointCloudpcl::PointXYZ::Ptr cluster(new pcl::PointCloudpcl::PointXYZ()); for (int idx : indices.indices) { cluster-push_back((*objects)[idx]); } cluster-width cluster-size(); cluster-height 1; cluster-is_dense true; clusters.push_back(cluster); std::cout 簇 cluster_id 点数: cluster-size() std::endl; } // 7. 可视化白色表示滤除的平面其他颜色表示不同簇 pcl::visualization::PCLVisualizer viewer(欧式聚类结果); viewer.setBackgroundColor(0.1, 0.1, 0.1); pcl::visualization::PointCloudColorHandlerCustompcl::PointXYZ plane_color(filtered, 255, 255, 255); viewer.addPointCloud(filtered, plane_color, scene); for (size_t i 0; i clusters.size(); i) { int r (i * 80) % 255; int g (i * 150) % 255; int b (i * 200) % 255; pcl::visualization::PointCloudColorHandlerCustompcl::PointXYZ color(clusters[i], r, g, b); viewer.addPointCloud(clusters[i], color, cluster_ std::to_string(i)); } while (!viewer.wasStopped()) { viewer.spinOnce(); } return 0; }4.3 代码里的几个关键点解读为什么先做体素滤波和平面分割体素滤波这一步不是可有可无的。它的作用有两个一是降采样减少点云数量让聚类搜索更快二是让点云密度更加均匀避免出现“近处密、远处疏”导致聚类容差难以选择的问题。如果你点云本身来自激光雷达不同距离的点密度差异很大这一步尤其重要。LeafSize 设多少一般是场景精度的 2 到 3 倍。做零件分拣设 0.005 到 0.01做室内场景 0.02 到 0.05具体看你对细节的要求。RANSAC 平面分割是为了把地面、墙面这些大平面先摘出去。这些平面在空间上连成一大片如果不处理欧式聚类会把它们当成一个超大的簇把附近所有物体的点全部“吸”进去导致聚类结果完全没法用。先分割平面再对剩余点云聚类是三维视觉里使用频率最高的组合拳。注意setNegative(true) 的意思是提取“不属于”平面内点的点云千万别搞反。忘了设这个你提取出来的反而是平面本身聚类会对着一面墙跑结果自然全是错的。5. 实际调试中遇到的高频问题与排查思路5.1 过分割一个物体被拆成了几个簇这个现象在调试初期几乎必然出现。表现是你明明看到一个完整的盒子结果聚类出来它被拆成了两三块。原因几乎一定是聚类容差设小了导致物体内部远端的点之间的间隔超过了容差值环路断裂。排查思路很简单先可视化把过分割簇的不同部分用不同颜色显示出来测量一下这两部分的距离再把聚类容差往上调一调。调整幅度别太大每次增加 30% 左右反复观察。另外还有一种可能你点云没有做体素滤波原始扫描点分布不均匀物体边缘点稀疏导致断裂。这种情况先回去把体素滤波做好。5.2 欠分割不同物体被连成了一个簇跟过分割相反欠分割的表现是几个物体被归到了一个簇里。常见原因有两个一是聚类容差太大把相邻物体之间的间隙也覆盖了二是物体本身就靠得很近距离小于容差阈值。排查时先确认物体间距是不是真的大于容差。如果物体间距确实很小比如只有 1 厘米而你的容差已经设到 5 厘米那就是参数问题往回缩。如果物体间距本身就只有几毫米容差已经压到很小还是粘连那么这个场景确实不适合欧式聚类可以考虑用区域生长分割它结合了法向量信息对近邻物体分界能力更强。另外也有可能是平面分割没做干净一张桌子面没被完全去掉导致两个物体通过桌面连在一起这种情况去调整平面分割的距离阈值。5.3 噪声点造成的孤立小簇最小点数怎么压点云里总有那么几个游离的噪声点。这些点周围没有真实邻居聚类时它们单独成簇数量还不少。解决办法是提高最小点数。但如果你调高了最小点数真的小目标也被滤掉了怎么办这时候建议先把离群点滤波StatisticalOutlierRemoval先跑一遍把明显的孤立噪声去掉再让最小点数维持在一个相对低的值保护小目标。5.4 聚类耗时过长从参数和数据结构两头优化点云规模到了百万级聚类耗时可能达到几秒甚至十几秒。排查方向有两个一是你的体素滤波 LeafSize 是不是太小了点云数量没降下来这个最直接二是你的聚类容差是不是设得过大导致搜索半径内邻居数量巨大每个点的近邻搜索都变成“全图搜索”。把容差往下压一点搜索代价会显著降低。另外把 KdTree 的搜索方式从 pcl::search::KdTree 换成 pcl::search::Octree在处理某些分布特征的点云时可能更快但通常情况下前者已经够用。5.5 常见问题速查表现象可能原因优先检查项一个物体分成多块容差过小 / 点云密度不均调大容差、检查体素滤波多个物体连成一簇容差过大 / 物体间距过小调小容差、检查平面分割大量细小碎片簇噪声点未滤除加离群点滤波、提高最小点数聚类时间过长点云未降采样 / 容差过大调整体素 LeafSize、缩小容差大簇吞掉周围物体平面未分割干净调整 RANSAC 距离阈值6. 工程化经验从“能跑”到“稳定可用”的几点体会6.1 坐标系与单位最容易忽略的坑PCL 里点云坐标默认单位是米但有些传感器驱动或者第三方数据格式用的是毫米、厘米。单位不一致的情况下一个本来应该设 0.03 的容差按毫米数据你就得设成 30。很多人拿着别人的参数跑不出效果第一反应是怀疑算法其实单位错了什么都白谈。拿到点云先打印一下点坐标的数值范围要是看到 x, y, z 都是几千几万的量级基本可以断定单位是毫米。6.2 多帧拼接前的预处理别忘了时域滤波静态场景单帧聚类相对简单但实际机器人项目往往要处理连续多帧点云。帧与帧之间会有重叠、抖动直接对拼接后的点云聚类容易出现同一个物体在边界处被拆分的现象。我建议在拼接之前对每一帧分别做一次欧式聚类然后用聚类结果的重心做跨帧匹配而不是把所有点云拼在一起再聚类。这样既减少了计算量也避免了边界撕裂。6.3 聚类之后的常用后处理质心、包围盒与尺寸过滤聚类只是第一步通常还要对每个簇做后续处理。最常用的是计算每个簇的质心和轴对齐包围盒AABB。质心可以用于抓取时的目标定位包围盒的长宽高可以帮你过滤掉尺寸不符的物体。这一步在 PCL 里实现很轻量用 pcl::getMinMax3D 随随便便就拿到了。尺寸过滤配合最小点数比单靠点数一个维度可靠得多——一个 2 米长的大平面可能点数不多但尺寸一看就不是你要的零件。6.4 参数自动化能不能自适应设定聚类容差有人问过我能不能让聚类容差自适应不用每次手调。有一定可行的思路先对点云做最近邻距离估计统计每个点到它最近邻的距离分布取一个百分位数比如 80% 分位作为容差。但这个方法对异常点很敏感实际项目中我一般不用它作为最终方案而是用来自动给一个初始值然后人工微调。毕竟聚类容差这个参数对结果影响太大完全交给自适应逻辑还是有点冒险。7. 最后分享一个小技巧聚类结果的可视化调试法调试欧式聚类最高效的办法其实不是打印点数而是让可视化工具直接告诉你问题出在哪。我给每个簇分配随机颜色渲染能一眼看出过分割和欠分割。如果你的簇数量很多逐个addPointCloud太慢可以先合并成一个带标签的点云用 PCL Visualizer 的 ColorHandler 按标签着色。另外 CloudCompare 里也有现成的连通分量分割模块参数一样可以先在里面快速试参找到大致范围再回到代码里精调省不少时间。我在实际项目里用这个流程处理过连续多帧的工业零件点云从拿到一帧带平面背景的数据到输出干净的聚类结果调参时间基本控制在十几分钟以内。关键是记住那个参数调试顺序先压平面再定容差最后调点数。顺序对了效率就上来了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。