Source code for pylorenzmie.analysis.Hologram

'''Normalized hologram image with pixel coordinates.'''

from dataclasses import dataclass
import numpy as np
from numpy.typing import NDArray
from pylorenzmie.lib import meshgrid
from pylorenzmie.lib.lmtypes import Image


[docs] @dataclass(eq=False) class Hologram: '''Normalized hologram paired with pixel coordinates. Parameters ---------- data : numpy.ndarray Normalized hologram, shape ``(ny, nx)``. Pixel values should be floating-point intensities normalized so the background level is approximately 1 (i.e. ``I(r) / I_0(r)``). corner : tuple[float, float], optional ``(left, top)`` pixel coordinates of the top-left corner of this image within the full camera frame. Default: ``(0., 0.)``. Notes ----- Pixel coordinates are generated automatically from ``data.shape`` and ``corner`` via :func:`~pylorenzmie.lib.meshgrid` and stored as a ``(2, ny, nx)`` array. Two flat views are provided for use by scattering models and numerical solvers: - :attr:`flat_data` — ``data.ravel()``, shape ``(npts,)`` - :attr:`flat_coordinates` — ``coordinates.reshape(2, -1)``, shape ``(2, npts)`` Both are numpy views (no copy, O(1)). Coordinate-aware cropping:: crop = hologram[y0:y1, x0:x1] returns a new :class:`Hologram` whose ``corner`` is updated so that its coordinates are consistent with those of the parent frame. ''' data: Image corner: tuple[float, float] = (0., 0.) def __post_init__(self) -> None: self._coordinates = meshgrid(self.data.shape, corner=self.corner, flatten=False) def __eq__(self, other: object) -> bool: if not isinstance(other, Hologram): return NotImplemented return (np.array_equal(self.data, other.data) and self.corner == other.corner) @property def shape(self) -> tuple[int, int]: '''Image dimensions ``(ny, nx)``.''' return self.data.shape @property def coordinates(self) -> NDArray[float]: '''Pixel coordinates, shape ``(2, ny, nx)``.''' return self._coordinates @property def flat_data(self) -> NDArray[float]: '''Pixel values as a 1-D view, shape ``(npts,)``.''' return self.data.ravel() @property def flat_coordinates(self) -> NDArray[float]: '''Pixel coordinates as a 2-D view, shape ``(2, npts)``.''' return self._coordinates.reshape(2, -1) @staticmethod def _from_slice(data: Image, coordinates: NDArray[float]) -> 'Hologram': '''Create a Hologram from pre-computed coordinates (no meshgrid call). The corner is read directly from the coordinate array so that the result is always consistent with the parent frame. ''' h = object.__new__(Hologram) h.data = data h.corner = (float(coordinates[0, 0, 0]), float(coordinates[1, 0, 0])) h._coordinates = coordinates return h def __getitem__(self, key: tuple) -> 'Hologram': '''Return a coordinate-aware crop. Parameters ---------- key : tuple[slice, slice] ``(slice_y, slice_x)`` row and column slices, as produced by a bounding-box from :class:`~pylorenzmie.analysis.Localizer`. Returns ------- Hologram Cropped hologram with coordinates inherited from the parent. ''' sy, sx = key return Hologram._from_slice(self.data[key], self._coordinates[:, sy, sx])
[docs] @classmethod def example(cls) -> None: # pragma: no cover from pylorenzmie.utilities import example_hologram hologram = cls(example_hologram()) print(f'Hologram: shape={hologram.shape}, corner={hologram.corner}') print(f'coordinates: {hologram.coordinates.shape}') crop = hologram[50:150, 50:150] print(f'Crop: shape={crop.shape}, corner={crop.corner}')
if __name__ == '__main__': # pragma: no cover Hologram.example()