Source code for QVideo.filters.samplehold

'''Sample-and-hold background normalization filter and companion Qt widget.'''
from qtpy import QtCore, QtWidgets
from QVideo.filters.median import Median
from QVideo.filters.normalize import Normalize
from QVideo.lib.QVideoFilter import QVideoFilter
from QVideo.lib.videotypes import Image
import numpy as np


__all__ = ['SampleHold', 'QSampleHold']


[docs] class SampleHold(Normalize): _sub_type = Median '''Normalize an image against a sampled background estimate. Accumulates ``3 ** order`` frames into the running-median background estimator inherited from :class:`~QVideo.filters.normalize.Normalize`, then holds that estimate fixed. Subsequent frames are normalized against the held background. Calling :meth:`reset` restarts the accumulation, allowing the background to be refreshed on demand. When the frame shape changes the accumulation is automatically restarted. Parameters ---------- *args : Positional arguments forwarded to :class:`Normalize`. **kwargs : Keyword arguments forwarded to :class:`Normalize` (``order``, ``scale``, ``mean``, ``darkcount``). Notes ----- This filter is designed to be used via :class:`QSampleHold`, which provides a *Reset* button to trigger :meth:`reset` interactively. ''' def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.reset()
[docs] def reset(self) -> None: '''Restart background accumulation. Resets the frame counter so that the next ``3 ** order`` frames are used to build a fresh background estimate. ''' self._count = 3 ** self.order
[docs] def get(self) -> Image | None: '''Return the current filter output. While the background is still being accumulated, returns the raw (dark-count-corrected) frame so the display stays live. Once accumulation is complete, returns the normalized frame. Returns ------- Image or None Raw frame during accumulation, normalized frame afterwards, or ``None`` if called before the first :meth:`add`. ''' if self._count > 0: if self._fg is None: return None return np.clip(self._fg + self.darkcount, 0, 255).astype(np.uint8) return super().get()
[docs] def add(self, image: Image) -> None: '''Incorporate a new frame into the filter state. While the frame counter is positive the frame is passed to the parent :class:`Normalize` accumulator to build the background estimate. Once the counter reaches zero the frame is stored as the foreground (normalized against the held background). If the frame shape changes :meth:`reset` is called automatically. Parameters ---------- image : Image Input frame. ''' if image.shape != self.shape: self.reset() if self._count > 0: super().add(image) self._count -= 1 else: self._fg = image - self.darkcount
[docs] class QSampleHold(QVideoFilter): '''Widget for :class:`SampleHold` with order buttons and a *Reset* button. Wraps :class:`SampleHold` in a checkable group box. Three radio buttons select the accumulation order (1, 2, or 3), corresponding to background estimates built from 3, 9, or 27 frames respectively. The *Reset* button triggers :meth:`~SampleHold.reset`, causing the filter to re-sample the background using the selected order. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Sample and Hold' display_category = 'Background' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Sample and Hold', SampleHold()) def _setupUi(self) -> None: super()._setupUi() self._layout.addWidget(QtWidgets.QLabel('order')) self._buttons = [] for order in (1, 2, 3): button = QtWidgets.QRadioButton(str(order)) self._layout.addWidget(button) self._buttons.append(button) self._buttons[self.filter.order - 1].setChecked(True) self._resetButton = QtWidgets.QPushButton('Reset', self) self._layout.addWidget(self._resetButton) def _connectSignals(self) -> None: super()._connectSignals() self._resetButton.clicked.connect(self.reset) for button in self._buttons: button.toggled.connect(lambda checked, b=button: self.setOrder(checked, int(b.text())))
[docs] @QtCore.Slot(bool, int) def setOrder(self, checked: bool, order: int) -> None: '''Set the accumulation order and restart background sampling. Only acts when *checked* is ``True`` to avoid double-firing on radio button deselection. Setting a new order clears the estimator and restarts accumulation immediately. Parameters ---------- checked : bool Whether the button is being selected (``True``) or deselected (``False``). order : int Accumulation order (1, 2, or 3). ''' if checked: self.filter.order = order self.filter.reset()
[docs] @QtCore.Slot(bool) def reset(self, _checked: bool = False) -> None: '''Reset the background estimate. Connected to the *Reset* button. The *_checked* argument is the toggle state emitted by :class:`~pyqtgraph.Qt.QtWidgets.QPushButton` and is ignored. Parameters ---------- _checked : bool Unused toggle state from the button signal. ''' self.filter.reset()
if __name__ == '__main__': # pragma: no cover QSampleHold.example()