Source code for linumpy.psf.extract
"""Extract PSF parameters (focal depth, Rayleigh length) from a stitched mosaic."""
import numpy as np
from scipy.ndimage import binary_dilation, binary_fill_holes, gaussian_filter
from scipy.stats import zscore
from skimage.filters import threshold_li
from skimage.morphology import disk
from linumpy.geometry.interface import find_tissue_interface
from linumpy.intensity.psf_model import confocal_psf, fit_tissue_confocal_model
# TODO: Fine-tune default values for 10x microscope or give heuristic
# for fixing them.
[docs]
def extract_psf_parameters_from_mosaic(
vol: np.ndarray,
f: float = 0.01,
n_profiles: int = 10,
zr_0: float = 610.0,
res: float = 6.5,
n_iterations: int = 15,
) -> tuple[float, float]:
"""Compute the confocal PSF from a slice.
Parameters
----------
vol : ndarray
A stitched tissue slice with axes in order (x, y, z).
f : float
Smoothing factor (in fraction of image size).
n_profiles : int
Number of intensity profile to use.
zr_0 : float
Initial Rayleigh length to use in micron (default=%(default)s for a 3X objective)
res : float
Z resolution (in micron).
n_iterations : int
Number of fitting iterations.
Returns
-------
(2,) tuple
Focal depth (zf) and Rayleigh length (zr) in micron
"""
nx, ny, nz = vol.shape
k = int(0.5 * f * (nx + ny))
aip = vol.mean(axis=2)
# Compute water-tissue interface
interface = find_tissue_interface(vol).astype(int)
# Compute the agarose mask with the li thresholding method
thresh = threshold_li(aip)
mask_tissue = binary_fill_holes(aip > thresh)
mask_agarose = ~binary_fill_holes(binary_dilation(mask_tissue, disk(k)))
mask_agarose[aip == 0] = 0
del mask_tissue
# Get min and max interface depth for the agarose
zmin = np.percentile(interface[mask_agarose], 2.5)
# Get the average iProfile / interface depth
profile_per_interface_depth = np.zeros((n_profiles, nz))
for ii in range(n_profiles):
for z in range(nz):
profile_per_interface_depth[ii, z] = np.mean(vol[:, :, z][mask_agarose * (interface == zmin + ii)])
# Detect outliers
i_profile_gradient = np.abs(gaussian_filter(profile_per_interface_depth, sigma=(0, 2), order=1))
profile_mask = np.abs(zscore(i_profile_gradient, axis=1)) <= 1.0
for ii in range(n_profiles):
profile_mask[ii, 0 : int(zmin + ii)] = 0
z = np.linspace(0, nz * res, nz)
zf_list = []
zr_list = []
total_err = []
for z0 in range(n_profiles):
# Find the coarse alignment of the focus based on
# pre-established Rayleigh length from thorlab
err_list = []
for zf in range(nz):
a = profile_per_interface_depth[z0, zf]
synthetic_signal = confocal_psf(z, zf, zr_0, a)
err = np.abs(synthetic_signal - profile_per_interface_depth[z0, :])
err = np.mean(err[profile_mask[z0, :]])
err_list.append(err)
err_list = np.array(err_list)
zf = np.argmin(err_list) * res
a = profile_per_interface_depth[z0, int(zf / res)]
zr: float = float(zr_0)
output: dict = {}
if not (np.isnan(a)):
last_zr = zr_0
for _ in range(n_iterations):
# Optimize the model (without using attenuation)
i_profile = profile_per_interface_depth[z0, :]
output = fit_tissue_confocal_model(
i_profile,
int(z0 + zmin),
last_zr,
res,
return_parameters=True,
return_full_model=True,
use_bump_model=True,
)
zf = output["parameters"]["zf"]
zr = output["parameters"]["zr"]
last_zr = zr
zf_list.append(zf)
zr_list.append(zr)
err_fit = (output["tissue_psf"] - profile_per_interface_depth[z0, :]) ** 2.0
total_err.append(np.mean(err_fit))
min_err = np.argmin(total_err)
zf_final = zf_list[min_err]
zr_final = zr_list[min_err]
return zf_final, zr_final