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 概念¶
最简单的方法是将**移动基座**和**机械臂****解耦**:
- 导航 — 将移动基座导航至目标物体附近的接近位(approach pose)
- 锁定基座 — 然后求解**逆运动学(IK)**,使机械臂到达物体
- 执行 — 完成抓取
这种方法易于实现和调试,但无法根据臂的可操作性来调整基座位姿——基座可能到达一个臂无法舒适够到目标的位置。
步骤 1:导航 步骤 2:锁定基座,求解 IK
┌──────────────────┐ ┌────────────────────────┐
│ │ │ │
│ [基座]──► │ │ [基座] [臂伸向目标] │
│ ↓ │ │ ↖ ───► [物体] │
│ 接近位姿 │ │ IK 解 │
└──────────────────┘ └────────────────────────┘
4.2 运动学模型¶
对于**解耦系统**,臂的正向运动学为:
其中 \(\mathbf{q}_{arm} \in \mathbb{R}^n\) 为臂关节位置,\(\mathbf{x}_{ee} = [p_x, p_y, p_z, \phi, \theta, \psi]\) 为末端执行器位姿(位置 + 姿态)。
**逆运动学**求解:
对于 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 概念¶
全身运动规划**联合优化**移动基座位姿和臂关节配置,同时满足:
- 可达性 — 末端执行器必须到达物体
- 可操作性 — 臂应处于灵巧构型(远离奇异点)
- 无碰撞 — 整个机器人身体避免障碍物
- 运动平滑 — 最小化关节力矩 / 移动距离
解耦方法: 全身规划:
┌──────────────┐ ┌──────────────────────────┐
│ 基座 → 位姿 │ │ 联合优化基座 + 臂: │
│ 臂 → IK │ │ │
│ (两个步骤) │ │ min ||q - q_init||² │
└──────────────┘ │ s.t. ||x_ee - x_obj|| < ε │
│ J(q) q̇ = ẋ │
└──────────────────────────┘
5.2 全身运动学¶
组合机器人状态为:
其中 \(\mathbf{q}_{base} = [x, y, \theta]\) 为差分驱动基座的位形,\(\mathbf{q}_{arm} \in \mathbb{R}^n\) 为臂关节。
**全身雅可比矩阵**将关节速度映射到末端执行器速度:
其中 \(J_{wb} \in \mathbb{R}^{6 \times (3+n)}\) 为全身雅可比矩阵:
臂的**可操作性度量**(Yoshikawa)为:
最大化 \(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 学习单一策略:
- 接收** RGB 图像**(以及可选的语言指令)作为输入
- 输出**连续机器人动作**(末端执行器增量位姿 + 夹爪命令)
这与第一、二层有本质区别——同一模型可以在没有显式几何或 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):
-
离散化 每个动作维度为 \(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)\)$
-
将每个
action_id视为词汇表 token,与语言 token 拼接 - 使用标准语言建模损失训练(下一个 token 预测)
- 推理时,采样动作 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. 分步实现指南¶
第一阶段 — 机器人搭建(所有层)¶
- URDF / SDF:定义完整机器人——基座 + 臂 + 传感器——的 URDF
- 运动学:将 URDF 加载到 MoveIt 并配置运动链
- 标定:标定基座到相机坐标变换(
tf树) - Gazebo 仿真:验证机器人在仿真环境中加载并可移动
第二阶段 — 第一层:解耦方法(第 1 周)¶
- 启动 SLAM 的 Navigation 2 栈(参见 SLAM 项目)
- 为已知的抓取位置定义一组**接近位**(approach poses)
- 使用 MoveIt's IK 服务实现
compute_ik() - 使用已知物体位姿运行抓取-放置演示
- 测试:导航到位 → IK → 抓取 → 提升
第三阶段 — 第二层:全身规划(第 2 周)¶
- 在 SRDF 中定义
whole_body规划组(基座 + 臂链) - 使用
RRTstar或PRMstar配置 OMPL 全身规划 - 将碰撞物体(桌子、架子)添加到规划场景
- 将第一层的解耦
navigate_to()+compute_ik()替换为whole_body_pick() - 实现可操作性优化用于基座位置选择
- 测试:单一全身规划完成抓取,验证碰撞避免
第四阶段 — 第三层:VLA 集成(第 3 周)¶
- 安装 Octo 或 RT-2(需要 Docker + GPU 环境)
- 使用真实相机输入运行预训练模型推理
- 添加动作归一化(将模型输出映射到机器人的动作限制)
- 实现安全层:在发送命令前检查关节限位、速度限幅
- 如性能不足,在特定领域数据上微调
- 测试:给出语言命令如"拾取蓝色盒子"
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 分层规划¶
在分层架构中结合第二层和第三层:
10. 参考资料¶
- Siciliano et al. (2016). Robotics: Modelling, Planning and Control — 全身运动学教科书
- Yoshikawa, T. (1985). "Manipulability of Robotic Mechanisms." IJHR — 可操作性度量
- Koenig & Howard (2004). "Design and Use Paradigms for Gazebo." IROS — 机器人仿真
- Coleman et al. (2014). "Reducing the Barrier to Entry of Complex Robotic Software." ICRA — MoveIt 介绍
- Sucan et al. (2012). "The Open Motion Planning Library." ICRA — OMPL
- Brohan et al. (2022). "RT-2: Vision-Language-Action Models Transfer Web Knowledge to Robotic Control." arXiv:2307.15218 — RT-2 论文
- Octo Model Team (2024). "Octo: An Open-Source Generalist Robot Policy." GitHub — Octo 模型
- Black et al. (2024). "π0: A Vision-Language-Action Model for Generalist Robot Control." Physical Intelligence — π0 论文
- Xiao et al. (2024). "Open X-Embodiment: Robotic Learning Datasets and RT-X Models." arXiv:2310.08864 — OXE 数据集(训练基础模型)
- Zhou et al. (2023). "Contact-GraspNet: Efficient 6-DoF Grasp Generation in Cluttered Scenes." ICRA — GraspNet
- Sundaresan et al. (2020). "Rethinking Grasp: A Survey of Deep Learning Approaches for Robotic Grasping." arXiv — 抓取检测综述
- ROS 2 MoveIt 2 Documentation — MoveIt 2 官方教程
- Stretch RE-1 Documentation — Hello Robot 移动操作机器人
- Fetch Robotics Mobile Manipulator — Fetch 硬件 + ROS 集成