从Lerobot SO-101看遥操作:如何构建高质量机器人模仿学习数据集
1. 项目概述从遥操作到高质量机器人数据集最近在机器人学习社区里Lerobot SO-101 这个项目挺火的尤其是它那个通过遥操作Teleoperation来制作数据集的方法。简单来说这个项目就是教你如何亲手“教”一个机器人手臂比如SO-101机械臂完成一系列复杂的操作任务并把整个过程录下来变成一个结构清晰、可直接用于训练机器学习模型的数据集。这听起来可能有点抽象但如果你玩过用游戏手柄控制机器人或者用过VR手柄来“手把手”教机器人抓东西那你已经接触过遥操作的核心了。Lerobot SO-101项目就是把这种直观的“教学”过程系统化、工程化最终产出学术界和工业界都急需的高质量机器人演示数据。为什么这事儿这么重要因为当前机器人学习特别是模仿学习和强化学习最大的瓶颈之一就是“数据荒”。你很难让一个机器人自己去摸索如何拧瓶盖、叠衣服这既低效又危险。而通过人类专家进行遥操作可以高效、安全地生成大量包含成功策略的“专家演示”数据。Lerobot SO-101数据集就是一个典型例子它包含了20个高质量的操作片段以大约30帧每秒的速度录制并且是多视角的这为算法提供了丰富的观察信息。这个项目不仅仅是一个数据集更是一套方法论展示了如何搭建一套从硬件控制、数据采集到格式整理的完整流水线。无论你是想复现最新的机器人学习论文还是为自己的机械臂项目收集训练数据理解这套流程都至关重要。2. 核心需求与方案选型解析2.1 为什么选择遥操作来构建数据集在机器人领域获取训练数据主要有几种方式仿真Simulation、程序化生成Programmatic和真实世界采集Real-world。仿真速度快、成本低但存在“仿真到现实”Sim2Real的鸿沟程序化生成的数据可能缺乏真实世界的复杂性和噪声。而真实世界采集的数据最保真但传统方式如预编程或笨拙的示教效率极低。遥操作恰恰在真实性和效率之间找到了一个绝佳的平衡点。它的核心需求可以分解为以下几点获取高质量的专家策略人类操作者凭借其强大的感知、规划和灵巧控制能力可以完成非常复杂的操作任务。通过记录人类操作者的动作关节角度、末端执行器位姿和对应的环境观测相机图像、力传感器读数我们就能得到一系列“成功范例”。覆盖多样的任务和场景像Lerobot SO-101数据集可能包含了抓取、放置、插拔、旋转等多种操作。通过设计不同的任务目标操作者可以生成覆盖广泛技能的数据这对于训练一个泛化能力强的策略模型至关重要。确保数据的一致性与安全性让机器人自主探索高风险动作如用力插入、快速移动可能导致设备损坏。遥操作由人类控制可以在保证安全的前提下执行这些必要但有一定风险的动作同时确保每次演示都以成功或可控的状态结束避免了数据中充斥大量失败轨迹。多模态数据同步采集一个现代机器人学习数据集远不止是动作和图像。它可能包括视觉多个固定视角的RGB摄像头提供全局和局部观察。深度深度相机信息用于三维感知。本体感知机械臂的关节角度、速度、扭矩。触觉/力觉末端执行器的六维力/力矩传感器数据。其他音频如放置物体发出的声音、任务特定的传感器如夹爪的握力。 遥操作系统需要能高频率、低延迟地同步记录所有这些模态的数据这是后续算法学习成功的关键。基于这些需求Lerobot项目或类似的遥操作数据采集方案通常会选择一种“混合”架构物理机器人硬件 灵活易用的控制器 统一的数据记录中间件。2.2 典型系统架构与工具链选择一个可用的遥操作数据采集系统其软件栈大致可以分为三层控制层这是人类操作者与机器人交互的接口。常见的选择有3D空间鼠标/游戏手柄如3Dconnexion SpaceMouse、Xbox手柄。这类设备成本低、易上手适合控制末端执行器的粗略移动和简单动作。但对于需要高精度或模仿人手灵巧操作的任务能力有限。VR控制器如HTC Vive、Oculus Touch控制器。能提供6自由度的位姿控制非常直观仿佛用手直接抓取虚拟物体来控制真实机器人。是当前灵巧操作研究的热门选择。力反馈主手如Geomagic TouchPhantom Omni、达芬奇手术机器人主手。这类设备能提供力反馈让操作者感受到机器人与环境的交互力实现更精细、更安全的操作但成本极高。键盘鼠标最基础的方案通过键盘按键映射关节或末端的增量运动。精度高但操作不直观效率低。 对于Lerobot SO-101这类项目为了平衡易用性和成本使用高端游戏手柄或VR控制器是更常见的选择。机器人接口与中间件层这一层负责将控制器的输入转换为机器人能够理解的指令如位置、速度、扭矩指令并实时获取机器人的状态。ROS (Robot Operating System)几乎是这个领域的事实标准。它提供了设备驱动、消息通信、数据记录等一系列标准化工具。你会有一个ROS节点专门读取手柄或VR控制器的数据例如使用joy包或vrpn_client_ros。另一个节点或同一个节点根据映射关系生成目标位姿或关节目标并通过ROS话题Topic发布给机器人的控制器节点。机器人的状态关节、图像等也通过ROS话题发布出来。数据记录与处理层这是数据集生成的核心。同样ROS提供了强大的工具rosbag。它可以订阅指定的ROS话题并将所有消息以时间同步的方式记录到一个.bag文件中。一个典型的录制命令可能是rosbag record -O so101_demo_01.bag /joint_states /camera_front/color/image_raw /camera_overhead/depth/image_rect_raw /franka_state_controller/tip_state /wrench录制结束后你会得到一堆.bag文件。但这还不是最终的数据集。数据集需要是结构化的、易于读取的格式如HDF5、TFRecord或简单的Numpy数组.npz。因此需要一个后处理流水线将rosbag文件解析、同步处理不同传感器的时间戳偏移、降采样如果需要并转换成目标格式。Lerobot项目很可能就提供了这样的转换脚本。注意工具链的选择高度依赖于你的机器人硬件。SO-101可能特指某款机械臂例如Franka Emika Panda的某个变种。你需要确认ROS中是否存在该机器人的官方或社区驱动包。如果使用仿真环境如MuJoCo、PyBullet、Isaac Sim那么控制接口和数据记录方式会有所不同但核心逻辑相通——记录状态和动作序列。3. 实操搭建一步步构建你的遥操作数据采集系统假设我们基于一个相对常见的配置进行一台Franka Emika Panda机械臂作为SO-101的类似物两个Intel RealSense D435i相机提供RGB-D数据一个游戏手柄作为控制器软件栈基于ROS Noetic。3.1 硬件连接与环境配置首先确保所有硬件正确连接并安装驱动机器人启动Franka Panda机器人并确保其franka_ros驱动包已安装并能正常启动。通常通过roslaunch franka_control franka_control.launch robot_ip:ROBOT_IP load_gripper:true来启动机器人的底层控制器。摄像头安装RealSense的ROS驱动realsense2_camera。分别启动两个相机节点并确保它们发布的话题名称不冲突例如/camera_front/color/image_raw和/camera_top/color/image_raw。手柄连接游戏手柄安装joy包。使用jstest或rosrun joy joy_node测试手柄按键和轴映射是否正确。3.2 创建遥操作控制节点这是整个系统的核心。我们需要编写一个ROS节点例如teleop_node.py它主要做三件事订阅手柄输入(/joy话题)。订阅当前机器人状态(如/franka_state_controller/joint_states,/franka_state_controller/tip_state)。根据映射关系计算并发布控制指令(如/cartesian_impedance_example_controller/desired_pose或/joint_trajectory_controller/command)。这里以更直观的笛卡尔空间末端控制为例。我们将手柄的摇杆映射到末端执行器在X, Y, Z方向的平移以及绕X, Y, Z轴的旋转欧拉角或角速度。同时用肩键L1/R1控制夹爪的开合。#!/usr/bin/env python3 import rospy from sensor_msgs.msg import Joy from geometry_msgs.msg import Pose, PoseStamped, Twist from franka_gripper.msg import MoveActionGoal import tf.transformations as tf_trans class PandaTeleop: def __init__(self): rospy.init_node(panda_teleop_node) # 订阅手柄话题 self.joy_sub rospy.Subscriber(/joy, Joy, self.joy_callback) # 发布目标位姿话题 (假设使用笛卡尔阻抗控制器) self.pose_pub rospy.Publisher(/cartesian_impedance_controller/desired_pose, PoseStamped, queue_size10) # 发布夹爪控制话题 self.gripper_pub rospy.Publisher(/franka_gripper/move/goal, MoveActionGoal, queue_size10) self.current_pose Pose() # 存储当前末端位姿 # 从TF或状态话题获取初始位姿这里简化为一个初始值 self.current_pose.position.x 0.3 self.current_pose.position.y 0.0 self.current_pose.position.z 0.2 self.current_pose.orientation.x 0.0 self.current_pose.orientation.y 0.0 self.current_pose.orientation.z 0.0 self.current_pose.orientation.w 1.0 # 控制灵敏度参数 (米/秒 或 弧度/秒 每摇杆最大偏移) self.linear_scale 0.05 # 5 cm/s self.angular_scale 0.5 # 0.5 rad/s self.last_buttons [0] * 12 # 假设手柄有12个按钮 self.gripper_width 0.08 # 初始夹爪宽度 def joy_callback(self, msg): # 映射左摇杆上下-Z轴左右-Y轴右摇杆上下-X轴左右-绕Z轴旋转 # 注意映射关系需根据你的手柄和习惯调整并考虑坐标系方向 dx msg.axes[3] * self.linear_scale # 右摇杆上下 - X dy msg.axes[0] * self.linear_scale # 左摇杆左右 - Y dz msg.axes[1] * self.linear_scale # 左摇杆上下 - Z (取反取决于你的习惯) droll msg.axes[2] * self.angular_scale # 举例左肩键轴 - 绕X旋转 dpitch msg.axes[5] * self.angular_scale # 举例右肩键轴 - 绕Y旋转 dyaw msg.axes[4] * self.angular_scale # 右摇杆左右 - 绕Z旋转 # 更新位置 self.current_pose.position.x dx self.current_pose.position.y dy self.current_pose.position.z dz # 更新姿态 (这里简化处理实际应用中应使用四元数进行增量旋转避免万向锁) # 这是一个简化示例生产环境应使用更稳健的姿态更新方法 current_euler tf_trans.euler_from_quaternion([self.current_pose.orientation.x, self.current_pose.orientation.y, self.current_pose.orientation.z, self.current_pose.orientation.w]) new_euler [current_euler[0] droll, current_euler[1] dpitch, current_euler[2] dyaw] new_quat tf_trans.quaternion_from_euler(new_euler[0], new_euler[1], new_euler[2]) self.current_pose.orientation.x new_quat[0] self.current_pose.orientation.y new_quat[1] self.current_pose.orientation.z new_quat[2] self.current_pose.orientation.w new_quat[3] # 发布目标位姿 target_pose_msg PoseStamped() target_pose_msg.header.stamp rospy.Time.now() target_pose_msg.header.frame_id panda_link0 # 通常在世界或基坐标系 target_pose_msg.pose self.current_pose self.pose_pub.publish(target_pose_msg) # 处理夹爪控制 (例如A键开B键关) if msg.buttons[0] 1 and self.last_buttons[0] 0: # A键按下事件 gripper_goal MoveActionGoal() gripper_goal.goal.width 0.08 # 打开到8cm gripper_goal.goal.speed 0.1 self.gripper_pub.publish(gripper_goal) self.gripper_width 0.08 elif msg.buttons[1] 1 and self.last_buttons[1] 0: # B键按下事件 gripper_goal MoveActionGoal() gripper_goal.goal.width 0.00 # 闭合 gripper_goal.goal.speed 0.1 self.gripper_pub.publish(gripper_goal) self.gripper_width 0.00 self.last_buttons msg.buttons[:] if __name__ __main__: try: teleop PandaTeleop() rospy.spin() except rospy.ROSInterruptException: pass实操心得姿态控制是遥操作中最棘手的部分之一。直接用欧拉角增量更新很容易导致万向锁和方向突变。更稳健的做法是将手柄摇杆的偏移量视为在当前末端坐标系下的角速度指令然后对角速度进行积分转换为旋转增量再通过四元数乘法应用到当前姿态上。这能保证旋转操作的直观性和平滑性。上面的代码仅作原理演示生产环境需要更严谨的实现。3.3 数据录制与同步策略启动所有节点机器人控制、相机、遥操作节点后就可以开始录制了。录制策略至关重要确定录制话题你需要记录所有算法训练可能用到的信息。一个典型的列表包括/joint_states机器人的所有关节状态位置、速度、力矩。/cartesian_impedance_controller/desired_pose你发布的目标位姿这将被视为“动作”Action。/camera_front/color/image_raw和/camera_front/depth/image_rect_raw前视RGB-D图像。/camera_top/color/image_raw和/camera_top/depth/image_rect_raw顶视RGB-D图像。/franka_state_controller/tip_state末端执行器的实际位姿和速度可作为状态的一部分。/franka_gripper/joint_states或/franka_gripper/state夹爪的宽度和力信息。/wrench如果有力/力矩传感器。使用rosbag录制在任务开始前在终端运行录制命令。建议为每次演示Episode单独录制一个bag文件并以任务名和序号命名如pick_and_place_ep01.bag。rosbag record -O /path/to/data/pick_and_place_ep01.bag \ /joint_states \ /cartesian_impedance_controller/desired_pose \ /camera_front/color/image_raw \ /camera_front/aligned_depth_to_color/image_raw \ /camera_top/color/image_raw \ /camera_top/aligned_depth_to_color/image_raw \ /franka_state_controller/tip_state \ /franka_gripper/joint_states同步与触发为了精确标记每个演示片段的开始和结束我强烈建议在系统中引入一个“录制控制”话题。例如你可以让操作者在开始任务前按下手柄的“开始”键如START键该节点发布一个/record_start消息任务完成后按“结束”键如SELECT键发布/record_stop消息。在后期处理时可以根据这两个消息的时间戳来精确裁剪bag文件。同时把这个控制话题也录进bag里。4. 从Rosbag到结构化数据集后处理流水线录制得到的一堆.bag文件还不是数据集。我们需要将它们转换成一种易于批量加载和处理的格式。Lerobot数据集很可能使用HDF5 (.h5) 或一种简单的字典式.npz文件。下面描述一个典型的处理流程。4.1 数据解析与对齐编写一个Python脚本例如bag_to_dataset.py使用rosbagAPI (rosbag包) 来读取数据。核心挑战在于时间同步不同传感器的发布频率和延迟不同。提取时间序列对于每个话题将其所有消息的时间戳和消息内容提取出来。时间戳最好统一转换为从该bag文件开始录制的相对时间秒。重采样到固定频率机器学习模型通常需要固定频率的输入。假设我们目标频率是30Hz约0.033秒间隔。我们需要将所有数据流图像、状态、动作插值或分配到统一的时间网格上。对于连续状态如关节角度、位姿可以使用线性插值。对于图像不能插值。通常选择距离目标时间戳最近的那一帧图像。这可能导致某些帧被轻微重复或跳过但对于30FPS的录制和目标频率影响很小。对于动作通常使用在目标时间戳之前最近发布的那个动作指令。关键步骤动作与状态的配对这是模仿学习的核心。在时间步t模型观察到的状态s_t应该包括此时的图像、关节状态等。而模型要预测的或在行为克隆中要学习的动作a_t应该是导致从状态s_t转移到s_{t1}的那个指令。因此你需要确保在时间对齐后s_t和a_t在数据中是正确配对的。通常a_t就是在时间t时记录到的目标位姿或关节目标。4.2 数据压缩与存储格式对齐后的数据需要高效存储。图像通常以JPEG或PNG格式压缩后存储而不是原始的BGR数组以节省大量空间。import numpy as np import h5py import cv2 from scipy import interpolate import rosbag from cv_bridge import CvBridge def process_bag(bag_path, output_h5_path, target_fps30): bridge CvBridge() bag rosbag.Bag(bag_path) target_interval 1.0 / target_fps # 1. 首先遍历一遍bag获取所有消息的时间范围和内容建立时间序列 # ... (此处省略详细的rosbag读取和原始数据列表构建代码) ... # 假设我们得到了以下列表 # joint_time, joint_positions_list (每个元素是np.array) # pose_time, target_pose_list (每个元素是geometry_msgs/Pose) # image_time_front, image_list_front (每个元素是sensor_msgs/Image) # 2. 创建统一的时间网格 start_time min(joint_time[0], pose_time[0], image_time_front[0]) end_time max(joint_time[-1], pose_time[-1], image_time_front[-1]) num_steps int((end_time - start_time) * target_fps) 1 timestamps np.linspace(start_time, end_time, num_steps) # 3. 为每个数据流创建插值器或最近邻查找器 # 关节位置插值 (假设7个关节) joint_interpolator interpolate.interp1d(joint_time, np.vstack(joint_positions_list).T, axis1, bounds_errorFalse, fill_valueextrapolate) resampled_joints joint_interpolator(timestamps).T # 形状 (num_steps, 7) # 目标位姿插值 (处理位置和四元数 separately) pose_positions np.array([[p.position.x, p.position.y, p.position.z] for p in target_pose_list]) pose_orientations np.array([[p.orientation.x, p.orientation.y, p.orientation.z, p.orientation.w] for p in target_pose_list]) pos_interpolator interpolate.interp1d(pose_time, pose_positions.T, axis1, bounds_errorFalse, fill_valueextrapolate) # 四元数插值需要球面线性插值(Slerp)这里简化用线性插值后归一化对于短时间隔或慢旋转可接受 ori_interpolator interpolate.interp1d(pose_time, pose_orientations.T, axis1, bounds_errorFalse, fill_valueextrapolate) resampled_pos pos_interpolator(timestamps).T resampled_ori ori_interpolator(timestamps).T resampled_ori resampled_ori / np.linalg.norm(resampled_ori, axis1, keepdimsTrue) # 归一化 # 图像处理寻找最近邻 resampled_images [] img_times_array np.array(image_time_front) for t in timestamps: idx np.argmin(np.abs(img_times_array - t)) msg image_list_front[idx] cv_image bridge.imgmsg_to_cv2(msg, desired_encodingbgr8) # 可在此处调整图像大小如从1280x720缩放到224x224 cv_image_resized cv2.resize(cv_image, (224, 224)) # 编码为JPEG字节流节省空间 _, img_encoded cv2.imencode(.jpg, cv_image_resized) resampled_images.append(img_encoded.tobytes()) # 4. 存储到HDF5 with h5py.File(output_h5_path, w) as f: # 创建数据集 f.create_dataset(timestamps, datatimestamps) f.create_dataset(joint_positions, dataresampled_joints, compressiongzip) f.create_dataset(target_positions, dataresampled_pos, compressiongzip) f.create_dataset(target_orientations, dataresampled_ori, compressiongzip) # 存储图像字节流需要特殊处理 dt h5py.special_dtype(vlennp.dtype(uint8)) # 可变长度数据类型 images_ds f.create_dataset(front_images, (len(resampled_images),), dtypedt) for i, img_bytes in enumerate(resampled_images): images_ds[i] np.frombuffer(img_bytes, dtypeuint8) # 存储元数据 f.attrs[fps] target_fps f.attrs[episode_name] bag_path.stem f.attrs[num_steps] num_steps bag.close() print(fProcessed {bag_path} - {output_h5_path})这个脚本将单个bag文件处理成了一个HDF5文件。一个完整的数据集就是由许多这样的HDF5文件组成可能还有一个总的metadata.json文件来描述每个文件对应的任务、成功与否、操作者等信息。5. 遥操作中的挑战与实战避坑指南在实际搭建和运行这套系统时你会遇到不少坑。以下是我从经验中总结的几个关键点和解决方案5.1 延迟与抖动流畅操作的杀手遥操作最大的体验破坏者就是延迟和抖动。即使几百毫秒的延迟也会让操作者感到极度不适难以进行精细操作。来源分析图像采集与传输延迟相机曝光、图像编码、通过USB或网络传输到主机、ROS话题发布每一步都有延迟。RGB-D相机的深度计算会引入额外延迟。控制回路延迟从手柄信号输入到ROS节点处理再到生成控制指令发送给机器人控制器最后机器人执行这个闭环的延迟。网络延迟如果机器人与主机是网络连接如Franka的FCI接口网络波动会带来不确定延迟。显示延迟操作者观看的反馈图像如果也有延迟会进一步恶化体验。缓解策略使用低延迟相机和接口优先选择带全局快门、支持USB3.0以上接口的相机。在ROS中可以尝试降低图像分辨率、关闭压缩、使用image_transport的raw主题来减少处理开销。优化ROS通信使用UDP传输模式如果支持代替默认的TCP可以减少连接开销和延迟。确保所有节点在同一台性能强劲的机器上运行避免跨网络通信。预测显示这是一个高级技巧。既然延迟不可避免可以在显示给操作者的画面上根据当前机器人的速度预测并渲染出未来几十毫秒后的画面从而在视觉上“抵消”一部分延迟。这对VR遥操作尤其有效。控制频率与滤波确保你的控制节点以足够高的频率如100Hz以上运行。对于手柄输入可以加入一个低通滤波器来平滑信号避免因操作者手部微小抖动导致的机器人高频抖动但滤波会引入相位延迟需要谨慎调整参数。5.2 坐标系变换与标定一切准确的基础你的遥操作可能感觉“不对劲”比如手柄向左推机器人却向斜前方走。这几乎都是坐标系映射错误。核心问题手柄坐标系 vs 机器人基坐标系 vs 末端坐标系你需要明确手柄摇杆的偏移量是映射到机器人基坐标系世界坐标系下的运动还是映射到当前末端坐标系下的运动前者对于全局导航直观后者对于精细的相对操作更直观。Lerobot这类操作通常使用后者“本体坐标系”控制。相机外参标定多个相机的画面需要知道彼此之间的位置关系以及相对于机器人基座的位置。这需要通过手眼标定Eye-in-Hand或Eye-to-Hand来获取精确的变换矩阵。没有准确的标定你录制的多视角图像就无法在三维空间中对齐会严重影响后续基于三维感知的算法。控制器缩放与死区手柄摇杆的原始输出是[-1, 1]。你需要将其缩放Scale到合适的线速度和角速度。同时设置一个死区Deadzone来忽略摇杆回中的微小漂移避免机器人无故微动。实操步骤明确映射关系在代码中清晰定义例如手柄左摇杆前后 - 末端坐标系下的Z轴移动。进行手眼标定使用aruco码或棋盘格通过ROS的camera_calibration和hand_eye_calibration包完成。这是一个必须且繁琐的步骤务必做好。在RViz中可视化在开发阶段一定要在RViz中实时显示目标位姿Pose、当前末端位姿、相机视野Camera显示。这能帮你快速发现坐标系映射错误。5.3 数据质量与一致性决定数据集上限不是所有录制的数据都是好数据。低质量的数据会污染数据集让模型学坏。常见问题部分失败演示操作中途失误但最终勉强完成或者通过“骚操作”完成。这类数据包含了许多非最优甚至错误的策略。不一致的初始状态每次演示开始时物体位置、姿态有较大随机性是好事情但如果是由于摆放不精确导致的“意外”随机性则可能引入无关噪声。操作者风格差异不同操作者习惯不同导致同一任务的动作轨迹差异巨大可能让模型困惑。传感器缺失或异常录制过程中相机短暂失焦、深度图出现空洞、关节扭矩超限警告等。质量控制方案定义清晰的成功标准在录制前明确任务成功的客观条件如物体被放入目标区域且保持静止2秒。可以编写一个简单的验证脚本在录制后自动或半自动地检查每个片段是否成功。标准化录制流程制作一个启动脚本确保每次录制前机器人回到固定的“Home”位置环境灯光、背景尽量一致。使用语音或提示音引导操作者开始和结束。后处理与筛选在将bag转换为最终数据集时加入一个手动或半自动的审核环节。可以快速浏览每个片段的图像缩略图或关键指标曲线如末端轨迹、夹爪力剔除明显异常的片段。Lerobot数据集强调“高质量”和“紧凑”很可能经过了严格的筛选和清理。记录元数据在数据集中除了状态和动作还应记录每个片段的标签如success: booloperator_id: strtask_variant: str。这为后续的数据分析、分层训练或过滤提供了可能。搭建一套可靠的遥操作数据采集系统是一项系统工程涉及机器人学、软件工程和用户体验的交叉。从Lerobot SO-101这样的项目中我们看到的不仅仅是一个数据集更是一套经过实践验证的、从真实物理交互中高效获取知识的方法论。当你亲手让机械臂随着你的手柄移动并完整记录下这一切时你会对“机器人如何学习”有更深刻的理解。这套系统不仅是数据工厂也是研究和验证新算法原型的强大工具。