Source code for QVideo.filters.sobel

'''Sobel gradient edge-detection filter and companion Qt widget.'''
from qtpy import QtCore, QtWidgets
from pyqtgraph import SpinBox
from QVideo.lib.QVideoFilter import VideoFilter, QVideoFilter
from QVideo.lib.videotypes import Image
import numpy as np
import cv2

__all__ = ['SobelFilter', 'QSobelFilter']


[docs] class SobelFilter(VideoFilter): '''Sobel edge detector with horizontal, vertical, and magnitude modes. Converts each frame to grayscale, then applies the Sobel operator. - **Horizontal**: first-order x-derivative (∂/∂x), converted to uint8. - **Vertical**: first-order y-derivative (∂/∂y), converted to uint8. - **Magnitude**: Euclidean magnitude of the (∂/∂x, ∂/∂y) gradient, clipped to [0, 255]. Parameters ---------- direction : str One of ``'Horizontal'``, ``'Vertical'``, ``'Magnitude'``. Default: ``'Magnitude'``. ksize : int Sobel kernel size [pixels]. Must be odd and in {1, 3, 5, 7}. Default: ``3``. ''' DIRECTIONS = ('Horizontal', 'Vertical', 'Magnitude') def __init__(self, direction: str = 'Magnitude', ksize: int = 3) -> None: super().__init__() self.direction = direction self.ksize = ksize @property def direction(self) -> str: '''Gradient mode; one of :attr:`DIRECTIONS`.''' return self._direction @direction.setter def direction(self, value: str) -> None: if value not in self.DIRECTIONS: raise ValueError(f'unknown direction: {value!r}') self._direction = value @property def ksize(self) -> int: '''Sobel kernel size [pixels], always odd and in {1, 3, 5, 7}.''' return self._ksize @ksize.setter def ksize(self, value: int) -> None: value = max(1, min(7, int(value))) self._ksize = value + (1 - value % 2)
[docs] def to_code(self) -> 'FilterCode': from QVideo.lib.QVideoFilter import FilterCode _GRAY = [ 'if image.ndim == 3:', ' image = image.mean(axis=2).astype(np.uint8)', ] imports = frozenset({'import cv2', 'import numpy as np'}) k = self._ksize if self._direction == 'Horizontal': return FilterCode( imports=imports, lines=_GRAY + [ f'image = cv2.convertScaleAbs(cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize={k}))', ], comment=f'Sobel horizontal, k={k}', ) if self._direction == 'Vertical': return FilterCode( imports=imports, lines=_GRAY + [ f'image = cv2.convertScaleAbs(cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize={k}))', ], comment=f'Sobel vertical, k={k}', ) return FilterCode( imports=imports, lines=_GRAY + [ f'_gx = cv2.Sobel(image, cv2.CV_32F, 1, 0, ksize={k})', f'_gy = cv2.Sobel(image, cv2.CV_32F, 0, 1, ksize={k})', 'image = np.clip(np.hypot(_gx, _gy), 0, 255).astype(np.uint8)', ], comment=f'Sobel magnitude, k={k}', )
[docs] def get(self) -> Image | None: '''Return the Sobel edge map of the stored frame. Returns ------- Image or None uint8 edge map, or ``None`` if no frame has been added. ''' if self.data is None: return None gray = (self.data.mean(axis=2).astype(np.uint8) if self.data.ndim == 3 else self.data) if self._direction == 'Horizontal': result = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=self._ksize) return cv2.convertScaleAbs(result) if self._direction == 'Vertical': result = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=self._ksize) return cv2.convertScaleAbs(result) gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=self._ksize) gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=self._ksize) return np.clip(np.hypot(gx, gy), 0, 255).astype(np.uint8)
[docs] class QSobelFilter(QVideoFilter): '''Widget for :class:`SobelFilter` with direction selector and kernel size spinbox. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Sobel' display_category = 'Edge Detection' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Sobel Edge Detection', SobelFilter()) def _setupUi(self) -> None: super()._setupUi() self._dirBox = QtWidgets.QComboBox() self._dirBox.addItems(SobelFilter.DIRECTIONS) self._dirBox.setCurrentText(self.filter.direction) self._layout.addWidget(self._dirBox) self._ksizeBox = SpinBox(value=self.filter.ksize, bounds=(1, 7), step=2, int=True, prefix='k ') self._layout.addWidget(self._ksizeBox) def _connectSignals(self) -> None: super()._connectSignals() self._dirBox.currentTextChanged.connect(self._setDirection) self._ksizeBox.valueChanged.connect(self._setKsize) @QtCore.Slot(str) def _setDirection(self, direction: str) -> None: self.filter.direction = direction @QtCore.Slot(object) def _setKsize(self, value: int) -> None: self.filter.ksize = value with QtCore.QSignalBlocker(self._ksizeBox): self._ksizeBox.setValue(self.filter.ksize)
if __name__ == '__main__': # pragma: no cover QSobelFilter.example()