Source code for QVideo.filters.momean

'''Exponential moving-average background estimator 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__ = ['MoMean', 'QMoMean']


[docs] class MoMean(VideoFilter): '''Exponential moving-average (EMA) background estimator. Maintains a per-pixel running average that weights each incoming frame by *alpha* and the existing estimate by *(1 − alpha)*: .. math:: \\hat{B}_t = \\alpha\\,I_t + (1 - \\alpha)\\,\\hat{B}_{t-1} where :math:`I_t` is the current frame and :math:`\\hat{B}` is the background estimate. A small *alpha* produces a slow-responding, heavily smoothed estimate; *alpha = 1* reduces to a passthrough. The effective time constant in frames is approximately :math:`1 / \\alpha`. Parameters ---------- alpha : float EMA weight on the incoming frame. Clamped to ``(0, 1]``. Default: ``0.1``. ''' def __init__(self, alpha: float = 0.1) -> None: super().__init__() self.alpha = alpha self._acc: np.ndarray | None = None @property def alpha(self) -> float: '''EMA weight on the incoming frame, in ``(0, 1]``.''' return self._alpha @alpha.setter def alpha(self, value: float) -> None: self._alpha = float(np.clip(value, 1e-6, 1.0))
[docs] def reset(self) -> None: '''Clear the accumulator and restart the estimator.''' self._acc = None self.data = None
[docs] def add(self, data: Image) -> None: '''Incorporate a new frame into the running average. Parameters ---------- data : Image Input frame. If the shape differs from the previous frame the accumulator is re-initialised. ''' if self._acc is None or data.shape != self._acc.shape[:len(data.shape)]: self._acc = data.astype(np.float32) else: cv2.accumulateWeighted(data, self._acc, self._alpha) self.data = data
[docs] def get(self) -> Image | None: '''Return the current background estimate. Returns ------- Image or None uint8 estimate, or ``None`` if no frames have been added. ''' if self._acc is None: return None return np.clip(self._acc, 0, 255).astype(np.uint8)
[docs] class QMoMean(QVideoFilter): '''Widget for :class:`MoMean` with an alpha spinbox. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Running Mean' display_category = 'Background' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Running Mean', MoMean()) def _setupUi(self) -> None: super()._setupUi() self._alphaBox = SpinBox(value=self.filter.alpha, bounds=(1e-6, 1.0), step=0.05, prefix='α ') self._layout.addWidget(self._alphaBox) def _connectSignals(self) -> None: super()._connectSignals() self._alphaBox.valueChanged.connect(self._setAlpha) @QtCore.Slot(object) def _setAlpha(self, value: float) -> None: self.filter.alpha = value
if __name__ == '__main__': # pragma: no cover QMoMean.example()