具身智能数据Pipeline

一.项目介绍

1.目的:写一个脚本去自动化的执行整套具身智能数据处理管线

输入为几百 GB 的原始 HDF5 格式机器人轨迹数据(含视觉图像、机械臂关节状态、动作指令、文本标签),输出为可直接用于 VLA 模型训练的标准化、高置信度、语义丰富的干净数据集

2.交付物:不是虚假的成功率,而是清洗速度(如处理100GB数据仅需XX分钟),以及清洗前后数据质量的可视化对比图。

3.核心技术栈:

  • 底层通信与数据采集:ROS 2 (Robot Operating System 2),MCAP (通过 rosbag2),Foxglove Studio(可以直接拖拽查看 MCAP 文件里的多视角视频、机械臂 3D 姿态和电流曲线,用于抽样检查数据质量。)
  • 数据清理:NumPy,SciPy(处理时间戳插值对齐),OpenCV,Polars(用 Rust 编写的 Polars 替代传统的 Pandas,可以带来 10 倍以上的数据清洗加速。)
  • 自动化打标与分布式计算:vLLm(vLLM 是目前吞吐量最高的推理后端引擎),Ray (构建大规模 Pipeline 的秘密武器)

二.环境搭建

打开 Anaconda Prompt / 终端,执行以下命令,创建干净的虚拟环境并安装依赖:

复制代码
# 创建虚拟环境(固定Python3.10,兼容所有具身智能工具包,避免版本冲突)
conda create -n vla_data_pipeline python=3.10 -y

# 激活虚拟环境
conda activate vla_data_pipeline

# 安装核心依赖
pip install h5py numpy pandas matplotlib opencv-python tqdm
pip install scipy pillow
pip install huggingface_hub

具身智能领域,无论是斯坦福 ALOHA、伯克利 BridgeData,还是实验室自研机器人采集的数据,99% 都以 HDF5(.h5/.hdf5)格式存储。它就像一个带压缩功能的虚拟文件夹,能嵌套存放高维图像数组、机械臂传感器数据、动作指令等多模态信息,但不同开源数据集的内部键名、层级结构完全不统一,新手往往连数据都读不出来,更别说后续处理。

接下来创建一个 Python 文件 dataset**_parser.py**。目的是编写能读取和解析 HDF5 格式轨迹数据集的脚本

python 复制代码
import h5py
import numpy as np
import cv2
import os

class VLADatasetParser:
    def __init__(self, h5_path):
        self.h5_path = h5_path
        self.data_dict = {}
        
    def extract_trajectory(self):
        print(f"🔄 正在解析数据集: {self.h5_path}")
        try:
            with h5py.File(self.h5_path, 'r') as f:
                # ALOHA 格式:直接在根目录
                demo = f

                # ==========================================
                # 1. 提取并解压缩图像数据
                # ==========================================
                # 选择相机:cam_high(主视角)、cam_left_wrist、cam_right_wrist
                camera_name = 'cam_high'
                compressed_images = demo[f'observations/images/{camera_name}'][:]
                
                # 解压缩图像(关键步骤!)
                print(f"🎬 正在解压缩 {camera_name} 图像...")
                images = []
                for i in range(len(compressed_images)):
                    # 用 cv2 解压缩 jpg 图像
                    img = cv2.imdecode(compressed_images[i], cv2.IMREAD_COLOR)
                    # cv2 读取的是 BGR,转回 RGB 保持一致
                    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
                    images.append(img)
                images = np.array(images)
                
                self.data_dict['images'] = images

                # ==========================================
                # 2. 提取机械臂本体状态
                # ==========================================
                self.data_dict['ee_pose'] = demo['observations/qpos'][:]

                # ==========================================
                # 3. 提取动作序列
                # ==========================================
                self.data_dict['actions'] = demo['action'][:]

                # ==========================================
                # 4. ALOHA 通常没有语言指令
                # ==========================================
                self.data_dict['instruction'] = "ALOHA Mobile Cabinet Task"
                    
        except Exception as e:
            print(f"❌ 解析失败: {e}")
            import traceback
            traceback.print_exc()

    def export_to_mp4(self, output_filename="trajectory_output.mp4", fps=30):
        if 'images' not in self.data_dict:
            print("❌ 字典中没有图像数据,无法生成视频!")
            return

        images = self.data_dict['images']
        num_frames, height, width, channels = images.shape
        
        print(f"\n🎬 正在生成视频: {output_filename} ({num_frames} 帧)...")
        
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        out = cv2.VideoWriter(output_filename, fourcc, fps, (width, height))

        for i in range(num_frames):
            frame = images[i]
            
            # 颜色空间转换:RGB → BGR
            frame_bgr = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR)
            out.write(frame_bgr)

        out.release()
        print(f"✅ 视频生成完毕!已保存至: {os.path.abspath(output_filename)}")


if __name__ == "__main__":
    target_file = "目标文件路径"
    
    parser = VLADatasetParser(target_file)
    parser.extract_trajectory()
    parser.export_to_mp4("result.mp4")

代码只需要修改路径便可以运行

python 复制代码
if __name__ == "__main__":
    # 【关键修改】把这里改成你自己的 HDF5 文件路径
    # 例如:target_file = "/path/to/your/libero_dataset.hdf5"
    target_file = "sample_dataset.hdf5" 
    
    parser = VLADatasetParser(target_file)
    parser.extract_trajectory()
    # 【可选修改】可以改输出视频的文件名
    parser.export_to_mp4("result.mp4")

如果发现代码报错,先检查hdf5数据集格式结构,创建一个test_structrue.py文件

python 复制代码
import h5py

# 【修改这里】改成你的数据集路径
h5_path = "/storage-ftp-data/datasets/metasynthesis/wangxiaorong/vla_data_pipline/aloha_mobile_cabinet/episode_0.hdf5"

def print_structure(name, obj):
    print(name)
    if isinstance(obj, h5py.Dataset):
        print(f"  形状: {obj.shape}, 类型: {obj.dtype}")

with h5py.File(h5_path, 'r') as f:
    f.visititems(print_structure)

实现功能:

1.多格式兼容的智能数据提取

这是脚本的核心基础能力,能自动适配主流 VLA 数据集格式:

  • 支持的数据集:Robomimic、Bridge V2、ALOHA、LIBERO 及自定义测试数据

2. 一键式视频可视化导出

把抽象的 HDF5 数据转化为人类可直观观察的 MP4 视频:

3. 内置 5 项健壮性预处理优化

解决 VLA 数据集常见的格式混乱问题,保证数据提取的稳定性:

  • 维度统一:自动识别并转换 PyTorch 常用的 NCHW 格式为 OpenCV 常用的 NHWC 格式
  • 字符串安全解码:兼容 HDF5 中常见的 numpy 标量 / 数组 /bytes 等多种字符串存储格式
  • 形状安全解包:兼容单通道灰度图缺失通道维度的情况
  • 图像值域归一化:自动处理深度学习常用的 [0.0, 1.0] 或 [-1.0, 1.0] 浮点图像,映射回 OpenCV 要求的 [0, 255] uint8 格式
  • 颜色空间自适应:根据实际通道数自动选择 RGB→BGR 或灰度→BGR 转换

result

刚开始脚本HDF5 数据的结构不对,重新识别进行合成。

三.基于运动学规则的脏数据过滤与质检

1.先制造脏数据,后面进行测试

新建generate_test_cases.py,代码如下:

python 复制代码
import h5py
import numpy as np
import os


def create_fake_aloha_hdf5(filename, num_frames, qpos_data, action_data):
    """
    按照ALOHA真实数据集格式生成测试用HDF5文件
    :param filename: 输出文件名
    :param num_frames: 轨迹总帧数
    :param qpos_data: 机械臂关节角度数据
    :param action_data: 动作指令数据
    """
    with h5py.File(filename, 'w') as f:
        # 写入动作数据
        f.create_dataset('action', data=action_data)
        # 创建观测值组
        obs = f.create_group('observations')
        # 写入关节角度
        obs.create_dataset('qpos', data=qpos_data)
        # 写入测试用图像数据(全黑占位,不影响过滤)
        obs.create_dataset('images/top', data=np.zeros((num_frames, 224, 224, 3), dtype=np.uint8))
    print(f"📦 生成测试文件: {filename} (帧数: {num_frames})")


# 创建测试数据存放目录
os.makedirs("test_data", exist_ok=True)

# ======================
# 测试用例1:完美的正常轨迹(质检通过)
# ======================
frames = 150
# 用linspace生成平滑渐变的关节数据,模拟机械臂正常移动
qpos_good = np.linspace(0, 1.5, frames).reshape(-1, 1) * np.ones((1, 14))
action_good = np.copy(qpos_good)
create_fake_aloha_hdf5("test_data/case1_perfect.hdf5", frames, qpos_good, action_good)

# ======================
# 测试用例2:轨迹过短(质检失败)
# ======================
frames_short = 10
qpos_short = np.zeros((frames_short, 14))
action_short = np.zeros((frames_short, 14))
create_fake_aloha_hdf5("test_data/case2_too_short.hdf5", frames_short, qpos_short, action_short)

# ======================
# 测试用例3:全程静止无动作(质检失败)
# ======================
frames_lazy = 100
qpos_lazy = np.zeros((frames_lazy, 14))
action_lazy = np.zeros((frames_lazy, 14))
create_fake_aloha_hdf5("test_data/case3_lazy.hdf5", frames_lazy, qpos_lazy, action_lazy)

# ======================
# 测试用例4:关节速度突变/抽搐(质检失败)
# ======================
frames_spike = 100
qpos_spike = np.linspace(0, 0.5, frames_spike).reshape(-1, 1) * np.ones((1, 14))
# 投毒:第50帧,第4个关节数值突然暴增,模拟传感器故障
qpos_spike[50, 3] = 10.0
action_spike = np.copy(qpos_spike)
create_fake_aloha_hdf5("test_data/case4_spike.hdf5", frames_spike, qpos_spike, action_spike)

print("\n🎯 4个极限测试用例已生成在 test_data/ 目录下!")

2.运动学过滤器核心代码

新建**kinematic_filter.py**,实现批量质检功能:

python 复制代码
import h5py
import numpy as np
import glob


class KinematicFilter:
    def __init__(self, min_length=15, movement_threshold=0.01, velocity_limit=1.0):
        """
        初始化运动学过滤器,阈值可根据机械臂硬件参数自定义
        :param min_length: 轨迹最小有效帧数
        :param movement_threshold: 最小位移阈值,单位rad
        :param velocity_limit: 最大允许瞬时角速度,单位rad/帧
        """
        self.min_length = min_length
        self.movement_threshold = movement_threshold
        self.velocity_limit = velocity_limit

    def run_checks(self, qpos, actions):
        """
        运行三大物理规则质检
        :param qpos: 机械臂关节角度数组 [帧数, 关节数]
        :param actions: 动作指令数组 [帧数, 动作维度]
        :return: (是否通过, 失败原因列表, 总位移量, 最大角速度)
        """
        num_frames = qpos.shape[0]
        fail_reasons = []

        # 规则1:轨迹过短剔除
        if num_frames < self.min_length:
            fail_reasons.append(f"轨迹过短: 仅 {num_frames} 帧 (要求 >= {self.min_length})")

        # 规则2:静止无动作剔除
        # 计算首尾帧关节角度的L2距离,衡量总位移
        total_movement = np.linalg.norm(qpos[-1] - qpos[0])
        if total_movement < self.movement_threshold:
            fail_reasons.append(f"轨迹疑似静止: 总位移量 {total_movement:.4f} < 阈值 {self.movement_threshold}")

        # 规则3:速度突变/奇异点剔除
        # np.diff计算相邻帧的差值,即瞬时角速度
        velocities = np.diff(qpos, axis=0)
        max_velocity = np.max(np.abs(velocities))
        if max_velocity > self.velocity_limit:
            fail_reasons.append(f"发现速度突变: 最大瞬时角速度 {max_velocity:.4f} > 阈值 {self.velocity_limit}")

        # 无失败原因则质检通过
        is_passed = len(fail_reasons) == 0
        return is_passed, fail_reasons, total_movement, max_velocity


def process_single_file(h5_path):
    """
    处理单个HDF5文件,执行质检
    :param h5_path: 待处理文件路径
    """
    print(f"🛡️  开始质检文件: {h5_path}")
    try:
        with h5py.File(h5_path, 'r') as f:
            # 兼容多种数据集格式,提取核心数据
            if 'action' in f and 'observations' in f:
                # ALOHA格式
                actions = f['action'][:]
                qpos = f['observations']['qpos'][:]
            elif 'data' in f:
                # BridgeData/Robomimic格式
                demo_key = list(f['data'].keys())[0]
                demo = f['data'][demo_key]
                actions = demo['actions'][:]
                qpos = demo['obs']['joint_positions'][:]
            else:
                print("❌ 数据集格式无法识别!跳过该文件")
                return None

            # 初始化过滤器并运行质检
            filter_engine = KinematicFilter(
                min_length=15,
                movement_threshold=0.01,
                velocity_limit=1.0
            )
            is_passed, reasons, movement, max_vel = filter_engine.run_checks(qpos, actions)

            # 打印质检报告
            print("\n=== 📝 质检报告 ===")
            print(f"轨迹长度: {qpos.shape[0]} 帧")
            print(f"总位移量: {movement:.4f}")
            print(f"最大角速度: {max_vel:.4f}")
            
            if is_passed:
                print("✅ 结论: 质检通过!这是一条高质量的有效轨迹。")
            else:
                print("❌ 结论: 质检失败!该轨迹将被丢弃。")
                for r in reasons:
                    print(f"   - 原因: {r}")

            return is_passed

    except Exception as e:
        print(f"❌ 读取文件时发生错误: {e}")
        return None


# 批量处理入口
if __name__ == "__main__":
    # 扫描test_data文件夹下所有的hdf5文件
    test_files = glob.glob("test_data/*.hdf5")
    
    print("🚀 启动数据清洗管线批量质检...\n")
    passed_count = 0
    total_count = len(test_files)

    for file_path in sorted(test_files):
        result = process_single_file(file_path)
        if result:
            passed_count += 1
        print("-" * 50)

    # 打印最终统计报告
    print("\n" + "="*50)
    print("📊 批量质检最终统计报告")
    print(f"总计处理文件数: {total_count}")
    print(f"质检通过文件数: {passed_count}")
    print(f"数据留存率: {(passed_count/total_count)*100:.1f}%")
    print("="*50)

最后会打印出

python 复制代码
🚀 启动数据清洗管线批量质检...

🛡️  开始质检文件: test_data/case1_perfect.hdf5

=== 📝 质检报告 ===
轨迹长度: 150 帧
总位移量: 5.6125
最大角速度: 0.0101
✅ 结论: 质检通过!这是一条高质量的有效轨迹。
--------------------------------------------------
🛡️  开始质检文件: test_data/case2_too_short.hdf5

=== 📝 质检报告 ===
轨迹长度: 10 帧
总位移量: 0.0000
最大角速度: 0.0000
❌ 结论: 质检失败!该轨迹将被丢弃。
   - 原因: 轨迹过短: 仅 10 帧 (要求 >= 15)
   - 原因: 轨迹疑似静止: 总位移量 0.0000 < 阈值 0.01
--------------------------------------------------
🛡️  开始质检文件: test_data/case3_lazy.hdf5

=== 📝 质检报告 ===
轨迹长度: 100 帧
总位移量: 0.0000
最大角速度: 0.0000
❌ 结论: 质检失败!该轨迹将被丢弃。
   - 原因: 轨迹疑似静止: 总位移量 0.0000 < 阈值 0.01
--------------------------------------------------
🛡️  开始质检文件: test_data/case4_spike.hdf5

=== 📝 质检报告 ===
轨迹长度: 100 帧
总位移量: 1.8708
最大角速度: 9.7525
❌ 结论: 质检失败!该轨迹将被丢弃。
   - 原因: 发现速度突变: 最大瞬时角速度 9.7525 > 阈值 1.0
--------------------------------------------------

==================================================
📊 批量质检最终统计报告
总计处理文件数: 4
质检通过文件数: 1
数据留存率: 25.0%
==================================================

四.多模态视觉 - 动作时序对齐

  1. 基准选择:永远以采样频率最低的传感器(通常是相机)为基准时间轴,因为无法凭空生成不存在的图像数据;
  2. 插值算法选择
    • 连续物理量(关节角度、末端位姿、连续动作):采用线性插值,保证动作平滑自然,符合机械臂运动规律;
    • 离散指令(夹爪开合 0/1、吸盘吸 / 放):采用最近邻插值,绝对不能用线性插值 ------ 否则会出现 0.5 的半开半合状态,导致硬件报错,甚至损坏机械臂。
  3. 未在下方算法表现出来,但也需要注意的地方
    • 旋转量(末端姿态)的插值陷阱:末端位姿(EE Pose)采用线性插值,这对于平移部分(X, Y, Z)是正确的,但对于旋转部分(通常是四元数 Quaternion 或欧拉角 Euler)绝对不行必须使用球面线性插值'
    • 降采样时的"混叠效应":通常机械臂的本体状态读取频率很高(如 500Hz 或 1000Hz),而相机频率很低(如 30Hz)。以相机为基准去"抽取"机械臂状态时,本质上是一个大幅度的降采样过程。如果你只是简单地做线性插值或最近邻抽取,一旦机械臂的高频信号中存在抖动或传感器噪声,你可能会刚好抽样到那个"噪声尖峰",导致低频轨迹出现莫名的剧烈波动(奈奎斯特混叠定理)。在进行对齐和降采样之前,应该对高频的连续物理量(如关节速度、力矩)先进行一次低通滤波(Low-Pass Filter,如巴特沃斯滤波器),滤除高于 15Hz(30Hz的一半)的高频噪声,然后再去插值对齐。
    • 时间戳的"绝对真伪"与硬件延迟:相机的 timestamp 通常是操作系统收到 USB 数据帧的时刻,此时距离真实的"快门曝光"时刻已经过去了 30ms~50ms。而机械臂的 timestamp 可能是从实时内核(RTOS)发出的。即使你用算法对齐了时间戳,在物理世界上它们依然是错位的。

先通过模拟数据,直观展示对齐前后的效果,新建time_sync_demo.py

python 复制代码
import numpy as np
from scipy.interpolate import interp1d


def synchronize_multimodal_demo():
    print("⏳ 开始多模态时间戳对齐模拟...")

    # ======================
    # 1. 模拟真实采集的不对齐数据
    # ======================
    total_duration = 1.0  # 轨迹总时长1秒
    cam_fps = 30  # 相机30Hz,1秒30帧
    robot_hz = 50  # 机械臂50Hz,1秒50组动作

    # 生成各自的时间戳
    cam_timestamps = np.linspace(0, total_duration, int(total_duration * cam_fps))
    robot_timestamps = np.linspace(0, total_duration, int(total_duration * robot_hz))

    # 模拟机械臂7个关节的连续动作:正弦波平滑运动
    robot_qpos = np.sin(robot_timestamps * 2 * np.pi).reshape(-1, 1) * np.ones((1, 7))
    # 模拟夹爪离散动作:前0.5秒张开(0),后0.5秒闭合(1)
    robot_gripper = np.where(robot_timestamps < 0.5, 0.0, 1.0).reshape(-1, 1)

    # 打印对齐前的状态
    print(f"🚨 对齐前状态:")
    print(f"   📷 相机帧数: {len(cam_timestamps)} 帧")
    print(f"   🦾 关节数据帧数: {len(robot_qpos)} 帧")
    print(f"   🤏 夹爪指令帧数: {len(robot_gripper)} 帧")

    # ======================
    # 2. 核心对齐算法
    # ======================
    # 对连续关节数据:线性插值
    interp_qpos = interp1d(
        robot_timestamps,
        robot_qpos,
        kind='linear',
        axis=0,
        fill_value="extrapolate"  # 边界外推,避免首尾帧报错
    )
    synced_qpos = interp_qpos(cam_timestamps)

    # 对离散夹爪数据:最近邻插值
    interp_gripper = interp1d(
        robot_timestamps,
        robot_gripper,
        kind='nearest',
        axis=0,
        fill_value="extrapolate"
    )
    synced_gripper = interp_gripper(cam_timestamps)

    # ======================
    # 3. 验证对齐结果
    # ======================
    print(f"\n✅ 对齐后状态:(以相机时间轴为基准)")
    print(f"   📷 相机帧数: {len(cam_timestamps)} 帧")
    print(f"   🦾 对齐后关节数据帧数: {len(synced_qpos)} 帧")
    print(f"   🤏 对齐后夹爪指令帧数: {len(synced_gripper)} 帧")

    # 拼接成最终的机器人状态矩阵
    final_robot_state = np.hstack([synced_qpos, synced_gripper])
    print(f"\n🎉 最终对齐后的机器人状态矩阵形状: {final_robot_state.shape}")
    print(f"   说明:{final_robot_state.shape[0]}帧,每帧对应{final_robot_state.shape[1]}维状态(7关节+1夹爪)")


if __name__ == "__main__":
    synchronize_multimodal_demo()

工业级对齐引擎代码

新建time_synchronizer.py,实现可复用的对齐功能,兼容真实数据集:

python 复制代码
import numpy as np
from scipy.interpolate import interp1d


class TimeSynchronizer:
    def __init__(self, target_fps=30):
        """
        初始化时间同步器
        :param target_fps: 目标对齐帧率,通常和相机帧率一致,默认30Hz
        """
        self.target_fps = target_fps

    def synchronize(self, images, qpos, actions):
        """
        通用多模态数据对齐核心方法
        :param images: 视觉图像数组 [相机帧数, 高度, 宽度, 通道数]
        :param qpos: 机械臂关节角度数组 [机器人帧数, 关节维度]
        :param actions: 动作指令数组 [机器人帧数, 动作维度]
        :return: 对齐后的图像、关节数据、动作指令
        """
        num_cam_frames = images.shape[0]
        num_robot_frames = qpos.shape[0]

        # 优化:如果帧数天然一致,直接返回,节省算力
        if num_cam_frames == num_robot_frames:
            print("✅ 图像与动作帧数天然一致,无需对齐")
            return images, qpos, actions

        print(f"⏳ 开始时序对齐:相机{num_cam_frames}帧 → 机械臂{num_robot_frames}帧")

        # 1. 根据帧数反推时间轴
        total_duration = num_cam_frames / self.target_fps
        cam_timestamps = np.linspace(0, total_duration, num_cam_frames)
        robot_timestamps = np.linspace(0, total_duration, num_robot_frames)

        # 2. 分离连续动作和离散夹爪指令
        # 行业通用规范:actions数组最后一列是夹爪开合指令(0/1)
        continuous_actions = actions[:, :-1]
        gripper_actions = actions[:, -1:]

        # 3. 对机械臂关节数据:线性插值
        interp_qpos = interp1d(
            robot_timestamps,
            qpos,
            kind='linear',
            axis=0,
            fill_value="extrapolate"
        )
        synced_qpos = interp_qpos(cam_timestamps)

        # 4. 对连续动作:线性插值
        interp_actions = interp1d(
            robot_timestamps,
            continuous_actions,
            kind='linear',
            axis=0,
            fill_value="extrapolate"
        )
        synced_continuous_actions = interp_actions(cam_timestamps)

        # 5. 对夹爪离散指令:最近邻插值
        interp_gripper = interp1d(
            robot_timestamps,
            gripper_actions,
            kind='nearest',
            axis=0,
            fill_value="extrapolate"
        )
        synced_gripper = interp_gripper(cam_timestamps)

        # 6. 重新拼接动作数组
        synced_actions = np.hstack([synced_continuous_actions, synced_gripper])

        print(f"✅ 时序对齐完成!对齐后帧数:{synced_actions.shape[0]}")
        return images, synced_qpos, synced_actions


# 测试入口
if __name__ == "__main__":
    # 模拟真实数据
    test_images = np.zeros((30, 224, 224, 3), dtype=np.uint8)  # 30帧图像
    test_qpos = np.zeros((50, 14))  # 50帧关节数据
    test_actions = np.zeros((50, 14))  # 50帧动作,最后一列是夹爪

    # 初始化同步器并执行对齐
    sync_engine = TimeSynchronizer(target_fps=30)
    aligned_imgs, aligned_qpos, aligned_actions = sync_engine.synchronize(test_images, test_qpos, test_actions)

    # 打印结果
    print(f"\n📊 对齐结果维度:")
    print(f"📷 图像: {aligned_imgs.shape}")
    print(f"🦾 关节: {aligned_qpos.shape}")
    print(f"⚡ 动作: {aligned_actions.shape}")

五.VLM 自动化重打标与端到端管线整合

传统具身智能数据集的语言标签存在两个致命问题:

  1. 标签质量极低 :几十万条轨迹共用同一句简单指令(如pick up the cup),没有任何细节;
  2. 标注成本极高:人工标注一条轨迹需要十几秒,大规模数据集标注需要耗费大量人力物力。

用这种标签训练的 VLA 模型,只能听懂极其简单的指令,稍微换个复杂描述就完全无法执行,泛化性极差。本阶段的核心目标:

  1. 开发 VLM 驱动的自动化重打标工具,用多模态大模型为每条轨迹生成细粒度、高丰富度的语言指令;
  2. 设计「离线模拟 + 真实 API」双轨模式,无外网、无 API 密钥也能跑通全流程;
  3. 把前三个阶段的所有模块整合,形成一键运行的端到端全流程管线,输入原始数据,直接输出可用于训练的干净数据集;
  4. 实现成品数据的校验与可视化,生成带 AI 标签的演示视频。
  • 关键帧提取 :无需把整段视频送入 VLM,只需提取轨迹的首帧(初始状态)和尾帧(任务完成状态),大幅降低 API 调用成本和推理时间;
  • 提示词工程:设计面向机器人领域的专业提示词,强制 VLM 生成包含物体属性、颜色、空间关系变化的细粒度指令;
  • 双轨模式兼容:无 API 时用模拟模式跑通流程,有 API 时直接接入 Gemini、GPT-4o、通义千问等大模型;
  • 管线解耦:所有模块可单独开关,可根据需求跳过过滤、对齐、重打标中的任意环节。

创建vlm_relabeler.py

python 复制代码
import h5py
import numpy as np
from PIL import Image
import time
import os
import base64
import io
import requests


class VLMRelabeler:
    def __init__(self, api_key=None, model_type="modelscope_api", modelscope_endpoint=None, model_name="Qwen/Qwen-VL-Chat"):
        """
        初始化VLM重打标器(适配魔搭社区原生在线API)
        :param api_key: 魔搭社区获取的API-KEY
        :param model_type: 模型类型,可选 "modelscope_api"(魔搭在线API)或 "mock"(离线模拟)
        :param modelscope_endpoint: 魔搭API服务的调用地址(Endpoint)
        :param model_name: 你在魔搭创建服务时选择的模型名称
        """
        self.api_key = api_key
        self.model_type = model_type
        self.endpoint = modelscope_endpoint
        self.model_name = model_name

        # 校验并初始化魔搭API客户端
        if self.model_type == "modelscope_api":
            if not self.api_key or not self.endpoint:
                print("⚠️  缺少魔搭API-KEY或调用地址,将自动切换到离线模拟模式")
                self.model_type = "mock"
            else:
                print("🔗 正在初始化魔搭社区在线API客户端...")
                print(f"📌 使用模型: {self.model_name}")
                # 配置请求头
                self.headers = {
                    "Authorization": f"Bearer {self.api_key}",
                    "Content-Type": "application/json"
                }
                print("✅ 魔搭API客户端初始化成功!")
        else:
            print("⚠️  启用本地离线模拟VLM模式")

    def _pil_to_base64(self, pil_image):
        """
        内部工具:将PIL图像转换为Base64编码(魔搭API通用的图片输入格式)
        """
        buffered = io.BytesIO()
        pil_image.save(buffered, format="JPEG")
        img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8')
        return f"data:image/jpeg;base64,{img_base64}"

    def extract_key_frames(self, h5_path):
        """
        从HDF5文件中提取轨迹的首帧和尾帧(和原有代码完全兼容,无需修改)
        """
        print(f"📦 正在打开数据文件: {h5_path}")
        try:
            with h5py.File(h5_path, 'r') as f:
                # 兼容多种数据集格式,提取图像数据
                if 'observations' in f and 'images/top' in f['observations']:
                    images = f['observations']['images/top'][:]
                elif 'data' in f:
                    demo_key = list(f['data'].keys())[0]
                    demo = f['data'][demo_key]
                    if 'obs' in demo and 'agentview_image' in demo['obs']:
                        images = demo['obs']['agentview_image'][:]
                    else:
                        raise ValueError("未找到图像数据")
                else:
                    raise ValueError("未找到图像数据")
                
                # 提取首尾关键帧
                start_frame_np = images[0]
                end_frame_np = images[-1]
                
                # 转换为PIL Image格式
                start_image = Image.fromarray(start_frame_np)
                end_image = Image.fromarray(end_frame_np)

                print(f"✅ 成功提取首尾关键帧,图像尺寸: {start_image.size}")
                return start_image, end_image

        except Exception as e:
            print(f"❌ 提取关键帧失败: {e}")
            return None, None

    def generate_rich_instruction(self, start_img, end_img):
        """
        核心方法:调用魔搭社区在线API生成高质量细粒度语言指令
        :param start_img: 首帧PIL图像
        :param end_img: 尾帧PIL图像
        :return: 生成的语言指令
        """
        # 机器人标注专用中文提示词,适配VLM模型理解
        prompt = """你是一位专业的机器人学家和数据标注专家。
我会给你看机器人操作任务的【初始状态图】和【完成状态图】。
请用一句**详细、自然、符合机器人操作指令**的中文描述这个任务。
要求:
1. 包含具体的物体名称、颜色、形状、材质;
2. 清晰描述物体之间的空间位置变化;
3. 不要使用模糊的词汇,要具体;
4. 只用一句话回答,不要分段,不要加序号。"""

        if self.model_type == "modelscope_api":
            print("🧠 [魔搭在线API] 正在调用云端模型分析...")
            max_retries = 3  # 加入重试逻辑,避免网络波动失败
            for attempt in range(max_retries):
                try:
                    # 1. 将PIL图像转换为Base64格式
                    start_img_b64 = self._pil_to_base64(start_img)
                    end_img_b64 = self._pil_to_base64(end_img)

                    # 2. 构建符合 /v1/chat/completions 标准规范的请求体
                    payload = {
                        "model": self.model_name,
                        "messages": [
                            {
                                "role": "user",
                                "content": [
                                    {
                                        "type": "image_url",
                                        "image_url": {"url": start_img_b64}
                                    },
                                    {
                                        "type": "image_url",
                                        "image_url": {"url": end_img_b64}
                                    },
                                    {
                                        "type": "text", 
                                        "text": prompt
                                    }
                                ]
                            }
                        ],
                        "temperature": 0.3,
                        "max_tokens": 200
                    }

                    # 3. 发送API请求
                    response = requests.post(
                        url=self.endpoint,
                        headers=self.headers,
                        json=payload,
                        timeout=30
                    )

                    # 4. 解析返回结果
                    if response.status_code == 200:
                        result = response.json()
                        # 适配魔搭主流VLM模型的返回格式
                        generated_text = result["choices"][0]["message"]["content"]
                        return generated_text.strip()
                    else:
                        print(f"第{attempt+1}次调用失败,状态码: {response.status_code},错误信息: {response.text}")
                        time.sleep(2)

                except Exception as e:
                    print(f"第{attempt+1}次调用异常: {e},重试中...")
                    time.sleep(2)

            return "API调用失败,已达到最大重试次数"
        else:
            # 离线模拟模式(和原有代码完全兼容)
            print("🧠 [模拟VLM] 正在分析视觉差异特征...")
            time.sleep(1)

            # 模拟生成的高质量中文机器人指令
            mock_instructions = [
                "机械臂成功抓起了透明的塑料瓶,并将它竖直放进了蓝色的收纳箱里。",
                "机器人从木质桌子上拿起了红色的苹果,轻轻地放进了金属水槽中。",
                "末端执行器握着黄色的海绵,在白色陶瓷盘子上平滑擦拭,清洁了表面的污渍。",
                "机械臂从白色书桌上拿起黑色的剪刀,放进了右侧的灰色抽屉里。",
                "机器人从下层架子上抓起绿色的陶瓷杯子,平稳地放在了上层的木桌上。"
            ]

            import random
            return random.choice(mock_instructions)


# 测试入口
if __name__ == "__main__":
    # 测试用的HDF5文件路径
    target_file = "/storage-ftp-data/datasets/metasynthesis/wangxiaorong/vla_data_pipline/aloha_mobile_cabinet/episode_0.hdf5"

    # ======================
    # 核心配置:填入你的魔搭API信息
    # ======================
    # 替换成你从魔搭获取的API-KEY
    YOUR_MODELSCOPE_API_KEY = "key"
    # 替换成你创建的API服务调用地址Endpoint
    YOUR_MODELSCOPE_ENDPOINT = "https://api-inference.modelscope.cn/v1/chat/completions"
    # 替换成你选择的模型名称
    YOUR_MODEL_NAME = "Qwen/Qwen3.5-397B-A17B"

    # 模式1:使用魔搭在线API
    relabeler = VLMRelabeler(
        api_key=YOUR_MODELSCOPE_API_KEY,
        model_type="modelscope_api",
        modelscope_endpoint=YOUR_MODELSCOPE_ENDPOINT,
        model_name=YOUR_MODEL_NAME
    )
    
    # 模式2:使用离线模拟模式(无API时用)
    # relabeler = VLMRelabeler(model_type="mock")

    # 执行测试
    print("\n" + "="*50)
    print("🎥 步骤1:提取轨迹首尾关键帧")
    img_start, img_end = relabeler.extract_key_frames(target_file)

    if img_start and img_end:
        print("\n✍️  步骤2:调用魔搭VLM API进行自动化重打标")
        new_instruction = relabeler.generate_rich_instruction(img_start, img_end)
        
        print("\n✅ VLM重打标完成!")
        print(f"👉 生成的高质量指令: \n\033[92m\"{new_instruction}\"\033[0m")
        print("="*50)

创建main_pipeline.py

python 复制代码
import os
import h5py
import glob
from tqdm import tqdm

# 导入前面开发的所有核心模块
from dataset_parser import VLADatasetParser
from kinematic_filter import KinematicFilter
from time_synchronizer import TimeSynchronizer
from vlm_relabeler import VLMRelabeler


def run_vla_data_pipeline(input_folder, output_folder, enable_filter=True, enable_sync=True, enable_relabel=True):
    """
    端到端VLA数据清洗全流程管线
    :param input_folder: 原始HDF5数据存放文件夹
    :param output_folder: 清洗后的干净数据输出文件夹
    :param enable_filter: 是否启用脏数据过滤
    :param enable_sync: 是否启用时序对齐
    :param enable_relabel: 是否启用VLM重打标
    """
    print("🚀 启动具身智能VLA大规模数据清洗全流程管线 🚀\n")

    # 创建输出文件夹
    os.makedirs(output_folder, exist_ok=True)

    # 实例化所有核心引擎
    filter_engine = KinematicFilter(min_length=15, movement_threshold=0.01, velocity_limit=1.0) if enable_filter else None
    sync_engine = TimeSynchronizer(target_fps=30) if enable_sync else None
        # 实例化所有核心引擎
    filter_engine = KinematicFilter(min_length=15, movement_threshold=0.01, velocity_limit=1.0) if enable_filter else None
    sync_engine = TimeSynchronizer(target_fps=30) if enable_sync else None
    
    # ======================
    # 修改点:初始化魔搭API重打标器
    # ======================
    relabel_engine = None
    if enable_relabel:
        # 注意:这里的参数名要和你 vlm_relabeler.py 里 __init__ 的参数名对应
        relabel_engine = VLMRelabeler(
            api_key=YOUR_MODELSCOPE_API_KEY,
            model_type="modelscope_api",
            modelscope_endpoint=YOUR_MODELSCOPE_ENDPOINT,
            model_name=YOUR_MODEL_NAME
        )

    # 扫描所有待处理的HDF5文件
    all_files = glob.glob(os.path.join(input_folder, "*.hdf5"))
    print(f"📂 扫描到 {len(all_files)} 个待处理的原始轨迹文件\n")

    valid_count = 0
    total_count = len(all_files)

    # 批量处理所有文件,带进度条
    for file_path in tqdm(all_files, desc="Processing Trajectories"):
        file_name = os.path.basename(file_path)
        tqdm.write(f"\n🔄 正在处理文件: {file_name}")

        # ======================
        # 步骤1:解析提取核心数据
        # ======================
        parser = VLADatasetParser(file_path)
        parser.extract_trajectory()
        data = parser.data_dict

        # 解析失败直接跳过
        if 'images' not in data or 'ee_pose' not in data or 'actions' not in data:
            tqdm.write(f"❌ {file_name} 解析失败,跳过")
            continue

        # ======================
        # 步骤2:脏数据过滤质检
        # ======================
        if enable_filter:
            is_passed, reasons, _, _ = filter_engine.run_checks(data['ee_pose'], data['actions'])
            if not is_passed:
                tqdm.write(f"❌ {file_name} 质检失败,已丢弃 | 失败原因: {reasons}")
                continue
            tqdm.write(f"✅ {file_name} 质检通过")

        # ======================
        # 步骤3:多模态时序对齐
        # ======================
        if enable_sync:
            synced_imgs, synced_qpos, synced_actions = sync_engine.synchronize(
                data['images'], data['ee_pose'], data['actions']
            )
            # 更新为对齐后的数据
            data['images'] = synced_imgs
            data['ee_pose'] = synced_qpos
            data['actions'] = synced_actions

        # ======================
        # 步骤4:VLM自动化重打标
        # ======================
        if enable_relabel:
            from PIL import Image
            img_start = Image.fromarray(data['images'][0])
            img_end = Image.fromarray(data['images'][-1])
            new_instruction = relabel_engine.generate_rich_instruction(img_start, img_end)
            data['instruction'] = new_instruction
            tqdm.write(f"🏷️  生成指令: {new_instruction}")

        # ======================
        # 步骤5:标准化落盘存储
        # ======================
        out_path = os.path.join(output_folder, f"clean_{file_name}")
        with h5py.File(out_path, 'w') as f:
            f.create_dataset('images', data=data['images'], compression='gzip')  # 压缩存储,节省空间
            f.create_dataset('qpos', data=data['ee_pose'])
            f.create_dataset('actions', data=data['actions'])
            f.create_dataset('instruction', data=data['instruction'])

        valid_count += 1
        tqdm.write(f"✅ {file_name} 处理完成,已保存至输出文件夹")

    # ======================
    # 最终统计报告
    # ======================
    print("\n" + "="*60)
    print("📊 数据清洗全流程任务完成最终报告")
    print(f"总计输入文件数: {total_count}")
    print(f"成功清洗并留存的文件数: {valid_count}")
    print(f"数据清洗留存率: {(valid_count/total_count)*100:.1f}%" if total_count > 0 else "无输入文件")
    print(f"干净数据集已保存至: {os.path.abspath(output_folder)}")
    print("="*60)


# 管线入口
# 管线入口
if __name__ == "__main__":
    # ======================
    # 核心配置区:填入你的魔搭API信息
    # ======================
    # 1. 输入/输出文件夹配置
    INPUT_FOLDER = "/storage-ftp-data/datasets/metasynthesis/wangxiaorong/vla_data_pipline/aloha_mobile_cabinet/"    # 存放原始脏数据的文件夹
    OUTPUT_FOLDER = "/storage-ftp-data/datasets/metasynthesis/wangxiaorong/vla_data_pipline/data/"  # 存放清洗后干净数据的文件夹

    # 2. 魔搭API配置(必填)
    # 替换成你从魔搭个人中心/服务详情页获取的API-KEY
    YOUR_MODELSCOPE_API_KEY = "key"
    # 替换成你创建的在线服务的调用地址(Endpoint)
    YOUR_MODELSCOPE_ENDPOINT = "https://api-inference.modelscope.cn/v1/chat/completions"
    # 替换成你选择的模型名称(如 Qwen/Qwen-VL-Chat)
    YOUR_MODEL_NAME = "Qwen/Qwen3.5-35B-A3B"

    # 3. 管线功能开关(可根据需求自由开启/关闭)
    ENABLE_FILTER = True    # 是否开启脏数据过滤
    ENABLE_SYNC = True      # 是否开启时序对齐
    ENABLE_RELABEL = True   # 是否开启VLM重打标

    # ======================
    # 启动全流程管线
    # ======================
    run_vla_data_pipeline(
        input_folder=INPUT_FOLDER,
        output_folder=OUTPUT_FOLDER,
        enable_filter=ENABLE_FILTER,
        enable_sync=ENABLE_SYNC,
        enable_relabel=ENABLE_RELABEL
    )

六.成品数据校验与可视化代码

新建inspect_and_visualize.py,校验清洗后的数据并生成带 AI 标签的演示视频:

python 复制代码
import h5py
import os
import cv2
import numpy as np


def inspect_clean_data(file_path):
    """
    校验清洗后的成品数据,打印数据结构和标签
    :param file_path: 成品HDF5文件路径
    """
    print(f"🕵️  开始检验成品数据: {file_path}")
    if not os.path.exists(file_path):
        print("❌ 找不到文件!请确认管线已成功生成clean_data目录")
        return

    with h5py.File(file_path, 'r') as f:
        print("\n=== 📦 成品数据集内部结构 ===")
        print(f"📷 图像序列: {f['images'].shape}, 数据类型: {f['images'].dtype}")
        print(f"🦾 机械臂关节: {f['qpos'].shape}, 数据类型: {f['qpos'].dtype}")
        print(f"⚡ 动作指令: {f['actions'].shape}, 数据类型: {f['actions'].dtype}")
        
        # 提取并解码VLM生成的标签
        instruction = f['instruction'][()]
        if isinstance(instruction, bytes):
            instruction = instruction.decode('utf-8')
        
        print("\n=== 🏷️  VLM自动生成的高质量语义标签 ===")
        print(f"\033[92m\"{instruction}\"\033[0m\n")
        return instruction


def generate_labeled_video(clean_h5_path, output_mp4="final_demo.mp4", fps=30):
    """
    生成带AI标签水印的演示视频
    :param clean_h5_path: 清洗后的成品文件路径
    :param output_mp4: 输出视频文件名
    :param fps: 视频帧率
    """
    print(f"🎬 开始生成带VLM标签的可视化Demo: {clean_h5_path}")
    
    if not os.path.exists(clean_h5_path):
        print("❌ 找不到成品文件!请确认管线已成功运行")
        return

    # 读取数据
    try:
        with h5py.File(clean_h5_path, 'r') as f:
            images = f['images'][:]
            instruction = f['instruction'][()]
            if isinstance(instruction, bytes):
                instruction = instruction.decode('utf-8')
    except Exception as e:
        print(f"❌ 读取文件失败: {e}")
        return

    num_frames, height, width, channels = images.shape
    
    # 初始化视频写入器
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_mp4, fourcc, fps, (width, height))

    print(f"✍️  正在将AI标签和质检水印烧录进 {num_frames} 帧画面中...")

    for i in range(num_frames):
        # 转换色彩空间
        frame_bgr = cv2.cvtColor(images[i], cv2.COLOR_RGB2BGR)

        # 左上角添加绿色质检通过水印
        cv2.putText(frame_bgr, "STATUS: Cleaned & Aligned", (20, 40),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
        
        # 底部添加黄色VLM标签,先画黑色半透明背景提升可读性
        display_text = f"VLM: {instruction[:60]}..." if len(instruction) > 60 else f"VLM: {instruction}"
        cv2.rectangle(frame_bgr, (0, height - 50), (width, height), (0, 0, 0), -1)
        cv2.putText(frame_bgr, display_text, (20, height - 20),
                    cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 255), 2)

        out.write(frame_bgr)

    out.release()
    print(f"✅ 可视化Demo生成完成!已保存至: {os.path.abspath(output_mp4)}")


# 执行入口
if __name__ == "__main__":
    # 指向清洗后的成品文件
    target_file = "clean_data/clean_case1_perfect.hdf5"

    # 校验数据
    instruction = inspect_clean_data(target_file)

    # 生成带标签的视频
    if instruction:
        generate_labeled_video(target_file, "vla_cleaned_demo.mp4")

本文大量引用该文章面向具身智能的大规模轨迹自动化清洗与数据重打标Pipeline_具身智能数据清洗主要方法有哪些-CSDN博客

感谢作者贡献。

相关推荐
流年似水~1 小时前
素材管理:剪辑前整理素材的底层逻辑
人工智能·程序人生·语言模型·ai编程
geneculture2 小时前
当前主流人工智能(大语言模型、世界模型)与融智学双重形式化路径之间的根本差异
人工智能·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·人际间性·人机间性
江南一点雨2 小时前
让AI更懂你,松哥教你一招!
人工智能
淡海水2 小时前
【AI模型】核心概念解析
人工智能·机器学习
AI 编程助手GPT2 小时前
GPT-5.6意外曝光、Claude安全检查全面公测、Grok 4.3搅局价格战——多模型混战的五月,开发者如何避坑?
人工智能·gpt·ai·chatgpt·bug·ai编程
刘~浪地球2 小时前
DeepSeek V3 vs GPT-4 深度对比测评:国产大模型能否一战?
人工智能
IT_陈寒2 小时前
JavaScript的异步地狱,我差点没爬出来
前端·人工智能·后端
AI木马人2 小时前
20.人工智能实战:大模型项目如何从 Demo 走向生产?一套可落地的上线验收清单与工程治理方案
java·开发语言·人工智能
湘-枫叶情缘2 小时前
穿透范畴的迷雾:从“四范式”到AI问题建模的现代认知框架
人工智能