Source code for pyAPIC.io.mat_reader

from dataclasses import dataclass
from typing import Optional

import h5py
import numpy as np


[docs] @dataclass class ImagingData: """Container for imaging data and acquisition parameters.""" I_low: np.ndarray # Intensity images stack, shape (N, H, W) illum_px: np.ndarray # Illumination angles in pixel coords, shape (2, N) system_na_px: float # System numerical aperture in pixel units pixel_size: Optional[float] = None # Physical pixel size (e.g. um/px) magnification: Optional[float] = None # Objective magnification illum_na: Optional[np.ndarray] = ( None # Illumination angles in NA units, shape (2, N) ) system_na: Optional[float] = None # System NA in unity wavelength: Optional[float] = None # Illumination wavelength (same units as pixel_size) # ------------------------------------------------------------------ # Backwards compatibility aliases # ------------------------------------------------------------------ # The following property pairs expose the old attribute names while # internally storing the values under their new descriptive names. @property def freqXY_calib(self) -> np.ndarray: return self.illum_px @freqXY_calib.setter def freqXY_calib(self, value: np.ndarray) -> None: self.illum_px = value @property def dpix_c(self) -> Optional[float]: return self.pixel_size @dpix_c.setter def dpix_c(self, value: Optional[float]) -> None: self.pixel_size = value @property def na_calib(self) -> Optional[np.ndarray]: return self.illum_na @na_calib.setter def na_calib(self, value: Optional[np.ndarray]) -> None: self.illum_na = value @property def na_cal(self) -> Optional[float]: return self.system_na @na_cal.setter def na_cal(self, value: Optional[float]) -> None: self.system_na = value @property def na_rp_cal(self) -> float: return self.system_na_px @na_rp_cal.setter def na_rp_cal(self, value: float) -> None: self.system_na_px = value @property def mag(self) -> Optional[float]: return self.magnification @mag.setter def mag(self, value: Optional[float]) -> None: self.magnification = value
from .imaging_utils import to_pixel_coords
[docs] def load_mat(path: str, downsample: int = 1) -> ImagingData: """ Load imaging data and calibration parameters from a MATLAB .mat file (v7.3/HDF5). Parameters: path (str): Path to the .mat file. downsample (int): Factor by which to subsample the image stack along the illumination axis. Returns: ImagingData: Populated dataclass. """ with h5py.File(path, "r") as f: I_low = f["I_low"][:] # shape (N, H, W) freqXY_calib = f["freqXY_calib"][:] if "freqXY_calib" in f else None system_na_px = float(f["na_rp_cal"][()]) if "na_rp_cal" in f else None dpix_c = float(f["dpix_c"][()]) if "dpix_c" in f else None na_calib = f["na_calib"][:] if "na_calib" in f else None system_na = float(f["na_cal"][()]) if "na_cal" in f else None wavelength = float(f["lambda"][()]) if "lambda" in f else None magnification = float(f["mag"][()]) if "mag" in f else None if system_na_px is None and all( val is not None for val in (dpix_c, magnification, system_na, wavelength) ): im_size = min(I_low.shape[1:]) system_na_px = im_size * dpix_c / magnification * system_na / wavelength if system_na_px is None: raise KeyError( "na_rp_cal not found and could not be computed from provided data" ) # Subsample if requested if downsample > 1: I_low = I_low[::downsample] if freqXY_calib is not None: freqXY_calib = freqXY_calib[:, ::downsample] if na_calib is not None: na_calib = na_calib[:, ::downsample] if freqXY_calib is None and na_calib is not None and system_na is not None: center = np.array(I_low.shape[1:]) // 2 freqXY_calib = to_pixel_coords( na_calib, system_na_px=system_na_px, system_na=system_na, wavelength=wavelength, center=center, units="na", ) if freqXY_calib is None: raise KeyError( "freqXY_calib not found and could not be computed from provided data" ) return ImagingData( I_low=I_low, illum_px=freqXY_calib, system_na_px=system_na_px, pixel_size=dpix_c, magnification=magnification, illum_na=na_calib, system_na=system_na, wavelength=wavelength, )