Source code for bioimageloader.transforms

"""Custom transforms for bioimages based on albumentations

"""

from typing import Optional, Tuple, Union

import albumentations
import cv2
import numpy as np


[docs]class HWCToCHW(albumentations.ImageOnlyTransform): """Transpose axes from (H, W, C) to (C, H, W) By default, ``bioimageloader`` returns images in shape of (H, W, C=3) regardless of its color mode for easy handling. Some models expect (C, H, W) shape of images as input. It converts (H, W, C) to (C, H, W). See Also -------- albumentations.ImageOnlyTransform : super class Examples -------- >>> import albumentations as A >>> from bioimageloader import Config >>> cfg = Config('config.yml') >>> transforms = A.Compose([ HWCToCHW(), ]) >>> datasets = cfg.load_datasets(transforms=transforms) >>> dset = datasets[0] # select only the first dataset >>> data = dset[0] # select only the first image >>> print(data['image'].shape) (3, H, W) """ def __init__(self, always_apply: bool = False, p: float = 1.0): super().__init__(always_apply=always_apply, p=p) def apply(self, img, **params): # img to have (H, W, C) return img.transpose(-1, 0, 1)
[docs]class SqueezeGrayImageCHW(albumentations.ImageOnlyTransform): """Squeeze grayscale image from (3, H, W) to (1, H, W)|(H, W) By default, ``bioimageloader`` returns images in 3 channels regardless of their color mode for easy handling. If a model requires (C=1, H, W) shape of input, first use ``HWCToCHW`` and use this transform to convert from (3, H, W) to (1, H, W). Parameters ---------- keep_dim : bool, default: True Keep channel axis to 1 always_apply : bool, default: False p : float, default: 1.0 Value between [0.0, 1.0] See Also -------- albumentations.ImageOnlyTransform : super class bioimageloader.transforms.HWCToCHW bioimageloader.transforms.SqueezeGrayImageHWC Examples -------- >>> import albumentations as A >>> from bioimageloader import Config >>> cfg = Config('config.yml') >>> transforms = A.Compose([ HWCToCHW(), SqueezeGrayImageCHW(), ]) >>> datasets = cfg.load_datasets(transforms=transforms) >>> dset = datasets[0] # select only the first dataset >>> data = dset[0] # select only the first image >>> print(data['image'].shape) (1, H, W) You can set ``keep_dim`` False to entirely drop channel axis, as some models require. But use ``SqueezeGrayImageHWC`` instead for that. >>> transforms = A.Compose([ HWCToCHW(), SqueezeGrayImageCHW(keep_dim=False), # drop channel axis ]) >>> datasets = cfg.load_datasets(transforms=transforms) >>> dset = datasets[0] # select only the first dataset >>> data = dset[0] # select only the first image >>> print(data['image'].shape) (H, W) """ def __init__( self, keep_dim=True, always_apply: bool = False, p: float = 1.0, ): super().__init__(always_apply=always_apply, p=p) self.keep_dim = keep_dim def apply(self, img, **params): if self.keep_dim: # img to have (1, H, W) return img[0:1] # img to have (H, W) return img[0] def get_transform_init_args_names(self): return ( 'keep_dim', )
[docs]class SqueezeGrayImageHWC(albumentations.ImageOnlyTransform): """Squeeze grayscale image from (H, W, 3) to (H, W)|(H, W, 1) By default, ``bioimageloader`` returns images in 3 channels regardless of their color mode for easy handling. It converts (H, W, 3) to (H, W). If a model requires (H, W) shape of input, use this transform to convert from (H, W, 3) to (H, W). Parameters ---------- keep_dim : bool, default: False always_apply : bool, default: False p : float, default: 1.0 Value between [0.0, 1.0] See Also -------- albumentations.ImageOnlyTransform : super class bioimageloader.transforms.SqueezeGrayImageHWC Examples -------- >>> import albumentations as A >>> from bioimageloader import Config >>> cfg = Config('config.yml') >>> transforms = A.Compose([ SqueezeGrayImageHWC(), ]) >>> datasets = cfg.load_datasets(transforms=transforms) >>> dset = datasets[0] # select only the first dataset >>> data = dset[0] # select only the first image >>> print(data['image'].shape) (H, W) """ def __init__( self, keep_dim=False, always_apply: bool = False, p: float = 1.0, ): super().__init__(always_apply=always_apply, p=p) self.keep_dim = keep_dim def apply(self, img, **params): if self.keep_dim: # img to have (H, W, 1) return img[..., 0:1] # img to have (H, W) return img[..., 0] def get_transform_init_args_names(self): return ( 'keep_dim', )
[docs]class ExpandToRGB(albumentations.DualTransform): """Make sure image/mask has 3 channels, either from HW or HW1 to HW3 Expand the channel axis of image array Notes ----- When used with ``albumentations.pytoch.ToTensorV2``, set ``transpose_mask=True`` to transpose masks. Targets: image, mask """ def __init__( self, always_apply: bool = False, p: float = 1.0, ): super().__init__(always_apply=always_apply, p=p) def apply(self, img, **params): if (ch := img.shape[-1]) == 3: # HW3: do nothing return img elif ch == 1: # HW1 print(img.shape) return img.repeat(3, axis=-1) # HW return img[..., np.newaxis].repeat(3, axis=-1) def apply_to_mask(self, img, **params): return self.apply(img, **params) def get_transform_init_args_names(self): return ()
[docs]class RGBToGray(albumentations.ImageOnlyTransform): """ToGray preserve all 3 channels from the input. This transform truncates channels dimension. Warnings -------- This will be deprecated. Grayscale conversion is done in eash Dataset Notes ----- Targets: image Image types: uint8, float32 """ def __init__(self, always_apply: bool = False, p: float = 1.0): super().__init__(always_apply=always_apply, p=p) def apply(self, img, **params): return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY) def get_transform_init_args_names(self): return ()
[docs]class ToGrayBySum(albumentations.ImageOnlyTransform): """Convert image to gray scale by tacking mean of existing channels For 2 channels, multi-modal images, ToGray does not make sense. Normally, rgb2gray conversions is a linear sum of RGB values (opencv [1]_, pillow [2]_). Just summing with eqaul weights would be more correct for bioimages. Warnings -------- This will be deprecated. Grayscale conversion is done through ``grayscale`` and ``grayscale_mode`` arguments in eash Dataset References ---------- .. [1] https://docs.opencv.org/3.4/de/d25/imgproc_color_conversions.html#color_convert_rgb_gray .. [2] https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.convert """ def __init__( self, always_apply: bool = False, p: float = 1.0, num_channels: Optional[int] = None, ): super().__init__(always_apply=always_apply, p=p) self.num_channels = num_channels def apply(self, img, **params): dtype = img.dtype if self.num_channels is not None: img = np.sum(img, axis=-1) / self.num_channels return img.astype(dtype) return np.mean(img, axis=-1).astype(dtype)
[docs]class ChannelReorder(albumentations.ImageOnlyTransform): """Reorder channel Expect images with 3 channels. Reorder and make it continuous in 'C' order. Parameters ---------- order : tuple of three integers Reorder by indexing always_apply : bool, default: False p : float, default: 1.0 Value between [0.0, 1.0] See Also -------- albumentations.ImageOnlyTransform : super class albumentations.augmentations.transforms.ChannelShuffle : random shuffling Examples -------- >>> import numpy as np >>> from bioimageloader.transforms import ChannelReorder >>> arr = np.arange(12).reshape((2, 2, 3)) >>> print(arr) [[[ 0 1 2] [ 3 4 5]] [[ 6 7 8] [ 9 10 11]]] >>> reorder = ChannelReorder((2, 1, 0)) >>> arr_reordered = reorder.apply(arr) >>> print(arr_reordered) [[[ 2 1 0] [ 5 4 3]] [[ 8 7 6] [11 10 9]]] """ def __init__( self, order: Tuple[int, int, int], always_apply: bool = False, p: float = 1.0, ): super().__init__(always_apply=always_apply, p=p) self.order = order def apply(self, img, **params): return np.ascontiguousarray(img[..., self.order]) def get_transform_init_args_names(self): return ( 'order', )
[docs]class NormalizePercentile(albumentations.ImageOnlyTransform): """Normalize using percentile Compute q-th percentile min- and max-values from given image array and normalize. Use ``numpy.percentile()`` [1]_ Expect images with 3 channels. Reorder and make it continuous in 'C' order. Parameters ---------- qmin : float, default: 0.0 Lower bound quantile in range of [0, 100) qmax : float, default: 99.8 Upper bound quantile in range of (0, 100] per_channel : bool, default: False Whether to calculate percentile per channel or not clip : bool, default: False Whether to clip in [0, 1] or not. Read more in Returns section. order : tuple of three integers Reorder by indexing always_apply : bool, default: False p : float, default: 1.0 Value between [0.0, 1.0] Returns ------- img_norm : numpy.ndarray Normalized image in float32 in range of [0.0, 1.0] if ``clip`` set to True, else its value overflows lower beyond 0.0 and higher beyond 1.0. See Also -------- albumentations.ImageOnlyTransform : super class albumentations.augmentations.transforms.ChannelShuffle : random shuffling References ---------- .. [1] https://numpy.org/doc/stable/reference/generated/numpy.percentile.html Examples -------- >>> import numpy as np >>> from bioimageloader.transforms import ChannelReorder >>> arr = np.arange(12).reshape((2, 2, 3)) >>> print(arr) [[[ 0 1 2] [ 3 4 5]] [[ 6 7 8] [ 9 10 11]]] >>> reorder = ChannelReorder((2, 1, 0)) >>> arr_reordered = reorder.apply(arr) >>> print(arr_reordered) [[[ 2 1 0] [ 5 4 3]] [[ 8 7 6] [11 10 9]]] """ def __init__( self, qmin: float = 0.0, qmax: float = 99.8, per_channel: bool = False, clip: bool = False, always_apply: bool = False, p: float = 1.0, ): super().__init__(always_apply=always_apply, p=p) self.qmin = qmin self.qmax = qmax self.per_channel = per_channel self._per_channel = (0, 1) if per_channel else None self.clip = clip def apply(self, img, **params): if self.qmin == 0: vmin = 0.0 vmax = np.percentile(img, self.qmax, axis=self._per_channel, keepdims=True) else: v = np.percentile(img, (self.qmin, self.qmax), axis=self._per_channel, keepdims=True) vmin = v[0] vmax = v[1] img = (img - vmin) / (vmax - vmin) if self.clip: img = np.clip(img, 0, 1) return img.astype(np.float32) def get_transform_init_args_names(self): return ( 'qmin', 'qmax', 'per_channel', 'clip', )
[docs]class BinarizeMask(albumentations.DualTransform): """Transform instance masks into binary masks Note that when composed with other transforms, BinarizeMask would rather come after them, because dtype will be boolean and ``albumentations`` does not like it. When you set ``val`` and ``dtype`` compatible with ``albumentations``, you can place BinarizeMask in any order safely. Parameters ---------- always_apply : bool, default: False p : float, default: 1.0 Value between [0.0, 1.0] dtype : str or dtype, optional Determine dtype. Default dtype becomes float32, when ``val`` is set. Otherwise, it becomes boolean. val : float, optional Change binarized mask value other than True if set. It also enforces dtype to be float32. Returns ------- mask : numpy.ndarray Mask that has binary value either [False, True] or [0, ``val``] See Also -------- albumentations.DualTransform : super class Examples -------- >>> import numpy as np >>> from bioimageloader.transforms import BinarizeMask >>> # instance mask >>> mask_inst = np.arange(12).reshape((3, 4)) >>> print(mask_inst) [[ 0 1 2 3] [ 4 5 6 7] [ 8 9 10 11]] >>> binarizemask = BinarizeMask() >>> mask_binary = binarizemask.apply_to_mask(mask_inst) >>> print(mask_binary) [[False True True True] [ True True True True] [ True True True True]] """ def __init__( self, always_apply: bool = False, p: float = 1.0, dtype: Optional[str] = None, val: Optional[Union[float, int]] = None, ): super().__init__(always_apply=always_apply, p=p) self.dtype = dtype self.val = val def apply(self, img, **params): return img def apply_to_mask(self, img, **params): mask = img > 0 if (val := self.val) is not None: mask = mask.astype('float32') # make sure float before multiplication mask = val * mask if (dtype := self.dtype) is not None: mask = mask.astype(dtype) return mask def get_transform_init_args_names(self): return ()