Ros2 Moveit 之 使用 MoveIt 任务构造器进行拾取和放置
本教程将引导您使用MoveIt Task Constructor创建一个用于规划拾取和放置操作的包。MoveIt Task Constructor 提供了一种规划由多个不同子任务(称为阶段)组成的任务的方法。如果您只想运行本教程,可以按照Docker 指南启动包含完整教程的容器。
1 基本概念
MTC 的基本思想是将复杂的运动规划问题组合成一组更简单的子问题。顶层规划问题被指定为任务,而所有子问题则由阶段指定。阶段可以按任意顺序排列,层次结构仅受各个阶段类型的限制。阶段的排列顺序受结果传递方向的限制。与结果流相关的可能阶段有三个:生成器、传播器和连接器阶段:
生成器独立于相邻阶段计算结果,并向前和向后传递结果。例如,用于几何姿势的 IK 采样器,其中接近和离开运动(相邻阶段)取决于解决方案。
传播器接收一个邻居阶段的结果,解决子问题,然后将结果传播到对面站点的邻居。根据实现方式,传播阶段可以分别向前、向后或双向传递解决方案。一个例子是根据起始状态或目标状态计算笛卡尔路径的阶段。
连接器不会传播任何结果,而是尝试弥合两个邻居的结果状态之间的差距。一个例子是计算从一个给定状态到另一个给定状态的自由运动计划。
除了顺序类型之外,还有不同的层次结构类型,允许封装下级阶段。没有下级阶段的阶段称为原始阶段,高级阶段称为容器阶段。有三种容器类型:
包装器封装单个从属阶段并修改或过滤结果。例如,仅接受满足特定约束的子阶段解决方案的过滤阶段可以实现为包装器。此类型的另一个标准用途包括 IK 包装器阶段,它根据标注有姿势目标属性的规划场景生成逆运动学解决方案。
串行容器包含一系列从属阶段,并且仅将端到端解决方案视为结果。例如,拾取动作由一系列连贯的步骤组成。
并行容器结合了一组从属阶段,可用于传递最佳备选结果、运行后备求解器或合并多个独立解决方案。例如,运行自由运动计划的备选规划器、用右手拾取物体或用左手作为后备,或者同时移动手臂和打开夹持器。
阶段不仅支持解决运动规划问题,还可用于各种状态转换,例如修改规划场景。结合使用类继承的可能性,可以仅依靠一组结构良好的原始阶段来构建非常复杂的行为。
有关 MTC 的更多详细信息,请参阅MoveIt 任务构造器概念页面
2 入门
如果您还没有这样做,请确保您已经完成入门指南中的步骤。
2.1 下载 MoveIt 任务构造器
移入您的 colcon 工作区并拉取 MoveIt Task Constructor 源:
cd ~/ws_moveit/src
git clone git@github.com:moveit/moveit_task_constructor.git -b ros2
3 尝试一下
MoveIt Task Constructor 包包含几个基本示例和一个拾取放置演示。对于所有演示,您都应该启动基本环境:
ros2 launch moveit_task_constructor_demo demo.launch.py
随后,您可以运行各个演示:
ros2 launch moveit_task_constructor_demo cartesian.launch.py
ros2 launch moveit_task_constructor_demo modular.launch.py
ros2 launch moveit_task_constructor_demo pickplace.launch.py
在右侧,您应该会看到运动规划任务面板,其中概述了任务的层次结构。当您选择特定阶段时,成功和失败的解决方案列表将显示在最右侧的窗口中。选择其中一个解决方案将开始其可视化。
4 使用 MoveIt 任务构造器设置项目
本节介绍使用 MoveIt 任务构造器构建简单任务所需的步骤。
4.1 创建新包
使用以下命令创建一个新包:
ros2 pkg create \ --build-type ament_cmake \ --dependencies moveit_task_constructor_core rclcpp \ --node-name mtc_node mtc_tutorial
这将创建一个名为 的新包和文件夹,它mtc_tutorial
具有 的依赖关系moveit_task_constructor_core
,以及 中的 hello world 示例src/mtc_node
。
4.2 代码
在您选择的编辑器中打开mtc_node.cpp
,并粘贴以下代码。
#include <rclcpp/rclcpp.hpp> #include <moveit/planning_scene/planning_scene.h> #include <moveit/planning_scene_interface/planning_scene_interface.h> #include <moveit/task_constructor/task.h> #include <moveit/task_constructor/solvers.h> #include <moveit/task_constructor/stages.h> #if __has_include(<tf2_geometry_msgs/tf2_geometry_msgs.hpp>) #include <tf2_geometry_msgs/tf2_geometry_msgs.hpp> #else #include <tf2_geometry_msgs/tf2_geometry_msgs.h> #endif #if __has_include(<tf2_eigen/tf2_eigen.hpp>) #include <tf2_eigen/tf2_eigen.hpp> #else #include <tf2_eigen/tf2_eigen.h> #endif static const rclcpp::Logger LOGGER = rclcpp::get_logger("mtc_tutorial"); namespace mtc = moveit::task_constructor; class MTCTaskNode { public: MTCTaskNode(const rclcpp::NodeOptions& options); rclcpp::node_interfaces::NodeBaseInterface::SharedPtr getNodeBaseInterface(); void doTask(); void setupPlanningScene(); private: // Compose an MTC task from a series of stages. mtc::Task createTask(); mtc::Task task_; rclcpp::Node::SharedPtr node_; }; MTCTaskNode::MTCTaskNode(const rclcpp::NodeOptions& options) : node_{ std::make_shared<rclcpp::Node>("mtc_node", options) } { } rclcpp::node_interfaces::NodeBaseInterface::SharedPtr MTCTaskNode::getNodeBaseInterface() { return node_->get_node_base_interface(); } void MTCTaskNode::setupPlanningScene() { moveit_msgs::msg::CollisionObject object; object.id = "object"; object.header.frame_id = "world"; object.primitives.resize(1); object.primitives[0].type = shape_msgs::msg::SolidPrimitive::CYLINDER; object.primitives[0].dimensions = { 0.1, 0.02 }; geometry_msgs::msg::Pose pose; pose.position.x = 0.5; pose.position.y = -0.25; pose.orientation.w = 1.0; object.pose = pose; moveit::planning_interface::PlanningSceneInterface psi; psi.applyCollisionObject(object); } void MTCTaskNode::doTask() { task_ = createTask(); try { task_.init(); } catch (mtc::InitStageException& e) { RCLCPP_ERROR_STREAM(LOGGER, e); return; } if (!task_.plan(5)) { RCLCPP_ERROR_STREAM(LOGGER, "Task planning failed"); return; } task_.introspection().publishSolution(*task_.solutions().front()); auto result = task_.execute(*task_.solutions().front()); if (result.val != moveit_msgs::msg::MoveItErrorCodes::SUCCESS) { RCLCPP_ERROR_STREAM(LOGGER, "Task execution failed"); return; } return; } mtc::Task MTCTaskNode::createTask() { mtc::Task task; task.stages()->setName("demo task"); task.loadRobotModel(node_); constauto&arm_group_name="panda_arm"; constauto&hand_group_name="hand"; constauto&hand_frame="panda_hand"; // Set task properties task.setProperty("group",arm_group_name); task.setProperty("eef",hand_group_name); task.setProperty("ik_frame",hand_frame); // Disable warnings for this line, as it's a variable that's set but not used in this example #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wunused-but-set-variable" mtc::Stage*current_state_ptr=nullptr;// Forward current_state on to grasp pose generator #pragma GCC diagnostic pop autostage_state_current=std::make_unique<mtc::stages::CurrentState>("current"); current_state_ptr=stage_state_current.get(); task.add(std::move(stage_state_current)); autosampling_planner=std::make_shared<mtc::solvers::PipelinePlanner>(node_); autointerpolation_planner=std::make_shared<mtc::solvers::JointInterpolationPlanner>(); autocartesian_planner=std::make_shared<mtc::solvers::CartesianPath>(); cartesian_planner->setMaxVelocityScalingFactor(1.0); cartesian_planner->setMaxAccelerationScalingFactor(1.0); cartesian_planner->setStepSize(.01); autostage_open_hand= std::make_unique<mtc::stages::MoveTo>("open hand",interpolation_planner); stage_open_hand->setGroup(hand_group_name); stage_open_hand->setGoal("open"); task.add(std::move(stage_open_hand)); returntask; } intmain(intargc,char**argv) { rclcpp::init(argc,argv); rclcpp::NodeOptionsoptions; options.automatically_declare_parameters_from_overrides(true); automtc_task_node=std::make_shared<MTCTaskNode>(options); rclcpp::executors::MultiThreadedExecutorexecutor; autospin_thread=std::make_unique<std::thread>([&executor,&mtc_task_node](){ executor.add_node(mtc_task_node->getNodeBaseInterface()); executor.spin(); executor.remove_node(mtc_task_node->getNodeBaseInterface()); }); mtc_task_node->setupPlanningScene(); mtc_task_node->doTask(); spin_thread->join(); rclcpp::shutdown(); return0; }
4.3 代码分解
代码的顶部包括此包使用的 ROS 和 MoveIt 库。
rclcpp/rclcpp.hpp
包括核心 ROS2 功能
moveit/planning_scene/planning_scene.h
并moveit/planning_scene_interface/planning_scene_interface.h
包括与机器人模型和碰撞对象的接口功能
moveit/task_constructor/task.h
、、moveit/task_constructor/solvers.h
并moveit/task_constructor/stages.h
包括示例中使用的 MoveIt Task Constructor 的不同组件
tf2_geometry_msgs/tf2_geometry_msgs.hpp
并且tf2_eigen/tf2_eigen.hpp
不会在初始示例中使用,但是当我们向 MoveIt 任务构造函数任务添加更多阶段时,它们将用于姿势生成。
#include <rclcpp/rclcpp.hpp> #include <moveit/planning_scene/planning_scene.h> #include <moveit/planning_scene_interface/planning_scene_interface.h> #include <moveit/task_constructor/task.h> #include <moveit/task_constructor/solvers.h> #include <moveit/task_constructor/stages.h> #if __has_include(<tf2_geometry_msgs/tf2_geometry_msgs.hpp>) #include <tf2_geometry_msgs/tf2_geometry_msgs.hpp> #else #include <tf2_geometry_msgs/tf2_geometry_msgs.h> #endif #if __has_include(<tf2_eigen/tf2_eigen.hpp>) #include <tf2_eigen/tf2_eigen.hpp> #else #include <tf2_eigen/tf2_eigen.h> #endif
moveit::task_constructor
下一行获取新节点的记录器。为了方便起见,我们还创建了命名空间别名。
static const rclcpp::Logger LOGGER = rclcpp::get_logger("mtc_tutorial"); namespace mtc = moveit::task_constructor;
我们首先定义一个包含主要 MoveIt 任务构造函数功能的类。我们还将 MoveIt 任务构造函数任务对象声明为我们类的成员变量:这对于给定的应用程序来说并不是绝对必要的,但它有助于保存任务以供以后可视化。我们将在下面分别探讨每个函数。
class MTCTaskNode { public: MTCTaskNode(const rclcpp::NodeOptions& options); rclcpp::node_interfaces::NodeBaseInterface::SharedPtr getNodeBaseInterface(); void doTask(); void setupPlanningScene(); private: // Compose an MTC task from a series of stages. mtc::Task createTask(); mtc::Task task_; rclcpp::Node::SharedPtr node_; };
这些行使用指定的选项初始化节点(它是我们MTCTaskNode
类的构造函数)。
MTCTaskNode::MTCTaskNode(const rclcpp::NodeOptions& options) : node_{ std::make_shared<rclcpp::Node>("mtc_node", options) } { }
接下来的几行定义了一个 getter 函数来获取节点基接口,稍后将用于执行器。
rclcpp::node_interfaces::NodeBaseInterface::SharedPtr MTCTaskNode::getNodeBaseInterface() { return node_->get_node_base_interface(); }
此类方法用于设置示例中使用的规划场景。它创建一个圆柱体,其尺寸由 指定object.primitives[0].dimensions
,位置由pose.position.x
和指定pose.position.y
。您可以尝试更改这些数字来调整圆柱体的大小并移动它。如果我们将圆柱体移出机器人的范围,规划将失败。
void MTCTaskNode::setupPlanningScene() { moveit_msgs::msg::CollisionObject object; object.id = "object"; object.header.frame_id = "world"; object.primitives.resize(1); object.primitives[0].type = shape_msgs::msg::SolidPrimitive::CYLINDER; object.primitives[0].dimensions = { 0.1, 0.02 }; geometry_msgs::msg::Pose pose; pose.position.x = 0.5; pose.position.y = -0.25; object.pose = pose; moveit::planning_interface::PlanningSceneInterface psi; psi.applyCollisionObject(object); }
此函数与 MoveIt Task Constructor 任务对象交互。它首先创建一个任务,其中包括设置一些属性和添加阶段。这将在createTask
函数定义中进一步讨论。接下来,task.init()
初始化任务并task.plan(5)
生成计划,在找到 5 个成功计划后停止。下一行发布要在 RViz 中可视化的解决方案 - 如果您不关心可视化,可以删除此行。最后,task.execute()
执行该计划。执行通过与 RViz 插件的操作服务器接口进行。
void MTCTaskNode::doTask() { task_ = createTask(); try { task_.init(); } catch (mtc::InitStageException& e) { RCLCPP_ERROR_STREAM(LOGGER, e); return; } if (!task_.plan(5)) { RCLCPP_ERROR_STREAM(LOGGER, "Task planning failed"); return; } task_.introspection().publishSolution(*task_.solutions().front()); auto result = task_.execute(*task_.solutions().front()); if (result.val != moveit_msgs::msg::MoveItErrorCodes::SUCCESS) { RCLCPP_ERROR_STREAM(LOGGER, "Task execution failed"); return; } return; }
如上所述,此函数创建一个 MoveIt Task Constructor 对象并设置一些初始属性。在本例中,我们将任务名称设置为“demo_task”,加载机器人模型,定义一些有用帧的名称,并使用 将这些帧名称设置为任务的属性。接下来的几个代码块将填充此函数体。task.setProperty(property_name, value)
mtc::Task MTCTaskNode::createTask() { moveit::task_constructor::Task task; task.stages()->setName("demo task"); task.loadRobotModel(node_); const auto& arm_group_name = "panda_arm"; const auto& hand_group_name = "hand"; const auto& hand_frame = "panda_hand"; // Set task properties task.setProperty("group", arm_group_name); task.setProperty("eef", hand_group_name); task.setProperty("ik_frame", hand_frame);
现在,我们向节点添加一个示例阶段。第一行设置current_state_ptr
为nullptr
;这会创建一个指向阶段的指针,以便我们可以在特定场景中重复使用阶段信息。此行目前未使用,但稍后在向任务添加更多阶段时将使用。接下来,我们创建一个current_state
阶段(生成器阶段)并将其添加到我们的任务中 - 这将以当前状态启动机器人。现在我们已经创建了阶段CurrentState
,我们将一个指向它的指针保存在中current_state_ptr
以供以后使用。
mtc::Stage* current_state_ptr = nullptr; // Forward current_state on to grasp pose generator auto stage_state_current = std::make_unique<mtc::stages::CurrentState>("current"); current_state_ptr = stage_state_current.get(); task.add(std::move(stage_state_current));
解算器用于定义机器人运动的类型。MoveIt Task Constructor 有三个解算器选项:
PipelinePlanner使用 MoveIt 的规划管道,通常默认为OMPL。
auto sampling_planner = std::make_shared<mtc::solvers::PipelinePlanner>(node_);JointInterpolation是一个简单的规划器,用于在起始关节状态和目标关节状态之间进行插值。它通常用于简单运动,因为它计算速度快,但不支持复杂运动。
auto interpolation_planner = std::make_shared<mtc::solvers::JointInterpolationPlanner>();CartesianPath用于在笛卡尔空间中沿直线移动末端执行器。
auto cartesian_planner = std::make_shared<mtc::solvers::CartesianPath>();
您可以随意尝试不同的求解器,看看机器人运动如何变化。第一阶段,我们将使用笛卡尔规划器,它需要设置以下属性:
auto cartesian_planner = std::make_shared<mtc::solvers::CartesianPath>(); cartesian_planner->setMaxVelocityScalingFactor(1.0); cartesian_planner->setMaxAccelerationScalingFactor(1.0); cartesian_planner->setStepSize(.01);
现在我们添加了规划器,我们可以添加一个移动机器人的阶段。以下几行使用一个MoveTo
阶段(传播器阶段)。由于张开手是一个相对简单的动作,我们可以使用关节插值规划器。此阶段计划移动到“张开手”姿势,这是熊猫机器人的SRDF中定义的命名姿势。我们返回任务并完成该createTask()
函数。
auto stage_open_hand = std::make_unique<mtc::stages::MoveTo>("open hand", interpolation_planner); stage_open_hand->setGroup(hand_group_name); stage_open_hand->setGoal("open"); task.add(std::move(stage_open_hand)); return task; }
最后,我们有main
:以下几行使用上面定义的类创建一个节点,并调用类方法来设置和执行基本 MTC 任务。在此示例中,任务执行完成后我们不取消执行器,以保持节点处于活动状态以检查 RViz 中的解决方案。
int main(int argc, char** argv) { rclcpp::init(argc, argv); rclcpp::NodeOptions options; options.automatically_declare_parameters_from_overrides(true); auto mtc_task_node = std::make_shared<MTCTaskNode>(options); rclcpp::executors::MultiThreadedExecutor executor; auto spin_thread = std::make_unique<std::thread>([&executor, &mtc_task_node]() { executor.add_node(mtc_task_node->getNodeBaseInterface()); executor.spin(); executor.remove_node(mtc_task_node->getNodeBaseInterface()); }); mtc_task_node->setupPlanningScene(); mtc_task_node->doTask(); spin_thread->join(); rclcpp::shutdown(); return 0; }
5 运行演示
5.1 启动文件
我们需要一个启动文件来启动move_group
、ros2_control
、static_tf
、robot_state_publisher
和rviz
节点,这些节点为我们提供运行演示的环境。我们将在本示例中使用的文件可在此处找到。
要运行 MoveIt Task Constructor 节点,我们将使用第二个启动文件mtc_tutorial
以适当的参数启动可执行文件。在这里,我们可以加载 URDF、SRDF 和 OMPL 参数,或者使用 MoveIt Configs Utils 来执行此操作。您的启动文件应该类似于此处教程包中的启动文件(请密切注意下面的package
和executable
参数,因为它们与链接的启动文件不同):
from launch import LaunchDescription from launch_ros.actions import Node from moveit_configs_utils import MoveItConfigsBuilder def generate_launch_description(): moveit_config = MoveItConfigsBuilder("moveit_resources_panda").to_dict() # MTC Demo node pick_place_demo = Node( package="mtc_tutorial", executable="mtc_node", output="screen", parameters=[ moveit_config, ], ) return LaunchDescription([pick_place_demo])
将启动文件另存为pick_place_demo.launch.py
或下载到包的启动目录中。确保编辑CMakeLists.txt
以使其包含启动文件夹,方法是添加以下几行:
install(DIRECTORY launch DESTINATION share/${PROJECT_NAME} )
现在我们可以构建并获取 colcon 工作区。
cd ~/ws_moveit colcon build --mixin release source ~/ws_moveit/install/setup.bash
首先启动第一个启动文件。如果你想使用教程提供的文件:
ros2 launch moveit2_tutorials mtc_demo.launch.py
RViz 现在将加载。如果您使用自己的启动文件并且未包含 rviz 配置(例如此配置),则需要先配置 RViz,然后才能看到任何显示内容。如果您使用教程包中的启动文件,RViz 将已为您配置,您可以跳至下一节的末尾。
5.2 RViz配置
如果您不使用提供的 RViz 配置,我们将不得不对 RViz 配置进行一些更改才能看到您的机器人和 MoveIt 任务构造器解决方案。首先,启动 RViz。以下步骤将介绍如何设置 RViz 以实现 MoveIt 任务构造器解决方案可视化。
-
如果MotionPlanning显示处于活动状态,请取消选中它以暂时隐藏它。
-
在“全局选项”下,将固定框架从更改
map
为panda_link0
(如果尚未完成)。 -
在窗口左下方,单击添加按钮。
-
选择运动规划任务并单击确定。运动规划任务显示应出现在左下角
moveit_task_constructor_visualization
。 -
在显示中的运动规划任务下,将任务解决方案主题更改为
/solution
您应该在主视图中看到熊猫手臂,左下角打开了运动规划任务显示,但其中没有任何内容。启动节点后,您的 MTC 任务将显示在此面板中mtc_tutorial
。如果您使用的是mtc_demo.launch.py
教程中的内容,请跳回此处。
5.3 启动演示
mtc_tutorial
使用以下方式启动节点
ros2 launch mtc_tutorial pick_place_demo.launch.py
您应该看到手臂以单级方式执行任务以张开手,其前面是绿色圆柱体。它看起来应该像这样:
如果您还没有制作自己的包,但仍想看看它是什么样子,您可以从教程中启动此文件:
ros2 launch moveit2_tutorials mtc_demo_minimal.launch.py
6 添加阶段
到目前为止,我们已经完成了创建和执行简单任务的演示,该任务可以运行,但作用不大。现在,我们将开始向任务中添加拾取和放置阶段。下图显示了我们将在任务中使用的阶段的概述。
我们将在现有的张开手阶段之后开始添加阶段。打开mtc_node.cpp
并找到以下几行:
auto stage_open_hand = std::make_unique<mtc::stages::MoveTo>("open hand", interpolation_planner); stage_open_hand->setGroup(hand_group_name); stage_open_hand->setGoal("open"); task.add(std::move(stage_open_hand)); // Add the next lines of codes to define more stages here
6.1 选择阶段
我们需要将手臂移动到可以拾取物体的位置。这通过一个Connect
阶段完成,顾名思义,它是一个连接器阶段。这意味着它试图在它之前和之后的阶段的结果之间架起桥梁。这个阶段用一个名称初始化,move_to_pick
以及一个GroupPlannerVector
指定规划组和规划器的。然后我们为该阶段设置超时,设置该阶段的属性,并将其添加到我们的任务中。
auto stage_move_to_pick = std::make_unique<mtc::stages::Connect>( "move to pick", mtc::stages::Connect::GroupPlannerVector{ { arm_group_name, sampling_planner } }); stage_move_to_pick->setTimeout(5.0); stage_move_to_pick->properties().configureInitFrom(mtc::Stage::PARENT); task.add(std::move(stage_move_to_pick));
接下来,我们创建一个指向 MoveIt 任务构造函数阶段对象的指针,并将其设置为nullptr
。稍后,我们将使用它来保存阶段。
mtc::Stage* attach_object_stage = nullptr; // Forward attach_object_stage to place pose generator
下一段代码创建一个SerialContainer
。这是一个可以添加到我们的任务中并可容纳多个子阶段的容器。在本例中,我们创建一个串行容器,它将包含与拾取操作相关的阶段。我们不会将阶段添加到任务中,而是将相关阶段添加到串行容器中。我们使用exposeTo()
在新的串行容器中从父任务声明任务属性,并使用configureInitFrom()
初始化它们。这允许所包含的阶段访问这些属性。
{ auto grasp = std::make_unique<mtc::SerialContainer>("pick object"); task.properties().exposeTo(grasp->properties(), { "eef", "group", "ik_frame" }); grasp->properties().configureInitFrom(mtc::Stage::PARENT, { "eef", "group", "ik_frame" });
然后我们创建一个阶段来接近物体。这个阶段是一个MoveRelative
阶段,它允许我们指定从当前位置的相对移动。MoveRelative
是一个传播器阶段:它从相邻阶段接收解决方案并将其传播到下一个或上一个阶段。使用cartesian_planner
找到一个涉及直线移动末端执行器的解决方案。我们设置属性,并设置移动的最小和最大距离。现在我们创建一条Vector3Stamped
消息来指示我们想要移动的方向——在本例中,从手框架开始的 Z 方向。最后,我们将这个阶段添加到我们的串行容器中
{ auto stage = std::make_unique<mtc::stages::MoveRelative>("approach object", cartesian_planner); stage->properties().set("marker_ns", "approach_object"); stage->properties().set("link", hand_frame); stage->properties().configureInitFrom(mtc::Stage::PARENT, { "group" }); stage->setMinMaxDistance(0.1, 0.15); // Set hand forward direction geometry_msgs::msg::Vector3Stamped vec; vec.header.frame_id = hand_frame; vec.vector.z = 1.0; stage->setDirection(vec); grasp->insert(std::move(stage)); }
现在,创建一个阶段来生成抓取姿势。这是一个生成器阶段,因此它在不考虑前后阶段的情况下计算其结果。第一阶段CurrentState
也是一个生成器阶段 - 要连接第一阶段和此阶段,必须使用我们已经在上面创建的连接阶段。此代码设置阶段属性,设置抓取前的姿势、角度增量和监控阶段。角度增量是阶段的属性,GenerateGraspPose
用于确定要生成的姿势数量;在生成解决方案时,MoveIt 任务构造函数将尝试从许多不同的方向抓取物体,角度增量指定方向之间的差异。增量越小,抓取方向就越接近。在定义当前阶段时,我们设置current_state_ptr
,现在用于将有关物体姿势和形状的信息转发给逆运动学解算器。此阶段不会像以前一样直接添加到串行容器中,因为我们仍然需要对它生成的姿势进行逆运动学计算。
{ // Sample grasp pose auto stage = std::make_unique<mtc::stages::GenerateGraspPose>("generate grasp pose"); stage->properties().configureInitFrom(mtc::Stage::PARENT); stage->properties().set("marker_ns", "grasp_pose"); stage->setPreGraspPose("open"); stage->setObject("object"); stage->setAngleDelta(M_PI / 12); stage->setMonitoredStage(current_state_ptr); // Hook into current state
在计算上面生成的姿势的逆运动学之前,我们首先需要定义框架。这可以通过PoseStamped
来自的消息来完成geometry_msgs
,或者在这种情况下,我们使用特征变换矩阵和相关链接的名称来定义变换。在这里,我们定义变换矩阵。
Eigen::Isometry3d grasp_frame_transform; Eigen::Quaterniond q = Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitX()) * Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitY()) * Eigen::AngleAxisd(M_PI / 2, Eigen::Vector3d::UnitZ()); grasp_frame_transform.linear() = q.matrix(); grasp_frame_transform.translation().z() = 0.1;
现在,我们创建ComputeIK
阶段,并为其命名,以及上面定义的阶段。一些机器人对于给定的姿势有多个逆运动学解决方案 - 我们将要解决的解决方案数量限制为最多 8 个。我们还设置了最小解决方案距离,这是不同解决方案必须有多大的阈值:如果解决方案中的关节位置与前一个解决方案太相似,它将被标记为无效。接下来,我们配置一些其他属性,并将阶段添加到串行容器中。generate pose IK
generate grasp pose
ComputeIK
// Compute IK auto wrapper = std::make_unique<mtc::stages::ComputeIK>("grasp pose IK", std::move(stage)); wrapper->setMaxIKSolutions(8); wrapper->setMinSolutionDistance(1.0); wrapper->setIKFrame(grasp_frame_transform, hand_frame); wrapper->properties().configureInitFrom(mtc::Stage::PARENT, { "eef", "group" }); wrapper->properties().configureInitFrom(mtc::Stage::INTERFACE, { "target_pose" }); grasp->insert(std::move(wrapper)); }
要拾起物体,我们必须允许手和物体之间发生碰撞。这可以通过阶段来完成ModifyPlanningScene
。该allowCollisions
函数让我们指定要禁用哪些碰撞。 allowCollisions
可以与名称容器一起使用,因此我们可以用来getLinkModelNamesWithCollisionGeometry
获取手组中具有碰撞几何的所有链接的名称。
{ auto stage = std::make_unique<mtc::stages::ModifyPlanningScene>("allow collision (hand,object)"); stage->allowCollisions("object", task.getRobotModel() ->getJointModelGroup(hand_group_name) ->getLinkModelNamesWithCollisionGeometry(), true); grasp->insert(std::move(stage)); }
允许碰撞后,我们现在可以闭合手部。这可以通过舞台完成MoveTo
,类似于上面的舞台,只是移动到SRDF 中定义的位置。open hand
close
{ auto stage = std::make_unique<mtc::stages::MoveTo>("close hand", interpolation_planner); stage->setGroup(hand_group_name); stage->setGoal("close"); grasp->insert(std::move(stage)); }
我们现在再次使用ModifyPlanningScene
舞台,这次使用 将物体附着到手上attachObject
。与我们对 所做的类似current_state_ptr
,我们获得指向此舞台的指针,以便稍后在为物体生成位置姿势时使用。
{ auto stage = std::make_unique<mtc::stages::ModifyPlanningScene>("attach object"); stage->attachObject("object", hand_frame); attach_object_stage = stage.get(); grasp->insert(std::move(stage)); }
接下来我们用一个舞台把物体抬起来MoveRelative
,类似于approach_object
舞台。
{ auto stage = std::make_unique<mtc::stages::MoveRelative>("lift object", cartesian_planner); stage->properties().configureInitFrom(mtc::Stage::PARENT, { "group" }); stage->setMinMaxDistance(0.1, 0.3); stage->setIKFrame(hand_frame); stage->properties().set("marker_ns", "lift_object"); // Set upward direction geometry_msgs::msg::Vector3Stamped vec; vec.header.frame_id = "world"; vec.vector.z = 1.0; stage->setDirection(vec); grasp->insert(std::move(stage)); }
这样,我们就有了拾取物体所需的所有阶段。现在,我们将序列容器(及其所有子阶段)添加到任务中。如果您按原样构建包,则可以看到机器人计划拾取物体。
task.add(std::move(grasp));
}
要测试新创建的阶段,请构建代码并执行:
ros2 launch mtc_tutorial pick_place_demo.launch.py
6.2 场地阶段
现在定义拾取的阶段已经完成,我们继续定义放置物体的阶段。接着上次停止的地方,我们添加一个Connect
阶段来连接这两个阶段,因为我们很快将使用生成器阶段来生成放置物体的姿势。
{ auto stage_move_to_place = std::make_unique<mtc::stages::Connect>( "move to place", mtc::stages::Connect::GroupPlannerVector{ { arm_group_name, sampling_planner }, { hand_group_name, interpolation_planner } }); stage_move_to_place->setTimeout(5.0); stage_move_to_place->properties().configureInitFrom(mtc::Stage::PARENT); task.add(std::move(stage_move_to_place)); }
我们还为放置阶段创建了一个串行容器。这与挑选串行容器类似。下一个阶段将添加到串行容器中,而不是任务中。
{ auto place = std::make_unique<mtc::SerialContainer>("place object"); task.properties().exposeTo(place->properties(), { "eef", "group", "ik_frame" }); place->properties().configureInitFrom(mtc::Stage::PARENT, { "eef", "group", "ik_frame" });
下一个阶段生成用于放置物体的姿势并计算这些姿势的逆运动学 - 它有点类似于pick 串行容器中的阶段。我们首先创建一个阶段来生成姿势并继承任务属性。我们使用来自的消息指定我们想要放置物体的姿势-在本例中,我们在框架中选择。然后我们使用将目标姿势传递给阶段。接下来,我们使用并将之前的阶段指针传递给它。这允许阶段知道物体是如何连接的。然后我们创建一个阶段并将我们的阶段传递给它- 其余部分遵循与上面 pick 阶段相同的逻辑。
generate grasp posePoseStampedgeometry_msgsy = 0.5"object"setPosesetMonitoredStageattach_objectComputeIKGeneratePlacePose { // Sample place pose auto stage = std::make_unique<mtc::stages::GeneratePlacePose>("generate place pose"); stage->properties().configureInitFrom(mtc::Stage::PARENT); stage->properties().set("marker_ns", "place_pose"); stage->setObject("object"); geometry_msgs::msg::PoseStamped target_pose_msg; target_pose_msg.header.frame_id = "object"; target_pose_msg.pose.position.y = 0.5; target_pose_msg.pose.orientation.w = 1.0; stage->setPose(target_pose_msg); stage->setMonitoredStage(attach_object_stage); // Hook into attach_object_stage // Compute IK auto wrapper = std::make_unique<mtc::stages::ComputeIK>("place pose IK", std::move(stage)); wrapper->setMaxIKSolutions(2); wrapper->setMinSolutionDistance(1.0); wrapper->setIKFrame("object"); wrapper->properties().configureInitFrom(mtc::Stage::PARENT, { "eef", "group" }); wrapper->properties().configureInitFrom(mtc::Stage::INTERFACE, { "target_pose" }); place->insert(std::move(wrapper)); }
现在我们已经准备好放置物体了,我们打开带有MoveTo
舞台的手和关节插值规划器。
{ auto stage = std::make_unique<mtc::stages::MoveTo>("open hand", interpolation_planner); stage->setGroup(hand_group_name); stage->setGoal("open"); place->insert(std::move(stage)); }
现在我们不再需要握住物体,因此我们还可以重新启用与物体的碰撞。这allowCollisions
几乎与禁用碰撞完全相同,只是将最后一个参数设置为false
而不是true
。
{ auto stage = std::make_unique<mtc::stages::ModifyPlanningScene>("forbid collision (hand,object)"); stage->allowCollisions("object", task.getRobotModel() ->getJointModelGroup(hand_group_name) ->getLinkModelNamesWithCollisionGeometry(), false); place->insert(std::move(stage)); }
现在,我们可以使用 分离对象detachObject
。
{ auto stage = std::make_unique<mtc::stages::ModifyPlanningScene>("detach object"); stage->detachObject("object", hand_frame); place->insert(std::move(stage)); }
我们使用阶段从对象中撤退MoveRelative
,这与和阶段类似。approach object
lift object
{ auto stage = std::make_unique<mtc::stages::MoveRelative>("retreat", cartesian_planner); stage->properties().configureInitFrom(mtc::Stage::PARENT, { "group" }); stage->setMinMaxDistance(0.1, 0.3); stage->setIKFrame(hand_frame); stage->properties().set("marker_ns", "retreat"); // Set retreat direction geometry_msgs::msg::Vector3Stamped vec; vec.header.frame_id = "world"; vec.vector.x = -0.5; stage->setDirection(vec); place->insert(std::move(stage)); }
我们完成了位置序列容器并将其添加到任务中。
task.add(std::move(place));
}
最后一步是返回主页:我们使用一个MoveTo
阶段并将目标姿势传递给它ready
,这是 Panda SRDF 中定义的姿势。
{ auto stage = std::make_unique<mtc::stages::MoveTo>("return home", interpolation_planner); stage->properties().configureInitFrom(mtc::Stage::PARENT, { "group" }); stage->setGoal("ready"); task.add(std::move(stage)); }
所有这些阶段都应添加在这些行上方。
// Stages all added to the task above this line return task; }
恭喜!您现在已经使用 MoveIt Task Constructor 定义了拾取和放置任务!要试用它,请构建代码并执行:
ros2 launch mtc_tutorial pick_place_demo.launch.py
7 进一步讨论
运动规划任务窗格中显示了每个组成阶段的任务。单击某个阶段,右侧将显示有关该阶段的其他信息。右侧窗格显示不同的解决方案及其相关成本。根据阶段类型和机器人配置,可能只显示一个解决方案。
单击其中一个解决方案成本,查看机器人按照该阶段计划进行的动画。单击窗格右上部分的“执行”按钮执行动作。
要运行 MoveIt 教程中包含的完整 MoveIt 任务构造函数示例:
ros2 launch moveit2_tutorials mtc_demo.launch.py
在第二个终端中:
ros2 launch moveit2_tutorials pick_place_demo.launch.py
7.1 打印到终端的调试信息
运行 MTC 时,它会将如下图表打印到终端:
[demo_node-1] 1 - ← 1 → - 0 / initial_state [demo_node-1] - 0 → 0 → - 0 / move_to_home
此示例显示了两个阶段。第一阶段(“initial_state”)是一种CurrentState
阶段,它初始化PlanningScene
并捕获当时存在的任何碰撞对象。指向此阶段的指针可用于检索机器人的状态。由于CurrentState
继承自 Generator
,它会向前和向后传播解决方案。这由两个方向的箭头表示。
-
第一个
1
表示一个解决方案已成功向后传播到前一阶段。 -
1
箭头之间的第二个符号表示已生成一个解决方案。 -
表示
0
解决方案未能成功传播到下一阶段,因为下一阶段失败了。
第二阶段(“move_to_home”)是一种MoveTo
阶段。它从上一阶段继承了传播方向,因此两个箭头都指向前方。0
' 表示此阶段完全失败。从左到右,0
' 表示:
-
该阶段没有收到前一阶段的解决方案
-
该阶段未生成解决方案
-
该阶段没有将解决方案传播到下一阶段
在这种情况下,我们可以说“move_to_home”是失败的根本原因。问题在于主状态发生碰撞。定义一个新的、无碰撞的主位置解决了这个问题。
7.2 阶段
可以从任务中检索有关各个阶段的信息。例如,我们在这里检索某个阶段的唯一 ID:
uint32_t const unique_stage_id = task_.stages()->findChild(stage_name)->introspectionId();
类型CurrentState
阶段不仅仅检索机器人的当前状态。它还会初始化一个PlanningScene
对象,捕获当时存在的任何碰撞对象。
MTC 阶段可以正向和反向传播。您可以通过 RViz GUI 中的箭头轻松检查阶段传播的方向。向后传播时,许多操作的逻辑是相反的。例如,要允许与阶段中的对象发生碰撞ModifyPlanningScene
,您可以调用allowCollisions(false)
而不是allowCollisions(true)
。这里有一个讨论可以阅读。