Source code for celldetective.utils.image_transforms

import collections
from typing import Optional, Union, Tuple, List, Any, Iterable, Dict

import numpy as np


[docs] def consume(iterator: Iterable[Any]) -> None: """ adapted from https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py Parameters ---------- iterator : iterable The iterator to consume. """ collections.deque(iterator, maxlen=0)
[docs] def axes_check_and_normalize( axes: str, length: Optional[int] = None, disallowed: Optional[str] = None, return_allowed: bool = False, ) -> Union[str, Tuple[str, str]]: """ adapted from https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py S(ample), T(ime), C(hannel), Z, Y, X Parameters ---------- axes : str The axes string to check and normalize. length : int, optional The expected length of the axes string. Default is None. disallowed : str, optional A string of disallowed axes. Default is None. return_allowed : bool, optional Whether to return the allowed axes string. Default is False. """ allowed = "STCZYX" assert axes is not None, ValueError("axis cannot be None.") axes = str(axes).upper() consume(a in allowed for a in axes) disallowed is None or consume(a not in disallowed for a in axes) consume(axes.count(a) == 1 for a in axes) length is None or len(axes) == length return (axes, allowed) if return_allowed else axes
[docs] def axes_dict(axes: str) -> Dict[str, Optional[int]]: """ adapted from https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py from axes string to dict Parameters ---------- axes : str The axes string to convert to a dictionary. """ axes, allowed = axes_check_and_normalize(axes, return_allowed=True) return {a: None if axes.find(a) == -1 else axes.find(a) for a in allowed}
# return collections.namedtuple('Axes',list(allowed))(*[None if axes.find(a) == -1 else axes.find(a) for a in allowed ])
[docs] def move_image_axes( x: np.ndarray, fr: str, to: str, adjust_singletons: bool = False ) -> np.ndarray: """ adapted from https://github.com/CSBDeep/CSBDeep/blob/main/csbdeep/utils/utils.py x: ndarray fr,to: axes string (see `axes_dict`) Parameters ---------- x : ndarray Input image. fr : str Source axes. to : str Target axes. adjust_singletons : bool, optional Whether to adjust singleton axes. Default is False. """ fr = axes_check_and_normalize(fr, length=x.ndim) to = axes_check_and_normalize(to) fr_initial = fr x_shape_initial = x.shape adjust_singletons = bool(adjust_singletons) if adjust_singletons: # remove axes not present in 'to' slices = [slice(None) for _ in x.shape] for i, a in enumerate(fr): if (a not in to) and (x.shape[i] == 1): # remove singleton axis slices[i] = 0 fr = fr.replace(a, "") x = x[tuple(slices)] # add dummy axes present in 'to' for i, a in enumerate(to): if a not in fr: # add singleton axis x = np.expand_dims(x, -1) fr += a if set(fr) != set(to): _adjusted = ( "(adjusted to %s and %s) " % (x.shape, fr) if adjust_singletons else "" ) raise ValueError( "image with shape %s and axes %s %snot compatible with target axes %s." % (x_shape_initial, fr_initial, _adjusted, to) ) ax_from, ax_to = axes_dict(fr), axes_dict(to) if fr == to: return x return np.moveaxis(x, [ax_from[a] for a in fr], [ax_to[a] for a in fr])
[docs] def estimate_unreliable_edge( activation_protocol: Optional[List[List[Any]]] = [["gauss", 2], ["std", 4]] ) -> Optional[int]: """ Safely estimate the distance to the edge of an image in which the filtered image values can be artefactual. Parameters ---------- activation_protocol : list of list, optional A list of lists, where each sublist contains a string naming the filter function, followed by its arguments (usually a kernel size). Default is [['gauss', 2], ['std', 4]]. Returns ------- int or None The sum of the kernel sizes in the activation protocol if the protocol is not empty. Returns None if the activation protocol is empty. Notes ----- This function assumes that the second element of each sublist in the activation protocol is a kernel size. Examples -------- >>> estimate_unreliable_edge([['gauss', 2], ['std', 4]]) 6 >>> estimate_unreliable_edge([]) None """ if activation_protocol is None or activation_protocol == []: return None else: edge = 0 for fct in activation_protocol: if isinstance(fct[1], (int, np.int_)) and not fct[0] == "invert": edge += fct[1] return edge
[docs] def unpad(img: np.ndarray, pad: int) -> np.ndarray: """ Remove padding from an image. This function removes the specified amount of padding from the borders of an image. The padding is assumed to be the same on all sides. Parameters ---------- img : ndarray The input image from which the padding will be removed. pad : int The amount of padding to remove from each side of the image. Returns ------- ndarray The image with the padding removed. Raises ------ ValueError If `pad` is greater than or equal to half of the smallest dimension of `img`. See Also -------- numpy.pad : Pads an array. Notes ----- This function assumes that the input image is a 2D array. Examples -------- >>> import numpy as np >>> img = np.array([[0, 0, 0, 0, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 1, 1, 1, 0], ... [0, 0, 0, 0, 0]]) >>> unpad(img, 1) array([[1, 1, 1], [1, 1, 1], [1, 1, 1]]) """ return img[pad:-pad, pad:-pad]
[docs] def mask_edges(binary_mask: np.ndarray, border_size: int) -> np.ndarray: """ Mask the edges of a binary mask. This function sets the edges of a binary mask to False, effectively masking out a border of the specified size. Parameters ---------- binary_mask : ndarray A 2D binary mask array where the edges will be masked. border_size : int The size of the border to mask (set to False) on all sides. Returns ------- ndarray The binary mask with the edges masked out. Raises ------ ValueError If `border_size` is greater than or equal to half of the smallest dimension of `binary_mask`. Notes ----- This function assumes that the input `binary_mask` is a 2D array. The input mask is converted to a boolean array before masking the edges. Examples -------- >>> import numpy as np >>> binary_mask = np.array([[1, 1, 1, 1, 1], ... [1, 1, 1, 1, 1], ... [1, 1, 1, 1, 1], ... [1, 1, 1, 1, 1], ... [1, 1, 1, 1, 1]]) >>> mask_edges(binary_mask, 1) array([[False, False, False, False, False], [False, True, True, True, False], [False, True, True, True, False], [False, True, True, True, False], [False, False, False, False, False]]) """ binary_mask = binary_mask.astype(bool) binary_mask[:border_size, :] = False binary_mask[(binary_mask.shape[0] - border_size) :, :] = False binary_mask[:, :border_size] = False binary_mask[:, (binary_mask.shape[1] - border_size) :] = False return binary_mask
def _estimate_scale_factor( spatial_calibration: Optional[float], required_spatial_calibration: Optional[float] ) -> Optional[float]: """ Estimates the scale factor needed to adjust spatial calibration to a required value. This function calculates the scale factor by which spatial dimensions (e.g., in microscopy images) should be adjusted to align with a specified calibration standard. This is particularly useful when preparing data for analysis with models trained on data of a specific spatial calibration. Parameters ---------- spatial_calibration : float or None The current spatial calibration factor of the data, expressed as units per pixel (e.g., micrometers per pixel). If None, indicates that the current spatial calibration is unknown or unspecified. required_spatial_calibration : float or None The spatial calibration factor required for compatibility with the model or analysis standard, expressed in the same units as `spatial_calibration`. If None, indicates no adjustment is required. Returns ------- float or None The scale factor by which the current data should be rescaled to match the required spatial calibration, or None if no scaling is necessary or if insufficient information is provided. Notes ----- - A scale factor close to 1 (within a tolerance defined by `epsilon`) indicates that no significant rescaling is needed, and the function returns None. - The function issues a warning if a significant rescaling is necessary, indicating the scale factor to be applied. Examples -------- >>> scale_factor = _estimate_scale_factor(spatial_calibration=0.5, required_spatial_calibration=0.25) # Each frame will be rescaled by a factor 2.0 to match with the model training data... >>> scale_factor = _estimate_scale_factor(spatial_calibration=None, required_spatial_calibration=0.25) # Returns None due to insufficient information about current spatial calibration. """ if (required_spatial_calibration is not None) * (spatial_calibration is not None): scale = spatial_calibration / required_spatial_calibration else: scale = None epsilon = 0.05 if scale is not None: if not np.all([scale >= (1 - epsilon), scale <= (1 + epsilon)]): print( f"Each frame will be rescaled by a factor {scale} to match with the model training data..." ) else: scale = None return scale
[docs] def threshold_image( img: np.ndarray, min_threshold: float, max_threshold: float, foreground_value: float = 255.0, fill_holes: bool = True, edge_exclusion: Optional[int] = None, ) -> np.ndarray: """ Threshold the input image to create a binary mask. Parameters ---------- img : ndarray The input image to be thresholded. min_threshold : float The minimum threshold value. max_threshold : float The maximum threshold value. foreground_value : float, optional The value assigned to foreground pixels in the binary mask. Default is 255. fill_holes : bool, optional Whether to fill holes in the binary mask. If True, the binary mask will be processed to fill any holes. If False, the binary mask will not be modified. Default is True. edge_exclusion : int, optional If provided, border pixels of this width will be masked (set to 0). Default is None. Returns ------- ndarray The binary mask after thresholding. Notes ----- This function applies a threshold to the input image to create a binary mask. Pixels with values within the specified threshold range are considered as foreground and assigned the `foreground_value`, while pixels outside the range are considered as background and assigned 0. If `fill_holes` is True, the binary mask will be processed to fill any holes using morphological operations. Examples -------- >>> image = np.random.rand(256, 256) >>> binary_mask = threshold_image(image, 0.2, 0.8, foreground_value=1., fill_holes=True) """ from scipy import ndimage as ndi binary = np.zeros_like(img).astype(bool) binary[img == img] = ( (img[img == img] >= min_threshold) * (img[img == img] <= max_threshold) * foreground_value ) if isinstance(edge_exclusion, (int, np.int_)): binary = mask_edges(binary, edge_exclusion) if fill_holes: binary = ndi.binary_fill_holes(binary.astype(int)) return binary