Data Collection#
There are two entry points, and both write the same directories. save_dir
defaults to ${runner.logger.log_path}.
Real-robot teleoperation starts
collect_real_data.pythroughexamples/embodiment/collect_data.sh.export_formatselects one store, orreplay_buffertogether with one episode format.Training or evaluation enables
data_collectionunderenv. The env worker writes one episode format at a time.
Both entry points take the same names: replay_buffer, pickle_episode,
and lerobot_dataset. pickle_episode and lerobot_dataset cannot be
selected together, and the env worker writes no replay_buffer. Omitting
export_format in the real-robot script writes replay_buffer and
lerobot_dataset. The earlier names pickle and lerobot still select
the two episode formats, with a deprecation warning.
Data format |
Save directory |
Reader |
|---|---|---|
|
|
RLPD. Set |
|
|
OpenPI SFT, HG-DAgger, and other imitation pipelines that read LeRobot shards. |
|
|
Reward-model preprocessing. |
To save a replay buffer and a LeRobot dataset from the real robot:
env:
eval:
data_collection:
enabled: True
save_dir: ${runner.logger.log_path}
export_format: [replay_buffer, lerobot_dataset]
Streaming LeRobot collection, such as dual-arm YAM, sets
export_format: lerobot_dataset and streaming: true. Frames are written
as they are recorded, so memory does not grow with episode length. Each kept
take becomes its own shard. Successful and failed takes are both stored and
stamped with is_success; only_success does not apply while streaming.
Operator discards and recording-queue overflows are not published into the
dataset. Merge the shards into one dataset before training, leaving out the
unsuccessful takes:
python toolkits/lerobot/merge_lerobot_datasets.py \
--source-dir logs/{timestamp}/lerobot_dataset \
--output-dir logs/{timestamp}/lerobot_merged \
--only-success
To collect pickle episodes during training for a reward model:
env:
eval:
data_collection:
enabled: True
save_dir: ${runner.logger.log_path}
export_format: pickle_episode
only_success: True
maniskill_ppo_mlp_collect uses this setup. .pkl files land in
{save_dir}/pickle_episode/. With export_format: lerobot_dataset, shards
land in {save_dir}/lerobot_dataset/rank_*/id_*/.
Real-robot teleoperation collection steps#
Set the robot, teleoperation device, and target pose in
examples/embodiment/config/realworld_collect_data.yaml:
cluster:
node_groups:
hardware:
configs:
robot_ip: "192.168.1.100"
env:
eval:
teleop: spacemouse # spacemouse, gello, pico, or none
override_cfg:
target_ee_pose: [0.5, 0.0, 0.3, 0.0, 3.14, 0.0]
success_hold_steps: 1 # consecutive steps at the target before success
runner:
num_data_episodes: 20 # stop after this many successes
Then run:
bash examples/embodiment/collect_data.sh
bash examples/embodiment/collect_data.sh realworld_collect_data_gello
The script exits after runner.num_data_episodes successes. When save_dir
is the log directory, the tree is:
logs/{timestamp}/
├── demo_buffer/
└── lerobot_dataset/rank_0/id_0/ # pickle_episode/*.pkl with that format
Each successful trajectory in demo_buffer is a .pt file whose
intervene_flags are all ones, marking expert data. To append data, point a
later run at the same demo_buffer directory.
Inspect the outputs:
python toolkits/replay_buffer/visualize.py \
--replay_dir logs/{timestamp}/demo_buffer
python toolkits/lerobot/visualize_lerobot_dataset.py \
--dataset-path logs/{timestamp}/lerobot_dataset/rank_0/id_0 \
--output-dir logs/{timestamp}/lerobot_visualized
Data format details#
CollectEpisode writes on a background thread and flushes the remainder in
env.close(), so collection does not block a training step. Each env worker
uses its own rank, which appears in filenames and shard directories.
replay_buffer
Only successful episodes are stored. On the real robot,
manual_episode_control_only counts manual_done alone; otherwise
reward >= 0.5 or manual_done counts as success. recording_invalid
and episode_discarded are neither stored nor counted. Each trajectory is a
trajectory_*.pt file, next to metadata.json and
trajectory_index.json. intervene_flags are all ones so RLPD can tell
expert data from online policy data. A later run pointed at the same directory
appends trajectories. Tensor shapes follow the robot and cameras; they are not
part of the format.
{
"transitions": {
"obs": {"states", "main_images"},
"next_obs": {"states", "main_images"},
"action": "float32[T, action_dim]",
"rewards": "float32[T, 1]",
"dones": "bool[T, 1]",
"terminations": "bool[T, 1]",
"truncations": "bool[T, 1]",
},
"intervene_flags": "all ones",
}
Images stay in memory until the episode ends, so usage grows with length.
pickle_episode
CollectEpisode writes one .pkl when the episode ends. With
only_success: true and streaming off, failed episodes are dropped.
The filename is:
rank_{rank}_env_{env_idx}_episode_{episode_id}_step_{global_step}_{success|fail}.pkl
Contents:
{
"rank": int,
"env_idx": int,
"episode_id": int,
"step": int,
"success": bool,
"observations": list, # length = num_steps + 1; item 0 comes from reset()
"actions": list,
"rewards": list,
"terminated": list,
"truncated": list,
"infos": list, # reward-model preprocessing reads the main image and per-step success
}
lerobot_dataset
CollectEpisode writes one or more episodes into a shard at
lerobot_dataset/rank_{rank}/id_{N}/; a streaming run writes one kept take
per shard. resume: true continues numbering from existing shards and does
not rewrite finished data.
id_0/
├── meta/info.json # fps, robot_type, dimensions
├── meta/episodes.jsonl
├── meta/tasks.jsonl
├── meta/stats.json
└── data/chunk-000/episode_*.parquet
Column |
Contents |
|---|---|
|
Main camera image, uint8 |
|
Other views. Empty when there is no extra view |
|
State and action vectors, |
|
Frame time in seconds, and the index within the episode |
|
Global episode number, global frame number, and task number |
|
Whether this step is the last frame; whether the episode succeeded |
The main image is taken from main_images, then image, then
full_image. Extra views come from extra_view_images, then
extra_view_image; a stacked [N, H, W, C] tensor is split into
extra_view_image-0, extra_view_image-1, and so on. State prefers
states, then state. Float images in [0, 1] are multiplied by 255
and stored as uint8.
To wrap an environment in your own script, pass the parent directory as
save_dir. The wrapper adds the format subdirectory:
env = CollectEpisode(
env=base_env,
save_dir="./logs/run",
export_format="lerobot_dataset", # or "pickle_episode"
robot_type="panda",
fps=10,
only_success=True,
)
env.close() # flush the remaining episodes
With streaming: true, each frame’s views are written as uncompressed PNGs
immediately and the parquet is written when the episode ends. The parquet keeps
those bytes, so a 640x480 view costs about 0.9 MB per frame on disk. Successful and failed episodes are
both kept, and is_success is stamped at the end. An operator discard
deletes the current unpublished episode. A recording-queue overflow is moved
to invalid_episodes/ inside that shard and is not published. A streaming
shard also has stream_frames.jsonl, which records each frame’s state
and actions.
Each info dict checks success_once, success_at_end, and success
inside final_info and episode before the info root. The episode is
successful if any of those values is true. If none of the keys appear, the
flag maintained during collection is used.