from __future__ import annotations
from numba import njit, prange, bool_, float64, int16, uint8
import numpy as np
import numpy.typing as npt
from .common import brightness_names
[docs]
def brightness_features(image: npt.NDArray[np.uint8],
mask: npt.NDArray[np.bool_],
image_bg: npt.NDArray[np.uint8] | None = None,
image_corr: npt.NDArray[np.int16] | None = None,
bg_off: float | None = None,
):
"""Compute brightness features
Parameters
----------
image: np.ndarray
2D array of "image" of shape (H, W)
mask: np.ndarray
3D array containing the N masks of shape (N, H, W)
image_bg: np.ndarray
2D array of "image_bg" of shape (H, W), required for computing
the "bg_med" feature.
image_corr: np.ndarray
2D array of (image - image_bg), which can be optionally passed
to this method. If not given, will be computed.
bg_off: float
Systematic offset value for correcting the brightness of the
background data which has an effect on "bright_bc_avg",
"bright_perc_10", "bright_perc_90", and "bg_med" (`bg_off` is
generated by sparsemed background correction).
"""
mask = np.asarray(mask, dtype=bool)
size = mask.shape[0]
br_dict = {}
for mkey in brightness_names:
br_dict[mkey] = np.full(size, np.nan, dtype=np.float64)
avg_sd = compute_avg_sd_masked_uint8(image, mask)
br_dict["bright_avg"][:] = avg_sd[:, 0]
br_dict["bright_sd"][:] = avg_sd[:, 1]
if image_bg is not None:
br_dict["bg_med"][:] = compute_median(image_bg)
if image_bg is not None or image_corr is not None:
# Background-corrected brightness values
if image_corr is None:
image_corr = np.asarray(image, dtype=np.int16) - image_bg
avg_sd_corr = compute_avg_sd_masked_int16(image_corr, mask)
br_dict["bright_bc_avg"][:] = avg_sd_corr[:, 0]
br_dict["bright_bc_sd"][:] = avg_sd_corr[:, 1]
# Percentiles
percentiles = compute_percentiles_10_90(image_corr, mask)
br_dict["bright_perc_10"][:] = percentiles[:, 0]
br_dict["bright_perc_90"][:] = percentiles[:, 1]
if bg_off is not None:
# subtract the background offset for all values that are computed
# from background-corrected images
for mkey in ["bright_bc_avg", "bright_perc_10", "bright_perc_90"]:
if mkey in br_dict:
br_dict[mkey] -= bg_off
# add the background offset to all values that were computed from
# the background only
for pkey in ["bg_med"]:
if pkey in br_dict:
br_dict[pkey] += bg_off
return br_dict
[docs]
@njit(float64[:, :](uint8[:, :, :], bool_[:, :, :]), cache=True)
def compute_avg_sd_masked_uint8(image, mask):
size = mask.shape[0]
avg_sd = np.zeros((size, 2), dtype=np.float64)
for ii in prange(size):
maski = np.where(mask[ii].ravel())[0]
if image.shape[0] == 1:
image_idx = 0
else:
image_idx = ii
masked = image[image_idx].ravel()[maski]
avg_sd[ii, 0] = np.mean(masked)
avg_sd[ii, 1] = np.std(masked)
return avg_sd
[docs]
@njit(float64[:, :](int16[:, :, :], bool_[:, :, :]), cache=True)
def compute_avg_sd_masked_int16(image, mask):
size = mask.shape[0]
avg_sd = np.zeros((size, 2), dtype=np.float64)
for ii in prange(size):
if image.shape[0] == 1:
image_idx = 0
else:
image_idx = ii
maski = np.where(mask[ii].ravel())[0]
masked = image[image_idx].ravel()[maski]
avg_sd[ii, 0] = np.mean(masked)
avg_sd[ii, 1] = np.std(masked)
return avg_sd
[docs]
@njit(float64[:, :](int16[:, :, :], bool_[:, :, :]), cache=True)
def compute_percentiles_10_90(image, mask):
size = mask.shape[0]
percentiles = np.zeros((size, 2), dtype=np.float64)
for ii in prange(size):
maski = np.where(mask[ii].ravel())[0]
if image.shape[0] == 1:
image_idx = 0
else:
image_idx = ii
masked = image[image_idx].ravel()[maski]
peri = np.percentile(masked, q=(10, 90))
percentiles[ii, 0] = peri[0]
percentiles[ii, 1] = peri[1]
return percentiles