8 ROS2的三维视觉应用

ROS2的三维视觉应用

实验介绍

​ 在现代机器人系统中,三维视觉是一个越来越常见的组成部分。随着微软Kinect和英特尔Realsense 这类RGB-D相机的大量普及,基于三维视觉的相关算法也是百花齐放、空前繁荣,其中一部分技术方案已经开始应用到商用机器人上。在这一节实验中,将会了解三维视觉相机采集的数据是以什么形式存在于ROS2中,以及如何转换成三维点云库(PCL)的数据格式,为后续使用PCL对点云数据进行进一步处理奠定基础。
​ 三维点云数据的获取是通过订阅三维相机驱动节点发布的话题,从话题中获取相机发出的消息包来实现的。三维点云的话题名称一般是“<相机名称>/<可选分辨率>/points”的格式。在这个实验中,使用的虚拟机器人配备的是 Kinect V2 相机,所以话题名称为“/kinect2/sd/points”,话题中的消息包格式为 sensor_msgs::PointCloud2。

在这里插入图片描述

​ 这个实验将会实现一个订阅者节点,订阅相机发布的话题“/kinect2/sd/points”。从此话题中接收 sensor_msgs::PointCloud2类型的消息包,并将其中的点云数据转换成PCL格式然后把所有三维点的坐标值显示在终端。

实验过程

RGB-D相机的三维点云数据获取

cd ~/ros2_ws/srcbash

​ 首先在工作空间中创建一个软件包,打开一个新的终端窗口,输入如下指令,进入工作空间。

cd ~/ros2_ws/src

​ 然后用如下指令创建一个名为“pc_pkg”的软件包(“pc”是“Point Cloud”的简称)。

ros2 pkg create pc_pkg

​ 创建好软件包后,接下来在这个软件包中创建一个节点,具体操作步骤如下。

1.编写节点代码

​ 先创建这个节点的源码文件,在VSCode中找到[pc_pkg]软件包,用鼠标右键单击它的[src]子目录,在弹出的快捷菜单中选择[新建文件],此时会提示输人文件名,输入“pc_data.cpp”,然后按[Enter]键创建文件。

​ 下面编写这个源码文件,其内容如下。

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>
#include <pcl/point_types.h>
#include <pcl/point_cloud.h>
#include <pcl_conversions/pcl_conversions.h>

std::shared_ptr<rclcpp::Node> node;

void PointcloudCallback(const sensor_msgs::msg::PointCloud2::SharedPtr msg)
{
  pcl::PointCloud<pcl::PointXYZ> pointCloudIn;
  pcl::fromROSMsg(*msg, pointCloudIn);

  int cloudSize = pointCloudIn.points.size();
  for (int i = 0; i < cloudSize; i++)
  {
    RCLCPP_INFO(node->get_logger(), "[i= %d] ( %.2f , %.2f , %.2f)",
                i,
                pointCloudIn.points[i].x,
                pointCloudIn.points[i].y,
                pointCloudIn.points[i].z);
  }
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  node = std::make_shared<rclcpp::Node>("pointcloud_data_node");
  auto pc_sub = node->create_subscription<sensor_msgs::msg::PointCloud2>(
    "/kinect2/sd/points",
    1,
    PointcloudCallback
  );

  rclcpp::spin(node);

  rclcpp::shutdown();

  return 0;
}

​ 上述代码可以从 wpr_simulation2 的例程文件中找到。如果编译报错,可以与wpr_simulation2demo_cpp\10_pc_data.cpp 文件中的代码进行比对。

​ 代码编写完成后,需要进行保存。保存成功后,编辑界面文件名后面的圆点符号会变成一个叉符号。

2.设置编译规则

​ 节点源码的编译规则写在 pc_pkg 的 CMakeLists.txt 文件里。在 VSCode 中打开这个文件,在这个文件里添加节点的编译规则。首先使用如下代码寻找节点源码中用到的依赖项。

find_package(rclcpp REQUIRED) 
find_package(sensor_msgs REQUIRED)
find_package(pcl_conversions REQUIRED)
find_package(pcl_ros REQUIRED)

​ 然后使用如下代码添加节点源码的编译规则

add_executable(pc_data src/pc_data.cpp)
ament_target_dependencies(pc_data
"rclcpp" "sensor_msgs" "pcl_conversions" "pcl_ros"
)

​ 最后使用如下代码添加节点编译完成后的安装规则。

install(TARGETS  pc_data
DESTINATION  lib/${PROJECT_NAME})

​ 这些内容可以从 wpr_simulation2 的例程文件中找到。如果编译报错,可以与 wpr_simulation2\demo_cmakelists\10_pc_data.txt 文件中的代码进行比对。

​ 上述规则添加完毕后,一定要保存文件,否则规则无法生效。

3.修改软件包信息

​ 在VSCode 中打开[pc_pkg]下的[package.xml]文件,使用如下代码添加依赖项信。

<depend>rclcpp</depend>
<depend>sensor_msgs</depend> 
<depend>pcl_conversions</depend>
<depend>pcl_ros</depend> 

​ 这些内容可以从 wpr_simulation2 的例程文件中找到。如果编译报错,可以与wpr_simulation2\demo_package\10_pc_data.xml 文件中的代码进行比对。

​ 文件修改后,一定要保存文件,否则新的包信息无法生效。

4.编译软件包

​ 修改完上述文件后,打开终端窗口,执行如下指令,进入工作空间。

cd ~/ros2_ws

​ 然后执行如下指令,对工作空间中的所有软件包进行编译

colcon build
仿真运行点云数据获取程序

​ 下面运行刚编写的节点。在运行前,确认已经下载了wpr_simulation2 仿真项目。然后,需要加载当前工作空间中的环境设置,这样ros2指令才能找到刚才编译后的软件包和节点文件。保持终端的当前路径依然在工作空间目录ros2_ws中然后执行如下指令。

source install/setup.bash

​ 然后,执行如下指令,启动带有机器人的仿真环境

ros2 launch wpr_simulation2 wpb_table.launch.py

​ 这时会启动仿真窗口,里面有一台虚拟机器人。机器人的面前摆放着一张桌子,桌子上放有两瓶饮料。

在这里插入图片描述

​ 接下来运行刚才编写的节点 pc_data。在Terminator 终端中,按组合键[Ctrl+shift+O],将终端分为上、下两个子窗口。在新的终端窗口中执行如下指令,加载工作空间的环境设置。

source install/setup.bash

​ 然后执行如下指令,运行刚才编写的 pc_data 节点

ros2 run pc_pkg pc_data

​ 节点运行起来之后,会不停地刷新显示点云所有点的三维坐标值,点云数据获取成功。

在这里插入图片描述

使用PCL进行物品检测

​ 在前面的实验里,实现了从ROS2 机器人头部的RGB-D相机获取三维点云数据。这次将继续深入,使用PCL实现三维特征提取,并对桌面上的物体进行检测和定位。在编写例程代码前,先确定程序的实现思路:

​ 1)对机器人头部相机采集到的三维点云消息包进行格式转换,从ROS2的点云格式转换为 PCL 点云格式,以方便后面调用PCL的函数对点云数据进行处理。

​ 2)先使用 PCL 函数对点云数据进行平面提取,将桌面的高度确定下来。

​ 3)将桌面高度以下的点集剔除掉,仅保留桌面之上的物体点云。

​ 4)使用欧几里得分割法对保留下来的物体点云进行点云簇的提取,将桌面上的多个相隔较远的点云簇区分开。这时可以认为每个点云簇就表示一个物体的点云集合。计算每个物体点云集合的质心坐标,用来表示物体的空间位置。

编写物品检测程序

​ 首先在工作空间中创建一个名为“pc_pkg”的软件包,如果前面已经创建过,这里可以直接跳过,打开一个新的终端窗口,输入如下指令,进入工作空间。

cd ~/ros2_ws/src

​ 然后用如下指令创建软件包

ros2 pkg create pc_pkg

​ 创建好软件包后,接下来在这个软件包中创建一个节点,具体操作步骤如下

1.编写节点代码

​ 先创建这个节点的源码文件,在VSCode中找到[pc_pkg]软件包用鼠标右键单击它的[src]子目录,在弹出的快捷菜单中选择「新建文件]。此时会提示输入文件名,输入“pc_objects.cpp”,’,然后按[Enter]键创建文件。

​ 下面编写这个源码文件,其内容如下

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>
#include <pcl/point_types.h>
#include <pcl_conversions/pcl_conversions.h>
#include <tf2_ros/transform_listener.h>
#include <pcl_ros/transforms.hpp>
#include <pcl/filters/passthrough.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/search/kdtree.h>
#include <pcl/segmentation/extract_clusters.h>

std::shared_ptr<rclcpp::Node> node;
tf2_ros::Buffer::SharedPtr tf_buffer_;
std::shared_ptr<tf2_ros::TransformListener> tf_listener_;

void PointcloudCallback(const sensor_msgs::msg::PointCloud2::SharedPtr msg)
{
  bool result = tf_buffer_->canTransform(
    "base_footprint", 
    msg->header.frame_id, 
    msg->header.stamp
  );
  if (!result)
  {
    return;
  }
  sensor_msgs::msg::PointCloud2 pc_footprint;
  pcl_ros::transformPointCloud(
    "base_footprint", 
    *msg, 
    pc_footprint, 
    *tf_buffer_
  );

  pcl::PointCloud<pcl::PointXYZ> cloud_src;
  pcl::fromROSMsg(pc_footprint, cloud_src);

  pcl::PassThrough<pcl::PointXYZ> pass;
  pass.setInputCloud(cloud_src.makeShared());
  pass.setFilterFieldName("x");
  pass.setFilterLimits(0.5, 1.5);
  pass.filter(cloud_src);
  pass.setInputCloud(cloud_src.makeShared());
  pass.setFilterFieldName("y");
  pass.setFilterLimits(-0.5, 0.5);
  pass.filter(cloud_src);
  pass.setInputCloud(cloud_src.makeShared());
  pass.setFilterFieldName("z");
  pass.setFilterLimits(0.5, 1.5);
  pass.filter(cloud_src);

  pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
  pcl::SACSegmentation<pcl::PointXYZ> segmentation;
  segmentation.setInputCloud(cloud_src.makeShared());
  segmentation.setModelType(pcl::SACMODEL_PLANE);
  segmentation.setMethodType(pcl::SAC_RANSAC);
  segmentation.setDistanceThreshold(0.05);
  segmentation.setOptimizeCoefficients(true);
  pcl::PointIndices::Ptr planeIndices(new pcl::PointIndices);
  segmentation.segment(*planeIndices, *coefficients);

  int point_num = planeIndices->indices.size();
  float points_z_sum = 0;
  for (int i = 0; i < point_num; i++)
  {
    int point_index = planeIndices->indices[i];
    points_z_sum += cloud_src.points[point_index].z;
  }
  float plane_height = points_z_sum / point_num;
  RCLCPP_INFO(node->get_logger(), "plane_height = %.2f", plane_height);

  pass.setInputCloud(cloud_src.makeShared());
  pass.setFilterFieldName("z");
  pass.setFilterLimits(plane_height + 0.2, 1.5);
  pass.filter(cloud_src);

  pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
  tree->setInputCloud(cloud_src.makeShared());

  pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec; 
  ec.setInputCloud(cloud_src.makeShared());
  ec.setMinClusterSize(100);
  ec.setMaxClusterSize(25000);
  ec.setClusterTolerance(0.1);
  ec.setSearchMethod(tree);
  std::vector<pcl::PointIndices> cluster_indices;
  ec.extract(cluster_indices);
  int object_num = cluster_indices.size(); 
  RCLCPP_INFO(node->get_logger(), "object_num = %d",object_num);
  for(int i = 0 ; i < object_num ; i ++)
  {
      int point_num =  cluster_indices[i].indices.size();
      float points_x_sum = 0;
      float points_y_sum = 0;
      float points_z_sum = 0;
      for(int j = 0 ; j < point_num ; j ++)
      {
          int point_index = cluster_indices[i].indices[j];
          points_x_sum += cloud_src.points[point_index].x;
          points_y_sum += cloud_src.points[point_index].y;
          points_z_sum += cloud_src.points[point_index].z;
      }
      float object_x = points_x_sum/point_num;
      float object_y = points_y_sum/point_num;
      float object_z = points_z_sum/point_num;
      RCLCPP_INFO(
        node->get_logger(), 
        "object %d pos=(%.2f , %.2f , %.2f)",
        i, 
        object_x,
        object_y,
        object_z
      );
  }
  RCLCPP_INFO(node->get_logger(), "---------------------" );
}

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  node = std::make_shared<rclcpp::Node>("pointcloud_objects_node");

  tf_buffer_ = std::make_shared<tf2_ros::Buffer>(node->get_clock());
  tf_listener_ = std::make_shared<tf2_ros::TransformListener>(*tf_buffer_);

  auto pc_sub = node->create_subscription<sensor_msgs::msg::PointCloud2>(
    "/kinect2/sd/points",
    1,
    PointcloudCallback
  );

  rclcpp::spin(node);

  rclcpp::shutdown();

  return 0;
}

​ 上述代码可以从 wpr_simulation2 的例程文件中找到。如果编译报错,可以与 wpr_simulation2\demo_cpp\10_pc_objects.cpp 文件中的代码进行比对。

​ 代码编写完成后,需要进行保存。保存成功后,编辑界面文件名后面的圆点符号会变成一个叉符号。

2.设置编译规则

​ 节点源码的编译规则写在 pc_pkg的CMakeLists.txt 文件里。在 VSCode 中打开这个文件在这个文件里添加节点的编译规则。首先使用如下代码寻找节点代码中用到的依赖项。

find_package(rclcpp REQUIRED) 
find_package(sensor_msgs REQUIRED)
find_package(pcl_conversions REQUIRED)
find_package(pcl_ros REQUIRED)

​ 然后使用如下代码添加节点源码的编译规则。

add_executable(pc_objects src/pc_objects.cpp)
ament_target_dependencies(pc_objects
"rclcpp" "sensor_msgs" "pcl_conversions" "pcl_ros"
)

​ 最后使用如下代码添加节点编译完成后的安装规则。

install(TARGETS  pc_objects
DESTINATION  lib/${PROJECT_NAME})

​ 这些内容可以从 wpr_simulation2 的例程文件中找到。如果编译报错,可以与 wpr_simulation2\demo_cmakelists\10_pc_objects.txt 文件中的代码进行比对。

​ 上述规则添加完毕后,一定要保存文件,否则规则无法生效。

3.修改软件包信息

​ 在 VSCode 中打开[pc_pkg]下的[package.xml]文件,使用如下代码添加依赖项信息

<depend>rclcpp</depend>
<depend>sensor_msgs</depend> 
<depend>pcl_conversions</depend>
<depend>pcl_ros</depend> 

​ 这些内容可以从 wpr_simulation2的例程文件中找到。如果编译报错,可以与 wpr_simulation2\demo_package\10_pc_objects.xml 文件中的代码进行比对。

​ 文件修改后,一定要保存文件,否则新的包信息无法生效。

4.编译软件包

​ 修改完上述文件后,打开终端窗口。执行如下指令,进入工作空间。

cd ~/ros2_ws

​ 然后执行如下指令,对工作空间中的所有软件包进行编译

colcon build
仿真运行物品检测程序

​ 下面运行刚编写的节点。在运行前,确认已经下载了wpr_simulation2 仿真项目。然后,需要加载当前工作空间中的环境设置,这样ros2指令才能找到刚才编译后的软件包和节点文件,保持终端的当前路径依然在工作空间目录ros2_ws中然后执行如下指令。

source install/setup.bash

​ 然后,先启动带有机器人的仿真环境,执行如下指令

ros2 launch wpr_simulation2 wpb_table.launch.py

​ 这时会启动仿真窗口,里面有一台虚拟机器人。机器人的面前摆放着一张桌子,桌子上放有两瓶饮料。

​ 接下来运行刚才编写的节点 pc_objects。在 Terminator 终端中,按组合键[Ctrl+Shift+O],将终端分为上、下两个子窗口。先在新的终端窗口中执行如下指令,加载工作空间的环境设置。

source install/setup.bash

​ 然后执行如下指令,运行刚才编写的pc_objects 节点

ros2 run pc_pkg pc_objects

​ 节点运行起来之后,会刷新显示物品检测的结果信息

在这里插入图片描述

​ 其中,plane_height 表示程序检测到的桌面的高度,单位为米(m);object_num 表示在桌面上检测到的物品点云簇个数;objectnpos=(x,y,z)表示第n个物体质心的三维坐标值,单位为米(m);每一帧点云数据包处理结束会输出一条分割符号。

​ 从输出信息可以看出,机器人检测到桌子的高度为0.78m;桌子上有两个物体;两个物体与机器人的距离一致,都是1.09m;横向位置不一致,0号物体在机器人右侧0.3m处1号物体在机器人左侧0.2m处;两个物体质心距离地面的高度都是0.99m。可以在仿真窗口里对比一下这些数值和两瓶饮料相对机器人的空间位置。至此,基于三维点云的物体检测功能就完成了。

实验总结

​ 在本次实验中,我们成功实现了ROS2中的三维视觉应用,包括三维点云数据的获取和物品检测。我们创建了一个名为pointcloud_data_node的ROS2节点,订阅了/kinect2/sd/points话题,接收并转换点云数据,实时显示每个点的三维坐标值。在此基础上,我们实现了物品检测功能,通过平面提取、点云过滤和欧几里得聚类算法,准确检测并定位桌面上的物体,输出每个物体的质心坐标。实验验证了点云数据的正确性和实时性,以及物品检测算法的有效性和鲁棒性。通过这次实验,我们不仅掌握了ROS2和PCL的使用方法,还为未来的机器人项目开发积累了宝贵的经验和技术支持。

ROS (Robot Operating System) 双目视觉三维重建是一种利用两台相机从两个不同的视角捕捉同一场景,通过计算图像之间的视差信息,进而构建出场景的三维模型的技术。这种技术广泛应用机器人导航、自动驾驶车辆以及无人机等领域,对于物体识别、定位等任务具有重要作用。 ### 工作原理 1. **图像捕获**:首先使用两台相机同时拍摄同一场景的不同视角的图片。这两张图像被称为“左图”和“右图”。 2. **特征点匹配**:在两张图片中寻找对应的特征点,并进行匹配。特征点通常选择容易识别且分布均匀的区域,如角点、边缘或纹理丰富的区域。 3. **视差计算**:由于相机的位置不同,相同的实体在两张图像中的位置会有微小差异,这个差异称为“视差”。通过对特征点在左右图像中的位置差异进行计算,可以得到每个特征点的视差值。 4. **深度信息计算**:基于视差与相机到目标的距离之间的关系,即“基线距离公式”,可以推算出每个特征点的实际深度。公式为: \[d = \frac{B \cdot f}{p}\] 其中 \(d\) 表示深度,\(B\) 表示相机间的基线长度,\(f\) 表示焦距,而 \(p\) 则是在一张图像中特征点的投影坐标,在另一张图像中则对应于 \(p'\),两者之差即为视差。 5. **三维重建**:将所有匹配成功并计算出深度的信息整合起来,就可以建立起整个场景的三维模型。这通常会涉及到一些数据结构的构建,比如点云数据、网格化表示或更复杂的模型。 6. **优化过程**:为了提高重建精度和稳定性,通常会对初始估计的结果进行迭代优化,减少噪声影响,提升重建质量。 ### ROS框架中的实现 在ROS环境中,可以通过一系列的节点和包来进行双目视觉的处理。例如,`openni2`库支持Kinect和其他传感器的使用,而`cv_bridge`可以帮助转换ROS的消息格式与OpenCV的数据类型。`rosbag`可以用于记录和回放传感器数据,便于调试和分析。 用户需要编写特定的功能模块,如图像预处理、特征检测与匹配、视差计算、深度映射生成等,然后使用ROS的发布者和订阅者机制将各个部分连接起来。此外,还可以利用ROS的图形界面工具如Rqt、Gazebo等进行系统监控和仿真测试。 ### 应用实例及挑战 应用实例包括但不限于: - 自动驾驶中的环境感知与避障。 - 服务机器人进行物品定位与抓取操作。 - 检测与追踪移动对象。 挑战主要包括: - 视觉噪音和遮挡导致的匹配误差。 - 环境光照变化对图像质量的影响。 - 实时性和计算资源的平衡,尤其是在移动平台上实现高效算法。 - 对不同尺度和形状物体的有效建模。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值