Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1418,3 +1418,6 @@ Utilities
---------
.. automodule:: monai.transforms.utils
:members:

.. automodule:: monai.transforms.utils_pytorch_numpy_unification
:members:
18 changes: 12 additions & 6 deletions monai/data/nifti_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@
import torch

from monai.config import DtypeLike
from monai.config.type_definitions import NdarrayOrTensor
from monai.data.utils import compute_shape_offset, to_affine_nd
from monai.networks.layers import AffineTransform
from monai.utils import GridSampleMode, GridSamplePadMode, optional_import
from monai.utils.type_conversion import convert_data_type

nib, _ = optional_import("nibabel")


def write_nifti(
data: np.ndarray,
data: NdarrayOrTensor,
file_name: str,
affine: Optional[np.ndarray] = None,
affine: Optional[NdarrayOrTensor] = None,
target_affine: Optional[np.ndarray] = None,
resample: bool = True,
output_spatial_shape: Union[Sequence[int], np.ndarray, None] = None,
Expand Down Expand Up @@ -96,13 +98,17 @@ def write_nifti(
If None, use the data type of input data.
output_dtype: data type for saving data. Defaults to ``np.float32``.
"""
if isinstance(data, torch.Tensor):
data, *_ = convert_data_type(data, np.ndarray)
if isinstance(affine, torch.Tensor):
affine, *_ = convert_data_type(affine, np.ndarray)
if not isinstance(data, np.ndarray):
raise AssertionError("input data must be numpy array.")
raise AssertionError("input data must be numpy array or torch tensor.")
dtype = dtype or data.dtype
sr = min(data.ndim, 3)
if affine is None:
affine = np.eye(4, dtype=np.float64)
affine = to_affine_nd(sr, affine)
affine = to_affine_nd(sr, affine) # type: ignore

if target_affine is None:
target_affine = affine
Expand All @@ -122,7 +128,7 @@ def write_nifti(
data = nib.orientations.apply_orientation(data, ornt_transform)
_affine = affine @ nib.orientations.inv_ornt_aff(ornt_transform, data_shape)
if np.allclose(_affine, target_affine, atol=1e-3) or not resample:
results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, _affine))
results_img = nib.Nifti1Image(data.astype(output_dtype), to_affine_nd(3, _affine)) # type: ignore
nib.save(results_img, file_name)
return

Expand All @@ -138,7 +144,7 @@ def write_nifti(
while len(output_spatial_shape_) < 3:
output_spatial_shape_ = output_spatial_shape_ + [1]
spatial_shape, channel_shape = data.shape[:3], data.shape[3:]
data_np = data.reshape(list(spatial_shape) + [-1])
data_np: np.ndarray = data.reshape(list(spatial_shape) + [-1]) # type: ignore
data_np = np.moveaxis(data_np, -1, 0) # channel first for pytorch
data_torch = affine_xform(
torch.as_tensor(np.ascontiguousarray(data_np).astype(dtype)).unsqueeze(0),
Expand Down
12 changes: 7 additions & 5 deletions monai/transforms/intensity/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,20 +725,21 @@ class AdjustContrast(Transform):
gamma: gamma value to adjust the contrast as function.
"""

backend = [TransformBackends.TORCH, TransformBackends.NUMPY]

def __init__(self, gamma: float) -> None:
if not isinstance(gamma, (int, float)):
raise ValueError("gamma must be a float or int number.")
self.gamma = gamma

def __call__(self, img: np.ndarray):
def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor:
"""
Apply the transform to `img`.
"""
img, *_ = convert_data_type(img, np.ndarray) # type: ignore
epsilon = 1e-7
img_min = img.min()
img_range = img.max() - img_min
return np.power(((img - img_min) / float(img_range + epsilon)), self.gamma) * img_range + img_min
return ((img - img_min) / float(img_range + epsilon)) ** self.gamma * img_range + img_min


class RandAdjustContrast(RandomizableTransform):
Expand All @@ -753,6 +754,8 @@ class RandAdjustContrast(RandomizableTransform):
If single number, value is picked from (0.5, gamma), default is (0.5, 4.5).
"""

backend = AdjustContrast.backend

def __init__(self, prob: float = 0.1, gamma: Union[Sequence[float], float] = (0.5, 4.5)) -> None:
RandomizableTransform.__init__(self, prob)

Expand All @@ -773,11 +776,10 @@ def randomize(self, data: Optional[Any] = None) -> None:
super().randomize(None)
self.gamma_value = self.R.uniform(low=self.gamma[0], high=self.gamma[1])

def __call__(self, img: np.ndarray):
def __call__(self, img: NdarrayOrTensor) -> NdarrayOrTensor:
"""
Apply the transform to `img`.
"""
img, *_ = convert_data_type(img, np.ndarray) # type: ignore
self.randomize()
if self.gamma_value is None:
raise ValueError("gamma_value is not set.")
Expand Down
8 changes: 6 additions & 2 deletions monai/transforms/intensity/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -737,11 +737,13 @@ class AdjustContrastd(MapTransform):
allow_missing_keys: don't raise exception if key is missing.
"""

backend = AdjustContrast.backend

def __init__(self, keys: KeysCollection, gamma: float, allow_missing_keys: bool = False) -> None:
super().__init__(keys, allow_missing_keys)
self.adjuster = AdjustContrast(gamma)

def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]:
def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]:
d = dict(data)
for key in self.key_iterator(d):
d[key] = self.adjuster(d[key])
Expand All @@ -764,6 +766,8 @@ class RandAdjustContrastd(RandomizableTransform, MapTransform):
allow_missing_keys: don't raise exception if key is missing.
"""

backend = AdjustContrast.backend

def __init__(
self,
keys: KeysCollection,
Expand Down Expand Up @@ -791,7 +795,7 @@ def randomize(self, data: Optional[Any] = None) -> None:
super().randomize(None)
self.gamma_value = self.R.uniform(low=self.gamma[0], high=self.gamma[1])

def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]:
def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]:
d = dict(data)
self.randomize()
if self.gamma_value is None:
Expand Down
55 changes: 30 additions & 25 deletions monai/transforms/spatial/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
https://github.com/Project-MONAI/MONAI/wiki/MONAI_Design
"""
import warnings
from copy import deepcopy
from typing import Any, List, Optional, Sequence, Tuple, Union

import numpy as np
Expand Down Expand Up @@ -85,6 +86,8 @@ class Spacing(Transform):
Resample input image into the specified `pixdim`.
"""

backend = [TransformBackends.TORCH]

def __init__(
self,
pixdim: Union[Sequence[float], float],
Expand Down Expand Up @@ -136,14 +139,14 @@ def __init__(

def __call__(
self,
data_array: np.ndarray,
affine: Optional[np.ndarray] = None,
data_array: NdarrayOrTensor,
affine: Optional[NdarrayOrTensor] = None,
mode: Optional[Union[GridSampleMode, str]] = None,
padding_mode: Optional[Union[GridSamplePadMode, str]] = None,
align_corners: Optional[bool] = None,
dtype: DtypeLike = None,
output_spatial_shape: Optional[np.ndarray] = None,
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
) -> Tuple[NdarrayOrTensor, NdarrayOrTensor, NdarrayOrTensor]:
"""
Args:
data_array: in shape (num_channels, H[, W, ...]).
Expand Down Expand Up @@ -171,17 +174,17 @@ def __call__(
data_array (resampled into `self.pixdim`), original affine, current affine.

"""
data_array, *_ = convert_data_type(data_array, np.ndarray) # type: ignore
_dtype = dtype or self.dtype or data_array.dtype
sr = data_array.ndim - 1
sr = int(data_array.ndim - 1)
if sr <= 0:
raise ValueError("data_array must have at least one spatial dimension.")
if affine is None:
# default to identity
affine = np.eye(sr + 1, dtype=np.float64)
affine_ = np.eye(sr + 1, dtype=np.float64)
else:
affine_ = to_affine_nd(sr, affine)
affine, *_ = convert_data_type(affine, np.ndarray)
affine_ = to_affine_nd(sr, affine) # type: ignore

out_d = self.pixdim[:sr]
if out_d.size < sr:
Expand All @@ -197,26 +200,28 @@ def __call__(

# no resampling if it's identity transform
if np.allclose(transform, np.diag(np.ones(len(transform))), atol=1e-3):
output_data = data_array.copy().astype(np.float32)
new_affine = to_affine_nd(affine, new_affine)
return output_data, affine, new_affine
output_data, *_ = convert_data_type(deepcopy(data_array), dtype=_dtype)
new_affine = to_affine_nd(affine, new_affine) # type: ignore

# resample
affine_xform = AffineTransform(
normalized=False,
mode=look_up_option(mode or self.mode, GridSampleMode),
padding_mode=look_up_option(padding_mode or self.padding_mode, GridSamplePadMode),
align_corners=self.align_corners if align_corners is None else align_corners,
reverse_indexing=True,
)
output_data = affine_xform(
# AffineTransform requires a batch dim
torch.as_tensor(np.ascontiguousarray(data_array).astype(_dtype)).unsqueeze(0),
torch.as_tensor(np.ascontiguousarray(transform).astype(_dtype)),
spatial_size=output_shape if output_spatial_shape is None else output_spatial_shape,
)
output_data = np.asarray(output_data.squeeze(0).detach().cpu().numpy(), dtype=np.float32) # type: ignore
new_affine = to_affine_nd(affine, new_affine)
else:
# resample
affine_xform = AffineTransform(
normalized=False,
mode=look_up_option(mode or self.mode, GridSampleMode),
padding_mode=look_up_option(padding_mode or self.padding_mode, GridSamplePadMode),
align_corners=self.align_corners if align_corners is None else align_corners,
reverse_indexing=True,
)
data_array_t: torch.Tensor
data_array_t, *_ = convert_data_type(data_array, torch.Tensor, dtype=_dtype) # type: ignore
output_data = affine_xform(
# AffineTransform requires a batch dim
data_array_t.unsqueeze(0),
convert_data_type(transform, torch.Tensor, data_array_t.device, dtype=_dtype)[0],
spatial_size=output_shape if output_spatial_shape is None else output_spatial_shape,
).squeeze(0)
output_data, *_ = convert_to_dst_type(output_data, data_array, dtype=_dtype)
new_affine = to_affine_nd(affine, new_affine) # type: ignore

return output_data, affine, new_affine

Expand Down
16 changes: 9 additions & 7 deletions monai/transforms/spatial/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ class Spacingd(MapTransform, InvertibleTransform):
:py:class:`monai.transforms.Spacing`
"""

backend = Spacing.backend

def __init__(
self,
keys: KeysCollection,
Expand Down Expand Up @@ -211,8 +213,8 @@ def __init__(
self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.keys))

def __call__(
self, data: Mapping[Union[Hashable, str], Dict[str, np.ndarray]]
) -> Dict[Union[Hashable, str], Union[np.ndarray, Dict[str, np.ndarray]]]:
self, data: Mapping[Union[Hashable, str], Dict[str, NdarrayOrTensor]]
) -> Dict[Union[Hashable, str], Union[NdarrayOrTensor, Dict[str, NdarrayOrTensor]]]:
d: Dict = dict(data)
for key, mode, padding_mode, align_corners, dtype, meta_key, meta_key_postfix in self.key_iterator(
d, self.mode, self.padding_mode, self.align_corners, self.dtype, self.meta_keys, self.meta_key_postfix
Expand All @@ -226,7 +228,7 @@ def __call__(
# using affine fetched from d[affine_key]
original_spatial_shape = d[key].shape[1:]
d[key], old_affine, new_affine = self.spacing_transform(
data_array=np.asarray(d[key]),
data_array=d[key],
affine=meta_data["affine"],
mode=mode,
padding_mode=padding_mode,
Expand All @@ -249,7 +251,7 @@ def __call__(
meta_data["affine"] = new_affine
return d

def inverse(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]:
def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]:
d = deepcopy(dict(data))
for key, dtype in self.key_iterator(d, self.dtype):
transform = self.get_most_recent_transform(d, key)
Expand All @@ -269,15 +271,15 @@ def inverse(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndar
inverse_transform = Spacing(orig_pixdim, diagonal=self.spacing_transform.diagonal)
# Apply inverse
d[key], _, new_affine = inverse_transform(
data_array=np.asarray(d[key]),
affine=meta_data["affine"],
data_array=d[key],
affine=meta_data["affine"], # type: ignore
mode=mode,
padding_mode=padding_mode,
align_corners=False if align_corners == "none" else align_corners,
dtype=dtype,
output_spatial_shape=orig_size,
)
meta_data["affine"] = new_affine
meta_data["affine"] = new_affine # type: ignore
# Remove the applied transform
self.pop_transform(d, key)

Expand Down
22 changes: 9 additions & 13 deletions monai/transforms/utility/array.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
map_binary_to_indices,
map_classes_to_indices,
)
from monai.transforms.utils_pytorch_numpy_unification import in1d, moveaxis
from monai.transforms.utils_pytorch_numpy_unification import in1d, moveaxis, unravel_indices
from monai.utils import (
convert_data_type,
convert_to_cupy,
Expand Down Expand Up @@ -789,16 +789,18 @@ class FgBgToIndices(Transform):

"""

backend = [TransformBackends.NUMPY, TransformBackends.TORCH]

def __init__(self, image_threshold: float = 0.0, output_shape: Optional[Sequence[int]] = None) -> None:
self.image_threshold = image_threshold
self.output_shape = output_shape

def __call__(
self,
label: np.ndarray,
image: Optional[np.ndarray] = None,
label: NdarrayOrTensor,
image: Optional[NdarrayOrTensor] = None,
output_shape: Optional[Sequence[int]] = None,
) -> Tuple[np.ndarray, np.ndarray]:
) -> Tuple[NdarrayOrTensor, NdarrayOrTensor]:
"""
Args:
label: input data to compute foreground and background indices.
Expand All @@ -807,18 +809,12 @@ def __call__(
output_shape: expected shape of output indices. if None, use `self.output_shape` instead.

"""
fg_indices: np.ndarray
bg_indices: np.ndarray
label, *_ = convert_data_type(label, np.ndarray) # type: ignore
if image is not None:
image, *_ = convert_data_type(image, np.ndarray) # type: ignore
if output_shape is None:
output_shape = self.output_shape
fg_indices, bg_indices = map_binary_to_indices(label, image, self.image_threshold) # type: ignore
fg_indices, bg_indices = map_binary_to_indices(label, image, self.image_threshold)
if output_shape is not None:
fg_indices = np.stack([np.unravel_index(i, output_shape) for i in fg_indices])
bg_indices = np.stack([np.unravel_index(i, output_shape) for i in bg_indices])

fg_indices = unravel_indices(fg_indices, output_shape)
bg_indices = unravel_indices(bg_indices, output_shape)
return fg_indices, bg_indices


Expand Down
4 changes: 3 additions & 1 deletion monai/transforms/utility/dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,8 @@ class FgBgToIndicesd(MapTransform):

"""

backend = FgBgToIndices.backend

def __init__(
self,
keys: KeysCollection,
Expand All @@ -1135,7 +1137,7 @@ def __init__(
self.image_key = image_key
self.converter = FgBgToIndices(image_threshold, output_shape)

def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]:
def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]:
d = dict(data)
image = d[self.image_key] if self.image_key else None
for key in self.key_iterator(d):
Expand Down
4 changes: 1 addition & 3 deletions monai/transforms/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,7 @@ def map_binary_to_indices(
fg_indices = nonzero(label_flat)
if image is not None:
img_flat = ravel(any_np_pt(image > image_threshold, 0))
img_flat, *_ = convert_data_type(
img_flat, type(label), device=label.device if isinstance(label, torch.Tensor) else None
)
img_flat, *_ = convert_to_dst_type(img_flat, label, dtype=img_flat.dtype)
bg_indices = nonzero(img_flat & ~label_flat)
else:
bg_indices = nonzero(~label_flat)
Expand Down
Loading