Source code for celldetective.utils.normalization

import gc
from typing import Optional, Union, Tuple, List, Any

import numpy as np


[docs] def normalize_mi_ma( x: np.ndarray, mi: Union[float, np.ndarray], ma: Union[float, np.ndarray], clip: bool = False, eps: float = 1e-20, dtype: Optional[Any] = np.float32, ) -> np.ndarray: """ Normalize array between min and max. Parameters ---------- x : ndarray Input array. mi : float Minimum value. ma : float Maximum value. clip : bool, optional If True, clip values between 0 and 1. Default is False. eps : float, optional Epsilon for numerical stability. Default is 1e-20. dtype : type, optional Data type. Default is np.float32. Returns ------- ndarray Normalized array. """ # from csbdeep https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py if dtype is not None: x = x.astype(dtype, copy=False) mi = dtype(mi) if np.isscalar(mi) else mi.astype(dtype, copy=False) ma = dtype(ma) if np.isscalar(ma) else ma.astype(dtype, copy=False) eps = dtype(eps) try: import numexpr x = numexpr.evaluate("(x - mi) / ( ma - mi + eps )") except ImportError: x = (x - mi) / (ma - mi + eps) if clip: x = np.clip(x, 0, 1) return x
[docs] def normalize( frame: np.ndarray, percentiles: Tuple[float, float] = (0.0, 99.99), values: Optional[Tuple[float, float]] = None, ignore_gray_value: Optional[float] = 0.0, clip: bool = False, amplification: Optional[float] = None, dtype: Any = float, ) -> np.ndarray: """ Normalize the intensity values of a frame. Parameters ---------- frame : ndarray The input frame to be normalized. percentiles : tuple, optional The percentiles used to determine the minimum and maximum values for normalization. Default is (0.0, 99.99). values : tuple or None, optional The specific minimum and maximum values to use for normalization. If None, percentiles are used. Default is None. ignore_gray_value : float or None, optional The gray value to ignore during normalization. If specified, the pixels with this value will not be normalized. Default is 0.0. clip : bool, optional If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float. Default is False. amplification : float, optional A factor by which to amplify the intensity values after normalization. If None, no amplification is applied. dtype : data-type, optional The desired data-type for the output normalized frame. The default is float. Returns ------- ndarray The normalized frame. Notes ----- This function performs intensity normalization on a frame. It computes the minimum and maximum values for normalization either using the specified values or by calculating percentiles from the frame. The frame is then normalized between the minimum and maximum values using the `normalize_mi_ma` function. If `ignore_gray_value` is specified, the pixels with this value will be left unmodified during normalization. Examples -------- >>> frame = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]]) >>> normalized = normalize(frame) >>> normalized array([[0. , 0.2, 0.4], [0.6, 0.8, 1. ], [1.2, 1.4, 1.6]], dtype=float32) >>> normalized = normalize(frame, percentiles=(10.0, 90.0)) >>> normalized array([[0.33333334, 0.44444445, 0.5555556 ], [0.6666667 , 0.7777778 , 0.8888889 ], [1. , 1.1111112 , 1.2222222 ]], dtype=float32) """ frame = frame.astype(float) if ignore_gray_value is not None: subframe = frame[frame != ignore_gray_value] else: subframe = frame.copy() if values is not None: mi = values[0] ma = values[1] else: mi = np.nanpercentile(subframe.flatten(), percentiles[0], keepdims=True) ma = np.nanpercentile(subframe.flatten(), percentiles[1], keepdims=True) frame0 = frame.copy() frame = normalize_mi_ma(frame0, mi, ma, clip=False, eps=1e-20, dtype=np.float32) if amplification is not None: frame *= amplification if clip: if amplification is None: amplification = 1.0 frame[frame >= amplification] = amplification frame[frame <= 0.0] = 0.0 if ignore_gray_value is not None: frame[np.where(frame0) == ignore_gray_value] = ignore_gray_value return frame.copy().astype(dtype)
[docs] def normalize_multichannel( multichannel_frame: np.ndarray, percentiles: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None, values: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None, ignore_gray_value: float = 0.0, clip: bool = False, amplification: Optional[float] = None, dtype: Any = float, ) -> np.ndarray: """ Normalizes a multichannel frame by adjusting the intensity values of each channel based on specified percentiles, direct value ranges, or amplification factors, with options to ignore a specific gray value and to clip the output. Parameters ---------- multichannel_frame : ndarray The input multichannel image frame to be normalized, expected to be a 3-dimensional array where the last dimension represents the channels. percentiles : list of tuples or tuple, optional Percentile ranges (low, high) for each channel used to scale the intensity values. If a single tuple is provided, it is applied to all channels. If None, the default percentile range of (0., 99.99) is used for each channel. values : list of tuples or tuple, optional Direct value ranges (min, max) for each channel to scale the intensity values. If a single tuple is provided, it is applied to all channels. This parameter overrides `percentiles` if provided. ignore_gray_value : float, optional A specific gray value to ignore during normalization (default is 0.). clip : bool, optional If True, clips the output values to the range [0, 1] or the specified `dtype` range if `dtype` is not float (default is False). amplification : float, optional A factor by which to amplify the intensity values after normalization. If None, no amplification is applied. dtype : data-type, optional The desired data-type for the output normalized frame. The default is float, but other types can be specified to change the range of the output values. Returns ------- ndarray The normalized multichannel frame as a 3-dimensional array of the same shape as `multichannel_frame`. Raises ------ AssertionError If the input `multichannel_frame` does not have 3 dimensions, or if the length of `values` does not match the number of channels in `multichannel_frame`. Notes ----- - This function provides flexibility in normalization by allowing the use of percentile ranges, direct value ranges, or amplification factors. - The function makes a copy of the input frame to avoid altering the original data. - When both `percentiles` and `values` are provided, `values` takes precedence for normalization. Examples -------- >>> multichannel_frame = np.random.rand(100, 100, 3) # Example multichannel frame >>> normalized_frame = normalize_multichannel(multichannel_frame, percentiles=[(1, 99), (2, 98), (0, 100)]) # Normalizes each channel of the frame using specified percentile ranges. """ mf = multichannel_frame.copy().astype(float) assert mf.ndim == 3, f"Wrong shape for the multichannel frame: {mf.shape}." if percentiles is None: percentiles = [(0.0, 99.99)] * mf.shape[-1] elif isinstance(percentiles, tuple): percentiles = [percentiles] * mf.shape[-1] if values is not None: if isinstance(values, tuple): values = [values] * mf.shape[-1] assert ( len(values) == mf.shape[-1] ), "Mismatch between the normalization values provided and the number of channels." mf_new = [] for c in range(mf.shape[-1]): if values is not None: v = values[c] else: v = None if np.all(mf[:, :, c] == 0.0): mf_new.append(mf[:, :, c].copy()) else: norm = normalize( mf[:, :, c].copy(), percentiles=percentiles[c], values=v, ignore_gray_value=ignore_gray_value, clip=clip, amplification=amplification, dtype=dtype, ) mf_new.append(norm) return np.moveaxis(mf_new, 0, -1)
[docs] def get_stack_normalization_values( stack: np.ndarray, percentiles: Optional[Union[Tuple[float, float], List[Tuple[float, float]]]] = None, ignore_gray_value: float = 0.0, ) -> List[Tuple[float, float]]: """ Computes the normalization value ranges (minimum and maximum) for each channel in a 4D stack based on specified percentiles. This function calculates the value ranges for normalizing each channel within a 4-dimensional stack, with dimensions expected to be in the order of Time (T), Y (height), X (width), and Channels (C). The normalization values are determined by the specified percentiles for each channel. An option to ignore a specific gray value during computation is provided, though its effect is not implemented in this snippet. Parameters ---------- stack : ndarray The input 4D stack with dimensions TYXC from which to calculate normalization values. percentiles : tuple, list of tuples, optional The percentile values (low, high) used to calculate the normalization ranges for each channel. If a single tuple is provided, it is applied to all channels. If a list of tuples is provided, each tuple is applied to the corresponding channel. If None, defaults to (0., 99.99) for each channel. ignore_gray_value : float, optional A gray value to potentially ignore during the calculation. This parameter is provided for interface consistency but is not utilized in the current implementation (default is 0.). Returns ------- list of tuples A list where each tuple contains the (minimum, maximum) values for normalizing each channel based on the specified percentiles. Raises ------ AssertionError If the input stack does not have 4 dimensions, or if the length of the `percentiles` list does not match the number of channels in the stack. Notes ----- - The function assumes the input stack is in TYXC format, where T is the time dimension, Y and X are spatial dimensions, and C is the channel dimension. - Memory management via `gc.collect()` is employed after calculating normalization values for each channel to mitigate potential memory issues with large datasets. Examples -------- >>> stack = np.random.rand(5, 100, 100, 3) # Example 4D stack with 3 channels >>> normalization_values = get_stack_normalization_values(stack, percentiles=((1, 99), (2, 98), (0, 100))) # Calculates normalization ranges for each channel using the specified percentiles. """ assert ( stack.ndim == 4 ), f"Wrong number of dimensions for the stack, expect TYXC (4) got {stack.ndim}." if percentiles is None: percentiles = [(0.0, 99.99)] * stack.shape[-1] elif isinstance(percentiles, tuple): percentiles = [percentiles] * stack.shape[-1] elif isinstance(percentiles, list): assert ( len(percentiles) == stack.shape[-1] ), f"Mismatch between the provided percentiles and the number of channels {stack.shape[-1]}. If you meant to apply the same percentiles to all channels, please provide a single tuple." values = [] for c in range(stack.shape[-1]): perc = percentiles[c] mi = np.nanpercentile(stack[:, :, :, c].flatten(), perc[0], keepdims=True)[0] ma = np.nanpercentile(stack[:, :, :, c].flatten(), perc[1], keepdims=True)[0] values.append(tuple((mi, ma))) gc.collect() return values
[docs] def normalize_per_channel( X: List[np.ndarray], normalization_percentile_mode: Union[bool, List[bool]] = True, normalization_values: Union[List[float], List[List[float]]] = [0.1, 99.99], normalization_clipping: Union[bool, List[bool]] = False, ) -> List[np.ndarray]: """ Applies per-channel normalization to a list of multi-channel images. This function normalizes each channel of every image in the list `X` based on either percentile values or fixed min-max values. Optionally, it can also clip the normalized values to stay within the [0, 1] range. The normalization can be applied in a percentile mode, where the lower and upper bounds for normalization are determined based on the specified percentiles of the non-zero values in each channel. Parameters ---------- X : list of ndarray A list of 3D numpy arrays, where each array represents a multi-channel image with dimensions (height, width, channels). normalization_percentile_mode : bool or list of bool, optional If True (or a list of True values), normalization bounds are determined by percentiles specified in `normalization_values` for each channel. If False, fixed `normalization_values` are used directly. Default is True. normalization_values : list of two floats or list of lists of two floats, optional The percentile values [lower, upper] used for normalization in percentile mode, or the fixed min-max values [min, max] for direct normalization. Default is [0.1, 99.99]. normalization_clipping : bool or list of bool, optional Determines whether to clip the normalized values to the [0, 1] range for each channel. Default is False. Returns ------- list of ndarray The list of normalized multi-channel images. Raises ------ AssertionError If the input images do not have a channel dimension, or if the lengths of `normalization_values`, `normalization_clipping`, and `normalization_percentile_mode` do not match the number of channels. Notes ----- - The normalization is applied in-place, modifying the input list `X`. - This function is designed to handle multi-channel images commonly used in image processing and computer vision tasks, particularly when different channels require separate normalization strategies. Examples -------- >>> X = [np.random.rand(100, 100, 3) for _ in range(5)] # Example list of 5 RGB images >>> normalized_X = normalize_per_channel(X) # Normalizes each channel of each image based on the default percentile values [0.1, 99.99]. """ assert X[0].ndim == 3, "Channel axis does not exist. Abort." n_channels = X[0].shape[-1] if isinstance(normalization_percentile_mode, bool): normalization_percentile_mode = [normalization_percentile_mode] * n_channels if isinstance(normalization_clipping, bool): normalization_clipping = [normalization_clipping] * n_channels if len(normalization_values) == 2 and not isinstance(normalization_values[0], list): normalization_values = [normalization_values] * n_channels assert len(normalization_values) == n_channels assert len(normalization_clipping) == n_channels assert len(normalization_percentile_mode) == n_channels X_normalized = [] for i in range(len(X)): x = X[i].copy() loc_i, loc_j, loc_c = np.where(x == 0.0) norm_x = np.zeros_like(x, dtype=np.float32) for k in range(x.shape[-1]): chan = x[:, :, k].copy() if not np.all(chan.flatten() == 0): if normalization_percentile_mode[k]: min_val = np.nanpercentile( chan[chan != 0.0].flatten(), normalization_values[k][0] ) max_val = np.nanpercentile( chan[chan != 0.0].flatten(), normalization_values[k][1] ) else: min_val = normalization_values[k][0] max_val = normalization_values[k][1] clip_option = normalization_clipping[k] norm_x[:, :, k] = normalize_mi_ma( chan.astype(np.float32).copy(), min_val, max_val, clip=clip_option, eps=1e-20, dtype=np.float32, ) else: norm_x[:, :, k] = 0.0 norm_x[loc_i, loc_j, loc_c] = 0.0 X_normalized.append(norm_x.copy()) return X_normalized