Newer
Older
# ============================================================================================= #
# 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. #
# ============================================================================================= #
Lucia D. Hradecka
committed
from typing import List, Sequence, Tuple, Union, Optional
from .utils import parse_limits, parse_coefs, parse_pads, to_tuple, get_spatio_temporal_domain_limit,\
Lucia D. Hradecka
committed
to_spatio_temporal, get_spatial_shape_from_image, get_sigma_axiswise
from src.core.transforms_interface import DualTransform, ImageOnlyTransform
from src.augmentations import functional as F
from src.augmentations.sitk_utils import parse_itk_interpolation
from src.biovol_typing import *
from src.random_utils import uniform, sample_range_uniform, randint, shuffle, sample
##########################################################################################
# #
# GEOMETRIC TRANSFORMATIONS #
# #
##########################################################################################
# TODO anti_aliasing_downsample keep parameter or remove?
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.
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 of the `image` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside of the 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, 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)
self.shape: TypeSpatioTemporalCoordinate = to_spatio_temporal(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
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)
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)
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 apply_to_keypoints(self, keypoints, **params):
return F.resize_keypoints(keypoints,
domain_limit=params['domain_limit'],
new_shape=self.shape)
"""
def apply_to_bboxes(self, bboxes, **params):
for bbox in bboxes:
new_bbox = F.resize_keypoints(bbox,
input_new_shape=self.shape,
original_shape=params['original_shape'],
keep_all=True)
if validate_bbox(bbox, new_bbox, min_overlay_ratio):
res.append(new_bbox)
return res
"""
Lucia D. Hradecka
committed
def get_params(self, targets, **data):
Lucia D. Hradecka
committed
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
'domain_limit': domain_limit,
Lucia D. Hradecka
committed
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})'
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
class Rescale(DualTransform):
""" Rescales the input and changes 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]``.
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 of the `image` domain. Only applied when ``border_mode = 'constant'``.
Defaults to ``0``.
mval (float, optional): Value of `mask` and `float_mask` voxels outside of the 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 ``True``.
p (float, optional): Chance 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 = True, p: float = 1, **kwargs):
super().__init__(always_apply, p)
self.scale = 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'
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
self.mval = ignore_index
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)
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)
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)
def apply_to_keypoints(self, keypoints, **params):
return F.resize_keypoints(keypoints,
domain_limit=params['domain_limit'],
new_shape=params['new_shape'])
"""
def apply_to_bboxes(self, bboxes, **params):
for bbox in bboxes:
new_bbox = F.resize_keypoints(bbox,
input_new_shape=params['new_shape'],
original_shape=params['original_shape'],
keep_all=True)
if validate_bbox(bbox, new_bbox, min_overlay_ratio):
res.append(new_bbox)
return res
"""
def get_params(self, targets, **data):
# read shape of the original image
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
# compute shape of the resize dimage
# TODO +(0,) because of the F.resize error/hotfix
new_shape = tuple(np.asarray(domain_limit[:3]) * np.asarray(self.scale)) + (0,)
return {
'domain_limit': domain_limit,
'new_shape': new_shape,
Lucia D. Hradecka
committed
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})'
"""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]``.
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.
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.
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` and `float_mask` voxels outside of the 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, key points, bounding boxes
"""
def __init__(self, scales: Union[float, TypeTripletFloat] = 1,
interpolation: str = 'linear', 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: str = parse_itk_interpolation(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
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)
def apply_to_mask(self, mask, **params):
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
scales=self.scale,
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
scales=self.scale,
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
scales=self.scale,
spacing = self.spacing,
domain_limit=params['domain_limit'])
"""
def apply_to_bboxes(self, bboxes, **params):
for bbox in bboxes:
new_bbox = F.affine_keypoints(bbox,
scales=self.scale,
domain_limit=params['domain_limit'],
spacing = self.spacing,
keep_all=True)
if validate_bbox(bbox, new_bbox):
res.append(new_bbox)
return res
"""
Lucia D. Hradecka
committed
def get_params(self, targets, **data):
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
return {'domain_limit': domain_limit}
Lucia D. Hradecka
committed
return f'Scale(scales={self.scale}, interpolation={self.interpolation}, spacing={self.spacing}, ' \
f'border_mode={self.border_mode}, ival={self.ival}, mval={self.mval},' \
f'always_apply={self.always_apply}, p={self.p})'
"""Randomly rescale the input image content by the given scale. The image shape remains unchanged.
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)``.
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
(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.
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` and `float_mask` voxels outside of the 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, 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,
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, scale=True)
self.interpolation: str = parse_itk_interpolation(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'
Lucia D. Hradecka
committed
def get_params(self, targets, **data):
Lucia D. Hradecka
committed
domain_limit: TypeSpatioTemporalCoordinate = get_spatio_temporal_domain_limit(data, targets)
scale = sample_range_uniform(self.scaling_limit)
return {
'domain_limit': domain_limit,
'scale': scale,
}
def apply(self, img, **params):
return F.affine(img,
interpolation=self.interpolation,
border_mode=self.border_mode,
value=self.ival,
spacing=self.spacing)
def apply_to_mask(self, mask, **params):
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
spacing=self.spacing,
domain_limit=params['domain_limit'])
Lucia D. Hradecka
committed
return f'RandomScale(scaling_limit={self.scaling_limit}, interpolation={self.interpolation}, ' \
f'spacing={self.spacing}, border_mode={self.border_mode}, ival={self.ival}, mval={self.mval}, ' \
f'always_apply={self.always_apply}, p={self.p})'
class AffineTransform(DualTransform):
"""Affine transformation of the input image with given parameters. Image shape remains unchanged.
angles (Tuple[float], optional): Angles of rotation for the spatial axes.
Defaults to ``(0, 0, 0)``.
translation (Tuple[float], optional): Translation vector for the spatial axes.
Defaults to ``(0, 0, 0)``.
scale (Tuple[float], optional): Scales for the spatial axes.
Defaults to ``(1, 1, 1)``.
spacing (Tuple[float, float, float], optional): Voxel spacing for individual spatial dimensions.
Must be: ``(S1, S2, S3)`` (a scale for each spatial dimension must be given).
Defaults to ``(1, 1, 1)``.
change_to_isotropic (bool, optional): Change data from anisotropic to isotropic.
Defaults to ``False``.
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``.
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` and `float_mask` voxels outside of the 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.
always_apply (bool, optional): Always apply this transformation in composition.
p (float, optional): Chance of applying this transformation in composition.
Defaults to ``0.5``.
image, mask, float mask, key points, bounding boxes
def __init__(self, angles: TypeTripletFloat = (0, 0, 0),
translation: TypeTripletFloat = (0, 0, 0),
scale: TypeTripletFloat = (1, 1, 1),
spacing: TypeTripletFloat = (1, 1, 1),
change_to_isotropic: bool = False,
interpolation: str = 'linear',
border_mode: str = 'constant', ival: float = 0, mval: float = 0,
ignore_index: Union[float, None] = None, always_apply: bool = False, p: float = 0.5):
self.angles: TypeTripletFloat = parse_coefs(angles, identity_element=0)
self.translation: TypeTripletFloat = parse_coefs(translation, identity_element=0)
self.scale: TypeTripletFloat = parse_coefs(scale, identity_element=1)
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1)
self.interpolation: str = parse_itk_interpolation(interpolation)
self.border_mode = border_mode # not used
self.mask_mode = border_mode # not used
self.ival = ival
self.mval = mval
self.keep_scale = not change_to_isotropic
self.mask_mode = 'constant'
return F.affine(img,
scales=self.scale,
degrees=self.angles,
translation=self.translation,
interpolation=self.interpolation,
border_mode=self.border_mode,
value=self.ival,
spacing=self.spacing)
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
return F.affine(np.expand_dims(mask, 0),
scales=self.scale,
degrees=self.angles,
translation=self.translation,
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)[0]
def apply_to_float_mask(self, mask, **params):
return F.affine(np.expand_dims(mask, 0),
scales=self.scale,
degrees=self.angles,
translation=self.translation,
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
spacing=self.spacing)[0]
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
scales=self.scale,
degrees=self.angles,
translation=self.translation,
spacing=self.spacing,
domain_limit=params['domain_limit'])
Lucia D. Hradecka
committed
def get_params(self, targets, **data):
# set parameters of the transform
domain_limit = get_spatio_temporal_domain_limit(data, targets)
'domain_limit': domain_limit
Lucia D. Hradecka
committed
def __repr__(self):
return f'AffineTransform(angles={self.angles}, translation={self.translation}, scale={self.scale}, ' \
f'spacing={self.spacing}, change_to_isotropic={not self.keep_scale}, ' \
f'interpolation={self.interpolation}, border_mode={self.border_mode}, ival={self.ival}, ' \
f'mval={self.mval}, always_apply={self.always_apply}, p={self.p})'
"""Affine transformation of the input image with randomly chosen parameters. Image shape remains unchanged.
Args:
angle_limit (Tuple[float] | float, optional): Intervals in degrees from which angles of
rotation for the spatial axes are chosen.
Must be either of: ``A``, ``(A1, A2)``, ``(A1, A2, A3)``, or ``(A_Z1, A_Z2, A_Y1, A_Y2, A_X1, A_X2)``.
If a float, equivalent to ``(-A, A, -A, A, -A, A)``.
If a tuple with 2 items, equivalent to ``(A1, A2, A1, A2, A1, A2)``.
If a tuple with 3 items, equivalent to ``(-A1, A1, -A2, A2, -A3, A3)``.
If a tuple with 6 items, angle of rotation is randomly chosen from an interval [A_a1, A_a2] for each
spatial axis.
Defaults to ``(15, 15, 15)``.
translation_limit (Tuple[float] | float | None, optional): Intervals from which the translation parameters
Must be either of: ``T``, ``(T1, T2)``, ``(T1, T2, T3)``, or ``(T_Z1, T_Z2, T_Y1, T_Y2, T_X1, T_X2)``.
If a float, equivalent to ``(2-T, T, 2-T, T, 2-T, T)``.
If a tuple with 2 items, equivalent to ``(T1, T2, T1, T2, T1, T2)``.
If a tuple with 3 items, equivalent to ``(2-T1, T1, 2-T2, T2, 2-T3, T3)``.
If a tuple with 6 items, the translation parameter is randomly chosen from an interval [T_a1, T_a2] for
each spatial axis.
Defaults to ``(0, 0, 0)``.
scaling_limit (Tuple[float] | float, optional): Intervals from which the scales for the spatial axes are chosen.
Must be either of: ``S``, ``(S1, S2)``, ``(S1, S2, S3)``, or ``(S_Z1, S_Z2, S_Y1, S_Y2, S_X1, S_X2)``.
If a float, equivalent to ``(1/S, S, 1/S, S, 1/S, S)``.
If a tuple with 2 items, equivalent to ``(S1, S2, S1, S2, S1, S2)``.
If a tuple with 3 items, equivalent to ``(1/S1, S1, 1/S2, S2, 1/S3, S3)``.
If a tuple with 6 items, the scale is randomly chosen from an interval [S_a1, S_a2] for
each spatial axis.
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``.
change_to_isotropic (bool, optional): Change data from anisotropic to isotropic.
Defaults to ``False``.
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.
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` and `float_mask` voxels outside of the domain. Only applied when ``border_mode = 'constant'``.
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.
p (float, optional): Chance of applying this transformation in composition.
image, mask, float mask, key points, bounding boxes
def __init__(self, angle_limit: Union[float, TypePairFloat, TypeTripletFloat, TypeSextetFloat] = (15., 15., 15.),
translation_limit: Union[float, TypePairFloat, TypeTripletFloat, TypeSextetFloat] = (0., 0., 0.),
scaling_limit: Union[float, TypePairFloat, TypeTripletFloat, TypeSextetFloat] = (1., 1., 1.),
spacing: Union[float, TypeTripletFloat] = None,
change_to_isotropic: bool = False,
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.angle_limit: TypeSextetFloat = parse_limits(angle_limit)
self.translation_limit: TypeSextetFloat = parse_limits(translation_limit)
self.scaling_limit: TypeSextetFloat = parse_limits(scaling_limit, scale=True)
self.spacing: TypeTripletFloat = parse_coefs(spacing, identity_element=1)
self.interpolation: int = parse_itk_interpolation(interpolation)
self.border_mode = border_mode # not used
self.mask_mode = border_mode # not used
self.ival = ival
self.mval = mval
self.keep_scale = not change_to_isotropic
if ignore_index is not None:
self.mask_mode = 'constant'
self.mval = ignore_index
def apply(self, img, **params):
return F.affine(img,
scales=params['scale'],
degrees=params['angles'],
translation=params['translation'],
interpolation=self.interpolation,
border_mode=self.border_mode,
value=self.ival,
spacing=self.spacing)
def apply_to_mask(self, mask, **params):
interpolation = parse_itk_interpolation('nearest') # refers to 'sitkNearestNeighbor'
scales=params['scale'],
degrees=params['angles'],
translation=params['translation'],
interpolation=interpolation,
border_mode=self.mask_mode,
value=self.mval,
scales=params['scale'],
degrees=params['angles'],
translation=params['translation'],
interpolation=self.interpolation,
border_mode=self.mask_mode,
value=self.mval,
def apply_to_keypoints(self, keypoints, **params):
return F.affine_keypoints(keypoints,
scales=params['scale'],
degrees=params['angles'],
translation=params['translation'],
spacing=self.spacing,
domain_limit=params['domain_limit'])
Lucia D. Hradecka
committed
def get_params(self, targets, **data):
# set parameters of the transform
scales = sample_range_uniform(self.scaling_limit)
angles = sample_range_uniform(self.angle_limit)
translation = sample_range_uniform(self.translation_limit)
Lucia D. Hradecka
committed
domain_limit = get_spatio_temporal_domain_limit(data, targets)
'scale': scales,
'angles': angles,
'translation': translation,
'domain_limit': domain_limit
Lucia D. Hradecka
committed
def __repr__(self):
return f'RandomAffineTransform(angle_limit={self.angle_limit}, translation_limit={self.translation_limit}, ' \
f'scaling_limit={self.scaling_limit}, spacing={self.spacing}, ' \
f'change_to_isotropic={not self.keep_scale}, interpolation={self.interpolation}, ' \
f'border_mode={self.border_mode}, ival={self.ival}, mval={self.mval}, ' \
f'always_apply={self.always_apply}, p={self.p})'
class RandomRotate90(DualTransform):
"""Rotation of input by 0, 90, 180, or 270 degrees around the specified spatial axes.
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): Number of times the array is rotated by 90 degrees. If ``None``, will be chosen randomly.
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, key points, bounding boxes
"""
Lucia D. Hradecka
committed
def __init__(self, axes: List[int] = None, shuffle_axis: bool = False, factor: Optional[int] = None,
always_apply: bool = False, p: float = 0.5):
super().__init__(always_apply, p)
self.axes = axes
self.shuffle_axis = shuffle_axis
self.factor = factor
def apply(self, img, **params):
for factor, axes in zip(params['factor'], params['rotation_around']):
img = np.rot90(img, factor, axes=axes)
return img
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
def apply_to_keypoints(self, keypoints, **params):
for rot, factor in zip(params['rotation_around'], params['factor']):
keypoints = F.rot90_keypoints(keypoints,
factor=factor,
axes=(rot[0], rot[1]),
img_shape=params['img_shape'])
return keypoints
def get_params(self, targets, **data):
Lucia D. Hradecka
committed
# Rotate around all spatial axes if not specified by the user:
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])
Lucia D. Hradecka
committed
# Shuffle the order of rotation axes
Lucia D. Hradecka
committed
# If not specified, choose the angle to rotate
factor = list(randint(0, 3, size=len(rotation_around)))
else:
factor = [self.factor]
rotation_around = [(1, 2)]
print('ROT90', factor, rotation_around)
Lucia D. Hradecka
committed
img_shape = get_spatial_shape_from_image(data, targets)
return {'factor': factor,
'rotation_around': rotation_around,
'img_shape': img_shape}
Lucia D. Hradecka
committed
return f'RandomRotate90(axes={self.axes}, shuffle_axis={self.shuffle_axis}, factor={self.factor}, ' \
f'always_apply={self.always_apply}, p={self.p})'
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. 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): Chance 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=False, p=1):
super().__init__(always_apply, p)
self.axes = axes
def apply(self, img, **params):
return np.flip(img, params['axes'])
def apply_to_mask(self, mask, **params):
# Mask has no dimension channel
return np.flip(mask, axis=[item - 1 for item in params['axes']])
def apply_to_keypoints(self, keypoints, **params):
return F.flip_keypoints(keypoints,
axes=params['axes'],
img_shape=params['img_shape'])
def get_params(self, targets, **data):
Lucia D. Hradecka
committed
# Use all spatial axes if not specified otherwise:
axes = [1, 2, 3] if self.axes is None else self.axes
Lucia D. Hradecka
committed
# Get image shape (needed for keypoints):
img_shape = get_spatial_shape_from_image(data, targets)
return {'axes': axes,
'img_shape': img_shape}