from QVideo.lib import QCamera, QVideoSource
from QVideo.cameras.OpenCV._devices import configure, probe_formats
import cv2
import platform
import logging
logger = logging.getLogger(__name__)
__all__ = ['QOpenCVCamera', 'QOpenCVSource']
_FLIP: dict[tuple[bool, bool], int] = {
(True, False): 1, # mirror only → horizontal flip
(False, True): 0, # flip only → vertical flip
(True, True): -1, # both → 180° rotation
}
# Curated set of device properties to probe at runtime.
# Maps display name → (CAP_PROP_* id, Python type).
# Width, height, and color are handled separately.
_PROBED_PROPS: dict[str, tuple[int, type]] = {
'fps': (cv2.CAP_PROP_FPS, float),
'brightness': (cv2.CAP_PROP_BRIGHTNESS, float),
'contrast': (cv2.CAP_PROP_CONTRAST, float),
'saturation': (cv2.CAP_PROP_SATURATION, float),
'hue': (cv2.CAP_PROP_HUE, float),
'gain': (cv2.CAP_PROP_GAIN, float),
'exposure': (cv2.CAP_PROP_EXPOSURE, float),
'sharpness': (cv2.CAP_PROP_SHARPNESS, float),
'gamma': (cv2.CAP_PROP_GAMMA, float),
'backlight': (cv2.CAP_PROP_BACKLIGHT, float),
'focus': (cv2.CAP_PROP_FOCUS, float),
'zoom': (cv2.CAP_PROP_ZOOM, float),
}
[docs]
class QOpenCVCamera(QCamera):
'''Camera backed by OpenCV's ``VideoCapture``.
Supports USB webcams and any device accessible via OpenCV.
On Linux the V4L2 backend is selected automatically; all other
platforms use ``CAP_ANY``.
Resolution and frame rate are configured once at device open time
via :func:`~QVideo.cameras.OpenCV._devices.configure`. Three modes are
supported:
- **Quality** (default): probes the device, selects the largest
supported resolution, and sets *fps* (default 30 fps).
- **Performance** (*fps* ``= None``): probes the device, selects the
smallest supported resolution, and lets the driver maximize frame
rate (slo-mo mode).
- **Explicit** (*width* and *height* both given): applies the
requested dimensions and *fps* directly.
``width`` and ``height`` are registered as writable properties.
Changes are applied to the open device via OpenCV's V4L2 backend, which
handles the required ``VIDIOC_STREAMOFF`` / ``VIDIOC_S_FMT`` /
``VIDIOC_STREAMON`` cycle internally. The camera mutex serialises these
writes against concurrent reads, so no source stop/restart is needed.
:class:`~QVideo.cameras.OpenCV.QOpenCVTree.QOpenCVTree` exposes them as
read-only display fields; the ``resolution`` enum in that tree changes
width, height, and fps together.
Transform properties (``mirrored``, ``flipped``) are registered
immediately on construction. Device properties (``color``, and any
properties in :data:`_PROBED_PROPS` that the device supports) are
registered inside :meth:`_initialize` once the capture device is
open.
Parameters
----------
cameraID : int
Index of the camera device to open. Default: ``0``.
mirrored : bool
Flip the image horizontally. Default: ``False``.
flipped : bool
Flip the image vertically. Default: ``False``.
gray : bool
Initial grayscale state. Equivalent to opening with ``color=False``.
Default: ``False`` (color output).
width : int or None
Desired frame width [pixels]. Must be paired with *height* for
explicit mode. ``None`` triggers auto-selection. Default: ``None``.
height : int or None
Desired frame height [pixels]. Must be paired with *width* for
explicit mode. ``None`` triggers auto-selection. Default: ``None``.
fps : float or None
Desired frame rate [fps]. ``None`` selects performance mode.
Default: ``30.``.
*args :
Forwarded to :class:`~QVideo.lib.QCamera`.
**kwargs :
Forwarded to :class:`~QVideo.lib.QCamera`.
'''
WIDTH = cv2.CAP_PROP_FRAME_WIDTH
HEIGHT = cv2.CAP_PROP_FRAME_HEIGHT
FPS = cv2.CAP_PROP_FPS
BGR2RGB = cv2.COLOR_BGR2RGB
BGR2GRAY = cv2.COLOR_BGR2GRAY
def __init__(self, *args,
cameraID: int = 0,
mirrored: bool = False,
flipped: bool = False,
gray: bool = False,
width: int | None = None,
height: int | None = None,
fps: float | None = 30.,
**kwargs) -> None:
super().__init__(*args, **kwargs)
self._cameraID = cameraID
self._mirrored = bool(mirrored)
self._flipped = bool(flipped)
self._gray = bool(gray)
self._configWidth = width
self._configHeight = height
self._configFps = fps
self.registerProperty('mirrored', ptype=bool)
self.registerProperty('flipped', ptype=bool)
self.open()
def _initialize(self) -> bool:
'''Open the OpenCV VideoCapture device and register device properties.
Configures resolution and frame rate via
:func:`~QVideo.cameras.OpenCV._devices.configure` using the values
supplied at construction time. Registers ``width`` and ``height`` as
writable properties, ``color`` as read-write, then probes the
device for each property in :data:`_PROBED_PROPS` and registers
those it supports.
Returns
-------
bool
``True`` if the device was opened and returned at least one frame.
'''
api = cv2.CAP_V4L2 if platform.system() == 'Linux' else cv2.CAP_ANY
self._device = cv2.VideoCapture(self._cameraID, api)
# Probe supported resolutions and their actual maximum frame rates on
# the live device before configuring. QtMultimedia nominal fps values
# are unreliable; reading back what the driver accepts is accurate.
self._formats = probe_formats(self._device)
self._formatLabels = {
f'{w}×{h} @ {fps:.0f} Hz': (w, h, float(fps))
for w, h, _min, fps in self._formats
}
resolutions = [(w, h) for w, h, *_ in self._formats] or None
configure(self._device, self._configWidth, self._configHeight,
self._configFps, resolutions=resolutions)
for _ in range(5):
if (ready := self._device.read()[0]):
break
if ready:
if self._formatLabels:
self.registerProperty(
'resolution',
getter=self._getResolution,
setter=self._setResolution,
ptype=str,
limits=list(self._formatLabels.keys()),
)
self.registerProperty('width', getter=self._getWidth,
setter=self._setWidth, ptype=int)
self.registerProperty('height', getter=self._getHeight,
setter=self._setHeight, ptype=int)
self.registerProperty('color', getter=self._getColor,
setter=self._setColor, ptype=bool)
self._probeProperties()
if 'resolution' in self._properties:
for name in ('width', 'height', 'fps'):
if name in self._properties:
self._properties[name]['hidden'] = True
self.shapeChanged.emit(self.shape)
else:
self._device.release()
return ready
def _probeProperties(self) -> None:
'''Register device properties that the camera actually supports.
For each entry in :data:`_PROBED_PROPS`, attempts to set the
property to its current value. If the device accepts the write,
the property is registered as read-write; otherwise it is skipped.
'''
registered = []
for name, (prop_id, ptype) in _PROBED_PROPS.items():
value = self._device.get(prop_id)
if self._device.set(prop_id, value):
setter = (self._setFps if name == 'fps'
else lambda v, p=prop_id: self._device.set(p, v))
self.registerProperty(
name,
getter=lambda p=prop_id, t=ptype: t(self._device.get(p)),
setter=setter,
ptype=ptype)
registered.append(name)
else:
logger.debug(f'Property {name!r} not supported by this device')
logger.debug(
f'Registered {len(registered)} device properties: {registered}')
def _deinitialize(self) -> None:
'''Release the OpenCV VideoCapture device.'''
self._device.release()
def _getResolution(self) -> str:
w, h = self._getWidth(), self._getHeight()
for label, (lw, lh, _) in self._formatLabels.items():
if lw == w and lh == h:
return label
return next(iter(self._formatLabels))
def _setResolution(self, label: str) -> None:
w, h, fps = self._formatLabels[label]
self._setWidth(w)
self._setHeight(h)
self._setFps(fps)
def _getWidth(self) -> int:
return int(self._device.get(self.WIDTH))
def _setWidth(self, value: int) -> None:
'''Set the frame width on the open device.
Stores the value for the next :meth:`_initialize` and writes it to
the device immediately. OpenCV's V4L2 backend handles the required
``VIDIOC_STREAMOFF`` / ``VIDIOC_S_FMT`` / ``VIDIOC_STREAMON`` cycle
internally, so no device close/reopen is needed. The caller must not
be holding :attr:`~QVideo.lib.QCamera.QCamera.mutex` — use
:meth:`~QVideo.lib.QCamera.QCamera.set` which acquires it.
'''
self._configWidth = int(value)
self._device.set(self.WIDTH, self._configWidth)
self.shapeChanged.emit(self.shape)
def _getHeight(self) -> int:
return int(self._device.get(self.HEIGHT))
def _setHeight(self, value: int) -> None:
'''Set the frame height on the open device.
See :meth:`_setWidth` for rationale.
'''
self._configHeight = int(value)
self._device.set(self.HEIGHT, self._configHeight)
self.shapeChanged.emit(self.shape)
def _setFps(self, value: float) -> None:
'''Store the requested frame rate and apply it to the open device.
Unlike width/height, frame-rate changes in V4L2 do not require
stopping the stream, so the value is written to the device
immediately as well as stored for the next :meth:`_initialize`.
'''
self._configFps = float(value)
self._device.set(self.FPS, self._configFps)
def _getColor(self) -> bool:
'''Return color mode
``True`` if frames are delivered in color
``False`` for grayscale.'''
return not self._gray
def _setColor(self, value: bool) -> None:
'''Set color mode.
``False`` converts frames to grayscale
``True`` restores color.'''
self._gray = not bool(value)
[docs]
def read(self) -> QCamera.CameraData:
'''Read one frame from the camera.
Applies color conversion and geometric transforms according to
the current ``color``, ``mirrored``, and ``flipped`` settings.
Returns
-------
tuple[bool, ndarray or None]
``(True, frame)`` on success, ``(False, None)`` when closed.
'''
if self.isOpen():
try:
ready, image = self._device.read()
except Exception as e:
logger.warning(f'Frame read failed: {e}')
return False, None
else:
return False, None
if ready:
if image.ndim == 3:
code = self.BGR2GRAY if self._gray else self.BGR2RGB
image = cv2.cvtColor(image, code)
if self._flipped or self._mirrored:
operation = _FLIP[(self._mirrored, self._flipped)]
image = cv2.flip(image, operation)
return ready, image
[docs]
class QOpenCVSource(QVideoSource):
'''Threaded video source backed by :class:`QOpenCVCamera`.
Parameters
----------
camera : QOpenCVCamera or None
Camera instance to wrap. If ``None``, a new :class:`QOpenCVCamera`
is created from the remaining arguments.
*args :
Forwarded to :class:`QOpenCVCamera` when ``camera`` is ``None``.
**kwargs :
Forwarded to :class:`QOpenCVCamera` when ``camera`` is ``None``.
'''
def __init__(self, *args,
camera: QOpenCVCamera | None = None,
**kwargs) -> None:
if camera is None:
camera = QOpenCVCamera(*args, **kwargs)
super().__init__(camera)
if __name__ == '__main__': # pragma: no cover
QOpenCVCamera.example()