Source code for QVideo.filters.foreground

'''Foreground estimator using MOG2 Gaussian-mixture background model.'''
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
import numpy as np


__all__ = ['ForegroundEstimator', 'QForegroundEstimator']


[docs] class ForegroundEstimator(AsyncVideoFilter): '''Foreground estimator using OpenCV MOG2 background subtraction. Models each pixel as a Gaussian mixture over time to identify a persistent background, then returns each frame divided by that background. For a multiplicative image model ``I = B × F`` this recovers the foreground modulation ``F ≈ I / B``. The output is scaled by *mean* and cast to ``uint8`` so that a pixel carrying no foreground modulation (``I ≈ B``) maps to *mean*. Pixels brighter than the background map above *mean*; darker pixels map below. Parameters ---------- history : int Number of frames used to build the background model. Longer histories give a more stable estimate but adapt more slowly to illumination drift. Default: ``500``. varThreshold : float Mahalanobis-distance threshold for classifying a pixel as foreground in the MOG2 model. Smaller values make the classifier more sensitive. Default: ``16.0``. mean : float Output scale factor. A pixel where ``frame == background`` maps to this value in the output. Default: ``128.0``. Notes ----- Changing *history* or *varThreshold* resets the background model, which discards accumulated statistics and triggers a re-learning phase. ''' def __init__(self, history: int = 500, varThreshold: float = 16.0, mean: float = 128.0) -> None: self._history = max(1, int(history)) self._varThreshold = max(0.0, float(varThreshold)) self._mean = max(1.0, float(mean)) self._bgs = cv2.createBackgroundSubtractorMOG2( history=self._history, varThreshold=self._varThreshold, detectShadows=False) super().__init__() def _newBgs(self) -> None: self._bgs = cv2.createBackgroundSubtractorMOG2( history=self._history, varThreshold=self._varThreshold, detectShadows=False) @property def history(self) -> int: '''Frames used to build the background model.''' return self._history @history.setter def history(self, value: int) -> None: self._history = max(1, int(value)) self._newBgs() @property def varThreshold(self) -> float: '''Mahalanobis-distance threshold for foreground classification.''' return self._varThreshold @varThreshold.setter def varThreshold(self, value: float) -> None: self._varThreshold = max(0.0, float(value)) self._newBgs() @property def mean(self) -> float: '''Output scale factor: ratio == 1 maps to this pixel value.''' return self._mean @mean.setter def mean(self, value: float) -> None: self._mean = max(1.0, float(value))
[docs] def process(self, image: Image) -> Image: '''Divide *image* by the MOG2 background estimate. Called in the background thread. Parameters ---------- image : Image Input frame (grayscale or BGR ``uint8``). Returns ------- Image Foreground-enhanced frame scaled to ``uint8``. ''' bgs = self._bgs bgs.apply(image) bg = bgs.getBackgroundImage() bg_f = bg.astype(np.float32) result = np.zeros(image.shape, dtype=np.float32) np.divide(image, bg_f, out=result, where=(bg_f > 0)) return np.clip(self._mean * result, 0, 255).astype(np.uint8)
[docs] class QForegroundEstimator(QVideoFilter): '''Widget for :class:`ForegroundEstimator` with history and threshold spinboxes. Parameters ---------- parent : QtWidgets.QWidget or None Parent widget. ''' display_name = 'Foreground' display_category = 'Background' def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: super().__init__(parent, 'Foreground', ForegroundEstimator()) def _setupUi(self) -> None: super()._setupUi() self._historyBox = SpinBox(self, prefix='history: ', value=self.filter.history, step=100, int=True) self._historyBox.setMinimum(1) self._layout.addWidget(self._historyBox) self._thresholdBox = SpinBox(self, prefix='threshold: ', value=self.filter.varThreshold, step=1.0) self._thresholdBox.setMinimum(0.0) self._layout.addWidget(self._thresholdBox) def _connectSignals(self) -> None: super()._connectSignals() self._historyBox.valueChanged.connect(self._setHistory) self._thresholdBox.valueChanged.connect(self._setThreshold) @QtCore.Slot(object) def _setHistory(self, value: int) -> None: self.filter.history = value with QtCore.QSignalBlocker(self._historyBox): self._historyBox.setValue(self.filter.history) @QtCore.Slot(object) def _setThreshold(self, value: float) -> None: self.filter.varThreshold = value
if __name__ == '__main__': # pragma: no cover QForegroundEstimator.example()