Hi, I’m having a problem setting the exposure on my Arducam stereo camera. Most of the time it works, but every now and then the exposure goes way too high and the only thing that fixes it is restarting. The module overall seems to work intermittently and is unstable for me.
The main problem is the exposure - that’s what I really need to solve.
There was one other thing: once the images came out upside down. A restart fixed it and it only happened that one time, so I don’t think it’s a big deal. The exposure is the thing I need help with.
One thing I’ve been wondering about: has the firmware been upgraded so these cameras work on the newer Raspberry Pis? If the firmware on your side is different from the firmware on mine, maybe that’s why my script doesn’t work properly. Did all the version numbers I listed above match what you expect?
Thanks, that all helps. The versions you listed are in the range we expect for that camera on a Pi 4B with Buster.
To move forward I need to see the actual code and the log. Please paste your script and the full console output from a run where the exposure doesn’t apply. If you can attach a couple of the saved frames too that would help - especially a good run vs. a bad run.
import shutil
import numpy as np
import os
import time
import cv2
import re
import subprocess
def synchronise_camera_and_driver():
"""Call this after opening a capture to use v4l2 commands
When you open a video capture for the camera we are using, v4l2 doesn't automatically apply settings. Furthermore,
if you execute a v4l2 command to set a setting to its current value, v4l2 does nothing. This function changes each
setting to two different values so that v4l2 and the camera are synchronised.
"""
# list current settings
result = subprocess.run(['v4l2-ctl', '-l'], capture_output=True)
output = result.stdout.decode()
# parse output to get the current settings
pattern = re.compile(r'\s(\w*) 0x\d{8} \(int\).*?min=(\d+) max=\d+ step=(\d+).*?value=(\d+)')
matches = pattern.findall(output)
# set each setting to two different values before returning it to its original value
# This ensures that the setting is synchronised.
for setting, _min, step, value in matches:
subprocess.run(['v4l2-ctl', '-c', f'{setting}={_min}'])
subprocess.run(['v4l2-ctl', '-c', f'{setting}={int(_min) + int(step)}'])
subprocess.run(['v4l2-ctl', '-c', f'{setting}={value}'])
def main():
if os.path.exists('images'):
shutil.rmtree('images')
os.mkdir('images')
if not os.path.exists('/dev/video0'):
print('no video0')
return
i = 0
while True:
for j in range(2):
# set up video capture
capture = cv2.VideoCapture(0, cv2.CAP_V4L2)
capture.set(cv2.CAP_PROP_CONVERT_RGB, 0)
success, _ = capture.read()
if success:
break
if j < 1:
print('cv2.read() failed. Restarting capture...')
capture.release()
else:
print('cv2.read() failed twice.')
return
print('started capture')
synchronise_camera_and_driver()
print('synchronised camera and driver')
subprocess.run(['v4l2-ctl', '-c', f'exposure=400'])
subprocess.run(['v4l2-ctl', '-c', f'gain=0'])
subprocess.run(['v4l2-ctl', '-c', f'frame_rate=30'])
print('set exposure')
for j in range(8):
success, image = capture.read()
if success:
cv2.imwrite(os.path.join('images', f'image{i}_{j}.jpg'), image)
print('image mean:', np.mean(image))
else:
print('failed to capture image')
break
time.sleep(0.1)
i += 1
capture.release()
if __name__ == '__main__':
main()
And here’s the console output. Most of the time I get the ~195 then ~94 pattern, but look at the third block - the exposure never kicks in:
started capture
synchronised camera and driver
set exposure
image mean: 195.62842836538462
image mean: 195.19442620192308
image mean: 195.29600096153845
image mean: 195.28548149038463
image mean: 94.39822067307692
image mean: 94.43818990384615
image mean: 94.43741225961539
image mean: 94.52201754807692
started capture
synchronised camera and driver
set exposure
image mean: 195.15711177884614
image mean: 195.41775192307693
image mean: 195.3825048076923
image mean: 195.31841802884617
image mean: 94.32984399038462
image mean: 94.4786233173077
image mean: 94.514896875
image mean: 94.41169879807693
started capture
synchronised camera and driver
set exposure
image mean: 195.23419471153846 # The problem occurs here.
image mean: 195.3195314903846
image mean: 195.33377908653847
image mean: 195.1443485576923
image mean: 195.59864927884615
image mean: 195.58069447115383
image mean: 195.66503485576922
image mean: 195.3583110576923
started capture
synchronised camera and driver
set exposure
image mean: 195.1981153846154
image mean: 195.38626490384615
image mean: 195.49565697115384
image mean: 195.1187610576923
image mean: 94.62874543269231
image mean: 94.53233149038462
image mean: 94.4588826923077
image mean: 94.51752019230769
Thanks, I ran your code on my setup and it doesn’t seem to work the way you’d hope - I’m seeing the same kind of flakiness. So it’s reproducible, at least.
A few observations from my side:
The camera is detected fine, so at the driver level it looks OK.
It also can be used normally by libcamera, which again points at the capture/settings path rather than the hardware itself.
I’ve attached the results/errors I got when running your code, a capture showing the camera being found, one showing libcamera using it fine, and a shot of the hardware side.
Two requests:
Could you send the code as a file (attach it)? When pasted inline the indentation gets mangled on my end and I can’t run it reliably.
Is there anything I need to notice when testing - any specific steps, timing, or conditions I should follow to trigger the failure? Just want to make sure I’m testing the same scenario you are.
I’ve sent the code to you as a file through private message - please grab it from there and try running it directly, that should avoid the indentation problem.
To answer your question about testing conditions: there’s nothing special, you just need to run it repeatedly. I ran the script over and over until I could reproduce the error. Usually it captures a few frames before the exposure changes, but occasionally the exposure doesn’t change at all - that’s the failure case.
Something else I noticed: I accidentally ran the script once without the capture.release() at the end, and I got a lot more error messages, and at one point the exposure went too low instead of too high. capture.release() seems to fix that particular case though.
Here’s the output from that run:
started capture
synchronised camera and driver
set exposure
image mean: 196.70916490384616
image mean: 196.6278153846154
image mean: 196.47773485576923
image mean: 196.34265697115384
image mean: 94.66292403846154
image mean: 94.78895384615385
image mean: 94.7960139423077
image mean: 94.77845216346154
[ WARN:0] global /tmp/pip-wheel-pd499c9i/opencv-python_3a15e83eee864e65b7311a199a94e9f1/opencv/modules/videoio/src/cap_v4l.cpp (890) open VIDEOIO(V4L2:/dev/video0): can't open camera by index
cv2.read() failed. Restarting capture...
started capture
synchronised camera and driver
set exposure
image mean: 196.4887389423077
image mean: 196.15295144230768
image mean: 196.431396875
image mean: 196.25021225961538
image mean: 94.74601634615385
image mean: 94.76040865384616
image mean: 94.75820192307692
image mean: 94.69263365384616
...
[ WARN:0] global /tmp/pip-wheel-pd499c9i/opencv-python_3a15e83eee864e65b7311a199a94e9f1/opencv/modules/videoio/src/cap_v4l.cpp (890) open VIDEOIO(V4L2:/dev/video0): can't open camera by index
cv2.read() failed. Restarting capture...
started capture
VIDIOC_S_CTRL: failed: Invalid argument
gain: Invalid argument
synchronised camera and driver
set exposure
image mean: 196.51813605769232
image mean: 196.12820240384616
image mean: 196.07549663461538
image mean: 196.16315576923077
image mean: 65.63367019230769 # exposure is too low
image mean: 65.62975721153846
image mean: 65.6336860576923
image mean: 65.62984615384616
...
# The camera failed to start at the end, and the script terminated.
[ WARN:0] global /tmp/pip-wheel-pd499c9i/opencv-python_3a15e83eee864e65b7311a199a94e9f1/opencv/modules/videoio/src/cap_v4l.cpp (890) open VIDEOIO(V4L2:/dev/video0): can't open camera by index
cv2.read() failed. Restarting capture...
cv2.read() failed twice.
The errors I keep hitting are:
VIDIOC_S_CTRL: failed: Invalid argument and gain: Invalid argument
[ WARN:0] ... open VIDEOIO(V4L2:/dev/video0): can't open camera by index
At the end the camera failed to start and the script just terminated.
So the question I really need answered: is there a method to set the exposure reliably? Because right now the exposure only applies most of the time, and when it doesn’t I have to restart everything.
Adding a bit more context on what I think is happening, in case it helps you on your side.
When a video capture is opened for this camera, v4l2 doesn’t automatically apply the settings. And if you run a v4l2 command that sets a setting to the value it’s already at, v4l2 does nothing at all. So v4l2 and the camera end up out of sync - that’s why my synchronise_camera_and_driver() function sets each control to two different values and then back to its original value, so the driver actually writes them.
And in the output you can see the tell-tale pattern: mean around 195 while exposure hasn’t been applied, then dropping to ~94 when it has. In the failure case the mean stays around 195 for all 8 frames, meaning the exposure command never took effect. And in that one bad run without the release, it went the other way and the mean sat around 65.
I’ve been calling synchronise_camera_and_driver() right after opening the capture, before issuing those v4l2 calls. It helps, but it’s clearly not bulletproof.
Thanks - the extra detail plus the file you sent privately lines up with what I got when I ran your code. Here’s where I think this stands after testing on my side:
What I can confirm:
The camera is detected normally and works through libcamera, so the module itself looks healthy at the driver/hardware level.
Your script is not reliable: it will often apply the exposure, but with repeated runs you hit cases where the exposure command silently never reaches the sensor, and the frame mean stays around 195 instead of dropping to ~94.
The VIDIOC_S_CTRL: failed: Invalid argument / gain: Invalid argument messages and the can't open camera by index warning appear when captures are opened/closed in quick succession - that’s a side effect of the capture lifecycle, and the run where you skipped capture.release() makes it visibly worse (all the way down to a mean around 65, and eventually the camera failing to open twice and the script exiting).
Root cause as it looks right now:
When the capture is opened, v4l2 doesn’t automatically apply the current settings, and issuing a v4l2 command that sets a control to the value it already holds is a no-op. That leaves v4l2 and the camera desynchronised, which is exactly the intermittent “exposure doesn’t change” behaviour you’re seeing.
Current workaround:
Keep the approach you already have: call synchronise_camera_and_driver() immediately after opening the capture so each control gets pushed to two different values and then back to its original value before you set exposure, gain, and frame_rate with v4l2-ctl. That forces the driver to actually write the values and makes the exposure apply far more consistently.
Always release the capture between iterations (capture.release()) - skipping it reliably produces the extra errors and the too-low exposure you saw.
The one-off upside-down image you mentioned earlier looks unrelated (a restart cleared it) - I’d keep an eye on it, but the exposure desync is the real issue here, and it isn’t fully solved yet on our side either. If you can run your script a few hundred iterations with the synchronise call in place and post the frame-mean log, I’ll use that to check whether there’s still a residual desync window and whether we need anything beyond the synchronise step.