Hi, I’m having trouble with the Arducam 2MP Stereo Camera (dual OV2311 monochrome global shutter) on a Raspberry Pi 4B. I set it up following the 2MP stereo camera global shutter MIPI doc.
Two problems:
/dev/video0 is sometimes just not there at boot — roughly 1 in 10 times. I have a hack where if /dev/video0 isn’t found I reboot the Pi and then it shows up.
The bigger one for me: setting the exposure. It works most of the time, but every 5–10 hours of use the exposure goes way too high and I have to restart. Not super common but annoying. Also, when I set exposure to 400 it sometimes doesn’t take effect at all.
One other odd thing that happened once: the images came out upside down, and a restart fixed it.
Overall it works intermittently and feels unstable. I’m mainly after a reliable way to set the exposure — any ideas?
Hi, thanks for the detailed report. A few things I want to pin down before digging in:
When /dev/video0 is missing, does it come back after a reboot? (Sounds like yes, but just confirming.)
Between the times it was missing and the times it was found, did you change anything in the hardware setup — plug/unplug the camera, reseat the ribbon, etc.? Or has the setup stayed untouched the whole time?
Can you share your environment (kernel, OS, Python/OpenCV versions), your script, and any console output from a run where it misbehaves?
Yes — after a reboot /dev/video0 comes back. No hardware changes between the failing and working cases. It’s mounted in a case so nothing gets plugged/unplugged.
Environment:
Platform: Raspberry Pi 4B
Kernel: 5.4.51-v7l+
OS: Raspbian GNU/Linux 10 (buster)
Python: 3.7.3
OpenCV: 4.5.3.56
Camera: Arducam 2MP Stereo Camera for Raspberry Pi / NVIDIA Jetson Nano / Xavier NX, dual OV2311 monochrome global shutter module
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()
The synchronise function is my attempt to force v4l2 and the camera to agree — I read the current settings, then set each one to two different values before restoring it, since setting a control to its current value is a no-op.
Thanks, the sync trick makes sense. Can you run it until it reproduces the exposure problem and paste the console output? I specifically want the image mean values around the run where the exposure doesn’t change. Also, when you say the exposure goes ‘way too high’, do you mean the image gets much brighter than expected (exposure stuck high), or darker? Just want to make sure I read the means the right way.
Here’s a run. Usually the script captures a few frames before the exposure change kicks in — you can see the mean drop partway through. Occasionally the exposure just never changes, and that’s the problem.
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.3583110576928
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
That’s helpful. Looking at it: in the normal runs the first 4 frames sit around 195, then the next 4 drop to ~94 — that drop is the exposure change actually being applied. In the run you flagged, all 8 frames stay around 195, so the exposure set never took effect at all. That lines up exactly with your description.
Can you run it again and, if you can, deliberately get the opposite failure too (the exposure going too low)? Also note whether you call capture.release() between runs — I want to see if that matters.
Ran it again, this time I accidentally left out capture.release() between the captures. Got some interesting stuff — there’s a VIDIOC_S_CTRL error, then a run where the exposure is way too low, and then the camera failed to start entirely and the script exited.
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.
Thanks, that second log is really useful. A couple of things stand out:
The VIDIOC_S_CTRL: failed: Invalid argument / gain: Invalid argument lines suggest a v4l2 control call was rejected at that point — worth knowing which control it was.
Missing capture.release() between iterations is a likely contributor to the camera-open warnings and the run where the exposure landed too low.
Two questions:
When the exposure is stuck, if you re-set it (exposure=400) does it ever recover without a restart?
What firmware are you on? There was an upgrade needed to make these cameras work on newer Pis — did you go through that, and did you match all the version numbers listed at the bottom of the setup doc?
On question 1: re-setting exposure=400 when it’s stuck doesn’t take effect — it stays wrong until I restart. But I did notice that calling capture.release() seems to fix the too-low exposure case.
On question 2: my firmware may differ from what you expect — I wasn’t sure whether I needed to do the upgrade that makes the cameras work on newer Raspberry Pis, or whether I had to match every version number at the bottom of the setup doc. I couldn’t tell from the doc whether that applied to my setup.
On the /dev/video0 thing: confirmed again that it comes back after a reboot, and there was definitely no plug/unplug in between. It’s just rare — maybe once a week.
On the /dev/video0 side, here’s what our stress testing showed:
We exercised the camera 1000 times and it kept working fine. Even after rebooting in the middle, video0 was there again after startup.
As soon as video0 is detected at startup, it doesn’t disappear again on its own.
The only failure we were able to reproduce: if the camera is plugged in mid-boot, the Pi can fail to detect the device when the camera boots up. Apart from that, we couldn’t reproduce the missing video0 at all.
So for your case — can you confirm once more that video0 reliably comes back after a reboot after it’s missing, and that the camera state was completely unchanged (no plugging, no re-seating) between the failed and the good boot? That’s the key detail I need to line up with what we see here.
Confirmed — after a reboot /dev/video0 comes back every time, and nothing in the hardware changed in between. Since it’s in a case, it’s physically untouched. Given it’s so rare and I already have the auto-reboot workaround, I’ll just leave the /dev/video0 thing as-is for now — doesn’t look like there’s a simple quick fix. The exposure instability is the one I really care about.
Understood. For the /dev/video0 side, since we can’t reproduce it outside of the plug-in-mid-boot case, the most useful next step is logs. You could add something to your code that saves the dmesg information before each reboot — that would give a good reference to diagnose the intermittent detection failure if it shows up again.
OK, I’ll add a dmesg dump right before the reboot so I catch it the next time /dev/video0 goes missing. I’ll leave the video0 issue as-is for now and focus on the exposure side.
Sure, let’s wrap up the current state so we’re on the same page:
Exposure issue: the logs line up with the sync/set sequence not applying. In the bad run, all 8 frames stayed around 195 instead of dropping to ~94, meaning the exposure set never took effect. Missing capture.release() between iterations seems tied to the too-low exposure case and the camera-open warnings. Current workaround: always capture.release() between iterations, call synchronise_camera_and_driver() before setting exposure, and if a run doesn’t apply, re-init the capture and re-set exposure rather than relying on re-sending exposure=400.
/dev/video0: we couldn’t reproduce the disappearance outside of plugging the camera in mid-boot, and stress testing (1000 uses, including reboots) worked fine. No fix yet — the plan is to capture dmesg before each reboot to get a reference next time it fails.
Nothing is fully resolved, so if the exposure misbehaves again please grab the console output around the failing run, and send the dmesg log whenever /dev/video0 drops out.