From 0287b9de725992deb99bc51aa3a9e447ce0c5298 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 26 May 2021 12:59:32 +0100 Subject: [PATCH 1/5] inverse some keys Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/compose.py | 17 +++++++ tests/test_inverse_subset.py | 87 ++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 tests/test_inverse_subset.py diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 085d5dcfc4..51161172e0 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -13,6 +13,7 @@ """ import warnings +from copy import deepcopy from typing import Any, Callable, Optional, Sequence, Union import numpy as np @@ -28,6 +29,7 @@ apply_transform, ) from monai.utils import MAX_SEED, ensure_tuple, get_seed +from monai.utils.enums import InverseKeys __all__ = ["Compose"] @@ -156,10 +158,25 @@ def __call__(self, input_): return input_ def inverse(self, data): + return self.inverse_with_omissions(data, []) + + def inverse_with_omissions(self, data, *to_skip: Sequence[str]): + # get all invertible transforms invertible_transforms = [t for t in self.flatten().transforms if isinstance(t, InvertibleTransform)] if len(invertible_transforms) == 0: warnings.warn("inverse has been called but no invertible transforms have been supplied") + if len(to_skip) > 0: + # omit certain invertible transforms + invertible_transforms = [t for t in invertible_transforms if t.__class__.__name__ not in to_skip] + if len(invertible_transforms) == 0: + warnings.warn("all invertible transforms have been omitted") + + data = deepcopy(data) + for key, val in data.items(): + if InverseKeys.KEY_SUFFIX in key: + data[key] = [t for t in val if t[InverseKeys.CLASS_NAME] not in to_skip] + # loop backwards over transforms for t in reversed(invertible_transforms): data = apply_transform(t.inverse, data, self.map_items) diff --git a/tests/test_inverse_subset.py b/tests/test_inverse_subset.py new file mode 100644 index 0000000000..1ebae4ba81 --- /dev/null +++ b/tests/test_inverse_subset.py @@ -0,0 +1,87 @@ +# Copyright 2020 - 2021 MONAI Consortium +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest +from typing import Callable, List, Optional, Sequence, Tuple + +from parameterized import parameterized + +from monai.data import create_test_image_2d +from monai.transforms import AddChanneld, Compose, LoadImaged, RandAdjustContrastd, RandAxisFlipd, RandFlipd +from monai.transforms.inverse import InvertibleTransform +from monai.utils import set_determinism +from monai.utils.enums import InverseKeys +from tests.utils import make_nifti_image + +KEYS = ["image", "label"] + +TESTS: List[Tuple] = [] + + +class ErrorRandAxisFlipd(RandAxisFlipd): + def inverse(self, _): + raise NotImplementedError + + +# Remove any applied "RandAdjustContrastd" (only "label") +TESTS.append( + ( + Compose( + [ + LoadImaged(KEYS), + AddChanneld(KEYS), + RandAxisFlipd("image"), + ErrorRandAxisFlipd("label"), + ErrorRandAxisFlipd(KEYS), + RandFlipd(KEYS), + ] + ), + "ErrorRandAxisFlipd", + False, + ) +) + +TESTS.append( + ( + Compose([LoadImaged(KEYS), AddChanneld(KEYS), ErrorRandAxisFlipd("label")]), + "", + True, + ) +) + + +class TestInverseSubset(unittest.TestCase): + def setUp(self): + set_determinism(seed=0) + + im_fnames = [make_nifti_image(i) for i in create_test_image_2d(101, 100)] + self.data = {k: v for k, v in zip(KEYS, im_fnames)} + + def tearDown(self): + set_determinism(seed=None) + + @parameterized.expand(TESTS) + def test_inverse_subset( + self, + transforms: Compose, + to_skip: Sequence[str], + expected_exception: bool, + ) -> None: + d = transforms(self.data) + if not expected_exception: + transforms.inverse_with_omissions(d, to_skip) + else: + with self.assertRaises(RuntimeError): + transforms.inverse_with_omissions(d, to_skip) + + +if __name__ == "__main__": + unittest.main() From 11e9535fed188135aa7858c2e43242b78f2dc820 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 26 May 2021 13:01:51 +0100 Subject: [PATCH 2/5] remove unused imports Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_inverse_subset.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_inverse_subset.py b/tests/test_inverse_subset.py index 1ebae4ba81..02217d843d 100644 --- a/tests/test_inverse_subset.py +++ b/tests/test_inverse_subset.py @@ -10,15 +10,13 @@ # limitations under the License. import unittest -from typing import Callable, List, Optional, Sequence, Tuple +from typing import List, Sequence, Tuple from parameterized import parameterized from monai.data import create_test_image_2d -from monai.transforms import AddChanneld, Compose, LoadImaged, RandAdjustContrastd, RandAxisFlipd, RandFlipd -from monai.transforms.inverse import InvertibleTransform +from monai.transforms import AddChanneld, Compose, LoadImaged, RandAxisFlipd, RandFlipd from monai.utils import set_determinism -from monai.utils.enums import InverseKeys from tests.utils import make_nifti_image KEYS = ["image", "label"] @@ -28,10 +26,11 @@ class ErrorRandAxisFlipd(RandAxisFlipd): def inverse(self, _): - raise NotImplementedError + raise RuntimeError -# Remove any applied "RandAdjustContrastd" (only "label") +# remove the ErrorRandAxisFlipd transform. Since its inverse +# raises an exception, we'll know if this wasn't successful TESTS.append( ( Compose( @@ -49,6 +48,7 @@ def inverse(self, _): ) ) +# Nothing is removed, so exception is expected TESTS.append( ( Compose([LoadImaged(KEYS), AddChanneld(KEYS), ErrorRandAxisFlipd("label")]), From 4864945c6c141a25c2ab87c2a0104696f2558bf8 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 26 May 2021 13:41:05 +0100 Subject: [PATCH 3/5] requires nibabel Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- monai/transforms/compose.py | 24 +++++++++++++++++++++--- monai/utils/enums.py | 1 + tests/test_inverse_subset.py | 24 +++++++++++++++++++++--- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/monai/transforms/compose.py b/monai/transforms/compose.py index 51161172e0..e9d366ceb6 100644 --- a/monai/transforms/compose.py +++ b/monai/transforms/compose.py @@ -161,6 +161,17 @@ def inverse(self, data): return self.inverse_with_omissions(data, []) def inverse_with_omissions(self, data, *to_skip: Sequence[str]): + """Perform the inverse on a subset of the applied transformations. + This is done by passing a list of strings of the transforms to be skipped. + + We'll get a subset of the transforms, excluding any in the `to_skip` list. We'll + also loop across the data and find the applied transforms (e.g., `image_transforms`), + and move those into the list of skipped inverse transforms (image_skipped_inverses). + + Args: + data: data to be inverted + *to_skip: Sequence of class names to skip. + """ # get all invertible transforms invertible_transforms = [t for t in self.flatten().transforms if isinstance(t, InvertibleTransform)] if len(invertible_transforms) == 0: @@ -173,9 +184,16 @@ def inverse_with_omissions(self, data, *to_skip: Sequence[str]): warnings.warn("all invertible transforms have been omitted") data = deepcopy(data) - for key, val in data.items(): - if InverseKeys.KEY_SUFFIX in key: - data[key] = [t for t in val if t[InverseKeys.CLASS_NAME] not in to_skip] + for key in list(data.keys()): + inv_key = key + InverseKeys.KEY_SUFFIX + if inv_key in data: + to_inverse = [t for t in data[inv_key] if t[InverseKeys.CLASS_NAME] not in to_skip] + to_not_inverse = [t for t in data[inv_key] if t not in to_inverse] + data[key + InverseKeys.KEY_SUFFIX] = to_inverse + skipped_key = key + InverseKeys.KEY_SUFFIX_SKIPPED + if skipped_key not in data: + data[skipped_key] = [] + data[key + InverseKeys.KEY_SUFFIX_SKIPPED] += to_not_inverse # loop backwards over transforms for t in reversed(invertible_transforms): diff --git a/monai/utils/enums.py b/monai/utils/enums.py index 1b9e4f615d..ace03a9744 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -243,6 +243,7 @@ class InverseKeys: EXTRA_INFO = "extra_info" DO_TRANSFORM = "do_transforms" KEY_SUFFIX = "_transforms" + KEY_SUFFIX_SKIPPED = "_skipped_inverses" class CommonKeys: diff --git a/tests/test_inverse_subset.py b/tests/test_inverse_subset.py index 02217d843d..a14b2839ca 100644 --- a/tests/test_inverse_subset.py +++ b/tests/test_inverse_subset.py @@ -10,15 +10,23 @@ # limitations under the License. import unittest -from typing import List, Sequence, Tuple +from typing import TYPE_CHECKING, List, Sequence, Tuple +from unittest.case import skipUnless from parameterized import parameterized from monai.data import create_test_image_2d from monai.transforms import AddChanneld, Compose, LoadImaged, RandAxisFlipd, RandFlipd from monai.utils import set_determinism +from monai.utils.enums import InverseKeys from tests.utils import make_nifti_image +if TYPE_CHECKING: + has_nib = True +else: + _, has_nib = optional_import("nibabel") + + KEYS = ["image", "label"] TESTS: List[Tuple] = [] @@ -44,6 +52,7 @@ def inverse(self, _): ] ), "ErrorRandAxisFlipd", + (1, 2), False, ) ) @@ -53,6 +62,7 @@ def inverse(self, _): ( Compose([LoadImaged(KEYS), AddChanneld(KEYS), ErrorRandAxisFlipd("label")]), "", + (0, 0), True, ) ) @@ -69,18 +79,26 @@ def tearDown(self): set_determinism(seed=None) @parameterized.expand(TESTS) + @skipUnless(has_nib, "Requires NiBabel") def test_inverse_subset( self, transforms: Compose, to_skip: Sequence[str], + expected_num_skipped: Sequence[int], expected_exception: bool, ) -> None: d = transforms(self.data) if not expected_exception: - transforms.inverse_with_omissions(d, to_skip) + d = transforms.inverse_with_omissions(d, to_skip) else: with self.assertRaises(RuntimeError): - transforms.inverse_with_omissions(d, to_skip) + d = transforms.inverse_with_omissions(d, to_skip) + + for key, num_skipped in zip(KEYS, expected_num_skipped): + if num_skipped > 0: + self.assertEqual(len(d[key + InverseKeys.KEY_SUFFIX_SKIPPED]), num_skipped) + else: + self.assertNotIn(key + InverseKeys.KEY_SUFFIX_SKIPPED, d) if __name__ == "__main__": From 159167902a0b07868efaae79789a261664f1e045 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 26 May 2021 14:03:55 +0100 Subject: [PATCH 4/5] optional import Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_inverse_subset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inverse_subset.py b/tests/test_inverse_subset.py index a14b2839ca..5f538d9765 100644 --- a/tests/test_inverse_subset.py +++ b/tests/test_inverse_subset.py @@ -17,7 +17,7 @@ from monai.data import create_test_image_2d from monai.transforms import AddChanneld, Compose, LoadImaged, RandAxisFlipd, RandFlipd -from monai.utils import set_determinism +from monai.utils import set_determinism, optional_import from monai.utils.enums import InverseKeys from tests.utils import make_nifti_image From a15468ad64626a851cac90cb51e70329d73a93eb Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 26 May 2021 14:16:38 +0100 Subject: [PATCH 5/5] import order Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> --- tests/test_inverse_subset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_inverse_subset.py b/tests/test_inverse_subset.py index 5f538d9765..ff3e195a67 100644 --- a/tests/test_inverse_subset.py +++ b/tests/test_inverse_subset.py @@ -17,7 +17,7 @@ from monai.data import create_test_image_2d from monai.transforms import AddChanneld, Compose, LoadImaged, RandAxisFlipd, RandFlipd -from monai.utils import set_determinism, optional_import +from monai.utils import optional_import, set_determinism from monai.utils.enums import InverseKeys from tests.utils import make_nifti_image