video2sim#
← 教程资料库 · 仿真与基准
本节介绍 LeRobot 的通用功能。请按目标机器人、数据集和运行环境配置示例;用于 AlohaMini 2 / 2 Pro 前,需要确认硬件与接口兼容性。
Phone video → photoreal Isaac Sim scene with a physics collider, on an 8 GB GPU.
video2sim turns a single hand-held phone walkthrough into an Isaac Sim 5.x stage: a NuRec Gaussian-splat room you can render photorealistically, plus an invisible, metric, gravity-aligned TSDF collider the robot can actually touch.
It is SfM-free: no COLMAP/GLOMAP mapping in the loop. Camera poses come from LingBot-Map streaming inference, get jointly refined during splat training (per-view se3 through the rasterizer gradients, view 0 anchored), and the splat is initialized densely from the MV-consistent TSDF cloud — InstantSplat-style. That dense init is what makes the whole thing fast: 10k iterations ≈ 20 min end-to-end training instead of the 80k-iteration growth schedule it replaces.
Every default in this package is a hard-won fix from ~10 failed runs on the
validated room3 capture. The defaults of record live in
configs/default.yaml — do not "improve" them
without re-validating.
Architecture#
Each stage is one module: importable and runnable as python -m video2sim.<stage>.
The orchestrator (python -m video2sim run) sequences them as subprocesses, routing
each stage to the interpreter its contract demands (it never auto-switches inside a
process).
phone video (.mp4)
|
v
[extract] video -> frames/f_%04d.jpg (ffmpeg, fps 8) | main venv (Basic_RL)
|
v
[lingbot_infer] frames -> lingbot/pred-*.pt | main venv
| streaming poses + depth; fp8 KV + sliding | + PYTHONPATH=<lingbot fork>
| window 48 (the 8 GB recipe) | + ninja on PATH
+---------------------------+
| |
v v
[fuse] [to_colmap] | main venv
MV-consistency filter + LingBot E/K + TSDF cloud |
4 mm TSDF fusion -> COLMAP text sparse/0 |
-> fuse/mvtsdf.ply(+mesh) (dense 350k init, NO SfM) |
| |
v v
[refine] [train] | main venv, GPU EXCLUSIVE
island filter, RANSAC gsplat MCMC 10k iters, |
floor -> z=0, metric packed=True, D-SSIM, PPISP-lite, |
scale (ceiling 2.4 m) joint pose-opt (view 0 anchor) |
-> final_mesh/cloud/align -> splat.pt (+refined_poses.npz) |
| |
v v
[scene_prep] [export] | main venv;
decimate to 300k tris splat.pt -> SH3 zero-padded PLY | USDZ substep runs under
-> collider.npz -> free-space prune (TSDF oracle) | nurec-venv (subprocess)
| -> export/scene.usdz |
+------------+--------------+
v
[scene] splat (align transform) + invisible | isaac5-venv
collider + robot + dome 450
-> scene/scene.usd [--gui]
side tools: [spots] open-spot finder + capture-camera pick | main venv
[check_env] preflight pass/fail table | any python (probes venvs)
Interpreter contract (documented in every module docstring, never auto-switched):
Stage(s) |
Interpreter |
|---|---|
extract, lingbot_infer, fuse, refine, to_colmap, train, export (steps 1–2), scene_prep, spots |
|
export step 3 (PLY → NuRec USDZ, |
|
scene (Isaac USD assembly) |
|
LingBot child process |
main venv + |
Requirements#
GPU: NVIDIA, 8 GiB VRAM minimum (validated card: RTX 4060 8 GB — the whole recipe is 8 GB-bounded). The GPU must be exclusive during LingBot inference, CUDA SfM and splat training: an open Isaac GUI alone eats 3–4 GB and OOMs the run.
RAM: 32 GB (validated on a 31 GB box; the trainer caches images as uint8 ≈ 3 GB — float32 would be ~12 GB — and fuse warns at 45 GB RSS).
Binaries:
ffmpeg/ffprobeon PATH.Venv inventory:
Venv |
Path |
Must provide |
|---|---|---|
main |
|
torch + CUDA, gsplat ≥ 1.5 (MCMCStrategy + packed rasterization), pycolmap, open3d, scipy, PIL, ninja in |
NuRec |
|
pxr (USD), torch, msgpack, ncore — for the threedgrut export path |
Isaac |
|
isaacsim 5.x + pxr (incl. |
Checkouts / models: LingBot-Map RTX4060-8GB fork (with
scripts/predict_stream.py), LingBot checkpointlingbot-map-long.pt, and a3dgrutcheckout (/home/perelman/3dgrut) for the NuRec exporter modules.
Setup#
Run the preflight check first — it verifies every runtime the pipeline touches before you burn an hour on a run (venv imports in subprocesses, ffmpeg, LingBot fork + ckpt, ninja, GPU VRAM/exclusivity, RAM), and exits nonzero on any FAIL:
cd /home/perelman/AlohaMini/video2sim
python3 -m video2sim.check_env
check_env is stdlib-only and runs under any Python; heavy imports are probed in
subprocesses under their own interpreters. Read the WARN rows too — a "busy GPU"
warning means training/inference will OOM if you proceed.
Quickstart#
One command, end-to-end (idempotent — re-running skips stages whose outputs exist):
cd /home/perelman/AlohaMini/video2sim
/home/perelman/Basic_RL/.venv/bin/python -m video2sim run ~/room.mp4 --workdir runs/room1
Then open the assembled scene in the Isaac GUI (needs the GPU free again):
/home/perelman/Basic_RL/.venv/bin/python -m video2sim run --workdir runs/room1 --gui
Outputs land in the workdir: frames/, lingbot/pred-*.pt, fuse/mvtsdf.ply(+_mesh),
final_{mesh,cloud}.ply + final_align.npz, sparse/0/, splat.pt,
export/scene.usdz, collider.npz, scene/scene.usd.
Per-stage overrides go through a YAML config (--config, key stage_args) or by
running any single stage directly, e.g.
python -m video2sim.train --workdir runs/room1 --iters 20000 --video.
For a ~60 s capture the wall-clock is ≈ 30–40 min, training-dominated (~20 min).
The standard recipe (and WHY each number is what it is)#
Mirrors configs/default.yaml == the argparse defaults. Every WHY below was measured
on this hardware; the odd-looking constants are failure post-mortems, not taste.
Stage |
Setting |
WHY (measured) |
|---|---|---|
extract |
|
Dense enough for sequential matching at walking speed, sparse enough to stay tractable. Thinning happens HERE — downstream is stride 1 always. Near-lossless JPEG: feature extractors are quality-sensitive. |
extract |
autorotate, no transpose filter |
ffmpeg applies rotation metadata before |
(any SfM validation) |
|
Per-frame independent cameras collapse focal lengths to −1e6…+8e4 → training loss oscillation + fog. Shared camera recovers fx 1642 (independently matching LingBot's 1643). |
lingbot_infer |
fp8 KV cache + sliding-window 48 |
THE 8 GB recipe. sw64+fp8 = confirmed OOM (KV +0.6 GB collides with the attention peak). |
lingbot_infer |
|
FlashInfer pre-allocates the paged-KV pool from it; the fork's default 1024 was 6.7 GB on its own → OOM on 8 GB. |
lingbot_infer |
|
The ckpt pos_embed (1,1370,1024) is size-bound — never override. |
fuse |
voxel 4 mm, trunc 2 cm, conf > 2.3, neighbors ±4/8/12, ≥ 2 consistent |
Collider-grade surface that still fits RAM. Kept-pixel % doubles as a pose-health metric: same-track ~88 %, cross-track collapses to ~58 %. |
refine |
camera-track gates on floor RANSAC (above > 0.9, med > 0.2, spread < 0.45) |
The biggest plane is often a WALL. The spread gate is what rejects walls: a hand-held phone keeps near-constant height over the FLOOR but varying distance to walls. |
refine |
ceiling anchor 2.4 m, fallback hand-height 1.35 m |
Metric scale from floor→ceiling when a horizontal ceiling plane is visible; else phone hand height over the floor. |
to_colmap |
dense init 350k points from the TSDF cloud |
The InstantSplat fast path: growth-free training converged in 10k iters vs 80k, with 38× fewer floaters. LingBot extrinsics are world→cam in COLMAP convention — qvec/tvec written directly, no inversion. |
train |
|
10k suffices because of the dense init. cap 500k = measured OOM; 350k @1080p fits. (Yesterday's 1080p OOM streak was actually the extra depth render channel, not the resolution.) |
train |
|
Sparse tile-intersection buffers are the difference between OOM and full-res on 8 GB (unpacked died at ~260k gaussians even at 2× downscale). |
train |
lrs keyed BY NAME |
|
train |
|
Corridor/forward-motion scenes give a huge camera spread that over-inflates means-lr + MCMC noise and collapses opacity to fog. |
train |
D-SSIM 0.2 term |
Plain L1 is minimized just as well by big translucent blobs — the fog optimum. |
train |
joint pose-opt on, lr 1e-4, view 0 anchored |
Absorbs feedforward pose drift without SfM: LingBot poses measured at 54 cm RMS vs a GLOMAP/BA trajectory (Umeyama residual; reproduced 53.8). Corrections keep growing with budget (1 cm median @20k → 3.8 cm @80k) — joint opt asymptotes toward SfM. |
train |
PPISP-lite per-view exposure/WB |
Phone auto-exposure/AWB otherwise gets baked into the splat as geometric mud; identity at novel-view time. |
train |
|
LingBot depth + poses from a different track = fog hedging (every gaussian goes translucent); MV re-fusion drops 88 % → 58 %. Depth and poses are one body. |
train |
optimizer step first, THEN MCMC relocation |
Official ordering; relocating first applied stale grads to teleported gaussians. |
export |
SH3 zero-padded PLY (62 props) |
NuRec assumes |
export |
free-space prune, radius 0.20 m, spatial criterion ONLY |
TSDF cloud is the surface oracle; conservative on purpose (TSDF holes only lose gaussians beyond the radius). Opacity pruning is FORBIDDEN for MCMC ensembles — |
scene_prep |
collider budget 300k tris |
Full contact honesty without blowing up PhysX cooking (same budget as the CuRobo env-mesh recipe). |
scene |
dome intensity 450, no key light |
The mesh-room recipe (dome 1200 + key 2000) overexposes the NuRec splat to white blur; approved GUI views used ~350–450. |
scene |
collider identity transform; only the splat gets the align matrix |
The refined mesh is already metric/aligned (floor z=0); the splat still lives in the raw LingBot frame. |
spots |
candidates within 1.2 m of the walked track |
The global max-clearance cell was reconstruction garbage OUTSIDE the walls — noise past the room boundary looks like open floor to a naive distance transform. |
spots |
demo camera = a real capture-trajectory pose |
A synthetic camera 2.4 m out sat inside splat furniture and filmed only blur; a pose the videographer actually stood at is guaranteed fog-free. |
Troubleshooting#
Failure modes actually hit during development, symptom-first:
Splat renders as fog / translucent blobs. Check in order: (1) lrs keyed by name, not position (
ParameterDictalphabetical-sort trap); (2) D-SSIM term present; (3)scene_scalecapped (corridor scenes); (4) you did NOT pair--depth-regwith poses from a different track.Training OOM on 8 GB.
packed=Truemust be on; cap ≤ 350k (500k OOMs); drop--depth-reg(the extraRGB+EDrender channel is what killed 1080p runs); make sure nothing else holds VRAM — the Isaac GUI alone eats 3–4 GB.LingBot OOM in the FlashInfer paged-KV pool.
max_frame_nummust be sized to the sequence (the wrapper does this; the fork default 1024 pre-allocates 6.7 GB). Sliding window > 48 with fp8 also OOMs. GPU must be exclusive.ninja: not foundduring LingBot startup. The fp8-KV CUDA extension is JIT-compiled and needs ninja in the main venv bin (leads PATH in the child);check_envverifies this.NuRec renders at ~47 s/frame. Your PLY is SH0-only. Export through
video2sim.export— it zero-pads the 45f_restSH3 props.Splat mostly disappeared after export. Opacity pruning was applied to an MCMC ensemble. Keep
--min-opacity 0.0(the default); pruning is spatial-only.Scene washes out to white blur in Isaac. Dome intensity too high for a NuRec splat — use the default 450, no key light.
fuse kept-pixel % collapses (~15–17 %). Capture problem, not code: portrait orientation + fast pans (room2) vs landscape + slow walk (room3, 88 %). Re-shoot landscape and slow; kept-% is the health gate to check first.
Floor/scale wrong after refine. A wall won the RANSAC or no ceiling was seen: check the per-plane gate log (
above/med/spread), and whether the hand-height fallback (1.35 m) fired on a scene where the ceiling anchor should have.extract refuses to run. Stale
f_*.jpgin the frames dir — they silently poison the single-camera track (mixed resolutions / numbering). Pass--forceto clobber deliberately.Open-spot picker chose a point outside the room. Keep the walked-track restriction (
--track-radius 1.2); the unrestricted max-clearance point was recon garbage beyond the walls.Demo camera films only blur. It was placed at a synthetic viewpoint inside splat geometry — pick a real capture pose (
video2sim.spots --camera-target ...).Stage crashes mid-pipeline. The runner is resumable: re-run the same
python -m video2sim runcommand; stages with existing outputs are skipped (--forceto redo,--from/--untilto slice).
Results — v10 vs v11#
Both validated on the room3 capture (62 s landscape 1080p phone walkthrough):
v10 |
v11 (standard) |
|
|---|---|---|
Init |
sparse points, MCMC growth |
dense 350k TSDF init (InstantSplat-style) |
Iters |
80k (most of the gain harvested in the post-relocation settle phase) |
10k |
Training wall-clock |
~7× v11 (hours) |
~20 min |
Floaters |
baseline |
38× fewer (vs sparse init) |
Visual |
slightly better (user judgment) |
approved; not worth 7× |
v11 is the adopted standard — this package's defaults are v11. Use the v10
budget (--iters 80000 + growth) only when squeezing the last bit of sharpness
matters more than turnaround. Reference artifacts from the session:
room3_nurec_v10.usdz / room3_nurec_v11.usdz (+ matching room3_splat_v*.ply).
Repository layout#
video2sim/
__main__.py python -m video2sim -> cli
cli.py end-to-end runner (subprocess per stage, resume, venv routing)
check_env.py stage 0: preflight pass/fail table
extract.py stage 1: video -> frames (ffmpeg)
lingbot_infer.py stage 2: frames -> pred-*.pt (LingBot 8GB recipe)
fuse.py stage 3: MV-consistency filtered TSDF fusion
refine.py stage 4: island filter + gravity/metric alignment
to_colmap.py stage 5: LingBot poses + TSDF cloud -> COLMAP text model
train.py stage 6: gsplat MCMC trainer (+ joint pose-opt, flythrough)
export.py stage 7: splat.pt -> SH3 PLY -> free-space prune -> NuRec USDZ
_nurec_convert.py (NuRec-venv helper subprocess for the USDZ step)
scene_prep.py stage 8: refined mesh -> decimated collider npz
scene.py stage 9: splat + collider + robot -> scene.usd (Isaac venv)
spots.py open-spot finder + capture-camera pick
configs/default.yaml the recipe of record (documented defaults)
tests/test_smoke.py CPU-only smoke tests
Tests#
CPU-only smoke tests (se3 exp vs scipy, 62-prop PLY roundtrip, COLMAP text ↔ pycolmap, quaternion roundtrips, free-space pruning):
cd /home/perelman/AlohaMini/video2sim
/home/perelman/Basic_RL/.venv/bin/python -m pytest tests/ -q