具身智能多模态数据集工具库:统一的读写、校验和格式转换(含完整工作空间)
Project description
modelbest_robo_dataset
具身智能多模态数据集工具库。提供统一的读写、校验、格式转换与一整套数据工程脚本。
安装
pip install modelbest-robo-dataset
PyPI 主页:https://pypi.org/project/modelbest-robo-dataset/。
自 v0.3.0 起,
pip install即可获得完整工作空间——包内同时包含 SDK、命令行脚本、URDF/标注资源和所有 SOP 文档,不再需要git clone就能上手。
可选 extra:
pip install modelbest-robo-dataset[torch] # 训练时需要 EmbodiedDataset (PyTorch)
pip install modelbest-robo-dataset[robomind] # 转换 RoboMIND 时需要 h5py
pip install modelbest-robo-dataset[all] # 全装
5 行入门
from modelbest_robo_dataset import EmbodiedReader, EmbodiedWriter, Episode
reader = EmbodiedReader("/path/to/mb_dataset")
for episode in reader:
print(episode.duration, len(episode.messages))
更多用法见下文 快速开始。
包内提供的资源
pip install 后,除 SDK 外还附带:
| 类别 | 路径(包内) | 用途 |
|---|---|---|
| CLI 脚本 | modelbest_robo_dataset.scripts.* |
数据转换、校验、可视化 |
| SOP 文档 | modelbest_robo_dataset/docs/*.md |
给 Cursor / AI 协作的标准 Runbook |
| 数据集配置 | modelbest_robo_dataset/configs/datasets/*.json |
load_dataset_config() 默认读取的内置配置 |
| 配置/标注 | modelbest_robo_dataset/annotations/ |
URDF、mesh、meta_annotations.json 模板 |
直接调用 CLI 脚本:
python -m modelbest_robo_dataset.scripts.convert --help
python -m modelbest_robo_dataset.scripts.verify_e2e --help
python -m modelbest_robo_dataset.scripts.expand_skeleton --help
访问包内文档/资源:
from importlib.resources import files
sop_dir = files("modelbest_robo_dataset") / "docs"
print((sop_dir / "sop_droid_adapter.md").read_text())
franka_urdf = files("modelbest_robo_dataset") / "annotations" / "custom_urdfs" / "franka" / "franka.urdf"
SOP 索引(适配新数据集 / 发版 / 校验)
| 场景 | SOP |
|---|---|
| 适配 LeRobot 类数据集 | sop_lerobot_to_mb_dataset.md |
| 适配 RLDS/TFRecord 数据集(如 DROID) | sop_droid_adapter.md |
| 通用数据集适配方法论 | sop_dataset_adapter.md |
| 数据生产 / 校验 / 可视化 | sop_data_production.md · sop_data_verification.md · sop_visualization.md |
| 发布到 PyPI | sop_pypi_release.md |
每个 SOP 都是给 Cursor / AI 下指令的标准 Runbook——直接把 请按 sop_droid_adapter.md 适配 droid 数据集 丢给 AI 即可执行。
想贡献代码?
git clone git@modelbest.codeup.aliyun.com:modelbest/embody/datapipe/mb-robo-dataset.git
cd mb-robo-dataset
pip install -e ".[dev,all]"
pytest tests/
设计理念
与 LeRobot 的区别
LeRobot 是优秀的机器人学习框架,本库在数据层面与其互补而非替代:
| LeRobot | modelbest_robo_dataset | |
|---|---|---|
| 定位 | 端到端训练框架(数据+策略+部署) | 纯数据工具库(格式转换+存储+读取) |
| 骨架存储 | Parquet 表 (每行一帧) | SSTable partition (每条一个 episode) |
| 数据模型 | 扁平表:每帧一行,所有 feature 列铺平 | 嵌套结构:Episode → Message → Content,env/user/ai 三轨道 |
| 元信息 | info.json + tasks.jsonl | MetaContent (嵌在骨架内,Pydantic 序列化) |
| 多模态 | 视频 + 状态 | 视频 + 状态 + 音频 + 力 + IMU + 语言指令 |
| 标注 | task description | task_id, user_id, scene_id, quality_rating, dim_names |
| 视频 | 原始 MP4,按 chunk 分文件 | per-episode MP4(默认)或逐帧 PNG(可选),H264 CRF23 + 720p 上限 |
| 扩展性 | 围绕 HuggingFace Hub 生态 | 围绕 modelbest_sdk SSTable 生态 |
为什么不直接用 LeRobot 格式
- 三轨道模型:env/user/ai 的消息结构天然支持人机交互场景(人类语音纠正、机器人语音回复),LeRobot 的扁平表不适合这种嵌套关系
- 多源异构:RH20T 有力传感器+音频,fuse 有 IMU+触觉麦克风,RoboMind 有 3 种机器人变体——需要一个足够灵活的骨架来容纳这些差异
- 生产级存储:SSTable partition 支持大规模分布式训练的随机读取,比单个 Parquet 文件更适合 10 万+ episode 的场景
- TimeseriesName 自描述:
ai.action.delta_cartesian_position本身就说明了控制空间和绝对/增量语义,不需要额外的 type 字段
与 LeRobot 的兼容
LeRobotSource读取 LeRobot v3.0 目录布局(meta/episodes/*.parquet+data/chunk-*/file-*.parquet)。若meta/episodes目录缺失(如 RoboMIND LeRobot),自动从data/parquet 的episode_index列推导可用 episode 列表- TimeseriesName 的命名风格(
env.obs.*/ai.action.*)兼容 LeRobot 社区惯例 - dim_names 的设计参考了 LeRobot info.json 中的
features.*.names.motors - 对损坏的单路视频会记录 warning 并跳过该相机,不会因为单个坏视频中断整条 episode 的转换
- 大体积 v3 布局(如 DROID 95K episodes):设置
lazy=True启用懒加载模式,初始化时不读取meta/episodes和data的 parquet 文件,按需加载单个 episode 的元数据和数据。DROID 的 config 已默认启用"lazy": true
Task 解析
LeRobotSource._resolve_task 按以下优先级解析自然语言任务字符串:
- Episode meta
tasks列表 — 首个非空字符串,或按整数task_index查tasks_df - Episode meta
task_index→tasks_df - Data parquet
task_index→tasks_df(v3.0 Libero 只在 data parquet 中存task_index,不在 episode meta 中) - Data parquet 的
task_fallback_column列(仅当 config 中显式设置了task_fallback_column时,如 DROID 的language_instruction) tasks_df首行(全局回退,仅适用于单任务数据集)
懒加载(Lazy Loading)
DROID 等超大数据集(95K+ episodes)在默认模式下会一次性加载所有 meta/episodes/*.parquet 和 data/*.parquet 到内存,导致 OOM。设置 lazy=True(config 或构造函数参数)启用懒加载:
| 默认模式 | 懒加载模式 | |
|---|---|---|
| 初始化 | 读取所有 meta/episodes + data parquet | 仅读取 meta/info.json + tasks.parquet |
list_episodes() |
从 episodes_df 提取 | range(total_episodes) |
load_episode(id) |
从内存 DataFrame 查找 | 按需读取对应 chunk 文件,LRU 缓存最近 8 个文件 |
| 内存占用 | 全量(DROID ~数 GB) | 仅当前 episode 所在文件(~数十 MB) |
| 初始化耗时 | DROID OOM | DROID 0.04s |
首次 load_episode 时会扫描所有 data/chunk-*/ 下 parquet 文件的 episode_index 列(仅读该列,非常轻量),构建 episode→file 映射索引。后续调用直接查索引并读取对应文件。
v3.0 共享数据文件(file-{file_index}.parquet,如 DROID/Libero)在非懒加载模式下也自动使用相同的按需加载策略。
视频处理
LeRobotSource.load_episode 返回 RawEpisode 时使用 video_files(视频文件路径字典)而非 video_frames(解码后的帧列表)。视频不在 source 层解码,由 EmbodiedWriter 在转换时按需复制或重编码为 per-episode MP4。
视频路径解析:按 episode parquet 中的 per-camera chunk_index / file_index + info.json 的 video_path 模板拼路径。
元数据流水线(新模型)
modelbest_robo_dataset 的元数据由三个制品共同表达,职责互不重叠。本节给出总览,详细 schema 与字段归属规则见 docs/lerobot_meta_mapping.md。
flowchart LR
LR["LeRobot info.json + parquet"]
Src["configs/sources/dataset.json<br/>HOW: state_key, camera_keys, lazy"]
Ann["annotations/dataset.meta.json<br/>WHAT: timeseries spec, urdf, robot_type"]
Boot["bootstrap_annotation.py (future)"]
Conv["convert.py (future)"]
Meta["MetaContent (derived from annotation)"]
Verify["vla_verify (annotation as truth)"]
StaticViz["generate_viz.py (static HTML)"]
LR -->|"auto-infer"| Boot
Src --> Boot
Boot --> Ann
Ann -->|"human review"| Ann
Ann --> Conv
Src --> Conv
LR --> Conv
Conv --> Meta
Meta --> Verify
Meta --> StaticViz
Ann --> Verify
| 制品 | 文件 | 维护者 | 主消费者 | 角色 |
|---|---|---|---|---|
| annotation | annotations/<dataset>.meta.json |
人工 | converter / vla_verify | WHAT —— 数据集应该是什么;converter 输入契约 |
| source 配置 | configs/sources/<dataset>.json |
dev | converter | HOW —— 怎么读 LeRobot 源 + 运行时开关 |
| runtime metadata | skeleton 内 MetaContent / TimeseriesRef / VideoRef |
converter 自动写 | reader / 静态可视化 / vla_verify | 派生输出 |
单一真值原则:
- 数据集 metadata(
robot_type/urdf_path/dim_names/timeseries_name/ ...)→ annotation 为真值 - 列名映射(
state_key/camera_keys/task_key/lazy)→configs/sources/为真值 - 派生量(
fps/total_frames/shape/dtype)→ source 数据本身为真值,不写入 annotation
迁移现状:当前仓库使用包内 modelbest_robo_dataset/configs/datasets/*.json(同时混了 WHAT 与 HOW),尚未迁到上面的两文件模型。迁移路线见 docs/lerobot_meta_mapping.md §9。
架构
modelbest_robo_dataset/
├── data_types.py # Episode/Message/Content 类型定义
├── dataset_config.py # 数据集配置注册表 (DatasetConfig + load/list)
├── lerobot_state_semantics.py # LeRobot observation.state 语义推断(关节 vs 末端)
├── writer.py # RawEpisode → 统一格式
├── reader.py # 统一格式 → 训练采样
├── validator.py # State/Action 一致性校验
├── sources/
│ ├── base.py # RawEpisode + EpisodeSource 接口
│ ├── lerobot.py # LeRobot v3.0 读取
│ ├── rh20t.py # RH20T (上海交大)
│ ├── fuse.py # fuse/DIGIT (TFRecord/RLDS)
│ └── robomind.py # RoboMIND (HDF5)
├── configs/datasets/ # 每个数据集的转换配置(pip 包内默认配置)
│ ├── libero.json
│ ├── droid.json
│ ├── agibotworld.json
│ ├── robomind_franka_*.json
│ ├── robotwin2_*.json
│ └── ...
├── annotations/ # URDF / mesh / meta annotation 模板
├── docs/ # SOP / 设计文档(随 wheel 分发)
└── scripts/ # 多源转换、校验、可视化工具
├── convert.py
├── expand_skeleton.py
├── convertion/
├── preprocess/
├── sim_comparison/
└── vla_verify/
tests/
├── test_lerobot_source.py # LeRobotSource 单元测试
└── test_sstable_record.py # SSTable 序列化测试
数据集配置注册表
过渡期说明:本节描述当前包内
modelbest_robo_dataset/configs/datasets/*.json用法。该文件同时混了「数据集 WHAT」与「源解析 HOW」,会在 docs/lerobot_meta_mapping.md §9 描述的迁移路线 Step 2–5 完成后,被annotations/<dataset>.meta.json(WHAT)+configs/sources/<dataset>.json(HOW)取代。本节内容当前与现状保持一致,便于过渡期使用。
modelbest_robo_dataset/configs/datasets/<name>.json 为每个 LeRobot 数据集定义转换参数,消除命令行逐一传参的需要。load_dataset_config("<name>") 默认读取这些包内配置;如果你需要实验性覆盖,可传入 configs_dir= 指向自己的目录。
配置文件格式
{
"state_key": "observation.state",
"action_key": "action",
"state_type": "joint_position",
"action_type": "joint_position",
"camera_keys": ["observation.images.image"],
"task_key": "index",
"task_fallback_column": null,
"lazy": false,
"description": "..."
}
对于双臂等多列拼接的数据集,state_key / action_key 支持 列表 形式,读取时按顺序沿最后一维拼接:
{
"state_key": ["observation.states.joint_position_left", "observation.states.joint_position_right"],
"action_key": ["actions.joint_position_left", "actions.joint_position_right"],
"camera_keys": ["observation.images.camera_front", "observation.images.camera_left_wrist", "observation.images.camera_right_wrist"]
}
上例将 left[7] + right[7] 拼接为 14 维 state/action。单字符串与列表均可,接口与 camera_keys 一致。
| 字段 | 说明 |
|---|---|
state_key |
必填。data parquet 中 state 列名。单列用字符串,多列拼接用列表(如双臂 left+right) |
action_key |
必填。data parquet 中 action 列名。格式同 state_key |
state_type |
必填。映射到 TimeseriesName(joint_position / cartesian_position / ...) |
action_type |
必填。映射到 TimeseriesName(joint_position / delta_cartesian_position / ...) |
camera_keys |
必填。使用的相机 feature key 列表(如 ["observation.images.image", "observation.images.wrist_image"]) |
task_key |
tasks.parquet 中任务文本的来源。"index" = DataFrame Index(多数 v3.0 数据集包括 DROID/Libero/AgiBotWorld/RoboTwin2),其他字符串 = 重命名该列,null = 假定已有 task 列 |
task_fallback_column |
当 tasks.parquet 中找不到任务时,从 data parquet 的该列回退(如 DROID 的 "language_instruction")。null = 不回退 |
lazy |
懒加载模式。true 时初始化不加载 meta/episodes 和 data parquet,按需读取单个 episode。适用于 DROID 等超大数据集(95K+ episodes),避免 OOM。默认 false |
| 其余字段 | 与 convert.py CLI 参数对应,作为默认值(CLI 显式传入时覆盖配置) |
已有配置
| 配置名 | state_key | action_key | cameras | lazy | task_key | 说明 |
|---|---|---|---|---|---|---|
libero |
observation.state | action | 2 | - | index | LIBERO(long/spatial/object/goal 共用) |
droid |
observation.state | action | 3 | true | index | DROID 1.0.1(95K eps,懒加载) |
agibotworld |
observation.states.joint.position | actions.joint.position | 8 | - | index | AgiBotWorld(全 8 相机) |
agibotworld_3rgb |
observation.states.joint.position | actions.joint.position | 3 | - | index | AgiBotWorld(head + hand_left/right,快速转换) |
robomind_franka_1rgb |
observation.states.joint_position | actions.joint_position | 1 | - | index | RoboMIND Franka 单臂 8d,1 相机 |
robomind_franka_2rgb |
observation.states.joint_position | actions.joint_position | 2 | - | index | RoboMIND Franka 单臂 8d,2 相机 |
robomind_franka_3rgb |
observation.states.joint_position | actions.joint_position | 3 | - | index | RoboMIND Franka 单臂 8d,3 相机 |
robomind_ur_1rgb |
observation.states.joint_position | actions.joint_position | 1 | - | index | RoboMIND UR 单臂 7d,1 相机 |
robomind_agilex_3rgb |
[joint_position_left, _right] | [joint_position_left, _right] | 3 | - | index | RoboMIND agilex 双臂 7+7=14d,3 相机(列表拼接) |
robomind_franka_fr3_dual |
observation.states.joint_position | actions.joint_position | 3 | - | index | RoboMIND FR3 双臂 16d,3 相机 |
robotwin2_franka |
observation.state | action | 4 | - | index | RoboTwin2 Franka |
robotwin2_aloha_agilex |
observation.state | action | 4 | - | index | RoboTwin2 ALOHA |
robotwin2_ur5 |
observation.state | action | 4 | - | index | RoboTwin2 UR5 |
pusht |
observation.state | action | 1 | - | - | PushT |
xarm_push_medium |
observation.state | action | 1 | - | - | xArm |
aloha_sim_insertion |
observation.state | action | 1 | - | - | ALOHA sim |
使用方式
Python API
from modelbest_robo_dataset.dataset_config import load_dataset_config
from modelbest_robo_dataset.sources.lerobot import LeRobotSource
# 所有参数由 config 驱动,无自动检测
cfg = load_dataset_config("libero")
src = LeRobotSource(
"/path/to/libero_long", # or libero_spatial, libero_object, libero_goal
state_type=cfg.state_type,
action_type=cfg.action_type,
state_key=cfg.state_key,
action_key=cfg.action_key,
camera_keys=cfg.camera_keys,
task_key=cfg.task_key,
)
# 大数据集使用懒加载(如 DROID,config 中 lazy=true)
cfg = load_dataset_config("droid") # lazy=True already set
src = LeRobotSource(
"/path/to/droid_1.0.1",
state_type=cfg.state_type,
action_type=cfg.action_type,
state_key=cfg.state_key,
action_key=cfg.action_key,
camera_keys=cfg.camera_keys,
task_key=cfg.task_key,
task_fallback_column=cfg.task_fallback_column,
lazy=cfg.lazy, # True — init 0.04s instead of OOM
)
eps = src.list_episodes() # 95617 episodes
ep = src.load_episode(eps[0]) # on-demand: reads only the needed parquet file
命令行
# 使用配置文件(无需手动传 --state-type / --action-type)
python -m modelbest_robo_dataset.scripts.convert --source lerobot \
--input /path/to/agibotworld/task_327 --output /path/to/output \
--config agibotworld
# CLI 参数优先于配置文件
python -m modelbest_robo_dataset.scripts.convert --source lerobot \
--input /path/to/dataset --output /path/to/output \
--config libero --max-episodes 100
批量转换
batch_convert_lerobot.py 支持 --configs-dir,自动按数据集名查找配置:
python -m modelbest_robo_dataset.scripts.convertion.batch_convert_lerobot \
--source-root /path/to/lerobot_datasets \
--output-root /path/to/output \
--configs-dir /path/to/custom/configs \
...
添加新数据集
- 查看数据集的
meta/info.json,确认features中 state/action 列名和dtype: "video"的相机列名 - 查看
meta/tasks.parquet结构,确认任务文本在 DataFrame Index 还是某个列中 - 创建自定义
<name>.json,或把配置加入包内modelbest_robo_dataset/configs/datasets/ - 运行
python -m modelbest_robo_dataset.scripts.convert --config <name>验证
LeRobot observation.state 语义推断
LeRobot v2/v3 里 observation.state 没有统一 schema:同一向量可能是关节角,也可能是末端位姿(位置 + 旋转 + 夹爪)。转换到本库时,writer.STATE_TYPE_MAP 需要区分 joint_position(映射到 env.obs.joint_position)与 ee_pose(映射到 env.obs.cartesian_position)。本库提供 元数据优先、Parquet 抽样数值为辅 的推断,不能单靠维度数判断。
实现与返回结果
- 模块:
lerobot_state_semantics.py - API:
infer_observation_state_semantics(root, max_sample_rows=5000) -> StateSemanticsResult - 字段:
label(joint_position/ee_pose/unknown)、confidence(0–1)、reasons(命中规则说明)、state_key、shape、sample_dim_names
规则优先级(概要)
- 特征键名(不区分大小写):
observation.state*中含eef、tcp、cartesian、pose、world_pose、ee_pose、ee_等子串 → 判为ee_pose(高置信度)。 - 维度名(与
LeRobotSource._extract_dim_names一致:支持names.motors、names.axes,或顶层names为列表;键存在但值为 JSONnull时视为缺失,避免异常):- 倾向末端:维度名集合中同时含
x、y、z,或名称文本中含quat、axis_angle、euler、rpy、rotation、orient等; - 倾向关节:名称中含
joint、shoulder、elbow、wrist、finger等; - 冲突:若末端与关节信号并存,且存在
x/y/z三元组 → 偏向ee_pose;否则继续走数值启发。 - 弱信号:仅
motor_0、motor_1… 等形式 不单独下结论,需结合 Parquet 抽样。
- 倾向末端:维度名集合中同时含
- 数值启发(读取
data/**/*.parquet中 state 列,总行数上限由max_sample_rows控制,默认 5000):- 维度 ≥ 7:对「最后 4 维」与「第 4–7 列」两种四元数候选块分别算均值
|‖q‖ - 1|,取更优者;若小于 0.05 →ee_pose; - 维度 = 6:若前三维幅度接近米级、后三维接近弧度量级 → 弱信号
ee_pose(置信度较低); - 仅
motor_*或 无名,且抽样不满足单位四元数块 →joint_position; - 缺少
meta/info.json、无observation.state*特征、无可用 Parquet 等 →unknown,并在reasons中写明原因。
- 维度 ≥ 7:对「最后 4 维」与「第 4–7 列」两种四元数候选块分别算均值
局限
无法保证 100% 自动正确(例如元数据全写成 motor_* 但实际存的是末端位姿)。请以 confidence 与 reasons 为准做抽检;必要时在流水线侧人工指定或修正。
说明:单数据集 / 批量脚本通过 importlib 直接加载 lerobot_state_semantics.py,不经过包根 __init__.py,可在未安装 PyAV 的环境下运行。若在已安装全量依赖的环境中使用 Python API,可正常 from modelbest_robo_dataset.lerobot_state_semantics import infer_observation_state_semantics。
命令行
# 单个 LeRobot 数据集根目录(需含 meta/info.json)
python -m modelbest_robo_dataset.scripts.convertion.infer_lerobot_state /path/to/lerobot_dataset
python -m modelbest_robo_dataset.scripts.convertion.infer_lerobot_state /path/to/lerobot_dataset --json --max-rows 2000
# 父目录下:每个直接子目录若含 meta/info.json 则推断一次
python -m modelbest_robo_dataset.scripts.convertion.batch_infer_lerobot_state /path/to/parent \
--output-format csv -o lerobot_state_summary.csv
# 递归查找所有 meta/info.json(数据集根 = meta 的父目录)
python -m modelbest_robo_dataset.scripts.convertion.batch_infer_lerobot_state /path/to/parent --recursive --output-format jsonl
batch_infer_lerobot_state.py 还支持 --output-format table|json、-o 输出到文件。
Python 调用示例
from pathlib import Path
from modelbest_robo_dataset.lerobot_state_semantics import infer_observation_state_semantics
r = infer_observation_state_semantics(Path("/path/to/lerobot_dataset"))
print(r.label, r.confidence, r.reasons)
数据格式
输出结构:
output_dir/
├── skeleton_episode/{name}/part-XXXXX # SSTable partition 骨架
├── data/{name}/state/chunk-000/file-000.parquet
├── data/{name}/action/chunk-000/file-000.parquet
├── videos/{name}/{cam}/episode_000000.mp4 # 默认:整段 MP4,H264 CRF23,上限 720p
│ 或 episode_000000.png # 逐帧模式:每 timestep 一张 PNG
├── meta/{name}/info.json # 轻量 summary(非 LeRobot 原始 info.json 全量镜像)
├── meta/{name}/stats.json # action/state 全量统计:mean/std/min/max/q01/q99/count
└── meta/{name}/norm_stats.json # 训练 norm_stats_path 直接消费的 {"norm_stats": {...}} 包装
说明:
meta/{name}/info.json当前只保存轻量 summary(如dataset_name、total_episodes、total_frames、accumulator 行数)。- 更细的结构化信息(例如
robot_type、task、dim_names、urdf_path、fps、video key、时序 ref 的from_ts/to_ts)保存在 skeleton 的MetaContent/TimeseriesRef/VideoRef中。 - 时序 Parquet 的每行也会保存自身的
fps列,便于下游核对。 meta/{name}/stats.json与meta/{name}/norm_stats.json同时包含q01/q99和min/max:openpi(默认)preprocessor 走q01/q99starvlapreprocessor 走min/max(缺失才会回落q01/q99并 warning)- 直接把
meta/{name}/norm_stats.json配给训练 config 的norm_stats_path即可同时支持两类
Episode 骨架
每个 Episode 包含三类消息:
| 角色 | 内容 | 说明 |
|---|---|---|
env |
MetaContent, VideoContent, TimeseriesContent(state), AudioContent | 环境感知 |
user |
TextContent, AudioContent | 人类干预 |
assistant |
TimeseriesContent(action), TextContent | 机器人输出 |
TimeseriesName 命名规范
采用 env.obs.* / ai.action.* 的 dot-separated 命名:
| key | 说明 |
|---|---|
env.obs.joint_position |
关节角度 (绝对) |
env.obs.cartesian_position |
末端位姿 (绝对) |
env.obs.gripper_position |
夹爪开度 |
env.obs.force_torque |
力/力矩 |
env.obs.imu |
IMU |
ai.action.joint_position |
绝对目标关节角 |
ai.action.cartesian_position |
绝对目标末端位姿 |
ai.action.delta_joint_position |
关节角增量 |
ai.action.delta_cartesian_position |
末端位姿增量 |
TimeseriesName 本身区分绝对/增量,不需要额外的 action_type 字段。
MB 原生预处理脚本
modelbest_robo_dataset.scripts.preprocess 下的脚本支持直接处理 modelbest_robo_dataset 原生格式(目录下包含 data/、meta/、skeleton_episode/、videos/)。
输入与输出约定
--input_dir可以传原生数据根目录,也可以直接传某个数据集目录,例如data/<dataset>、meta/<dataset>、skeleton_episode/<dataset>。- 对 原生 MB 数据 执行预处理时,脚本现在会先复制该数据集的完整内容,再在副本上修改;不会复用源数据文件,也不会把新数据集内部引用继续指向原数据集。
- 复制时会同步带走并改写同名数据集下的
data/、meta/、videos/、skeleton_episode/,以及已有的skeleton_single_frame//skeleton_ctx*。 - 输出参数
--output_dir对原生 MB 数据来说表示“新的 MB 根目录”,不是单个数据集目录;真正的新数据集会出现在output_dir/<collection>/<new_dataset_name>/...下。
新数据集命名规则
原生 MB 预处理脚本会自动给新数据集名追加操作后缀:
abs2rel.py-><dataset>_abs2reldownsampling.py-><dataset>_downsample_<source>hz_to_<target>hznormalization.py-><dataset>_q99_normclear_statistic.py-><dataset>_clean_xyznative_single_frame_clean.py-><dataset>_clean_single_frame
例如对 libero_goal_no_noops_1.0.0_lerobot 运行 native_single_frame_clean.py,输出数据集名会变成 libero_goal_no_noops_1.0.0_lerobot_clean_single_frame。
派生 skeleton 的处理
- 如果预处理会改变帧数、时长或 fps(例如
downsampling.py、clear_statistic.py、native_single_frame_clean.py),脚本会自动删除复制副本中旧的skeleton_single_frame/和skeleton_ctx*,避免它们继续引用过期的时间范围。 native_single_frame_clean.py会在清洗完成后重新生成新的skeleton_single_frame。- 对不改变帧数的脚本(如
abs2rel.py、normalization.py),已有 skeleton 会保留,并同步改写数据集名与内部引用路径。
dry-run
clear_statistic.py --dry_run和native_single_frame_clean.py --dry_run只做统计,不写出新数据。- dry-run 会复用源数据集名做分析;只有实际写出时才会生成带操作后缀的新数据集副本。
示例
# 先做 dry-run,看清洗后会删掉多少帧
python -m modelbest_robo_dataset.scripts.preprocess.native_single_frame_clean \
--input_dir /path/to/mb_root \
--dataset_name libero_goal_no_noops_1.0.0_lerobot \
--output_dir /path/to/mb_preprocessed \
--threshold 1e-4 \
--dry_run
# 实际转换:会复制整套数据,并生成一个新的数据集
python -m modelbest_robo_dataset.scripts.preprocess.native_single_frame_clean \
--input_dir /path/to/mb_root \
--dataset_name libero_goal_no_noops_1.0.0_lerobot \
--output_dir /path/to/mb_preprocessed \
--threshold 1e-4 \
--keep_endpoints
实际运行后,输出中会保留原始数据不变,并在:
/path/to/mb_preprocessed/data/libero_goal_no_noops_1.0.0_lerobot_clean_single_frame/...
/path/to/mb_preprocessed/meta/libero_goal_no_noops_1.0.0_lerobot_clean_single_frame/...
/path/to/mb_preprocessed/skeleton_episode/libero_goal_no_noops_1.0.0_lerobot_clean_single_frame/...
/path/to/mb_preprocessed/skeleton_single_frame/libero_goal_no_noops_1.0.0_lerobot_clean_single_frame/...
/path/to/mb_preprocessed/videos/libero_goal_no_noops_1.0.0_lerobot_clean_single_frame/...
MetaContent 结构化字段
| 字段 | 类型 | 说明 |
|---|---|---|
urdf_path |
str | 机器人 URDF 路径,可由 python -m modelbest_robo_dataset.scripts.convert --urdf-path 写入 |
joint_names |
list[str] | 关节名称列表(如源数据可提供) |
task_id |
str | 任务 ID (如 "task_0001",从目录名解析) |
quality_rating |
int | 质量评分: 0=机器人失败, 1=任务失败, 2-9=完成质量, -1=未标注 |
user_id |
str | 操作者 ID (如 "user_0001") |
scene_id |
str | 场景 ID (如 "scene_0001") |
dim_names |
dict | key=TimeseriesName, value=维度名列表 |
快速开始
转换数据集
from modelbest_robo_dataset import EmbodiedWriter
from modelbest_robo_dataset.dataset_config import load_dataset_config
from modelbest_robo_dataset.sources.lerobot import LeRobotSource
# 使用配置注册表(推荐,所有参数由 config 显式定义)
cfg = load_dataset_config("libero")
source = LeRobotSource(
root="/path/to/libero_long", # or any LIBERO variant
state_type=cfg.state_type,
action_type=cfg.action_type,
state_key=cfg.state_key,
action_key=cfg.action_key,
camera_keys=cfg.camera_keys,
task_key=cfg.task_key,
)
writer = EmbodiedWriter(output_dir="/path/to/output", dataset_name="pusht")
for ep_id in source.list_episodes():
raw = source.load_episode(ep_id)
writer.write_episode(raw)
writer.finalize()
逐帧 episode(每 timestep 一条骨架 + 单帧 PNG)
默认下,每个源 trajectory 对应一条 Episode,视频为整段 episode_XXXXXX.mp4。若需要 每个时间步单独一条 Episode,且相机保存为 单张 PNG(而非 MP4),使用 EmbodiedWriter(..., one_frame_per_episode=True)。
可选:在写入前调用 set_shuffled_episode_indices(total_frames, seed),为每条帧级 Episode 分配 打乱后的 episode_index(与 Parquet 行、PNG 文件名一致)。LeRobot 下可用 LeRobotSource.episode_frame_counts() 先求总帧数(需与 list_episodes 的截断方式一致,例如同样应用 max_episodes)。
from modelbest_robo_dataset import EmbodiedWriter
from modelbest_robo_dataset.dataset_config import load_dataset_config
from modelbest_robo_dataset.sources.lerobot import LeRobotSource
cfg = load_dataset_config("my_ds")
source = LeRobotSource(
root="/path/to/lerobot_ds",
name="my_ds",
state_type=cfg.state_type,
action_type=cfg.action_type,
state_key=cfg.state_key,
action_key=cfg.action_key,
camera_keys=cfg.camera_keys,
task_key=cfg.task_key,
)
episodes = source.list_episodes()
counts = source.episode_frame_counts()
total_frames = sum(counts)
writer = EmbodiedWriter(
output_dir="/path/to/output",
dataset_name="my_ds",
one_frame_per_episode=True,
)
writer.set_shuffled_episode_indices(total_frames, seed=42)
for ep_id in episodes:
writer.write_episode(source.load_episode(ep_id))
writer.finalize()
说明:
- 状态/动作/各相机帧数不一致时,按 最短长度 对齐并打日志警告。
- 仅
video_files、无video_frames时,本库 不会 自动逐帧解码 MP4;可能只有时序无图像。 - 多帧 trajectory 下 不写入 整段
audio_env(仅单帧 trajectory 保留原逻辑)。
读取数据
from modelbest_robo_dataset import EmbodiedReader
reader = EmbodiedReader("/path/to/output", "pusht")
reader.summary()
sample = reader.load_sample(episode_idx=0, timestamp=1.0)
print(sample.keys())
命令行转换
# LeRobot: 使用配置注册表(推荐)
python -m modelbest_robo_dataset.scripts.convert --source lerobot \
--input /path/to/libero_long --output /path/to/output --config libero
# LeRobot: 非标准 feature key(如 AgiBotWorld,config 中已定义)
python -m modelbest_robo_dataset.scripts.convert --source lerobot \
--input /path/to/agibotworld/task_327 --output /path/to/output --config agibotworld
# RH20T 全部
python -m modelbest_robo_dataset.scripts.convert --source rh20t --all --output /path/to/output
# fuse
python -m modelbest_robo_dataset.scripts.convert --source fuse --output /path/to/output
# RoboMind (支持 puppet/franka/tiangong 三种变体,自动检测)
python -m modelbest_robo_dataset.scripts.convert --source robomind \
--input /path/to/failure_data --output /path/to/output --name robomind_failure
# RoboMind 全量转换 (约4小时)
python -m modelbest_robo_dataset.scripts.convert --source robomind \
--input /backup/.../robomind/failure_data --output /path/to/output \
--name robomind_failure --max-episodes 1678
LeRobot:逐帧 PNG + 打乱 episode_index
适用于希望 每条样本对应一帧图像(videos/.../episode_XXXXXX.png),并对全数据集的 episode_index 随机打乱(可复现)的场景。
python -m modelbest_robo_dataset.scripts.convert --source lerobot \
--input /path/to/lerobot_dataset --output /path/to/output --name my_ds \
--one-frame-per-episode --shuffle-seed 42
(v0.3.1 起推荐始终使用 python -m modelbest_robo_dataset.scripts.convert,无论是 pip 安装还是 git clone 开发环境。)
| 参数 | 说明 |
|---|---|
--one-frame-per-episode |
每个 timestep 写一条 Episode;相机输出为 PNG,不再为整段 MP4。 |
--shuffle-seed N |
必须与上一参数同时使用。在写入前根据数据表统计总帧数,对 0..N-1 的 episode_index 做固定种子的随机排列。 |
注意:--shuffle-seed 依赖数据源的 episode_frame_counts();当前 LeRobot 已实现。其他 --source 若未实现该方法,请勿对该源使用 --shuffle-seed。
骨架记录在 SSTable 中的 追加顺序 仍为按源 Episode 依次写入;打乱的是每条记录内的 episode_index 及对应的 Parquet/图像路径,而非磁盘上的记录物理顺序。
已有 episode 骨架转 single_frame
适用于你已经有 modelbest_robo_dataset 标准输出目录,且目录下已有 skeleton_episode/{dataset},现在只想继续生成 skeleton_single_frame/{dataset} 的场景。
# 转换指定数据集
python -m modelbest_robo_dataset.scripts.expand_skeleton \
--input /path/to/output \
--dataset my_dataset \
--mode single_frame \
--action-chunk 50
# 一次转换 skeleton_episode/ 下全部数据集
python -m modelbest_robo_dataset.scripts.expand_skeleton \
--input /path/to/output \
--all \
--shuffle-seed 42
| 参数 | 说明 |
|---|---|
--input |
modelbest_robo_dataset 数据根目录,内部应包含 skeleton_episode/。 |
--datasets |
指定一个或多个数据集名称。 |
--all |
转换 skeleton_episode/ 下所有数据集。 |
--action-chunk |
每条 single_frame 样本中 action 窗口长度,默认 50。 |
--shuffle-seed |
写入前打乱全部帧级记录,传固定值可复现。 |
--include-terminal-frame |
保留末尾不足一个完整 action chunk 的终止帧。 |
说明:
- 输出目录为
skeleton_single_frame/{dataset}。 - 脚本会读取
skeleton_episode中的 SSTable 记录,并保持原有data/、videos/、meta/目录不变。 - 若目标目录已存在旧的
part-*文件,脚本会先清理再重写,避免重复运行后混入旧分片。
校验与诊断
LeRobot / MB 一致性校验
modelbest_robo_dataset.scripts.verify_lerobot_to_mb_dataset 用于校验:
- LeRobot 数据集与
modelbest_robo_dataset的single_frame输出是否一致 - 两个 LeRobot 数据集之间的
state/action/task/video是否一致 single_frame记录顺序被 shuffle 的情况
示例:
# 校验 LeRobot 与 MB single_frame
python -m modelbest_robo_dataset.scripts.verify_lerobot_to_mb_dataset \
--lerobot-root /path/to/lerobot_ds \
--mb-root /path/to/mb_root \
--dataset-name my_dataset
# 严格模式:额外逐帧比较视频
python -m modelbest_robo_dataset.scripts.verify_lerobot_to_mb_dataset \
--lerobot-root /path/to/lerobot_ds \
--mb-root /path/to/mb_root \
--dataset-name my_dataset \
--check-video
# 对比两个 LeRobot 数据集
python -m modelbest_robo_dataset.scripts.verify_lerobot_to_mb_dataset \
--lerobot-root /path/to/lerobot_a \
--compare-lerobot-root /path/to/lerobot_b \
--dataset-name my_dataset \
--check-video
打印骨架中的机器人 meta / video key / fps
modelbest_robo_dataset.scripts.convertion.print_robot_meta_info 用于直接读取 skeleton_episode 或 skeleton_single_frame,打印:
MetaContent中的机器人相关字段dim_names- video key
- skeleton 中推导出的 timeseries / fps 信息
示例:
python -m modelbest_robo_dataset.scripts.convertion.print_robot_meta_info /path/to/mb_root --dataset-name my_dataset
python -m modelbest_robo_dataset.scripts.convertion.print_robot_meta_info /path/to/mb_root/skeleton_single_frame --dataset-name my_dataset
python -m modelbest_robo_dataset.scripts.convertion.print_robot_meta_info /path/to/mb_root --dataset-name my_dataset --json
构建 LeRobot 采样子集(大数据集测试用)
modelbest_robo_dataset.scripts.convertion.build_lerobot_sample_subset 从大型 LeRobot v3.0 数据集(如 DROID 95K episodes)中快速创建最小可测试子集。通过软链接(symlink)避免复制大文件:
source ~/minicpmo/bin/activate
python -m modelbest_robo_dataset.scripts.convertion.build_lerobot_sample_subset \
--source /path/to/droid_1.0.1 \
--output /tmp/droid_sample \
--max-data-files 1
输出目录结构:
meta/info.json:调整total_episodes为子集大小meta/episodes/chunk-000/file-000.parquet:按 data shard 中出现的 episode_index 过滤data/chunk-000/file-000.parquet:软链接到源文件meta/tasks.parquet:软链接到源文件videos/:软链接到源目录
端到端 skeleton 验证
modelbest_robo_dataset.scripts.verify_skeleton_identity 用于验证对 LeRobotSource 的修改不会破坏已有的 skeleton_episode / skeleton_single_frame 输出:
- Phase 1:用当前
LeRobotSource加载每个 episode,与已有skeleton_episodeSSTable 中的 task、state/action 维度、视频数等进行比较 - Phase 2:从已有
skeleton_episode重新运行expand_skeleton,MD5 比较生成的skeleton_single_frame每个part-*文件
source ~/minicpmo/bin/activate
cd /path/to/modelbest-robo-dataset
python -m modelbest_robo_dataset.scripts.verify_skeleton_identity \
--lerobot-root /path/to/raw/libero_long \
--unified-root /path/to/unified \
--dataset libero_long \
--state-type joint_position \
--action-type delta_ee \
--action-chunk 10 \
--shuffle-seed 42
检查 single_frame 是否被 shuffle
modelbest_robo_dataset.scripts.convertion.check_single_frame_shuffle 用于判断 single_frame 数据是否经过 shuffle,支持:
python -m modelbest_robo_dataset.scripts.convert --one-frame-per-episodepython -m modelbest_robo_dataset.scripts.expand_skeleton --mode single_frame
示例:
python -m modelbest_robo_dataset.scripts.convertion.check_single_frame_shuffle \
--input /path/to/mb_root --dataset-name my_dataset
多数据集共享 action 归一化并批量转换
modelbest_robo_dataset.scripts.convertion.convert_multi_lerobot_normalized 支持:
- 对多个 LeRobot 数据集计算共享 action 统计量
- 输出归一化后的 LeRobot 副本
- 转换为 modelbest
episode格式 - 再展开为
single_frame
示例:
python -m modelbest_robo_dataset.scripts.convertion.convert_multi_lerobot_normalized \
--root /path/to/lerobot_root \
--datasets task_a task_b \
--output /path/to/modelbest_out \
--state-type joint_position \
--action-type delta_ee
已接入数据集
| 数据集 | 源格式 | 机器人 | State (env.obs) | Action (ai.action) | 模态 | 结构化标注 |
|---|---|---|---|---|---|---|
| pusht | LeRobot | - | cartesian_position | cartesian_position | 视频+时序 | - |
| xarm_push_medium | LeRobot | xarm | joint_position | delta_joint_position | 视频+时序 | - |
| aloha_sim_insertion | LeRobot | aloha | joint_position | joint_position | 视频+时序 | - |
| fuse | TFRecord | DIGIT | cartesian_position, imu | delta_cartesian_position | 视频+音频+IMU | - |
| rh20t_cfg1~7 | RH20T | 多种 | cartesian_position, force_torque | cartesian_position | 视频+音频+力 | task_id, user_id, scene_id, quality_rating |
| droid_1.0.1 | LeRobot v3.0 | 多种 | joint_position | joint_position | 3视频+时序 | language_instruction (via tasks.parquet index) |
| libero | LeRobot v3.0 | Franka | joint_position | delta_cartesian_position | 2视频+时序 | task (long/spatial/object/goal) |
| agibotworld | LeRobot v3.0 | a2d | joint_position (14d) | joint_position (14d) | 8视频+时序 | task (需 state_key/action_key 覆盖) |
| agibotworld_3rgb | LeRobot v3.0 | a2d | joint_position (14d) | joint_position (14d) | 3视频+时序 | 同上,仅 head+hand_left/right(快速转换) |
| robomind_franka (LeRobot) | LeRobot v3.0 | Franka | joint_position (8d) | joint_position (8d) | 1–3 视频+时序 | task(1/2/3 相机变体配置) |
| robomind_ur (LeRobot) | LeRobot v3.0 | UR | joint_position (7d) | joint_position (7d) | 1 视频+时序 | task |
| robomind_agilex (LeRobot) | LeRobot v3.0 | agilex 双臂 | joint_position (7+7=14d) | joint_position (7+7=14d) | 3 视频+时序 | task(state/action 多列拼接) |
| robomind_franka_fr3_dual (LeRobot) | LeRobot v3.0 | FR3 双臂 | joint_position (16d) | joint_position (16d) | 3 视频+时序 | task |
| robotwin2_franka | LeRobot v3.0 | Franka 双臂 | joint_position (16d) | joint_position (16d) | 4视频+时序 | task (per-episode) |
| robotwin2_aloha_agilex | LeRobot v3.0 | ALOHA-Agilex 双臂 | joint_position (14d) | joint_position (14d) | 4视频+时序 | task (per-episode) |
| robotwin2_ur5 | LeRobot v3.0 | UR5 双臂 | joint_position (14d) | joint_position (14d) | 4视频+时序 | task (per-episode) |
| robomind_failure | HDF5 | tiangong/puppet/franka | joint_position | joint_position | 多视频+时序 | task_id, quality_rating=0 (失败数据) |
| robomind_puppet | HDF5 | puppet (双臂) | joint_position | joint_position | 多视频+时序 | task_id |
| robomind_franka | HDF5 | Franka | joint_position | joint_position | 多视频+时序 | task_id |
依赖
运行本仓库的 Python 脚本或 pytest 前,请先激活你的 Python 环境(例如 minicpmo):
source ~/minicpmo/bin/activate
numpy
pandas
pyarrow
pydantic>=2.0
av (PyAV) # EmbodiedWriter 视频重编码;LeRobotSource 不再依赖
Pillow
h5py # RoboMind
tensorflow # fuse (可选)
modelbest_sdk # SSTable 骨架存储
thriftpy2 # modelbest_sdk 依赖
测试
source ~/minicpmo/bin/activate
pytest tests/ -v
| 测试文件 | 覆盖范围 |
|---|---|
tests/test_lerobot_source.py |
LeRobotSource 初始化、list_episodes、load_episode、task 解析(标准/DROID index 风格/language_instruction 回退)、_normalize_tasks_df、build_lerobot_sample_subset |
tests/test_sstable_record.py |
SSTable 记录序列化/反序列化 |
State/Action 规范
详见 docs/sop_state_action.md。
多源数据集接入与适配说明详见 docs/sop_dataset_adapter.md。
核心原则:
- Action 必须使用原始数据,禁止用
np.diff等方式构造 - 没有原始 action 的遥操作数据,用
action[t] = state[t+1](shifted state) - TimeseriesName 自描述,
delta_前缀表示增量,无前缀表示绝对目标
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file modelbest_robo_dataset-0.3.1.tar.gz.
File metadata
- Download URL: modelbest_robo_dataset-0.3.1.tar.gz
- Upload date:
- Size: 4.2 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c0d48335f53c52a1907dd94a6aaa8d8d152e5e39b3292e68fb5b4ae1340ebdc
|
|
| MD5 |
0551c2dde8160803e564a0d10b2bfcf8
|
|
| BLAKE2b-256 |
9716207adcdfce3c55fc305e1ea0ee169cb390dcbf305a328a96c439a6c4062b
|
File details
Details for the file modelbest_robo_dataset-0.3.1-py3-none-any.whl.
File metadata
- Download URL: modelbest_robo_dataset-0.3.1-py3-none-any.whl
- Upload date:
- Size: 4.3 MB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
58f2595e07e5e83296a6b481c98a1b10653d003a9c790904fed96b4a2b94ee44
|
|
| MD5 |
dc1858f4be81f171b09348a438f88ce7
|
|
| BLAKE2b-256 |
a33a5f08bf4ef0978e0e913bfb21b329259275bc20015fcb3de8d0a55fa4c0ab
|