Source code for pylorenzmie.analysis.Frame

from pylorenzmie.theory import Instrument, LorenzMie
from pylorenzmie.analysis import Localizer, Feature
from pylorenzmie.lib import LMObject
from pylorenzmie.lib.types import Image, Properties, Results
import pandas as pd
import numpy as np


[docs] class Frame(LMObject): '''Full-frame holographic microscopy analysis pipeline. Manages detection, estimation, and optimization of all particle features in a single normalized hologram. Instrument settings are shared across all features detected in the frame. Inherits from :class:`pylorenzmie.lib.LMObject`. Parameters ---------- instrument : Instrument, optional Microscope parameters shared by all features. Default: ``Instrument()``. localizer : Localizer, optional Feature detection backend. Default: ``Localizer()``. data : numpy.ndarray, optional Normalized hologram. Setting data clears any previous detection results. Attributes ---------- shape : tuple[int, int] Height × width of the most recently assigned image. coordinates : numpy.ndarray Pixel coordinate grid of shape ``(2, height, width)``. features : list[Feature] Feature objects created by the most recent :meth:`detect` call. bboxes : list Bounding boxes ``((x0, y0), w, h)`` for current features. results : pandas.DataFrame Tracking and characterization results from the most recent :meth:`optimize` or :meth:`analyze` call. ''' def __init__(self, instrument: Instrument | None = None, localizer: Localizer | None = None, data: Image | None = None) -> None: super().__init__() self.instrument = instrument or Instrument() self.localizer = localizer or Localizer() self._shape: tuple[int, int] = (0, 0) self._features: list[Feature] = [] self._bboxes: list = [] self._results: pd.DataFrame = pd.DataFrame() self._coordinates: np.ndarray = np.empty((2, 0, 0)) self._data: Image | None = None if data is not None: self.data = data @LMObject.properties.getter def properties(self) -> Properties: return dict() @property def shape(self) -> tuple[int, int]: '''Height × width of the current image.''' return self._shape @shape.setter def shape(self, shape: tuple[int, int] | None) -> None: if shape is None: return shape = tuple(shape) if shape == self._shape: return self._coordinates = self.meshgrid(shape, flatten=False) self._shape = shape @property def coordinates(self) -> np.ndarray: '''Pixel coordinate grid, shape ``(2, height, width)``.''' return self._coordinates @property def data(self) -> Image | None: '''Normalized hologram intensity.''' return self._data @data.setter def data(self, data: Image | None) -> None: self._features = [] self._results = pd.DataFrame() if data is not None: self.shape = data.shape self._data = data @property def results(self) -> Results: '''Tracking and characterization results from the most recent fit.''' return self._results @property def features(self) -> list[Feature]: '''Features detected in the current frame.''' return self._features @property def bboxes(self) -> list: '''Bounding boxes of current features, each ``((x0, y0), w, h)``.''' return self._bboxes @bboxes.setter def bboxes(self, bboxes: tuple | list) -> None: if (isinstance(bboxes, tuple) and len(bboxes) == 3 and isinstance(bboxes[0], tuple)): bboxes = [bboxes] self._bboxes = list(bboxes) self._features = [] for bbox in self._bboxes: (x0, y0), w, h = bbox dim = min(w, h) d = self.data[y0:y0 + dim, x0:x0 + dim] c = self.coordinates[:, y0:y0 + dim, x0:x0 + dim].reshape((2, -1)) this = Feature(data=d, coordinates=c, model=LorenzMie(instrument=self.instrument)) this.particle.x_p = x0 + dim / 2. this.particle.y_p = y0 + dim / 2. self._features.append(this)
[docs] def detect(self) -> int: '''Detect and localize features in :attr:`data`. Returns ------- nfeatures : int Number of features found. ''' if self.data is None: self._features = [] self._bboxes = [] return 0 self._results = self.localizer.localize(self.data) self._features = [] self._bboxes = [] for _, row in self._results.iterrows(): (x0, y0), w, h = row.bbox dim = min(w, h) d = self.data[y0:y0 + dim, x0:x0 + dim] c = self.coordinates[:, y0:y0 + dim, x0:x0 + dim].reshape((2, -1)) this = Feature(data=d, coordinates=c, model=LorenzMie(instrument=self.instrument)) this.particle.x_p = row.x_p this.particle.y_p = row.y_p self._features.append(this) self._bboxes.append(row.bbox) return len(self._features)
[docs] def estimate(self) -> None: '''Estimate parameters for all current features.''' for feature in self.features: feature.estimate()
[docs] def optimize(self) -> Results: '''Optimize parameters for all current features. Returns ------- results : pandas.DataFrame Fitted values, uncertainties, and goodness-of-fit for each feature. ''' results = [feature.optimize() for feature in self.features] self._results = pd.DataFrame(results) return self._results
[docs] def analyze(self, data: Image | None = None) -> Results: '''Detect features, estimate parameters, and optimize fits. Parameters ---------- data : numpy.ndarray, optional Normalized hologram. Updates :attr:`data` if provided. Returns ------- results : pandas.DataFrame Optimized parameters of the generative model for each detected feature. ''' self.data = data self.detect() self.estimate() return self.optimize()
[docs] @classmethod def example(cls) -> None: # pragma: no cover from time import perf_counter from pylorenzmie.utilities import example_hologram frame = cls() frame.instrument.wavelength = 0.447 frame.instrument.magnification = 0.048 frame.instrument.n_m = 1.34 frame.data = example_hologram('image0010.png') n = frame.detect() print(f'Detected {n} feature(s)') frame.estimate() for i, feature in enumerate(frame.features): print(f'Feature {i}: {feature.particle}') start = perf_counter() results = frame.optimize() print(f'Optimized in {perf_counter() - start:.3f} s') print(results)
if __name__ == '__main__': # pragma: no cover Frame.example()