Source code for QVideo.filters.smoothing

'''Smoothing filter with selectable method and companion Qt widget.'''
from qtpy import QtCore, QtWidgets
from pyqtgraph import SpinBox
from QVideo.lib.AsyncVideoFilter import AsyncVideoFilter
from QVideo.lib.QVideoFilter import QVideoFilter
from QVideo.lib.videotypes import Image
import cv2


__all__ = ['SmoothingFilter', 'QSmoothingFilter']

[docs] class SmoothingFilter(AsyncVideoFilter): '''Smoothing filter supporting box, Gaussian, and median blur. Parameters ---------- width : int Kernel width in pixels. Must be odd and at least 1; even values are rounded up to the next odd integer. Default: ``15``. method : str Smoothing method: ``'box'``, ``'gaussian'``, or ``'median'``. Default: ``'gaussian'``. Notes ----- OpenCV requires an odd, positive kernel size for ``GaussianBlur`` and ``medianBlur``. ``blur`` (box filter) accepts any positive size, but the :attr:`width` setter enforces odd values uniformly across all methods. For Gaussian blur, ``sigma`` is set to 0, which instructs OpenCV to derive it from the kernel size. ''' METHODS = ('box', 'gaussian', 'median') def __init__(self, width: int = 15, method: str = 'gaussian') -> None: super().__init__() self.width = width self.method = method @property def width(self) -> int: '''Kernel width [pixels], always odd and at least 1.''' return self._width @width.setter def width(self, width: int) -> None: width = max(1, int(width)) self._width = width - (width % 2) + 1 @property def method(self) -> str: '''Smoothing method: ``'box'``, ``'gaussian'``, or ``'median'``.''' return self._method @method.setter def method(self, method: str) -> None: if method not in self.METHODS: raise ValueError(f'method must be one of {self.METHODS}') self._method = method
[docs] def to_code(self) -> 'FilterCode': from QVideo.lib.QVideoFilter import FilterCode k = self._width if self._method == 'box': return FilterCode( imports=frozenset({'import cv2'}), lines=[f'image = cv2.blur(image, ({k}, {k}))'], comment=f'box smoothing, k={k}', ) if self._method == 'median': return FilterCode( imports=frozenset({'import cv2'}), lines=[f'image = cv2.medianBlur(image, {k})'], comment=f'median smoothing, k={k}', ) return FilterCode( imports=frozenset({'import cv2'}), lines=[f'image = cv2.GaussianBlur(image, ({k}, {k}), 0)'], comment=f'Gaussian smoothing, k={k}', )
[docs] def process(self, image: Image) -> Image: '''Return the smoothed frame. Called in the background thread. Returns ------- Image Smoothed version of *image*. ''' if self._method == 'box': return cv2.blur(image, (self._width, self._width)) if self._method == 'median': return cv2.medianBlur(image, self._width) return cv2.GaussianBlur(image, (self._width, self._width), 0)
[docs] class QSmoothingFilter(QVideoFilter): '''Widget for :class:`SmoothingFilter` with method selector and width spinbox. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Smoothing' display_category = 'Preprocessing' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Smoothing', SmoothingFilter()) def _setupUi(self) -> None: super()._setupUi() self._methodBox = QtWidgets.QComboBox() self._methodBox.addItems(['Box', 'Gaussian', 'Median']) self._methodBox.setCurrentText(self.filter.method.capitalize()) self._layout.addWidget(self._methodBox) self._spinbox = SpinBox(self, prefix='width: ', value=self.filter.width, step=1, int=True) self._spinbox.setMinimum(3) self._layout.addWidget(self._spinbox) def _connectSignals(self) -> None: super()._connectSignals() self._methodBox.currentTextChanged.connect(self._setMethod) self._spinbox.valueChanged.connect(self._setWidth) @QtCore.Slot(str) def _setMethod(self, text: str) -> None: self.filter.method = text.lower() @QtCore.Slot(object) def _setWidth(self, width: int) -> None: self.filter.width = width with QtCore.QSignalBlocker(self._spinbox): self._spinbox.setValue(self.filter.width)
if __name__ == '__main__': # pragma: no cover QSmoothingFilter.example()