Source code for QVideo.cameras.OpenCV._camera

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()