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