PCL源码剖析 -- 欧式聚类

PCL源码剖析 – 欧式聚类

参考
1. pcl Euclidean Cluster Extraction教程
2. 欧式聚类分析
3. pcl-api源码
4. 点云欧式聚类
5. 本文完整工程地址


可视化结果

在这里插入图片描述

一. 理论

聚类方法需要将无组织的点云模型P划分为更小的部分,以便显著减少P的总体处理时间。欧式空间的简单数据聚类方法可以利用固定宽度box的3D网格划分或者一般的八叉树数据结构实现。这种特殊的表征速度快,在体素表征的占据空间。然后,更普遍使用的手段是使用最近邻来实现聚类技术,本质类似于洪水填充(flood fill)算法。

二. 伪码

1. 为点云P 创建KD-Tree的输入表征
2. 创建空的聚类列表C 和 点云的检查队列Q
3. 对于P中的每一个点Pi,执行如下操作:
4.    - 将Pi添加到当前队列Q(并标记为已处理);
5.    - while处理 Q 中的每一个Pi:
6.          - 对Pi进行近邻搜索,查找满足半径 < d 的点集合;
7.          - 检查上一步中每一个邻居点,如果标记是未被处理的点,则加入到队列Q中;
8.    - 直到Q中的所有点都被标记为已处理,将Q加入到聚类列表C中,将Q重新置为空
9. 当所有的Pi都被处理过之后结束,聚类结果为列表 C

三. 源码分析

3.1 关于主要函数extractEuclideanClusters的一些说明

  1. 功能说明
    根据最大、最小聚类点数,欧式聚类阈值,将点云进行欧式聚类。
  2. 参数说明
- cloud: 输入点云
- tree: kd-tree
- tolerance: 欧式聚类距离阈值
- clusters: 输出参数,最终聚类结果数组
- min_pts_per_cluster: 每个聚类结果最小点数
- max_pts_per_cluster: 每个聚类结果最大点数

3.2 重点函数extractEuclideanClusters的实现

已为源码添加注释

 #include <pcl/segmentation/extract_clusters.h> // 分割模块
 #include <pcl/search/organized.h> // for OrganizedNeighbor
 
 //
 template <typename PointT> void
 pcl::extractEuclideanClusters (const PointCloud<PointT> &cloud,
                                const typename search::Search<PointT>::Ptr &tree,
                                float tolerance, std::vector<PointIndices> &clusters,
                                unsigned int min_pts_per_cluster,
                                unsigned int max_pts_per_cluster)
 { 
   // 若构建的kd-tree中点数,与输入点云数据点数不同,直接错误打印,并退出
   if (tree->getInputCloud ()->size () != cloud.size ())
   {
     PCL_ERROR("[pcl::extractEuclideanClusters] Tree built for a different point cloud "
               "dataset (%zu) than the input cloud (%zu)!\n",
               static_cast<std::size_t>(tree->getInputCloud()->size()),
               static_cast<std::size_t>(cloud.size()));
     return;
   }
   // 检查kdtree是否经过排序,若结果为true,则不需要检查第一个元素;否则需要检查第一个元素,默认为0
   int nn_start_idx = tree->getSortedResults () ? 1 : 0;
   // 建立处理队列Q,数目为点数数目,每一个值为false
   std::vector<bool> processed (cloud.size (), false);
   
   // nn_indices代表某一个点knn近邻查找,满足距离阈值的点云索引
   Indices nn_indices;
   std::vector<float> nn_distances;
   // 遍历每一个点
   for (int i = 0; i < static_cast<int> (cloud.size ()); ++i)
   { 
     // 若该点云被标记为true(处理过),则跳过该点
     if (processed[i])
       continue;
     
     // 当前某一个聚类的种子队列Q
     Indices seed_queue;
     int sq_idx = 0; // 种子队列索引
     seed_queue.push_back (i); // 将当前点加入种子队列,并标记为true
     processed[i] = true; 
     
     // 循环处理种子队列Q中的每一个点,进行K近邻查找以及标记工作
     while (sq_idx < static_cast<int> (seed_queue.size ()))
     {
       // 对种子队列中索引的点进行半径距离查找,若半径内没有可查询的点,则索引+1, 跳过后续处理
       if (!tree->radiusSearch (seed_queue[sq_idx], tolerance, nn_indices, nn_distances))
       {
         sq_idx++;
         continue;
       }
       
       // 半径内有满足条件的点,点云索引保存在nn_indices。遍历这些点,判断是否处理过,若没有处理过,则加入到种子队列Q中
       for (std::size_t j = nn_start_idx; j < nn_indices.size (); ++j)             // can't assume sorted (default isn't!)
       {
         // 若该索引为0 或者标记为被处理过之后,则跳过该点
         if (nn_indices[j] == UNAVAILABLE || processed[nn_indices[j]])        // Has this point been processed before ?
           continue;
  
         // 进行简单的欧式聚类,即将其加入到种子队列中,并标记为true
         seed_queue.push_back (nn_indices[j]);
         processed[nn_indices[j]] = true;
       }
       
       // 跳到队列中下一个需要处理的种子点
       sq_idx++;
     }
  
     // 当一个聚类结果完成后,若种子队列数目在最大值与最小值之间,则需要将聚类结果加入到聚类列表中
     if (seed_queue.size () >= min_pts_per_cluster && seed_queue.size () <= max_pts_per_cluster)
     {
       pcl::PointIndices r; // PointIndices类中indices是一个vector
       r.indices.resize (seed_queue.size ());
       // 完成种子队列中索引的赋值操作
       for (std::size_t j = 0; j < seed_queue.size (); ++j)
         r.indices[j] = seed_queue[j];
  
       // 点云索引值排序和去重,源码注释中表示下面两行可以去掉,并不需要
       std::sort (r.indices.begin (), r.indices.end ());
       r.indices.erase (std::unique (r.indices.begin (), r.indices.end ()), r.indices.end ());
        
       // 头赋值和点云结果加入到聚类列表中
       r.header = cloud.header;
       clusters.push_back (r);   
     }
     else
     {
       // 打印聚类数目超限
       PCL_DEBUG("[pcl::extractEuclideanClusters] This cluster has %zu points, which is not between %u and %u points, so it is not a final cluster\n",
                 seed_queue.size (), min_pts_per_cluster, max_pts_per_cluster);
     }
   }
 }

四. 应用

测试数据table_scene_lms400.pcd 下载
应用完整工程地址
分析:应用整体流程经历了下采样,使用平面模型分割桌面,过滤多个平面;对剩余点云进行欧式聚类,欧式聚类结果可视化。

#include <pcl/ModelCoefficients.h>
#include <pcl/point_types.h>
#include <pcl/io/pcd_io.h>
#include <pcl/filters/extract_indices.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/features/normal_3d.h>
#include <pcl/kdtree/kdtree.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>
#include<pcl/visualization/pcl_visualizer.h>

bool isPushSpace = false;

//键盘事件
void keyboard_event_occurred(const pcl::visualization::KeyboardEvent& event, void * nothing)
{
	if (event.getKeySym() == "space" && event.keyDown())
	{
		isPushSpace = true;
	}
}

int main(int argc, char** argv)
{
	// 从PCD文件中读取点云数据
	pcl::PCDReader reader;
	pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>), cloud_f(new pcl::PointCloud<pcl::PointXYZ>);
	reader.read("../../../data/table_scene_lms400.pcd", *cloud);
	std::cout << "PointCloud before filtering has: " << cloud->points.size() << " data points." << std::endl; //*

	pcl::visualization::PCLVisualizer viewer("Cluster Extraction");

    // 注册键盘事件
	viewer.registerKeyboardCallback(&keyboard_event_occurred, (void*)NULL);
	int v1(1);
	int v2(2);
	viewer.createViewPort(0, 0, 0.5, 1, v1);
	viewer.createViewPort(0.5, 0, 1, 1, v2);

	// 使用下采样,分辨率 1cm
	pcl::VoxelGrid<pcl::PointXYZ> vg;
	pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
	vg.setInputCloud(cloud);
	vg.setLeafSize(0.01f, 0.01f, 0.01f);
	vg.filter(*cloud_filtered);
	std::cout << "PointCloud after filtering has: " << cloud_filtered->points.size() << " data points." << std::endl; //*

	viewer.addPointCloud(cloud, "cloud1", v1);
	viewer.addPointCloud(cloud_filtered, "cloud2", v2);
	//渲染10秒再继续
	viewer.spinOnce(10000);

	// 创建平面分割对象
	pcl::SACSegmentation<pcl::PointXYZ> seg;
	pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
	pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
	pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_plane(new pcl::PointCloud<pcl::PointXYZ>());
	pcl::PCDWriter writer;
	seg.setOptimizeCoefficients(true);
	seg.setModelType(pcl::SACMODEL_PLANE);
	seg.setMethodType(pcl::SAC_RANSAC);
	seg.setMaxIterations(100);
	seg.setDistanceThreshold(0.02);

	// 把点云中所有的平面全部过滤掉,重复过滤,直到点云数量小于原来的0.3倍
	int i = 0, nr_points = (int)cloud_filtered->points.size();
	while (cloud_filtered->points.size() > 0.3 * nr_points)
	{
		// Segment the largest planar component from the remaining cloud
		seg.setInputCloud(cloud_filtered);
		seg.segment(*inliers, *coefficients);
		if (inliers->indices.size() == 0)
		{
			std::cout << "Could not estimate a planar model for the given dataset." << std::endl;
			break;
		}
 
		// Extract the planar inliers from the input cloud
		pcl::ExtractIndices<pcl::PointXYZ> extract;
		extract.setInputCloud(cloud_filtered);
		extract.setIndices(inliers);
		extract.setNegative(false);
 
		// Write the planar inliers to disk
		extract.filter(*cloud_plane);
		std::cout << "PointCloud representing the planar component: " << cloud_plane->points.size() << " data points." << std::endl;
 
		// Remove the planar inliers, extract the rest
		extract.setNegative(true);
		extract.filter(*cloud_f);
 
		//更新显示点云
		viewer.updatePointCloud(cloud_filtered, "cloud1");
		viewer.updatePointCloud(cloud_f, "cloud2");
		//渲染3秒再继续
		viewer.spinOnce(3000);
 
		cloud_filtered = cloud_f;
 
	}

	viewer.removePointCloud("cloud2", v2);

	/// 创建KdTree对象作为搜索方法
	pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
	tree->setInputCloud(cloud_filtered);

    /// 欧式聚类
	std::vector<pcl::PointIndices> cluster_indices;
	pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
	ec.setClusterTolerance(0.02); // 2cm
	ec.setMinClusterSize(100);
	ec.setMaxClusterSize(25000);
	ec.setSearchMethod(tree);
	ec.setInputCloud(cloud_filtered);
	ec.extract(cluster_indices);

	//遍历抽取结果,将其显示并保存
	int j = 0;
	for (std::vector<pcl::PointIndices>::const_iterator it = cluster_indices.begin(); it != cluster_indices.end(); ++it)
	{
		//创建临时保存点云族的点云
		pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster(new pcl::PointCloud<pcl::PointXYZ>);
		//通过下标,逐个填充
		for (std::vector<int>::const_iterator pit = it->indices.begin(); pit != it->indices.end(); pit++)
			cloud_cluster->points.push_back(cloud_filtered->points[*pit]); //*

		//设置点云属性
		cloud_cluster->width = cloud_cluster->points.size();
		cloud_cluster->height = 1;
		cloud_cluster->is_dense = true;

		std::cout << "当前聚类 "<<j<<" 包含的点云数量: " << cloud_cluster->points.size() << " data points." << std::endl;
		std::stringstream ss;
		ss << "cloud_cluster_" << j << ".pcd";
		// writer.write<pcl::PointXYZ>(ss.str(), *cloud_cluster, false); //*
		j++;

		//显示,随机设置不同颜色,以区分不同的聚类
		pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> cluster_color(cloud_cluster, rand()*100 + j * 80, rand() * 50 + j * 90, rand() * 200 + j * 100);
		viewer.addPointCloud(cloud_cluster,cluster_color, ss.str(), v2);
		viewer.spinOnce(5000);
	}
	while (!viewer.wasStopped())
	{
		viewer.spinOnce();
	}
	return (0);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/9275.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【hello Linux】Linux下 gitee 的使用

目录 1. 安装 git 2. gitee 的使用 2.1 注册 gitee 账号 2.2 创建项目&#xff1a;也就是仓库 2.3 下载项目到本地 3. 上传gitee三步走 3.1 三板斧第一招&#xff1a;git add 3.2 三板斧第二招&#xff1a;git commit 3.3 三板斧第三招&#xff1a;git push Linux&#x1f337…

海外虚拟主机空间:如何使用CDN加速提升用户体验?

随着互联网的迅速发展和全球化的趋势&#xff0c;越来越多的企业和个人选择海外虚拟主机空间。然而&#xff0c;由于服务器的地理位置和网络延迟等原因&#xff0c;这些网站在国内访问时可能会遇到较慢的加载速度和不稳定的用户体验。为了解决这一问题&#xff0c;使用CDN加速是…

【Linux】进程理解与学习Ⅳ-进程地址空间

环境&#xff1a;centos7.6&#xff0c;腾讯云服务器Linux文章都放在了专栏&#xff1a;【Linux】欢迎支持订阅&#x1f339;相关文章推荐&#xff1a;【Linux】冯.诺依曼体系结构与操作系统【Linux】进程理解与学习Ⅰ-进程概念浅谈Linux下的shell--BASH【Linux】进程理解与学习…

[JavaEE]----Spring01

文章目录Spring_day011&#xff0c;课程介绍1.1 为什么要学?1.2 学什么?1.3 怎么学?2&#xff0c;Spring相关概念2.1 初识Spring2.1.1 Spring家族2.1.2 了解Spring发展史2.2 Spring系统架构2.2.1 系统架构图2.2.2 课程学习路线2.3 Spring核心概念2.3.1 目前项目中的问题2.3.…

VN5620以太网测试——环境搭建篇

文章目录 前言一、新建以太网工程二、Port Configuration三、Link up四 Trace界面五、添加Ethernet Packet Builder六、添加ARP Packet七、添加Ethernet IG总结前言 CANoe(CAN open environment)VN5620 :是一个紧凑而强大的接口,用于以太网网络的分析、仿真、测试和验证。 …

面试篇-深入理解 Java 中的 HashMap 实现原理

一、HashMap实现原理 HashMap 的实现主要包括两个部分&#xff1a;哈希函数和解决哈希冲突的方法。 1.哈希函数 当使用 put() 方法将键值对存储在 HashMap 中时&#xff0c;首先需要计算键的哈希值。HashMap 使用 hashCode() 方法获取键的哈希值&#xff0c;并将其转换为桶&…

2023-04-11 无向图的匹配问题

无向图的匹配问题 之所以把无向图的这个匹配问题放到最后讲是因为匹配问题借鉴了有向图中一些算法的思想 1 最大匹配和完美匹配 二分图回顾 二分图&#xff1a;把一个图中的所有顶点分成两部分&#xff0c;如果每条边的两端分别属于不同部分&#xff0c;则这个图是二分图。更多…

springcloud——gateway功能拓展

目录 1.获取用户真实IP 2.统一跨域配置 3.redis令牌桶算法限流 1.获取用户真实IP 在我们的日常业务中&#xff0c;我们时常需要获取用户的IP地址&#xff0c;作登录日志、访问限制等相关操作。 而在我们的开发架构中&#xff0c;一般我们将服务分为多个微服务&#xff0c;…

Python 进阶指南(编程轻松进阶):一、处理错误和寻求帮助

原文&#xff1a;http://inventwithpython.com/beyond/chapter1.html 请您不要将计算机当成佣人&#xff0c;因为这样会让您常常感觉很烦躁。比如说当计算机向您显示错误消息时&#xff0c;并不是因为您冒犯了它。计算机是我们大多数人都会接触到的最复杂的工具&#xff0c;但归…

HBase高手之路4-Shell操作

文章目录HBase高手之路3—HBase的shell操作一、hbase的shell命令汇总二、需求三、表的操作1&#xff0e;进入shell命令行2&#xff0e;创建表3&#xff0e;查看表的定义4&#xff0e;列出所有的表5&#xff0e;删除表1)禁用表2)启用表3)删除表四、数据的操作1&#xff0e;添加数…

【HAL库】BMP180气压传感器+STM32,hal库移植

BMP180气压传感器STM321 导入.c.h文件&#xff08;不再赘述&#xff0c;详细见LED部分&#xff09;2 Cubemx配置3 修改 .h 文件4 测试将BMP180从标准库移植到HAL库。模拟IIC。 极简工程代码如下&#xff1a; https://github.com/wyfroom/HAL_BMP180 该份代码硬件配置&#xff…

Oracle_EBS_核心功能(MFG)(1)

INV: Items参考《深入浅出Oracle EBS之核心功能&#xff08;DIS&#xff09;》。canca INV: Transactions基本库存事务处理参考《深入浅出Oracle EBS之核心功能&#xff08;DIS&#xff09;》。canca BOM: Bills of Material物料清单应用&#xff1a;Bills of Material 职责&am…

day-004-链表-两两交换链表中的节点、删除链表的倒数第N个节点、链表相交、环形链表II

两两交换链表中的节点 题目建议&#xff1a;用虚拟头结点&#xff0c;这样会方便很多。 题目链接/文章讲解/视频讲解 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* Li…

阿里9年测试经验告诉你:作为一名年薪40w自动化测试需要具备那些能力

前言 前段时间张同学问我说&#xff1a;我已经功能测试2年多了&#xff0c;在功能测试的阶段中也一直在自学自动化测试&#xff0c;有了一定的代码基础还学习了很多的工具&#xff0c;问题是我不知道自动化测试到底需要具备什么样的能力。 我相信有很多小伙伴也是在思索这个问…

计算机组成的基本认识

计算机——> 数值计算——> 处理电信号——> 基本单元&#xff08;逻辑元件&#xff09; 电子管——> 晶体管——>中小规模集成电路 ——>大规模&#xff0c;超大规模集成电路 机器字长&#xff1a;计算机一次整数运算所能处理的二进制位数 解析存储器中的程…

这家年销售额309亿的Tier 1,要谈一场千亿新生意

跨入2023年&#xff0c;智能汽车软件赛道更热闹了。 相较于传统汽车开发模式&#xff0c;软件属于分布式ECU工程开发的一部分&#xff0c;由一级供应商作为黑盒提供&#xff0c;软件开发成本等被认为是硬件系统成本的一部分&#xff0c;没有实现单独定价。 如今&#xff0c;“…

如何在windows/linux下启动OpenOffice

上面一篇文章使用openOffice来实现预览word、excel、pdf、txt等的功能时&#xff0c;发现openOffice没有启动&#xff0c;也怕有些同学安装后不会启动&#xff0c;所以便写下这一篇文章&#xff0c;来为大家说明如何启动openOffice&#xff0c;上一篇讲的如何下载安装openOffic…

2.5 函数的微分

思维导图&#xff1a; 学习目标&#xff1a; 我认为学习函数的微分需要以下几个步骤&#xff1a; 熟练掌握导数的定义和基本性质&#xff0c;包括求导法则和高阶导数的概念。学习一些重要的函数的导数&#xff0c;例如多项式函数、三角函数、指数函数和对数函数等。这些函数的…

CSDN——Markdown编辑器——官方指导

CSDN——Markdown编辑器——官方指导欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表…

Node.js -- http模块

1. 什么是http模块 在网络节点中&#xff0c;负责消费资源的电脑&#xff0c;叫客户端&#xff1b;负责对外提供网络资源的电脑&#xff0c;叫做服务器。 http模块是Node.js官方提供的&#xff0c;用来创建web服务器的模块。通过http模块提供的http.createServer()方法&#…