From 0ff2d45c1cb02581dd5950d6128843ba2aa70f78 Mon Sep 17 00:00:00 2001 From: Ana Matoso Date: Fri, 22 Mar 2024 16:25:16 +0000 Subject: [PATCH 01/16] Add SubtractItemsd in __init__.py Signed-off-by: Ana Matoso --- monai/transforms/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 2aa8fbf8a1..e682376153 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -603,6 +603,7 @@ SqueezeDimd, SqueezeDimD, SqueezeDimDict, + SubtractItemsd, ToCupyd, ToCupyD, ToCupyDict, From 6d472182d051d2fd85dbc8f844b6c822807192e3 Mon Sep 17 00:00:00 2001 From: Ana Matoso Date: Fri, 22 Mar 2024 16:32:12 +0000 Subject: [PATCH 02/16] Add SubtractItemsd in dictionary.py Signed-off-by: Ana Matoso --- monai/transforms/utility/dictionary.py | 51 ++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 7e3a7b0454..466b84766c 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -156,6 +156,7 @@ "SqueezeDimD", "SqueezeDimDict", "SqueezeDimd", + "SubtractItemsd", "ToCupyD", "ToCupyDict", "ToCupyd", @@ -923,6 +924,56 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N d[new_key] = MetaObj.copy_items(val) if isinstance(val, (torch.Tensor, np.ndarray)) else deepcopy(val) return d +class SubtractItemsd(MapTransform): + """ + Subtract specified items from data dictionary elementwise. + Expect all the items are numpy array or PyTorch Tensor or MetaTensor. + Return the first input's meta information when items are MetaTensor. + """ + + backend = [TransformBackends.TORCH, TransformBackends.NUMPY] + + + def __init__(self, keys: KeysCollection, name: str, allow_missing_keys: bool = False) -> None: + """ + Args: + keys: keys of the corresponding items to be subtracted. + See also: :py:class:`monai.transforms.compose.MapTransform` + name: the name corresponding to the key to store the resulting data. + allow_missing_keys: don't raise exception if key is missing. + """ + super().__init__(keys, allow_missing_keys) + self.name = name + + def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, NdarrayOrTensor]: + """ + Raises: + TypeError: When items in ``data`` differ in type. + TypeError: When the item type is not in ``Union[numpy.ndarray, torch.Tensor, MetaTensor]``. + + """ + d = dict(data) + output = [] + data_type = None + for key in self.key_iterator(d): + if data_type is None: + data_type = type(d[key]) + elif not isinstance(d[key], data_type): + raise TypeError("All items in data must have the same type.") + output.append(d[key]) + + if len(output) == 0: + return d + + if data_type is np.ndarray: + d[self.name] = np.subtract(output[0], output[1]) + elif issubclass(data_type, torch.Tensor): + d[self.name] = torch.sub(output[0], output[1]) + else: + raise TypeError( + f"Unsupported data type: {data_type}, available options are (numpy.ndarray, torch.Tensor, MetaTensor)." + ) + return d class ConcatItemsd(MapTransform): """ From e7d307b9ebf2781465a693c40d86f52ddb9a31fe Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:26:40 +0000 Subject: [PATCH 03/16] Change according to code formater checks --- monai/transforms/utility/dictionary.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 75307bd730..ed28a6d14b 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -957,6 +957,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N d[new_key] = MetaObj.copy_items(val) if isinstance(val, (torch.Tensor, np.ndarray)) else deepcopy(val) return d + class SubtractItemsd(MapTransform): """ Subtract specified items from data dictionary elementwise. @@ -966,7 +967,6 @@ class SubtractItemsd(MapTransform): backend = [TransformBackends.TORCH, TransformBackends.NUMPY] - def __init__(self, keys: KeysCollection, name: str, allow_missing_keys: bool = False) -> None: """ Args: @@ -1000,7 +1000,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N if data_type is np.ndarray: d[self.name] = np.subtract(output[0], output[1]) - elif issubclass(data_type, torch.Tensor): + elif issubclass(data_type, torch.Tensor): d[self.name] = torch.sub(output[0], output[1]) else: raise TypeError( @@ -1008,6 +1008,7 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N ) return d + class ConcatItemsd(MapTransform): """ Concatenate specified items from data dictionary together on the first dim to construct a big array. From 7c41ce5dbf557ce59c5b57bdbf2280660f197893 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 14:26:50 +0000 Subject: [PATCH 04/16] Add function to docs --- docs/source/transforms.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index d2585daf63..a02cde960e 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -2199,6 +2199,12 @@ Utility (Dict) :members: :special-members: __call__ +`SubtractItemsd` +"""""""""""""" +.. autoclass:: SubtractItemsd + :members: + :special-members: __call__ + `ConcatItemsd` """""""""""""" .. autoclass:: ConcatItemsd From d1f0257575e62554804236b6565da3a093f6d7c2 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:39:36 +0000 Subject: [PATCH 05/16] Add tests --- tests/test_subtract_itemsd.py | 62 +++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_subtract_itemsd.py diff --git a/tests/test_subtract_itemsd.py b/tests/test_subtract_itemsd.py new file mode 100644 index 0000000000..aae0eee04b --- /dev/null +++ b/tests/test_subtract_itemsd.py @@ -0,0 +1,62 @@ +# 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. + +from __future__ import annotations + +import unittest + +import numpy as np +import torch + +from monai.data import MetaTensor +from monai.transforms import SubtractItemsd +from tests.utils import assert_allclose + + +class TestSubtractItemsd(unittest.TestCase): + + def test_tensor_values(self): + device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu:0") + input_data = { + "img1": torch.tensor([[0, 1], [1, 2]], device=device), + "img2": torch.tensor([[0, 1], [1, 2]], device=device), + "name" : "key_name" + } + result = SubtractItemsd(keys=["img1", "img2"], name="sub_img")(input_data) + self.assertIn("sub_img", result) + result["sub_img"] += 1 + assert_allclose(result["img1"], torch.tensor([[0, 1], [1, 2]], device=device)) + assert_allclose(result["sub_img"], torch.tensor([[0, 0], [0, 0]], device=device)) + + def test_metatensor_values(self): + device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu:0") + input_data = { + "img1": MetaTensor([[0, 1], [1, 2]], device=device), + "img2": MetaTensor([[0, 1], [1, 2]], device=device), + } + result = SubtractItemsd(keys=["img1", "img2"], name="sub_img")(input_data) + self.assertIn("sub_img", result) + self.assertIsInstance(result["sub_img"], MetaTensor) + self.assertEqual(result["img1"].meta, result["sub_img"].meta) + result["sub_img"] += 1 + assert_allclose(result["img1"], torch.tensor([[0, 1], [1, 2]], device=device)) + assert_allclose(result["sub_img"], torch.tensor([[0, 0], [0, 0]], device=device)) + + def test_numpy_values(self): + input_data = {"img1": np.array([[0, 1], [1, 2]]), "img2": np.array([[0, 1], [1, 2]])} + result = SubtractItemsd(keys=["img1", "img2"], name="sub_img")(input_data) + self.assertIn("sub_img", result) + result["sub_img"] += 1 + np.testing.assert_allclose(result["img1"], np.array([[0, 1], [1, 2]])) + np.testing.assert_allclose(result["sub_img"], np.array([[0, 0], [0, 0]])) + +if __name__ == "__main__": + unittest.main() From 259ed0f4c39bd168cd159bf34f684252445d5fff Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 15:47:37 +0000 Subject: [PATCH 06/16] Correct misscalculation --- tests/test_subtract_itemsd.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_subtract_itemsd.py b/tests/test_subtract_itemsd.py index aae0eee04b..c5f2529172 100644 --- a/tests/test_subtract_itemsd.py +++ b/tests/test_subtract_itemsd.py @@ -34,7 +34,7 @@ def test_tensor_values(self): self.assertIn("sub_img", result) result["sub_img"] += 1 assert_allclose(result["img1"], torch.tensor([[0, 1], [1, 2]], device=device)) - assert_allclose(result["sub_img"], torch.tensor([[0, 0], [0, 0]], device=device)) + assert_allclose(result["sub_img"], torch.tensor([[1, 1], [1, 1]], device=device)) def test_metatensor_values(self): device = torch.device("cuda:0") if torch.cuda.is_available() else torch.device("cpu:0") @@ -48,7 +48,7 @@ def test_metatensor_values(self): self.assertEqual(result["img1"].meta, result["sub_img"].meta) result["sub_img"] += 1 assert_allclose(result["img1"], torch.tensor([[0, 1], [1, 2]], device=device)) - assert_allclose(result["sub_img"], torch.tensor([[0, 0], [0, 0]], device=device)) + assert_allclose(result["sub_img"], torch.tensor([[1, 1], [1, 1]], device=device)) def test_numpy_values(self): input_data = {"img1": np.array([[0, 1], [1, 2]]), "img2": np.array([[0, 1], [1, 2]])} @@ -56,7 +56,7 @@ def test_numpy_values(self): self.assertIn("sub_img", result) result["sub_img"] += 1 np.testing.assert_allclose(result["img1"], np.array([[0, 1], [1, 2]])) - np.testing.assert_allclose(result["sub_img"], np.array([[0, 0], [0, 0]])) + np.testing.assert_allclose(result["sub_img"], np.array([[1, 1], [1, 1]])) if __name__ == "__main__": unittest.main() From 6b505628afbfa5361f7b5b58870886d6c2b54574 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:50:15 +0000 Subject: [PATCH 07/16] Reformat according to code formatter checks --- tests/test_subtract_itemsd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_subtract_itemsd.py b/tests/test_subtract_itemsd.py index c5f2529172..d754acbc30 100644 --- a/tests/test_subtract_itemsd.py +++ b/tests/test_subtract_itemsd.py @@ -28,7 +28,7 @@ def test_tensor_values(self): input_data = { "img1": torch.tensor([[0, 1], [1, 2]], device=device), "img2": torch.tensor([[0, 1], [1, 2]], device=device), - "name" : "key_name" + "name": "key_name", } result = SubtractItemsd(keys=["img1", "img2"], name="sub_img")(input_data) self.assertIn("sub_img", result) @@ -58,5 +58,6 @@ def test_numpy_values(self): np.testing.assert_allclose(result["img1"], np.array([[0, 1], [1, 2]])) np.testing.assert_allclose(result["sub_img"], np.array([[1, 1], [1, 1]])) + if __name__ == "__main__": unittest.main() From b837235aeb7e5980512bee5c8b2cb4717f220df7 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:51:20 +0000 Subject: [PATCH 08/16] DCO Remediation Commit for Ana Matoso I, Ana Matoso , hereby add my Signed-off-by to this commit: 0ff2d45c1cb02581dd5950d6128843ba2aa70f78, 6d472182d051d2fd85dbc8f844b6c822807192e3, 670e39d71686dc85d616821540909cbbfb1cb908, e7d307b9ebf2781465a693c40d86f52ddb9a31fe, 7c41ce5dbf557ce59c5b57bdbf2280660f197893, d1f0257575e62554804236b6565da3a093f6d7c2, 259ed0f4c39bd168cd159bf34f684252445d5fff, 6b505628afbfa5361f7b5b58870886d6c2b54574 Signed-off-by: Ana Matoso From 75d4d15446399075fcc337856216874052099eda Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:57:29 +0000 Subject: [PATCH 09/16] Correct title underline Signed-off-by: Ana Matoso --- docs/source/transforms.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index a02cde960e..ff313b7135 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -2200,7 +2200,7 @@ Utility (Dict) :special-members: __call__ `SubtractItemsd` -"""""""""""""" +"""""""""""""""" .. autoclass:: SubtractItemsd :members: :special-members: __call__ From 8695428f81a20503be022be01f62c447ecef4df6 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 17:57:55 +0000 Subject: [PATCH 10/16] Add SubtractItems Signed-off-by: Author Name --- tests/test_module_list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_module_list.py b/tests/test_module_list.py index 833441cbca..4d3dca1ffc 100644 --- a/tests/test_module_list.py +++ b/tests/test_module_list.py @@ -42,7 +42,7 @@ def test_transform_api(self): """monai subclasses of MapTransforms must have alias names ending with 'd', 'D', 'Dict'""" to_exclude = {"MapTransform"} # except for these transforms to_exclude_docs = {"Decollate", "Ensemble", "Invert", "SaveClassification", "RandTorchVision", "RandCrop"} - to_exclude_docs.update({"DeleteItems", "SelectItems", "FlattenSubKeys", "CopyItems", "ConcatItems"}) + to_exclude_docs.update({"DeleteItems", "SelectItems", "FlattenSubKeys", "CopyItems", "ConcatItems", "SubtractItems"}) to_exclude_docs.update({"ToMetaTensor", "FromMetaTensor"}) xforms = { name: obj From 544ce4021ae85cdb6e05ebb4c0f4c8d32157d28e Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:01:22 +0000 Subject: [PATCH 11/16] DCO Remediation Commit for Ana Matoso <78906907+anamatoso@users.noreply.github.com> I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: e7d307b9ebf2781465a693c40d86f52ddb9a31fe I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 7c41ce5dbf557ce59c5b57bdbf2280660f197893 I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: d1f0257575e62554804236b6565da3a093f6d7c2 I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 259ed0f4c39bd168cd159bf34f684252445d5fff I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 6b505628afbfa5361f7b5b58870886d6c2b54574 I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: b837235aeb7e5980512bee5c8b2cb4717f220df7 I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 75d4d15446399075fcc337856216874052099eda I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 8695428f81a20503be022be01f62c447ecef4df6 Signed-off-by: Ana Matoso <78906907+anamatoso@users.noreply.github.com> From 6a6243cf4e07b62f314d99e678f7dce8124d6275 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:25:43 +0000 Subject: [PATCH 12/16] Add SubtractItemsDict and SubtractItemsD Signed-off-by: Ana Matoso --- monai/transforms/__init__.py | 2 ++ monai/transforms/utility/dictionary.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index cc759d4c83..80f54975b1 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -648,6 +648,8 @@ SqueezeDimD, SqueezeDimDict, SubtractItemsd, + SubtractItemsD, + SubtractItemsDict, ToCupyd, ToCupyD, ToCupyDict, diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index ed28a6d14b..23001bd2e1 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -161,6 +161,8 @@ "SqueezeDimD", "SqueezeDimDict", "SqueezeDimd", + "SubtractItemsD", + "SubtractItemsDict", "SubtractItemsd", "ToCupyD", "ToCupyDict", @@ -1979,6 +1981,7 @@ def inverse(self, data: Mapping[Hashable, torch.Tensor]) -> dict[Hashable, torch DataStatsD = DataStatsDict = DataStatsd SimulateDelayD = SimulateDelayDict = SimulateDelayd CopyItemsD = CopyItemsDict = CopyItemsd +SubtractItemsD = SubtractItemsDict = SubtractItemsd ConcatItemsD = ConcatItemsDict = ConcatItemsd LambdaD = LambdaDict = Lambdad LabelToMaskD = LabelToMaskDict = LabelToMaskd From 970ca68b2267a60eb316b81737d6c1331947091c Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:56:49 +0000 Subject: [PATCH 13/16] DCO Remediation Commit for Ana Matoso <78906907+anamatoso@users.noreply.github.com> I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 6a6243cf4e07b62f314d99e678f7dce8124d6275 Signed-off-by: Ana Matoso <78906907+anamatoso@users.noreply.github.com> From 261c81d7865550b68a443d72641afad1fefcbba5 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 20:39:49 +0000 Subject: [PATCH 14/16] Reformat test_module_list.py Signed-off-by: Ana Matoso --- tests/test_module_list.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test_module_list.py b/tests/test_module_list.py index 4d3dca1ffc..761a5016fd 100644 --- a/tests/test_module_list.py +++ b/tests/test_module_list.py @@ -42,7 +42,9 @@ def test_transform_api(self): """monai subclasses of MapTransforms must have alias names ending with 'd', 'D', 'Dict'""" to_exclude = {"MapTransform"} # except for these transforms to_exclude_docs = {"Decollate", "Ensemble", "Invert", "SaveClassification", "RandTorchVision", "RandCrop"} - to_exclude_docs.update({"DeleteItems", "SelectItems", "FlattenSubKeys", "CopyItems", "ConcatItems", "SubtractItems"}) + to_exclude_docs.update( + {"DeleteItems", "SelectItems", "FlattenSubKeys", "CopyItems", "ConcatItems", "SubtractItems"} + ) to_exclude_docs.update({"ToMetaTensor", "FromMetaTensor"}) xforms = { name: obj From 16c4b0c2d436b14d823316a6cb560547d22eba99 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:56:59 +0000 Subject: [PATCH 15/16] Add comments Signed-off-by: Ana Matoso <78906907+anamatoso@users.noreply.github.com> --- monai/transforms/utility/dictionary.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 23001bd2e1..4144094d6b 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -1002,8 +1002,8 @@ def __call__(self, data: Mapping[Hashable, NdarrayOrTensor]) -> dict[Hashable, N if data_type is np.ndarray: d[self.name] = np.subtract(output[0], output[1]) - elif issubclass(data_type, torch.Tensor): - d[self.name] = torch.sub(output[0], output[1]) + elif issubclass(data_type, torch.Tensor): # type: ignore + d[self.name] = torch.sub(output[0], output[1]) # type: ignore else: raise TypeError( f"Unsupported data type: {data_type}, available options are (numpy.ndarray, torch.Tensor, MetaTensor)." From 52e89f9a16340a047a329f531e546cc671205019 Mon Sep 17 00:00:00 2001 From: Ana Matoso <78906907+anamatoso@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:57:17 +0000 Subject: [PATCH 16/16] DCO Remediation Commit for Ana Matoso <78906907+anamatoso@users.noreply.github.com> I, Ana Matoso <78906907+anamatoso@users.noreply.github.com>, hereby add my Signed-off-by to this commit: 261c81d7865550b68a443d72641afad1fefcbba5 Signed-off-by: Ana Matoso <78906907+anamatoso@users.noreply.github.com>