# ============================================================================================= #
# Author: Pavel Iakubovskii, ZFTurbo, ashawkey, Dominik Müller, #
# Samuel Šuľan, Lucia Hradecká, Filip Lux #
# Copyright: albumentations: : https://github.com/albumentations-team #
# Pavel Iakubovskii : https://github.com/qubvel #
# ZFTurbo : https://github.com/ZFTurbo #
# ashawkey : https://github.com/ashawkey #
# Dominik Müller : https://github.com/muellerdo #
# Lucia Hradecká : lucia.d.hradecka@gmail.com #
# Filip Lux : lux.filip@gmail.com #
# #
# Volumentations History: #
# - Original: https://github.com/albumentations-team/albumentations #
# - 3D Conversion: https://github.com/ashawkey/volumentations #
# - Continued Development: https://github.com/ZFTurbo/volumentations #
# - Enhancements: https://github.com/qubvel/volumentations #
# - Further Enhancements: https://github.com/muellerdo/volumentations #
# - Biomedical Enhancements: https://gitlab.fi.muni.cz/cbia/bio-volumentations #
# #
# MIT License. #
# #
# Permission is hereby granted, free of charge, to any person obtaining a copy #
# of this software and associated documentation files (the "Software"), to deal #
# in the Software without restriction, including without limitation the rights #
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell #
# copies of the Software, and to permit persons to whom the Software is #
# furnished to do so, subject to the following conditions: #
# #
# The above copyright notice and this permission notice shall be included in all #
# copies or substantial portions of the Software. #
# #
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR #
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, #
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE #
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER #
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, #
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE #
# SOFTWARE. #
# ============================================================================================= #
import random
import numpy as np
from ..core.transforms_interface import DualTransform, ImageOnlyTransform
from ..augmentations import functional as F
from ..random_utils import uniform, sample_range_uniform
from typing import List, Sequence, Tuple, Union
from ..typing import TypeSextetFloat, TypeTripletFloat, TypePairFloat
from .utils import parse_limits, parse_coefs, to_tuple
# TODO potential upgrade : different sigmas for different channels
[docs]
class GaussianNoise(ImageOnlyTransform):
"""Adds Gaussian noise to the image. The noise is drawn from normal distribution with given parameters.
Args:
var_limit (tuple, optional): Variance of normal distribution is randomly chosen from this interval.
Defaults to ``(0.001, 0.1)``.
mean (float, optional): Mean of normal distribution.
Defaults to ``0``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, var_limit: tuple = (0.001, 0.1), mean: float = 0,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.var_limit = var_limit
self.mean = mean
[docs]
def apply(self, img, **params):
return F.gaussian_noise(img, sigma=params['sigma'], mean=self.mean)
[docs]
def get_params(self, **params):
var = uniform(self.var_limit[0], self.var_limit[1])
sigma = var ** 0.5
return {"sigma": sigma}
def __repr__(self):
return f'GaussianNoise({self.var_limit}, {self.mean}, {self.always_apply}, {self.p})'
[docs]
class PoissonNoise(ImageOnlyTransform):
"""Adds Poisson noise to the image.
Args:
intensity_limit (tuple): Range to sample the expected intensity of Poisson noise.
Defaults to ``(1, 10)``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self,
intensity_limit=(1, 10),
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.intensity_limit = intensity_limit
[docs]
def apply(self, img, **params):
return F.poisson_noise(img, intensity=params['intensity'])
[docs]
def get_params(self, **params):
intensity = uniform(self.intensity_limit[0], self.intensity_limit[1])
return {"intensity": intensity}
def __repr__(self):
return f'PoissonNoise({self.always_apply}, {self.p})'
# TODO anti_aliasing_downsample keep parameter or remove?
[docs]
class Resize(DualTransform):
"""Resize input to the given shape.
Internally, the ``skimage.transform.resize`` function is used.
The ``interpolation``, ``border_mode``, ``ival``, ``mval``,
and ``anti_aliasing_downsample`` arguments are forwarded to it. More details at:
https://scikit-image.org/docs/stable/api/skimage.transform.html#skimage.transform.resize.
Args:
shape (tuple of ints): The desired image shape.
Must be of either of: ``(Z, Y, X)`` or ``(Z, Y, X, T)``.
The unspecified dimensions (C and possibly T) are not affected.
interpolation (int, optional): Order of spline interpolation.
Defaults to ``1``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'reflect'``.
ival (float, optional): Value of `image` voxels outside of the `image` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` voxels outside of the `mask` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
anti_aliasing_downsample (bool, optional): Controls if the Gaussian filter should be applied before
downsampling. Recommended.
Defaults to ``True``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, shape: tuple, interpolation: int = 1, border_mode: str = 'reflect', ival: float = 0,
mval: float = 0, anti_aliasing_downsample: bool = True, ignore_index: Union[float, None] = None,
always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.shape = shape
self.interpolation = interpolation
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
self.anti_aliasing_downsample = anti_aliasing_downsample
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.resize(img, input_new_shape=self.shape, interpolation=self.interpolation,
border_mode=self.border_mode, cval=self.ival,
anti_aliasing_downsample=self.anti_aliasing_downsample)
[docs]
def apply_to_mask(self, mask, **params):
return F.resize(mask, input_new_shape=self.shape, interpolation=0,
border_mode=self.mask_mode, cval=self.mval, anti_aliasing_downsample=False,
mask=True)
[docs]
def apply_to_float_mask(self, mask, **params):
return F.resize(mask, input_new_shape=self.shape, interpolation=self.interpolation,
border_mode=self.mask_mode, cval=self.mval, anti_aliasing_downsample=False,
mask=True)
def __repr__(self):
return f'Resize({self.shape}, {self.interpolation}, {self.border_mode} , {self.ival}, {self.mval},' \
f'{self.anti_aliasing_downsample}, {self.always_apply}, {self.p})'
[docs]
class Scale(DualTransform):
"""Rescale input by the given scale.
Args:
scales (float|List[float], optional): Value by which the input should be scaled.
Must be either of: ``S``, ``[S_Z, S_Y, S_X]``, or ``[S_Z, S_Y, S_X, S_T]``.
If a float, then all spatial dimensions are scaled by it (equivalent to ``[S, S, S]``).
The unspecified dimensions (C and possibly T) are not affected.
Defaults to ``1``.
interpolation (int, optional): Order of spline interpolation.
Defaults to ``1``.
spacing (float | Tuple[float, float, float] | None, optional): Voxel spacing for individual spatial dimensions.
Must be either of: ``S``, ``(S1, S2, S3)``, or ``None``.
If ``None``, equivalent to ``(1, 1, 1)``.
If a float ``S``, equivalent to ``(S, S, S)``.
Otherwise, a scale for each spatial dimension must be given.
Defaults to ``None``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'constant'``.
ival (float, optional): Value of `image` voxels outside of the `image` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` voxels outside of the `mask` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, scales: Union[float, TypeTripletFloat] = 1,
interpolation: int = 1, spacing: Union[float, TypeTripletFloat] = None,
border_mode: str = 'constant', ival: float = 0, mval: float = 0,
ignore_index: Union[float, None] = None, always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.scale = parse_coefs(scales, identity_element=1.)
self.interpolation: int = interpolation
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1.)
self.border_mode = border_mode # not implemented
self.mask_mode = border_mode # not implemented
self.ival = ival
self.mval = mval
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.affine(img,
scales=self.scale,
interpolation=self.interpolation,
border_mode=self.border_mode,
value=self.ival,
spacing=self.spacing)
[docs]
def apply_to_mask(self, mask, **params):
interpolation = 0 # refers to 'sitkNearestNeighbor'
return F.affine(mask,
scales=self.scale,
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)
[docs]
def apply_to_float_mask(self, mask, **params):
return F.affine(mask,
scales=self.scale,
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)
def __repr__(self):
return f'Scale({self.scale}, {self.interpolation}, {self.border_mode}, {self.ival}, {self.mval},' \
f'{self.always_apply}, {self.p})'
# TODO cannot rescale T dimension
[docs]
class RandomScale(DualTransform):
"""Randomly rescale input.
Args:
scaling_limit (float | Tuple[float] | List[Tuple[float]], optional): Limits of scaling factors.
Must be either of: ``S``, ``(S1, S2)``, ``(S_Z, S_Y, S_X)``, or ``(S_Z1, S_Z2, S_Y1, S_Y2, S_X1, S_X2)``.
If a float ``S``, then all spatial dimensions are scaled by a random number drawn uniformly from
the interval [1-S, 1+S] (equivalent to inputting ``(1-S, 1+S, 1-S, 1+S, 1-S, 1+S)``).
If a tuple of 2 floats, then all spatial dimensions are scaled by a random number drawn uniformly
from the interval [S1, S2] (equivalent to inputting ``(S1, S2, S1, S2, S1, S2)``).
If a tuple of 3 floats, then an interval [1-S_a, 1+S_a] is constructed for each spatial
dimension and the scale is randomly drawn from it
(equivalent to inputting ``(1-S_Z, 1+S_Z, 1-S_Y, 1+S_Y, 1-S_X, 1+S_X)``).
If a tuple of 6 floats, the scales for individual spatial dimensions are randomly drawn from the
respective intervals [S_Z1, S_Z2], [S_Y1, S_Y2], [S_X1, S_X2].
The unspecified dimensions (C and T) are not affected.
Defaults to ``(0.9, 1.1)``.
interpolation (int, optional): Order of spline interpolation.
Defaults to ``1``.
spacing (float | Tuple[float, float, float] | None, optional): Voxel spacing for individual spatial dimensions.
Must be either of: ``S``, ``(S1, S2, S3)``, or ``None``.
If ``None``, equivalent to ``(1, 1, 1)``.
If a float ``S``, equivalent to ``(S, S, S)``.
Otherwise, a scale for each spatial dimension must be given.
Defaults to ``None``.
border_mode (str, optional): Values outside image domain are filled according to the mode.
Defaults to ``'constant'``.
ival (float, optional): Value of `image` voxels outside of the `image` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` voxels outside of the `mask` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float_mask
"""
def __init__(self, scaling_limit: Union[float, TypePairFloat, TypeTripletFloat, TypeSextetFloat] = (0.9, 1.1),
interpolation: int = 1, spacing: Union[float, TypeTripletFloat] = None,
border_mode: str = 'constant', ival: float = 0, mval: float = 0,
ignore_index: Union[float, None] = None, always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.scaling_limit: TypeSextetFloat = parse_limits(scaling_limit)
self.interpolation: int = interpolation
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1.)
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival: float = ival
self.mval: float = mval
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def get_params(self, **data):
# set parameters of the transform
scale = sample_range_uniform(self.scaling_limit)
return {
"scale": scale,
}
[docs]
def apply(self, img, **params):
return F.affine(img,
scales=params["scale"],
interpolation=self.interpolation,
border_mode=self.border_mode,
value=self.ival,
spacing=self.spacing)
[docs]
def apply_to_mask(self, mask, **params):
interpolation = 0 # refers to 'sitkNearestNeighbor'
return F.affine(mask,
scales=params["scale"],
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)
[docs]
def apply_to_float_mask(self, mask, **params):
return F.affine(mask,
scales=params["scale"],
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)
def __repr__(self):
return f'RandomScale({self.scaling_limit}, {self.interpolation}, {self.always_apply}, {self.p})'
[docs]
class RandomRotate90(DualTransform):
"""Rotation of input by 0, 90, 180, or 270 degrees around the specified spatial axes.
Args:
axes (List[int], optional): List of axes around which the input is rotated. Recognised axis symbols are
``1`` for Z, ``2`` for Y, and ``3`` for X. A single axis can occur multiple times in the list.
If ``shuffle_axis = False``, the order of axes determines the order of transformations.
Defaults to ``[1, 2, 3]``.
shuffle_axis (bool, optional): If set to ``True``, the order of rotations is random.
Defaults to ``False``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float_mask
"""
def __init__(self, axes: List[int] = None, shuffle_axis: bool = False,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.axes = axes
self.shuffle_axis = shuffle_axis
[docs]
def apply(self, img, **params):
for factor, axes in zip(params["factor"], params["rotation_around"]):
img = np.rot90(img, factor, axes=axes)
return img
[docs]
def apply_to_mask(self, mask, **params):
for i in range(len(params["rotation_around"])):
mask = np.rot90(mask, params["factor"][i], axes=(
params["rotation_around"][i][0] - 1, params["rotation_around"][i][1] - 1))
return mask
[docs]
def get_params(self, **data):
# Rotate by all axis by default
if self.axes is None:
self.axes = [1, 2, 3]
# Create all combinations for rotating
axes_to_rotate = {1: (2, 3), 2: (1, 3), 3: (1, 2)}
rotation_around = []
for i in self.axes:
if i in axes_to_rotate.keys():
rotation_around.append(axes_to_rotate[i])
# shuffle order of axis
if self.shuffle_axis:
random.shuffle(rotation_around)
# choose angle to rotate
factor = [random.randint(0, 3) for _ in range(len(rotation_around))]
return {"factor": factor,
"rotation_around": rotation_around}
def __repr__(self):
return f'RandomRotate90({self.axes}, {self.always_apply}, {self.p})'
[docs]
class Flip(DualTransform):
"""Flip input around the specified spatial axes.
Args:
axes (List[int], optional): List of axes around which is flip done. Recognised axis symbols are
``1`` for Z, ``2`` for Y, and ``3`` for X.
Defaults to ``[1,2,3]``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, axes: List[int] = None, always_apply=False, p=1):
super().__init__(always_apply, p)
self.axes = axes
[docs]
def apply(self, img, **params):
return np.flip(img, params["axes"])
[docs]
def apply_to_mask(self, mask, **params):
# Mask has no dimension channel
return np.flip(mask, axis=[item - 1 for item in params["axes"]])
[docs]
def get_params(self, **data):
if self.axes is None:
axes = [1, 2, 3]
else:
axes = self.axes
return {"axes": axes}
def __repr__(self):
return f'Flip({self.axes}, {self.always_apply}, {self.p})'
# TODO include possibility to pick empty combination = no flipping
[docs]
class RandomFlip(DualTransform):
"""Flip input around a set of axes randomly chosen from the input list of axis combinations.
Args:
axes_to_choose (List[Tuple[int]] or None, optional): List of axis combinations from which one option
is randomly chosen. Recognised axis symbols are ``1`` for Z, ``2`` for Y, and ``3`` for X.
The image will be flipped around all axes in the chosen combination.
If ``None``, a random subset of spatial axes is chosen, corresponding to inputting
``[(1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]``.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float_mask
"""
def __init__(self, axes_to_choose: Union[None, List[Tuple[int]]] = None, always_apply=False, p=0.5):
super().__init__(always_apply, p)
self.axes = axes_to_choose
[docs]
def apply(self, img, **params):
return np.flip(img, params["axes"])
[docs]
def apply_to_mask(self, mask, **params):
# Mask has no dimension channel
return np.flip(mask, axis=[item - 1 for item in params["axes"]])
[docs]
def get_params(self, **data):
if self.axes is None or len(self.axes) == 0:
# Pick random combination of axes to flip
# TODO include possibility to pick empty combination = no flipping
combinations = [(1,), (2,), (3,), (1, 2),
(1, 3), (2, 3), (1, 2, 3)]
axes = random.choice(combinations)
else:
# Pick a random choice from input
axes = random.choice(self.axes)
return {"axes": axes}
def __repr__(self):
return f'Flip({self.axes}, {self.always_apply}, {self.p})'
[docs]
class CenterCrop(DualTransform):
"""Crops the central region of the input of given size.
Unlike ``CenterCrop`` from `Albumentations`, this transform pads the input in dimensions
where the input is smaller than the ``shape`` with ``numpy.pad``. The ``border_mode``, ``ival`` and ``mval``
arguments are forwarded to ``numpy.pad`` if padding is necessary. More details at:
https://numpy.org/doc/stable/reference/generated/numpy.pad.html.
Args:
shape (Tuple[int]): The desired shape of input.
Must be either of: ``[Z, Y, X]`` or ``[Z, Y, X, T]``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'reflect'``.
ival (float | Sequence, optional): Values of `image` voxels outside of the `image` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
mval (float | Sequence, optional): Values of `mask` voxels outside of the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, shape: Tuple[int], border_mode: str = "reflect", ival: Union[Sequence[float], float] = (0, 0),
mval: Union[Sequence[float], float] = (0, 0), ignore_index: Union[float, None] = None,
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
self.shape = np.asarray(shape, dtype=np.intc)
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.center_crop(img, self.shape, self.border_mode, self.ival, False)
[docs]
def apply_to_mask(self, mask, **params):
return F.center_crop(mask, self.shape, self.mask_mode, self.mval, True)
def __repr__(self):
return f'CenterCrop({self.shape}, {self.always_apply}, {self.p})'
[docs]
class RandomCrop(DualTransform):
"""Randomly crops a region of given size from the input.
Unlike ``RandomCrop`` from `Albumentations`, this transform pads the input in dimensions
where the input is smaller than the ``shape`` with ``numpy.pad``. The ``border_mode``, ``ival`` and ``mval``
arguments are forwarded to ``numpy.pad`` if padding is necessary. More details at:
https://numpy.org/doc/stable/reference/generated/numpy.pad.html.
Args:
shape (Tuple[int]): The desired shape of input.
Must be either of: ``[Z, Y, X]`` or ``[Z, Y, X, T]``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'reflect'``.
ival (float | Sequence, optional): Values of `image` voxels outside of the `image` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
mval (float | Sequence, optional): Values of `mask` voxels outside of the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, shape: tuple, border_mode: str = "reflect", ival: Union[Sequence[float], float] = (0, 0),
mval: Union[Sequence[float], float] = (0, 0), ignore_index: Union[float, None] = None,
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
self.shape = np.asarray(shape, dtype=np.intc)
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def apply(self, img, crop_start=np.array((0, 0, 0))):
return F.random_crop(img, self.shape, crop_start, self.border_mode, self.ival, mask=False)
[docs]
def apply_to_mask(self, mask, crop_start=np.array((0, 0, 0))):
return F.random_crop(mask, self.shape, crop_start, self.mask_mode, self.mval, mask=True)
[docs]
def get_params(self, **data):
return {
"crop_start": [random.random() for _ in range(len(self.shape))]
}
def __repr__(self):
return f'RandomCrop({self.shape}, {self.always_apply}, {self.p})'
# TODO create checks (mean, std, got good shape, and etc.), what if given list but only one channel, and reverse.
[docs]
class NormalizeMeanStd(ImageOnlyTransform):
"""Normalize image values to have mean 0 and standard deviation 1, given channel-wise means and standard deviations.
For a single-channel image, the normalization is applied by the formula: :math:`img = (img - mean) / std`.
If the image contains more channels, then the previous formula is used for each channel separately.
It is recommended to input dataset-wide means and standard deviations.
Args:
mean (float | List[float]): Channel-wise image mean.
Must be either of: ``M``, ``(M_1, M_2, ..., M_C)``.
std (float | List[float]): Channel-wise image standard deviation.
Must be either of: ``S``, ``(S_1, S_2, ..., S_C)``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``True``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image
"""
def __init__(self, mean: Union[List[float], float], std: Union[List[float], float],
always_apply: bool = True, p: float = 1.0):
super().__init__(always_apply, p)
self.mean = np.array(mean, dtype=np.float32)
self.std = np.array(std, dtype=np.float32)
self.denominator = np.reciprocal(self.std, dtype=np.float32)
[docs]
def apply(self, image, **params):
return F.normalize_mean_std(image, self.mean, self.denominator)
def __repr__(self):
return f'NormalizeMeanStd({self.mean}, {self.std}, ' \
f' {self.always_apply}, {self.p})'
[docs]
class GaussianBlur(ImageOnlyTransform):
"""Performs Gaussian blurring of the image. In case of a multi-channel image, individual channels are blured separately.
Internally, the ``scipy.ndimage.gaussian_filter`` function is used. The ``border_mode`` and ``cval``
arguments are forwarded to it. More details at:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.gaussian_filter.html.
Args:
sigma (float, Tuple(float), List[Tuple(float) | float] , optional): Gaussian sigma.
Must be either of: ``S``, ``(S_Z, S_Y, S_X)``, ``(S_Z, S_Y, S_X, S_T)``, ``[S_1, S_2, ..., S_C]``,
``[(S_Z1, S_Y1, S_X1), (S_Z2, S_Y2, S_X2), ..., (S_ZC, S_YC, S_XC)]``, or
``[(S_Z1, S_Y1, S_X1, S_T1), (S_Z2, S_Y2, S_X2, S_T2), ..., (S_ZC, S_YC, S_XC, S_TC)]``.
If a float, the spatial dimensions are blurred with the same strength (equivalent to ``(S, S, S)``).
If a tuple, the sigmas for spatial dimensions and possibly the time dimension must be specified.
If a list, sigmas for each channel must be specified either as a single number or as a tuple.
Defaults to ``0.8``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'reflect'``.
cval (float, optional): Value to fill past edges of image. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, sigma: Union[float , Tuple[float], List[ Union[Tuple[float], float]]] = 0.8,
border_mode: str = "reflect", cval: float = 0,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.sigma = sigma
self.border_mode = border_mode
self.cval = cval
[docs]
def apply(self, img, **params):
return F.gaussian_blur(img, self.sigma, self.border_mode, self.cval)
[docs]
class RandomGaussianBlur(ImageOnlyTransform):
"""Performs Gaussian blur on the image with a random strength blurring.
In case of a multi-channel image, individual channels are blured separately.
Behaves similarly to GaussianBlur. The Gaussian sigma is randomly drawn from
the interval [min_sigma, s] for the respective s from ``max_sigma`` for each channel and dimension.
Internally, the ``scipy.ndimage.gaussian_filter`` function is used. The ``border_mode`` and ``cval``
arguments are forwarded to it. More details at:
https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.gaussian_filter.html.
Args:
max_sigma (float, Tuple(float), List[Tuple(float) | float] , optional): Maximum Gaussian sigma.
Must be either of: ``S``, ``(S_Z, S_Y, S_X)``, ``(S_Z, S_Y, S_X, S_T)``, ``[S_1, S_2, ..., S_C]``,
``[(S_Z1, S_Y1, S_X1), (S_Z2, S_Y2, S_X2), ..., (S_ZC, S_YC, S_XC)]``, or
``[(S_Z1, S_Y1, S_X1, S_T1), (S_Z2, S_Y2, S_X2, S_T2), ..., (S_ZC, S_YC, S_XC, S_TC)]``.
If a float, the spatial dimensions are blurred equivalently (equivalent to ``(S, S, S)``).
If a tuple, the sigmas for spatial dimensions and possibly the time dimension must be specified.
If a list, sigmas for each channel must be specified either as a single number or as a tuple.
Defaults to ``0.8``.
min_sigma (float, optional): Minimum Gaussian sigma for all channels and dimensions.
Defaults to ``0``.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'reflect'``.
cval (float, optional): Value to fill past edges of image. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, max_sigma: Union[float, TypeTripletFloat] = 0.8,
min_sigma: float = 0, border_mode: str = "reflect", cval: float = 0,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.max_sigma = parse_coefs(max_sigma)
self.min_sigma = min_sigma
self.border_mode = border_mode
self.cval = cval
[docs]
def apply(self, img, **params):
return F.gaussian_blur(img, params["sigma"], self.border_mode, self.cval)
[docs]
def get_params(self, **data):
if isinstance(self.max_sigma, (float, int)):
sigma = random.uniform(self.min_sigma, self.max_sigma)
elif isinstance(self.max_sigma, tuple):
sigma = tuple([random.uniform(self.min_sigma, self.max_sigma[i]) for i in range(len(self.max_sigma))])
else:
sigma = []
for channel in self.max_sigma:
if isinstance(channel, (float, int)):
sigma.append(random.uniform(self.min_sigma, channel))
else:
sigma.append(tuple([random.uniform(self.min_sigma, channel) for i in range(len(channel))]))
return {"sigma": sigma}
[docs]
class RandomGamma(ImageOnlyTransform):
"""Performs the gamma transformation with a randomly chosen gamma. If image values (in any channel) are outside
the [0,1] interval, this transformation is not performed.
Args:
gamma_limit (Tuple(float), optional): Interval from which gamma is selected.
Defaults to ``(0.8, 1.2)``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, gamma_limit: Tuple[float] = (0.8, 1.2),
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.gamma_limit = gamma_limit
[docs]
def apply(self, img, gamma=1, **params):
return F.gamma_transform(img, gamma=gamma)
[docs]
def get_params(self, **data):
return {"gamma": random.uniform(self.gamma_limit[0], self.gamma_limit[1])}
def __repr__(self):
return f'RandomGamma({self.gamma_limit}, {self.always_apply}, {self.p})'
[docs]
class RandomBrightnessContrast(ImageOnlyTransform):
"""Randomly change brightness and contrast of the input image.
Unlike ``RandomBrightnessContrast`` from `Albumentations`, this transform is using the
formula :math:`f(a) = (c+1) * a + b`, where :math:`c` is contrast and :math:`b` is brightness.
Args:
brightness_limit ((float, float) | float, optional): Interval from which the change in brightness is
randomly drawn. If the change in brightness is 0, the brightness will not change.
Must be either of: ``B``, ``(B1, B2)``.
If a float, the interval will be ``(-B, B)``.
Defaults to ``0.2``.
contrast_limit ((float, float) | float, optional): Interval from which the change in contrast is
randomly drawn. If the change in contrast is 1, the contrast will not change.
Must be either of: ``C``, ``(C1, C2)``.
If a float, the interval will be ``(-C, C)``.
Defaults to ``0.2``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, brightness_limit=0.2, contrast_limit=0.2, always_apply=False, p=0.5,):
super().__init__(always_apply, p)
self.brightness_limit = to_tuple(brightness_limit)
self.contrast_limit = to_tuple(contrast_limit)
[docs]
def apply(self, img, **params):
return F.brightness_contrast_adjust(img, params['alpha'], params['beta'])
[docs]
def get_params(self, **data):
return {
"alpha": 1.0 + random.uniform(self.contrast_limit[0], self.contrast_limit[1]),
"beta": 0.0 + random.uniform(self.brightness_limit[0], self.brightness_limit[1]),
}
def __repr__(self):
return f'RandomBrightnessContrast({self.brightness_limit}, {self.contrast_limit}, ' \
f'{self.always_apply}, {self.p})'
[docs]
class HistogramEqualization(ImageOnlyTransform):
"""Performs equalization of histogram. The equalization is done channel-wise, meaning that each channel is equalized
separately.
**Warning! Images are normalized over both spatial and temporal domains together. The output is in the range [0, 1].**
Args:
bins (int, optional): Number of bins for image histogram.
Defaults to ``256``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image
"""
def __init__(self, bins: int = 256, always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.bins = bins
[docs]
def apply(self, img, **params):
return F.histogram_equalization(img, self.bins)
[docs]
class Pad(DualTransform):
"""Pads the input.
Args:
pad_size (int | Tuple[int] | List[int | Tuple[int]]): Number of pixels padded to the edges of each axis.
Must be either of: ``P``, ``(P1, P2)``, ``[P_Z, P_Y, P_X]``, ``[P_Z, P_Y, P_X, P_T]``,
``[(P_Z1, P_Z2), (P_Y1, P_Y2), (P_X1, P_X2)]``, or
``[(P_Z1, P_Z2), (P_Y1, P_Y2), (P_X1, P_X2), (P_T1, P_T2)]``.
If an integer, it is equivalent to ``[(P, P), (P, P), (P, P)]``.
If a tuple, it is equivalent to ``[(P1, P2), (P1, P2), (P1, P2)]``.
If a list, it must specify padding for all spatial dimensions and possibly also for the time dimension.
The unspecified dimensions (C and possibly T) are not affected.
border_mode (str, optional): Values outside image domain are filled according to this mode.
Defaults to ``'constant'``.
ival (float | Sequence, optional): Values of `image` voxels outside of the `image` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``0``.
mval (float | Sequence, optional): Values of `mask` voxels outside of the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``0``.
ignore_index (float | None, optional): If a float, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``True``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, pad_size: Union[int, Tuple[int], List[Union[int, Tuple[int]]]], border_mode: str = 'constant',
ival: Union[float, Sequence] = 0, mval: Union[float, Sequence] = 0,
ignore_index: Union[float, None] = None, always_apply: bool = True, p : float = 1):
super().__init__(always_apply, p)
self.pad_size = pad_size
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
if not (ignore_index is None):
self.mask_mode = "constant"
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.pad_pixels(img, self.pad_size, self.border_mode, self.ival)
[docs]
def apply_to_mask(self, mask, **params):
return F.pad_pixels(mask, self.pad_size, self.mask_mode, self.mval, True)
def __repr__(self):
return f'Pad({self.pad_size}, {self.border_mode}, {self.ival}, {self.mval}, {self.always_apply}, ' \
f'{self.p})'
[docs]
class Normalize(ImageOnlyTransform):
"""Change image mean and standard deviation to the given values (channel-wise).
Args:
mean (float | List[float], optional): The desired channel-wise means.
Must be either of: ``M``, ``[M_1, M_2, ..., M_C]``.
Defaults to ``0``.
std (float | List[float], optional): The desired channel-wise standard deviations.
Must be either of: ``S``, ``[S_1, S_2, ..., S_C]``.
Defaults to ``1``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``True``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image
"""
def __init__(self, mean: Union[float, List[float]] = 0, std: Union[float, List[float]] = 1,
always_apply: bool = True, p: float = 1.0):
super().__init__(always_apply, p)
self.mean = mean
self.std = std
[docs]
def apply(self, img, **params):
return F.normalize(img, self.mean, self.std)
def __repr__(self):
return f'Normalize({self.mean}, {self.std}, {self.always_apply}, {self.p})'
[docs]
class Contiguous(DualTransform):
"""Transform the image data to a contiguous array.
Args:
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``True``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, always_apply: bool = True, p: float = 1.0):
super().__init__(always_apply, p)
[docs]
def apply(self, image, **params):
return np.ascontiguousarray(image)
[docs]
def apply_to_mask(self, mask, **params):
return np.ascontiguousarray(mask)
def __repr__(self):
return f'Contiguous({self.always_apply}, {self.p})'
[docs]
class Float(DualTransform):
"""Change datatype to ``np.float32`` without changing intensities.
Args:
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``True``.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float_mask
"""
def __init__(self, always_apply: bool = True, p: float = 1.0):
super().__init__(always_apply, p)
[docs]
def apply(self, image, **params):
return image.astype(np.float32)
[docs]
def apply_to_mask(self, mask, **params):
return mask.astype(np.float32)
def __repr__(self):
return f'Float({self.always_apply}, {self.p})'