Source code for pylorenzmie.analysis.Mask
from dataclasses import dataclass
import numpy as np
from numpy.typing import NDArray
from pylorenzmie.lib import LMObject
from pylorenzmie.lib.lmtypes import Properties, Image, Coordinates
from pylorenzmie.analysis.Hologram import Hologram
[docs]
@dataclass
class Mask(LMObject):
'''Pixel selection mask for subsampling a hologram during fitting.
Randomly selects a fraction of pixels for analysis, with optional
exclusion of saturated, NaN, or infinite pixels. Regenerates
automatically whenever :attr:`shape`, :attr:`fraction`, or
:attr:`exclude` is changed.
Inherits from :class:`pylorenzmie.lib.LMObject`.
Parameters
----------
shape : tuple[int, int], optional
``(height, width)`` of the image. Default: ``None``.
fraction : float, optional
Fraction of pixels to include. Default: 0.1.
exclude : numpy.ndarray of bool, optional
Boolean array of pixels to force-exclude. Default: ``None``.
Notes
-----
Call :meth:`update` to resample the mask without changing any
parameter. Subclasses may override :meth:`_select` to implement
non-uniform sampling strategies.
References
----------
1. T. G. Dimiduk, R. W. Perry, J. Fung and V. N. Manoharan,
"Random-subset fitting of digital holograms for fast
three-dimensional particle tracking,"
*Applied Optics* **53**, G177–G183 (2014).
'''
shape: tuple[int, int] | None = None
fraction: float = 0.1
exclude: NDArray[np.bool_] | None = None
def __post_init__(self) -> None:
self._mask: NDArray[np.bool_] = np.empty((0, 0), dtype=bool)
self.update()
def __setattr__(self, prop: str, value: object) -> None:
super().__setattr__(prop, value)
if prop in ('shape', 'fraction', 'exclude') and hasattr(self, '_mask'):
self.update()
def __call__(self) -> NDArray[np.bool_]:
return self._mask
@LMObject.properties.getter
def properties(self) -> Properties:
'''Mask configuration: fraction of pixels to sample.'''
return dict(fraction=self.fraction)
def _select(self) -> None:
'''Randomly select pixels according to :attr:`fraction`.
Subclasses can override this to implement other sampling
distributions.
'''
if self.shape is None:
return
self._mask = np.random.rand(*self.shape) < self.fraction
[docs]
def update(self) -> None:
'''Regenerate the mask with the current parameters.'''
if self.shape is None:
return
self._select()
if self.exclude is not None and self.exclude.shape == self._mask.shape:
self._mask[self.exclude] = False
[docs]
def apply(self, hologram: Hologram) -> tuple[Image, Coordinates]:
'''Apply mask to a hologram, returning flat data and coordinates.
This is the explicit 2D-to-flat boundary for numerical solvers.
Parameters
----------
hologram : Hologram
Normalized hologram to subsample.
Returns
-------
data : numpy.ndarray
Selected pixel values, shape ``(nselected,)``.
coordinates : numpy.ndarray
Selected pixel coordinates, shape ``(2, nselected)``.
Notes
-----
Each call draws a fresh random subsample from ``hologram.shape``.
'''
self.shape = hologram.shape
m = self._mask
return hologram.data[m], hologram.coordinates[:, m]
[docs]
@classmethod
def example(cls) -> None: # pragma: no cover
import matplotlib.pyplot as plt
mask = cls(shape=(201, 201), fraction=0.2)
print(f'fraction = {np.sum(mask()) / mask().size:.2f}')
plt.imshow(mask(), cmap='gray')
plt.show()
if __name__ == '__main__': # pragma: no cover
Mask.example()