From 9626480a89b043c8976655c21745d6915ccb3946 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Mon, 20 Sep 2021 13:23:32 +0100 Subject: [PATCH 1/3] remove deprecated Signed-off-by: Wenqi Li --- docs/source/handlers.rst | 5 - monai/handlers/__init__.py | 10 +- monai/handlers/segmentation_saver.py | 2 +- monai/handlers/transform_inverter.py | 144 --------------------- monai/handlers/utils.py | 78 +----------- monai/networks/nets/netadapter.py | 3 + monai/networks/nets/resnet.py | 4 + monai/transforms/post/array.py | 6 + monai/transforms/post/dictionary.py | 3 + monai/transforms/spatial/array.py | 27 ++++ monai/transforms/spatial/dictionary.py | 16 +++ tests/test_handler_transform_inverter.py | 152 ----------------------- 12 files changed, 63 insertions(+), 387 deletions(-) delete mode 100644 monai/handlers/transform_inverter.py delete mode 100644 tests/test_handler_transform_inverter.py diff --git a/docs/source/handlers.rst b/docs/source/handlers.rst index 5caccc6b4b..cb4333d1da 100644 --- a/docs/source/handlers.rst +++ b/docs/source/handlers.rst @@ -150,11 +150,6 @@ GarbageCollector handler .. autoclass:: GarbageCollector :members: -Transform inverter ------------------- -.. autoclass:: TransformInverter - :members: - Post processing --------------- .. autoclass:: PostProcessing diff --git a/monai/handlers/__init__.py b/monai/handlers/__init__.py index c9eecc6d46..bf1a9d3f89 100644 --- a/monai/handlers/__init__.py +++ b/monai/handlers/__init__.py @@ -32,13 +32,5 @@ from .stats_handler import StatsHandler from .surface_distance import SurfaceDistance from .tensorboard_handlers import TensorBoardHandler, TensorBoardImageHandler, TensorBoardStatsHandler -from .transform_inverter import TransformInverter -from .utils import ( - evenly_divisible_all_gather, - from_engine, - stopping_fn_from_loss, - stopping_fn_from_metric, - string_list_all_gather, - write_metrics_reports, -) +from .utils import from_engine, stopping_fn_from_loss, stopping_fn_from_metric, write_metrics_reports from .validation_handler import ValidationHandler diff --git a/monai/handlers/segmentation_saver.py b/monai/handlers/segmentation_saver.py index 535f58945b..27b2cbc039 100644 --- a/monai/handlers/segmentation_saver.py +++ b/monai/handlers/segmentation_saver.py @@ -26,7 +26,7 @@ Engine, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Engine") -@deprecated(since="0.6.0", removed="0.7.0", msg_suffix="Please consider using `SaveImage[d]` transform instead.") +@deprecated(since="0.6.0", removed="0.8.0", msg_suffix="Please consider using `SaveImage[d]` transform instead.") class SegmentationSaver: """ Event handler triggered on completing every iteration to save the segmentation predictions into files. diff --git a/monai/handlers/transform_inverter.py b/monai/handlers/transform_inverter.py deleted file mode 100644 index 83b5f56396..0000000000 --- a/monai/handlers/transform_inverter.py +++ /dev/null @@ -1,144 +0,0 @@ -# Copyright 2020 - 2021 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 warnings -from typing import TYPE_CHECKING, Callable, Optional, Sequence, Union - -import torch - -from monai.config import IgniteInfo, KeysCollection -from monai.engines.utils import CommonKeys, IterationEvents -from monai.transforms import Invertd, InvertibleTransform -from monai.utils import deprecated, ensure_tuple, ensure_tuple_rep, min_version, optional_import - -Events, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Events") -if TYPE_CHECKING: - from ignite.engine import Engine -else: - Engine, _ = optional_import("ignite.engine", IgniteInfo.OPT_IMPORT_VERSION, min_version, "Engine") - - -@deprecated(since="0.6.0", removed="0.7.0", msg_suffix="Please consider using `Invertd` transform instead.") -class TransformInverter: - """ - Ignite handler to automatically invert `transforms`. - It takes `engine.state.output` as the input data and uses the transforms information from `engine.state.batch`. - Expect both `engine.state.output` and `engine.state.batch` to be list of dictionaries data. - The inverted data is in-place saved back to `engine.state.output` with key: "{output_key}". - And the inverted meta dict will be stored in `engine.state.batch` - with key: "{meta_keys}" or "{key}_{meta_key_postfix}". - - .. deprecated:: 0.6.0 - Use :class:`monai.transforms.Invertd` instead. - - """ - - def __init__( - self, - transform: InvertibleTransform, - output_keys: KeysCollection = CommonKeys.PRED, - batch_keys: KeysCollection = CommonKeys.IMAGE, - meta_keys: Optional[KeysCollection] = None, - batch_meta_keys: Optional[KeysCollection] = None, - meta_key_postfix: str = "meta_dict", - nearest_interp: Union[bool, Sequence[bool]] = True, - to_tensor: Union[bool, Sequence[bool]] = True, - device: Union[Union[str, torch.device], Sequence[Union[str, torch.device]]] = "cpu", - post_func: Union[Callable, Sequence[Callable]] = lambda x: x, - num_workers: Optional[int] = 0, - ) -> None: - """ - Args: - transform: a callable data transform on input data. - output_keys: the key of expected data in `ignite.engine.output`, invert transforms on it. - it also can be a list of keys, will invert transform for each of them. - Default to "pred". it's in-place operation. - batch_keys: the key of input data in `ignite.engine.batch`. will get the applied transforms - for this input data, then invert them for the expected data with `output_keys`. - It can also be a list of keys, each matches to the `output_keys` data. default to "image". - meta_keys: explicitly indicate the key for the inverted meta data dictionary. - the meta data is a dictionary object which contains: filename, original_shape, etc. - it can be a sequence of string, map to the `keys`. - if None, will try to construct meta_keys by `{key}_{meta_key_postfix}`. - batch_meta_keys: the key of the meta data of input data in `ignite.engine.batch`, - will get the `affine`, `data_shape`, etc. - the meta data is a dictionary object which contains: filename, original_shape, etc. - it can be a sequence of string, map to the `keys`. - if None, will try to construct meta_keys by `{orig_key}_{meta_key_postfix}`. - meta data will also be inverted and stored in `meta_keys`. - meta_key_postfix: if `orig_meta_keys` is None, use `{orig_key}_{meta_key_postfix}` to to fetch the - meta data from dict, if `meta_keys` is None, use `{key}_{meta_key_postfix}`. - default is `meta_dict`, the meta data is a dictionary object. - For example, to handle orig_key `image`, read/write `affine` matrices from the - metadata `image_meta_dict` dictionary's `affine` field. - the inverted meta dict will be stored with key: "{key}_{meta_key_postfix}". - nearest_interp: whether to use `nearest` interpolation mode when inverting the spatial transforms, - default to `True`. If `False`, use the same interpolation mode as the original transform. - it also can be a list of bool, each matches to the `output_keys` data. - to_tensor: whether to convert the inverted data into PyTorch Tensor first, default to `True`. - it also can be a list of bool, each matches to the `output_keys` data. - device: if converted to Tensor, move the inverted results to target device before `post_func`, - default to "cpu", it also can be a list of string or `torch.device`, - each matches to the `output_keys` data. - post_func: post processing for the inverted data, should be a callable function. - it also can be a list of callable, each matches to the `output_keys` data. - - """ - self.inverter = Invertd( - keys=output_keys, - transform=transform, - orig_keys=batch_keys, - meta_keys=meta_keys, - orig_meta_keys=batch_meta_keys, - meta_key_postfix=meta_key_postfix, - nearest_interp=nearest_interp, - to_tensor=to_tensor, - device=device, - post_func=post_func, - ) - self.output_keys = ensure_tuple(output_keys) - self.meta_keys = ensure_tuple_rep(None, len(self.output_keys)) if meta_keys is None else ensure_tuple(meta_keys) - if len(self.output_keys) != len(self.meta_keys): - raise ValueError("meta_keys should have the same length as output_keys.") - self.meta_key_postfix = ensure_tuple_rep(meta_key_postfix, len(self.output_keys)) - - def attach(self, engine: Engine) -> None: - """ - Args: - engine: Ignite Engine, it can be a trainer, validator or evaluator. - """ - engine.add_event_handler(IterationEvents.MODEL_COMPLETED, self) - - def __call__(self, engine: Engine) -> None: - """ - Args: - engine: Ignite Engine, it can be a trainer, validator or evaluator. - """ - if not isinstance(engine.state.batch, list) or not isinstance(engine.state.output, list): - warnings.warn("inverter requires `engine.state.batch` and `engine.state.output` to be lists.") - else: - for i, (b, o) in enumerate(zip(engine.state.batch, engine.state.output)): - # combine `batch` and `output` to temporarily act as 1 dict for postprocessing - data = dict(b) - data.update(o) - ret = self.inverter(data) - - for output_key, meta_key, meta_key_postfix in zip( - self.output_keys, self.meta_keys, self.meta_key_postfix - ): - # save the inverted data into state.output - engine.state.output[i][output_key] = ret.get(output_key) - # save the inverted meta dict into state.batch - meta_key = meta_key or f"{output_key}_{meta_key_postfix}" - if meta_key in ret: - # FIXME: we save inverted meta dict into `batch` to be compatible with `SegmentationSaver` - # will deprecate both handlers soon - engine.state.batch[i][meta_key] = ret.get(meta_key) diff --git a/monai/handlers/utils.py b/monai/handlers/utils.py index 13f23c582a..15d2c59682 100644 --- a/monai/handlers/utils.py +++ b/monai/handlers/utils.py @@ -11,13 +11,13 @@ import os from collections import OrderedDict -from typing import TYPE_CHECKING, Dict, List, Optional, Sequence, Union +from typing import TYPE_CHECKING, Dict, Optional, Sequence, Union import numpy as np import torch from monai.config import IgniteInfo, KeysCollection -from monai.utils import deprecated, ensure_tuple, get_torch_version_tuple, look_up_option, min_version, optional_import +from monai.utils import ensure_tuple, look_up_option, min_version, optional_import idist, _ = optional_import("ignite", IgniteInfo.OPT_IMPORT_VERSION, min_version, "distributed") if TYPE_CHECKING: @@ -28,8 +28,6 @@ __all__ = [ "stopping_fn_from_metric", "stopping_fn_from_loss", - "evenly_divisible_all_gather", - "string_list_all_gather", "write_metrics_reports", "from_engine", ] @@ -57,78 +55,6 @@ def stopping_fn(engine: Engine): return stopping_fn -@deprecated(since="0.6.0", removed="0.7.0", msg_suffix="The API had been moved to monai.utils module.") -def evenly_divisible_all_gather(data: torch.Tensor) -> torch.Tensor: - """ - Utility function for distributed data parallel to pad at first dim to make it evenly divisible and all_gather. - - Args: - data: source tensor to pad and execute all_gather in distributed data parallel. - - Note: - The input data on different ranks must have exactly same `dtype`. - - .. versionchanged:: 0.6.0 - The API had been moved to `monai.utils`. - - """ - if not isinstance(data, torch.Tensor): - raise ValueError("input data must be PyTorch Tensor.") - - if idist.get_world_size() <= 1: - return data - - # make sure the data is evenly-divisible on multi-GPUs - length = data.shape[0] - all_lens = idist.all_gather(length) - max_len = max(all_lens) - if length < max_len: - size = [max_len - length] + list(data.shape[1:]) - data = torch.cat([data, data.new_full(size, 0)], dim=0) - # all gather across all processes - data = idist.all_gather(data) - # delete the padding NaN items - return torch.cat([data[i * max_len : i * max_len + l, ...] for i, l in enumerate(all_lens)], dim=0) - - -@deprecated(since="0.6.0", removed="0.7.0", msg_suffix="The API had been moved to monai.utils module.") -def string_list_all_gather(strings: List[str]) -> List[str]: - """ - Utility function for distributed data parallel to all gather a list of strings. - Note that if the item in `strings` is longer than 1024 chars, it will be truncated to 1024: - https://pytorch.org/ignite/v0.4.5/distributed.html#ignite.distributed.utils.all_gather. - - Args: - strings: a list of strings to all gather. - - .. versionchanged:: 0.6.0 - The API had been moved to `monai.utils`. - - """ - world_size = idist.get_world_size() - if world_size <= 1: - return strings - - result: List[List[str]] = [[] for _ in range(world_size)] - # get length of strings - length = len(strings) - all_lens = idist.all_gather(length) - max_len = max(all_lens) - # pad the item to make sure the same length - if length < max_len: - strings += ["" for _ in range(max_len - length)] - - if get_torch_version_tuple() <= (1, 6): - raise RuntimeError("string all_gather can not be supported in PyTorch < 1.7.0.") - - for s in strings: - gathered = idist.all_gather(s) - for i, g in enumerate(gathered): - if len(g) > 0: - result[i].append(g) - return [i for k in result for i in k] - - def write_metrics_reports( save_dir: str, images: Optional[Sequence[str]], diff --git a/monai/networks/nets/netadapter.py b/monai/networks/nets/netadapter.py index 80288f7945..13da7698d3 100644 --- a/monai/networks/nets/netadapter.py +++ b/monai/networks/nets/netadapter.py @@ -37,6 +37,9 @@ class NetAdapter(torch.nn.Module): bias: the bias value when replacing the last layer. if False, the layer will not learn an additive bias, default to True. + .. deprecated:: 0.6.0 + ``n_classes`` is deprecated, use ``num_classes`` instead. + """ @deprecated_arg("n_classes", since="0.6") diff --git a/monai/networks/nets/resnet.py b/monai/networks/nets/resnet.py index 3b86dc3d62..49d4f3d2fa 100644 --- a/monai/networks/nets/resnet.py +++ b/monai/networks/nets/resnet.py @@ -168,6 +168,10 @@ class ResNet(nn.Module): - 'B': kernel_size 1 conv + norm. widen_factor: widen output for each layer. num_classes: number of output (classifications) + + .. deprecated:: 0.6.0 + ``n_classes`` is deprecated, use ``num_classes`` instead. + """ @deprecated_arg("n_classes", since="0.6") diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 631947025c..a7280cfe98 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -129,6 +129,9 @@ class AsDiscrete(Transform): rounding: if not None, round the data according to the specified option, available options: ["torchrounding"]. + .. deprecated:: 0.6.0 + ``n_classes`` is deprecated, use ``num_classes`` instead. + """ @deprecated_arg("n_classes", since="0.6") @@ -181,6 +184,9 @@ def __call__( rounding: if not None, round the data according to the specified option, available options: ["torchrounding"]. + .. deprecated:: 0.6.0 + ``n_classes`` is deprecated, use ``num_classes`` instead. + """ # in case the new num_classes is default but you still call deprecated n_classes if n_classes is not None and num_classes is None: diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 2fc3993e3e..97ae4ec5a9 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -158,6 +158,9 @@ def __init__( each element corresponds to a key in ``keys``. allow_missing_keys: don't raise exception if key is missing. + .. deprecated:: 0.6.0 + ``n_classes`` is deprecated, use ``num_classes`` instead. + """ # in case the new num_classes is default but you still call deprecated n_classes if n_classes is not None and num_classes is None: diff --git a/monai/transforms/spatial/array.py b/monai/transforms/spatial/array.py index 6f662f2dce..01bd0cec6d 100644 --- a/monai/transforms/spatial/array.py +++ b/monai/transforms/spatial/array.py @@ -1029,6 +1029,9 @@ class AffineGrid(Transform): supplied matrix. Should be square with each side = num of image spatial dimensions + 1. + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ backend = [TransformBackends.TORCH, TransformBackends.NUMPY] @@ -1152,6 +1155,10 @@ def __init__( - :py:meth:`monai.transforms.utils.create_shear` - :py:meth:`monai.transforms.utils.create_translate` - :py:meth:`monai.transforms.utils.create_scale` + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ self.rotate_range = ensure_tuple(rotate_range) self.shear_range = ensure_tuple(shear_range) @@ -1290,6 +1297,10 @@ def __init__( Padding mode for outside grid values. Defaults to ``"border"``. See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample device: device on which the tensor will be allocated. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ self.mode: GridSampleMode = look_up_option(mode, GridSampleMode) self.padding_mode: GridSamplePadMode = look_up_option(padding_mode, GridSamplePadMode) @@ -1418,6 +1429,10 @@ def __init__( See also: https://pytorch.org/docs/stable/nn.functional.html#grid-sample device: device on which the tensor will be allocated. image_only: if True return only the image volume, otherwise return (image, affine). + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ self.affine_grid = AffineGrid( rotate_params=rotate_params, @@ -1531,6 +1546,10 @@ def __init__( See also: - :py:class:`RandAffineGrid` for the random affine parameters configurations. - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ RandomizableTransform.__init__(self, prob) @@ -1707,6 +1726,10 @@ def __init__( See also: - :py:class:`RandAffineGrid` for the random affine parameters configurations. - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ RandomizableTransform.__init__(self, prob) self.deform_grid = RandDeformGrid( @@ -1853,6 +1876,10 @@ def __init__( See also: - :py:class:`RandAffineGrid` for the random affine parameters configurations. - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ RandomizableTransform.__init__(self, prob) self.rand_affine_grid = RandAffineGrid( diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index f36300dea6..0b6473a22c 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -633,6 +633,10 @@ def __init__( See also: - :py:class:`monai.transforms.compose.MapTransform` - :py:class:`RandAffineGrid` for the random affine parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ MapTransform.__init__(self, keys, allow_missing_keys) self.affine = Affine( @@ -761,6 +765,10 @@ def __init__( See also: - :py:class:`monai.transforms.compose.MapTransform` - :py:class:`RandAffineGrid` for the random affine parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) @@ -922,6 +930,10 @@ def __init__( See also: - :py:class:`RandAffineGrid` for the random affine parameters configurations. - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) @@ -1052,6 +1064,10 @@ def __init__( See also: - :py:class:`RandAffineGrid` for the random affine parameters configurations. - :py:class:`Affine` for the affine transformation parameters configurations. + + .. deprecated:: 0.6.0 + ``as_tensor_output`` is deprecated. + """ MapTransform.__init__(self, keys, allow_missing_keys) RandomizableTransform.__init__(self, prob) diff --git a/tests/test_handler_transform_inverter.py b/tests/test_handler_transform_inverter.py deleted file mode 100644 index 385311eba7..0000000000 --- a/tests/test_handler_transform_inverter.py +++ /dev/null @@ -1,152 +0,0 @@ -# Copyright 2020 - 2021 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 sys -import unittest - -import numpy as np -import torch -from ignite.engine import Engine - -from monai.data import CacheDataset, DataLoader, create_test_image_3d, decollate_batch -from monai.engines.utils import IterationEvents -from monai.handlers import TransformInverter -from monai.transforms import ( - AddChanneld, - CastToTyped, - Compose, - CopyItemsd, - LoadImaged, - Orientationd, - RandAffined, - RandAxisFlipd, - RandFlipd, - RandRotate90d, - RandRotated, - RandZoomd, - ResizeWithPadOrCropd, - ScaleIntensityd, - Spacingd, - ToTensord, -) -from monai.utils.misc import set_determinism -from tests.utils import make_nifti_image - -KEYS = ["image", "label"] - - -class TestTransformInverter(unittest.TestCase): - def test_invert(self): - set_determinism(seed=0) - im_fname, seg_fname = [make_nifti_image(i) for i in create_test_image_3d(101, 100, 107, noise_max=100)] - transform = Compose( - [ - LoadImaged(KEYS), - AddChanneld(KEYS), - Orientationd(KEYS, "RPS"), - Spacingd(KEYS, pixdim=(1.2, 1.01, 0.9), mode=["bilinear", "nearest"], dtype=np.float32), - ScaleIntensityd("image", minv=1, maxv=10), - RandFlipd(KEYS, prob=0.5, spatial_axis=[1, 2]), - RandAxisFlipd(KEYS, prob=0.5), - RandRotate90d(KEYS, spatial_axes=(1, 2)), - RandZoomd(KEYS, prob=0.5, min_zoom=0.5, max_zoom=1.1, keep_size=True), - RandRotated(KEYS, prob=0.5, range_x=np.pi, mode="bilinear", align_corners=True), - RandAffined(KEYS, prob=0.5, rotate_range=np.pi, mode="nearest"), - ResizeWithPadOrCropd(KEYS, 100), - ToTensord("image"), # test to support both Tensor and Numpy array when inverting - CastToTyped(KEYS, dtype=[torch.uint8, np.uint8]), - CopyItemsd("label", times=2, names=["label_inverted1", "label_inverted2"]), - CopyItemsd("image", times=2, names=["image_inverted1", "image_inverted2"]), - ] - ) - data = [{"image": im_fname, "label": seg_fname} for _ in range(12)] - - # num workers = 0 for mac or gpu transforms - num_workers = 0 if sys.platform == "darwin" or torch.cuda.is_available() else 2 - - dataset = CacheDataset(data, transform=transform, progress=False) - loader = DataLoader(dataset, num_workers=num_workers, batch_size=5) - - # set up engine - def _train_func(engine, batch): - self.assertTupleEqual(batch["image"].shape[1:], (1, 100, 100, 100)) - engine.state.output = engine.state.batch = decollate_batch(batch) - engine.fire_event(IterationEvents.MODEL_COMPLETED) - return engine.state.output - - engine = Engine(_train_func) - engine.register_events(*IterationEvents) - - # set up testing handler - TransformInverter( - transform=transform, - output_keys=["image_inverted1", "label_inverted1"], - batch_keys="label", - meta_keys=["image_inverted1_meta_dict", "label_inverted1_meta_dict"], - batch_meta_keys="label_meta_dict", - nearest_interp=True, - to_tensor=[True, False], - device="cpu", - ).attach(engine) - - # test different nearest interpolation values - TransformInverter( - transform=transform, - output_keys=["image_inverted2", "label_inverted2"], - batch_keys="image", - meta_keys=None, - batch_meta_keys="image_meta_dict", - meta_key_postfix="meta_dict", - nearest_interp=[True, False], - post_func=[lambda x: x + 10, lambda x: x], - ).attach(engine) - - engine.run(loader, max_epochs=1) - set_determinism(seed=None) - - for output in engine.state.output: - self.assertTupleEqual(output["image"].shape, (1, 100, 100, 100)) - self.assertTupleEqual(output["label"].shape, (1, 100, 100, 100)) - # check the nearest inerpolation mode - i = output["image_inverted1"] - torch.testing.assert_allclose(i.to(torch.uint8).to(torch.float), i.to(torch.float)) - self.assertTupleEqual(i.shape, (1, 100, 101, 107)) - i = output["label_inverted1"] - np.testing.assert_allclose(i.astype(np.uint8).astype(np.float32), i.astype(np.float32)) - self.assertTupleEqual(i.shape, (1, 100, 101, 107)) - - # check the case that different items use different interpolation mode to invert transforms - d = output["image_inverted2"] - # if the interpolation mode is nearest, accumulated diff should be smaller than 1 - self.assertLess(torch.sum(d.to(torch.float) - d.to(torch.uint8).to(torch.float)).item(), 1.0) - self.assertTupleEqual(d.shape, (1, 100, 101, 107)) - - d = output["label_inverted2"] - # if the interpolation mode is not nearest, accumulated diff should be greater than 10000 - self.assertGreater(torch.sum(d.to(torch.float) - d.to(torch.uint8).to(torch.float)).item(), 10000.0) - self.assertTupleEqual(d.shape, (1, 100, 101, 107)) - - # check labels match - reverted = engine.state.output[-1]["label_inverted1"].astype(np.int32) - original = LoadImaged(KEYS)(data[-1])["label"] - n_good = np.sum(np.isclose(reverted, original, atol=1e-3)) - reverted_name = engine.state.batch[-1]["label_inverted1_meta_dict"]["filename_or_obj"] - original_name = data[-1]["label"] - self.assertEqual(reverted_name, original_name) - print("invert diff", reverted.size - n_good) - # 25300: 2 workers (cpu, non-macos) - # 1812: 0 workers (gpu or macos) - # 1824: torch 1.5.1 - self.assertTrue((reverted.size - n_good) in (25300, 1812, 1824), "diff. in 3 possible values") - - -if __name__ == "__main__": - unittest.main() From ac73507ce8a638ba978cb053d527e9d13d9f4918 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Mon, 20 Sep 2021 13:31:34 +0100 Subject: [PATCH 2/3] min-test list Signed-off-by: Wenqi Li --- tests/min_tests.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/min_tests.py b/tests/min_tests.py index bac6521889..e9350abbad 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -75,7 +75,6 @@ def run_testsuit(): "test_handler_surface_distance", "test_handler_tb_image", "test_handler_tb_stats", - "test_handler_transform_inverter", "test_handler_validation", "test_hausdorff_distance", "test_header_correct", From 9e8cb423afb52cc2d1ab3615043c505c7d48233a Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Mon, 20 Sep 2021 14:51:54 +0100 Subject: [PATCH 3/3] preserves tests from tests/test_handler_transform_inverter.py Signed-off-by: Wenqi Li --- tests/test_invertd.py | 72 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/tests/test_invertd.py b/tests/test_invertd.py index 5b98653f0a..eda900bf2d 100644 --- a/tests/test_invertd.py +++ b/tests/test_invertd.py @@ -34,6 +34,7 @@ ResizeWithPadOrCropd, ScaleIntensityd, Spacingd, + ToTensord, ) from monai.utils.misc import set_determinism from tests.utils import make_nifti_image @@ -63,8 +64,10 @@ def test_invert(self): CopyItemsd("image_meta_dict", times=1, names="test_dict"), # test to support Tensor, Numpy array and dictionary when inverting EnsureTyped(keys=["image", "test_dict"]), + ToTensord("image"), CastToTyped(KEYS, dtype=[torch.uint8, np.uint8]), - CopyItemsd("label", times=1, names="label_inverted"), + CopyItemsd("label", times=2, names=["label_inverted", "label_inverted1"]), + CopyItemsd("image", times=2, names=["image_inverted", "image_inverted1"]), ] ) data = [{"image": im_fname, "label": seg_fname} for _ in range(12)] @@ -76,25 +79,58 @@ def test_invert(self): loader = DataLoader(dataset, num_workers=num_workers, batch_size=5) inverter = Invertd( # `image` was not copied, invert the original value directly - keys=["image", "label_inverted", "test_dict"], + keys=["image_inverted", "label_inverted", "test_dict"], transform=transform, orig_keys=["label", "label", "test_dict"], - meta_keys=["image_meta_dict", "label_inverted_meta_dict", None], + meta_keys=["image_inverted_meta_dict", "label_inverted_meta_dict", None], orig_meta_keys=["label_meta_dict", "label_meta_dict", None], nearest_interp=True, to_tensor=[True, False, False], device="cpu", ) + inverter_1 = Invertd( + # `image` was not copied, invert the original value directly + keys=["image_inverted1", "label_inverted1"], + transform=transform, + orig_keys=["image", "image"], + meta_keys=["image_inverted1_meta_dict", "label_inverted1_meta_dict"], + orig_meta_keys=["image_meta_dict", "image_meta_dict"], + nearest_interp=[True, False], + to_tensor=[True, True], + device="cpu", + ) + + expected_keys = [ + "image", + "image_inverted", + "image_inverted1", + "image_inverted1_meta_dict", + "image_inverted_meta_dict", + "image_meta_dict", + "image_transforms", + "label", + "label_inverted", + "label_inverted1", + "label_inverted1_meta_dict", + "label_inverted_meta_dict", + "label_meta_dict", + "label_transforms", + "test_dict", + "test_dict_transforms", + ] # execute 1 epoch for d in loader: d = decollate_batch(d) for item in d: item = inverter(item) - # this unit test only covers basic function, test_handler_transform_inverter covers more + item = inverter_1(item) + + self.assertListEqual(sorted(item), expected_keys) + self.assertTupleEqual(item["image"].shape[1:], (100, 100, 100)) self.assertTupleEqual(item["label"].shape[1:], (100, 100, 100)) - # check the nearest inerpolation mode - i = item["image"] + # check the nearest interpolation mode + i = item["image_inverted"] torch.testing.assert_allclose(i.to(torch.uint8).to(torch.float), i.to(torch.float)) self.assertTupleEqual(i.shape[1:], (100, 101, 107)) i = item["label_inverted"] @@ -104,6 +140,30 @@ def test_invert(self): self.assertTrue(isinstance(item["test_dict"]["affine"], np.ndarray)) self.assertTrue(isinstance(item["test_dict"]["filename_or_obj"], str)) + # check the case that different items use different interpolation mode to invert transforms + d = item["image_inverted1"] + # if the interpolation mode is nearest, accumulated diff should be smaller than 1 + self.assertLess(torch.sum(d.to(torch.float) - d.to(torch.uint8).to(torch.float)).item(), 1.0) + self.assertTupleEqual(d.shape, (1, 100, 101, 107)) + + d = item["label_inverted1"] + # if the interpolation mode is not nearest, accumulated diff should be greater than 10000 + self.assertGreater(torch.sum(d.to(torch.float) - d.to(torch.uint8).to(torch.float)).item(), 10000.0) + self.assertTupleEqual(d.shape, (1, 100, 101, 107)) + + # check labels match + reverted = item["label_inverted"].astype(np.int32) + original = LoadImaged(KEYS)(data[-1])["label"] + n_good = np.sum(np.isclose(reverted, original, atol=1e-3)) + reverted_name = item["label_inverted_meta_dict"]["filename_or_obj"] + original_name = data[-1]["label"] + self.assertEqual(reverted_name, original_name) + print("invert diff", reverted.size - n_good) + # 25300: 2 workers (cpu, non-macos) + # 1812: 0 workers (gpu or macos) + # 1824: torch 1.5.1 + self.assertTrue((reverted.size - n_good) in (25300, 1812, 1824), "diff. in 3 possible values") + set_determinism(seed=None)