# ============================================================================================= #
# Author: Pavel Iakubovskii, ZFTurbo, ashawkey, Dominik Müller, #
# Samuel Šuľan, Lucia Hradecká, Filip Lux, Jakub Polonský #
# 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 #
# Samuel Šuľan #
# Jakub Polonský #
# #
# 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. #
# ============================================================================================= #
from typing import List, Sequence, Tuple, Union, Optional
from warnings import warn
import numpy as np
from .utils import parse_limits, parse_coefs, parse_pads, to_tuple, get_spatio_temporal_domain_limit, \
get_spatial_shape_from_image, get_sigma_axiswise
from ..core.transforms_interface import DualTransform, ImageOnlyTransform
from ..augmentations import functional as F
from ..augmentations import functional_bbox as FB
from ..augmentations.sitk_utils import parse_itk_interpolation, get_affine_transform
from ..biovol_typing import *
from ..random_utils import uniform, sample_range_uniform, randint, shuffle, sample
##########################################################################################
# #
# GEOMETRIC TRANSFORMATIONS #
# #
##########################################################################################
[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. If the shape is invalid (i.e., it contains negative numbers
or zero), the transformation takes no effect.
Must be ``(Z, Y, X)``.
The unspecified dimensions (C and 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 the `image` domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside the domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
anti_aliasing_downsample (bool, optional): Apply the Gaussian filter before down-sampling. Recommended.
Defaults to ``True``.
ignore_index (float | None, optional): If specified, 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): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, shape: TypeSpatialShape, 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)
if (not isinstance(shape, Sequence)) or (len(shape) < 3):
self.shape = None
warn(f'Resize is skipped due to invalid input shape {shape}.')
else:
if len(shape) > 3 and np.all(np.asarray(shape[:3]) > 0):
shape = shape[:3]
warn(f'Resize: ignoring additional shape values, resizing to shape {shape}.')
self.shape = tuple(shape)
if np.any(np.asarray(self.shape) <= 0):
self.shape = None
warn(f'Resize is skipped due to invalid input shape values: {self.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=params['new_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=params['new_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=params['new_shape'], interpolation=self.interpolation,
border_mode=self.mask_mode, cval=self.mval, anti_aliasing_downsample=False,
mask=True)
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.resize_keypoints(keypoints,
domain_limit=params['domain_limit'],
new_shape=params['new_shape'])
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.resize_bboxes(bboxes,
domain_limit=params['domain_limit'],
new_shape=params['new_shape'])
[docs]
def get_params(self, targets, **data):
# read shape of the original image
domain_limit: TypeSpatialShape = get_spatio_temporal_domain_limit(data, targets)[:3]
return {
'domain_limit': domain_limit, 'new_shape': self.shape
}
def __repr__(self):
return f'Resize(shape={self.shape}, interpolation={self.interpolation}, border_mode={self.border_mode}, ' \
f'ival={self.ival}, mval={self.mval}, anti_aliasing_downsample={self.anti_aliasing_downsample}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class Rescale(DualTransform):
""" Rescale the input and change its shape accordingly.
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:
scales (float|List[float], optional): Value by which the input should be scaled.
Must be either of: ``S``, ``[S_Z, S_Y, S_X]``. All values must be positive; values <=0 will be ignored
and the respective axis will not be scaled.
If a float, then all spatial dimensions are scaled by it (equivalent to ``[S, S, S]``).
The unspecified dimensions (C and T) are not affected.
Defaults to ``1``.
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 the `image` domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside the domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
anti_aliasing_downsample (bool, optional): Apply the Gaussian filter before down-sampling. Recommended.
Defaults to ``True``.
ignore_index (float | None, optional): If specified, 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): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, scales=1, interpolation: int = 1, border_mode: str = 'reflect', ival: float = 0,
mval: float = 0, anti_aliasing_downsample: bool = True, ignore_index=None,
always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.scale = tuple([(s if s > 0 else 1) for s in parse_coefs(scales, identity_element=1.)])
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=params['new_shape'], interpolation=self.interpolation, cval=self.ival,
border_mode=self.border_mode, anti_aliasing_downsample=self.anti_aliasing_downsample)
[docs]
def apply_to_mask(self, mask, **params):
return F.resize(mask, input_new_shape=params['new_shape'], interpolation=0, cval=self.mval,
border_mode=self.mask_mode, anti_aliasing_downsample=False, mask=True)
[docs]
def apply_to_float_mask(self, mask, **params):
return F.resize(mask, input_new_shape=params['new_shape'], interpolation=self.interpolation, cval=self.mval,
border_mode=self.mask_mode, anti_aliasing_downsample=False, mask=True)
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.resize_keypoints(keypoints,
domain_limit=params['domain_limit'],
new_shape=params['new_shape'])
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.resize_bboxes(bboxes,
domain_limit=params['domain_limit'],
new_shape=params['new_shape'])
[docs]
def get_params(self, targets, **data):
# read shape of the original image
domain_limit: TypeSpatialShape = get_spatio_temporal_domain_limit(data, targets)[:3]
# compute shape of the resized image
new_shape: TypeSpatialShape = tuple((np.asarray(domain_limit) * np.asarray(self.scale)).tolist())
# Zero or negative check
if np.any(np.asarray(new_shape) <= 0):
warn(f'Rescale(): rescaling to a shape with zero or negative numbers ({new_shape}), continuing without Rescale().',
UserWarning)
new_shape = None
return {
'domain_limit': domain_limit,
'new_shape': new_shape,
}
def __repr__(self):
return f'Rescale(scales={self.scale}, interpolation={self.interpolation}, border_mode={self.border_mode}, ' \
f'ival={self.ival}, mval={self.mval}, anti_aliasing_downsample={self.anti_aliasing_downsample}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class Scale(DualTransform):
"""Rescale the input image content by the given scale. The image shape remains unchanged.
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]``. All values must be positive; values <=0 will be ignored
and the respective axis will not be scaled.
If a float, then all spatial dimensions are scaled by it (equivalent to ``[S, S, S]``).
The unspecified dimensions (C and T) are not affected.
Defaults to ``1``.
interpolation (str, optional): SimpleITK interpolation type for `image` and `float_mask`.
Must be one of ``linear``, ``nearest``, ``bspline``, ``gaussian``.
For `mask`, the ``nearest`` interpolation is always used.
Defaults to ``linear``.
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``.
ival (float, optional): Value of `image` voxels outside the `image` domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside the domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
ignore_index (float | None, optional): If specified, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
keep_all (bool, optional): When true, ALL keypoints and bounding boxes will be kept,
even when they fully lie outside the image domain. Overrides `min_volume` and `min_percentage`.
Defaults to ``False``.
min_volume (float, optional): Volume threshold below which bounding boxes will be removed.
Is mutually exclusive with `min_percentage`.
Defaults to ``None``
min_percentage (float, optional): Percentage threshold below which bounding boxes will be removed.
Value between 0.0 and 1.0.
Is mutually exclusive with `min_volume`.
Defaults to ``None``
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, scales: Union[float, TypeTripletFloat] = 1,
interpolation: str = 'linear', spacing: Union[float, TypeTripletFloat] = None,
ival: float = 0, mval: float = 0,
ignore_index: Union[float, None] = None,
keep_all: bool = False, min_volume: float = None, min_percentage: float = None,
always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.scale = tuple([(s if s > 0 else 1) for s in parse_coefs(scales, identity_element=1.)])
self.interpolation: str = parse_itk_interpolation(interpolation)
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1.)
self.ival = ival
self.mval = mval
if min_volume is not None and min_percentage is not None:
raise ValueError("min_volume and min_percentage are mutually exclusive")
self.keep_all = keep_all
self.min_volume = min_volume
self.min_percentage = min_percentage
if not (ignore_index is None):
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.affine(img,
transform=params['affine_transform'],
interpolation=self.interpolation,
value=self.ival,
spacing=params['spacing'])
[docs]
def apply_to_mask(self, mask, **params):
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
return F.affine(np.expand_dims(mask, 0),
transform=params['affine_transform'],
interpolation=interpolation,
value=self.mval,
spacing=params['spacing'])[0]
[docs]
def apply_to_float_mask(self, mask, **params):
return F.affine(np.expand_dims(mask, 0),
transform=params['affine_transform'],
interpolation=self.interpolation,
value=self.mval,
spacing=params['spacing'])[0]
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
transform=params['affine_transform'],
domain_limit=params['domain_limit'],
keep_all=self.keep_all)
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.affine_bboxes(bboxes,
transform=params['affine_transform'],
domain_limit=params['domain_limit'],
min_percentage=self.min_percentage,
min_volume=self.min_volume,
keep_all=self.keep_all)
[docs]
def get_params(self, targets, **data):
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
# get affine transformation
# we need to change the order of axes of all parameters to XYZ
affine_transform = get_affine_transform(domain_limit[2::-1] + (domain_limit[3],), # domain_limit is image shape without the channel axis, must be [X, Y, Z, T]
scales=self.scale[::-1],
degrees=(0, 0, 0),
translation=(0, 0, 0),
spacing=self.spacing[::-1])
return {
'domain_limit': domain_limit,
'affine_transform': affine_transform,
'spacing': self.spacing[::-1],
}
def __repr__(self):
return f'Scale(scales={self.scale}, interpolation={self.interpolation}, spacing={self.spacing}, ' \
f'ival={self.ival}, mval={self.mval}, always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomScale(DualTransform):
"""Randomly rescale the input image content by the given scale. The image shape remains unchanged.
Args:
scaling_limit (float | 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)``. All values must be positive; values <=0 will be ignored
and the respective axis will not be scaled.
If a float ``S``, then all spatial dimensions are scaled by a random number drawn uniformly from
the interval [1/S, S] (equivalent to inputting ``(1/S, S, 1/S, S, 1/S, 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, S_a] is constructed for each spatial
dimension and the scale is randomly drawn from it
(equivalent to inputting ``(1/S_Z, S_Z, 1/S_Y, S_Y, 1/S_X, 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 ``(1.1)``.
interpolation (str, optional): SimpleITK interpolation type for `image` and `float_mask`.
Must be one of ``linear``, ``nearest``, ``bspline``, ``gaussian``.
For `mask`, the ``nearest`` interpolation is always used.
Defaults to ``linear``.
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``.
ival (float, optional): Value of `image` voxels outside the `image` domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside the domain.
Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
ignore_index (float | None, optional): If specified, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
keep_all (bool, optional): When true, ALL keypoints and bounding boxes will be kept,
even when they fully lie outside the image domain. Overrides `min_volume` and `min_percentage`.
Defaults to ``False``.
min_volume (float, optional): Volume threshold below which bounding boxes will be removed.
Is mutually exclusive with `min_percentage`.
Defaults to ``None``
min_percentage (float, optional): Percentage threshold below which bounding boxes will be removed.
Value between 0.0 and 1.0.
Is mutually exclusive with `min_volume`.
Defaults to ``None``
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, scaling_limit: Union[float, TypePairFloat, TypeTripletFloat, TypeSextetFloat] = (0.9, 1.1),
interpolation: str = 'linear', spacing: Union[float, TypeTripletFloat] = None,
ival: float = 0, mval: float = 0, ignore_index: Union[float, None] = None,
keep_all: bool = False, min_volume: float = None, min_percentage: float = None,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.scaling_limit: TypeSextetFloat = tuple([(s if s > 0 else 1)
for s in parse_limits(scaling_limit, scale=True)])
self.interpolation: str = parse_itk_interpolation(interpolation)
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1.)
self.ival: float = ival
self.mval: float = mval
if min_volume is not None and min_percentage is not None:
raise ValueError("min_volume and min_percentage are mutually exclusive")
self.keep_all = keep_all
self.min_volume = min_volume
self.min_percentage = min_percentage
if not (ignore_index is None):
self.mval = ignore_index
[docs]
def get_params(self, targets, **data):
# set parameters of the transform
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
scale = sample_range_uniform(self.scaling_limit)
# get affine transformation
# we need to change the order of axes of all parameters to XYZ
affine_transform = get_affine_transform(domain_limit[2::-1] + (domain_limit[3],), # domain_limit is image shape without the channel axis, must be [X, Y, Z, T]
scales=scale[::-1],
degrees=(0, 0, 0),
translation=(0, 0, 0),
spacing=self.spacing[::-1])
return {
'domain_limit': domain_limit,
'scale': scale,
'affine_transform': affine_transform,
'spacing': self.spacing[::-1],
}
[docs]
def apply(self, img, **params):
return F.affine(img,
transform=params['affine_transform'],
interpolation=self.interpolation,
value=self.ival,
spacing=params['spacing'])
[docs]
def apply_to_mask(self, mask, **params):
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
return F.affine(np.expand_dims(mask, 0),
transform=params['affine_transform'],
interpolation=interpolation,
value=self.mval,
spacing=params['spacing'])[0]
[docs]
def apply_to_float_mask(self, mask, **params):
return F.affine(np.expand_dims(mask, 0),
transform=params['affine_transform'],
interpolation=self.interpolation,
value=self.mval,
spacing=params['spacing'])[0]
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
transform=params['affine_transform'],
domain_limit=params['domain_limit'],
keep_all=self.keep_all)
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.affine_bboxes(bboxes,
transform=params['affine_transform'],
domain_limit=params['domain_limit'],
min_percentage=self.min_percentage,
min_volume=self.min_volume,
keep_all=self.keep_all)
def __repr__(self):
return f'RandomScale(scaling_limit={self.scaling_limit}, interpolation={self.interpolation}, ' \
f'spacing={self.spacing}, ival={self.ival}, mval={self.mval}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomRotate90(DualTransform):
"""Rotation of input by 0, 90, 180, or 270 degrees around the specified spatial axes.
Note:
**This transformation assumes the data is isotropic.**
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.
If ``None``, will be rotated around all spatial axes.
Defaults to ``None``.
shuffle_axis (bool, optional): If set to ``True``, the order of rotations is random.
Defaults to ``False``.
factor (int, optional): The number of times the array is rotated by 90 degrees. Only non-negative integer
factors are supported. If ``None``, the factor will be chosen randomly from the interval [0, 3].
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, axes: List[int] = None, shuffle_axis: bool = False, factor: Optional[int | List[int]] = None,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.axes = axes
if self.axes is None:
self.axes = [1, 2, 3] # rotate around all spatial axes if not specified by the user
if (factor is not None) and (not isinstance(factor, int)) and (len(self.axes) != len(factor)):
raise ValueError(f'Lengths of the "axes" and "factor" arguments are not equal: '
f'{len(self.axes)} vs {len(factor)}')
# Create all combinations for rotating
axes_to_rotate = {1: (3, 2), 2: (1, 3), 3: (2, 1)}
self.rotation_around = []
for i in self.axes:
if i in axes_to_rotate.keys():
self.rotation_around.append(axes_to_rotate[i])
self.shuffle_axis = shuffle_axis
# save factor: modulo; negative factors are ignored
def fix_factor(f):
if f < 0:
return 0
return f % 4
if isinstance(factor, int):
factor = fix_factor(factor)
if isinstance(factor, list) or isinstance(factor, tuple):
factor = [fix_factor(f) for f in factor]
self.factor = factor
[docs]
def apply(self, img, **params):
for rot, factor in zip(params['rotation_around'], params['factor']):
img = np.rot90(img, factor, axes=rot)
return img
[docs]
def apply_to_mask(self, mask, **params):
for rot, factor in zip(params['rotation_around'], params['factor']):
mask = np.rot90(mask, factor, axes=(rot[0] - 1, rot[1] - 1))
return mask
[docs]
def apply_to_keypoints(self, keypoints, **params):
img_shape = params['img_shape']
for rot, factor in zip(params['rotation_around'], params['factor']):
keypoints, img_shape = F.rot90_keypoints(keypoints, factor=factor, axes=(rot[0], rot[1]),
img_shape=img_shape)
return keypoints
[docs]
def apply_to_bboxes(self, bboxes, **params):
img_shape = params['img_shape']
for rot, factor in zip(params['rotation_around'], params['factor']):
bboxes, img_shape = FB.rot90_bboxes(bboxes,
factor=factor,
axes=(rot[0], rot[1]),
img_shape=img_shape)
return bboxes
[docs]
def get_params(self, targets, **data):
# Shuffle the order of rotation axes
rotation_around = np.array(self.rotation_around, copy=True)
if self.shuffle_axis:
shuffle(rotation_around)
# If not specified, choose the angle to rotate
if self.factor is None:
factor = list(randint(0, 3, size=len(self.axes)))
elif isinstance(self.factor, int) or isinstance(self.factor, float):
factor = [self.factor] * len(self.axes)
else:
factor = self.factor
img_shape = get_spatial_shape_from_image(data, targets)
return {'factor': factor,
'rotation_around': rotation_around,
'img_shape': img_shape}
def __repr__(self):
return f'RandomRotate90(axes={self.axes}, shuffle_axis={self.shuffle_axis}, factor={self.factor}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class Flip(DualTransform):
"""Flip input along the specified spatial axes.
Args:
axes (List[int], optional): List of axes along which is flip done. Recognised axis symbols are
``1`` for Z, ``2`` for Y, and ``3`` for X. If ``None``, will be flipped around all spatial axes.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, axes: List[int] = None, always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
if axes is not None and any(x not in (1, 2, 3) for x in axes):
axes = [x for x in axes if x in (1, 2, 3)]
warn(f'Flip: Ignoring invalid axis indices, using axes={axes}.')
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 apply_to_keypoints(self, keypoints, **params):
return F.flip_keypoints(keypoints,
axes=params['axes'],
img_shape=params['img_shape'])
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.flip_bboxes(bboxes,
axes=params['axes'],
img_shape=params['img_shape'])
[docs]
def get_params(self, targets, **data):
# Use all spatial axes if not specified otherwise:
axes = [1, 2, 3] if self.axes is None else self.axes
# Get image shape (needed for keypoints):
img_shape = get_spatial_shape_from_image(data, targets)
return {'axes': axes,
'img_shape': img_shape}
def __repr__(self):
return f'Flip(axes={self.axes}, always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomFlip(DualTransform):
"""Flip input along a set of axes randomly chosen from the input list of axis combinations.
Args:
axes_to_choose (List[int], Tuple[int], or None, optional): List of axis indices from which some are randomly
chosen. Recognised axis symbols are ``1`` for Z, ``2`` for Y, and ``3`` for X. The image will be
flipped along the chosen axes.
If ``None``, a random subset of spatial axes is chosen, corresponding to inputting
``[1, 2, 3]``.
If an empty list or an empty tuple, the transformation takes no effect.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, axes_to_choose: Union[None, List[int], Tuple[int]] = None, always_apply: bool = False,
p: float = 0.5):
super().__init__(always_apply, p)
if axes_to_choose is not None and any(x not in (1, 2, 3) for x in axes_to_choose):
axes_to_choose = [x for x in axes_to_choose if x in (1, 2, 3)]
warn(f'RandomFlip: Ignoring invalid axis indices, using axes_to_choose={axes_to_choose}.')
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, (np.asarray(params['axes']) - 1).tolist())
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.flip_keypoints(keypoints,
axes=params['axes'],
img_shape=params['img_shape'])
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.flip_bboxes(bboxes,
axes=params['axes'],
img_shape=params['img_shape'])
[docs]
def get_params(self, targets, **data):
if isinstance(self.axes, Sequence) and len(self.axes) == 0:
axes = []
else:
# Use all spatial axes if not specified otherwise:
to_choose = [1, 2, 3] if self.axes is None else self.axes
# Randomly choose some axes from the given list:
axes = sample(population=to_choose, k=randint(0, len(to_choose)))
# Get image shape (needed for keypoints):
img_shape = get_spatial_shape_from_image(data, targets)
return {'axes': axes,
'img_shape': img_shape}
def __repr__(self):
return f'RandomFlip(axes_to_choose={self.axes}, always_apply={self.always_apply}, p={self.p})'
[docs]
class CenterCrop(DualTransform):
"""Crop the central region of the given size from the input.
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 output shape.
Must be ``[Z, Y, X]``.
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 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 the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
ignore_index (float | None, optional): If specified, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
keep_all (bool, optional): When true, ALL keypoints and bounding boxes will be kept,
even when they fully lie outside the image domain. Overrides `min_volume` and `min_percentage`.
Defaults to ``False``.
min_volume (float, optional): Volume threshold below which bounding boxes will be removed.
Is mutually exclusive with `min_percentage`.
Defaults to ``None``.
min_percentage (float, optional): Percentage threshold below which bounding boxes will be removed.
Value between 0.0 and 1.0.
Is mutually exclusive with `min_volume`.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, shape: TypeSpatialShape, border_mode: str = 'reflect',
ival: Union[Sequence[float], float] = (0, 0),
mval: Union[Sequence[float], float] = (0, 0), ignore_index: Union[float, None] = None,
keep_all: bool = False, min_volume: float = None, min_percentage: float = None,
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
if (not isinstance(shape, Sequence)) or (len(shape) < 3):
self.output_shape = None
warn(f'CenterCrop is skipped due to invalid input shape {shape}.')
else:
if len(shape) > 3 and np.all(np.asarray(shape[:3]) > 0):
shape = shape[:3]
warn(f'CenterCrop: ignoring additional shape values, cropping to shape {shape}.')
self.output_shape = tuple(shape)
if np.any(np.asarray(self.output_shape) <= 0):
self.output_shape = None
warn(f'CenterCrop is skipped due to invalid input shape values: {self.output_shape}.')
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
if min_volume is not None and min_percentage is not None:
raise ValueError("min_volume and min_percentage are mutually exclusive")
self.keep_all = keep_all
self.min_volume = min_volume
self.min_percentage = min_percentage
if not (ignore_index is None):
self.mask_mode = 'constant'
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.crop(img,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
border_mode=self.border_mode, cval=self.ival, mask=False)
[docs]
def apply_to_mask(self, mask, **params):
return F.crop(mask,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
border_mode=self.mask_mode, cval=self.mval, mask=True)
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.crop_keypoints(keypoints,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
keep_all=self.keep_all)
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.crop_bboxes(bboxes,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
keep_all=self.keep_all,
min_volume=self.min_volume,
min_percentage=self.min_percentage)
[docs]
def get_params(self, targets, **data):
if self.output_shape is None:
return {'crop_position': None, 'pad_dims': None}
# Get crop coordinates:
# 1. Original image shape
img_spatial_shape = get_spatial_shape_from_image(data, targets)
# 2. Position of the corner closest to the image origin when cropping from the center of the image
position: TypeSpatialCoordinate = (img_spatial_shape - np.asarray(self.output_shape, dtype=np.intc)) // 2
position = np.maximum(position, 0).astype(int)
# 3. Padding size if necessary
pad_dims = F.get_pad_dims(img_spatial_shape, self.output_shape)
return {'crop_position': position,
'pad_dims': pad_dims}
def __repr__(self):
return f'CenterCrop(shape={self.output_shape}, border_mode={self.border_mode}, ival={self.ival}, ' \
f'mval={self.mval}, always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomCrop(DualTransform):
"""Randomly crop a region of the 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 output shape.
Must be ``[Z, Y, X]``.
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 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 the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``(0, 0)``.
ignore_index (float | None, optional): If specified, then transformation of `mask` is done with
``border_mode = 'constant'`` and ``mval = ignore_index``.
If ``None``, this argument is ignored.
Defaults to ``None``.
keep_all (bool, optional): When true, ALL keypoints and bounding boxes will be kept,
even when they fully lie outside the image domain. Overrides `min_volume` and `min_percentage`.
Defaults to ``False``.
min_volume (float, optional): Volume threshold below which bounding boxes will be removed.
Is mutually exclusive with `min_percentage`.
Defaults to ``None``.
min_percentage (float, optional): Percentage threshold below which bounding boxes will be removed.
Value between 0.0 and 1.0.
Is mutually exclusive with `min_volume`.
Defaults to ``None``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, shape: TypeSpatialShape, border_mode: str = 'reflect',
ival: Union[Sequence[float], float] = (0, 0),
mval: Union[Sequence[float], float] = (0, 0), ignore_index: Union[float, None] = None,
keep_all: bool = False, min_volume: float = None, min_percentage: float = None,
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
if (not isinstance(shape, Sequence)) or (len(shape) < 3):
self.output_shape = None
warn(f'RandomCrop is skipped due to invalid input shape {shape}.')
else:
if len(shape) > 3 and np.all(np.asarray(shape[:3]) > 0):
shape = shape[:3]
warn(f'RandomCrop: ignoring additional shape values, cropping to shape {shape}.')
self.output_shape = tuple(shape)
if np.any(np.asarray(self.output_shape) <= 0):
self.output_shape = None
warn(f'RandomCrop is skipped due to invalid input shape values: {self.output_shape}.')
self.border_mode = border_mode
self.mask_mode = border_mode
self.ival = ival
self.mval = mval
if min_volume is not None and min_percentage is not None:
raise ValueError("min_volume and min_percentage are mutually exclusive")
self.keep_all = keep_all
self.min_volume = min_volume
self.min_percentage = min_percentage
if not (ignore_index is None):
self.mask_mode = 'constant'
self.mval = ignore_index
[docs]
def apply(self, img, **params):
return F.crop(img,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
border_mode=self.border_mode, cval=self.ival, mask=False)
[docs]
def apply_to_mask(self, mask, **params):
return F.crop(mask,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
border_mode=self.mask_mode, cval=self.mval, mask=True)
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.crop_keypoints(keypoints,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
keep_all=self.keep_all)
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.crop_bboxes(bboxes,
crop_shape=self.output_shape,
crop_position=params['crop_position'],
pad_dims=params['pad_dims'],
keep_all=self.keep_all,
min_volume=self.min_volume,
min_percentage=self.min_percentage)
[docs]
def get_params(self, targets, **data):
if self.output_shape is None:
return {'crop_position': None, 'pad_dims': None}
# Get crop coordinates:
# 1. Original image shape
img_spatial_shape = get_spatial_shape_from_image(data, targets)
# 2. Position of the corner closest to the image origin, positioned randomly so that the whole crop is
# within the image domain if possible
ranges: TypeSpatialShape = np.maximum(img_spatial_shape - np.asarray(self.output_shape, dtype=np.intc), 0)
position = randint(0, ranges)
# 3. Padding size if necessary
pad_dims = F.get_pad_dims(img_spatial_shape, self.output_shape)
return {'crop_position': position,
'pad_dims': pad_dims}
def __repr__(self):
return f'RandomCrop(shape={self.output_shape}, border_mode={self.border_mode}, ival={self.ival}, ' \
f'mval={self.mval}, always_apply={self.always_apply}, p={self.p})'
[docs]
class Pad(DualTransform):
"""Pad the input.
Internally, the ``numpy.pad`` function is used. The ``border_mode``, ``ival`` and ``mval``
arguments are forwarded to it. More details at:
https://numpy.org/doc/stable/reference/generated/numpy.pad.html.
Args:
pad_size (int | Tuple[int]): Number of pixels padded to the edges of each axis. Negative padding values are
ignored.
Must be either of: ``P``, ``(P1, P2)``, or ``(P_Z1, P_Z2, P_Y1, P_Y2, P_X1, P_X2)``.
If an integer, it is equivalent to ``(P, P, P, P, P, P)``.
If a tuple of two numbers, it is equivalent to ``(P1, P2, P1, P2, P1, P2)``.
Otherwise, it must specify padding for all spatial dimensions.
The unspecified dimensions (C and 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 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 the `mask` domain.
Only applied when ``border_mode = 'constant'`` or ``border_mode = 'linear_ramp'``.
Defaults to ``0``.
ignore_index (float | None, optional): If specified, 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): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image, mask, float mask, key points, bounding boxes
"""
def __init__(self, pad_size: Union[int, TypePairInt, TypeSextetInt],
border_mode: str = 'constant', ival: Union[float, Sequence] = 0, mval: Union[float, Sequence] = 0,
ignore_index: Union[float, None] = None, always_apply: bool = False, p: float = 1):
super().__init__(always_apply, p)
self.pad_size: TypeSextetInt = tuple([(pds if pds >= 0 else 0) for pds in parse_pads(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)
[docs]
def apply_to_keypoints(self, keypoints, **params):
return F.pad_keypoints(keypoints, self.pad_size)
[docs]
def apply_to_bboxes(self, bboxes, **params):
return FB.pad_bboxes(bboxes, self.pad_size)
def __repr__(self):
return f'Pad(pad_size={self.pad_size}, border_mode={self.border_mode}, ival={self.ival}, mval={self.mval}, ' \
f'always_apply={self.always_apply}, p={self.p})'
##########################################################################################
# #
# INTENSITY-BASED TRANSFORMATIONS (LOCAL) #
# #
##########################################################################################
[docs]
class GaussianBlur(ImageOnlyTransform):
"""Perform Gaussian blurring of an 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): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, sigma: Union[float, tuple, List[Union[tuple, 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)
def __repr__(self):
return f'GaussianBlur(sigma={self.sigma}, border_mode={self.border_mode}, cval={self.cval}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomGaussianBlur(ImageOnlyTransform):
"""Perform Gaussian blurring of an image with a random blurring strength.
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): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, max_sigma: Union[float, tuple, List[Union[float, tuple]]] = 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 = max_sigma # parse_coefs(max_sigma, d4=True)
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, targets, **data):
if isinstance(self.max_sigma, (float, int, tuple)):
# Randomly choose a single sigma for all axes and channels OR a sigma for each axis (except the C axis)
sigma = get_sigma_axiswise(self.min_sigma, self.max_sigma)
else:
# max_sigma is list --> randomly choose sigmas for each channel
sigma = [get_sigma_axiswise(self.min_sigma, channel) for channel in self.max_sigma]
return {'sigma': sigma}
def __repr__(self):
return f'RandomGaussianBlur(max_sigma={self.max_sigma}, min_sigma={self.min_sigma}, ' \
f'border_mode={self.border_mode}, cval={self.cval}, always_apply={self.always_apply}, p={self.p})'
[docs]
class RemoveBackgroundGaussian(ImageOnlyTransform):
"""
Remove background from the input image by subtracting a blurred image from the original image.
The background image is created using Gaussian blurring. 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 ``10``.
mode (str, optional): Mode of background removal. Possible values:
``'default'`` (subtract blurred image from the input image),
``'bright_objects'`` (subtract the point-wise minimum of (blurred image, input image) from the input image),
``'dark_objects'`` (subtract the input image from the point-wise maximum of (blurred image, input image)).
Defaults to ``'default'``.
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): Probability of applying this transformation in composition.
Defaults to ``1.0``.
Targets:
image
"""
def __init__(self, sigma: Union[float, tuple, List[Union[tuple, float]]] = 10, mode: str = 'default',
border_mode: str = 'reflect', cval: float = 0,
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
self.sigma = sigma
self.mode = mode
self.border_mode = border_mode
self.cval = cval
[docs]
def apply(self, img, **params):
background = F.gaussian_blur(img, self.sigma, self.border_mode, self.cval)
if self.mode == 'bright_objects':
return img - np.minimum(background, img)
if self.mode == 'dark_objects':
return np.maximum(background, img) - img
return img - background
def __repr__(self):
return f'RemoveBackgroundGaussian(sigma={self.sigma}, mode={self.mode}, border_mode={self.border_mode}, ' \
f'cval={self.cval}, always_apply={self.always_apply}, p={self.p})'
##########################################################################################
# #
# INTENSITY-BASED TRANSFORMATIONS (POINT) #
# #
##########################################################################################
[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): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, brightness_limit: Union[float, TypePairFloat] = 0.2,
contrast_limit: Union[float, TypePairFloat] = 0.2,
always_apply: bool = False, p: float = 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, targets, **data):
# Get transformation parameters:
return {
'alpha': 1.0 + uniform(self.contrast_limit[0], self.contrast_limit[1]),
'beta': 0.0 + uniform(self.brightness_limit[0], self.brightness_limit[1]),
}
def __repr__(self):
return f'RandomBrightnessContrast(brightness_limit={self.brightness_limit}, ' \
f'contrast_limit={self.contrast_limit}, always_apply={self.always_apply}, p={self.p})'
[docs]
class RandomGamma(ImageOnlyTransform):
"""Perform the gamma transformation with a randomly chosen gamma.
Note:
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): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, gamma_limit: TypePairFloat = (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, targets, **data):
return {'gamma': uniform(self.gamma_limit[0], self.gamma_limit[1])}
def __repr__(self):
return f'RandomGamma(gamma_limit={self.gamma_limit}, always_apply={self.always_apply}, p={self.p})'
[docs]
class HistogramEqualization(ImageOnlyTransform):
"""Equalize image histogram. The equalization is done channel-wise, meaning that each channel is equalized
separately.
Note:
**Warning! Images are normalized over the spatial and temporal axes together. The output is in 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): Probability 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)
def __repr__(self):
return f'HistogramEqualization(bins={self.bins}, always_apply={self.always_apply}, p={self.p})'
[docs]
class GaussianNoise(ImageOnlyTransform):
"""Add 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): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, var_limit: TypePairFloat = (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, targets, **params):
# Choose noise standard deviation randomly (noise mean is given deterministically)
var = uniform(self.var_limit[0], self.var_limit[1])
sigma = var ** 0.5
return {'sigma': sigma}
def __repr__(self):
return f'GaussianNoise(var_limit={self.var_limit}, mean={self.mean}, ' \
f'always_apply={self.always_apply}, p={self.p})'
[docs]
class PoissonNoise(ImageOnlyTransform):
"""Add Poisson noise to the image.
Args:
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``0.5``.
Targets:
image
"""
def __init__(self, always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
[docs]
def apply(self, img, **params):
return F.poisson_noise(img)
def __repr__(self):
return f'PoissonNoise(always_apply={self.always_apply}, p={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`` (for single-channel images),
``[M_1, M_2, ..., M_C]`` (for multi-channel images).
Defaults to ``0``.
std (float | List[float], optional): The desired channel-wise standard deviations.
Must be either of: ``S`` (for single-channel images),
``[S_1, S_2, ..., S_C]`` (for multi-channel images).
Defaults to ``1``.
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image
"""
def __init__(self, mean: Union[float, List[float], Tuple[float]] = 0,
std: Union[float, List[float], Tuple[float]] = 1,
always_apply: bool = False, 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(mean={self.mean}, std={self.std}, always_apply={self.always_apply}, p={self.p})'
[docs]
class NormalizeMeanStd(ImageOnlyTransform):
"""Normalize image values to mean 0 and standard deviation 1, given the channel 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 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`` (for single-channel images),
``(M_1, M_2, ..., M_C)`` (for multi-channel images).
std (float | List[float]): Channel-wise image standard deviation.
Must be either of: ``S`` (for single-channel images),
``(S_1, S_2, ..., S_C)`` (for multi-channel images).
always_apply (bool, optional): Always apply this transformation in composition.
Defaults to ``False``.
p (float, optional): Probability of applying this transformation in composition.
Defaults to ``1``.
Targets:
image
"""
def __init__(self, mean: Union[tuple, float], std: Union[tuple, float],
always_apply: bool = False, p: float = 1.0):
super().__init__(always_apply, p)
self.mean: np.ndarray = np.array(mean, dtype=np.float32)
self.std: np.ndarray = np.array(std, dtype=np.float32)
assert self.mean.shape == self.std.shape
# Compute the formula denominator once as it is computationally expensive:
self.denominator = np.reciprocal(self.std, dtype=np.float32)
if len(self.mean.shape) == 0: # shapes of self.mean and self.denominator are the same
self.mean = self.mean[..., None]
self.denominator = self.denominator[..., None]
[docs]
def apply(self, image, **params):
return F.normalize_mean_std(image, self.mean, self.denominator)
def __repr__(self):
return f'NormalizeMeanStd(mean={self.mean}, std={self.std}, always_apply={self.always_apply}, p={self.p})'