【完结】【PCL实现点云分割】ROS深度相机实践指南(下):pcl::BoundaryEstimation实现3D点云轮廓检测的原理(论文解读)和代码实现

news2024/11/15 21:51:16

前言

  • 本教程使用PCL对ROS深度相机捕获到的画面进行操场上锥桶的分割

    • 上:[csdn 博客] 【PCL实现点云分割】ROS深度相机实践指南(上):PCL库初识和ROS-PCL数据类型转换
    • 中:[csdn 博客] 【PCL实现点云分割】ROS深度相机实践指南(中):Plane Model Segmentation实现平面分割原理及其实践
  • 本人相关的RGBD深度相机原理及其使用教程:

    • [csdn 博客] 上手一个RGBD深度相机:从原理到实践–ROS noetic+Astra S(上):解读深度测距原理和内外参推导
    • [csdn 博客] 上手一个RGBD深度相机:从原理到实践–ROS noetic+Astra S(中):RGB相机的标定和使用
  • 关于PCL的学习教程强烈推荐双愚大大的文章:PCL(Point Cloud Library)学习指南&资料推荐(2024版)请添加图片描述

  • 同样不错的基础入门:黑马机器人PCL教程请添加图片描述

  • 本文使用的环境和设备

    • ubuntu 20.04LTS
    • ros1 noetic
    • Astra S 深度相机
    • pcl-1.10
  • 上一节讲述如何使用PCL中Plane Model Segmentation的原理和代码实践操作及其细节,这一节我们来讲讲点云轮廓提取的原理,及其如何使用 pcl::BoundaryEstimation对提取到的点云信息进行轮廓提取进行代码实操,然后映射到2D平面上(类似于cv的轮廓检测),得到如下图的结果。请添加图片描述


1 论文解读Detecting Holes in Point Set Surfaces

  • 在正式开始我们的代码实操环节之前,我们来阅览一下这篇论文。今天我们要使用到的 pcl::BoundaryEstimation类进行点云轮廓检测的原理和下述这篇论文中提到的点云轮廓检测原理有异曲同工之处,通过阅读这篇论文,能更好的理解空间中点云轮廓检测的原理,方便我们进行代码书写和记忆。
  • Detecting Holes in Point Set Surfaces这篇论文提出了一种基于概率的孔洞检测方法,通过结合多种边缘检测准则,利用点云数据中的局部邻域信息,有效地检测出点云表面上的孔洞。该方法具有较高的鲁棒性、可调性和效率,可以应用于多种场景,例如表面重建、数据采集、表面修复和模型编辑等。 [Detecting Holes in Point Set Surfaces 论文链接](NimbusSanL-Regu (zcu.cz))请添加图片描述
1-1 总览
  • 根据这篇论文的3,4,5节,我们大致可以把整个3D点云轮廓分为如下几个步骤

    1. 邻域构建(见3,4.1): 首先,需要构建每个点的局部邻域。论文中提出了一种对称邻域构建方法The symmetric kε neighbourhood,该方法能够有效地过滤掉采样密度突变的影响,从而提高边缘检测的鲁棒性。
    2. 边缘检测准则(见4.2~4.4): 利用多种边缘检测准则,包括角度准则Angle Criterion、半圆准则Halfdisc Criterion和形状准则Shape Criterion,计算每个点的边界概率。每种准则都从不同的角度对点云数据进行分析,并判断哪些点更有可能是边界点。
    3. 概率融合(见4.5): 将每种准则的计算结果进行融合,得到一个综合的边界概率Π(p)。综合概率反映了每个点属于边界点的可能性。
    4. 边界一致性处理(见5.1): 利用边界点之间的一致性,使用 Kruskal 算法构建基于邻域图G的最小生成图 MSG,进一步确认边界点,并提取出包围空洞的闭合循环。
    5. 轮廓提取(见5.2): 通过BFS遍历闭合循环,提取出点云的轮廓信息。
  • 在论文的第3小节提到了关于整个算法的一个基本概述:请添加图片描述请添加图片描述

  • 这段概述结合第3小节提出的边界计算算子,解释了算法通过计算每个在P点集中的点的边界概率Π§,来判断这个点是否属于点云边界。

  • 下面我们来分别看上述步骤涉及到的每一部分内容解读:


1-2 邻域构建(见3,4.1)
1-2-1 三种邻域
  • 对于点集中的一个点p,判断它为边界点的条件往往不是p本身,而是由p周围的领域点决定的

  • 故论文的4.1小节中讨论了三种关于一个点p领域的确定方法:请添加图片描述

    1. k-邻域:包含距离点p最近的k个样本点。这种定义简单直观,但在采样密度变化较大的区域(例如边缘区域)会出现偏差,导致邻域被密集采样区域所主导(如上左图,取的点受到了密集点集的影响)
    2. kε-邻域: 包含距离点 p 最近的k个样本点,以及距离点p中心在 ε ε ε范围内的所有点( 在 k-邻域的基础上,增加一个半径为 ϵ 的球体,将球体内的所有点也包含在内。)。这种定义可以缓解 k-邻域的偏差问题,但在稀疏采样区域中,ke-邻域会包含来自密集采样区域的点,增加了计算成本。
    3. 对称 ke-邻域: 包含距离点 p最近的 k个样本点,以及距离这些k个样本点中心在€范围内的所有点。(同时考虑点 p 和其 k 个最近邻点 q kϵ-邻域,并将两个邻域的交集作为最终的邻域。)这种定义可以克服 kε-邻域的偏差问题,并有效地处理采样密度变化,因为它考虑了点p的k个最近邻域点的邻域信息。
  • 请添加图片描述

  • 论文采用对称 ke-邻域的方法进行领域确认,同时为了高效地收集每个点的邻域,论文中使用了kd-tree数据结构。

1-2-2 KD树-kd_tree

请添加图片描述

  • kd-tree 是一种用于多维空间中快速检索最近邻点的数据结构,它类似于二叉搜索树,但每个节点包含多个键值。
  • *kd-tree 的构造
    1. 选择一个维度作为分割维度,例如 x 维。
    2. 将点云中的所有点按照该维度的值进行排序。
    3. 选择排序后的中点作为根节点,将点云分成两部分,左半部分的所有点都小于根节点,右半部分的所有点都大于根节点。
    4. 对左半部分和右半部分分别递归地进行步骤 1-3,直到所有点都被分配到叶子节点。
  • *kd-tree 的查询
    1. 从根节点开始,比较查询点与当前节点的分割维度值。
    2. 如果查询点小于分割维度值,则进入左子树;否则进入右子树。
    3. 重复步骤 1-2,直到找到叶子节点或者当前节点与查询点的距离小于某个阈值。
  • *kd-tree 的优点
    • 查询效率高: kd-tree 可以在 O(log n) 的时间复杂度内找到最近邻点,其中 n 是点云中点的数量。
    • 空间复杂度低: kd-tree 的空间复杂度为 O(n),与点云中点的数量成正比。
  • *kd-tree 的应用
    • 最近邻搜索: 在点云处理、图像处理、机器学习等领域中,经常需要进行最近邻搜索,kd-tree 可以有效地加速搜索过程。
    • 空间分割: kd-tree 可以将空间分割成多个区域,每个区域包含一组相邻的点。
    • 聚类分析: kd-tree 可以用于聚类分析,例如将点云中的点分成不同的簇。

1-3 边缘检测准则(见4.2~4.4)
  • 这一节介绍了三种准则用于计算每个点的边界概率:角度准则Angle Criterion、半圆准则Halfdisc Criterion和形状准则Shape Criterion
1-3-1 角度准则Angle Criterion

请添加图片描述

  • 角度准则的原理是基于点与其邻域点之间的角度关系来判断一个点是否位于边界上。具体步骤如下:
    1. 对于点云中的每个点,找出其最近的几个邻居点(通常使用k-邻域)。
    2. 计算这些邻居点与当前点形成的向量。
    3. 分析这些向量之间的角度。如果这些角度中的最大值超过某个阈值,则认为当前点位于边界上,因为边界点通常会有较大的角度变化。
    4. 边界概率可以通过角度大小与预设阈值的比较来计算,如果角度接近阈值,则边界概率较低;如果远超阈值,则边界概率较高。

1-3-2 半圆准则Halfdisc Criterion

请添加图片描述

  • 半圆准则的原理是基于点与其邻居点的位置关系来判断边界概率。具体步骤如下:
    1. 对于每个点,计算其k-邻域内所有点的平均值,作为半圆的中心
    2. 计算当前点到半圆中心的距离。
    3. 比较这个距离与半圆的半径。如果当前点位于半圆的边缘,即其到半圆中心的距离接近半径,则认为该点有较高的边界概率。
    4. 边界概率可以通过当前点到半圆中心的距离与半径的比例来计算。
  • 原文提到半圆准则借鉴了2D图像中提取图像轮廓的算法原理,即在识别图像轮廓时,通过检测亮度偏差较大的像素点来提取边缘。

1-3-3 形状准则Shape Criterion

请添加图片描述

  • 形状准则利用了点的局部几何形状信息来判断边界概率。具体步骤如下:
    1. 对于每个点,计算其k-邻域内点的协方差矩阵。(协方差矩阵能告诉我们这些点在各个方向上的分布情况。)
    2. 计算协方差矩阵的特征值和特征向量。
    3. 分析特征值的大小。特征值可以帮助我们了解这些点是如何分布的。如果特征值之间有很大的差异,那就意味着这些点在某个方向上特别集中,而在其他方向上则比较分散。
    4. 边界概率可以通过特征值差异的量化来计算。特征值差异越大,边界概率越高。

1-3-4 法线估计
  • 值得一提的是由于角度准则平均准则都严重依赖于点 p 处的法线,因此在没有法线信息的情况下,需要对法线进行良好的估计。
    • 角度准则: 该准则通过计算邻域内点在切平面上的投影之间的最大夹角来判断点 p 是否位于边界上。如果点 p 位于边界上,那么其邻域内点在切平面上的投影将会出现较大的夹角,因为它们分布在边界两侧。而切平面的计算需要依赖于点 p 处的法线。
    • 平均准则: 该准则通过比较点 p 与其邻域内点的平均值来判断点 p 是否位于边界上。如果点 p 位于边界上,那么其邻域内点的平均值将会偏离点 p,因为它们分布在边界两侧。而邻域内点的平均值计算也需要依赖于点 p 处的法线,以便将邻域内点投影到切平面上进行计算。

1-4 概率融合(见4.5)
  • 上一节论文提到了三种准则用于计算一个点的边界概率,这一节中论文分析了三个方法的优势

    • 角判据: 擅长检测较宽的边界,并能突出边界点形成的细线。
    • 半圆判据: 更适合检测小孔,尤其是当孔被邻域图的边交叉时。
    • 形状判据: 在噪声环境下表现最佳。
  • 同时论文提出了一种加权和的方案:请添加图片描述

  • 其中,w̸、wµ 和 wφ 分别是角判据、半圆判据和形状判据的权重,且它们的和为 1。用户可以根据视觉检查调整这些权重,以适应不同的应用场景。例如,对于噪声模型,可以增加形状判据的权重。


1-5 边界一致性处理(见5.1)
  • 论文中这一小节主要介绍了如何利用边界点的连贯性来提高孔洞检测的鲁棒性。其核心思想是,边界上的任意点都至少有一个邻居点也属于边界(孔洞的边界不会是孤立的,而是由一系列相邻的边界点组成的。)。利用这一特性,可以采用以下步骤进行迭代分类:
    1. 初始分类: 首先根据用户定义的阈值,将边界概率大于该阈值的点标记为边界点。
    2. 迭代更新:(不断迭代的过程删除误检点
      1. 选择已标记为边界的点: 从所有已标记为边界的点中选择一个点 p。
      2. 寻找最大角度差的邻居点: 找到 p 的邻居点中,与 p 构成最大角度差的两个点 q1 和 q2。
      3. 检查邻居点是否为边界点: 检查 q1 和 q2 是否也是边界点。
      4. 更新边界点标记
        • 如果 q1 和 q2 都是边界点,则 p 保持为边界点。
        • 如果 q1 和 q2 中至少有一个不是边界点,则将 p 标记为内部点。
      5. 重复步骤 1-4: 重复以上步骤,直到没有更多点的状态发生变化。
    3. 构造最小生成树: 利用邻接图 G 构造最小生成树 (MST)。MST 中的环与边界环相对应。
    4. 环提取: 使用广度优先搜索 (BFS) 从 MST 中提取边界环。

1-6 论文小结
  • 至此论文的分析就到这里,后续的内容是有关轮廓上孔洞的检测,我们只需要前面有关点的轮廓概率的原理即可。pcl::BoundaryEstimation的实现中,使用到了和这篇论文在4.3小结提到的角度准则Angle Criterion一致。

2 pcl::BoundaryEstimation进行轮廓提取

2-1 计算法向量
  • 正如1-3-4 法线估计中提到的角度准则严重依赖于点 p 处的法线,故在进行轮廓提取之前,我们需要计算点集的法向量。(remaining_cloud是上一节通过Plane Model Segmentation分离出的平面以上的点云)
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
pcl::PointCloud<pcl::Normal>::Ptr  normals(new  pcl::PointCloud<pcl::Normal>);
pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;

normalEstimation.setInputCloud(remaining_cloud.makeShared());
normalEstimation.setSearchMethod(tree);
normalEstimation.setRadiusSearch(0.02);  // 法向量的半径
normalEstimation.compute(*normals);
  • 创建 Kd 树搜索对象: 使用 KdTree 类创建一个 Kd 树搜索对象 tree,用于加速近邻搜索。
  • 创建法线点云对象: 使用 PointCloud 类创建一个法线点云对象 normals,用于存储每个点的法线信息。
  • 创建法线估计对象: 使用 NormalEstimation 类创建一个法线估计对象 normalEstimation,用于计算点云中每个点的法线。
  • 设置输入点云: 使用 setInputCloud 方法将剩余点云 remaining_cloud 设置为输入点云。
  • 设置搜索方法: 使用 setSearchMethod 方法将 Kd 树搜索对象 tree 设置为搜索方法。
  • 设置搜索半径: 使用 setRadiusSearch 方法设置搜索半径为 0.02 米。这意味着每个点的法线将根据其 0.02 米范围内的邻域点进行计算。
  • 计算法线: 使用 compute 方法计算点云中每个点的法线,并将结果存储在 normals 对象中。
2-2 pcl 计算边界
  • 如下
pcl::PointCloud<pcl::Boundary>::Ptr boundaries(new pcl::PointCloud<pcl::Boundary>); //声明一个boundary类指针,作为返回值
boundaries->resize(remaining_cloud.size()); //初始化大小
pcl::BoundaryEstimation<pcl::PointXYZ, pcl::Normal, pcl::Boundary> boundary_estimation; //声明一个BoundaryEstimation类
boundary_estimation.setInputCloud(remaining_cloud.makeShared()); //设置输入点云
boundary_estimation.setInputNormals(normals); //设置输入法线
pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree_ptr(new pcl::search::KdTree<pcl::PointXYZ>);
boundary_estimation.setSearchMethod(kdtree_ptr); //设置搜寻k近邻的方式
boundary_estimation.setKSearch(30); //设置k近邻数量
boundary_estimation.setAngleThreshold(M_PI * 0.6); //设置角度阈值,大于阈值为边界
boundary_estimation.compute(*boundaries); //计算点云边界,结果保存在boundaries中
  • 创建边界点云对象: 使用 PointCloud 类创建一个边界点云对象 boundaries,用于存储每个点的边界信息。

  • 初始化大小: 使用 resize 方法将 boundaries 的大小初始化为剩余点云的大小。

  • 创建边界估计对象: 使用 BoundaryEstimation 类创建一个边界估计对象 boundary_estimation,用于计算点云的边界。

  • 设置输入点云和法线: 使用 setInputCloudsetInputNormals 方法将剩余点云 remaining_cloud 和法线点云 normals 设置为输入。

  • 创建 Kd 树搜索对象: 使用 KdTree 类创建一个 Kd 树搜索对象 kdtree_ptr,用于加速近邻搜索。

  • 设置搜索方法: 使用 setSearchMethod 方法将 Kd 树搜索对象 kdtree_ptr 设置为搜索方法。

  • 设置 k 近邻数量: 使用 setKSearch 方法设置 k 近邻数量为 30。这意味着每个点的边界信息将根据其 30 个近邻点进行计算。

  • 设置角度阈值: 使用 setAngleThreshold 方法设置角度阈值为 M_PI * 0.6。这意味着如果一个点的法线与其 k 个近邻点的法线之间的最大夹角大于该阈值,则该点将被视为边界点。请添加图片描述

  • 计算边界: 使用 compute 方法计算点云的边界,并将结果存储在 boundaries 对象中。

2-3 可视化轮廓点云
  • 值得注意的是pcl::PointCloud<pcl::Boundary>::Ptr boundaries不是正常的点云信息,如果需要使用viewer可视化,需要转换为正常的点云信息。
 pcl::PointCloud<pcl::PointXYZ>::Ptr boundary_cloud(new pcl::PointCloud<pcl::PointXYZ>);

// 遍历所有边界点,并将它们转换为pcl::PointXYZ类型
for (size_t i = 0; i < boundaries->size(); ++i)
{
	pcl::PointXYZ point;
	if(boundaries->points[i].boundary_point != 0)
	{
		boundary_cloud->push_back(remaining_cloud.points[i]);
	}
}
// 清除所有点云并重新添加
viewer.removeAllPointClouds();
pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> boundary_color_handler(plane_cloud.makeShared(), 0, 0, 255);
viewer.addPointCloud<pcl::PointXYZ>(boundary_cloud->makeShared(), boundary_color_handler, "boundary_cloud");
viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "boundary_cloud");

// 设置背景色
viewer.setBackgroundColor(0, 0, 0);

viewer.spinOnce(0.0001);
  • 效果如下,下图是本人在室内房间里头测试的画面,分别是洗衣液一瓶和一个椅子,可以看到点云的蓝色轮廓被很好的勾勒出来请添加图片描述请添加图片描述

  • 我们来做一个小实验,回顾角度准则Angle Criterion的其中一个步骤:
    3. 比较这个距离与半圆的半径。如果当前点位于半圆的边缘,即其到半圆中心的距离接近半径,则认为该点有较高的边界概率。

  • 对应的代码如下

boundary_estimation.setAngleThreshold(M_PI * 0.6); //设置角度阈值,大于阈值为边界
  • 我们略微增大和减小这个角度阈值,来看看轮廓的变化:

  • 0.2请添加图片描述

  • 0.9请添加图片描述

  • 可以看的出来,这个角度阈值直接决定轮廓最终提取的结果。


3 轮廓2D投射

  • 在这一节中,我希望把3维度的轮廓信息透色到2D的平面上,着非常简单。我们只需要把轮廓点云映射到一个平面上即可,这里我的相机是向下进行拍摄的(如下坐标系)请添加图片描述

  • 值得一提的是这个坐标系可以在viewer初始化的时候进行设置

viewer.addCoordinateSystem(0.1f); // 添加基础坐标系,大小为0.1
  • 而上述图像的点云信息其实是基于xoy平面进行拍摄获取到的(也就是从xoy平面往z轴看去,应该得到的是一张640* 480大小的平面图像),那么我们只需要把点云轮廓信息的z轴设置为零,并根据自己相机的映射关系映射,便可以得到轮廓信息的2D映射图
 cv::Mat contour_image = cv::Mat(480,640, CV_8UC3, cv::Scalar(255, 255, 255)); // 白色背景

if (boundary_cloud->size() > 0) {
	// 在新图像上绘制轮廓点
	for (const auto& point : *boundary_cloud) {
		// 转换点云坐标到图像坐标
		int u = static_cast<int>(point.x * 570 + 300); // 假设x坐标直接对应图像宽度
		int v = static_cast<int>(point.y * 570 + 220); // 假设y坐标直接对应图像高度

		// 检查点是否在图像范围内
		if (u >= 0 && u < contour_image.cols && v >= 0 && v < contour_image.rows) {
			// 在新图像上绘制点
			cv::circle(contour_image, cv::Point(u, v), 2, cv::Scalar(0, 0, 255), -1); // 使用蓝色绘制点,大小为2
		}
	}
}

// 显示新的轮廓图像
imshow("Contour Image", contour_image);
cv::waitKey(1); // 等待按键后继续
  • 地板,洗衣液,椅子请添加图片描述

  • 电脑,书桌(乱七八糟没收拾)![[da88a222cee5be765ab59bf5cf5f95d.png]]请添加图片描述

  • 操场请添加图片描述


4 完整代码

请添加图片描述

#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <sensor_msgs/PointCloud2.h>
#include <cv_bridge/cv_bridge.h> // Include c to convert ROS images to OpenCV images
#include <opencv2/opencv.hpp>
#include <sensor_msgs/PointCloud2.h>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <pcl/ModelCoefficients.h>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>
#include <pcl/sample_consensus/method_types.h>
#include <pcl/sample_consensus/model_types.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/voxel_grid.h>

#include <pcl/filters/extract_indices.h>
#include <pcl/filters/passthrough.h>
#include <pcl/search/kdtree.h> 
#include <pcl/features/normal_3d.h>
#include <pcl/features/boundary.h>
#include <pcl/common/transforms.h>
#include <pcl/visualization/range_image_visualizer.h>
using namespace cv;

class PCL_center
{
private:
    ros::NodeHandle nh;
    ros::Subscriber imageSub;
    ros::Subscriber depthImageSub;
    ros::Subscriber depthPCloudSub;
    ros::Subscriber pointCloudSub;
    pcl::visualization::PCLVisualizer viewer;
    pcl::PointCloud<pcl::PointXYZ> raw_cloud;
    pcl::ModelCoefficients::Ptr coefficients;
    pcl::PointIndices::Ptr inliers;
    cv_bridge::CvImagePtr cv_ptr;
    pcl::SACSegmentation<pcl::PointXYZ> seg;

    void imageCB(const sensor_msgs::ImageConstPtr& img_msg)
    {
    }

    void depthImageCB(const sensor_msgs::ImageConstPtr& dimg_msg)
    {
    }

    void depthPCloudCB(const sensor_msgs::PointCloud2ConstPtr& pc_msg)
    {
    }
    void pointCloudCB(const sensor_msgs::PointCloud2& pc_msg)
    {
        // 检查点云是否为空
        if (pc_msg.width * pc_msg.height == 0)
        {
            std::cout << "Received an empty point cloud message." << std::endl;
            return; // 如果消息为空,则返回
        }
        std::cout << "pointCloudCB()" << std::endl;
        // 从sensor_msgs/PointCloud2消息转换为PCL点云
        pcl::fromROSMsg(pc_msg, raw_cloud);
    }
public:
    PCL_center(ros::NodeHandle& nh_) : nh(nh_),
    viewer("3D Viewer"),coefficients(new pcl::ModelCoefficients),
    inliers(new pcl::PointIndices)
    {
        imageSub = nh.subscribe("/camera/color/image_raw", 10, &PCL_center::imageCB, this);
        depthImageSub = nh.subscribe("/camera/depth/image_raw", 10, &PCL_center::depthImageCB, this);
        depthPCloudSub = nh.subscribe("/camera/depth/points", 10, &PCL_center::depthPCloudCB, this);
        pointCloudSub=nh.subscribe("/camera/depth/points",10,&PCL_center::pointCloudCB,this);
        // 可选配置:是否优化模型系数
        seg.setOptimizeCoefficients(true);
        // 必选配置:设置分割的模型类型、分割算法、距离阈值、输入点云
        seg.setModelType(pcl::SACMODEL_PLANE);
        seg.setMethodType(pcl::SAC_RANSAC);
        seg.setDistanceThreshold(0.02);
   
        viewer.addCoordinateSystem(0.1f); // 添加基础坐标系,大小为0.1

       
    }
  

    void run()
    {
        while(ros::ok())
        {
            ros::spinOnce();
        
            if (raw_cloud.width * raw_cloud.height == 0)
            {
                std::cout << "Received an empty point cloud message." << std::endl;
                continue; // 如果消息为空,则返回
            }
            pcl::VoxelGrid<pcl::PointXYZ> sor;
            sor.setInputCloud (raw_cloud.makeShared());
            sor.setLeafSize (0.01f, 0.01f, 0.01f);
            sor.filter (raw_cloud);

            seg.setInputCloud(raw_cloud.makeShared());

            // 执行分割操作,并存储分割结果保存到点集合 inliers 及存储平面模型系数 coefficients
            seg.segment(*inliers, *coefficients);

      

            if (inliers->indices.size() == 0) {
                PCL_ERROR ("Could not estimate a planar model for the given dataset.");
                continue;
            }
            //std::cout<<coefficients->values[0]<<"x+"<<coefficients->values[1]<<"y+"<<coefficients->values[2]<<"z+"<<coefficients->values[3]<<std::endl;
            // 提取平面点云
            pcl::PointCloud<pcl::PointXYZ> plane_cloud;
            pcl::ExtractIndices<pcl::PointXYZ> extract;
            extract.setInputCloud(raw_cloud.makeShared());
            extract.setIndices(inliers);
            extract.setNegative(false); // 设置为false以提取平面点云
            extract.filter(plane_cloud);

            // 提取剩余点云
            pcl::PointCloud<pcl::PointXYZ> remaining_cloud;
            extract.setNegative(true); // 设置为true以提取非平面点云
            extract.filter(remaining_cloud);




            // 1 计算法向量
            pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
            pcl::PointCloud<pcl::Normal>::Ptr  normals(new  pcl::PointCloud<pcl::Normal>);
            pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> normalEstimation;
            normalEstimation.setInputCloud(remaining_cloud.makeShared());
            normalEstimation.setSearchMethod(tree);
            normalEstimation.setRadiusSearch(0.02);  // 法向量的半径
            normalEstimation.compute(*normals);
        
            //2 pcl计算边界
            pcl::PointCloud<pcl::Boundary>::Ptr boundaries(new pcl::PointCloud<pcl::Boundary>); //声明一个boundary类指针,作为返回值
            boundaries->resize(remaining_cloud.size()); //初始化大小
            pcl::BoundaryEstimation<pcl::PointXYZ, pcl::Normal, pcl::Boundary> boundary_estimation; //声明一个BoundaryEstimation类
            boundary_estimation.setInputCloud(remaining_cloud.makeShared()); //设置输入点云
            boundary_estimation.setInputNormals(normals); //设置输入法线
            pcl::search::KdTree<pcl::PointXYZ>::Ptr kdtree_ptr(new pcl::search::KdTree<pcl::PointXYZ>);
            boundary_estimation.setSearchMethod(kdtree_ptr); //设置搜寻k近邻的方式
            boundary_estimation.setKSearch(30); //设置k近邻数量
            boundary_estimation.setAngleThreshold(M_PI * 0.6); //设置角度阈值,大于阈值为边界
            boundary_estimation.compute(*boundaries); //计算点云边界,结果保存在boundaries中



            pcl::PointCloud<pcl::PointXYZ>::Ptr boundary_cloud(new pcl::PointCloud<pcl::PointXYZ>);

            // 遍历所有边界点,并将它们转换为pcl::PointXYZ类型
            for (size_t i = 0; i < boundaries->size(); ++i)
            {
                pcl::PointXYZ point;
                if(boundaries->points[i].boundary_point != 0)
                {
                    boundary_cloud->push_back(remaining_cloud.points[i]);

                }
            }


         cv::Mat contour_image = cv::Mat(480,640, CV_8UC3, cv::Scalar(255, 255, 255)); // 白色背景

        if (boundary_cloud->size() > 0) {
            // 在新图像上绘制轮廓点
            for (const auto& point : *boundary_cloud) {
                // 转换点云坐标到图像坐标
                int u = static_cast<int>(point.x * 570 + 300); // 假设x坐标直接对应图像宽度
                int v = static_cast<int>(point.y * 570 + 220); // 假设y坐标直接对应图像高度

                // 检查点是否在图像范围内
                if (u >= 0 && u < contour_image.cols && v >= 0 && v < contour_image.rows) {
                    // 在新图像上绘制点
                    cv::circle(contour_image, cv::Point(u, v), 2, cv::Scalar(0, 0, 255), -1); // 使用蓝色绘制点,大小为2
                }
            }
        }

	        // 显示新的轮廓图像
	        imshow("Contour Image", contour_image);
	        cv::waitKey(1); // 等待按键后继续
    
            // 清除所有点云并重新添加
            viewer.removeAllPointClouds();

            // 添加平面点云到可视化窗口,并设置颜色为红色
            pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> plane_color_handler(plane_cloud.makeShared(), 255, 0, 0);
            viewer.addPointCloud<pcl::PointXYZ>(plane_cloud.makeShared(), plane_color_handler, "plane cloud");
            viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "plane cloud");

            // 添加剩余点云到可视化窗口,并设置颜色为绿色
            pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> remaining_color_handler(remaining_cloud.makeShared(), 0, 255, 0);
            viewer.addPointCloud<pcl::PointXYZ>(remaining_cloud.makeShared(), remaining_color_handler, "remaining cloud");
            viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "remaining cloud");


            pcl::visualization::PointCloudColorHandlerCustom<pcl::PointXYZ> boundary_color_handler(plane_cloud.makeShared(), 0, 0, 255);
            viewer.addPointCloud<pcl::PointXYZ>(boundary_cloud->makeShared(), boundary_color_handler, "boundary_cloud");
            viewer.setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_POINT_SIZE, 1, "boundary_cloud");



            // 设置背景色
            viewer.setBackgroundColor(0, 0, 0);

            viewer.spinOnce(0.0001);



        }


    }
};

int main(int argc, char *argv[])
{
    ros::init(argc, argv, "pcl_test");
    ros::NodeHandle nh;
    PCL_center pcl_center(nh);
    pcl_center.run();
    namedWindow("Input Window", WINDOW_AUTOSIZE);

    ros::spin();
    destroyWindow("Input Window");
    return 0;
}


总结

  • 本文介绍点云轮廓提取的原理(论文解读 Detecting Holes in Point Set Surfaces)和 pcl::BoundaryEstimation 类的使用,并以代码示例展示了如何将 3D 点云轮廓信息投射到 2D 平面上。
  • 这个系列也是完结辣,感谢大家的支持,如有错误,欢迎指出!!!
  • 一把无敌的椅子请添加图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

电梯节能 引领趋势

电梯&#xff0c;之前对我们来说&#xff0c;就是让我们省时省力的工具&#xff0c;谁知电梯也可加装【节能设备】。 电梯节能评估&#xff0c;节电率达20%-50%。 电梯节能&#xff08;电梯回馈装置&#xff09;通常电梯在轻载上行&#xff0c;重载下行和平层停梯状态下&#…

监控和维护 Linux 系统的健康状态:从服务启动故障到操作系统查询

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

rar文件怎么打开?这几款软件压缩和查看很方便!

在这个数字化信息爆炸的时代&#xff0c;我们每天都会接触到各种各样的文件&#xff0c;其中RAR格式文件以其高压缩率和良好的文件保护特性&#xff0c;成为了许多人分享和存储大文件的首选。然而&#xff0c;面对这样一个看似“神秘”的文件格式&#xff0c;不少朋友可能会感到…

Stable Diffusion绘画 | 来训练属于自己的模型:配置完成,炼丹启动

前言 效率设置-优化器 优化器可以分为4类&#xff1a; 第一类 AdamW &#xff1a;梯度下降算法&#xff0c;结合自适应学习率&#xff0c;既可以快速收敛&#xff0c;又可以避免 Loss值 震荡 AdamW8bit&#xff1a;能降低显存占用&#xff0c;并略微加快训练速度&#xff0…

Mysql—主从复制的slave添加及延迟回放

MySQL 主从复制是什么&#xff1f; ​ MySQL 主从复制是指数据可以从一个 MySQL 数据库服务器主节点复制到一个或多个从节点。MySQL 默认采用异步复制方式&#xff0c;这样从节点不用一直访问主服务器来更新自己的数据&#xff0c;数据的更新可以在远程连接上进行&#xff0c;…

国产分布式数据库-tidb单机部署文档

tidb单机部署文档 1、创建用户 #创建用户 useradd tidb #设置密码 passwd tidb2、配置免密码登录 编辑/etc/sudoers文件,文末加入&#xff1a; tidb ALL(ALL) NOPASSWD:ALL如果想要控制某个用户(或某个组用户)只能执行root权限中的一部分命令, 或者允许某些用户使用sudo时…

充电桩设备升级扩展多段计费

一 项目背景 某省某市的一个充电桩项目近日收到业主需求&#xff0c;需在国庆节增加一个时间段&#xff08;深谷计费段&#xff09;&#xff0c;但充电桩设备仅支持4段&#xff08;尖时段&#xff0c;峰时段&#xff0c;平时段&#xff0c;谷时段&#xff09;&#xff0c;今…

【CoppeliaSim V4.7】The Python interpreter could not handle the wrapper script

[sandboxScript:error] The Python interpreter could not handle the wrapper script (or communication between the launched subprocess and CoppeliaSim could not be established via sockets). Make sure that the Python modules ‘cbor2’ and ‘zmq’ are properly i…

Spring MVC 基本配置步骤 总结

1.简介 本文记录Spring MVC基本项目拉起配置步骤。 2.步骤 在pom.xml中导入依赖&#xff1a; <dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>6.0.6</version><scope>…

关于javascript中防抖和节流的使用详解

防抖&#xff08;Debounce&#xff09;和节流&#xff08;Throttle&#xff09;是两种常见的优化技巧&#xff0c;通常用于控制函数在短时间内频繁触发的场景&#xff0c;尤其是在处理用户输入、滚动、窗口大小调整等事件时。它们的主要目的是减少不必要的函数调用&#xff0c;…

想把泰文从文本上识别,什么软件工具好用呢?

泰文识别技术涉及将泰文图像转换成数字文本&#xff0c;主要通过光学字符识别&#xff08;OCR&#xff09;技术实现。这项技术广泛应用于文档处理、语言学习和翻译服务。实现泰文识别的方法包括使用手机应用程序、在线服务、专业软件&#xff0c;以及结合人工智能和机器学习。此…

UE5 C++: 插件编写04 | 自动增加前缀

准备工作 UObject* Asset UObject* Asset 通常指的是一个指向UObject的指针。UObject是Unreal Engine中的基类&#xff0c;几乎所有的引擎对象都继承自UObject。这个指针可以引用任何派生自UObject的对象&#xff0c;比如蓝图、材质、贴图、音频资源等资产。 如果你看到UObj…

【C++】——set和map的使用

文章目录 set的特性set初始化set迭代器和常见成员函数multisetmap的特性map初始化map迭代器和常见成员函数insert[]运算符重载multimap set的特性 自动排序&#xff1a; set中的元素会默认排升序存储唯一性&#xff1a; set中每个元素都是唯一的&#xff0c;如果插入一个已有元…

如何找到实力突出的建站公司,2024网络建站公司推荐

选择网站建设公司需要考虑公司以下几点&#xff1a; 是否对的业务需求的了解程度如何&#xff1f; 与公司的文化契合度 相同企业文化的公司&#xff0c;往往能取得很好的合作 沟通的方式 考虑&#xff1a;谁将是解决疑虑、查询、反馈的联系人&#xff0c;查询的响应时间是…

ShiroFilterFactoryBean登录认证成功后没有正常跳转到successUrl问题解决

问题出现&#xff1a; 分析&#xff1a;在配置了ShiroFilter之后&#xff0c;直接尝试在页面端访问login.jsp,但是login.jsp需要做认证过滤也就是FormAuthenticationFilter。 应为没有登录信息所以可想而知&#xff0c;会走ShiroFilterFactoryBean定义的loginUrl也就是认定为没…

VBA技术资料MF202:添加右键多按钮弹出菜单

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

使用PLSQL Developer快速连接数据库

文章目录 前言一、定义设置方式二、固定用户设置方式三、连接设置方式总结前言 PLSQL Developer是一个集成开发环境,由Allround Automations公司开发,专门面向Oracle数据库存储的程序单元的开发。该工具提供了多种设置方式,便于使用者在不需要输入用户名称、密码的情况下,…

易航网址导航系统V2.45完美去授权版

简介 易航网址导航系统V2.45完美去授权版 界面

2024 年 CSS 终于增加了垂直居中特性,效率翻倍!

在 2024 年的Chrome 123 版本中&#xff0c; CSS 原生可以使用 1 个 CSS 属性 align-content: center进行垂直居中。 有何魅力&#xff1f; 这个特性的魅力在哪儿呢&#xff1f;我举例给你看一下 <div style"align-content:center; height:200px; background: #614e…

18722 稀疏矩阵的运算

思路&#xff1a; 快速转置算法的基本思想是预先计算出转置后的三元组在新数组中的位置&#xff0c;然后直接将元素放到对应的位置上。这样做的好处是只需要遍历一次原数组&#xff0c;就可以完成转置操作。 步骤如下&#xff1a; 1. 初始化一个新的三元组数组&#xff0c;用于…