Source code for pylorenzmie.analysis.Estimator
from dataclasses import dataclass
from pylorenzmie.lib import Azimuthal
from pylorenzmie.lib.lmtypes import Image, Properties, Result
from pylorenzmie.analysis.BaseEstimator import BaseEstimator
from pylorenzmie.analysis.Hologram import Hologram
from pylorenzmie.theory import Instrument
import pandas as pd
import numpy as np
from scipy.signal import argrelmin, argrelmax
from scipy.special import jn_zeros
[docs]
@dataclass
class Estimator(BaseEstimator):
'''Estimate initial parameters of a holographic feature.
Uses the azimuthal average of a cropped hologram to estimate the
axial position and radius of the scattering particle, providing
initial values for :class:`Optimizer`.
Inherits from :class:`BaseEstimator`.
Parameters
----------
instrument : Instrument
Optical parameters of the microscope.
n_p : float, optional
Particle refractive index. Default: 1.5. Cannot be estimated
from the azimuthal profile; serves as an initial value for
the optimizer.
k_p : float, optional
Imaginary part of the refractive index. Default: 0.
Notes
-----
Call :meth:`estimate` to populate all particle parameters.
Results are available via :attr:`properties` afterwards.
:meth:`predict` is a backward-compatibility alias for
:meth:`estimate`.
This class provides a lightweight conventional-algorithm baseline.
For higher-quality initial estimates — including ``n_p`` — use the
CATCH deep neural network model [1]_ [2]_.
References
----------
.. [1] L. E. Altman and D. G. Grier,
"CATCH: Characterizing and Tracking Colloids Holographically
Using Deep Neural Networks,"
J. Phys. Chem. B **124**, 1602 (2020).
.. [2] L. E. Altman and D. G. Grier,
"Machine learning enables precise holographic characterization
of colloidal materials in real time,"
Soft Matter **19**, 3002 (2023).
'''
instrument: Instrument
n_p: float = 1.5
k_p: float = 0.
def __post_init__(self) -> None:
self.predict = self.estimate
self.x_p: float | None = None
self.y_p: float | None = None
self.z_p: float | None = None
self.a_p: float | None = None
@BaseEstimator.properties.getter
def properties(self) -> Properties:
'''Estimated particle properties.'''
return dict(x_p=self.x_p, y_p=self.y_p,
z_p=self.z_p, a_p=self.a_p,
n_p=self.n_p, k_p=self.k_p)
def _initialize(self, data: Image) -> None:
self._k = self.instrument.wavenumber()
self._magnification = self.instrument.magnification
self._profile = Azimuthal.avg(data)
def _estimate_z(self) -> None:
'''Estimate axial position from fringe spacing.
Uses the paraxial approximation: consecutive extrema at radii
r_n satisfy Δ(r²) = 2π z_p / k.
'''
b = self._profile
nmax = argrelmax(b, order=5)
nmin = argrelmin(b, order=5)
rn = np.sort(np.concatenate(nmax + nmin))
zn = self._k / (2. * np.pi) * (rn[1:]**2 - rn[:-1]**2)
self.z_p = float(np.median(zn))
def _estimate_a(self) -> None:
'''Estimate radius from scattering minima.
Uses the Fraunhofer approximation: minima of the azimuthal
profile approximate zeros of J_1. The approximation is
inherently rough (~factor of two) because the profile minima
are holographic fringe minima rather than true Fraunhofer zeros.
The optimizer refines the estimate. For higher-quality initial
estimates use the CATCH neural network model cited in the class
docstring.
Taking the minimum of j_{1,n} alpha_n over the first few minima
(rather than the median over all or just minima[0]) is more
robust: spurious near-center minima produce large products and
are automatically excluded by the minimum.
'''
minima = argrelmin(self._profile, order=5)[0].astype(float)
minima = minima[minima > 5][:5] # first 5, skip DC noise
if len(minima) == 0:
self.logger.warning(
'No intensity minima found; a_p could not be estimated')
return
alpha_n = np.sqrt(np.square(self.z_p / minima) + 1.)
a_p = np.min(jn_zeros(1, len(minima)) * alpha_n) / self._k
self.a_p = float(self._magnification * a_p)
[docs]
def estimate(self, hologram: Hologram) -> Result:
'''Estimate particle properties from a normalized hologram.
Parameters
----------
hologram : Hologram
Normalized hologram crop.
Returns
-------
result : pandas.Series
Estimated particle properties.
'''
self._initialize(hologram.data)
self.x_p = float(hologram.coordinates[0].mean())
self.y_p = float(hologram.coordinates[1].mean())
self._estimate_z()
self._estimate_a()
return pd.Series(self.properties)
[docs]
@classmethod
def example(cls) -> None: # pragma: no cover
from time import perf_counter
from pylorenzmie.utilities import example_hologram
instrument = Instrument()
instrument.wavelength = 0.447
instrument.magnification = 0.048
instrument.n_m = 1.34
estimator = cls(instrument=instrument)
hologram = example_hologram()
print(f'{cls.__name__} example')
start = perf_counter()
result = estimator.estimate(hologram)
print(f'Time: {perf_counter() - start:.3f} s')
print(result)
if __name__ == '__main__': # pragma: no cover
Estimator.example()