Stereo Capture Strategy
This document details the approach implemented by examples.uvc_capture_stereo
to obtain low-latency synchronisation between two UVC cameras. It covers the
threading model, queue handling, timestamp usage, calibration workflow, and
provides a battle-tested launch command for dual HDMI grabbers.
Architecture Overview
The script relies on three coordinated threads:
Main consumer – pairs frames, applies calibration, logs/plots results, and drives the OpenCV preview.
Left producer – opens the left camera, negotiates PROBE/COMMIT, captures frames, and pushes them into a bounded queue.
Right producer – identical to the left producer but targeting the other camera.
A threading.Barrier (size 3) ensures that both producers have completed the
USB negotiation before any frames are consumed. Once the barrier releases, the
main thread flushes stray buffers, sets a start_event semaphore, and each
producer can optionally sleep for --*-start-delay-ms to compensate for hub or
bus asymmetries before entering the capture loop.
Queueing and Drop Policy
Each producer writes into a small queue (
--queue-size; default 3). On overflow, the oldest frame is dropped so the most recent frame is always available to the consumer.--pairing-mode latest(default) drains each queue every iteration so the consumer always pairs the freshest frame, minimising display lag.--pairing-mode fifoconsumes frames one-by-one when strict sequencing matters more than absolute freshness.Libusb/libuvc have their own internal buffers. Lowering
--stream-queueto 2 (or even 1 when the firmware allows it) reduces the total latency.
Timestamp Handling
Every queued frame carries two timestamps:
host_tstime.monotonic()when the frame finished decoding on the host.ptsHardware timestamp provided by the camera firmware, when available. Not all devices expose this field.
The consumer works in three stages:
Calibration –
--calibration-pairs Naverages the first N host deltas to estimate the steady-state offset between cameras. Once collected, the script locks on the derived target and recentres future deltas around it.Manual override –
--target-delta-mscan be set when the expected offset is already known (for example-36). Set--calibration-pairs 0to skip auto-calibration.Tolerance –
--max-ts-diff(seconds) defines the pairing window after recentering. Frames outside this window are dropped so the capture remains real-time.
PTS deltas are logged when present, but the pairing decision is driven by the host delta because many firmwares omit valid PTS.
CPU Affinity and Coordination
--left-core and --right-core pin the producer threads via
psutil.Process().cpu_affinity so each capture loop can run on a dedicated
CPU. The consumer stays on the default scheduler, which keeps the UI
responsive. Producers are daemon threads: Ctrl+C or window close events set
the shared stop_event, join the streams, close the cameras, and destroy the
OpenCV window.
Recommended Command
On a dual HDMI capture rig the following command delivered the most stable results (5 FPS MJPEG, minimal latency):
python3 examples/uvc_capture_stereo.py \
--device-id 32e4:9415 \
--left-device-sn 406c101e3c214ef3 \
--right-device-sn 3054481e58586223 \
--interface 1 \
--width 1920 --height 1080 --fps 5 \
--codec mjpeg --decoder pyav \
--max-ts-diff 0.050 \
--pairing-mode latest \
--calibration-pairs 30 \
--print-deltas --display \
--left-core 2 --right-core 3 \
--left-start-delay-ms 0 --right-start-delay-ms 60
Key takeaways:
Lowering FPS and using MJPEG reduces the USB bandwidth requirement and decoder workload.
The 60 ms post-barrier delay pushes the slower bus to “catch up” on this hardware.
--print-deltasshows both the raw host delta and the centred value (after calibration) so you can monitor drift in real time.
Tuning Checklist
Start with
--calibration-pairs 0 --print-deltasto inspect the raw delta.Decide whether to rely on auto-calibration or set
--target-delta-ms.Trim
--stream-queueand--queue-sizeif the preview feels laggy.Adjust
--left/right-start-delay-msafter you know which camera leads.When PTS deltas diverge but host deltas remain stable, suspect firmware clock drift and fall back to host-only pairing.
Following this process should keep the pairing error within a few milliseconds for identical cameras connected to different buses.