feat(stereo-split): re-time IMU samples onto the device grid - #230
Conversation
The recorder stamps /decxin/imu with the host time at which each message was emitted and repeats one sample at every packet boundary, so a 600 Hz sensor arrives as ~661 messages per second in bursts of eleven, and the payload header carries a per-frame interpolation whose rate is about 13% off. The image metadata column still carries the device's own microsecond timestamps for one packet per camera frame, and that packet plus the rigid link between the device grid and the frame strobe is enough to reconstruct the whole stream. - imu_retiming.py measures the grid spacing, the offset from the frame's exposure end, and the frame period per file, then checks all of them on every decoded barcode before it commits to re-timing anything. - convert_mcap_stereo_h264.py reads those barcodes in the analysis pass it already runs for colour fitting, and the conversion pass rewrites the IMU payload header and its MCAP timestamps, dropping the repeated samples. - The manifest gains a lean imu_retiming summary and the metadata keeps the full report, so a skip is always explainable. Split inputs are untouched. Measured on a real 1106-frame capture: 24794 -> 22541 messages, every step exactly 1664 us, 601.0 Hz unique rate, every surviving sample an ordered subset of the source, and header stamps equal to log times.
Replaying five fresh Ego Portal Stereo captures surfaced two ways the grid check was stricter than the device actually is: - Device timestamps are whole microseconds, so a frame's samples can sit one microsecond off the ideal grid and a repeated sample can land one microsecond after its predecessor. The grid check now allows a two microsecond tolerance and treats anything closer than half a grid step as a repeat, instead of demanding exact equality. - A capture does not always contain a whole number of 11-sample packets (84606 = 11 * 7691 + 5 on the newest one), so the tail is now continued on the same rigid grid instead of being left uncovered, which used to abort the conversion.
Replay on five fresh captures from KeystoneFetched the newest five distinct Ego Portal Stereo captures from production Keystone (recorded 2026-09-16 23:46Z … 2026-09-17 02:43Z) and ran the job on them locally end to end.
On all five: the retiming decision was The replay is what caught two cases the unit fixtures did not cover, both fixed in 77cf414:
Colour was applied on four of the five; EP-000210 was reported as Each capture also reports |
What
/decxin/imuis currently written with the host time at which each message was emitted, and one sample is repeated at every packet boundary. A 600 Hz sensor therefore arrives as 661 messages per second in bursts of eleven, and the payload header carries a per-frame interpolation that is about 13% off the real sample rate. Anything that resamples IMU onlog_time, or counts messages as samples, gets a wrong time base.This PR re-times the IMU stream onto the device's own rigid sampling grid, using the metadata barcode that is already in every joined frame.
jobs/stereo-split/imu_retiming.py(new) measures the grid per file — spacing, offset from the frame's exposure end, and the frame period — and then verifies all of them on every decoded barcode before committing to anything.convert_mcap_stereo_h264.pyreads those barcodes in the analysis pass it already runs for colour fitting, so no extra decode is needed, and the conversion pass rewrites the IMU payload header together with its MCAP timestamps while dropping the repeated samples.imu_decoder.pynow exposes the exposure start/end values that the barcode header already carried.Evidence
The device grid is rigidly locked to the camera frame strobe. Measured on a real joined capture (1106 frames), every one of 1072 decodable barcodes agrees to the microsecond:
Ten different Ego Portal Stereo devices were checked separately (10/10): identical spacing of 1664 µs, identical frame period, identical exposure-end anchor, 100% barcode decode rate, and 22 IMU messages per frame on every device.
Result of the replay through the job on the same 1106-frame capture:
The job also reports
grid_spacing_us,frame_period_us,clock_scale_ns_per_us(999.96),clock_residual_p95_ms(5.24) and the number of extrapolated frames in both the manifest and the output metadata.Safety
Re-timing is all-or-nothing.
resolve()returns either a plan that covers the whole stream or a skip decision that leaves the source timestamps alone; a skip is reported with its reason (insufficient_barcodes,spacing_not_constant,anchor_not_constant,packet_alignment_failed,barcode_grid_mismatch,clock_fit_failed, …). Frames whose barcode fails to decode are placed on the same rigid grid, and the conversion pass refuses to finish unless the plan covered every source message.Tests
python3 -m unittest discover -s jobs/stereo-split/tests -p 'test_*.py'→ 85 tests pass (12 new).New coverage: grid fitting with a rigid synthetic stream, uniform monotonic output steps, repeats being the only dropped messages, frames without a barcode placed on the grid, and skip paths for every failure reason — plus converter-level tests for the re-timed output, the fail-safe pass-through when barcodes are unusable, and the disabled flag.
Cost
The barcode decode adds about 5 s to the analysis pass and the IMU analysis and payload rewrite about 4 s, so the job grows by roughly 13% on the regression clip.
Follow-ups (not in this PR)