External trigger snapshot not working on OV9281 USB camera (OpenCV, Raspberry Pi 4): missing exposure_auto_priority

Hi, I’m having trouble getting external trigger snapshot mode working on a Raspberry Pi 4 with Python, and I’m hoping someone has run into this before.

The camera is an Arducam 120fps Global Shutter USB Camera Board - 1MP 720P OV9281 with M12 lens. I’ve been following the Arducam wiki for external trigger mode on the OV9281 global shutter:
https://docs.arducam.com/UVC-Camera/Appilcation-Note/External-Trigger-Mode/OV9281-Global-Shutter/

My end goal is to trigger and capture synchronized images from multiple cameras from Python on Linux.

What I’ve tried so far:

  • The wiki says to set exposure_auto_priority=1, but that control doesn’t exist on my unit, so I set exposure_dynamic_framerate=1 instead.
  • External trigger mode works fine on Windows with AMCap, so the basic trigger path seems OK there.
  • In Python I tried running cap.read() in a loop inside a thread with OpenCV VideoCapture, but it never returns a frame.
  • I also tried creating the VideoCapture object before sending the GPIO trigger pulse, and cap.read() still doesn’t give me anything.

So my main question: since there’s no exposure_auto_priority control on this camera, which control should I be changing to enable external trigger snapshot mode?

Thanks for the detailed write-up, that helps a lot.

Can you post a few more things so we can see what the Pi is actually seeing?

  • v4l2-ctl -c output (and -l if you can)
  • the device/format info from v4l2-ctl --all or similar
  • the exact warning/error you get when cap.read() fails

Also, one quick check: are you setting exposure_dynamic_framerate before you open the VideoCapture, or after? The order matters with these UVC trigger modes.

Here’s the info. This is from v4l2-ctl --all (trimmed to the relevant parts):

Driver name: uvcvideo
Card type: Arducam OV9281 USB Camera: Ardu
Bus info: usb-0000:01:00.0-1.1
Driver version: 6.1.63
Capabilities: 0x84a00001 Video Capture Metadata Capture Streaming Extended Pix Format
Device Caps: 0x04200001 Video Capture Streaming Extended Pix Format

Media driver info: uvcvideo, model Arducam OV9281 USB Camera: Ardu, serial UC762, media version 6.1.63, driver version 6.1.63.

Format Video Capture: Width/Height 1280/720, Pixel Format ‘YUYV’ (YUYV 4:2:2), Field None, Bytes per Line 2560, Size Image 1843200, Colorspace sRGB, Transfer Function Rec. 709, YCbCr/HSV Encoding ITU-R 601, Quantization Default (maps to Limited Range).

Streaming Parameters Video Capture: Capabilities timeperframe; Frames per second: 10.000 (10/1); Read buffers: 0.

And v4l2-ctl -c shows:

auto_exposure 0x009a0901 (menu) : min=0 max=3 default=3 value=3 (Aperture Priority Mode); 1: Manual Mode; 3: Aperture Priority Mode
exposure_time_absolute 0x009a0902 (int) : min=1 max=5000 step=1 default=157 value=157 flags=inactive
exposure_dynamic_framerate 0x009a0903 (bool) : default=0 value=1

I also attached the full v4l2-ctl -l output. The important part is there’s no exposure_auto_priority at all, so the wiki command doesn’t apply. What control should I be using for the external trigger snapshot mode?

OK, so your unit really does have a different control set than the wiki page. The wiki assumes exposure_auto_priority is present, but your camera exposes exposure_dynamic_framerate instead.

Can you paste the exact Python script you’re using? And to answer the earlier question, show the order of the v4l2-ctl calls relative to when you create cv2.VideoCapture. That will help narrow down whether it’s the control setup or how OpenCV is reading the stream.

Here’s the test script I’m using:

import RPi.GPIO as GPIO
from datetime import datetime
import time
import cv2
import subprocess
from threading import Thread
target_path = ‘path/to/save/img’

Function for running a command in the shell

def run_cmd(cmd):
print(f’{cmd}‘)
try:
subprocess.run(cmd, universal_newlines=True, check=False, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except RuntimeError as e:
print(f’Error: {e}’)
except:
print(“Set up error!”)
def cap_img(cap_path):
cap = cv2.VideoCapture(cap_path)
print(f"Image Captured: {cap}“)
suffix = datetime.now().strftime(”%M%S_%f")[:-3]
ret, frame = cap.read()
print(ret, frame)
if ret:
cam_num = cap_path.split(‘/’)[2]
# Extract grayscale from mono camera
frame_gray = frame[:, :, 0]
cv2.imwrite(f’{target_path}/{cam_num}_{suffix}.png’, frame_gray)
cap.release()
def cap_thread(cap_path):
capture = Thread(target=cap_img, args=(cap_path, ))
capture.start()
def is_valid(voltage, threshold):
# from config
if voltage >= threshold:
return True
else:
return False
set_auto_exposure = “v4l2-ctl -c auto_exposure=1 -d”
set_exposure_dynamic_framerate = “v4l2-ctl -c exposure_dynamic_framerate=1 -d”

Camera setting

run_cmd(f"{set_auto_exposure} 0")

time.sleep(2)

run_cmd(f"{set_exposure_dynamic_framerate} 0")
time.sleep(2)

Pin Definitons:

INPUT = 2 # Broadcom pin 2
OUTPUT = 3

Pin Setup:

GPIO.setmode(GPIO.BCM) # Broadcom pin-numbering scheme

GPIO.setup(INPUT, GPIO.IN)

GPIO.setup(OUTPUT, GPIO.OUT)

Initial state for LEDs:

GPIO.output(OUTPUT, GPIO.LOW)
time.sleep(1)
cap = cv2.VideoCapture(0)
time.sleep(2)
print(“out”)
ret1, frame1 = cap.read()
GPIO.output(OUTPUT, GPIO.HIGH)
time.sleep(0.002)
GPIO.output(OUTPUT, GPIO.LOW)
for i in range(100):
ret, frame = cap.read()
print(ret, frame)
if ret:
suffix = datetime.now().strftime(“%m%D_%H%M%S_%f”)[:-3]
# Extract grayscale from mono camera
frame_gray = frame[:, :, 0]
cv2.imwrite(f’{target_path}/video0_{suffix}.png’, frame_gray)
GPIO.cleanup()

So the v4l2-ctl call sets exposure_dynamic_framerate=1, then I sleep, set up the GPIO, open cv2.VideoCapture(0), wait 2 seconds, and try the first cap.read() before the trigger pulse.

When exposure_dynamic_framerate=1 is set, cap.read() fails with:

[ WARN:[email protected]] global cap_v4l.cpp:1119 tryIoctl VIDEOIO(V4L2:/dev/video0): select() timeout.

After that warning, ret is False and frame is None, and the loop keeps printing False/None.

Thanks for the script and the exact warning.

So, where we stand right now:

  • External trigger works in AMCap on Windows, so the trigger path is functional there.
  • This unit exposes exposure_dynamic_framerate instead of the wiki’s exposure_auto_priority, so the wiki steps don’t map 1:1 to your camera.
  • With exposure_dynamic_framerate=1, OpenCV’s V4L2 backend hits a select() timeout and cap.read() returns (False, None), meaning no frame is reaching OpenCV in that mode.

I don’t have a confirmed root cause or fix yet. The timeout is at the V4L2 layer, so it’s not just a Python read-loop issue, but I need to check how external trigger is supposed to be configured on this OV9281 USB board revision under uvcvideo 6.1.63. I’ll post back once I have something concrete.