# Copyright 2020 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
A collection of "vanilla" transforms
https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design
"""
import numpy as np
import scipy.ndimage
import nibabel as nib
import torch
from torch.utils.data._utils.collate import np_str_obj_array_pattern
from skimage.transform import resize
import monai
from monai.data.utils import get_random_patch, get_valid_patch_size, correct_nifti_header_if_necessary
from monai.networks.layers.simplelayers import GaussianFilter
from monai.transforms.compose import Randomizable
from monai.transforms.utils import (create_control_grid, create_grid, create_rotate, create_scale, create_shear,
create_translate, rescale_array)
from monai.utils.misc import ensure_tuple
export = monai.utils.export("monai.transforms")
[docs]@export
class Spacing:
"""
Resample input image into the specified `pixdim`.
"""
def __init__(self, pixdim, keep_shape=False):
"""
Args:
pixdim (sequence of floats): output voxel spacing.
keep_shape (bool): whether to maintain the original spatial shape
after resampling. Defaults to False.
"""
self.pixdim = pixdim
self.keep_shape = keep_shape
self.original_pixdim = pixdim
[docs] def __call__(self, data_array, original_affine=None, original_pixdim=None, interp_order=1):
"""
Args:
data_array (ndarray): in shape (num_channels, H[, W, ...]).
original_affine (4x4 matrix): original affine.
original_pixdim (sequence of floats): original voxel spacing.
interp_order (int): The order of the spline interpolation, default is 3.
The order has to be in the range 0-5.
https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.zoom.html
Returns:
resampled array (in spacing: `self.pixdim`), original pixdim, current pixdim.
"""
if original_affine is None and original_pixdim is None:
raise ValueError('please provide either original_affine or original_pixdim.')
spatial_rank = data_array.ndim - 1
if original_affine is not None:
affine = np.array(original_affine, dtype=np.float64, copy=True)
if not affine.shape == (4, 4):
raise ValueError('`original_affine` must be 4 x 4.')
original_pixdim = np.sqrt(np.sum(np.square(affine[:spatial_rank, :spatial_rank]), 1))
inp_d = np.asarray(original_pixdim)[:spatial_rank]
if inp_d.size < spatial_rank:
inp_d = np.append(inp_d, [1.] * (inp_d.size - spatial_rank))
out_d = np.asarray(self.pixdim)[:spatial_rank]
if out_d.size < spatial_rank:
out_d = np.append(out_d, [1.] * (out_d.size - spatial_rank))
self.original_pixdim, self.pixdim = inp_d, out_d
scale = inp_d / out_d
if not np.isfinite(scale).all():
raise ValueError('Unknown pixdims: source {}, target {}'.format(inp_d, out_d))
zoom_ = monai.transforms.Zoom(scale, order=interp_order, mode='nearest', keep_size=self.keep_shape)
return zoom_(data_array), self.original_pixdim, self.pixdim
[docs]@export
class Orientation:
"""
Change the input image's orientation into the specified based on `axcodes`.
"""
def __init__(self, axcodes, labels=None):
"""
Args:
axcodes (N elements sequence): for spatial ND input's orientation.
e.g. axcodes='RAS' represents 3D orientation:
(Left, Right), (Posterior, Anterior), (Inferior, Superior).
default orientation labels options are: 'L' and 'R' for the first dimension,
'P' and 'A' for the second, 'I' and 'S' for the third.
labels : optional, None or sequence of (2,) sequences
(2,) sequences are labels for (beginning, end) of output axis.
See Also: `nibabel.orientations.ornt2axcodes`.
"""
self.axcodes = axcodes
self.labels = labels
[docs] def __call__(self, data_array, original_affine=None, original_axcodes=None):
"""
if `original_affine` is provided, the orientation is computed from the affine.
Args:
data_array (ndarray): in shape (num_channels, H[, W, ...]).
original_affine (4x4 matrix): original affine.
original_axcodes (N elements sequence): for spatial ND input's orientation.
Returns:
data_array (reoriented in `self.axcodes`), original axcodes, current axcodes.
"""
if original_affine is None and original_axcodes is None:
raise ValueError('please provide either original_affine or original_axcodes.')
spatial_rank = len(data_array.shape) - 1
if original_affine is not None:
affine = np.array(original_affine, dtype=np.float64, copy=True)
if not affine.shape == (4, 4):
raise ValueError('`original_affine` must be 4 x 4.')
original_axcodes = nib.aff2axcodes(original_affine, labels=self.labels)
original_axcodes = original_axcodes[:spatial_rank]
self.axcodes = self.axcodes[:spatial_rank]
src = nib.orientations.axcodes2ornt(original_axcodes, labels=self.labels)
dst = nib.orientations.axcodes2ornt(self.axcodes)
spatial_ornt = nib.orientations.ornt_transform(src, dst)
spatial_ornt[:, 0] += 1 # skip channel dim
ornt = np.concatenate([np.array([[0, 1]]), spatial_ornt])
data_array = nib.orientations.apply_orientation(data_array, ornt)
return data_array, original_axcodes, self.axcodes
[docs]@export
class LoadNifti:
"""
Load Nifti format file from provided path.
"""
def __init__(self, as_closest_canonical=False, image_only=False, dtype=None):
"""
Args:
as_closest_canonical (bool): if True, load the image as closest to canonical axis format.
image_only (bool): if True return only the image volume, other return image volume and header dict.
dtype (np.dtype, optional): if not None convert the loaded image to this data type.
Note:
The loaded image volume if `image_only` is True, or a tuple containing the volume and the Nifti
header in dict format otherwise.
header['original_affine'] stores the original affine loaded from `filename_or_obj`.
header['affine'] stores the affine after the optional `as_closest_canonical` transform.
"""
self.as_closest_canonical = as_closest_canonical
self.image_only = image_only
self.dtype = dtype
[docs] def __call__(self, filename):
"""
Args:
filename (str or file): path to file or file-like object.
"""
img = nib.load(filename)
img = correct_nifti_header_if_necessary(img)
header = dict(img.header)
header['filename_or_obj'] = filename
header['original_affine'] = img.affine
header['affine'] = img.affine
header['as_closest_canonical'] = self.as_closest_canonical
if self.as_closest_canonical:
img = nib.as_closest_canonical(img)
header['affine'] = img.affine
if self.dtype is not None:
img = img.get_fdata(dtype=self.dtype)
else:
img = np.asanyarray(img.dataobj)
if self.image_only:
return img
compatible_meta = dict()
for meta_key in header:
meta_datum = header[meta_key]
if type(meta_datum).__name__ == 'ndarray' \
and np_str_obj_array_pattern.search(meta_datum.dtype.str) is not None:
continue
compatible_meta[meta_key] = meta_datum
return img, compatible_meta
[docs]@export
class AsChannelFirst:
"""
Change the channel dimension of the image to the first dimension.
Most of the image transformations in ``monai.transforms``
assumes the input image is in the channel-first format, which has the shape
(num_channels, spatial_dim_1[, spatial_dim_2, ...]).
This transform could be used to convert, for example, a channel-last image array in shape
(spatial_dim_1[, spatial_dim_2, ...], num_channels) into the channel-first format,
so that the multidimensional image array can be correctly interpreted by the other
transforms.
Args:
channel_dim (int): which dimension of input image is the channel, default is the last dimension.
"""
def __init__(self, channel_dim=-1):
assert isinstance(channel_dim, int) and channel_dim >= -1, 'invalid channel dimension.'
self.channel_dim = channel_dim
[docs] def __call__(self, img):
return np.moveaxis(img, self.channel_dim, 0)
[docs]@export
class AddChannel:
"""
Adds a 1-length channel dimension to the input image.
Most of the image transformations in ``monai.transforms``
assumes the input image is in the channel-first format, which has the shape
(num_channels, spatial_dim_1[, spatial_dim_2, ...]).
This transform could be used, for example, to convert a (spatial_dim_1[, spatial_dim_2, ...])
spatial image into the channel-first format so that the
multidimensional image array can be correctly interpreted by the other
transforms.
"""
[docs] def __call__(self, img):
return img[None]
[docs]@export
class Transpose:
"""
Transposes the input image based on the given `indices` dimension ordering.
"""
def __init__(self, indices):
self.indices = indices
[docs] def __call__(self, img):
return img.transpose(self.indices)
[docs]@export
class Rescale:
"""
Rescales the input image to the given value range.
"""
def __init__(self, minv=0.0, maxv=1.0, dtype=np.float32):
self.minv = minv
self.maxv = maxv
self.dtype = dtype
[docs] def __call__(self, img):
return rescale_array(img, self.minv, self.maxv, self.dtype)
[docs]@export
class GaussianNoise(Randomizable):
"""Add gaussian noise to image.
Args:
mean (float or array of floats): Mean or “centre” of the distribution.
std (float): Standard deviation (spread) of distribution.
"""
def __init__(self, mean=0.0, std=0.1):
self.mean = mean
self.std = std
[docs] def __call__(self, img):
return img + self.R.normal(self.mean, self.R.uniform(0, self.std), size=img.shape)
[docs]@export
class Flip:
"""Reverses the order of elements along the given spatial axis. Preserves shape.
Uses ``np.flip`` in practice. See numpy.flip for additional details.
https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html
Args:
spatial_axis (None, int or tuple of ints): spatial axes along which to flip over. Default is None.
"""
def __init__(self, spatial_axis=None):
self.spatial_axis = spatial_axis
[docs] def __call__(self, img):
"""
Args:
img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
"""
flipped = list()
for channel in img:
flipped.append(
np.flip(channel, self.spatial_axis)
)
return np.stack(flipped)
[docs]@export
class Resize:
"""
Resize the input image to given resolution. Uses skimage.transform.resize underneath.
For additional details, see https://scikit-image.org/docs/dev/api/skimage.transform.html#skimage.transform.resize.
Args:
output_spatial_shape (tuple or list): expected shape of spatial dimensions after resize operation.
order (int): Order of spline interpolation. Default=1.
mode (str): Points outside boundaries are filled according to given mode.
Options are 'constant', 'edge', 'symmetric', 'reflect', 'wrap'.
cval (float): Used with mode 'constant', the value outside image boundaries.
clip (bool): Wheter to clip range of output values after interpolation. Default: True.
preserve_range (bool): Whether to keep original range of values. Default is True.
If False, input is converted according to conventions of img_as_float. See
https://scikit-image.org/docs/dev/user_guide/data_types.html.
anti_aliasing (bool): Whether to apply a gaussian filter to image before down-scaling.
Default is True.
anti_aliasing_sigma (float, tuple of floats): Standard deviation for gaussian filtering.
"""
def __init__(self, output_spatial_shape, order=1, mode='reflect', cval=0,
clip=True, preserve_range=True, anti_aliasing=True, anti_aliasing_sigma=None):
assert isinstance(order, int), "order must be integer."
self.output_spatial_shape = output_spatial_shape
self.order = order
self.mode = mode
self.cval = cval
self.clip = clip
self.preserve_range = preserve_range
self.anti_aliasing = anti_aliasing
self.anti_aliasing_sigma = anti_aliasing_sigma
[docs] def __call__(self, img):
"""
Args:
img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
"""
resized = list()
for channel in img:
resized.append(
resize(channel, self.output_spatial_shape, order=self.order,
mode=self.mode, cval=self.cval,
clip=self.clip, preserve_range=self.preserve_range,
anti_aliasing=self.anti_aliasing,
anti_aliasing_sigma=self.anti_aliasing_sigma)
)
return np.stack(resized).astype(np.float32)
[docs]@export
class Rotate:
"""
Rotates an input image by given angle. Uses scipy.ndimage.rotate. For more details, see
https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.rotate.html
Args:
angle (float): Rotation angle in degrees.
spatial_axes (tuple of 2 ints): Spatial axes of rotation. Default: (0, 1).
This is the first two axis in spatial dimensions.
reshape (bool): If true, output shape is made same as input. Default: True.
order (int): Order of spline interpolation. Range 0-5. Default: 1. This is
different from scipy where default interpolation is 3.
mode (str): Points outside boundary filled according to this mode. Options are
'constant', 'nearest', 'reflect', 'wrap'. Default: 'constant'.
cval (scalar): Values to fill outside boundary. Default: 0.
prefiter (bool): Apply spline_filter before interpolation. Default: True.
"""
def __init__(self, angle, spatial_axes=(0, 1), reshape=True, order=1, mode='constant', cval=0, prefilter=True):
self.angle = angle
self.reshape = reshape
self.order = order
self.mode = mode
self.cval = cval
self.prefilter = prefilter
self.spatial_axes = spatial_axes
[docs] def __call__(self, img):
"""
Args:
img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
"""
rotated = list()
for channel in img:
rotated.append(
scipy.ndimage.rotate(channel, self.angle, self.spatial_axes, reshape=self.reshape,
order=self.order, mode=self.mode, cval=self.cval, prefilter=self.prefilter)
)
return np.stack(rotated).astype(np.float32)
[docs]@export
class Zoom:
""" Zooms a nd image. Uses scipy.ndimage.zoom or cupyx.scipy.ndimage.zoom in case of gpu.
For details, please see https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.zoom.html.
Args:
zoom (float or sequence): The zoom factor along the spatial axes.
If a float, zoom is the same for each spatial axis.
If a sequence, zoom should contain one value for each spatial axis.
order (int): order of interpolation. Default=3.
mode (str): Determines how input is extended beyond boundaries. Default is 'constant'.
cval (scalar, optional): Value to fill past edges. Default is 0.
use_gpu (bool): Should use cpu or gpu. Uses cupyx which doesn't support order > 1 and modes
'wrap' and 'reflect'. Defaults to cpu for these cases or if cupyx not found.
keep_size (bool): Should keep original size (pad if needed).
"""
def __init__(self, zoom, order=3, mode='constant', cval=0, prefilter=True, use_gpu=False, keep_size=False):
assert isinstance(order, int), "Order must be integer."
self.zoom = zoom
self.order = order
self.mode = mode
self.cval = cval
self.prefilter = prefilter
self.use_gpu = use_gpu
self.keep_size = keep_size
if self.use_gpu:
try:
from cupyx.scipy.ndimage import zoom as zoom_gpu
self._zoom = zoom_gpu
except ImportError:
print('For GPU zoom, please install cupy. Defaulting to cpu.')
self._zoom = scipy.ndimage.zoom
self.use_gpu = False
else:
self._zoom = scipy.ndimage.zoom
[docs] def __call__(self, img):
"""
Args:
img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
"""
zoomed = list()
if self.use_gpu:
import cupy
for channel in cupy.array(img):
zoom_channel = self._zoom(channel,
zoom=self.zoom,
order=self.order,
mode=self.mode,
cval=self.cval,
prefilter=self.prefilter)
zoomed.append(cupy.asnumpy(zoom_channel))
else:
for channel in img:
zoomed.append(
self._zoom(channel,
zoom=self.zoom,
order=self.order,
mode=self.mode,
cval=self.cval,
prefilter=self.prefilter))
zoomed = np.stack(zoomed).astype(np.float32)
if not self.keep_size or np.allclose(img.shape, zoomed.shape):
return zoomed
pad_vec = [[0, 0]] * len(img.shape)
slice_vec = [slice(None)] * len(img.shape)
for idx, (od, zd) in enumerate(zip(img.shape, zoomed.shape)):
diff = od - zd
half = abs(diff) // 2
if diff > 0: # need padding
pad_vec[idx] = [half, diff - half]
elif diff < 0: # need slicing
slice_vec[idx] = slice(half, half + od)
zoomed = np.pad(zoomed, pad_vec, mode='constant')
return zoomed[tuple(slice_vec)]
[docs]@export
class ToTensor:
"""
Converts the input image to a tensor without applying any other transformations.
"""
[docs] def __call__(self, img):
return torch.from_numpy(img)
[docs]@export
class IntensityNormalizer:
"""Normalize input based on provided args, using calculated mean and std if not provided
(shape of subtrahend and divisor must match. if 0, entire volume uses same subtrahend and
divisor, otherwise the shape can have dimension 1 for channels).
Current implementation can only support 'channel_last' format data.
Args:
subtrahend (ndarray): the amount to subtract by (usually the mean)
divisor (ndarray): the amount to divide by (usually the standard deviation)
"""
def __init__(self, subtrahend=None, divisor=None):
if subtrahend is not None or divisor is not None:
assert isinstance(subtrahend, np.ndarray) and isinstance(divisor, np.ndarray), \
'subtrahend and divisor must be set in pair and in numpy array.'
self.subtrahend = subtrahend
self.divisor = divisor
[docs] def __call__(self, img):
if self.subtrahend is not None and self.divisor is not None:
img -= self.subtrahend
img /= self.divisor
else:
img -= np.mean(img)
img /= np.std(img)
return img
[docs]@export
class ImageEndPadder:
"""Performs padding by appending to the end of the data all on one side for each dimension.
Uses np.pad so in practice, a mode needs to be provided. See numpy.lib.arraypad.pad
for additional details.
Args:
out_size (list): the size of region of interest at the end of the operation.
mode (string): a portion from numpy.lib.arraypad.pad is copied below.
"""
def __init__(self, out_size, mode):
assert out_size is not None and isinstance(out_size, (list, tuple)), 'out_size must be list or tuple.'
self.out_size = out_size
assert isinstance(mode, str), 'mode must be str.'
self.mode = mode
def _determine_data_pad_width(self, data_shape):
return [(0, max(self.out_size[i] - data_shape[i], 0)) for i in range(len(self.out_size))]
[docs] def __call__(self, img):
data_pad_width = self._determine_data_pad_width(img.shape[2:])
all_pad_width = [(0, 0), (0, 0)] + data_pad_width
img = np.pad(img, all_pad_width, self.mode)
return img
[docs]@export
class Rotate90:
"""
Rotate an array by 90 degrees in the plane specified by `axes`.
"""
def __init__(self, k=1, spatial_axes=(0, 1)):
"""
Args:
k (int): number of times to rotate by 90 degrees.
spatial_axes (2 ints): defines the plane to rotate with 2 spatial axes.
Default: (0, 1), this is the first two axis in spatial dimensions.
"""
self.k = k
self.spatial_axes = spatial_axes
[docs] def __call__(self, img):
"""
Args:
img (ndarray): channel first array, must have shape: (num_channels, H[, W, ..., ]),
"""
rotated = list()
for channel in img:
rotated.append(
np.rot90(channel, self.k, self.spatial_axes)
)
return np.stack(rotated)
[docs]@export
class RandRotate90(Randomizable):
"""
With probability `prob`, input arrays are rotated by 90 degrees
in the plane specified by `spatial_axes`.
"""
def __init__(self, prob=0.1, max_k=3, spatial_axes=(0, 1)):
"""
Args:
prob (float): probability of rotating.
(Default 0.1, with 10% probability it returns a rotated array)
max_k (int): number of rotations will be sampled from `np.random.randint(max_k) + 1`.
(Default 3)
spatial_axes (2 ints): defines the plane to rotate with 2 spatial axes.
Default: (0, 1), this is the first two axis in spatial dimensions.
"""
self.prob = min(max(prob, 0.0), 1.0)
self.max_k = max_k
self.spatial_axes = spatial_axes
self._do_transform = False
self._rand_k = 0
[docs] def randomize(self):
self._rand_k = self.R.randint(self.max_k) + 1
self._do_transform = self.R.random() < self.prob
[docs] def __call__(self, img):
self.randomize()
if not self._do_transform:
return img
rotator = Rotate90(self._rand_k, self.spatial_axes)
return rotator(img)
[docs]@export
class SpatialCrop:
"""General purpose cropper to produce sub-volume region of interest (ROI).
It can support to crop ND spatial (channel-first) data.
Either a spatial center and size must be provided, or alternatively if center and size
are not provided, the start and end coordinates of the ROI must be provided.
The sub-volume must sit the within original image.
Note: This transform will not work if the crop region is larger than the image itself.
"""
def __init__(self, roi_center=None, roi_size=None, roi_start=None, roi_end=None):
"""
Args:
roi_center (list or tuple): voxel coordinates for center of the crop ROI.
roi_size (list or tuple): size of the crop ROI.
roi_start (list or tuple): voxel coordinates for start of the crop ROI.
roi_end (list or tuple): voxel coordinates for end of the crop ROI.
"""
if roi_center is not None and roi_size is not None:
roi_center = np.asarray(roi_center, dtype=np.uint16)
roi_size = np.asarray(roi_size, dtype=np.uint16)
self.roi_start = np.subtract(roi_center, np.floor_divide(roi_size, 2))
self.roi_end = np.add(self.roi_start, roi_size)
else:
assert roi_start is not None and roi_end is not None, 'roi_start and roi_end must be provided.'
self.roi_start = np.asarray(roi_start, dtype=np.uint16)
self.roi_end = np.asarray(roi_end, dtype=np.uint16)
assert np.all(self.roi_start >= 0), 'all elements of roi_start must be greater than or equal to 0.'
assert np.all(self.roi_end > 0), 'all elements of roi_end must be positive.'
assert np.all(self.roi_end >= self.roi_start), 'invalid roi range.'
[docs] def __call__(self, img):
max_end = img.shape[1:]
sd = min(len(self.roi_start), len(max_end))
assert np.all(max_end[:sd] >= self.roi_start[:sd]), 'roi start out of image space.'
assert np.all(max_end[:sd] >= self.roi_end[:sd]), 'roi end out of image space.'
slices = [slice(None)] + [slice(s, e) for s, e in zip(self.roi_start[:sd], self.roi_end[:sd])]
data = img[tuple(slices)].copy()
return data
[docs]@export
class RandRotate(Randomizable):
"""Randomly rotates the input arrays.
Args:
prob (float): Probability of rotation.
degrees (tuple of float or float): Range of rotation in degrees. If single number,
angle is picked from (-degrees, degrees).
spatial_axes (tuple of 2 ints): Spatial axes of rotation. Default: (0, 1).
This is the first two axis in spatial dimensions.
reshape (bool): If true, output shape is made same as input. Default: True.
order (int): Order of spline interpolation. Range 0-5. Default: 1. This is
different from scipy where default interpolation is 3.
mode (str): Points outside boundary filled according to this mode. Options are
'constant', 'nearest', 'reflect', 'wrap'. Default: 'constant'.
cval (scalar): Value to fill outside boundary. Default: 0.
prefiter (bool): Apply spline_filter before interpolation. Default: True.
"""
def __init__(self, degrees, prob=0.1, spatial_axes=(0, 1), reshape=True, order=1,
mode='constant', cval=0, prefilter=True):
self.prob = prob
self.degrees = degrees
self.reshape = reshape
self.order = order
self.mode = mode
self.cval = cval
self.prefilter = prefilter
self.spatial_axes = spatial_axes
if not hasattr(self.degrees, '__iter__'):
self.degrees = (-self.degrees, self.degrees)
assert len(self.degrees) == 2, "degrees should be a number or pair of numbers."
self._do_transform = False
self.angle = None
[docs] def randomize(self):
self._do_transform = self.R.random_sample() < self.prob
self.angle = self.R.uniform(low=self.degrees[0], high=self.degrees[1])
[docs] def __call__(self, img):
self.randomize()
if not self._do_transform:
return img
rotator = Rotate(self.angle, self.spatial_axes, self.reshape, self.order,
self.mode, self.cval, self.prefilter)
return rotator(img)
[docs]@export
class RandFlip(Randomizable):
"""Randomly flips the image along axes. Preserves shape.
See numpy.flip for additional details.
https://docs.scipy.org/doc/numpy/reference/generated/numpy.flip.html
Args:
prob (float): Probability of flipping.
spatial_axis (None, int or tuple of ints): Spatial axes along which to flip over. Default is None.
"""
def __init__(self, prob=0.1, spatial_axis=None):
self.prob = prob
self.flipper = Flip(spatial_axis=spatial_axis)
self._do_transform = False
[docs] def randomize(self):
self._do_transform = self.R.random_sample() < self.prob
[docs] def __call__(self, img):
self.randomize()
if not self._do_transform:
return img
return self.flipper(img)
[docs]@export
class RandZoom(Randomizable):
"""Randomly zooms input arrays with given probability within given zoom range.
Args:
prob (float): Probability of zooming.
min_zoom (float or sequence): Min zoom factor. Can be float or sequence same size as image.
If a float, min_zoom is the same for each spatial axis.
If a sequence, min_zoom should contain one value for each spatial axis.
max_zoom (float or sequence): Max zoom factor. Can be float or sequence same size as image.
If a float, max_zoom is the same for each spatial axis.
If a sequence, max_zoom should contain one value for each spatial axis.
order (int): order of interpolation. Default=3.
mode ('reflect', 'constant', 'nearest', 'mirror', 'wrap'): Determines how input is
extended beyond boundaries. Default: 'constant'.
cval (scalar, optional): Value to fill past edges. Default is 0.
use_gpu (bool): Should use cpu or gpu. Uses cupyx which doesn't support order > 1 and modes
'wrap' and 'reflect'. Defaults to cpu for these cases or if cupyx not found.
keep_size (bool): Should keep original size (pad if needed).
"""
def __init__(self, prob=0.1, min_zoom=0.9, max_zoom=1.1, order=3,
mode='constant', cval=0, prefilter=True,
use_gpu=False, keep_size=False):
if hasattr(min_zoom, '__iter__') and \
hasattr(max_zoom, '__iter__'):
assert len(min_zoom) == len(max_zoom), "min_zoom and max_zoom must have same length."
self.min_zoom = min_zoom
self.max_zoom = max_zoom
self.prob = prob
self.order = order
self.mode = mode
self.cval = cval
self.prefilter = prefilter
self.use_gpu = use_gpu
self.keep_size = keep_size
self._do_transform = False
self._zoom = None
[docs] def randomize(self):
self._do_transform = self.R.random_sample() < self.prob
if hasattr(self.min_zoom, '__iter__'):
self._zoom = (self.R.uniform(l, h) for l, h in zip(self.min_zoom, self.max_zoom))
else:
self._zoom = self.R.uniform(self.min_zoom, self.max_zoom)
[docs] def __call__(self, img):
self.randomize()
if not self._do_transform:
return img
zoomer = Zoom(self._zoom, self.order, self.mode, self.cval, self.prefilter, self.use_gpu, self.keep_size)
return zoomer(img)
class AffineGrid:
"""
Affine transforms on the coordinates.
"""
def __init__(self,
rotate_params=None,
shear_params=None,
translate_params=None,
scale_params=None,
as_tensor_output=True,
device=None):
self.rotate_params = rotate_params
self.shear_params = shear_params
self.translate_params = translate_params
self.scale_params = scale_params
self.as_tensor_output = as_tensor_output
self.device = device
def __call__(self, spatial_size=None, grid=None):
"""
Args:
spatial_size (list or tuple of int): output grid size.
grid (ndarray): grid to be transformed. Shape must be (3, H, W) for 2D or (4, H, W, D) for 3D.
"""
if grid is None:
if spatial_size is not None:
grid = create_grid(spatial_size)
else:
raise ValueError('Either specify a grid or a spatial size to create a grid from.')
spatial_dims = len(grid.shape) - 1
affine = np.eye(spatial_dims + 1)
if self.rotate_params:
affine = affine @ create_rotate(spatial_dims, self.rotate_params)
if self.shear_params:
affine = affine @ create_shear(spatial_dims, self.shear_params)
if self.translate_params:
affine = affine @ create_translate(spatial_dims, self.translate_params)
if self.scale_params:
affine = affine @ create_scale(spatial_dims, self.scale_params)
affine = torch.tensor(affine, device=self.device)
grid = torch.tensor(grid) if not torch.is_tensor(grid) else grid.clone().detach()
if self.device:
grid = grid.to(self.device)
grid = (affine.float() @ grid.reshape((grid.shape[0], -1)).float()).reshape([-1] + list(grid.shape[1:]))
if self.as_tensor_output:
return grid
return grid.cpu().numpy()
class RandAffineGrid(Randomizable):
"""
generate randomised affine grid
"""
def __init__(self,
rotate_range=None,
shear_range=None,
translate_range=None,
scale_range=None,
as_tensor_output=True,
device=None):
"""
Args:
rotate_range (a sequence of positive floats): rotate_range[0] with be used to generate the 1st rotation
parameter from `uniform[-rotate_range[0], rotate_range[0])`. Similarly, `rotate_range[2]` and
`rotate_range[3]` are used in 3D affine for the range of 2nd and 3rd axes.
shear_range (a sequence of positive floats): shear_range[0] with be used to generate the 1st shearing
parameter from `uniform[-shear_range[0], shear_range[0])`. Similarly, `shear_range[1]` to
`shear_range[N]` controls the range of the uniform distribution used to generate the 2nd to
N-th parameter.
translate_range (a sequence of positive floats): translate_range[0] with be used to generate the 1st
shift parameter from `uniform[-translate_range[0], translate_range[0])`. Similarly, `translate_range[1]`
to `translate_range[N]` controls the range of the uniform distribution used to generate
the 2nd to N-th parameter.
scale_range (a sequence of positive floats): scaling_range[0] with be used to generate the 1st scaling
factor from `uniform[-scale_range[0], scale_range[0]) + 1.0`. Similarly, `scale_range[1]` to
`scale_range[N]` controls the range of the uniform distribution used to generate the 2nd to
N-th parameter.
See also:
- :py:meth:`monai.transforms.utils.create_rotate`
- :py:meth:`monai.transforms.utils.create_shear`
- :py:meth:`monai.transforms.utils.create_translate`
- :py:meth:`monai.transforms.utils.create_scale`
"""
self.rotate_range = ensure_tuple(rotate_range)
self.shear_range = ensure_tuple(shear_range)
self.translate_range = ensure_tuple(translate_range)
self.scale_range = ensure_tuple(scale_range)
self.rotate_params = None
self.shear_params = None
self.translate_params = None
self.scale_params = None
self.as_tensor_output = as_tensor_output
self.device = device
def randomize(self):
if self.rotate_range:
self.rotate_params = [self.R.uniform(-f, f) for f in self.rotate_range if f is not None]
if self.shear_range:
self.shear_params = [self.R.uniform(-f, f) for f in self.shear_range if f is not None]
if self.translate_range:
self.translate_params = [self.R.uniform(-f, f) for f in self.translate_range if f is not None]
if self.scale_range:
self.scale_params = [self.R.uniform(-f, f) + 1.0 for f in self.scale_range if f is not None]
def __call__(self, spatial_size=None, grid=None):
"""
Returns:
a 2D (3xHxW) or 3D (4xHxWxD) grid.
"""
self.randomize()
affine_grid = AffineGrid(rotate_params=self.rotate_params, shear_params=self.shear_params,
translate_params=self.translate_params, scale_params=self.scale_params,
as_tensor_output=self.as_tensor_output, device=self.device)
return affine_grid(spatial_size, grid)
class RandDeformGrid(Randomizable):
"""
generate random deformation grid
"""
def __init__(self, spacing, magnitude_range, as_tensor_output=True, device=None):
"""
Args:
spacing (2 or 3 ints): spacing of the grid in 2D or 3D.
e.g., spacing=(1, 1) indicates pixel-wise deformation in 2D,
spacing=(1, 1, 1) indicates voxel-wise deformation in 3D,
spacing=(2, 2) indicates deformation field defined on every other pixel in 2D.
magnitude_range (2 ints): the random offsets will be generated from
`uniform[magnitude[0], magnitude[1])`.
as_tensor_output (bool): whether to output tensor instead of numpy array.
defaults to True.
device (torch device): device to store the output grid data.
"""
self.spacing = spacing
self.magnitude = magnitude_range
self.rand_mag = 1.0
self.as_tensor_output = as_tensor_output
self.random_offset = 0.0
self.device = device
def randomize(self, grid_size):
self.random_offset = self.R.normal(size=([len(grid_size)] + list(grid_size)))
self.rand_mag = self.R.uniform(self.magnitude[0], self.magnitude[1])
def __call__(self, spatial_size):
control_grid = create_control_grid(spatial_size, self.spacing)
self.randomize(control_grid.shape[1:])
control_grid[:len(spatial_size)] += self.rand_mag * self.random_offset
if self.as_tensor_output:
control_grid = torch.tensor(control_grid, device=self.device)
return control_grid
class Resample:
def __init__(self, padding_mode='zeros', as_tensor_output=False, device=None):
"""
computes output image using values from `img`, locations from `grid` using pytorch.
supports spatially 2D or 3D (num_channels, H, W[, D]).
Args:
padding_mode ('zeros'|'border'|'reflection'): mode of handling out of range indices. Defaults to 'zeros'.
as_tensor_output(bool): whether to return a torch tensor. Defaults to False.
device (torch.device): device on which the tensor will be allocated.
"""
self.padding_mode = padding_mode
self.as_tensor_output = as_tensor_output
self.device = device
def __call__(self, img, grid, mode='bilinear'):
"""
Args:
img (ndarray or tensor): shape must be (num_channels, H, W[, D]).
grid (ndarray or tensor): shape must be (3, H, W) for 2D or (4, H, W, D) for 3D.
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'bilinear'.
"""
if not torch.is_tensor(img):
img = torch.tensor(img)
grid = torch.tensor(grid) if not torch.is_tensor(grid) else grid.clone().detach()
if self.device:
img = img.to(self.device)
grid = grid.to(self.device)
for i, dim in enumerate(img.shape[1:]):
grid[i] = 2. * grid[i] / (dim - 1.)
grid = grid[:-1] / grid[-1:]
grid = grid[range(img.ndim - 2, -1, -1)]
grid = grid.permute(list(range(grid.ndim))[1:] + [0])
out = torch.nn.functional.grid_sample(img[None].float(),
grid[None].float(),
mode=mode,
padding_mode=self.padding_mode,
align_corners=False)[0]
if not self.as_tensor_output:
return out.cpu().numpy()
return out
[docs]@export
class Affine:
"""
transform ``img`` given the affine parameters.
"""
def __init__(self,
rotate_params=None,
shear_params=None,
translate_params=None,
scale_params=None,
spatial_size=None,
mode='bilinear',
padding_mode='zeros',
as_tensor_output=False,
device=None):
"""
The affines are applied in rotate, shear, translate, scale order.
Args:
rotate_params (float, list of floats): a rotation angle in radians,
a scalar for 2D image, a tuple of 3 floats for 3D. Defaults to no rotation.
shear_params (list of floats):
a tuple of 2 floats for 2D, a tuple of 6 floats for 3D. Defaults to no shearing.
translate_params (list of floats):
a tuple of 2 floats for 2D, a tuple of 3 floats for 3D. Translation is in pixel/voxel
relative to the center of the input image. Defaults to no translation.
scale_params (list of floats):
a tuple of 2 floats for 2D, a tuple of 3 floats for 3D. Defaults to no scaling.
spatial_size (list or tuple of int): output image spatial size.
if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w].
if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'bilinear'.
padding_mode ('zeros'|'border'|'reflection'): mode of handling out of range indices. Defaults to 'zeros'.
as_tensor_output (bool): the computation is implemented using pytorch tensors, this option specifies
whether to convert it back to numpy arrays.
device (torch.device): device on which the tensor will be allocated.
"""
self.affine_grid = AffineGrid(rotate_params=rotate_params,
shear_params=shear_params,
translate_params=translate_params,
scale_params=scale_params,
as_tensor_output=True,
device=device)
self.resampler = Resample(padding_mode=padding_mode, as_tensor_output=as_tensor_output, device=device)
self.spatial_size = spatial_size
self.mode = mode
[docs] def __call__(self, img, spatial_size=None, mode=None):
"""
Args:
img (ndarray or tensor): shape must be (num_channels, H, W[, D]),
spatial_size (list or tuple of int): output image spatial size.
if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w].
if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'bilinear'.
"""
spatial_size = spatial_size or self.spatial_size
mode = mode or self.mode
grid = self.affine_grid(spatial_size=spatial_size)
return self.resampler(img=img, grid=grid, mode=mode)
[docs]@export
class RandAffine(Randomizable):
"""
Random affine transform.
"""
def __init__(self,
prob=0.1,
rotate_range=None,
shear_range=None,
translate_range=None,
scale_range=None,
spatial_size=None,
mode='bilinear',
padding_mode='zeros',
as_tensor_output=True,
device=None):
"""
Args:
prob (float): probability of returning a randomized affine grid.
defaults to 0.1, with 10% chance returns a randomized grid.
spatial_size (list or tuple of int): output image spatial size.
if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w].
if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'bilinear'.
padding_mode ('zeros'|'border'|'reflection'): mode of handling out of range indices. Defaults to 'zeros'.
as_tensor_output (bool): the computation is implemented using pytorch tensors, this option specifies
whether to convert it back to numpy arrays.
device (torch.device): device on which the tensor will be allocated.
See also:
- :py:class:`RandAffineGrid` for the random affine paramters configurations.
- :py:class:`Affine` for the affine transformation parameters configurations.
"""
self.rand_affine_grid = RandAffineGrid(rotate_range=rotate_range, shear_range=shear_range,
translate_range=translate_range, scale_range=scale_range,
as_tensor_output=True, device=device)
self.resampler = Resample(padding_mode=padding_mode, as_tensor_output=as_tensor_output, device=device)
self.spatial_size = spatial_size
self.mode = mode
self.do_transform = False
self.prob = prob
[docs] def set_random_state(self, seed=None, state=None):
self.rand_affine_grid.set_random_state(seed, state)
Randomizable.set_random_state(self, seed, state)
return self
[docs] def randomize(self):
self.do_transform = self.R.rand() < self.prob
self.rand_affine_grid.randomize()
[docs] def __call__(self, img, spatial_size=None, mode=None):
"""
Args:
img (ndarray or tensor): shape must be (num_channels, H, W[, D]),
spatial_size (list or tuple of int): output image spatial size.
if `img` has two spatial dimensions, `spatial_size` should have 2 elements [h, w].
if `img` has three spatial dimensions, `spatial_size` should have 3 elements [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'bilinear'.
"""
self.randomize()
spatial_size = spatial_size or self.spatial_size
mode = mode or self.mode
if self.do_transform:
grid = self.rand_affine_grid(spatial_size=spatial_size)
else:
grid = create_grid(spatial_size)
return self.resampler(img=img, grid=grid, mode=mode)
[docs]@export
class Rand2DElastic(Randomizable):
"""
Random elastic deformation and affine in 2D
"""
def __init__(self,
spacing,
magnitude_range,
prob=0.1,
rotate_range=None,
shear_range=None,
translate_range=None,
scale_range=None,
spatial_size=None,
mode='bilinear',
padding_mode='zeros',
as_tensor_output=False,
device=None):
"""
Args:
spacing (2 ints): distance in between the control points.
magnitude_range (2 ints): the random offsets will be generated from
``uniform[magnitude[0], magnitude[1])``.
prob (float): probability of returning a randomized affine grid.
defaults to 0.1, with 10% chance returns a randomized grid,
otherwise returns a ``spatial_size`` centered area extracted from the input image.
spatial_size (2 ints): specifying output image spatial size [h, w].
mode ('nearest'|'bilinear'): interpolation order. Defaults to ``'bilinear'``.
padding_mode ('zeros'|'border'|'reflection'): mode of handling out of range indices.
Defaults to ``'zeros'``.
as_tensor_output (bool): the computation is implemented using pytorch tensors, this option specifies
whether to convert it back to numpy arrays.
device (torch.device): device on which the tensor will be allocated.
See also:
- :py:class:`RandAffineGrid` for the random affine paramters configurations.
- :py:class:`Affine` for the affine transformation parameters configurations.
"""
self.deform_grid = RandDeformGrid(spacing=spacing, magnitude_range=magnitude_range,
as_tensor_output=True, device=device)
self.rand_affine_grid = RandAffineGrid(rotate_range=rotate_range, shear_range=shear_range,
translate_range=translate_range, scale_range=scale_range,
as_tensor_output=True, device=device)
self.resampler = Resample(padding_mode=padding_mode, as_tensor_output=as_tensor_output, device=device)
self.spatial_size = spatial_size
self.mode = mode
self.prob = prob
self.do_transform = False
[docs] def set_random_state(self, seed=None, state=None):
self.deform_grid.set_random_state(seed, state)
self.rand_affine_grid.set_random_state(seed, state)
Randomizable.set_random_state(self, seed, state)
return self
[docs] def randomize(self, spatial_size):
self.do_transform = self.R.rand() < self.prob
self.deform_grid.randomize(spatial_size)
self.rand_affine_grid.randomize()
[docs] def __call__(self, img, spatial_size=None, mode=None):
"""
Args:
img (ndarray or tensor): shape must be (num_channels, H, W),
spatial_size (2 ints): specifying output image spatial size [h, w].
mode ('nearest'|'bilinear'): interpolation order. Defaults to ``self.mode``.
"""
spatial_size = spatial_size or self.spatial_size
self.randomize(spatial_size)
mode = mode or self.mode
if self.do_transform:
grid = self.deform_grid(spatial_size=spatial_size)
grid = self.rand_affine_grid(grid=grid)
grid = torch.nn.functional.interpolate(grid[None], spatial_size, mode='bicubic', align_corners=False)[0]
else:
grid = create_grid(spatial_size)
return self.resampler(img, grid, mode)
[docs]@export
class Rand3DElastic(Randomizable):
"""
Random elastic deformation and affine in 3D
"""
def __init__(self,
sigma_range,
magnitude_range,
prob=0.1,
rotate_range=None,
shear_range=None,
translate_range=None,
scale_range=None,
spatial_size=None,
mode='bilinear',
padding_mode='zeros',
as_tensor_output=False,
device=None):
"""
Args:
sigma_range (2 ints): a Gaussian kernel with standard deviation sampled
from ``uniform[sigma_range[0], sigma_range[1])`` will be used to smooth the random offset grid.
magnitude_range (2 ints): the random offsets on the grid will be generated from
``uniform[magnitude[0], magnitude[1])``.
prob (float): probability of returning a randomized affine grid.
defaults to 0.1, with 10% chance returns a randomized grid,
otherwise returns a ``spatial_size`` centered area extracted from the input image.
spatial_size (3 ints): specifying output image spatial size [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to ``'bilinear'``.
padding_mode ('zeros'|'border'|'reflection'): mode of handling out of range indices.
Defaults to ``'zeros'``.
as_tensor_output (bool): the computation is implemented using pytorch tensors, this option specifies
whether to convert it back to numpy arrays.
device (torch.device): device on which the tensor will be allocated.
See also:
- :py:class:`RandAffineGrid` for the random affine paramters configurations.
- :py:class:`Affine` for the affine transformation parameters configurations.
"""
self.rand_affine_grid = RandAffineGrid(rotate_range, shear_range, translate_range, scale_range, True, device)
self.resampler = Resample(padding_mode=padding_mode, as_tensor_output=as_tensor_output, device=device)
self.sigma_range = sigma_range
self.magnitude_range = magnitude_range
self.spatial_size = spatial_size
self.mode = mode
self.device = device
self.prob = prob
self.do_transform = False
self.rand_offset = None
self.magnitude = 1.0
self.sigma = 1.0
[docs] def set_random_state(self, seed=None, state=None):
self.rand_affine_grid.set_random_state(seed, state)
Randomizable.set_random_state(self, seed, state)
return self
[docs] def randomize(self, grid_size):
self.do_transform = self.R.rand() < self.prob
if self.do_transform:
self.rand_offset = self.R.uniform(-1., 1., [3] + list(grid_size))
self.magnitude = self.R.uniform(self.magnitude_range[0], self.magnitude_range[1])
self.sigma = self.R.uniform(self.sigma_range[0], self.sigma_range[1])
self.rand_affine_grid.randomize()
[docs] def __call__(self, img, spatial_size=None, mode=None):
"""
Args:
img (ndarray or tensor): shape must be (num_channels, H, W, D),
spatial_size (3 ints): specifying spatial 3D output image spatial size [h, w, d].
mode ('nearest'|'bilinear'): interpolation order. Defaults to 'self.mode'.
"""
spatial_size = spatial_size or self.spatial_size
mode = mode or self.mode
self.randomize(spatial_size)
grid = create_grid(spatial_size)
if self.do_transform:
grid = torch.tensor(grid).to(self.device)
gaussian = GaussianFilter(3, self.sigma, 3., device=self.device)
grid[:3] += gaussian(self.rand_offset[None])[0] * self.magnitude
grid = self.rand_affine_grid(grid=grid)
return self.resampler(img, grid, mode)