Code for a chronophotography-style still: a mobile dual-arm robot carrying a cardboard box horizontally across a table, composited from a single fixed-camera video. Dense semi-transparent ghosts of the arms, a handful of box positions, no overlaid path graphics.
RB-Y1 design notes.md covers why it looks this way, what was measured, and which
approaches were tried and rejected — including a few that failed and why.
| File | What it is |
|---|---|
step1_track_box.py |
Lucas–Kanade box tracker → box_track.json |
step2_prep.py |
Background plate + sharpness table → plate.png, sharp.npy |
step3_compose.py |
The compositor — produces the figure |
render_options.sh |
Renders four variants (7 or 9 poses, two opacity levels) |
RB-Y1 design notes.md |
Design rationale and measurements |
The source video is not included — it is 161 MB, well past the gist limit. Everything here needs it. It is on YouTube:
https://www.youtube.com/watch?v=4xJldqOGSdQ
Easiest is to download it manually at 1080p (any of the browser extensions or
online downloaders will do) and save it beside the scripts as
20260811_174657.mp4.
If you would rather do it from the shell, yt-dlp works. Format 137 is the
1080p video-only stream; it returns HTTP 403 on its own, so pair it with an
audio stream and let yt-dlp merge — the scripts ignore the audio track:
yt-dlp -f "137+140/bv*[height=1080]+ba/b[height<=1080]" \
--merge-output-format mp4 \
-o "20260811_174657.%(ext)s" \
"https://www.youtube.com/watch?v=4xJldqOGSdQ"
Use a current yt-dlp — older versions silently fall back to 360p, at which point the figure still renders but comes out around 380 x 290 and the lidar clean-up stops firing.
A note on fidelity: YouTube re-encodes, so a download does not reproduce the figures bit-for-bit. Frame count and nominal frame rate differ slightly from the original (2623 @ 30 fps vs 2622 @ 29.99), which shifts the chosen poses by up to ~0.15 s and the auto-crop by a pixel. The result is visually equivalent — the sweep, the box trail and the lidar removal all come through — but if you need the exact PNGs, ask me for the original file.
pip install opencv-python-headless numpy
chmod +x render_options.sh # gists do not preserve the executable bit
./render_options.sh
That tracks the box, builds the background plate and sharpness table, and writes
sweep_p{7,9}_{low,med}.png. First run takes about three minutes; afterwards
the derived files are cached and re-rendering is seconds.
One variant at a time, with your own opacities:
python3 step3_compose.py <poses> <out.png> <arm_lo> <arm_hi> <box_lo> <box_hi> <arm_final> <box_final>
python3 step3_compose.py 9 mine.png 0.30 0.42 0.80 0.92 0.65 0.97
The scripts expect the video beside them as 20260811_174657.mp4; override with
SWEEP_VIDEO=/path/to.mp4.
All 1152 x 862 from the original file (a pixel shorter from a YouTube copy), cropped to content, from the same 58.2–66.5 s segment.
| Output | Poses | Arm α (sweep → final) | Box α (sweep → final) |
|---|---|---|---|
sweep_p7_low.png |
7 | 0.20 → 0.30, 0.52 | 0.72 → 0.86, 0.94 |
sweep_p7_med.png |
7 | 0.30 → 0.42, 0.65 | 0.80 → 0.92, 0.97 |
sweep_p9_low.png |
9 | 0.20 → 0.30, 0.52 | 0.72 → 0.86, 0.94 |
sweep_p9_med.png |
9 | 0.30 → 0.42, 0.65 | 0.80 → 0.92, 0.97 |
7 poses keeps the box positions discrete with table visible between them; 9 reads as a more continuous swept region. Low keeps the arm sweep a soft backdrop with the box dominant; medium makes individual arm links traceable.
- Track the box with pyramidal Lucas–Kanade — median flow of features inside the box, forward/backward consistency checks, reseeding when features drop off.
- Segment the trajectory by speed to isolate the horizontal transport (58.2–66.5 s); the lift before and the set-down after are excluded.
- Pick poses evenly spaced along the box path rather than in time, each the sharpest frame within ±3 frames, which rejects motion blur.
- Split each pose into cardboard (by HSV colour) and arm (everything else in the motion mask), so the white gripper stays with the arm rather than being drawn at the box's opacity.
- Composite in temporal order over a median background plate, uniform opacity per object per pose.
The camera is static to well under a pixel, so this happens in raw image coordinates with no stabilisation.
Most of it is generic, but these are specific to this footage and would need
re-deriving for another clip — all in step3_compose.py unless noted:
T0, T1 = 58.2, 66.5— the segment, found from the speed profile of the track.- The seed box
SEED_BOX_720and window instep1_track_box.py. - The cardboard HSV range (H 7–20, S 85–185, V 60–180) separating box from arm.
- The lidar-module footprint painted out of each frame, and the x > 1150 cutoff that keeps passers-by out of the crop.
Pixel constants are authored at 720p and scaled by K = width/1280, so the code
runs at either resolution unchanged.