# 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.
import random
import numpy as np
from monai.utils.misc import ensure_tuple
[docs]def rand_choice(prob=0.5):
"""Returns True if a randomly chosen number is less than or equal to `prob`, by default this is a 50/50 chance."""
return random.random() <= prob
[docs]def img_bounds(img):
"""Returns the minimum and maximum indices of non-zero lines in axis 0 of `img`, followed by that for axis 1."""
ax0 = np.any(img, axis=0)
ax1 = np.any(img, axis=1)
return np.concatenate((np.where(ax0)[0][[0, -1]], np.where(ax1)[0][[0, -1]]))
[docs]def in_bounds(x, y, margin, maxx, maxy):
"""Returns True if (x,y) is within the rectangle (margin, margin, maxx-margin, maxy-margin)."""
return margin <= x < (maxx - margin) and margin <= y < (maxy - margin)
[docs]def is_empty(img):
"""Returns True if `img` is empty, that is its maximum value is not greater than its minimum."""
return not (img.max() > img.min()) # use > instead of <= so that an image full of NaNs will result in True
[docs]def ensure_tuple_size(tup, dim):
"""Returns a copy of `tup` with `dim` values by either shortened or padded with zeros as necessary."""
tup = tuple(tup) + (0,) * dim
return tup[:dim]
[docs]def zero_margins(img, margin):
"""Returns True if the values within `margin` indices of the edges of `img` in dimensions 1 and 2 are 0."""
if np.any(img[:, :, :margin]) or np.any(img[:, :, -margin:]):
return False
if np.any(img[:, :margin, :]) or np.any(img[:, -margin:, :]):
return False
return True
[docs]def rescale_array(arr, minv=0.0, maxv=1.0, dtype=np.float32):
"""Rescale the values of numpy array `arr` to be from `minv` to `maxv`."""
if dtype is not None:
arr = arr.astype(dtype)
mina = np.min(arr)
maxa = np.max(arr)
if mina == maxa:
return arr * minv
norm = (arr - mina) / (maxa - mina) # normalize the array first
return (norm * (maxv - minv)) + minv # rescale by minv and maxv, which is the normalized array by default
[docs]def rescale_instance_array(arr, minv=0.0, maxv=1.0, dtype=np.float32):
"""Rescale each array slice along the first dimension of `arr` independently."""
out = np.zeros(arr.shape, dtype)
for i in range(arr.shape[0]):
out[i] = rescale_array(arr[i], minv, maxv, dtype)
return out
[docs]def rescale_array_int_max(arr, dtype=np.uint16):
"""Rescale the array `arr` to be between the minimum and maximum values of the type `dtype`."""
info = np.iinfo(dtype)
return rescale_array(arr, info.min, info.max).astype(dtype)
[docs]def copypaste_arrays(src, dest, srccenter, destcenter, dims):
"""
Calculate the slices to copy a sliced area of array `src` into array `dest`. The area has dimensions `dims` (use 0
or None to copy everything in that dimension), the source area is centered at `srccenter` index in `src` and copied
into area centered at `destcenter` in `dest`. The dimensions of the copied area will be clipped to fit within the
source and destination arrays so a smaller area may be copied than expected. Return value is the tuples of slice
objects indexing the copied area in `src`, and those indexing the copy area in `dest`.
Example
.. code-block:: python
src = np.random.randint(0,10,(6,6))
dest = np.zeros_like(src)
srcslices, destslices = copypaste_arrays(src, dest, (3, 2),(2, 1),(3, 4))
dest[destslices] = src[srcslices]
print(src)
print(dest)
>>> [[9 5 6 6 9 6]
[4 3 5 6 1 2]
[0 7 3 2 4 1]
[3 0 0 1 5 1]
[9 4 7 1 8 2]
[6 6 5 8 6 7]]
[[0 0 0 0 0 0]
[7 3 2 4 0 0]
[0 0 1 5 0 0]
[4 7 1 8 0 0]
[0 0 0 0 0 0]
[0 0 0 0 0 0]]
"""
srcslices = [slice(None)] * src.ndim
destslices = [slice(None)] * dest.ndim
for i, ss, ds, sc, dc, dim in zip(range(src.ndim), src.shape, dest.shape, srccenter, destcenter, dims):
if dim:
# dimension before midpoint, clip to size fitting in both arrays
d1 = np.clip(dim // 2, 0, min(sc, dc))
# dimension after midpoint, clip to size fitting in both arrays
d2 = np.clip(dim // 2 + 1, 0, min(ss - sc, ds - dc))
srcslices[i] = slice(sc - d1, sc + d2)
destslices[i] = slice(dc - d1, dc + d2)
return tuple(srcslices), tuple(destslices)
[docs]def resize_center(img, *resize_dims, fill_value=0):
"""
Resize `img` by cropping or expanding the image from the center. The `resize_dims` values are the output dimensions
(or None to use original dimension of `img`). If a dimension is smaller than that of `img` then the result will be
cropped and if larger padded with zeros, in both cases this is done relative to the center of `img`. The result is
a new image with the specified dimensions and values from `img` copied into its center.
"""
resize_dims = tuple(resize_dims[i] or img.shape[i] for i in range(len(resize_dims)))
dest = np.full(resize_dims, fill_value, img.dtype)
half_img_shape = np.asarray(img.shape) // 2
half_dest_shape = np.asarray(dest.shape) // 2
srcslices, destslices = copypaste_arrays(img, dest, half_img_shape, half_dest_shape, resize_dims)
dest[destslices] = img[srcslices]
return dest
[docs]def one_hot(labels, num_classes):
"""
Converts label image `labels` to a one-hot vector with `num_classes` number of channels as last dimension.
"""
labels = labels % num_classes
y = np.eye(num_classes)
onehot = y[labels.flatten()]
return onehot.reshape(tuple(labels.shape) + (num_classes,)).astype(labels.dtype)
[docs]def generate_pos_neg_label_crop_centers(label, size, num_samples, pos_ratio, rand_state=np.random):
"""Generate valid sample locations based on image with option for specifying foreground ratio
Valid: samples sitting entirely within image, expected input shape: [C, H, W, D] or [C, H, W]
Args:
label (numpy.ndarray): use the label data to get the foreground/background information.
size (list or tuple): size of the ROIs to be sampled.
num_samples (int): total sample centers to be generated.
pos_ratio (float): ratio of total locations generated that have center being foreground.
rand_state (random.RandomState): numpy randomState object to align with other modules.
"""
max_size = label.shape[1:]
assert len(max_size) == len(size), 'expected size does not match label dim.'
assert (np.subtract(max_size, size) >= 0).all(), 'proposed roi is larger than image itself.'
# Select subregion to assure valid roi
valid_start = np.floor_divide(size, 2)
valid_end = np.subtract(max_size + np.array(1), size / np.array(2)).astype(np.uint16) # add 1 for random
# int generation to have full range on upper side, but subtract unfloored size/2 to prevent rounded range
# from being too high
for i in range(len(valid_start)): # need this because np.random.randint does not work with same start and end
if valid_start[i] == valid_end[i]:
valid_end[i] += 1
# Prepare fg/bg indices
label_flat = label.ravel()
fg_indicies = np.where(label_flat > 0)[0]
bg_indicies = np.where(label_flat == 0)[0]
centers = []
for _ in range(num_samples):
if rand_state.rand() < pos_ratio:
indicies_to_use = fg_indicies
else:
indicies_to_use = bg_indicies
random_int = rand_state.randint(len(indicies_to_use))
center = np.unravel_index(indicies_to_use[random_int], label.shape)
center = center[1:]
# shift center to range of valid centers
center_ori = [c for c in center]
for i, c in enumerate(center):
center_i = c
if c < valid_start[i]:
center_i = valid_start[i]
if c >= valid_end[i]:
center_i = valid_end[i] - 1
center_ori[i] = center_i
centers.append(center_ori)
return centers
[docs]def create_grid(spatial_size, spacing=None, homogeneous=True, dtype=float):
"""
compute a `spatial_size` mesh.
Args:
spatial_size (sequence of ints): spatial size of the grid.
spacing (sequence of ints): same len as ``spatial_size``, defaults to 1.0 (dense grid).
homogeneous (bool): whether to make homogeneous coordinates.
dtype (type): output grid data type.
"""
spacing = spacing or tuple(1.0 for _ in spatial_size)
ranges = [np.linspace(-(d - 1.) / 2. * s, (d - 1.) / 2. * s, int(d)) for d, s in zip(spatial_size, spacing)]
coords = np.asarray(np.meshgrid(*ranges, indexing='ij'), dtype=dtype)
if not homogeneous:
return coords
return np.concatenate([coords, np.ones_like(coords[0:1, ...])])
[docs]def create_control_grid(spatial_shape, spacing, homogeneous=True, dtype=float):
"""
control grid with two additional point in each direction
"""
grid_shape = []
for d, s in zip(spatial_shape, spacing):
d = int(d)
if d % 2 == 0:
grid_shape.append(np.ceil((d - 1.) / (2. * s) + 0.5) * 2. + 2.)
else:
grid_shape.append(np.ceil((d - 1.) / (2. * s)) * 2. + 3.)
return create_grid(grid_shape, spacing, homogeneous, dtype)
[docs]def create_rotate(spatial_dims, radians):
"""
create a 2D or 3D rotation matrix
Args:
spatial_dims (2|3): spatial rank
radians (float or a sequence of floats): rotation radians
when spatial_dims == 3, the `radians` sequence corresponds to
rotation in the 1st, 2nd, and 3rd dim respectively.
"""
radians = ensure_tuple(radians)
if spatial_dims == 2:
if len(radians) >= 1:
sin_, cos_ = np.sin(radians[0]), np.cos(radians[0])
return np.array([[cos_, -sin_, 0.], [sin_, cos_, 0.], [0., 0., 1.]])
if spatial_dims == 3:
affine = None
if len(radians) >= 1:
sin_, cos_ = np.sin(radians[0]), np.cos(radians[0])
affine = np.array([
[1., 0., 0., 0.],
[0., cos_, -sin_, 0.],
[0., sin_, cos_, 0.],
[0., 0., 0., 1.],
])
if len(radians) >= 2:
sin_, cos_ = np.sin(radians[1]), np.cos(radians[1])
affine = affine @ np.array([
[cos_, 0.0, sin_, 0.],
[0., 1., 0., 0.],
[-sin_, 0., cos_, 0.],
[0., 0., 0., 1.],
])
if len(radians) >= 3:
sin_, cos_ = np.sin(radians[2]), np.cos(radians[2])
affine = affine @ np.array([
[cos_, -sin_, 0., 0.],
[sin_, cos_, 0., 0.],
[0., 0., 1., 0.],
[0., 0., 0., 1.],
])
return affine
raise ValueError('create_rotate got spatial_dims={}, radians={}.'.format(spatial_dims, radians))
[docs]def create_shear(spatial_dims, coefs):
"""
create a shearing matrix
Args:
spatial_dims (int): spatial rank
coefs (floats): shearing factors, defaults to 0.
"""
coefs = list(ensure_tuple(coefs))
if spatial_dims == 2:
while len(coefs) < 2:
coefs.append(0.0)
return np.array([
[1, coefs[0], 0.],
[coefs[1], 1., 0.],
[0., 0., 1.],
])
if spatial_dims == 3:
while len(coefs) < 6:
coefs.append(0.0)
return np.array([
[1., coefs[0], coefs[1], 0.],
[coefs[2], 1., coefs[3], 0.],
[coefs[4], coefs[5], 1., 0.],
[0., 0., 0., 1.],
])
raise NotImplementedError
[docs]def create_scale(spatial_dims, scaling_factor):
"""
create a scaling matrix
Args:
spatial_dims (int): spatial rank
scaling_factor (floats): scaling factors, defaults to 1.
"""
scaling_factor = list(ensure_tuple(scaling_factor))
while len(scaling_factor) < spatial_dims:
scaling_factor.append(1.)
return np.diag(scaling_factor[:spatial_dims] + [1.])
[docs]def create_translate(spatial_dims, shift):
"""
create a translation matrix
Args:
spatial_dims (int): spatial rank
shift (floats): translate factors, defaults to 0.
"""
shift = ensure_tuple(shift)
affine = np.eye(spatial_dims + 1)
for i, a in enumerate(shift[:spatial_dims]):
affine[i, spatial_dims] = a
return affine