'''Difference-of-Gaussians bandpass filter and companion Qt widget.'''
import logging
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__ = ['DoGFilter', 'QDoGFilter']
logger = logging.getLogger(__name__)
[docs]
class DoGFilter(VideoFilter):
'''Difference-of-Gaussians (DoG) bandpass filter.
Subtracts a wide Gaussian blur from a narrow one, suppressing both
slowly-varying background (*low_sigma*) and high-frequency noise
(*high_sigma* sets the noise cutoff). The result is displayed as the
absolute value scaled to ``uint8``, so both positive and negative
excursions appear bright.
DoG is the standard preprocessing step for particle tracking and
fluorescence microscopy: it isolates features at the scale set by
*low_sigma* while removing background and pixel noise.
Color input is converted to grayscale before filtering.
Parameters
----------
low_sigma : float
Standard deviation of the narrower Gaussian [pixels]. Controls the
smallest feature scale retained. Must be < *high_sigma*.
Default: ``1.0``.
high_sigma : float
Standard deviation of the wider Gaussian [pixels]. Controls the
largest background scale removed. Must be > *low_sigma*.
Default: ``3.0``.
'''
def __init__(self,
low_sigma: float = 1.0,
high_sigma: float = 3.0) -> None:
super().__init__()
self._low_sigma = 0.1
self._high_sigma = 0.2
self.high_sigma = high_sigma
self.low_sigma = low_sigma
@property
def low_sigma(self) -> float:
'''Narrow Gaussian σ [pixels]; must be < :attr:`high_sigma`.'''
return self._low_sigma
@low_sigma.setter
def low_sigma(self, value: float) -> None:
value = max(0.1, float(value))
if value >= self._high_sigma:
logger.warning(
f'low_sigma ({value}) must be less than '
f'high_sigma ({self._high_sigma}); ignoring')
return
self._low_sigma = value
@property
def high_sigma(self) -> float:
'''Wide Gaussian σ [pixels]; must be > :attr:`low_sigma`.'''
return self._high_sigma
@high_sigma.setter
def high_sigma(self, value: float) -> None:
value = max(0.2, float(value))
if value <= self._low_sigma:
logger.warning(
f'high_sigma ({value}) must be greater than '
f'low_sigma ({self._low_sigma}); ignoring')
return
self._high_sigma = value
[docs]
def get(self) -> Image | None:
'''Return the DoG-filtered frame.
Returns
-------
Image or None
uint8 image of the absolute DoG response, or ``None`` if no
frame has been added.
'''
if self.data is None:
return None
gray = (self.data.mean(axis=2).astype(np.float32)
if self.data.ndim == 3 else self.data.astype(np.float32))
lo = cv2.GaussianBlur(gray, (0, 0), self._low_sigma)
hi = cv2.GaussianBlur(gray, (0, 0), self._high_sigma)
return cv2.convertScaleAbs(lo - hi)
[docs]
def to_code(self) -> 'FilterCode':
from QVideo.lib.QVideoFilter import FilterCode
return FilterCode(
imports=frozenset({'import cv2', 'import numpy as np'}),
lines=[
'if image.ndim == 3:',
' image = image.mean(axis=2).astype(np.float32)',
'else:',
' image = image.astype(np.float32)',
f'_lo = cv2.GaussianBlur(image, (0, 0), {self._low_sigma})',
f'_hi = cv2.GaussianBlur(image, (0, 0), {self._high_sigma})',
'image = cv2.convertScaleAbs(_lo - _hi)',
],
comment=f'DoG bandpass, σ_low={self._low_sigma}, σ_high={self._high_sigma}',
)
[docs]
class QDoGFilter(QVideoFilter):
'''Widget for :class:`DoGFilter` with low and high sigma spinboxes.
Parameters
----------
parent : QtWidgets.QWidget or None
Parent widget.
'''
display_name = 'Difference of Gaussians'
display_category = 'Preprocessing'
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
super().__init__(parent, 'Difference of Gaussians', DoGFilter())
def _setupUi(self) -> None:
super()._setupUi()
self._lowBox = SpinBox(value=self.filter.low_sigma,
bounds=(0.1, None), step=0.5,
prefix='σ_low ')
self._layout.addWidget(self._lowBox)
self._highBox = SpinBox(value=self.filter.high_sigma,
bounds=(0.2, None), step=0.5,
prefix='σ_high ')
self._layout.addWidget(self._highBox)
def _connectSignals(self) -> None:
super()._connectSignals()
self._lowBox.valueChanged.connect(self._setLowSigma)
self._highBox.valueChanged.connect(self._setHighSigma)
@QtCore.Slot(object)
def _setLowSigma(self, value: float) -> None:
self.filter.low_sigma = value
with QtCore.QSignalBlocker(self._lowBox):
self._lowBox.setValue(self.filter.low_sigma)
@QtCore.Slot(object)
def _setHighSigma(self, value: float) -> None:
self.filter.high_sigma = value
with QtCore.QSignalBlocker(self._highBox):
self._highBox.setValue(self.filter.high_sigma)
if __name__ == '__main__': # pragma: no cover
QDoGFilter.example()