Source code for QVideo.cameras.OpenCV._devices
'''Camera and format enumeration for OpenCV-backed cameras.'''
import platform
import cv2
try:
from qtpy import QtMultimedia as _QtMultimedia
_QMediaDevices = _QtMultimedia.QMediaDevices
except (ImportError, AttributeError):
_QMediaDevices = None
__all__ = ['QOpenCVDevices', 'COMMON_RESOLUTIONS',
'probe_resolutions', 'probe_formats', 'configure']
COMMON_RESOLUTIONS: list[tuple[int, int]] = [
(160, 120),
(320, 240),
(640, 480),
(800, 600),
(1280, 720),
(1920, 1080),
(2560, 1440),
(3840, 2160),
]
[docs]
def probe_resolutions(device: cv2.VideoCapture) -> list[tuple[int, int]]:
'''Return resolutions accepted by an open OpenCV VideoCapture device.
Probes each entry in :data:`COMMON_RESOLUTIONS` by writing width and
height to the device and reading back what it actually accepted.
Restores the original resolution and frame rate when done.
Parameters
----------
device : cv2.VideoCapture
An already-open capture device.
Returns
-------
list[tuple[int, int]]
Sorted list of ``(width, height)`` pairs accepted by the device.
'''
original = (int(device.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(device.get(cv2.CAP_PROP_FRAME_HEIGHT)))
original_fps = device.get(cv2.CAP_PROP_FPS)
supported: set[tuple[int, int]] = set()
for w, h in COMMON_RESOLUTIONS:
device.set(cv2.CAP_PROP_FRAME_WIDTH, w)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, h)
actual = (int(device.get(cv2.CAP_PROP_FRAME_WIDTH)),
int(device.get(cv2.CAP_PROP_FRAME_HEIGHT)))
supported.add(actual)
device.set(cv2.CAP_PROP_FRAME_WIDTH, original[0])
device.set(cv2.CAP_PROP_FRAME_HEIGHT, original[1])
device.set(cv2.CAP_PROP_FPS, original_fps)
return sorted(supported)
[docs]
def probe_formats(device: cv2.VideoCapture,
resolutions: list[tuple[int, int]] | None = None,
) -> list[tuple[int, int, float, float]]:
'''Return ``(width, height, 1.0, max_fps)`` for each accepted resolution.
For each candidate resolution, the device is asked to deliver 120 fps
(above any real hardware limit) and the value the driver accepts is
recorded as the maximum. Restores the original resolution and frame rate
when done.
Parameters
----------
device : cv2.VideoCapture
An already-open capture device.
resolutions : list[tuple[int, int]] or None
Resolution candidates to probe. Defaults to
:data:`COMMON_RESOLUTIONS`.
Returns
-------
list[tuple[int, int, float, float]]
Sorted list of ``(width, height, 1.0, max_fps)`` tuples, one per
distinct resolution the device accepts.
'''
if resolutions is None:
resolutions = COMMON_RESOLUTIONS
original_w = int(device.get(cv2.CAP_PROP_FRAME_WIDTH))
original_h = int(device.get(cv2.CAP_PROP_FRAME_HEIGHT))
original_fps = device.get(cv2.CAP_PROP_FPS)
seen: set[tuple[int, int]] = set()
results: list[tuple[int, int, float, float]] = []
for w, h in resolutions:
device.set(cv2.CAP_PROP_FRAME_WIDTH, w)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, h)
actual_w = int(device.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(device.get(cv2.CAP_PROP_FRAME_HEIGHT))
if (actual_w, actual_h) in seen:
continue
seen.add((actual_w, actual_h))
device.set(cv2.CAP_PROP_FPS, 120.)
max_fps = device.get(cv2.CAP_PROP_FPS)
if max_fps <= 0:
max_fps = original_fps if original_fps > 0 else 30.
results.append((actual_w, actual_h, 1., max_fps))
device.set(cv2.CAP_PROP_FRAME_WIDTH, original_w)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, original_h)
device.set(cv2.CAP_PROP_FPS, original_fps)
return sorted(results)
[docs]
def configure(device: cv2.VideoCapture,
width: int | None = None,
height: int | None = None,
fps: float | None = 30.,
resolutions: list[tuple[int, int]] | None = None) -> None:
'''Configure an open OpenCV VideoCapture device.
Three modes:
- **Explicit** (both *width* and *height* given): apply those values
directly, then set *fps* if provided.
- **Quality** (default, *fps* is not ``None``): select the largest
supported resolution, then set *fps*.
- **Performance** (*fps* is ``None``): select the smallest supported
resolution, letting the driver maximize frame rate.
Parameters
----------
device : cv2.VideoCapture
An already-open capture device.
width : int or None
Desired frame width [pixels]. Must be paired with *height*
for explicit mode. ``None`` triggers auto-selection.
height : int or None
Desired frame height [pixels]. Must be paired with *width*
for explicit mode. ``None`` triggers auto-selection.
fps : float or None
Desired frame rate [fps]. ``None`` selects performance mode
(smallest resolution, driver-maximum frame rate).
Default: ``30.``.
resolutions : list[tuple[int, int]] or None
Known supported resolutions as ``(width, height)`` pairs.
When provided, skips trial-and-error probing via
:func:`probe_resolutions`. ``None`` (default) triggers probing.
'''
if width is not None and height is not None:
device.set(cv2.CAP_PROP_FRAME_WIDTH, width)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
if fps is not None:
device.set(cv2.CAP_PROP_FPS, fps)
return
if resolutions is None:
resolutions = probe_resolutions(device)
if not resolutions:
return
if fps is None:
# performance mode: smallest resolution, driver-maximum frame rate
w, h = resolutions[0]
device.set(cv2.CAP_PROP_FRAME_WIDTH, w)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, h)
return
# quality mode: largest resolution that achieves the target frame rate.
# Iterate from largest to smallest; stop at the first resolution where
# the driver confirms it can deliver at least 90 % of the target fps.
for w, h in reversed(resolutions):
device.set(cv2.CAP_PROP_FRAME_WIDTH, w)
device.set(cv2.CAP_PROP_FRAME_HEIGHT, h)
device.set(cv2.CAP_PROP_FPS, fps)
if device.get(cv2.CAP_PROP_FPS) >= fps * 0.9:
return
# fallback: all iterations set the smallest resolution last; fps is
# already requested on the device, clamped to whatever the driver allows.
[docs]
class QOpenCVDevices:
'''Camera discovery and format enumeration for OpenCV cameras.
Uses :class:`QtMultimedia.QMediaDevices` when available to enumerate
cameras and their supported formats without opening the device. Falls
back to trial-and-error probing via OpenCV when QtMultimedia is not
present.
All methods are static — this class is a namespace, not instantiated.
'''
[docs]
@staticmethod
def cameras() -> list[tuple[int, str]]:
'''Return a list of available cameras as ``(index, name)`` pairs.
The index is the integer *cameraID* suitable for passing to
:class:`~QVideo.cameras.OpenCV.QOpenCVCamera`.
Returns
-------
list[tuple[int, str]]
``(cameraID, description)`` for each detected camera, sorted
by *cameraID*.
'''
if _QMediaDevices is not None:
return [(i, dev.description())
for i, dev in enumerate(_QMediaDevices.videoInputs())]
return QOpenCVDevices._probe_cameras()
[docs]
@staticmethod
def formats(cameraID: int = 0) -> list[tuple[int, int, float, float]]:
'''Return supported formats for camera *cameraID*.
Each entry is ``(width, height, 1.0, max_fps)`` where *max_fps* is
the highest frame rate the driver actually delivers at that resolution,
determined by opening the device briefly and querying via OpenCV.
:class:`QtMultimedia.QMediaDevices` is used to obtain the resolution
list when available (it may know about non-standard resolutions that
:data:`COMMON_RESOLUTIONS` does not cover).
Frame rates from QtMultimedia are **not** used because they reflect
nominal/declared values that often differ from what the driver accepts.
Parameters
----------
cameraID : int
Camera index (same convention as OpenCV ``VideoCapture``).
Returns
-------
list[tuple[int, int, float, float]]
``(width, height, 1.0, max_fps)`` for each distinct resolution,
sorted by ``(width, height)``.
'''
qt_resolutions = None
if _QMediaDevices is not None:
device = QOpenCVDevices._find_device(
cameraID, _QMediaDevices.videoInputs())
if device is not None:
qt_resolutions = [(w, h)
for w, h, *_ in
QOpenCVDevices._formats_from_device(device)]
return QOpenCVDevices._probe_formats(cameraID, qt_resolutions)
@staticmethod
def _find_device(cameraID: int, devices) -> object | None:
'''Match *cameraID* to a :class:`~QtMultimedia.QCameraDevice`.
On Linux, ``QCameraDevice.id()`` returns a byte string containing
the V4L2 device path (e.g. ``b'/dev/video0'``), which maps directly
to the OpenCV camera index. On other platforms, the enumeration
order matches OpenCV, so the index is used directly.
Parameters
----------
cameraID : int
OpenCV camera index.
devices : sequence
List of :class:`~QtMultimedia.QCameraDevice` objects from
``QMediaDevices.videoInputs()``.
Returns
-------
QCameraDevice or None
The matching device, or ``None`` if not found.
'''
if platform.system() == 'Linux':
# Qt6/GStreamer backend reports IDs as b'v4l2:///dev/videoN';
# Qt6/V4L2 backend reports b'/dev/videoN'. Match on the suffix
# '/videoN' to handle both formats.
target_suffix = f'/video{cameraID}'.encode()
for dev in devices:
dev_id = bytes(dev.id()).rstrip(b'\x00')
if dev_id.endswith(target_suffix):
return dev
return None
# macOS: Qt6 and OpenCV both use AVFoundation, order matches.
# Windows: Qt6 uses WMF; OpenCV may use DirectShow or MSMF.
# Index correlation holds when both use the same backend but
# may fail when virtual cameras (OBS, etc.) differ.
if cameraID < len(devices):
return devices[cameraID]
return None
@staticmethod
def _formats_from_device(device) -> list[tuple[int, int, float, float]]:
'''Extract and deduplicate formats from a
:class:`~QtMultimedia.QCameraDevice`.
Multiple :class:`~QtMultimedia.QVideoFormat` entries that share the
same ``(width, height)`` but differ in pixel format are merged; the
resulting entry spans the union of their fps ranges.
Parameters
----------
device : QCameraDevice
Camera device from ``QMediaDevices.videoInputs()``.
Returns
-------
list[tuple[int, int, float, float]]
Sorted ``(width, height, min_fps, max_fps)`` for each distinct
resolution.
'''
seen: dict[tuple[int, int], tuple[float, float]] = {}
for fmt in device.videoFormats():
w = fmt.resolution().width()
h = fmt.resolution().height()
lo = fmt.minFrameRate()
hi = fmt.maxFrameRate()
if (w, h) in seen:
prev_lo, prev_hi = seen[(w, h)]
seen[(w, h)] = (min(prev_lo, lo), max(prev_hi, hi))
else:
seen[(w, h)] = (lo, hi)
return sorted((w, h, lo, hi) for (w, h), (lo, hi) in seen.items())
@staticmethod
def _probe_cameras() -> list[tuple[int, str]]:
'''Discover cameras by opening each index in turn.
Tries indices 0–9 and stops at the first one that fails to open.
Returns
-------
list[tuple[int, str]]
``(cameraID, name)`` for each detected camera.
'''
found = []
for i in range(10):
cap = cv2.VideoCapture(i)
if not cap.isOpened():
cap.release()
break
found.append((i, f'Camera {i}'))
cap.release()
return found
@staticmethod
def _probe_formats(cameraID: int,
resolutions: list[tuple[int, int]] | None = None,
) -> list[tuple[int, int, float, float]]:
'''Open *cameraID* briefly and return actual ``(w, h, 1.0, max_fps)``
entries via :func:`probe_formats`.
Parameters
----------
cameraID : int
Camera index to probe.
resolutions : list[tuple[int, int]] or None
Resolution candidates. ``None`` uses
:data:`COMMON_RESOLUTIONS`.
Returns
-------
list[tuple[int, int, float, float]]
``(width, height, 1.0, max_fps)`` for each accepted resolution.
'''
api = cv2.CAP_V4L2 if platform.system() == 'Linux' else cv2.CAP_ANY
cap = cv2.VideoCapture(cameraID, api)
if not cap.isOpened():
cap.release()
return []
result = probe_formats(cap, resolutions)
cap.release()
return result