Stage 2 — Assembly#
Your recording captured one camera. The trainer needs all of them. This stage replays the recorded action trajectory in the simulator and renders every camera of a scene configuration in one pass, one dataset per bin, then concatenates those bins into the corpus you train on.
Because all views come from the same replayed trajectory, policies trained on different camera subsets see identical motions, cube poses and contacts. That is what makes a comparison between camera sets a comparison of cameras.
flowchart LR
REC[01_raw_gamepad/bin_c1_r1<br/>recording] -->|trajectories/*.npy<br/>+ scene_state| SMR[multicam_replay<br/>num_envs=1, N cameras]
CFG[scene config JSON<br/>lift_cube_6cam.json] --> SMR
SMR -->|one dataset per bin| BIN[(lift_cube_6cam_cylroom/bin_c1_r1/)]
BIN --> FUS[bin_fusion]
BIN2[(bin_c2_r1)] -.-> FUS
BIN3[(bin_c3_r1)] -.-> FUS
FUS -->|training-ready| OUT[(04_fused/merged_6cam_cylroom/)]
How to use#
1. Make sure the trajectories exist#
This stage reads <bin>/trajectories/*.npy, not the recorded dataset. If that
directory is missing, run the extraction from
stage 1, step 6 first.
2. Choose the cameras#
The camera set is a JSON file, not code. lift_cube_6cam.json defines six
views, and each external one is an azimuth, an elevation and a radius on a
hemisphere around the workspace:
"front": { "type": "external", "azimuth_deg": 0, "elevation_deg": 45, "radius_m": 0.4 }
Adding a viewpoint means adding such an entry. Nothing else changes.
3. Render one bin#
python -m so101_mvbench.recording.multicam_replay \
--dataset_root datasets/01_raw_gamepad/bin_c1_r1 \
--episodes all \
--scene_config src/so101_mvbench/tasks/scene_configs/lift_cube_6cam.json \
--output_base datasets \
--mode create --headless
One simulator boot handles the whole bin. The output lands in
datasets/<config_stem>_cylroom/<bin>/, so the camera set is readable from
the path.
Start with --episodes 0 to render a single episode and look at it before
committing an hour of GPU time. --episodes also takes comma lists and ranges
(0,5-10).
Note
The replay deliberately runs one environment at a time. Rendering many environments in parallel degrades the image quality of the tiled cameras. At one environment, all cameras render at full quality in the same step.
4. Repeat for every bin#
for bin in datasets/01_raw_gamepad/bin_c*_r*; do
python -m so101_mvbench.recording.multicam_replay \
--dataset_root "$bin" --episodes all \
--scene_config src/so101_mvbench/tasks/scene_configs/lift_cube_6cam.json \
--output_base datasets --mode create --headless
done
Expect roughly 20 minutes per bin. Run this under a watchdog that kills a bin whose log has stopped moving, see the deadlock below.
5. Fuse the bins into one dataset#
export TMPDIR="datasets/04_fused/.tmp"
mkdir -p "$TMPDIR" # nothing creates it, and tempfile does not either
python -m so101_mvbench.assembly.bin_fusion \
--datasets_root datasets/lift_cube_6cam_cylroom \
--target datasets/04_fused/merged_6cam_cylroom \
--bins bin_c1_r1 bin_c2_r1 bin_c3_r1
--bins picks bins explicitly, --exclude drops them, --exclude-where
filters at episode level (yaw_rad=0.0), and --dry-run validates without
writing anything.
Danger
Set TMPDIR on the same partition as the target. LeRobot’s video
concatenation moves files with shutil.move. Across partitions that raises
OSError: Invalid cross-device link, and the fallback copy then trips over
read-only MP4s. This is the single most common failure of this step.
6. Check the merge#
bin_fusion pixel-validates the concatenation itself and leaves the evidence
in validation_proof/. Confirm the episode count matches your bins, and read
MANIFEST_fusion.json to see which bin contributed which episode range. That
file is what you consult later to hold out a bin.
What you get#
datasets/lift_cube_6cam_cylroom/bin_c1_r1/ # step 3, one dataset, six camera streams
├── meta/scene_config.json # the camera set, archived
├── videos/observation.images.{wrist,front,left,right,top,back}/
└── timing.json # how long each phase took
datasets/04_fused/merged_6cam_cylroom/ # step 5, trainer-ready
├── MANIFEST_fusion.json # bin to episode-range map
└── validation_proof/ # pixel-diff evidence of the merge
Important
Everything in here is synthetic, including the wrist view. Every frame was rendered during replay. The only human part is the action trajectory. A visual state that differed while you were recording but resets to scene defaults during replay simply will not be in the training data. Audit the scene config before you render twenty bins with it.
Note
Decide the out-of-distribution split here, not while recording. Record all
bins, then choose which ones enter the corpus via --bins and --exclude.
That way you can revisit the split without re-recording anything.
When something goes wrong#
See Troubleshooting, which collects the failures of every stage in one place: a hung encoder, the cross-device TMPDIR error, duplicated episodes after an append.
A note on older datasets#
Earlier corpora were built by a four-stage chain that rendered one camera pose
per pass and merged afterwards. multicam_replay replaced it, and those tools
are not part of this repository. Both produce the same LeRobot v3 layout, so
nothing downstream can tell them apart.
Where to go next#
Stage 3 — Training — train a policy on the fused dataset
Camera configurations — what the viewpoints look like
Data layout — what each directory contains