From 1af1ebb2b5a2cccbb6e0cc0902e7a10e36df4c4f Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Wed, 6 Jul 2022 22:33:59 +0100 Subject: [PATCH 01/18] bc nonbreaking tests Signed-off-by: Wenqi Li --- monai/transforms/inverse.py | 38 ++++++++++++++++++++++++++++++++++- monai/transforms/transform.py | 31 +++++++++++++++++++++++++++- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 41a02989db..51f1d09612 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -12,13 +12,15 @@ import os import warnings from contextlib import contextmanager +from functools import wraps from typing import Any, Hashable, Mapping, Optional, Tuple import torch +from monai import transforms from monai.data.meta_tensor import MetaTensor from monai.transforms.transform import Transform -from monai.utils.enums import TraceKeys +from monai.utils.enums import PostFix, TraceKeys __all__ = ["TraceableTransform", "InvertibleTransform"] @@ -213,6 +215,18 @@ def trace_transform(self, to_trace: bool): self.tracing = prev +def attach_pre_hook(func, hook): + @wraps(func) + def wrapper(*args, **kwargs): + if not isinstance(args[0], transforms.MapTransform): + return func(*args, **kwargs) + inst, data_dict = args + data_dict = hook(inst, data_dict) + return func(inst, data_dict) + + return wrapper + + class InvertibleTransform(TraceableTransform): """Classes for invertible transforms. @@ -250,6 +264,28 @@ class InvertibleTransform(TraceableTransform): """ + def __new__(cls, *args, **kwargs): + cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) + return super().__new__(cls) + + def comp_update(self, data): + if not isinstance(data, dict): + return data + if not isinstance(self, transforms.MapTransform): + return data + d = dict(data) + for k in self.key_iterator(data): + transform_key = transforms.TraceableTransform.trace_key(k) + if transform_key in data: + if not isinstance(data[k], MetaTensor): + d[k] = MetaTensor(data[k]) + d[k].applied_operations = data[transform_key] + meta_dict_key = PostFix.meta(k) + if meta_dict_key in data: + d[k].meta.update(data[meta_dict_key]) + d.pop(transform_key) + return d + def inverse(self, data: Any) -> Any: """ Inverse of ``__call__``. diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 5819d2971d..927aa333ad 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -14,6 +14,7 @@ import logging from abc import ABC, abstractmethod +from functools import wraps from typing import Any, Callable, Dict, Generator, Hashable, Iterable, List, Mapping, Optional, Tuple, TypeVar, Union import numpy as np @@ -21,8 +22,9 @@ from monai import transforms from monai.config import KeysCollection +from monai.data.meta_tensor import MetaTensor from monai.utils import MAX_SEED, ensure_tuple, first -from monai.utils.enums import TransformBackends +from monai.utils.enums import PostFix, TransformBackends __all__ = ["ThreadUnsafe", "apply_transform", "Randomizable", "RandomizableTransform", "Transform", "MapTransform"] @@ -286,6 +288,15 @@ def randomize(self, data: Any) -> None: self._do_transform = self.R.rand() < self.prob +def attach_post_hook(func, hook): + @wraps(func) + def wrapper(*args, **kwargs): + out = func(*args, **kwargs) + return hook(args[0], out) + + return wrapper + + class MapTransform(Transform): """ A subclass of :py:class:`monai.transforms.Transform` with an assumption @@ -311,6 +322,10 @@ def __call__(self, data): """ + def __new__(cls, *args, **kwargs): + cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) + return super().__new__(cls) + def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: self.keys: Tuple[Hashable, ...] = ensure_tuple(keys) self.allow_missing_keys = allow_missing_keys @@ -320,6 +335,20 @@ def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> No if not isinstance(key, Hashable): raise TypeError(f"keys must be one of (Hashable, Iterable[Hashable]) but is {type(keys).__name__}.") + def comp_update(self, data): + if not isinstance(data, Mapping): + return data + d = dict(data) + for k in data: + if isinstance(data[k], MetaTensor): + meta_dict_key = PostFix.meta(k) + if meta_dict_key in d: + d[meta_dict_key].update(data[k].meta) + if data[k].applied_operations: + transform_key = transforms.TraceableTransform.trace_key(k) + d[transform_key] = data[k].applied_operations + return d + @abstractmethod def __call__(self, data): """ From 7f65b370620ae9b5d6cbe5d4619162cb0257475f Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 07:29:17 +0100 Subject: [PATCH 02/18] update docstring Signed-off-by: Wenqi Li --- monai/transforms/inverse.py | 28 +++++++++++++++++----------- monai/transforms/transform.py | 22 +++++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 51f1d09612..a6506fa5ee 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -216,6 +216,8 @@ def trace_transform(self, to_trace: bool): def attach_pre_hook(func, hook): + """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" + @wraps(func) def wrapper(*args, **kwargs): if not isinstance(args[0], transforms.MapTransform): @@ -269,21 +271,25 @@ def __new__(cls, *args, **kwargs): return super().__new__(cls) def comp_update(self, data): - if not isinstance(data, dict): - return data - if not isinstance(self, transforms.MapTransform): + """ + This function is to be called before every `self.inverse(data)`, + update each MetaTensor `data[key]` using `data[key_transforms]` and `data[key_meta_dict]`, + for MetaTensor backward compatibility. + """ + if not isinstance(data, dict) or not isinstance(self, transforms.MapTransform): return data d = dict(data) for k in self.key_iterator(data): transform_key = transforms.TraceableTransform.trace_key(k) - if transform_key in data: - if not isinstance(data[k], MetaTensor): - d[k] = MetaTensor(data[k]) - d[k].applied_operations = data[transform_key] - meta_dict_key = PostFix.meta(k) - if meta_dict_key in data: - d[k].meta.update(data[meta_dict_key]) - d.pop(transform_key) + if transform_key not in data: + continue + if not isinstance(data[k], MetaTensor): + d[k] = MetaTensor(data[k]) + d[k].applied_operations = data[transform_key] + meta_dict_key = PostFix.meta(k) + if meta_dict_key in data: + d[k].meta.update(data[meta_dict_key]) + d.pop(transform_key) return d def inverse(self, data: Any) -> Any: diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 927aa333ad..ab47a0d3de 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -289,6 +289,8 @@ def randomize(self, data: Any) -> None: def attach_post_hook(func, hook): + """adds `hook` after `func` calls using `func`'s return value""" + @wraps(func) def wrapper(*args, **kwargs): out = func(*args, **kwargs) @@ -336,17 +338,23 @@ def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> No raise TypeError(f"keys must be one of (Hashable, Iterable[Hashable]) but is {type(keys).__name__}.") def comp_update(self, data): + """ + This function is to be called after every `self.__call__(data)`, + update `data[key_transforms]` and `data[key_meta_dict]` using the content from MetaTensor `data[key]`, + for MetaTensor backward compatibility. + """ if not isinstance(data, Mapping): return data d = dict(data) for k in data: - if isinstance(data[k], MetaTensor): - meta_dict_key = PostFix.meta(k) - if meta_dict_key in d: - d[meta_dict_key].update(data[k].meta) - if data[k].applied_operations: - transform_key = transforms.TraceableTransform.trace_key(k) - d[transform_key] = data[k].applied_operations + if not isinstance(data[k], MetaTensor): + continue + meta_dict_key = PostFix.meta(k) + if meta_dict_key in d: + d[meta_dict_key].update(data[k].meta) + if data[k].applied_operations: + transform_key = transforms.TraceableTransform.trace_key(k) + d[transform_key] = data[k].applied_operations return d @abstractmethod From c01dd6365ee3473bcc8ed3713fbfb85076cb65d6 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 09:01:13 +0100 Subject: [PATCH 03/18] compatible collate Signed-off-by: Wenqi Li --- monai/data/utils.py | 8 +++++++- monai/transforms/inverse.py | 2 +- monai/transforms/transform.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index 45294cc66e..ea86497a44 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -404,7 +404,13 @@ def collate_meta_tensor(batch): collated.is_batch = True return collated if isinstance(elem_0, Mapping): - return {k: collate_meta_tensor([d[k] for d in batch]) for k in elem_0} + return { + k: [d[k] or TraceKeys.NONE for d in batch] + if (isinstance(k, str) and k.endswith(TraceKeys.KEY_SUFFIX)) # for compatibility 0.9.0 + else collate_meta_tensor([d[k] for d in batch]) + for k in elem_0 + } + # no more recursive search for MetaTensor return default_collate(batch) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index a6506fa5ee..10088b12aa 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -274,7 +274,7 @@ def comp_update(self, data): """ This function is to be called before every `self.inverse(data)`, update each MetaTensor `data[key]` using `data[key_transforms]` and `data[key_meta_dict]`, - for MetaTensor backward compatibility. + for MetaTensor backward compatibility 0.9.0. """ if not isinstance(data, dict) or not isinstance(self, transforms.MapTransform): return data diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index ab47a0d3de..6571aff59e 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -341,7 +341,7 @@ def comp_update(self, data): """ This function is to be called after every `self.__call__(data)`, update `data[key_transforms]` and `data[key_meta_dict]` using the content from MetaTensor `data[key]`, - for MetaTensor backward compatibility. + for MetaTensor backward compatibility 0.9.0. """ if not isinstance(data, Mapping): return data From 621bf58ad9f9100810b8bb97d2a67ff50b9febc8 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 10:24:22 +0100 Subject: [PATCH 04/18] adds flag Signed-off-by: Wenqi Li --- monai/config/__init__.py | 2 ++ monai/config/deviceconfig.py | 12 ++++++++++++ monai/transforms/inverse.py | 11 ++++++----- monai/transforms/transform.py | 4 +++- tests/test_rand_affined.py | 2 ++ tests/test_to_from_meta_tensord.py | 7 +++++++ 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/monai/config/__init__.py b/monai/config/__init__.py index 5f67ea6584..0109664c14 100644 --- a/monai/config/__init__.py +++ b/monai/config/__init__.py @@ -11,6 +11,7 @@ from .deviceconfig import ( USE_COMPILED, + USE_METATENSOR, IgniteInfo, get_config_values, get_gpu_info, @@ -20,6 +21,7 @@ print_debug_info, print_gpu_info, print_system_info, + set_use_metatensor, ) from .type_definitions import ( DtypeLike, diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index ad633a133d..6e653f3143 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -27,6 +27,8 @@ except (OptionalImportError, ImportError, AttributeError): HAS_EXT = USE_COMPILED = False +USE_METATENSOR = os.environ.get("METATENSOR", "0") == "1" # use MetaTensor new feature, set to False for compatibility + psutil, has_psutil = optional_import("psutil") psutil_version = psutil.__version__ if has_psutil else "NOT INSTALLED or UNKNOWN VERSION." @@ -39,9 +41,19 @@ "print_debug_info", "USE_COMPILED", "IgniteInfo", + "set_use_metatensor", + "USE_METATENSOR", ] +def set_use_metatensor(flag: bool) -> bool: + """whether to use MetaTensor full new feature, set to False for backward compatibility (v0.9.0).""" + global USE_METATENSOR + original_flag = USE_METATENSOR + USE_METATENSOR = flag + return original_flag + + def get_config_values(): """ Read the package versions into a dictionary. diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 10088b12aa..17f2211877 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -17,7 +17,7 @@ import torch -from monai import transforms +import monai from monai.data.meta_tensor import MetaTensor from monai.transforms.transform import Transform from monai.utils.enums import PostFix, TraceKeys @@ -220,7 +220,7 @@ def attach_pre_hook(func, hook): @wraps(func) def wrapper(*args, **kwargs): - if not isinstance(args[0], transforms.MapTransform): + if not isinstance(args[0], monai.transforms.MapTransform): return func(*args, **kwargs) inst, data_dict = args data_dict = hook(inst, data_dict) @@ -267,7 +267,8 @@ class InvertibleTransform(TraceableTransform): """ def __new__(cls, *args, **kwargs): - cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) + if not monai.config.deviceconfig.USE_METATENSOR: + cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) return super().__new__(cls) def comp_update(self, data): @@ -276,11 +277,11 @@ def comp_update(self, data): update each MetaTensor `data[key]` using `data[key_transforms]` and `data[key_meta_dict]`, for MetaTensor backward compatibility 0.9.0. """ - if not isinstance(data, dict) or not isinstance(self, transforms.MapTransform): + if not isinstance(data, dict) or not isinstance(self, monai.transforms.MapTransform): return data d = dict(data) for k in self.key_iterator(data): - transform_key = transforms.TraceableTransform.trace_key(k) + transform_key = monai.transforms.TraceableTransform.trace_key(k) if transform_key not in data: continue if not isinstance(data[k], MetaTensor): diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 6571aff59e..12251c2c2a 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -20,6 +20,7 @@ import numpy as np import torch +import monai.config.deviceconfig from monai import transforms from monai.config import KeysCollection from monai.data.meta_tensor import MetaTensor @@ -325,7 +326,8 @@ def __call__(self, data): """ def __new__(cls, *args, **kwargs): - cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) + if not monai.config.deviceconfig.USE_METATENSOR: + cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) return super().__new__(cls) def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: diff --git a/tests/test_rand_affined.py b/tests/test_rand_affined.py index a33496895c..566eed68ef 100644 --- a/tests/test_rand_affined.py +++ b/tests/test_rand_affined.py @@ -221,6 +221,8 @@ def test_rand_affined(self, input_param, input_data, expected_val, track_meta): if input_param.get("cache_grid", False): self.assertTrue(g.rand_affine._cached_grid is not None) for key in res: + if isinstance(key, str) and key.endswith("_transforms"): + continue result = res[key] if track_meta: self.assertIsInstance(result, MetaTensor) diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py index 6f46055d6a..8ab619daf8 100644 --- a/tests/test_to_from_meta_tensord.py +++ b/tests/test_to_from_meta_tensord.py @@ -18,6 +18,7 @@ import torch from parameterized import parameterized +from monai.config import set_use_metatensor from monai.data.meta_tensor import MetaTensor from monai.transforms import FromMetaTensord, ToMetaTensord from monai.utils.enums import PostFix @@ -40,6 +41,12 @@ def rand_string(min_len=5, max_len=10): class TestToFromMetaTensord(unittest.TestCase): + def setUp(self): + self.flag = set_use_metatensor(True) + + def tearDown(self): + set_use_metatensor(self.flag) + @staticmethod def get_im(shape=None, dtype=None, device=None): if shape is None: From fde69177a49659f9eeed4ed19c413c1f74778996 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 12:52:13 +0100 Subject: [PATCH 05/18] update Signed-off-by: Wenqi Li --- monai/config/deviceconfig.py | 2 +- monai/transforms/inverse.py | 5 +++-- tests/test_decollate.py | 3 ++- tests/test_meta_tensor.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 6e653f3143..770effffd0 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -101,7 +101,7 @@ def print_config(file=sys.stdout): """ for k, v in get_config_values().items(): print(f"{k} version: {v}", file=file, flush=True) - print(f"MONAI flags: HAS_EXT = {HAS_EXT}, USE_COMPILED = {USE_COMPILED}") + print(f"MONAI flags: HAS_EXT = {HAS_EXT}, USE_COMPILED = {USE_COMPILED}, METATENSOR = {USE_METATENSOR}") print(f"MONAI rev id: {monai.__revision_id__}") print(f"MONAI __file__: {monai.__file__}") diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 17f2211877..6a1f62a976 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -220,7 +220,7 @@ def attach_pre_hook(func, hook): @wraps(func) def wrapper(*args, **kwargs): - if not isinstance(args[0], monai.transforms.MapTransform): + if len(args) < 2: return func(*args, **kwargs) inst, data_dict = args data_dict = hook(inst, data_dict) @@ -267,7 +267,8 @@ class InvertibleTransform(TraceableTransform): """ def __new__(cls, *args, **kwargs): - if not monai.config.deviceconfig.USE_METATENSOR: + if not monai.config.deviceconfig.USE_METATENSOR and issubclass(cls, monai.transforms.MapTransform): + # only update MapTransform, MapTransforms may call array transform's inverse cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) return super().__new__(cls) diff --git a/tests/test_decollate.py b/tests/test_decollate.py index 440f5e59e7..a634471be5 100644 --- a/tests/test_decollate.py +++ b/tests/test_decollate.py @@ -101,7 +101,8 @@ def check_match(self, in1, in2): # Transform ids won't match for windows with multiprocessing, so don't check values if k1 == TraceKeys.ID and sys.platform in ["darwin", "win32"]: continue - self.check_match(v1, v2) + if not (isinstance(k1, str) and k1.endswith("_transforms")): + self.check_match(v1, v2) # transform stack not necessarily match elif isinstance(in1, (list, tuple)): for l1, l2 in zip(in1, in2): self.check_match(l1, l2) diff --git a/tests/test_meta_tensor.py b/tests/test_meta_tensor.py index c1b8bc5046..c592a1b2c0 100644 --- a/tests/test_meta_tensor.py +++ b/tests/test_meta_tensor.py @@ -454,7 +454,7 @@ def test_transforms(self): data = _tr(data) is_meta = isinstance(_tr, (ToMetaTensord, BorderPadd, DivisiblePadd)) if is_meta: - self.assertEqual(len(data), 1) # im + self.assertEqual(len(data), 2) # im, im_transforms, compatibility self.assertIsInstance(data[key], MetaTensor) n_applied = len(data[key].applied_operations) else: From 49d91e8e17423d64cddde5766231c1316d886fa8 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 13:16:51 +0100 Subject: [PATCH 06/18] fixes min tests Signed-off-by: Wenqi Li --- monai/config/__init__.py | 1 - monai/config/deviceconfig.py | 11 +---------- monai/transforms/inverse.py | 3 ++- monai/transforms/transform.py | 5 ++--- tests/test_one_of.py | 8 ++++---- tests/test_to_from_meta_tensord.py | 14 ++++++-------- 6 files changed, 15 insertions(+), 27 deletions(-) diff --git a/monai/config/__init__.py b/monai/config/__init__.py index 0109664c14..26e08939ca 100644 --- a/monai/config/__init__.py +++ b/monai/config/__init__.py @@ -21,7 +21,6 @@ print_debug_info, print_gpu_info, print_system_info, - set_use_metatensor, ) from .type_definitions import ( DtypeLike, diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 770effffd0..23b3f08950 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -40,20 +40,11 @@ "print_gpu_info", "print_debug_info", "USE_COMPILED", - "IgniteInfo", - "set_use_metatensor", "USE_METATENSOR", + "IgniteInfo", ] -def set_use_metatensor(flag: bool) -> bool: - """whether to use MetaTensor full new feature, set to False for backward compatibility (v0.9.0).""" - global USE_METATENSOR - original_flag = USE_METATENSOR - USE_METATENSOR = flag - return original_flag - - def get_config_values(): """ Read the package versions into a dictionary. diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 6a1f62a976..6035ef380b 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -18,6 +18,7 @@ import torch import monai +from monai import config from monai.data.meta_tensor import MetaTensor from monai.transforms.transform import Transform from monai.utils.enums import PostFix, TraceKeys @@ -267,7 +268,7 @@ class InvertibleTransform(TraceableTransform): """ def __new__(cls, *args, **kwargs): - if not monai.config.deviceconfig.USE_METATENSOR and issubclass(cls, monai.transforms.MapTransform): + if not config.USE_METATENSOR and issubclass(cls, monai.transforms.MapTransform): # only update MapTransform, MapTransforms may call array transform's inverse cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) return super().__new__(cls) diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 12251c2c2a..d45c5b1aa8 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -20,8 +20,7 @@ import numpy as np import torch -import monai.config.deviceconfig -from monai import transforms +from monai import config, transforms from monai.config import KeysCollection from monai.data.meta_tensor import MetaTensor from monai.utils import MAX_SEED, ensure_tuple, first @@ -326,7 +325,7 @@ def __call__(self, data): """ def __new__(cls, *args, **kwargs): - if not monai.config.deviceconfig.USE_METATENSOR: + if not config.USE_METATENSOR: cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) return super().__new__(cls) diff --git a/tests/test_one_of.py b/tests/test_one_of.py index 29d13d7d0c..cdd59e07a9 100644 --- a/tests/test_one_of.py +++ b/tests/test_one_of.py @@ -165,10 +165,10 @@ def test_inverse(self, transform, invertible): if invertible: for k in KEYS: - # check transform was removed - self.assertTrue( - len(fwd_inv_data[TraceableTransform.trace_key(k)]) < len(fwd_data[TraceableTransform.trace_key(k)]) - ) + # # check transform was removed, compatibility 0.9.0 removed '_transforms' + # self.assertTrue( + # len(fwd_inv_data[TraceableTransform.trace_key(k)]) < len(fwd_data[TraceableTransform.trace_key(k)]) + # ) # check data is same as original (and different from forward) self.assertEqual(fwd_inv_data[k], data[k]) self.assertNotEqual(fwd_inv_data[k], fwd_data[k]) diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py index 8ab619daf8..a7c887d990 100644 --- a/tests/test_to_from_meta_tensord.py +++ b/tests/test_to_from_meta_tensord.py @@ -18,15 +18,12 @@ import torch from parameterized import parameterized -from monai.config import set_use_metatensor +from monai import config from monai.data.meta_tensor import MetaTensor from monai.transforms import FromMetaTensord, ToMetaTensord from monai.utils.enums import PostFix -from monai.utils.module import get_torch_version_tuple from tests.utils import TEST_DEVICES, assert_allclose -PT_VER_MAJ, PT_VER_MIN = get_torch_version_tuple() - DTYPES = [[torch.float32], [torch.float64], [torch.float16], [torch.int64], [torch.int32]] TESTS = [] for _device in TEST_DEVICES: @@ -42,15 +39,16 @@ def rand_string(min_len=5, max_len=10): class TestToFromMetaTensord(unittest.TestCase): def setUp(self): - self.flag = set_use_metatensor(True) + self.flag = config.USE_METATENSOR + config.USE_METATENSOR = True def tearDown(self): - set_use_metatensor(self.flag) + config.USE_METATENSOR = self.flag @staticmethod def get_im(shape=None, dtype=None, device=None): if shape is None: - shape = shape = (1, 10, 8) + shape = (1, 10, 8) affine = torch.randint(0, 10, (4, 4)) meta = {"fname": rand_string()} t = torch.rand(shape) @@ -149,7 +147,7 @@ def test_from_to_meta_tensord(self, device, dtype): # TO -> Forward t_to_meta = ToMetaTensord(["m1", "m2"]) d_dict_meta = t_to_meta(d_dict) - self.assertEqual(sorted(d_dict_meta.keys()), ["m1", "m2", "m3"]) + self.assertEqual(sorted(d_dict_meta.keys()), ["m1", "m2", "m3"], f"flag: {config.USE_METATENSOR}") self.check(d_dict_meta["m3"], m3, ids=True) # unchanged (except deep copy in inverse) self.check(d_dict_meta["m1"], m1, ids=False) meta_out = {k: v for k, v in d_dict_meta["m1"].meta.items() if k != "affine"} From eb988fc7462d0834a5809f272c97e2a3280b27ea Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 15:11:55 +0100 Subject: [PATCH 07/18] integration Signed-off-by: Wenqi Li --- monai/data/utils.py | 13 ++++++++----- tests/test_to_from_meta_tensord.py | 1 + 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index ea86497a44..c27c795742 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -406,7 +406,7 @@ def collate_meta_tensor(batch): if isinstance(elem_0, Mapping): return { k: [d[k] or TraceKeys.NONE for d in batch] - if (isinstance(k, str) and k.endswith(TraceKeys.KEY_SUFFIX)) # for compatibility 0.9.0 + if f"{k}".endswith(TraceKeys.KEY_SUFFIX) # for compatibility 0.9.0 else collate_meta_tensor([d[k] for d in batch]) for k in elem_0 } @@ -434,7 +434,10 @@ def list_data_collate(batch: Sequence): for k in elem: key = k data_for_batch = [d[key] for d in data] - ret[key] = collate_meta_tensor(data_for_batch) + if f"{key}".endswith(TraceKeys.KEY_SUFFIX): # for compatibility 0.9.0 + ret[key] = data_for_batch + else: + ret[key] = collate_meta_tensor(data_for_batch) else: ret = collate_meta_tensor(data) return ret @@ -445,9 +448,9 @@ def list_data_collate(batch: Sequence): re_str += f"\nCollate error on the key '{key}' of dictionary data." re_str += ( "\n\nMONAI hint: if your transforms intentionally create images of different shapes, creating your " - + "`DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem (check its " - + "documentation)." + + "`DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem (check its docs)." ) + _ = dev_collate(data) raise RuntimeError(re_str) from re except TypeError as re: @@ -458,8 +461,8 @@ def list_data_collate(batch: Sequence): re_str += ( "\n\nMONAI hint: if your transforms intentionally create mixtures of torch Tensor and numpy ndarray, " + "creating your `DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem " - + "(check its documentation)." ) + _ = dev_collate(data) raise TypeError(re_str) from re diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py index a7c887d990..302c17abed 100644 --- a/tests/test_to_from_meta_tensord.py +++ b/tests/test_to_from_meta_tensord.py @@ -37,6 +37,7 @@ def rand_string(min_len=5, max_len=10): return "".join(random.choice(chars) for _ in range(str_size)) +@unittest.skipIf(not config.USE_METATENSOR, "skipping not latest") class TestToFromMetaTensord(unittest.TestCase): def setUp(self): self.flag = config.USE_METATENSOR From aeb823dd1314c2eaacc4d02449acee6b8bd8b2cd Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 16:36:28 +0100 Subject: [PATCH 08/18] default to metatensor Signed-off-by: Wenqi Li --- monai/config/deviceconfig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index 23b3f08950..d1a72db65c 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -27,7 +27,7 @@ except (OptionalImportError, ImportError, AttributeError): HAS_EXT = USE_COMPILED = False -USE_METATENSOR = os.environ.get("METATENSOR", "0") == "1" # use MetaTensor new feature, set to False for compatibility +USE_METATENSOR = os.environ.get("METATENSOR", "1") == "1" # use MetaTensor new feature, set to False for compatibility psutil, has_psutil = optional_import("psutil") psutil_version = psutil.__version__ if has_psutil else "NOT INSTALLED or UNKNOWN VERSION." From e5269dcea079e5f1cf0127cbdc7c9eb200ed2f66 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 17:51:03 +0100 Subject: [PATCH 09/18] update Signed-off-by: Wenqi Li --- monai/config/__init__.py | 2 +- monai/config/deviceconfig.py | 6 ++--- monai/data/utils.py | 38 ++++++++++++++++++------------ monai/transforms/inverse.py | 4 ++-- monai/transforms/transform.py | 8 +++---- tests/test_meta_tensor.py | 3 ++- tests/test_to_from_meta_tensord.py | 10 ++++---- 7 files changed, 40 insertions(+), 31 deletions(-) diff --git a/monai/config/__init__.py b/monai/config/__init__.py index 26e08939ca..f494202a56 100644 --- a/monai/config/__init__.py +++ b/monai/config/__init__.py @@ -11,7 +11,7 @@ from .deviceconfig import ( USE_COMPILED, - USE_METATENSOR, + USE_META_DICT, IgniteInfo, get_config_values, get_gpu_info, diff --git a/monai/config/deviceconfig.py b/monai/config/deviceconfig.py index d1a72db65c..87d46895aa 100644 --- a/monai/config/deviceconfig.py +++ b/monai/config/deviceconfig.py @@ -27,7 +27,7 @@ except (OptionalImportError, ImportError, AttributeError): HAS_EXT = USE_COMPILED = False -USE_METATENSOR = os.environ.get("METATENSOR", "1") == "1" # use MetaTensor new feature, set to False for compatibility +USE_META_DICT = os.environ.get("USE_META_DICT", "0") == "1" # set to True for compatibility, use meta dict. psutil, has_psutil = optional_import("psutil") psutil_version = psutil.__version__ if has_psutil else "NOT INSTALLED or UNKNOWN VERSION." @@ -40,7 +40,7 @@ "print_gpu_info", "print_debug_info", "USE_COMPILED", - "USE_METATENSOR", + "USE_META_DICT", "IgniteInfo", ] @@ -92,7 +92,7 @@ def print_config(file=sys.stdout): """ for k, v in get_config_values().items(): print(f"{k} version: {v}", file=file, flush=True) - print(f"MONAI flags: HAS_EXT = {HAS_EXT}, USE_COMPILED = {USE_COMPILED}, METATENSOR = {USE_METATENSOR}") + print(f"MONAI flags: HAS_EXT = {HAS_EXT}, USE_COMPILED = {USE_COMPILED}, USE_META_DICT = {USE_META_DICT}") print(f"MONAI rev id: {monai.__revision_id__}") print(f"MONAI __file__: {monai.__file__}") diff --git a/monai/data/utils.py b/monai/data/utils.py index c27c795742..578f2e7a0f 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -391,6 +391,22 @@ def dev_collate(batch, level: int = 1, logger_name: str = "dev_collate"): return +def pickle_operations(data, key=TraceKeys.KEY_SUFFIX, is_encode: bool = True): + """applied_operations are dictionaries with varying sizes, converting them to bytes so that we can (de-)collate.""" + if isinstance(data, Mapping): + data, has_items = dict(data), False + for k in data: + if f"{k}".endswith(TraceKeys.KEY_SUFFIX): + if is_encode and not isinstance(data[k], bytes): + data[k] = pickle.dumps(data[k], 0) + if not is_encode and isinstance(data[k], bytes): + data[k] = pickle.loads(data[k]) + return data if has_items else {k: pickle_operations(v, key=key, is_encode=is_encode) for k, v in data.items()} + elif isinstance(data, (list, tuple)): + return [pickle_operations(item, key=key, is_encode=is_encode) for item in data] + return data + + def collate_meta_tensor(batch): """collate a sequence of meta tensor sequences/dictionaries into a single batched metatensor or a dictionary of batched metatensor""" @@ -404,13 +420,7 @@ def collate_meta_tensor(batch): collated.is_batch = True return collated if isinstance(elem_0, Mapping): - return { - k: [d[k] or TraceKeys.NONE for d in batch] - if f"{k}".endswith(TraceKeys.KEY_SUFFIX) # for compatibility 0.9.0 - else collate_meta_tensor([d[k] for d in batch]) - for k in elem_0 - } - + return {k: collate_meta_tensor([d[k] for d in batch]) for k in elem_0} # no more recursive search for MetaTensor return default_collate(batch) @@ -429,15 +439,13 @@ def list_data_collate(batch: Sequence): data = [i for k in batch for i in k] if isinstance(elem, list) else batch key = None try: + data = pickle_operations(data) # bc 0.9.0 if isinstance(elem, Mapping): ret = {} for k in elem: key = k data_for_batch = [d[key] for d in data] - if f"{key}".endswith(TraceKeys.KEY_SUFFIX): # for compatibility 0.9.0 - ret[key] = data_for_batch - else: - ret[key] = collate_meta_tensor(data_for_batch) + ret[key] = collate_meta_tensor(data_for_batch) else: ret = collate_meta_tensor(data) return ret @@ -448,9 +456,9 @@ def list_data_collate(batch: Sequence): re_str += f"\nCollate error on the key '{key}' of dictionary data." re_str += ( "\n\nMONAI hint: if your transforms intentionally create images of different shapes, creating your " - + "`DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem (check its docs)." + + "`DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem (check its " + + "documentation)." ) - _ = dev_collate(data) raise RuntimeError(re_str) from re except TypeError as re: @@ -461,8 +469,8 @@ def list_data_collate(batch: Sequence): re_str += ( "\n\nMONAI hint: if your transforms intentionally create mixtures of torch Tensor and numpy ndarray, " + "creating your `DataLoader` with `collate_fn=pad_list_data_collate` might solve this problem " + + "(check its documentation)." ) - _ = dev_collate(data) raise TypeError(re_str) from re @@ -582,7 +590,7 @@ def decollate_batch(batch, detach: bool = True, pad=True, fill_value=None): deco[k] = [deepcopy(deco[k]) for _ in range(b)] if isinstance(deco, Mapping): _gen = zip_longest(*deco.values(), fillvalue=fill_value) if pad else zip(*deco.values()) - return [dict(zip(deco, item)) for item in _gen] + return pickle_operations([dict(zip(deco, item)) for item in _gen]) # bc 0.9.0 if isinstance(deco, Iterable): _gen = zip_longest(*deco, fillvalue=fill_value) if pad else zip(*deco) return [list(item) for item in _gen] diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 6035ef380b..987180e655 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -268,10 +268,10 @@ class InvertibleTransform(TraceableTransform): """ def __new__(cls, *args, **kwargs): - if not config.USE_METATENSOR and issubclass(cls, monai.transforms.MapTransform): + if config.USE_META_DICT and issubclass(cls, monai.transforms.MapTransform): # only update MapTransform, MapTransforms may call array transform's inverse cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) - return super().__new__(cls) + return TraceableTransform.__new__(cls) def comp_update(self, data): """ diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index d45c5b1aa8..e87b1a8e53 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -324,10 +324,10 @@ def __call__(self, data): """ - def __new__(cls, *args, **kwargs): - if not config.USE_METATENSOR: - cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) - return super().__new__(cls) + def __new__(cls, keys: KeysCollection = None, allow_missing_keys: bool = False, *args, **kwargs): + if config.USE_META_DICT: + cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) # type: ignore + return Transform.__new__(cls) def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: self.keys: Tuple[Hashable, ...] = ensure_tuple(keys) diff --git a/tests/test_meta_tensor.py b/tests/test_meta_tensor.py index c592a1b2c0..f3a6205b94 100644 --- a/tests/test_meta_tensor.py +++ b/tests/test_meta_tensor.py @@ -25,6 +25,7 @@ import torch.multiprocessing from parameterized import parameterized +from monai import config from monai.data import DataLoader, Dataset from monai.data.meta_obj import get_track_meta, set_track_meta from monai.data.meta_tensor import MetaTensor @@ -454,7 +455,7 @@ def test_transforms(self): data = _tr(data) is_meta = isinstance(_tr, (ToMetaTensord, BorderPadd, DivisiblePadd)) if is_meta: - self.assertEqual(len(data), 2) # im, im_transforms, compatibility + self.assertEqual(len(data), 1 if not config.USE_META_DICT else 2) # im, im_transforms, compatibility self.assertIsInstance(data[key], MetaTensor) n_applied = len(data[key].applied_operations) else: diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py index 302c17abed..19972fc069 100644 --- a/tests/test_to_from_meta_tensord.py +++ b/tests/test_to_from_meta_tensord.py @@ -37,14 +37,14 @@ def rand_string(min_len=5, max_len=10): return "".join(random.choice(chars) for _ in range(str_size)) -@unittest.skipIf(not config.USE_METATENSOR, "skipping not latest") +@unittest.skipIf(config.USE_META_DICT, "skipping not metatensor") class TestToFromMetaTensord(unittest.TestCase): def setUp(self): - self.flag = config.USE_METATENSOR - config.USE_METATENSOR = True + self.flag = config.USE_META_DICT + config.USE_META_DICT = False def tearDown(self): - config.USE_METATENSOR = self.flag + config.USE_META_DICT = self.flag @staticmethod def get_im(shape=None, dtype=None, device=None): @@ -148,7 +148,7 @@ def test_from_to_meta_tensord(self, device, dtype): # TO -> Forward t_to_meta = ToMetaTensord(["m1", "m2"]) d_dict_meta = t_to_meta(d_dict) - self.assertEqual(sorted(d_dict_meta.keys()), ["m1", "m2", "m3"], f"flag: {config.USE_METATENSOR}") + self.assertEqual(sorted(d_dict_meta.keys()), ["m1", "m2", "m3"], f"flag: {config.USE_META_DICT}") self.check(d_dict_meta["m3"], m3, ids=True) # unchanged (except deep copy in inverse) self.check(d_dict_meta["m1"], m1, ids=False) meta_out = {k: v for k, v in d_dict_meta["m1"].meta.items() if k != "affine"} From 4c014732385cfc1fda5fa2210375ecd5e83b28d3 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Thu, 7 Jul 2022 23:31:24 +0100 Subject: [PATCH 10/18] update baesd on comments, refactoring, more tests Signed-off-by: Wenqi Li --- monai/data/utils.py | 9 ++++-- monai/transforms/__init__.py | 2 ++ monai/transforms/inverse.py | 36 ++++++--------------- monai/transforms/meta_utility/dictionary.py | 5 ++- monai/transforms/post/dictionary.py | 11 ++++--- monai/transforms/spatial/dictionary.py | 26 +++++++-------- monai/transforms/transform.py | 28 ++++++---------- monai/transforms/utils.py | 24 ++++++++++++++ tests/test_flipd.py | 7 ++++ tests/test_metatensor_integration.py | 8 +++-- tests/test_one_of.py | 8 ++--- tests/test_to_from_meta_tensord.py | 8 ----- tests/utils.py | 7 ++-- 13 files changed, 94 insertions(+), 85 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index 578f2e7a0f..9be8595919 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -27,6 +27,7 @@ import torch from torch.utils.data._utils.collate import default_collate +from monai import config from monai.config.type_definitions import NdarrayOrTensor, NdarrayTensor, PathLike from monai.data.meta_obj import MetaObj from monai.networks.layers.simplelayers import GaussianFilter @@ -439,7 +440,8 @@ def list_data_collate(batch: Sequence): data = [i for k in batch for i in k] if isinstance(elem, list) else batch key = None try: - data = pickle_operations(data) # bc 0.9.0 + if config.USE_META_DICT: + data = pickle_operations(data) # bc 0.9.0 if isinstance(elem, Mapping): ret = {} for k in elem: @@ -590,7 +592,10 @@ def decollate_batch(batch, detach: bool = True, pad=True, fill_value=None): deco[k] = [deepcopy(deco[k]) for _ in range(b)] if isinstance(deco, Mapping): _gen = zip_longest(*deco.values(), fillvalue=fill_value) if pad else zip(*deco.values()) - return pickle_operations([dict(zip(deco, item)) for item in _gen]) # bc 0.9.0 + ret = [dict(zip(deco, item)) for item in _gen] + if not config.USE_META_DICT: + return ret + return pickle_operations(ret, is_encode=False) # bc 0.9.0 if isinstance(deco, Iterable): _gen = zip_longest(*deco, fillvalue=fill_value) if pad else zip(*deco) return [list(item) for item in _gen] diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 0cc7fb3e67..718b4485e6 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -574,6 +574,8 @@ from .utils import ( Fourier, allow_missing_keys_mode, + attach_post_hook, + attach_pre_hook, compute_divisible_spatial_size, convert_applied_interp_mode, convert_pad_mode, diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 987180e655..b447bdf49e 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -12,13 +12,11 @@ import os import warnings from contextlib import contextmanager -from functools import wraps from typing import Any, Hashable, Mapping, Optional, Tuple import torch -import monai -from monai import config +from monai import transforms from monai.data.meta_tensor import MetaTensor from monai.transforms.transform import Transform from monai.utils.enums import PostFix, TraceKeys @@ -216,18 +214,7 @@ def trace_transform(self, to_trace: bool): self.tracing = prev -def attach_pre_hook(func, hook): - """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" - @wraps(func) - def wrapper(*args, **kwargs): - if len(args) < 2: - return func(*args, **kwargs) - inst, data_dict = args - data_dict = hook(inst, data_dict) - return func(inst, data_dict) - - return wrapper class InvertibleTransform(TraceableTransform): @@ -267,32 +254,27 @@ class InvertibleTransform(TraceableTransform): """ - def __new__(cls, *args, **kwargs): - if config.USE_META_DICT and issubclass(cls, monai.transforms.MapTransform): - # only update MapTransform, MapTransforms may call array transform's inverse - cls.inverse = attach_pre_hook(cls.inverse, InvertibleTransform.comp_update) - return TraceableTransform.__new__(cls) - - def comp_update(self, data): + def inverse_update(self, data): """ This function is to be called before every `self.inverse(data)`, update each MetaTensor `data[key]` using `data[key_transforms]` and `data[key_meta_dict]`, for MetaTensor backward compatibility 0.9.0. """ - if not isinstance(data, dict) or not isinstance(self, monai.transforms.MapTransform): + if not isinstance(data, dict) or not isinstance(self, transforms.MapTransform): return data d = dict(data) for k in self.key_iterator(data): - transform_key = monai.transforms.TraceableTransform.trace_key(k) - if transform_key not in data: + transform_key = transforms.TraceableTransform.trace_key(k) + if transform_key not in data or not data[transform_key]: continue if not isinstance(data[k], MetaTensor): d[k] = MetaTensor(data[k]) - d[k].applied_operations = data[transform_key] + if not d[k].applied_operations: + d[k].applied_operations = d[transform_key] + d[transform_key] = d[k].applied_operations meta_dict_key = PostFix.meta(k) if meta_dict_key in data: - d[k].meta.update(data[meta_dict_key]) - d.pop(transform_key) + d[meta_dict_key].update(d[k].meta) return d def inverse(self, data: Any) -> Any: diff --git a/monai/transforms/meta_utility/dictionary.py b/monai/transforms/meta_utility/dictionary.py index 1dcdf3483c..5430dd57e2 100644 --- a/monai/transforms/meta_utility/dictionary.py +++ b/monai/transforms/meta_utility/dictionary.py @@ -15,7 +15,6 @@ Class names are ended with 'd' to denote dictionary-based transforms. """ -from copy import deepcopy from typing import Dict, Hashable, Mapping from monai.config.type_definitions import NdarrayOrTensor @@ -53,7 +52,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): # check transform _ = self.get_most_recent_transform(d, key) @@ -90,7 +89,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): # check transform _ = self.get_most_recent_transform(d, key) diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 3704d92ec3..d25f75dd27 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -21,6 +21,7 @@ import torch +from monai import config from monai.config.type_definitions import KeysCollection, NdarrayOrTensor, PathLike from monai.data.csv_saver import CSVSaver from monai.data.meta_tensor import MetaTensor @@ -39,8 +40,7 @@ from monai.transforms.transform import MapTransform from monai.transforms.utility.array import ToTensor from monai.transforms.utils import allow_missing_keys_mode, convert_applied_interp_mode -from monai.utils import convert_to_tensor, deprecated_arg, ensure_tuple, ensure_tuple_rep -from monai.utils.enums import PostFix +from monai.utils import PostFix, convert_to_tensor, deprecated_arg, ensure_tuple, ensure_tuple_rep __all__ = [ "ActivationsD", @@ -542,8 +542,8 @@ class Invertd(MapTransform): def __init__( self, - keys: KeysCollection, - transform: InvertibleTransform, + keys: KeysCollection = "", + transform: Optional[InvertibleTransform] = None, orig_keys: Optional[KeysCollection] = None, meta_keys: Optional[KeysCollection] = None, orig_meta_keys: Optional[KeysCollection] = None, @@ -656,6 +656,9 @@ def __call__(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: # construct the input dict data input_dict = {orig_key: inputs} + if config.USE_META_DICT: + input_dict[InvertibleTransform.trace_key(orig_key)] = transform_info + input_dict[PostFix.meta(orig_key)] = meta_info with allow_missing_keys_mode(self.transform): # type: ignore inverted = self.transform.inverse(input_dict) diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index c809d38ba0..256e006783 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -221,7 +221,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.sp_transform.inverse(d[key]) return d @@ -289,7 +289,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.resampler.inverse(d[key]) return d @@ -384,7 +384,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.spacing_transform.inverse(d[key]) return d @@ -440,7 +440,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.ornt_transform.inverse(d[key]) return d @@ -473,7 +473,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.rotator.inverse(d[key]) return d @@ -595,7 +595,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.resizer.inverse(d[key]) return d @@ -696,7 +696,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.affine.inverse(d[key]) return d @@ -1143,7 +1143,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.flipper.inverse(d[key]) return d @@ -1197,7 +1197,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): xform = self.pop_transform(d[key]) if not xform[TraceKeys.DO_TRANSFORM]: @@ -1325,7 +1325,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.rotator.inverse(d[key]) return d @@ -1423,7 +1423,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): xform = self.pop_transform(d[key]) if xform[TraceKeys.DO_TRANSFORM]: @@ -1491,7 +1491,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): d[key] = self.zoomer.inverse(d[key]) return d @@ -1591,7 +1591,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): xform = self.pop_transform(d[key]) if xform[TraceKeys.DO_TRANSFORM]: diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index e87b1a8e53..5de2ef00ae 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -14,7 +14,6 @@ import logging from abc import ABC, abstractmethod -from functools import wraps from typing import Any, Callable, Dict, Generator, Hashable, Iterable, List, Mapping, Optional, Tuple, TypeVar, Union import numpy as np @@ -288,17 +287,6 @@ def randomize(self, data: Any) -> None: self._do_transform = self.R.rand() < self.prob -def attach_post_hook(func, hook): - """adds `hook` after `func` calls using `func`'s return value""" - - @wraps(func) - def wrapper(*args, **kwargs): - out = func(*args, **kwargs) - return hook(args[0], out) - - return wrapper - - class MapTransform(Transform): """ A subclass of :py:class:`monai.transforms.Transform` with an assumption @@ -324,9 +312,11 @@ def __call__(self, data): """ - def __new__(cls, keys: KeysCollection = None, allow_missing_keys: bool = False, *args, **kwargs): + def __new__(cls, *args, **kwargs): if config.USE_META_DICT: - cls.__call__ = attach_post_hook(cls.__call__, MapTransform.comp_update) # type: ignore + cls.__call__ = transforms.attach_post_hook(cls.__call__, MapTransform.call_update) # type: ignore + if issubclass(cls, transforms.InvertibleTransform): + cls.inverse = transforms.attach_pre_hook(cls.inverse, transforms.InvertibleTransform.inverse_update) return Transform.__new__(cls) def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: @@ -338,7 +328,7 @@ def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> No if not isinstance(key, Hashable): raise TypeError(f"keys must be one of (Hashable, Iterable[Hashable]) but is {type(keys).__name__}.") - def comp_update(self, data): + def call_update(self, data): """ This function is to be called after every `self.__call__(data)`, update `data[key_transforms]` and `data[key_meta_dict]` using the content from MetaTensor `data[key]`, @@ -351,11 +341,11 @@ def comp_update(self, data): if not isinstance(data[k], MetaTensor): continue meta_dict_key = PostFix.meta(k) - if meta_dict_key in d: - d[meta_dict_key].update(data[k].meta) + if meta_dict_key not in d: + d[meta_dict_key] = data[k].meta + d[meta_dict_key].update(data[k].meta) if data[k].applied_operations: - transform_key = transforms.TraceableTransform.trace_key(k) - d[transform_key] = data[k].applied_operations + d[transforms.TraceableTransform.trace_key(k)] = data[k].applied_operations return d @abstractmethod diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index ccc467bda4..7c2d61a990 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -13,6 +13,7 @@ import random import warnings from contextlib import contextmanager +from functools import wraps from inspect import getmembers, isclass from typing import Any, Callable, Hashable, Iterable, List, Mapping, Optional, Sequence, Set, Tuple, Union @@ -106,6 +107,8 @@ "convert_to_contiguous", "get_unique_labels", "scale_affine", + "attach_pre_hook", + "attach_post_hook", ] @@ -1599,6 +1602,27 @@ def scale_affine(affine, spatial_size, new_spatial_size, centered: bool = True): scale[:r, -1] = (np.diag(scale)[:r] - 1) / 2 # type: ignore return affine @ convert_to_dst_type(scale, affine)[0] +def attach_pre_hook(func, hook): + """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" + @wraps(func) + def wrapper(*args, **kwargs): + if len(args) < 2: + return func(*args, **kwargs) + inst, data_dict = args + data_dict = hook(inst, data_dict) + return func(inst, data_dict) + + return wrapper + +def attach_post_hook(func, hook): + """adds `hook` after `func` calls using `func`'s return value""" + + @wraps(func) + def wrapper(*args, **kwargs): + out = func(*args, **kwargs) + return hook(args[0], out) + + return wrapper if __name__ == "__main__": print_transform_backends() diff --git a/tests/test_flipd.py b/tests/test_flipd.py index c97674b83b..b0e656b83f 100644 --- a/tests/test_flipd.py +++ b/tests/test_flipd.py @@ -15,6 +15,7 @@ import torch from parameterized import parameterized +from monai import config from monai.data.meta_obj import set_track_meta from monai.data.meta_tensor import MetaTensor from monai.transforms import Flipd @@ -63,6 +64,12 @@ def test_torch(self, init_param, img: torch.Tensor, track_meta: bool, device): with self.assertRaisesRegex(ValueError, "MetaTensor"): xform.inverse(res) + @unittest.skipIf(not config.USE_META_DICT, "not using meta dict") + def test_meta_dict(self): + xform = Flipd("image", [0, 1]) + res = xform({"image": torch.zeros(1, 3, 4)}) + self.assertTrue(res["image"].applied_operations == res["image_transforms"]) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_metatensor_integration.py b/tests/test_metatensor_integration.py index d6908815ee..3af5f56c4a 100644 --- a/tests/test_metatensor_integration.py +++ b/tests/test_metatensor_integration.py @@ -16,6 +16,7 @@ import numpy as np from parameterized import parameterized +from monai import config as monai_config from monai.bundle import ConfigParser from monai.data import CacheDataset, DataLoader, MetaTensor, decollate_batch from monai.data.utils import TraceKeys @@ -54,7 +55,7 @@ def test_transforms(self, case_id): config = ConfigParser() config.read_config(TEST_CASES) config["input_keys"] = keys - test_case = config.get_parsed_content(id=case_id, instantiate=True) # transform instance + test_case = config.get_parsed_content(id=case_id, instantiate=True, lazy=False) # transform instance dataset = CacheDataset(self.files, transform=test_case) loader = DataLoader(dataset, batch_size=3, shuffle=True) @@ -65,7 +66,10 @@ def test_transforms(self, case_id): # test forward patches loaded = out[0] - self.assertEqual(len(loaded), len(keys)) + if not monai_config.USE_META_DICT: + self.assertEqual(len(loaded), len(keys)) + else: + self.assertNotEqual(len(loaded), len(keys)) img, seg = loaded[keys[0]], loaded[keys[1]] expected = config.get_parsed_content(id=f"{case_id}_answer", instantiate=True) # expected results self.assertEqual(expected["load_shape"], list(x[keys[0]].shape)) diff --git a/tests/test_one_of.py b/tests/test_one_of.py index cdd59e07a9..29d13d7d0c 100644 --- a/tests/test_one_of.py +++ b/tests/test_one_of.py @@ -165,10 +165,10 @@ def test_inverse(self, transform, invertible): if invertible: for k in KEYS: - # # check transform was removed, compatibility 0.9.0 removed '_transforms' - # self.assertTrue( - # len(fwd_inv_data[TraceableTransform.trace_key(k)]) < len(fwd_data[TraceableTransform.trace_key(k)]) - # ) + # check transform was removed + self.assertTrue( + len(fwd_inv_data[TraceableTransform.trace_key(k)]) < len(fwd_data[TraceableTransform.trace_key(k)]) + ) # check data is same as original (and different from forward) self.assertEqual(fwd_inv_data[k], data[k]) self.assertNotEqual(fwd_inv_data[k], fwd_data[k]) diff --git a/tests/test_to_from_meta_tensord.py b/tests/test_to_from_meta_tensord.py index 19972fc069..806c93e254 100644 --- a/tests/test_to_from_meta_tensord.py +++ b/tests/test_to_from_meta_tensord.py @@ -39,13 +39,6 @@ def rand_string(min_len=5, max_len=10): @unittest.skipIf(config.USE_META_DICT, "skipping not metatensor") class TestToFromMetaTensord(unittest.TestCase): - def setUp(self): - self.flag = config.USE_META_DICT - config.USE_META_DICT = False - - def tearDown(self): - config.USE_META_DICT = self.flag - @staticmethod def get_im(shape=None, dtype=None, device=None): if shape is None: @@ -138,7 +131,6 @@ def test_from_to_meta_tensord(self, device, dtype): # FROM -> inverse d_meta_dict_meta = t_from_meta.inverse(d_dict) self.assertEqual(sorted(d_meta_dict_meta.keys()), ["m1", "m2", "m3"]) - self.check(d_meta_dict_meta["m3"], m3, ids=False) # unchanged (except deep copy in inverse) self.check(d_meta_dict_meta["m1"], m1, ids=False) meta_out = {k: v for k, v in d_meta_dict_meta["m1"].meta.items() if k != "affine"} aff_out = d_meta_dict_meta["m1"].affine diff --git a/tests/utils.py b/tests/utils.py index e3e77a9c32..2a061b3ee4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -722,13 +722,14 @@ def test_local_inversion(invertible_xform, to_invert, im, dict_key=None): im_item = im if dict_key is None else im[dict_key] if not isinstance(im_item, MetaTensor): return + im_ref = copy.deepcopy(im) im_inv = invertible_xform.inverse(to_invert) if dict_key: im_inv = im_inv[dict_key] - im = im[dict_key] + im_ref = im_ref[dict_key] np.testing.assert_array_equal(im_inv.applied_operations, []) - assert_allclose(im_inv.shape, im.shape) - assert_allclose(im_inv.affine, im.affine, atol=1e-3, rtol=1e-3) + assert_allclose(im_inv.shape, im_ref.shape) + assert_allclose(im_inv.affine, im_ref.affine, atol=1e-3, rtol=1e-3) TEST_TORCH_TENSORS: Tuple[Callable] = (torch.as_tensor,) From 60948524fe1b3a13c525a3fb7e6dd528314e7c42 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 8 Jul 2022 11:11:40 +0100 Subject: [PATCH 11/18] debug typing Signed-off-by: Wenqi Li --- monai/transforms/inverse.py | 3 --- monai/transforms/utils.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index b447bdf49e..4ce2006c69 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -214,9 +214,6 @@ def trace_transform(self, to_trace: bool): self.tracing = prev - - - class InvertibleTransform(TraceableTransform): """Classes for invertible transforms. diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index 7c2d61a990..e590d62a35 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -1602,6 +1602,7 @@ def scale_affine(affine, spatial_size, new_spatial_size, centered: bool = True): scale[:r, -1] = (np.diag(scale)[:r] - 1) / 2 # type: ignore return affine @ convert_to_dst_type(scale, affine)[0] + def attach_pre_hook(func, hook): """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" @@ -1615,6 +1616,7 @@ def wrapper(*args, **kwargs): return wrapper + def attach_post_hook(func, hook): """adds `hook` after `func` calls using `func`'s return value""" @@ -1624,5 +1626,7 @@ def wrapper(*args, **kwargs): return hook(args[0], out) return wrapper + + if __name__ == "__main__": print_transform_backends() From 5d6844d1465f2dd0ede9d2a4fd0450d4a948d331 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 8 Jul 2022 11:23:51 +0100 Subject: [PATCH 12/18] debug typing Signed-off-by: Wenqi Li --- monai/transforms/utils.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index e590d62a35..5d7ad72864 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -1607,10 +1607,7 @@ def attach_pre_hook(func, hook): """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" @wraps(func) - def wrapper(*args, **kwargs): - if len(args) < 2: - return func(*args, **kwargs) - inst, data_dict = args + def wrapper(inst, data_dict): data_dict = hook(inst, data_dict) return func(inst, data_dict) From 3d81031305d26634ce18908ec1d0b135bfdb01ca Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 8 Jul 2022 11:50:12 +0100 Subject: [PATCH 13/18] debug typing Signed-off-by: Wenqi Li --- monai/transforms/transform.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 5de2ef00ae..8d055c85b5 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -315,7 +315,7 @@ def __call__(self, data): def __new__(cls, *args, **kwargs): if config.USE_META_DICT: cls.__call__ = transforms.attach_post_hook(cls.__call__, MapTransform.call_update) # type: ignore - if issubclass(cls, transforms.InvertibleTransform): + if hasattr(cls, "inverse"): cls.inverse = transforms.attach_pre_hook(cls.inverse, transforms.InvertibleTransform.inverse_update) return Transform.__new__(cls) From 6851913d51139e71f694c990fca26f6578363d06 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 8 Jul 2022 14:28:40 +0100 Subject: [PATCH 14/18] simplify, more tests Signed-off-by: Wenqi Li --- monai/data/utils.py | 4 +-- monai/transforms/__init__.py | 1 + monai/transforms/inverse.py | 11 ++----- monai/transforms/post/dictionary.py | 7 +++-- monai/transforms/transform.py | 9 ++---- monai/transforms/utils.py | 49 +++++++++++++++++++++++++---- 6 files changed, 54 insertions(+), 27 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index 9be8595919..0ec7536d19 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -395,14 +395,14 @@ def dev_collate(batch, level: int = 1, logger_name: str = "dev_collate"): def pickle_operations(data, key=TraceKeys.KEY_SUFFIX, is_encode: bool = True): """applied_operations are dictionaries with varying sizes, converting them to bytes so that we can (de-)collate.""" if isinstance(data, Mapping): - data, has_items = dict(data), False + data = dict(data) for k in data: if f"{k}".endswith(TraceKeys.KEY_SUFFIX): if is_encode and not isinstance(data[k], bytes): data[k] = pickle.dumps(data[k], 0) if not is_encode and isinstance(data[k], bytes): data[k] = pickle.loads(data[k]) - return data if has_items else {k: pickle_operations(v, key=key, is_encode=is_encode) for k, v in data.items()} + return {k: pickle_operations(v, key=key, is_encode=is_encode) for k, v in data.items()} elif isinstance(data, (list, tuple)): return [pickle_operations(item, key=key, is_encode=is_encode) for item in data] return data diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 718b4485e6..89d5af6f9e 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -609,6 +609,7 @@ rescale_array_int_max, rescale_instance_array, resize_center, + sync_meta_info, weighted_patch_samples, zero_margins, ) diff --git a/monai/transforms/inverse.py b/monai/transforms/inverse.py index 4ce2006c69..55bef0e549 100644 --- a/monai/transforms/inverse.py +++ b/monai/transforms/inverse.py @@ -19,7 +19,7 @@ from monai import transforms from monai.data.meta_tensor import MetaTensor from monai.transforms.transform import Transform -from monai.utils.enums import PostFix, TraceKeys +from monai.utils.enums import TraceKeys __all__ = ["TraceableTransform", "InvertibleTransform"] @@ -264,14 +264,7 @@ def inverse_update(self, data): transform_key = transforms.TraceableTransform.trace_key(k) if transform_key not in data or not data[transform_key]: continue - if not isinstance(data[k], MetaTensor): - d[k] = MetaTensor(data[k]) - if not d[k].applied_operations: - d[k].applied_operations = d[transform_key] - d[transform_key] = d[k].applied_operations - meta_dict_key = PostFix.meta(k) - if meta_dict_key in data: - d[meta_dict_key].update(d[k].meta) + d = transforms.sync_meta_info(k, data, t=False) return d def inverse(self, data: Any) -> Any: diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index d25f75dd27..b3e7729f3d 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -634,12 +634,13 @@ def __call__(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: warnings.warn(f"transform info of `{orig_key}` is not available or no InvertibleTransform applied.") continue + orig_meta_key = orig_meta_key or f"{orig_key}_{meta_key_postfix}" if orig_key in d and isinstance(d[orig_key], MetaTensor): transform_info = d[orig_key].applied_operations meta_info = d[orig_key].meta else: transform_info = d[InvertibleTransform.trace_key(orig_key)] - meta_info = d.get(orig_meta_key or f"{orig_key}_{meta_key_postfix}", {}) + meta_info = d.get(orig_meta_key, {}) if nearest_interp: transform_info = convert_applied_interp_mode( trans_info=deepcopy(transform_info), mode="nearest", align_corners=None @@ -659,7 +660,6 @@ def __call__(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: if config.USE_META_DICT: input_dict[InvertibleTransform.trace_key(orig_key)] = transform_info input_dict[PostFix.meta(orig_key)] = meta_info - with allow_missing_keys_mode(self.transform): # type: ignore inverted = self.transform.inverse(input_dict) @@ -668,8 +668,9 @@ def __call__(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: inverted_data = self._totensor(inverted[orig_key]) else: inverted_data = inverted[orig_key] + if config.USE_META_DICT and InvertibleTransform.trace_key(orig_key) in d: + d[InvertibleTransform.trace_key(orig_key)] = inverted_data.applied_operations d[key] = post_func(inverted_data.to(device)) - # save the inverted meta dict if orig_meta_key in d: meta_key = meta_key or f"{key}_{meta_key_postfix}" diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 8d055c85b5..cde080ac7e 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -23,7 +23,7 @@ from monai.config import KeysCollection from monai.data.meta_tensor import MetaTensor from monai.utils import MAX_SEED, ensure_tuple, first -from monai.utils.enums import PostFix, TransformBackends +from monai.utils.enums import TransformBackends __all__ = ["ThreadUnsafe", "apply_transform", "Randomizable", "RandomizableTransform", "Transform", "MapTransform"] @@ -340,12 +340,7 @@ def call_update(self, data): for k in data: if not isinstance(data[k], MetaTensor): continue - meta_dict_key = PostFix.meta(k) - if meta_dict_key not in d: - d[meta_dict_key] = data[k].meta - d[meta_dict_key].update(data[k].meta) - if data[k].applied_operations: - d[transforms.TraceableTransform.trace_key(k)] = data[k].applied_operations + d = transforms.sync_meta_info(k, data, t=not isinstance(self, transforms.InvertD)) return d @abstractmethod diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index 5d7ad72864..dcbc90169b 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -43,6 +43,7 @@ GridSampleMode, InterpolateMode, NumpyPadMode, + PostFix, PytorchPadMode, TraceKeys, deprecated_arg, @@ -109,6 +110,7 @@ "scale_affine", "attach_pre_hook", "attach_post_hook", + "sync_meta_info", ] @@ -1607,9 +1609,9 @@ def attach_pre_hook(func, hook): """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" @wraps(func) - def wrapper(inst, data_dict): - data_dict = hook(inst, data_dict) - return func(inst, data_dict) + def wrapper(inst, data): + data = hook(inst, data) + return func(inst, data) return wrapper @@ -1618,12 +1620,47 @@ def attach_post_hook(func, hook): """adds `hook` after `func` calls using `func`'s return value""" @wraps(func) - def wrapper(*args, **kwargs): - out = func(*args, **kwargs) - return hook(args[0], out) + def wrapper(inst, data): + out = func(inst, data) + return hook(inst, out) return wrapper +def sync_meta_info(key, data_dict, t: bool = True): + """given the key, sync up between metatensor `data_dict[key]` and meta_dict `data_dict[key_transforms/meta_dict]`. + t=True: more applied_operations is the output, t=False: less applied_operations is the output.""" + if not isinstance(data_dict, Mapping): + return data_dict + d = dict(data_dict) + + # update meta dicts + meta_dict_key = PostFix.meta(key) + if meta_dict_key not in d: + d[meta_dict_key] = monai.data.MetaTensor.get_default_meta() + if not isinstance(d[key], monai.data.MetaTensor): + d[key] = monai.data.MetaTensor(data_dict[key]) + d[key].meta = d[meta_dict_key] + d[meta_dict_key].update(d[key].meta) # prefer metatensor's data + + # update xform info + xform_key = monai.transforms.TraceableTransform.trace_key(key) + if xform_key not in d: + d[xform_key] = monai.data.MetaTensor.get_default_applied_operations() + from_meta, from_dict = d[key].applied_operations, d[xform_key] + if not from_meta: # avoid [] + d[key].applied_operations = d[xform_key] = from_dict + return d + if not from_dict: + d[key].applied_operations = d[xform_key] = from_meta + return d + if t: # larger transform info stack is used as the result + ref = from_meta if len(from_meta) > len(from_dict) else from_dict + else: # smaller transform info stack is used as the result + ref = from_dict if len(from_meta) > len(from_dict) else from_meta + d[key].applied_operations = d[xform_key] = ref + return d + + if __name__ == "__main__": print_transform_backends() From e6d1b66227df244e21457de0b288dc9e77159cbf Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Fri, 8 Jul 2022 15:33:52 +0100 Subject: [PATCH 15/18] multisamples cropping Signed-off-by: Wenqi Li --- monai/data/utils.py | 5 ++++- monai/transforms/transform.py | 20 +++++++++++++------- monai/transforms/utils.py | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index 0ec7536d19..80591b2008 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -598,7 +598,10 @@ def decollate_batch(batch, detach: bool = True, pad=True, fill_value=None): return pickle_operations(ret, is_encode=False) # bc 0.9.0 if isinstance(deco, Iterable): _gen = zip_longest(*deco, fillvalue=fill_value) if pad else zip(*deco) - return [list(item) for item in _gen] + ret_list = [list(item) for item in _gen] + if not config.USE_META_DICT: + return ret_list + return pickle_operations(ret_list, is_encode=False) # bc 0.9.0 raise NotImplementedError(f"Unable to de-collate: {batch}, type: {type(batch)}.") diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index cde080ac7e..6512832988 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -334,14 +334,20 @@ def call_update(self, data): update `data[key_transforms]` and `data[key_meta_dict]` using the content from MetaTensor `data[key]`, for MetaTensor backward compatibility 0.9.0. """ - if not isinstance(data, Mapping): + if not isinstance(data, (list, tuple, Mapping)): return data - d = dict(data) - for k in data: - if not isinstance(data[k], MetaTensor): - continue - d = transforms.sync_meta_info(k, data, t=not isinstance(self, transforms.InvertD)) - return d + is_dict = False + if isinstance(data, Mapping): + data, is_dict = [data], True + if not data or not isinstance(data[0], Mapping): + return data[0] if is_dict else data + list_d = [dict(x) for x in data] # list of dict for crop samples + for idx, dict_i in enumerate(list_d): + for k in dict_i: + if not isinstance(dict_i[k], MetaTensor): + continue + list_d[idx] = transforms.sync_meta_info(k, dict_i, t=not isinstance(self, transforms.InvertD)) + return list_d[0] if is_dict else list_d @abstractmethod def __call__(self, data): diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index dcbc90169b..eb46098d23 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -1629,7 +1629,7 @@ def wrapper(inst, data): def sync_meta_info(key, data_dict, t: bool = True): """given the key, sync up between metatensor `data_dict[key]` and meta_dict `data_dict[key_transforms/meta_dict]`. - t=True: more applied_operations is the output, t=False: less applied_operations is the output.""" + t=True: the one with more applied_operations in metatensor vs meta_dict is the output, False: less is the output.""" if not isinstance(data_dict, Mapping): return data_dict d = dict(data_dict) From f613e3757ceaa93bc949d74a597d768b1efa67ee Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Sat, 9 Jul 2022 13:33:07 +0100 Subject: [PATCH 16/18] simplified utilities, docstrings Signed-off-by: Wenqi Li --- monai/data/utils.py | 11 ++++++++-- monai/transforms/__init__.py | 3 +-- monai/transforms/transform.py | 6 ++++-- monai/transforms/utils.py | 38 +++++++++++++++++------------------ 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/monai/data/utils.py b/monai/data/utils.py index 80591b2008..320c315fb7 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -393,11 +393,18 @@ def dev_collate(batch, level: int = 1, logger_name: str = "dev_collate"): def pickle_operations(data, key=TraceKeys.KEY_SUFFIX, is_encode: bool = True): - """applied_operations are dictionaries with varying sizes, converting them to bytes so that we can (de-)collate.""" + """ + Applied_operations are dictionaries with varying sizes, this method converts them to bytes so that we can (de-)collate. + + Args: + data: a list or dictionary with substructures to be pickled/unpickled. + key: the key suffix indicating the target substructures, defaults to "_transforms". + is_encode: whether it's encoding using pickle.dumps (True) or decoding using pickle.loads (False). + """ if isinstance(data, Mapping): data = dict(data) for k in data: - if f"{k}".endswith(TraceKeys.KEY_SUFFIX): + if f"{k}".endswith(key): if is_encode and not isinstance(data[k], bytes): data[k] = pickle.dumps(data[k], 0) if not is_encode and isinstance(data[k], bytes): diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 89d5af6f9e..905a8f4494 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -574,8 +574,7 @@ from .utils import ( Fourier, allow_missing_keys_mode, - attach_post_hook, - attach_pre_hook, + attach_hook, compute_divisible_spatial_size, convert_applied_interp_mode, convert_pad_mode, diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 31ad075ae0..730cb634c0 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -314,10 +314,12 @@ def __call__(self, data): def __new__(cls, *args, **kwargs): if config.USE_META_DICT: - cls.__call__ = transforms.attach_post_hook(cls.__call__, MapTransform.call_update) + # call_update after MapTransform.__call__ + cls.__call__ = transforms.attach_hook(cls.__call__, MapTransform.call_update, "post") if hasattr(cls, "inverse"): - cls.inverse = transforms.attach_pre_hook(cls.inverse, transforms.InvertibleTransform.inverse_update) + # inverse_update before InvertibleTransform.inverse + cls.inverse = transforms.attach_hook(cls.inverse, transforms.InvertibleTransform.inverse_update) return Transform.__new__(cls) def __init__(self, keys: KeysCollection, allow_missing_keys: bool = False) -> None: diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index eb46098d23..48af2f9f23 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -108,8 +108,7 @@ "convert_to_contiguous", "get_unique_labels", "scale_affine", - "attach_pre_hook", - "attach_post_hook", + "attach_hook", "sync_meta_info", ] @@ -1605,31 +1604,32 @@ def scale_affine(affine, spatial_size, new_spatial_size, centered: bool = True): return affine @ convert_to_dst_type(scale, affine)[0] -def attach_pre_hook(func, hook): - """adds `hook` before `func` calls, `func` will use the returned data dict from `hook` as the input.""" - - @wraps(func) - def wrapper(inst, data): - data = hook(inst, data) - return func(inst, data) - - return wrapper - - -def attach_post_hook(func, hook): - """adds `hook` after `func` calls using `func`'s return value""" +def attach_hook(func, hook, mode="pre"): + """ + Adds `hook` before or after a `func` call. If mode is "pre", the wrapper will call hook then func. + If the mode is "post", the wrapper will call func then hook. + """ + supported = {"pre", "post"} + if look_up_option(mode, supported) == "pre": + _hook = hook + _func = func + else: + _hook = func + _func = hook @wraps(func) def wrapper(inst, data): - out = func(inst, data) - return hook(inst, out) + data = _hook(inst, data) + return _func(inst, data) return wrapper def sync_meta_info(key, data_dict, t: bool = True): - """given the key, sync up between metatensor `data_dict[key]` and meta_dict `data_dict[key_transforms/meta_dict]`. - t=True: the one with more applied_operations in metatensor vs meta_dict is the output, False: less is the output.""" + """ + Given the key, sync up between metatensor `data_dict[key]` and meta_dict `data_dict[key_transforms/meta_dict]`. + t=True: the one with more applied_operations in metatensor vs meta_dict is the output, False: less is the output. + """ if not isinstance(data_dict, Mapping): return data_dict d = dict(data_dict) From bc1e4c8afb734572aa2617fc1881d43ad14501b3 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Sun, 10 Jul 2022 12:10:23 +0100 Subject: [PATCH 17/18] update based on comments Signed-off-by: Wenqi Li --- monai/apps/detection/transforms/dictionary.py | 16 ++-- monai/data/__init__.py | 1 + monai/data/utils.py | 8 +- monai/transforms/__init__.py | 1 + monai/transforms/post/array.py | 52 ++++++++++- monai/transforms/post/dictionary.py | 4 +- monai/transforms/utility/dictionary.py | 8 +- monai/transforms/utils.py | 6 +- tests/test_invert.py | 91 +++++++++++++++++++ 9 files changed, 166 insertions(+), 21 deletions(-) create mode 100644 tests/test_invert.py diff --git a/monai/apps/detection/transforms/dictionary.py b/monai/apps/detection/transforms/dictionary.py index cb08d0ed69..fc65954358 100644 --- a/monai/apps/detection/transforms/dictionary.py +++ b/monai/apps/detection/transforms/dictionary.py @@ -136,7 +136,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): tr = self.get_most_recent_transform(d, key) src_mode, dst_mode = tr[TraceKeys.EXTRA_INFO]["src"], tr[TraceKeys.EXTRA_INFO]["dst"] @@ -191,7 +191,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): tr = self.get_most_recent_transform(d, key) original_mode = tr[TraceKeys.EXTRA_INFO]["mode"] @@ -280,7 +280,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) affine = transform["extra_info"]["affine"] @@ -594,7 +594,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) @@ -675,7 +675,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) @@ -752,7 +752,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) @@ -1274,7 +1274,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Mapping[Hashable, t return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) @@ -1360,7 +1360,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Mapping[Hashable, t return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) if self._rand_k % 4 == 0: return d diff --git a/monai/data/__init__.py b/monai/data/__init__.py index b7a160b3de..e85806de05 100644 --- a/monai/data/__init__.py +++ b/monai/data/__init__.py @@ -71,6 +71,7 @@ from .thread_buffer import ThreadBuffer, ThreadDataLoader from .torchscript_utils import load_net_with_metadata, save_net_with_metadata from .utils import ( + PICKLE_KEY_SUFFIX, affine_to_spacing, compute_importance_map, compute_shape_offset, diff --git a/monai/data/utils.py b/monai/data/utils.py index 320c315fb7..32a9980799 100644 --- a/monai/data/utils.py +++ b/monai/data/utils.py @@ -91,6 +91,7 @@ "remove_keys", "remove_extra_metadata", "get_extra_metadata_keys", + "PICKLE_KEY_SUFFIX", ] # module to be used by `torch.save` @@ -392,13 +393,16 @@ def dev_collate(batch, level: int = 1, logger_name: str = "dev_collate"): return -def pickle_operations(data, key=TraceKeys.KEY_SUFFIX, is_encode: bool = True): +PICKLE_KEY_SUFFIX = TraceKeys.KEY_SUFFIX + + +def pickle_operations(data, key=PICKLE_KEY_SUFFIX, is_encode: bool = True): """ Applied_operations are dictionaries with varying sizes, this method converts them to bytes so that we can (de-)collate. Args: data: a list or dictionary with substructures to be pickled/unpickled. - key: the key suffix indicating the target substructures, defaults to "_transforms". + key: the key suffix for the target substructures, defaults to "_transforms" (`data.utils.PICKLE_KEY_SUFFIX`). is_encode: whether it's encoding using pickle.dumps (True) or decoding using pickle.loads (False). """ if isinstance(data, Mapping): diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 905a8f4494..713f848f86 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -257,6 +257,7 @@ Activations, AsDiscrete, FillHoles, + Invert, KeepLargestConnectedComponent, LabelFilter, LabelToContour, diff --git a/monai/transforms/post/array.py b/monai/transforms/post/array.py index 29aa39d7ac..aab4aaa12f 100644 --- a/monai/transforms/post/array.py +++ b/monai/transforms/post/array.py @@ -21,10 +21,17 @@ from monai.config.type_definitions import NdarrayOrTensor from monai.data.meta_obj import get_track_meta +from monai.data.meta_tensor import MetaTensor from monai.networks import one_hot from monai.networks.layers import GaussianFilter, apply_filter +from monai.transforms.inverse import InvertibleTransform from monai.transforms.transform import Transform -from monai.transforms.utils import fill_holes, get_largest_connected_component_mask, get_unique_labels +from monai.transforms.utils import ( + convert_applied_interp_mode, + fill_holes, + get_largest_connected_component_mask, + get_unique_labels, +) from monai.transforms.utils_pytorch_numpy_unification import unravel_index from monai.utils import ( TransformBackends, @@ -46,6 +53,7 @@ "MeanEnsemble", "ProbNMS", "VoteEnsemble", + "Invert", ] @@ -765,3 +773,45 @@ def __call__(self, prob_map: NdarrayOrTensor): prob_map[slices] = 0 return outputs + + +class Invert(Transform): + """ + Utility transform to automatically invert the previously applied transforms. + """ + + def __init__( + self, + transform: Optional[InvertibleTransform] = None, + nearest_interp: 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, + ) -> None: + """ + Args: + transform: the previously applied transform. + 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. + device: move the inverted results to a target device before `post_func`, default to "cpu". + post_func: postprocessing for the inverted MetaTensor, should be a callable function. + """ + if not isinstance(transform, InvertibleTransform): + raise ValueError("transform is not invertible, can't invert transform for the data.") + self.transform = transform + self.nearest_interp = nearest_interp + self.device = device + self.post_func = post_func + + def __call__(self, data): + if not isinstance(data, MetaTensor): + return data + + if self.nearest_interp: + data.applied_operations = convert_applied_interp_mode( + trans_info=data.applied_operations, mode="nearest", align_corners=None + ) + + data = data.detach() + inverted = self.transform.inverse(data) + inverted = self.post_func(inverted.to(self.device)) + return inverted diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index b3e7729f3d..3b2838fe0e 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -542,8 +542,8 @@ class Invertd(MapTransform): def __init__( self, - keys: KeysCollection = "", - transform: Optional[InvertibleTransform] = None, + keys: KeysCollection, + transform: InvertibleTransform, orig_keys: Optional[KeysCollection] = None, meta_keys: Optional[KeysCollection] = None, orig_meta_keys: Optional[KeysCollection] = None, diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index f8b5bb737b..fb37fcf641 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -508,7 +508,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, NdarrayOrTensor]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): # Create inverse transform inverse_transform = ToNumpy() @@ -683,7 +683,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> Dict[Hashable, N return d def inverse(self, data: Mapping[Hashable, Any]) -> Dict[Hashable, Any]: - d = deepcopy(dict(data)) + d = dict(data) for key in self.key_iterator(d): transform = self.get_most_recent_transform(d, key) # Create inverse transform @@ -1030,7 +1030,7 @@ def __call__(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torc return d def inverse(self, data): - d = deepcopy(dict(data)) + d = dict(data) for key, overwrite in self.key_iterator(d, self.overwrite): ret = self._lambd.inverse(data=d[key]) if overwrite: @@ -1100,7 +1100,7 @@ def __call__(self, data): return d def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> Dict[Hashable, torch.Tensor]: - d = deepcopy(dict(data)) + d = dict(data) for key, overwrite in self.key_iterator(d, self.overwrite): if isinstance(d[key], MetaTensor): tr = self.pop_transform(d[key]) diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index 48af2f9f23..d6ba666767 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -1611,11 +1611,9 @@ def attach_hook(func, hook, mode="pre"): """ supported = {"pre", "post"} if look_up_option(mode, supported) == "pre": - _hook = hook - _func = func + _hook, _func = hook, func else: - _hook = func - _func = hook + _hook, _func = func, hook @wraps(func) def wrapper(inst, data): diff --git a/tests/test_invert.py b/tests/test_invert.py new file mode 100644 index 0000000000..884499cf44 --- /dev/null +++ b/tests/test_invert.py @@ -0,0 +1,91 @@ +# Copyright (c) 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 +from copy import deepcopy + +import numpy as np +import torch + +from monai.data import DataLoader, Dataset, MetaTensor, create_test_image_3d, decollate_batch +from monai.transforms import ( + CastToType, + Compose, + EnsureChannelFirst, + Invert, + LoadImage, + Orientation, + RandAffine, + RandAxisFlip, + RandFlip, + RandRotate, + RandRotate90, + RandZoom, + ResizeWithPadOrCrop, + Spacing, +) +from monai.utils import set_determinism +from tests.utils import make_nifti_image + + +class TestInvert(unittest.TestCase): + def test_invert(self): + set_determinism(seed=0) + im_fname = make_nifti_image(create_test_image_3d(101, 100, 107, noise_max=100)[1]) # label image, discrete + data = [im_fname for _ in range(12)] + transform = Compose( + [ + LoadImage(image_only=True), + EnsureChannelFirst(), + Orientation("RPS"), + Spacing(pixdim=(1.2, 1.01, 0.9), mode="bilinear", dtype=np.float32), + RandFlip(prob=0.5, spatial_axis=[1, 2]), + RandAxisFlip(prob=0.5), + RandRotate90(prob=0, spatial_axes=(1, 2)), + RandZoom(prob=0.5, min_zoom=0.5, max_zoom=1.1, keep_size=True), + RandRotate(prob=0.5, range_x=np.pi, mode="bilinear", align_corners=True, dtype=np.float64), + RandAffine(prob=0.5, rotate_range=np.pi, mode="nearest"), + ResizeWithPadOrCrop(100), + CastToType(dtype=torch.uint8), + ] + ) + + # num workers = 0 for mac or gpu transforms + num_workers = 0 if sys.platform != "linux" or torch.cuda.is_available() else 2 + dataset = Dataset(data, transform=transform) + self.assertIsInstance(transform.inverse(dataset[0]), MetaTensor) + loader = DataLoader(dataset, num_workers=num_workers, batch_size=1) + inverter = Invert(transform=transform, nearest_interp=True, device="cpu") + + for d in loader: + d = decollate_batch(d) + for item in d: + orig = deepcopy(item) + i = inverter(item) + self.assertTupleEqual(orig.shape[1:], (100, 100, 100)) + # check the nearest interpolation mode + torch.testing.assert_allclose(i.to(torch.uint8).to(torch.float), i.to(torch.float)) + self.assertTupleEqual(i.shape[1:], (100, 101, 107)) + # check labels match + reverted = i.detach().cpu().numpy().astype(np.int32) + original = LoadImage(image_only=True)(data[-1]) + n_good = np.sum(np.isclose(reverted, original.numpy(), atol=1e-3)) + reverted_name = i.meta["filename_or_obj"] + original_name = original.meta["filename_or_obj"] + self.assertEqual(reverted_name, original_name) + print("invert diff", reverted.size - n_good) + self.assertTrue((reverted.size - n_good) < 50000, f"diff. {reverted.size - n_good}") + set_determinism(seed=None) + + +if __name__ == "__main__": + unittest.main() From 4de09bc5634d4b5c32b57caa88ceef691a9bdf37 Mon Sep 17 00:00:00 2001 From: Wenqi Li Date: Sun, 10 Jul 2022 12:17:40 +0100 Subject: [PATCH 18/18] exclude in min Signed-off-by: Wenqi Li --- tests/min_tests.py | 1 + tests/test_invert.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/min_tests.py b/tests/min_tests.py index 898d1b7b00..0959d0fe00 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -98,6 +98,7 @@ def run_testsuit(): "test_integration_workflows", "test_integration_workflows_gan", "test_integration_bundle_run", + "test_invert", "test_invertd", "test_iterable_dataset", "test_keep_largest_connected_component", diff --git a/tests/test_invert.py b/tests/test_invert.py index 884499cf44..b867a646fa 100644 --- a/tests/test_invert.py +++ b/tests/test_invert.py @@ -83,7 +83,7 @@ def test_invert(self): original_name = original.meta["filename_or_obj"] self.assertEqual(reverted_name, original_name) print("invert diff", reverted.size - n_good) - self.assertTrue((reverted.size - n_good) < 50000, f"diff. {reverted.size - n_good}") + self.assertTrue((reverted.size - n_good) < 300000, f"diff. {reverted.size - n_good}") set_determinism(seed=None)