Source code for QVideo.filters.threshold

'''Binary threshold filter with global, Otsu, and adaptive methods.'''
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__ = ['ThresholdFilter', 'QThresholdFilter']


[docs] class ThresholdFilter(VideoFilter): '''Binary threshold filter supporting global, Otsu, and adaptive methods. Converts each frame to a binary image using one of four methods: - **Global**: pixels above *threshold* become 255, others become 0. - **Otsu**: threshold level chosen automatically to minimise intra-class variance (Otsu 1979). The *threshold* parameter is ignored. - **Adaptive Mean**: threshold at each pixel is the mean of the *block_size* × *block_size* neighbourhood minus *C*. - **Adaptive Gaussian**: threshold at each pixel is the Gaussian-weighted mean of the neighbourhood minus *C*. Colour input is converted to grayscale before thresholding. Parameters ---------- threshold : int Global threshold value [0, 255]. Default: ``127``. method : str One of ``'Global'``, ``'Otsu'``, ``'Adaptive Mean'``, ``'Adaptive Gaussian'``. Default: ``'Global'``. block_size : int Neighbourhood size for adaptive methods [pixels]. Must be odd and ≥ 3. Default: ``11``. C : int Constant subtracted from the adaptive mean. Default: ``2``. ''' METHODS = ('Global', 'Otsu', 'Adaptive Mean', 'Adaptive Gaussian') def __init__(self, threshold: int = 127, method: str = 'Global', block_size: int = 11, C: int = 2) -> None: super().__init__() self.threshold = threshold self.method = method self.block_size = block_size self.C = C @property def threshold(self) -> int: '''Global threshold value [0, 255], clamped on assignment.''' return self._threshold @threshold.setter def threshold(self, value: int) -> None: self._threshold = int(np.clip(value, 0, 255)) @property def method(self) -> str: '''Thresholding method; one of :attr:`METHODS`.''' return self._method @method.setter def method(self, method: str) -> None: if method not in self.METHODS: raise ValueError(f'unknown method: {method!r}') self._method = method @property def block_size(self) -> int: '''Adaptive neighbourhood size [pixels], always odd and ≥ 3.''' return self._block_size @block_size.setter def block_size(self, size: int) -> None: size = max(3, int(size)) self._block_size = size + (1 - size % 2) @property def C(self) -> int: '''Constant subtracted from the adaptive neighbourhood mean.''' return self._C @C.setter def C(self, value: int) -> None: self._C = int(value)
[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'}) if self._method == 'Global': return FilterCode( imports=imports, lines=_GRAY + [ f'_, image = cv2.threshold(image, {self._threshold}, 255, cv2.THRESH_BINARY)', ], comment=f'global threshold, level={self._threshold}', ) if self._method == 'Otsu': return FilterCode( imports=imports, lines=_GRAY + [ 'image = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]', ], comment='Otsu threshold', ) if self._method == 'Adaptive Mean': return FilterCode( imports=imports, lines=_GRAY + [ f'image = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_MEAN_C, ' f'cv2.THRESH_BINARY, {self._block_size}, {self._C})', ], comment=f'adaptive mean threshold, block={self._block_size}, C={self._C}', ) return FilterCode( imports=imports, lines=_GRAY + [ f'image = cv2.adaptiveThreshold(image, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, ' f'cv2.THRESH_BINARY, {self._block_size}, {self._C})', ], comment=f'adaptive Gaussian threshold, block={self._block_size}, C={self._C}', )
[docs] def get(self) -> Image | None: '''Return the thresholded frame. Returns ------- Image or None Binary uint8 image, 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._method == 'Global': _, result = cv2.threshold( gray, self._threshold, 255, cv2.THRESH_BINARY) elif self._method == 'Otsu': _, result = cv2.threshold( gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU) elif self._method == 'Adaptive Mean': result = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, self._block_size, self._C) else: result = cv2.adaptiveThreshold( gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, self._block_size, self._C) return result
[docs] class QThresholdFilter(QVideoFilter): '''Widget for :class:`ThresholdFilter` with method selector and context-sensitive parameter spinboxes. Shows a method combobox and, depending on the selected method: - **Global**: a *level* spinbox [0, 255]. - **Otsu**: no extra controls (threshold is computed automatically). - **Adaptive Mean / Adaptive Gaussian**: *block* (neighbourhood size) and *C* (subtracted constant) spinboxes. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Threshold' display_category = 'Segmentation' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Threshold', ThresholdFilter()) def _setupUi(self) -> None: super()._setupUi() self._methodBox = QtWidgets.QComboBox() self._methodBox.addItems(ThresholdFilter.METHODS) self._layout.addWidget(self._methodBox) self._levelBox = SpinBox(value=self.filter.threshold, bounds=(0, 255), int=True, prefix='level ') self._layout.addWidget(self._levelBox) self._blockBox = SpinBox(value=self.filter.block_size, bounds=(3, None), step=2, int=True, prefix='block ') self._layout.addWidget(self._blockBox) self._cBox = SpinBox(value=self.filter.C, int=True, prefix='C ') self._layout.addWidget(self._cBox) self._blockBox.setVisible(False) self._cBox.setVisible(False) def _connectSignals(self) -> None: super()._connectSignals() self._methodBox.currentTextChanged.connect(self._setMethod) self._levelBox.valueChanged.connect(self._setLevel) self._blockBox.valueChanged.connect(self._setBlockSize) self._cBox.valueChanged.connect(self._setC) @QtCore.Slot(str) def _setMethod(self, method: str) -> None: self.filter.method = method is_global = method == 'Global' is_adaptive = method.startswith('Adaptive') self._levelBox.setVisible(is_global) self._blockBox.setVisible(is_adaptive) self._cBox.setVisible(is_adaptive) @QtCore.Slot(object) def _setLevel(self, value: int) -> None: self.filter.threshold = value with QtCore.QSignalBlocker(self._levelBox): self._levelBox.setValue(self.filter.threshold) @QtCore.Slot(object) def _setBlockSize(self, value: int) -> None: self.filter.block_size = value with QtCore.QSignalBlocker(self._blockBox): self._blockBox.setValue(self.filter.block_size) @QtCore.Slot(object) def _setC(self, value: int) -> None: self.filter.C = value
if __name__ == '__main__': # pragma: no cover QThresholdFilter.example()