Source code for QVideo.lib.QVideoFilter

'''Base classes for image-processing filters in the QVideo filter pipeline.'''
from __future__ import annotations
import dataclasses
from qtpy import QtCore, QtWidgets
from QVideo.lib.videotypes import Image
import pyqtgraph as pg


__all__ = ['FilterCode', 'VideoFilter', 'QVideoFilter']


[docs] @dataclasses.dataclass class FilterCode: '''Code fragment emitted by a filter's :meth:`~VideoFilter.to_code` method. Attributes ---------- imports : frozenset[str] Complete import lines required by *lines*, e.g. ``'import cv2'``. lines : list[str] Source lines (no leading indentation) that implement one filter step. The variable ``image`` holds the current frame on entry and must hold the result on exit. comment : str Optional one-line description included as a comment above the generated code block. ''' imports: frozenset[str] lines: list[str] comment: str = ''
[docs] class VideoFilter(QtCore.QObject): '''Base class for video filters. Provides a two-stage ``add``/``get`` interface so that subclasses can accumulate state across frames (e.g. running averages) before returning a result. The default implementation is a passthrough: ``add`` stores the frame and ``get`` returns it unchanged. The :meth:`__call__` operator chains :meth:`add` and :meth:`get` so that filters can be used as plain callables. ''' def __init__(self) -> None: super().__init__() self.data: Image | None = None def __call__(self, data: Image) -> Image | None: '''Apply the filter to *data* and return the result. Parameters ---------- data : Image Input frame. Returns ------- Image or None Filtered frame, or ``None`` if no result is available yet. ''' self.add(data) return self.get()
[docs] def add(self, data: Image) -> None: '''Incorporate a new frame into the filter state. Parameters ---------- data : Image Input frame. ''' self.data = data
[docs] def get(self) -> Image | None: '''Return the current filter output. Returns ------- Image Filtered frame. Raises ------ RuntimeError If called before the first :meth:`add`. ''' if self.data is None: raise RuntimeError('get() called before add()') return self.data
[docs] def shutdown(self) -> None: '''Release background resources held by this filter. Called by the pipeline when the filter is removed. The default is a no-op; subclasses that own background threads override this. '''
[docs] def to_code(self) -> FilterCode | None: '''Return a :class:`FilterCode` fragment for pipeline export. Stateless filters implement this to enable :meth:`~QVideo.lib.QFilterRack.QFilterRack.exportPipeline`. Stateful filters (those that accumulate state across frames) return ``None`` to indicate they cannot be expressed as a single-frame pure function. Returns ------- FilterCode or None Code fragment, or ``None`` if export is not supported. ''' return None
[docs] class QVideoFilter(QtWidgets.QGroupBox): '''Widget wrapper for a :class:`VideoFilter` with an enable checkbox. Displays the filter as a checkable :class:`~pyqtgraph.Qt.QtWidgets.QGroupBox`. When checked the filter is applied to incoming frames; when unchecked frames pass through unchanged. Subclasses can extend the UI by overriding :meth:`_setupUi`. The override should call ``super()._setupUi()`` first, then add widgets to ``self._layout``. Subclasses should set :attr:`display_name` to a short human-readable label. :class:`~QVideo.lib.QFilterRack.QFilterRack` uses this to populate the "Add filter…" picker. Parameters ---------- parent : QtWidgets.QWidget Parent widget. title : str Label displayed in the group box border. videoFilter : VideoFilter The filter to apply when enabled. Class Attributes ---------------- display_name : str Human-readable name shown in the filter picker. Empty string means the filter will not appear in the picker. display_category : str Category label used to group filters in the picker tree. Defaults to ``'Other'`` in the picker when empty. ''' display_name: str = '' display_category: str = '' def __init__(self, parent: QtWidgets.QWidget | None, title: str, videoFilter: VideoFilter) -> None: super().__init__(title, parent) self._filter = videoFilter self._setupUi() self._connectSignals() @property def filter(self) -> VideoFilter: '''The :class:`VideoFilter` applied when this widget is enabled.''' return self._filter @filter.setter def filter(self, videoFilter: VideoFilter) -> None: if not isinstance(videoFilter, VideoFilter): raise TypeError( f'expected VideoFilter, got {type(videoFilter).__name__}') self._filter = videoFilter def __call__(self, image: Image) -> Image | None: '''Apply the filter if enabled, otherwise return *image* unchanged. Parameters ---------- image : Image Input frame. Returns ------- Image or None Filtered frame if checked, otherwise *image* unchanged. ''' return self.filter(image) if self.isChecked() else image def _setupUi(self) -> None: '''Configure the group box and create the horizontal layout. Subclasses should call ``super()._setupUi()`` and then add their own widgets to ``self._layout``. ''' self.setCheckable(True) self.setChecked(False) self.setFlat(True) self._layout = QtWidgets.QHBoxLayout(self) self._layout.setContentsMargins(2, 5, 2, 5) def _connectSignals(self) -> None: '''Connect signals for UI elements Subclasses should call ``super()._connectSignals()`` and then connect their own signals. '''
[docs] @classmethod def example(cls: type['QVideoFilter']) -> None: # pragma: no cover '''Demonstrate the filter widget. Intended to be called on a concrete subclass that supplies its own ``__init__`` defaults, not on :class:`QVideoFilter` directly. ''' pg.mkQApp() widget = cls() widget.show() pg.exec()