Source code for QVideo.filters.momedian
'''Streaming median-of-medians background estimator (rolling remedian).'''
from qtpy import QtCore, QtWidgets
from pyqtgraph import SpinBox
from QVideo.filters._MedianBase import _MedianBase
from QVideo.lib.QVideoFilter import QVideoFilter
from QVideo.lib.videotypes import Image
import numpy as np
__all__ = ['MoMedian', 'QMoMedian']
[docs]
class MoMedian(_MedianBase):
'''Streaming median-of-medians background estimator.
Computes a running pixel-wise median over ``3 ** order`` frames
using a rolling two-frame buffer. Unlike :class:`Median`, a new
estimate is produced on *every* frame (not every third), at the
cost of a slightly less accurate result for small ``order``.
Parameters
----------
order : int
Recursion depth. The estimate draws from ``3 ** order``
frames. Default: ``1`` (median of 3 frames).
data : Image or None
Optional seed frame used to pre-allocate internal buffers.
If ``None`` the buffers are allocated on the first call to
:meth:`add`. Default: ``None``.
Notes
-----
:class:`MoMedian` is a rolling variant of the remedian [R90]_: rather than
waiting for a complete triplet, it uses the two most recently stored
frames together with the current frame to produce a new estimate on
every call. This reduces latency at the cost of slight accuracy loss
relative to the strict remedian.
References
----------
.. [R90] P.J. Rousseeuw and G.W. Bassett Jr., "The remedian: a robust
averaging method for large data sets", *Journal of the American
Statistical Association*, 85(409):97–104, 1990.
`doi:10.1080/01621459.1990.10475311 <https://doi.org/10.1080/01621459.1990.10475311>`_
'''
[docs]
def add(self, data: Image) -> None:
'''Incorporate a new frame into the median estimate.
Produces a new estimate on every call using a rolling
two-frame buffer.
Parameters
----------
data : Image
Input frame. If the shape differs from the previously seen
shape, the internal buffers are reallocated.
'''
if data.shape != self.shape:
self._initialize(data)
return
if self._order > 1:
data = self._next(data)
a = self._buffer[0]
b = self._buffer[1]
self._result = np.maximum(np.minimum(a, b),
np.minimum(np.maximum(a, b), data))
self._buffer[self._index] = data
self._index = (self._index + 1) % 2
[docs]
class QMoMedian(QVideoFilter):
'''Widget for :class:`MoMedian` with an order spinbox.
Parameters
----------
parent : QtWidgets.QWidget or None
Parent widget.
'''
display_name = 'Running Median'
display_category = 'Background'
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
super().__init__(parent, 'Running Median', MoMedian())
def _setupUi(self) -> None:
super()._setupUi()
self._orderBox = SpinBox(value=self.filter.order,
bounds=(1, 4), int=True, step=1,
prefix='order ')
self._layout.addWidget(self._orderBox)
def _connectSignals(self) -> None:
super()._connectSignals()
self._orderBox.valueChanged.connect(self._setOrder)
@QtCore.Slot(object)
def _setOrder(self, value: int) -> None:
self.filter.order = int(value)
if __name__ == '__main__': # pragma: no cover
QMoMedian.example()