System designTroubleshooting

Triggered stereo pair plus a free-run color camera in one pipeline

Fix a pipeline where a triggered stereo pair plus a free-run color camera run together: keep the color sensor continuous and block with a timeout.

LumaSpec Vision10 min read Share on LinkedIn

On this page 9 sections
  1. 01The symptom, and what each observation proves
  2. 02Cause 1: trigger settings on a sensor with no snapshot mode
  3. 03Cause 2: polling a free-run stream with a triggered loop
  4. 04The fix: continuous color, one queue per stream, blocking reads
  5. 05Worked example: the timing budget at a 10 Hz trigger
  6. 06When free-run color is the wrong answer
  7. 07Building the rig from three discrete cameras
  8. 08Checklist before you ship the pipeline
  9. 09Questions

You have a 3D camera with two mono sensors taking an external FSYNC trigger and a third, color sensor you want streaming all the time. The pair delivers frames; the color OutputQueue never does, has() sits at false, and deleting the trigger lines from the example pipeline changes nothing. This guide gives the diagnosis and the fix that let all three streams share one host loop: the color sensor stays in continuous mode, every stream gets its own queue, and every read blocks with a timeout instead of polling. You will see why trigger settings kill a sensor that lacks snapshot support, what a 1000 ms wait proves, and when to stop mixing modes and gate three discrete cameras on one edge.

The symptom, and what each observation proves

The failure pattern narrows the cause faster than any settings change, so start from what you already know:

Observation What it proves
Stereo frames arrive on every trigger edge The FSYNC wiring and the mono sensors' snapshot mode are correct.
The color queue stays empty after the trigger lines are deleted What remains is a configuration or read-pattern fault, not wiring.
The color camera streams when it runs without the pair The color sensor and its link are healthy; the two modes conflict inside one pipeline.
QObject::moveToThread prints once at start-up A GUI-framework thread message; it does not touch frame delivery. Set it aside.

That last row matters because it is the only error text you will see. An empty queue here fails silently, so the diagnosis rests on the observations above, not on logs.

Cause 1: trigger settings on a sensor with no snapshot mode

Snapshot mode is the sensor capability that lets an external edge start an exposure. Support is listed per sensor in the device's FSYNC documentation, and on this device class the color sensor does not have it. The two lines most multi-camera examples copy onto every camera node are:

camRgb.initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT)
camRgb.initialControl.setExternalTrigger(4, 3)

On a sensor with snapshot support, those calls start frames on the edge. On a sensor without it, frame start waits for a capability that does not exist: the sensor produces nothing, the queue never fills, and no exception names the reason. If those two lines ever landed on your color node, that alone explains a permanently empty color queue.

Deleting the lines is necessary, but on its own it is not always enough, because a second, independent fault hides behind the first.

Cause 2: polling a free-run stream with a triggered loop

has() on an OutputQueue answers one question: "is a frame buffered at this instant". It never means "no frames are coming". A free-run color sensor at 30 fps delivers a frame every 1000 / 30 = 33.3 ms on its own clock, unrelated to your trigger. A loop built around the pair wakes on trigger events, checks the color queue once, reads false, and moves on — reporting a dead stream while about three color frames per 100 ms trigger cycle arrive behind it. A false from has() almost always means "not yet"; treating it as "never" turns a live stream into a reported failure.

Setting the same FPS on all three sensors changes none of this. Equal frame rates set the cadence, not the phase: the color sensor still starts each exposure from its own clock, wherever inside your 100 ms cycle that falls. Matching numbers is not synchronization; only a shared edge on sensors that support it is.

The fix: continuous color, one queue per stream, blocking reads

Apply four changes, in this order:

  1. Strip the trigger configuration from the color node. Delete both initialControl lines from camRgb and leave the sensor in its default continuous mode — the free-run discipline described in continuous camera acquisition mode for live image loops.
  2. Set the trigger per node, on the pair only. Per-camera trigger configuration stops one sensor's settings leaking onto another, the habit explained in hardware trigger configuration per camera.
  3. Create one OutputQueue per stream, each with its own depth. The color queue must be sized and drained for 33.3 ms arrivals, not for trigger events.
  4. Read every queue with a blocking get(timeout_ms=1000). One call per frame, as covered in frame didn't arrive within timeout: one wait call per frame.
# Trigger only where the sensor supports it: the mono pair.
camLeft.initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT)
camLeft.initialControl.setExternalTrigger(4, 3)
camRight.initialControl.setFrameSyncMode(dai.CameraControl.FrameSyncMode.INPUT)
camRight.initialControl.setExternalTrigger(4, 3)
# camRgb carries no initialControl trigger lines: continuous free-run.

# q_left, q_right, q_color: one OutputQueue per stream, depth 4 each.
timeout_ms = 1000    # int; 30 color frame periods at 30 fps
count_pair = 0       # int; triggered stereo pairs received
count_color = 0      # int; color frames received

while count_pair < 100:              # 100 pairs = 10 s at a 10 Hz trigger
    left = q_left.get(timeout_ms)    # blocking read, own queue
    right = q_right.get(timeout_ms)
    color = q_color.get(timeout_ms)
    count_pair += 1
    count_color += 1

print(count_pair, count_color)       # expect: 100 300 (plus or minus a few)

The 10-second count is the test that proves the fix: 100 pairs and about 300 color frames, with the pair still edge-gated. Because 3 color frames arrive per 100 ms cycle but this loop consumes 1, drain the color queue each cycle or set it to keep the newest frame only — the drain pattern in latest image only returns duplicate frames: drain the buffer queue and the transport setting in stream buffer handling mode: set newest only.

Worked example: the timing budget at a 10 Hz trigger

Put numbers on the coexistence so you can tell margin from luck:

color frame period = 1000 ms / 30 fps = 33.3 ms color frames per 100 ms trigger cycle = 100 ms / 33.3 ms = 3 timeout headroom = 1000 ms / 33.3 ms = 30 frame periods

The trigger runs at 10 Hz, so one cycle is 100 ms. The pair's exposure is 5 ms; on a conveyor at 150 mm/s a part travels 150 mm/s × 0.005 s = 0.75 mm during that exposure, which is why the pair is gated to a strobe edge instead of free-running. The pair is ready about 10 ms after the edge (5 ms exposure plus 5 ms readout in this example), and your processing takes 20 ms. The next color frame is at most 33.3 ms away, because one is always in flight. Worst-case loop body: 10 + 20 + 33.3 = 63.3 ms against a 100 ms cycle, leaving 36.7 ms of margin. The 1000 ms timeout is 30 frame periods against a 33.3 ms worst-case wait, so it proves liveness without ever firing in a healthy pipeline.

Queue depth follows from the same numbers: depth 4 holds the 3 frames that arrive per cycle plus one. Drain fully each cycle, or let the queue keep newest only.

When free-run color is the wrong answer

Free-run is correct only when the color frame does not need the trigger instant. If you map color texture onto depth of a moving part, the color frame can sit up to one period away from the pair's exposure: 33.3 ms at 30 fps, which on a 150 mm/s conveyor is 150 mm/s × 0.0333 s = 5.0 mm of mismatch between what the pair measured and what the color camera saw. No queue discipline, timeout or FPS setting removes that skew — the color exposure must start on the same edge as the pair. That is a hardware decision, not a software one.

Building the rig from three discrete cameras

When the skew budget says "same edge", stop mixing modes inside one device and gate three cameras on one trigger line: two mono cameras for the pair and one global-shutter color camera, every exposure starting on the same pulse. Trigger-input CMOS cameras hold their frame rate under trigger — one current datasheet specifies 58 fps at full resolution — so a 10 Hz edge leaves a wide margin.

For the color channel, pick a global-shutter color model from the area scan cameras range, filtered in the camera finder:

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

The MV-CU020-90GC is a 2.5 MP GigE camera with a global shutter and a C-mount, so it gates on the shared line exactly like the mono pair. For the host, a single card carries the whole rig:

Frame grabbersHikrobot MV-GE2002P Dual-port PoE NIC, full height V2.0MV-GE2002P
Interface
GigE
Ports
2 ports
$249.00View partBuild a system with it

The MV-GE2002P is a dual-port PoE NIC, which hosts the pair and the color camera on one card. For trigger and power wiring at both ends:

Cables5 m 12-Pin IO I/O and power cable (LS-CBL-0211)LS-CBL-0211$56.00View part
CablesFrame Grabber I/O Cable, DB9F to Open, 7 m (LS-CBL-0224)LS-CBL-0224
Type
Frame grabber I/O cables
Length
6-8 m
$79.00View part

The LS-CBL-0211 is a 5 m 12-pin I/O and power cable for the camera side; the LS-CBL-0224 is a 7 m frame grabber I/O cable, DB9F to open leads, for the host end. More host cards sit in frame grabbers, and the full trigger-wiring range sits under cables.

AccessoriesCables

Camera data, power and I/O cables

401 models · from $22.00

Three cameras on one pulse is an electrical fan-out, not just software: each opto-isolated trigger input draws current, so size the source before wiring. The resistor math is covered in fanning out one TTL pulse safely and wiring a trigger to an opto-isolated input. On the capture side, multi-image capture per trigger shows the one-edge, three-frame pattern, and setting up a stereo trigger pair covers the pair's ready and timeout handshake. If a GigE camera in the rig ignores the line, start at GigE camera ignores external trigger: check acquisition mode first, and size the transport with USB3 bandwidth for a multi-stream pipeline or acquiring from multiple cameras.

Configure the three-camera rig once you have a shortlist, or send the skew budget and trigger rate through the contact page and we will size it with you.

Checklist before you ship the pipeline

  • No setFrameSyncMode or setExternalTrigger line on the color node; it runs continuous.
  • One OutputQueue per stream, each read with a blocking get and a timeout.
  • The 10-second count test passes: about 100 pairs at 10 Hz and 300 color frames at 30 fps.
  • The color queue is drained or newest-only, sized for 3 arrivals per 100 ms cycle.
  • Timestamps are logged per stream, with the accepted color-to-pair skew under the motion budget.
  • Where skew must be zero, three discrete cameras share one trigger line instead of free-run color.

Questions

Why does has() return false on the color queue while frames are arriving?

has() reports the queue state at the instant of the call, nothing more. At 30 fps a frame lands every 33.3 ms; a loop that wakes on the trigger and polls once reads false between arrivals and calls a live stream dead. Block with get(timeout_ms=1000) instead, which is 30 frame periods, and if you must poll, poll from a thread that sleeps between checks rather than one that spins.

Can I trigger the color sensor anyway if it lacks snapshot mode?

No. Snapshot mode is what lets an edge start an exposure on that sensor; without it, setFrameSyncMode(CameraControl.FrameSyncMode.INPUT) and setExternalTrigger(4, 3) gate a frame start that never comes, and the queue stays empty with no error. Leave that sensor in continuous mode and pair frames by timestamp; the mono pair keeps its trigger unchanged, and the worst offset is one color period.

How long should the per-queue timeout be?

Bring the pipeline up at 1000 ms, then tighten once the 10-second count test passes. Drop to 150 ms: about four and a half color frame periods and one and a half trigger cycles at 10 Hz, so a stalled stream raises quickly enough to retry inside the cycle instead of waiting a full second. Keep the larger value around for first bring-up and for debugging, when you would rather see one slow failure than a tight loop of retries.

Does setting the same FPS on all three sensors synchronize them?

No. Equal frame rates set the cadence, not the phase: each free-run sensor starts exposures from its own clock, so the color frame still falls anywhere inside the 100 ms trigger cycle. Only a shared hardware edge on sensors with snapshot support aligns frame starts. Matched FPS makes the offset steady instead of removing it, which is worse for diagnosing.

How do I prove the color stream is alive while the pair is triggered?

Log a timestamp for every color frame over 10 s. At 30 fps expect about 300 frames roughly 33.3 ms apart, independent of the 10 Hz trigger. Zero frames points at configuration, meaning trigger lines still sitting on the color node. Periodic gaps instead point at the shared link: total the bandwidth of all three streams before blaming the trigger, then re-test.

Is the QObject::moveToThread warning the reason no color frames arrive?

No. That message comes from the host GUI framework relocating an object between threads at start-up; it concerns display ownership, not data transport. Frames move from the acquisition thread into the queues regardless. Chase it only after the 10-second count test passes and a display artifact remains — it is not on the path that fills the color queue.