跳转至

Mobile Manipulation(移动操作机器人)

项目类型: 操作 + 导航 | 难度: ★★★☆☆ 至 ★★★★★(取决于方法) | 预计时间: 2–4 个周末


1. 项目概述

移动操作(Mobile Manipulation) 将**移动能力**(移动基座的自主导航)与**操作能力**(机械臂控制)相结合,使机器人能够接近目标场景、定位物体,并对其实施加动作——拾取、放置、打开抽屉或按下按钮。这是服务机器人领域的经典问题之一。

  ┌──────────────────────────────────────────────────────────────────┐
  │              移动操作流水线(俯视图)                               │
  │                                                                   │
  │    ┌──────────┐                                                  │
  │    │  场景 /  │                                                  │
  │    │  目标物体│                                                  │
  │    └────┬─────┘                                                  │
  │         │ (感知)                                                 │
  │         ▼                                                         │
  │    ┌──────────────┐   ┌─────────────────┐                       │
  │    │  目标关联    │──►│  可达性分析      │                       │
  │    │ (检测/定位)│   │  (哪个接近位     │                       │
  │    │              │   │   置是合法的?) │                       │
  │    └──────────────┘   └────────┬─────────┘                       │
  │                                │                                   │
  │                                ▼ (规划)                            │
  │         ┌───────────────────────────────────┐                    │
  │         │      全身运动规划器                 │                    │
  │         │  (基座位姿 + 臂关节目标)             │                    │
  │         └──────────────┬────────────────────┘                    │
  │                        │                                          │
  │         ┌──────────────┴────────────────────┐                    │
  │         │                                     │                   │
  │         ▼ (执行)                              ▼ (执行)             │
  │  ┌─────────────┐                      ┌─────────────┐           │
  │  │  移动基座    │                      │  臂 + 末端   │           │
  │  │  控制器      │                      │  执行器控制  │           │
  │  └──────┬──────┘                      └──────┬──────┘           │
  │         │                                     │                   │
  │         ▼                                     ▼                   │
  │   移动到接近位                      抓取 / 操作                    │
  │                                                                   │
  │     机器人 = [移动基座] + [机械臂] + [末端执行器 / 夹爪]            │
  └──────────────────────────────────────────────────────────────────┘

本项目将探索**三种逐步递进的方法**:

层次 方法 关键技术 ROS 包
第一层 解耦方法 导航至接近位,然后逆运动学控制臂 move_base + moveit_ros
第二层 全身规划 基座与臂联合优化 moveit_ros(全身)+ OMPL
第三层 基础模型 VLA 策略(RT-2 / Octo / π0) ros2_manipulation + 推理服务

2. 学习目标

  • 理解移动操作机器人的运动学模型(基座 + 机械臂)
  • 实现解耦的导航 + 逆运动学(IK)控制
  • 执行全身可操作性分析(manipulability analysis)和关节优化
  • 在 ROS 2 中为移动操作机器人配置 MoveIt 2
  • 将目标检测与抓取规划整合
  • 理解用于操作的视觉-语言-动作(VLA)基础模型

3. 硬件与软件需求

硬件

组件 规格 备注
移动操作机器人 Stretch RE-1、Fetch,或自定义(LoCoArm + TurtleBot) 任意的移动基座 + 机械臂组合
深度相机 Intel RealSense D435i / Azure Kinect 用于目标检测和点云
激光雷达(可选) 2D 激光雷达(如 RPLIDAR A1) 仅第一、二层需要
机载计算机 NVIDIA Jetson Orin / x86 + ROS 2 第三层需要 GPU
夹爪 并行夹爪(Robotiq 2F-85)或自定义 电动夹爪
电池 足以支持 2 小时以上运行

软件

软件包 版本 用途
ROS 2 Humble / Iron 核心框架
MoveIt 2 Humble 全身运动规划
Navigation 2 Humble 移动基座导航
OpenCV ≥ 4.5 图像处理
PyTorch ≥ 1.13 神经网络推理(第三层)
抓取位姿检测 GraspNet / Contact-GraspNet 抓取规划
OMPL (通过 MoveIt) 基于采样的运动规划
Isaac Sim / Gazebo 最新版 仿真
# 安装核心依赖(ROS 2 Humble)
sudo apt install ros-humble-moveit ros-humble-navigation2 ros-humble-ros2-control
pip install opencv-python numpy torch torchvision open3d

4. 第一层 — 传统方法:解耦导航 + 逆运动学控制

4.1 概念

最简单的方法是将**移动基座**和**机械臂****解耦**:

  1. 导航 — 将移动基座导航至目标物体附近的接近位(approach pose)
  2. 锁定基座 — 然后求解**逆运动学(IK)**,使机械臂到达物体
  3. 执行 — 完成抓取

这种方法易于实现和调试,但无法根据臂的可操作性来调整基座位姿——基座可能到达一个臂无法舒适够到目标的位置。

  步骤 1:导航                      步骤 2:锁定基座,求解 IK
  ┌──────────────────┐              ┌────────────────────────┐
  │                  │              │                         │
  │     [基座]──►    │              │   [基座]  [臂伸向目标]   │
  │        ↓         │              │     ↖     ───► [物体]   │
  │   接近位姿        │              │   IK 解                  │
  └──────────────────┘              └────────────────────────┘

4.2 运动学模型

对于**解耦系统**,臂的正向运动学为:

\[ \mathbf{x}_{ee} = f(\mathbf{q}_{arm}) \in \mathbb{R}^6 \]

其中 \(\mathbf{q}_{arm} \in \mathbb{R}^n\) 为臂关节位置,\(\mathbf{x}_{ee} = [p_x, p_y, p_z, \phi, \theta, \psi]\) 为末端执行器位姿(位置 + 姿态)。

**逆运动学**求解:

\[ \mathbf{q}_{arm} = f^{-1}(\mathbf{x}_{des}) \]

对于 6 自由度臂,通常使用**数值 IK 求解器**(如 TRAC-IK 或 KDL):

def compute_ik(robot_model, q_init, target_pose, timeout=1.0):
    """
    求解 IK,使臂到达期望的末端执行器位姿。

    Parameters
    ----------
    robot_model : robot_model_loader.RobotModelLoader
        加载的机器人模型(URDF + SRDF)
    q_init : list[float]
        初始关节配置猜测
    target_pose : geometry_msgs/Pose
        基坐标系中的目标末端执行器位姿
    timeout : float
        求解器超时时间(秒)

    Returns
    -------
    list[float] or None
        求解得到的关节配置,若无解则返回 None
    """
    # 使用 MoveIt Python API(更简单)
    group_name = "arm_group"
    move_group = MoveGroupInterface(group_name, "robot_description")

    move_group.setPoseTarget(target_pose)
    move_group.setMaxVelocityScalingFactor(0.5)
    move_group.setMaxAccelerationScalingFactor(0.5)

    plan = move_group.plan()
    if plan[0]:  # 成功
        return plan[1].joint_trajectory.points[-1].positions
    return None

4.3 接近位选择

在导航之前,必须计算一个**合法的基座接近位**:

"""
第一层:解耦移动操作
======================================
将基座导航至接近位,然后使用 IK 控制机械臂。
"""

import numpy as np
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import PoseStamped, Pose
from nav2_msgs.action import NavigateToPose
from moveitInterfaces import MoveGroupInterface, PlanningSceneInterface
from rclpy.action import ActionClient


class DecoupledMobileManipulator(Node):
    """
    解耦方法:导航基座至接近位,
    然后使用逆运动学控制臂。
    """

    def __init__(self):
        super().__init__('decoupled_mobile_manipulator')

        # ── MoveIt 接口 ──────────────────────────────────────────────
        self.arm_group = MoveGroupInterface("arm_group", "robot_description")
        self.gripper_group = MoveGroupInterface("gripper_group", "robot_description")
        self.scene = PlanningSceneInterface()

        # ── 导航动作客户端 ─────────────────────────────────────────────
        self.nav_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')

        self.get_logger().info("解耦移动操作机器人就绪")

    def navigate_to(self, x: float, y: float, yaw: float, timeout: float = 30.0):
        """
        使用 Navigation 2 将移动基座发送至 (x, y, yaw)。

        Parameters
        ----------
        x, y : float      — 地图坐标系中的目标位置
        yaw  : float      — 目标偏航角(弧度)
        timeout : float   — 最大等待时间(秒)
        """
        from tf_transformations import quaternion_from_euler

        goal = NavigateToPose.Goal()
        goal.pose.header.frame_id = "map"
        goal.pose.header.stamp = self.get_clock().now().to_msg()
        goal.pose.pose.position.x = x
        goal.pose.pose.position.y = y
        goal.pose.pose.position.z = 0.0

        q = quaternion_from_euler(0, 0, yaw)
        goal.pose.pose.orientation.x = q[0]
        goal.pose.pose.orientation.y = q[1]
        goal.pose.pose.orientation.z = q[2]
        goal.pose.pose.orientation.w = q[3]

        self.get_logger().info(f"正在导航至 ({x:.2f}, {y:.2f})...")
        self.nav_client.wait_for_server()
        send_goal = self.nav_client.send_goal_async(goal)
        rclpy.spin_until_future_complete(self, send_goal)

        result = send_goal.result()
        if result:
            self.get_logger().info("导航成功!")
            return True
        else:
            self.get_logger().warn("导航失败或超时")
            return False

    def pick_object(self, object_pose: PoseStamped, approach_height: float = 0.15):
        """
        使用基于 IK 的臂控制抓取物体。

        序列:预抓 → 抓取 → 提升。

        Parameters
        ----------
        object_pose : PoseStamped
            物体在基座坐标系中的位置
        approach_height : float
            下降前在物体上方的高度
        """
        # ── 1. 计算接近位(物体上方)─────────────────────────────────────
        pregrasp_pose = Pose()
        pregrasp_pose.position.CopyFrom(object_pose.pose.position)
        pregrasp_pose.position.z += approach_height  # 先上升
        pregrasp_pose.orientation.CopyFrom(object_pose.pose.orientation)

        # ── 2. 预抓取:通过 IK 将臂移动至接近位 ────────────────────────────
        self.arm_group.setPoseTarget(pregrasp_pose)
        success = self.arm_group.go(wait=True)
        self.arm_group.stop()
        self.arm_group.clearPoseTargets()

        if not success:
            self.get_logger().error("预抓取 IK 失败")
            return False

        # ── 3. 线性插值下降至抓取位 ──────────────────────────────────────
        grasp_pose = Pose()
        grasp_pose.position.CopyFrom(object_pose.pose.position)
        grasp_pose.orientation.CopyFrom(object_pose.pose.orientation)

        # 10 步线性插值下降
        for t in np.linspace(0, 1, 10):
            intermediate = Pose()
            intermediate.position.z = grasp_pose.position.z + (1 - t) * approach_height
            intermediate.position.x = grasp_pose.position.x
            intermediate.position.y = grasp_pose.position.y
            intermediate.orientation.CopyFrom(grasp_pose.orientation)

            self.arm_group.setPoseTarget(intermediate)
            self.arm_group.go(wait=True)
            self.arm_group.stop()
            self.arm_group.clearPoseTargets()

        # ── 4. 闭合夹爪 ──────────────────────────────────────────────────
        self.gripper_group.setNamedTarget("close")
        self.gripper_group.go(wait=True)
        self.gripper_group.stop()

        # ── 5. 提升 ──────────────────────────────────────────────────────
        lift_pose = grasp_pose
        lift_pose.position.z += 0.2
        self.arm_group.setPoseTarget(lift_pose)
        self.arm_group.go(wait=True)
        self.arm_group.stop()
        self.arm_group.clearPoseTargets()

        self.get_logger().info("抓取完成!")
        return True

    def place_object(self, place_pose: PoseStamped):
        """
        将物体放置到目标位置。
        """
        # 下降至放置位
        for t in np.linspace(0, 1, 10):
            intermediate = Pose()
            intermediate.position.z = place_pose.pose.position.z + (1 - t) * 0.2
            intermediate.position.x = place_pose.pose.position.x
            intermediate.position.y = place_pose.pose.position.y
            intermediate.orientation.CopyFrom(place_pose.pose.orientation)

            self.arm_group.setPoseTarget(intermediate)
            self.arm_group.go(wait=True)
            self.arm_group.stop()
            self.arm_group.clearPoseTargets()

        # 打开夹爪
        self.gripper_group.setNamedTarget("open")
        self.gripper_group.go(wait=True)
        self.gripper_group.stop()

        # 回退
        retreat_pose = Pose()
        retreat_pose.position.CopyFrom(place_pose.pose.position)
        retreat_pose.position.z += 0.3
        retreat_pose.orientation.CopyFrom(place_pose.pose.orientation)
        self.arm_group.setPoseTarget(retreat_pose)
        self.arm_group.go(wait=True)
        self.arm_group.stop()
        self.arm_group.clearPoseTargets()


def main():
    rclpy.init()
    node = DecoupledMobileManipulator()

    # ── 演示序列 ────────────────────────────────────────────────────────
    # 步骤 1:导航至预定义的接近位
    success = node.navigate_to(x=2.0, y=1.5, yaw=np.pi / 2)
    if not success:
        node.get_logger().error("无法到达接近位!")
        rclpy.shutdown()
        return

    # 步骤 2:抓取物体(目标位姿来自感知系统)
    object_pose = PoseStamped()
    object_pose.header.frame_id = "base_link"
    object_pose.pose.position.x = 0.4
    object_pose.pose.position.y = 0.0
    object_pose.pose.position.z = 0.6
    object_pose.pose.orientation.w = 1.0

    node.pick_object(object_pose)

    # 步骤 3:导航至放置位置
    node.navigate_to(x=3.0, y=1.5, yaw=np.pi / 2)
    node.place_object(object_pose)

    node.get_logger().info("演示完成!")
    rclpy.shutdown()


if __name__ == "__main__":
    main()

4.4 解耦方法的局限性

问题 影响
基座位姿固定 臂可能能到达物体但处于奇异构型或关节极限附近
基座姿态未优化 抓取方向不理想(腕部可能反转)
无基座-环境碰撞检测 基座可能遮挡臂的工作空间
顺序失败 若导航失败,臂的准备工作浪费

5. 第二层 — 中级方法:全身运动规划

5.1 概念

全身运动规划**联合优化**移动基座位姿和臂关节配置,同时满足:

  1. 可达性 — 末端执行器必须到达物体
  2. 可操作性 — 臂应处于灵巧构型(远离奇异点)
  3. 无碰撞 — 整个机器人身体避免障碍物
  4. 运动平滑 — 最小化关节力矩 / 移动距离
  解耦方法:                         全身规划:
  ┌──────────────┐                  ┌──────────────────────────┐
  │ 基座 → 位姿   │                  │ 联合优化基座 + 臂:        │
  │ 臂 → IK      │                  │                          │
  │ (两个步骤)   │                  │ min  ||q - q_init||²      │
  └──────────────┘                  │ s.t. ||x_ee - x_obj|| < ε │
                                    │      J(q) q̇ = ẋ          │
                                    └──────────────────────────┘

5.2 全身运动学

组合机器人状态为:

\[ \mathbf{q} = \begin{bmatrix} \mathbf{q}_{base} \\ \mathbf{q}_{arm} \end{bmatrix} \in \mathbb{R}^{(3+n)} \]

其中 \(\mathbf{q}_{base} = [x, y, \theta]\) 为差分驱动基座的位形,\(\mathbf{q}_{arm} \in \mathbb{R}^n\) 为臂关节。

**全身雅可比矩阵**将关节速度映射到末端执行器速度:

\[ \dot{\mathbf{x}}_{ee} = J_{wb}(\mathbf{q}) \, \dot{\mathbf{q}} \]

其中 \(J_{wb} \in \mathbb{R}^{6 \times (3+n)}\) 为全身雅可比矩阵:

\[ J_{wb} = \begin{bmatrix} J_{base} \\ J_{arm} \end{bmatrix} \]

臂的**可操作性度量**(Yoshikawa)为:

\[ w = \sqrt{\det(J_{arm} J_{arm}^\top)} \]

最大化 \(w\) 可使臂远离奇异点。

5.3 MoveIt 2 全身配置

<!-- launch/whole_body_planning.launch -->
<launch>
  <!-- 加载机器人描述 -->
  <node pkg="robot_state_publisher" type="robot_state_publisher"
        name="robot_state_publisher" output="screen">
    <param name="robot_description" textfile="$(find my_mobile_manipulator)/urdf/robot.urdf"/>
  </node>

  <!-- MoveIt 配置 -->
  <node pkg="moveit_ros_move_group" type="move_group"
        name="move_group" output="screen">
    <param name="robot_description" textfile="$(find my_mobile_manipulator)/urdf/robot.urdf"/>
    <rosparam file="$(find my_mobile_manipulator)/config/ompl_planning.yaml"/>
    <rosparam file="$(find my_mobile_manipulator)/config/kinematics.yaml"/>
    <param name="robot_description_semantic"
           textfile="$(find my_mobile_manipulator)/config/robot.srdf"/>
  </node>
</launch>

SRDF 配置 — 定义全身规划组:

<!-- config/robot.srdf -->
<robot name="mobile_manipulator">
  <!-- 全身组:基座 + 臂 -->
  <group name="whole_body">
    <chain base_link="base_footprint" tip_link="gripper_link"/>
  </group>

  <!-- 子组用于专门控制 -->
  <group name="arm_group">
    <link name="shoulder_link"/>
    <link name="gripper_link"/>
  </group>

  <group name="base_group">
    <joint name="x_joint"/>
    <joint name="y_joint"/>
    <joint name="theta_joint"/>
  </group>

  <!-- 命名的主位形 -->
  <group_state name="home" group="whole_body">
    <joint name="x_joint" value="0"/>
    <joint name="y_joint" value="0"/>
    <joint name="theta_joint" value="0"/>
    <joint name="shoulder_pan_joint"  value="0"/>
    <joint name="shoulder_lift_joint" value="-1.57"/>
    <joint name="elbow_joint"         value="1.57"/>
    <joint name="wrist_1_joint"       value="0"/>
    <joint name="wrist_2_joint"       value="-1.57"/>
    <joint name="wrist_3_joint"       value="0"/>
  </group_state>
</robot>

5.4 全身规划 Python 代码

"""
第二层:MoveIt 2 全身移动操作
======================================================
使用 MoveGroupInterface 和全身组联合优化基座位姿 + 臂关节配置。
"""

import numpy as np
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Pose, PoseStamped
from moveitInterfaces import MoveGroupInterface, PlanningSceneInterface
from moveit_msgs.msg import Constraints, JointConstraint, PositionConstraint


class WholeBodyMobileManipulator(Node):
    """
    使用 MoveIt 2 进行全身操作。

    与第一层相比的优势:规划器将基座位姿和臂关节一起考虑,
    优化可操作性并避免整个机器人身体的碰撞。
    """

    def __init__(self):
        super().__init__('whole_body_mobile_manipulator')

        # ── 全身规划组 ─────────────────────────────────────────────────
        # 包括:基座(x, y, theta)+ 臂关节
        self.whole_body_group = MoveGroupInterface(
            "whole_body", "robot_description"
        )
        self.arm_group = MoveGroupInterface("arm_group", "robot_description")
        self.scene = PlanningSceneInterface()

        # ── 配置规划器 ─────────────────────────────────────────────────
        self.whole_body_group.setNumPlanningAttempts(10)
        self.whole_body_group.setPlanningTime(5.0)
        self.whole_body_group.setMaxVelocityScalingFactor(0.6)
        self.whole_body_group.setMaxAccelerationScalingFactor(0.6)
        self.whole_body_group.setPlannerId("RRTstar")

        # ── 添加桌子作为碰撞物体 ─────────────────────────────────────────
        self._add_collision_objects()

        self.get_logger().info("全身移动操作机器人就绪")

    def _add_collision_objects(self):
        """将环境障碍物添加到规划场景。"""
        # 桌子
        table_pose = PoseStamped()
        table_pose.header.frame_id = "base_link"
        table_pose.pose.position.x = 0.8
        table_pose.pose.position.y = 0.0
        table_pose.pose.position.z = -0.3
        self.scene.addBox("table", 0.6, 0.6, 0.74, table_pose)

        # 架子
        shelf_pose = PoseStamped()
        shelf_pose.header.frame_id = "base_link"
        shelf_pose.pose.position.x = 0.8
        shelf_pose.pose.position.y = 0.3
        shelf_pose.pose.position.z = 0.3
        self.scene.addBox("shelf", 0.4, 0.3, 0.05, shelf_pose)

        self.get_logger().info("碰撞物体已添加")

    def whole_body_pick(self, object_position: np.ndarray,
                        approach_direction: np.ndarray = np.array([0, 0, -1]),
                        approach_height: float = 0.15):
        """
        使用全身规划抓取物体。

        与第一层不同,这会联合规划基座位姿和臂关节,
        考虑可操作性并避免臂/基座自碰撞。

        Parameters
        ----------
        object_position : ndarray [3]
            世界坐标系中物体位置 (x, y, z)
        approach_direction : ndarray [3]
            接近方向单位向量(默认:垂直向下)
        approach_height : float
            物体上方的接近距离
        """
        # ── 1. 计算接近位 ────────────────────────────────────────────────
        approach_pose = PoseStamped()
        approach_pose.header.frame_id = "base_link"
        approach_pose.header.stamp = self.get_clock().now().to_msg()

        # 从物体上方开始
        approach_pose.pose.position.x = object_position[0]
        approach_pose.pose.position.y = object_position[1]
        approach_pose.pose.position.z = object_position[2] + approach_height

        # 接近方向决定夹爪朝向
        from tf_transformations import quaternion_from_matrix

        # 构建与接近方向对齐的旋转矩阵
        # (夹爪 Z 轴与接近方向对齐)
        z = approach_direction / np.linalg.norm(approach_direction)
        x_temp = np.array([1, 0, 0]) if abs(z[2]) < 0.99 else np.array([0, 1, 0])
        x = x_temp - np.dot(x_temp, z) * z
        x = x / np.linalg.norm(x)
        y = np.cross(z, x)

        R = np.column_stack([x, y, z])
        q = quaternion_from_matrix(np.vstack([np.hstack([R, [0, 0, 0]]), [0, 0, 0, 1]]))

        approach_pose.pose.orientation.x = q[0]
        approach_pose.pose.orientation.y = q[1]
        approach_pose.pose.orientation.z = q[2]
        approach_pose.pose.orientation.w = q[3]

        # ── 2. 设置全身位姿目标 ──────────────────────────────────────────
        # 这会联合优化基座(x, y, theta)和臂关节
        self.whole_body_group.setPoseTarget(approach_pose)

        # ── 3. 在约束下规划(碰撞 + 可操作性)───────────────────────────────
        constraints = Constraints()
        constraints.joint_constraints = self._make_joint_constraints()

        plan = self.whole_body_group.plan()
        if not plan[0]:
            self.get_logger().warn("接近位全身规划失败")
            return False

        # 执行
        self.whole_body_group.execute(plan[1], wait=True)
        self.whole_body_group.stop()
        self.whole_body_group.clearPoseTargets()

        # ── 4. 线性下降至抓取位 ──────────────────────────────────────────
        grasp_pose = PoseStamped()
        grasp_pose.header.frame_id = "base_link"
        grasp_pose.pose.position.x = object_position[0]
        grasp_pose.pose.position.y = object_position[1]
        grasp_pose.pose.position.z = object_position[2]
        grasp_pose.pose.orientation.CopyFrom(approach_pose.pose.orientation)

        # 使用笛卡尔路径进行线性下降
        waypoints = [approach_pose.pose, grasp_pose.pose]
        (plan, fraction) = self.arm_group.computeCartesianPath(
            waypoints,
            eef_step=0.01,      # 1 cm 步长
            jump_threshold=1.0,  # 禁用跳跃检测
        )
        if fraction < 0.95:
            self.get_logger().warn(f"笛卡尔路径仅完成 {fraction*100:.1f}%")
        self.arm_group.execute(plan, wait=True)

        # ── 5. 闭合夹爪 ────────────────────────────────────────────────────
        self.arm_group.stop()
        self.arm_group.setNamedTarget("gripper_close")
        self.arm_group.go(wait=True)
        self.arm_group.stop()

        # ── 6. 提升 ────────────────────────────────────────────────────────
        lift_pose = grasp_pose
        lift_pose.pose.position.z += 0.2
        self.whole_body_group.setPoseTarget(lift_pose)
        plan = self.whole_body_group.plan()
        if plan[0]:
            self.whole_body_group.execute(plan[1], wait=True)
        self.whole_body_group.stop()
        self.whole_body_group.clearPoseTargets()

        self.get_logger().info("全身抓取完成!")
        return True

    def _make_joint_constraints(self) -> list:
        """创建柔性关节约束,使臂远离关节极限。"""
        constraints = []
        margin = 0.1  # 留出 0.1 rad 余量

        for joint_name, lower, upper in [
            ("shoulder_pan_joint",  -2.5, 2.5),
            ("shoulder_lift_joint", -2.0, 2.0),
            ("elbow_joint",         -2.8, 2.8),
            ("wrist_1_joint",       -2.0, 2.0),
            ("wrist_2_joint",       -2.0, 2.0),
            ("wrist_3_joint",       -2.0, 2.0),
        ]:
            jc = JointConstraint()
            jc.joint_name = joint_name
            jc.position = 0.0
            jc.tolerance_above = upper - margin
            jc.tolerance_below = lower + margin
            jc.weight = 0.5
            constraints.append(jc)

        return constraints

    def compute_manipulability(self, q: np.ndarray) -> float:
        """
        计算当前臂配置的可操作性度量。

        使用 Yoshikawa 公式:w = sqrt(det(J * J^T))

        返回可操作性值(奇异点处为 0,越大越好)。
        """
        # 实际应用中从 FK 求解器获取雅可比矩阵
        # w = np.sqrt(np.linalg.det(J @ J.T))
        return 1.0  # 占位符


def main():
    rclpy.init()
    node = WholeBodyMobileManipulator()

    # ── 演示:抓取已知位置的物体 ─────────────────────────────────────────
    # 物体在基座坐标系中位于 (0.65, 0.1, 0.75)(桌上)
    obj_pos = np.array([0.65, 0.1, 0.75])
    node.whole_body_pick(obj_pos)

    node.get_logger().info("演示完成!")
    rclpy.shutdown()


if __name__ == "__main__":
    main()

5.5 可操作性优化

臂的**可操作性椭球**揭示了臂在不同方向上的运动能力。在奇异点处,椭球会塌缩——臂会失去一个自由度。

def compute_manipulability_optimized_base_pose(
    object_pos: np.ndarray,
    robot_model,
    n_candidates: int = 100,
) -> tuple:
    """
    找到使臂对物体可达性最佳基座位姿。

    在候选基座网格上搜索,选择 IK 解处可操作性最高的位姿。
    """
    import random

    best_pose = None
    best_manipulability = -1.0

    # 在候选基座位姿上搜索
    for _ in range(n_candidates):
        # 基座随机位姿(x: 0.3-1.0m, y: -0.3-0.3m, theta: -pi 到 pi)
        q_base = np.array([
            random.uniform(0.3, 1.0),
            random.uniform(-0.3, 0.3),
            random.uniform(-np.pi, np.pi),
        ])

        # 计算臂从该基座到达物体位置的 IK
        q_arm = compute_ik_for_base(object_pos, q_base, robot_model)
        if q_arm is None:
            continue  # 该基座的 IK 失败

        # 计算可操作性
        q_full = np.concatenate([q_base, q_arm])
        w = compute_manipulability_from_jacobian(q_full, robot_model)

        if w > best_manipulability:
            best_manipulability = w
            best_pose = q_base

    return best_pose, best_manipulability

6. 第三层 — 现代方法:基于基础模型的操作

6.1 概念

基础模型(在海量数据上预训练的大型神经网络)革新了操作领域。**视觉-语言-动作(VLA)模型**如 RT-2、Octo 和 π0 学习单一策略:

  1. 接收** RGB 图像**(以及可选的语言指令)作为输入
  2. 输出**连续机器人动作**(末端执行器增量位姿 + 夹爪命令)

这与第一、二层有本质区别——同一模型可以在没有显式几何或 IK 的情况下处理新物体、新场景和新任务。

  ┌──────────────────────────────────────────────────────────────────┐
  │              视觉-语言-动作(VLA)模型                            │
  │                                                                  │
  │   [图像] ──┐                                                    │
  │             │                                                    │
  │   [语言]  ──┼──► ┌──────────────┐ ──► 动作 Token 序列            │
  │             │    │  视觉编码器   │       [Δx, Δy, Δz, Δroll,      │
  │             │    │  (ViT/Sam)   │        Δpitch, Δyaw, grip]     │
  │             │    └──────────────┘                                 │
  │             │           │                                         │
  │             │           ▼                                          │
  │             │    ┌──────────────┐                                 │
  │             └───►│  LLM 核心     │◄── (冻结或微调)              │
  │                  │  (PaLM-E /   │                                 │
  │                  │   LLaMA /    │                                 │
  │                  │   RT-2 核心)  │                                 │
  │                  └──────────────┘                                 │
  │                           │                                       │
  │                           ▼                                       │
  │                  ┌──────────────┐                                 │
  │                  │  动作头       │                                 │
  │                  │  (token →   │                                 │
  │                  │   连续值)    │                                 │
  │                  └──────────────┘                                 │
  └──────────────────────────────────────────────────────────────────┘

6.2 动作 Token 化

RT-2 风格模型将连续动作离散化为 token(灵感来自语言 token):

  1. 离散化 每个动作维度为 \(B\) 个箱子(例如 \(B=256\)): $\(\text{action\_id} = \text{Clip}\left(\left\lfloor \frac{a - a_{\min}}{a_{\max} - a_{\min}} \cdot B \right\rfloor, 0, B-1\right)\)$

  2. 将每个 action_id 视为词汇表 token,与语言 token 拼接

  3. 使用标准语言建模损失训练(下一个 token 预测)
  4. 推理时,采样动作 token 并映射回连续值

6.3 ROS 2 VLA 集成

"""
第三层:基于基础模型的移动操作
===============================================
集成 VLA 模型(如 Octo)与 ROS 2,实现通用移动操作。
"""

import rclpy
from rclpy.node import Node
from sensor_msgs.msg import Image, JointState
from geometry_msgs.msg import Twist
from cv_bridge import CvBridge
import numpy as np
import torch

try:
    import octo
    from octo.model import OctoModel
except ImportError:
    print("[WARN] Octo 未安装。请运行: pip install octo-model")
    OctoModel = None


class VLAMobileManipulator(Node):
    """
    基于 VLA 的移动操作。

    VLA 模型接收相机图像和语言指令,
    然后输出控制移动基座和臂的连续动作。
    这是一个端到端的策略——无需单独的导航或 IK 模块。
    """

    def __init__(self):
        super().__init__('vla_mobile_manipulator')
        self.bridge = CvBridge()

        # ── 加载 VLA 模型 ────────────────────────────────────────────────
        self.device = "cuda" if torch.cuda.is_available() else "cpu"
        self.get_logger().info(f"使用设备: {self.device}")

        if OctoModel is not None:
            # 加载 Octo-small(基于 OXE 数据集预训练)
            self.model = OctoModel.load_pretrained("octo-small-1")
            self.model.to(self.device)
            self.get_logger().info("Octo 模型已加载")
        else:
            self.model = None
            self.get_logger().warn("VLA 模型不可用——运行模拟模式")

        # ── ROS 接口 ────────────────────────────────────────────────────
        self.image_sub = self.create_subscription(
            Image,
            "/camera/color/image_raw",
            self.image_callback,
            10
        )
        self.joint_sub = self.create_subscription(
            JointState,
            "/joint_states",
            self.joint_callback,
            10
        )

        # 基座 + 臂速度指令发布器
        self.base_cmd_pub = self.create_publisher(Twist, "/cmd_vel", 10)
        self.arm_cmd_pub = self.create_publisher(
            JointState, "/arm_controller/joint_commands", 10
        )

        self.current_joints = np.zeros(7)  # 臂关节状态
        self.current_image = None
        self.get_logger().info("VLA 移动操作机器人已初始化")

    def image_callback(self, msg: Image):
        """接收相机图像。"""
        try:
            self.current_image = self.bridge.imgmsg_to_cv2(msg, desired_encoding="rgb8")
        except Exception as e:
            self.get_logger().error(f"图像转换失败: {e}")

    def joint_callback(self, msg: JointState):
        """接收当前关节状态。"""
        self.current_joints = np.array(msg.position[:7])

    def compute_vla_action(self, instruction: str,
                             image: np.ndarray,
                             arm_joints: np.ndarray) -> np.ndarray:
        """
        给定当前观测,查询 VLA 模型获取动作。

        Parameters
        ----------
        instruction : str
            语言指令,如 "pick up the red cup"(拾取红色杯子)
        image : ndarray
            当前 RGB 相机图像 (H, W, 3)
        arm_joints : ndarray
            当前臂关节位置

        Returns
        -------
        action : ndarray
            预测动作:[base_vx, base_vy, base_omega,
                      delta_q1, ..., delta_qn, gripper]
        """
        if self.model is None:
            # 模拟模式:返回零动作
            return np.zeros(10)

        # 预处理图像
        image_tensor = torch.from_numpy(image).permute(2, 0, 1)  # CHW
        image_tensor = torch.nn.functional.interpolate(
            image_tensor.unsqueeze(0).float(), size=(256, 256)
        ).squeeze(0)

        # 推理
        with torch.no_grad():
            # Octo 在夹爪坐标系中输出动作
            # Shape: (batch=1, pred_horizon=1, action_dim)
            action = self.model.predict_action(
                image=image_tensor.to(self.device).unsqueeze(0),
                text=[instruction],
            )

        return action[0, 0].cpu().numpy()

    def execute_action(self, action: np.ndarray):
        """
        发送动作到机器人硬件。

        动作格式:[base_vx, base_vy, base_omega,
                  delta_q1, ..., delta_q6, gripper]
        """
        # ── 移动基座速度 ───────────────────────────────────────────────
        base_cmd = Twist()
        base_cmd.linear.x = float(action[0])
        base_cmd.linear.y = float(action[1])
        base_cmd.angular.z = float(action[2])
        self.base_cmd_pub.publish(base_cmd)

        # ── 臂关节增量命令 ──────────────────────────────────────────────
        n_arm_joints = len(self.current_joints)
        arm_delta = action[3:3 + n_arm_joints]
        new_joints = self.current_joints + arm_delta

        arm_cmd = JointState()
        arm_cmd.header.stamp = self.get_clock().now().to_msg()
        arm_cmd.name = [
            "shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint",
            "wrist_1_joint", "wrist_2_joint", "wrist_3_joint"
        ]
        arm_cmd.position = new_joints.tolist()
        self.arm_cmd_pub.publish(arm_cmd)

        # ── 夹爪 ───────────────────────────────────────────────────────
        gripper_cmd = JointState()
        gripper_cmd.name = ["gripper_joint"]
        gripper_cmd.position = [float(action[-1])]
        # 发布到夹爪控制器...

    def run_policy(self, instruction: str, max_steps: int = 200):
        """
        对给定指令运行 VLA 策略。

        Parameters
        ----------
        instruction : str
            执行的语音命令
        max_steps : int
            最大控制步数
        """
        self.get_logger().info(f"执行中: '{instruction}'")

        rate = self.create_rate(10)  # 10 Hz 控制循环
        for step in range(max_steps):
            if self.current_image is None:
                rate.sleep()
                continue

            # 查询模型
            action = self.compute_vla_action(
                instruction, self.current_image, self.current_joints
            )

            # 在机器人上执行
            self.execute_action(action)

            # 检查终止(如果模型输出"完成"信号)
            if np.abs(action).max() < 1e-3:
                self.get_logger().info("动作收敛——停止")
                break

            rate.sleep()

        # 停止机器人
        self.execute_action(np.zeros_like(action))
        self.get_logger().info("策略执行完成")


def main():
    rclpy.init()
    node = VLAMobileManipulator()

    # ── 示例指令 ────────────────────────────────────────────────────────
    instructions = [
        "pick up the cup on the table",      # 拾取桌上的杯子
        "place the cup on the shelf",         # 把杯子放到架子上
        "push the button on the wall",        # 按墙上的按钮
        "open the drawer",                    # 打开抽屉
    ]

    for instruction in instructions:
        node.run_policy(instruction, max_steps=200)

    rclpy.shutdown()


if __name__ == "__main__":
    main()

6.4 仿真到真实迁移(Sim-to-Real Transfer)

训练 VLA 模型涉及**仿真与真实之间的显著领域差距**:

挑战 描述 缓解方法
视觉领域差距 纹理、光照、相机噪声不同 领域随机化(DR)、神经渲染(NeRF, 3DGS)
物理差距 接触动力学、摩擦、柔顺性 仿真到真实迁移 + 强化学习微调、系统辨识
动作空间不匹配 不同机器人型号 动作归一化、跨具身训练
数据效率 VLA 需要数百万条演示 在大数据集上预训练(OXE, BridgeData)
延迟 模型推理速度慢 ONNX 优化、GPU 批处理、动作分块

7. 方法对比

评估维度 第一层:解耦 第二层:全身规划 第三层:VLA 基础模型
基座 + 臂协调 ❌ 独立 ✅ 联合 ✅ 隐式(学习得到)
新物体处理 ❌ 需要模型 ❌ 需要模型 ✅ 零样本泛化
泛化能力 任务专用 任务专用 任务通用
实时性能 ✅ 快 ✅ ~1-5 Hz ⚠️ 取决于模型大小
硬件要求 高(需要 GPU)
搭建复杂度
碰撞感知 ❌ 仅臂 ✅ 全身 ⚠️ 学习得到
奇异点处理 ❌ 手动 ✅ 基于雅可比 ⚠️ 学习得到
适用场景 结构化环境 半结构化环境 开放世界操作
代码复杂度 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐
可靠性 高(可预测) 高(可验证) 中(随机性)

8. 分步实现指南

第一阶段 — 机器人搭建(所有层)

  1. URDF / SDF:定义完整机器人——基座 + 臂 + 传感器——的 URDF
  2. 运动学:将 URDF 加载到 MoveIt 并配置运动链
  3. 标定:标定基座到相机坐标变换(tf 树)
  4. Gazebo 仿真:验证机器人在仿真环境中加载并可移动

第二阶段 — 第一层:解耦方法(第 1 周)

  1. 启动 SLAM 的 Navigation 2 栈(参见 SLAM 项目)
  2. 为已知的抓取位置定义一组**接近位**(approach poses)
  3. 使用 MoveIt's IK 服务实现 compute_ik()
  4. 使用已知物体位姿运行抓取-放置演示
  5. 测试:导航到位 → IK → 抓取 → 提升

第三阶段 — 第二层:全身规划(第 2 周)

  1. 在 SRDF 中定义 whole_body 规划组(基座 + 臂链)
  2. 使用 RRTstarPRMstar 配置 OMPL 全身规划
  3. 将碰撞物体(桌子、架子)添加到规划场景
  4. 将第一层的解耦 navigate_to() + compute_ik() 替换为 whole_body_pick()
  5. 实现可操作性优化用于基座位置选择
  6. 测试:单一全身规划完成抓取,验证碰撞避免

第四阶段 — 第三层:VLA 集成(第 3 周)

  1. 安装 Octo 或 RT-2(需要 Docker + GPU 环境)
  2. 使用真实相机输入运行预训练模型推理
  3. 添加动作归一化(将模型输出映射到机器人的动作限制)
  4. 实现安全层:在发送命令前检查关节限位、速度限幅
  5. 如性能不足,在特定领域数据上微调
  6. 测试:给出语言命令如"拾取蓝色盒子"

9. 扩展与变体

9.1 带臂约束的移动基座导航

使用 MoveIt 中的**任务空间区域(TSR)**约束,确保臂在基座导航时保持特定朝向或保持在工作空间内:

# 基座导航时保持臂向下指
from moveit_msgs.msg import Constraints, OrientationConstraint

orientation_constraint = OrientationConstraint()
orientation_constraint.header.frame_id = "base_link"
orientation_constraint.link_name = "gripper_link"
orientation_constraint.orientation.w = 1.0
orientation_constraint.absolute_x_axis_tolerance = 0.1
orientation_constraint.absolute_y_axis_tolerance = 0.1
orientation_constraint.absolute_z_axis_tolerance = 3.14  # Z 轴自由旋转
orientation_constraint.weight = 1.0

constraints = Constraints()
constraints.orientation_constraints.append(orientation_constraint)
move_group.setPathConstraints(constraints)

9.2 抓取位姿检测

用**基于学习的抓取检测**替代硬编码抓取位姿:

方法 描述 ROS 包
GraspNet 从点云预测 6-DoF 抓取位姿 graspnet_ros
Contact-GraspNet 从部分视图进行接触级抓取合成 contact_graspnet
GPD 基于几何的抓取检测 gpd
6-DOF GraspNet (Isaac) NVIDIA 优化的抓取检测 Isaac SDK

9.3 双臂移动操作

扩展到**双臂**用于双手操作任务(例如,用一只臂握住物体同时用基座移动,或协作搬运):

<!-- 双臂全身组 -->
<group name="dual_arm_whole_body">
  <chain base_link="base_footprint" tip_link="left_gripper_link"/>
  <chain base_link="base_footprint" tip_link="right_gripper_link"/>
</group>

9.4 分层规划

在分层架构中结合第二层和第三层:

  任务规划器(LLM/VLA) ──► 高层目标:"把杯子放到架子上"
  运动规划器(第二层) ──► 基座 + 臂的全身轨迹
  控制器(PID/阻抗) ──► 在硬件上执行轨迹

10. 参考资料