From 145d31a25f80e997a6171324e17eb9c0e4b45313 Mon Sep 17 00:00:00 2001 From: Nic Ma Date: Wed, 13 Jan 2021 21:41:41 +0800 Subject: [PATCH 1/2] Refine Brats transform and add missing transforms docs (#1444) * [DLMED] refine Brats transform and add missing docs Signed-off-by: Nic Ma * [MONAI] python code formatting Signed-off-by: monai-bot Co-authored-by: monai-bot --- docs/source/transforms.rst | 24 +++++++++++++++++++ monai/transforms/__init__.py | 1 + monai/transforms/utility/array.py | 22 +++++++++++++++++ monai/transforms/utility/dictionary.py | 15 ++++++------ tests/test_convert_to_multi_channel.py | 33 ++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 tests/test_convert_to_multi_channel.py diff --git a/docs/source/transforms.rst b/docs/source/transforms.rst index 4fad2711095..57170a33a9e 100644 --- a/docs/source/transforms.rst +++ b/docs/source/transforms.rst @@ -504,6 +504,18 @@ Utility :members: :special-members: __call__ +`ConvertToMultiChannelBasedOnBratsClasses` +"""""""""""""""""""""""""""""""""""""""""" +.. autoclass:: ConvertToMultiChannelBasedOnBratsClasses + :members: + :special-members: __call__ + +`AddExtremePointsChannel` +""""""""""""""""""""""""" +.. autoclass:: AddExtremePointsChannel + :members: + :special-members: __call__ + `TorchVision` """"""""""""" .. autoclass:: TorchVision @@ -975,6 +987,18 @@ Utility (Dict) :members: :special-members: __call__ +`ConvertToMultiChannelBasedOnBratsClassesd` +""""""""""""""""""""""""""""""""""""""""""" +.. autoclass:: ConvertToMultiChannelBasedOnBratsClassesd + :members: + :special-members: __call__ + +`AddExtremePointsChanneld` +"""""""""""""""""""""""""" +.. autoclass:: AddExtremePointsChanneld + :members: + :special-members: __call__ + `TorchVisiond` """""""""""""" .. autoclass:: TorchVisiond diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index c7b4c67488b..4bfc8acfbd5 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -199,6 +199,7 @@ AsChannelFirst, AsChannelLast, CastToType, + ConvertToMultiChannelBasedOnBratsClasses, DataStats, FgBgToIndices, Identity, diff --git a/monai/transforms/utility/array.py b/monai/transforms/utility/array.py index 7e7fe816a99..a851a56a44c 100644 --- a/monai/transforms/utility/array.py +++ b/monai/transforms/utility/array.py @@ -41,6 +41,7 @@ "Lambda", "LabelToMask", "FgBgToIndices", + "ConvertToMultiChannelBasedOnBratsClasses", "AddExtremePointsChannel", "TorchVision", ] @@ -556,6 +557,27 @@ def __call__( return fg_indices, bg_indices +class ConvertToMultiChannelBasedOnBratsClasses(Transform): + """ + Convert labels to multi channels based on brats18 classes: + label 1 is the necrotic and non-enhancing tumor core + label 2 is the the peritumoral edema + label 4 is the GD-enhancing tumor + The possible classes are TC (Tumor core), WT (Whole tumor) + and ET (Enhancing tumor). + """ + + def __call__(self, img: np.ndarray) -> np.ndarray: + result = [] + # merge labels 1 (tumor non-enh) and 4 (tumor enh) to TC + result.append(np.logical_or(img == 1, img == 4)) + # merge labels 1 (tumor non-enh) and 4 (tumor enh) and 2 (large edema) to WT + result.append(np.logical_or(np.logical_or(img == 1, img == 4), img == 2)) + # label 4 is ET + result.append(img == 4) + return np.stack(result, axis=0).astype(np.float32) + + class AddExtremePointsChannel(Transform, Randomizable): """ Add extreme points of label to the image as a new channel. This transform generates extreme diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 5c08f72c92e..0ed328be0a0 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -29,6 +29,7 @@ AsChannelFirst, AsChannelLast, CastToType, + ConvertToMultiChannelBasedOnBratsClasses, DataStats, FgBgToIndices, Identity, @@ -649,6 +650,7 @@ def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.nda class ConvertToMultiChannelBasedOnBratsClassesd(MapTransform): """ + Dictionary-based wrapper of :py:class:`monai.transforms.ConvertToMultiChannelBasedOnBratsClasses`. Convert labels to multi channels based on brats18 classes: label 1 is the necrotic and non-enhancing tumor core label 2 is the the peritumoral edema @@ -657,17 +659,14 @@ class ConvertToMultiChannelBasedOnBratsClassesd(MapTransform): and ET (Enhancing tumor). """ + def __init__(self, keys: KeysCollection): + super().__init__(keys) + self.converter = ConvertToMultiChannelBasedOnBratsClasses() + def __call__(self, data: Mapping[Hashable, np.ndarray]) -> Dict[Hashable, np.ndarray]: d = dict(data) for key in self.keys: - result = [] - # merge labels 1 (tumor non-enh) and 4 (tumor enh) to TC - result.append(np.logical_or(d[key] == 1, d[key] == 4)) - # merge labels 1 (tumor non-enh) and 4 (tumor enh) and 2 (large edema) to WT - result.append(np.logical_or(np.logical_or(d[key] == 1, d[key] == 4), d[key] == 2)) - # label 4 is ET - result.append(d[key] == 4) - d[key] = np.stack(result, axis=0).astype(np.float32) + d[key] = self.converter(d[key]) return d diff --git a/tests/test_convert_to_multi_channel.py b/tests/test_convert_to_multi_channel.py new file mode 100644 index 00000000000..ea27371ac7e --- /dev/null +++ b/tests/test_convert_to_multi_channel.py @@ -0,0 +1,33 @@ +# 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 + +import numpy as np +from parameterized import parameterized + +from monai.transforms import ConvertToMultiChannelBasedOnBratsClasses + +TEST_CASE = [ + np.array([[0, 1, 2], [1, 2, 4], [0, 1, 4]]), + np.array([[[0, 1, 0], [1, 0, 1], [0, 1, 1]], [[0, 1, 1], [1, 1, 1], [0, 1, 1]], [[0, 0, 0], [0, 0, 1], [0, 0, 1]]]), +] + + +class TestConvertToMultiChannel(unittest.TestCase): + @parameterized.expand([TEST_CASE]) + def test_type_shape(self, data, expected_result): + result = ConvertToMultiChannelBasedOnBratsClasses()(data) + np.testing.assert_equal(result, expected_result) + + +if __name__ == "__main__": + unittest.main() From 3cb82cfc781d8d67a1541076a6c5badc95f38978 Mon Sep 17 00:00:00 2001 From: Richard Brown <33289025+rijobro@users.noreply.github.com> Date: Wed, 13 Jan 2021 16:40:10 +0000 Subject: [PATCH 2/2] add aliases to import (#1440) * add aliases to import Signed-off-by: Richard Brown <33289025+rijobro@users.noreply.github.com> * all-> __all__; resolves built-in name conficts Signed-off-by: Wenqi Li * fixes typo Signed-off-by: Wenqi Li * exit -> sys.exit Signed-off-by: Wenqi Li * maintainer field deprecated https://docs.docker.com/engine/reference/builder/\#maintainer-deprecated Signed-off-by: Wenqi Li Co-authored-by: Wenqi Li --- Dockerfile | 2 +- monai/networks/utils.py | 6 +- monai/transforms/__init__.py | 94 +++++++++++++++++++++++++- monai/transforms/io/dictionary.py | 16 ++++- monai/transforms/post/dictionary.py | 12 ++++ monai/transforms/spatial/dictionary.py | 28 ++++++++ monai/transforms/utility/dictionary.py | 40 +++++++++++ monai/utils/aliases.py | 2 +- monai/utils/decorators.py | 2 +- monai/utils/enums.py | 2 +- monai/utils/misc.py | 2 +- tests/min_tests.py | 2 +- tests/test_load_image.py | 2 +- tests/test_occlusion_sensitivity.py | 6 +- tests/test_reg_loss_integration.py | 4 +- 15 files changed, 203 insertions(+), 17 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9430db076b7..2d01f573012 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ ARG PYTORCH_IMAGE=nvcr.io/nvidia/pytorch:20.10-py3 FROM ${PYTORCH_IMAGE} -MAINTAINER MONAI Consortium +LABEL maintainer="monai.miccai2019@gmail.com" WORKDIR /opt/monai diff --git a/monai/networks/utils.py b/monai/networks/utils.py index a2104945b00..bc3d291203f 100644 --- a/monai/networks/utils.py +++ b/monai/networks/utils.py @@ -301,13 +301,13 @@ def train_mode(*nets: nn.Module): """ # Get original state of network(s) - eval = [n for n in nets if not n.training] + eval_list = [n for n in nets if not n.training] try: # set to train mode with torch.set_grad_enabled(True): yield [n.train() for n in nets] finally: - # Return required networks to eval - for n in eval: + # Return required networks to eval_list + for n in eval_list: n.eval() diff --git a/monai/transforms/__init__.py b/monai/transforms/__init__.py index 4bfc8acfbd5..e79b2ce38bb 100644 --- a/monai/transforms/__init__.py +++ b/monai/transforms/__init__.py @@ -138,7 +138,21 @@ ThresholdIntensityDict, ) from .io.array import LoadImage, LoadNifti, LoadNumpy, LoadPNG -from .io.dictionary import LoadDatad, LoadImaged, LoadNiftid, LoadNumpyd, LoadPNGd +from .io.dictionary import ( + LoadDatad, + LoadImaged, + LoadImageD, + LoadImageDict, + LoadNiftid, + LoadNiftiD, + LoadNiftiDict, + LoadNumpyd, + LoadNumpyD, + LoadNumpyDict, + LoadPNGd, + LoadPNGD, + LoadPNGDict, +) from .post.array import ( Activations, AsDiscrete, @@ -149,12 +163,24 @@ ) from .post.dictionary import ( Activationsd, + ActivationsD, + ActivationsDict, AsDiscreted, + AsDiscreteD, + AsDiscreteDict, Ensembled, KeepLargestConnectedComponentd, + KeepLargestConnectedComponentD, + KeepLargestConnectedComponentDict, LabelToContourd, + LabelToContourD, + LabelToContourDict, MeanEnsembled, + MeanEnsembleD, + MeanEnsembleDict, VoteEnsembled, + VoteEnsembleD, + VoteEnsembleDict, ) from .spatial.array import ( Affine, @@ -179,19 +205,47 @@ ) from .spatial.dictionary import ( Flipd, + FlipD, + FlipDict, Orientationd, + OrientationD, + OrientationDict, Rand2DElasticd, + Rand2DElasticD, + Rand2DElasticDict, Rand3DElasticd, + Rand3DElasticD, + Rand3DElasticDict, RandAffined, + RandAffineD, + RandAffineDict, RandFlipd, + RandFlipD, + RandFlipDict, RandRotate90d, + RandRotate90D, + RandRotate90Dict, RandRotated, + RandRotateD, + RandRotateDict, RandZoomd, + RandZoomD, + RandZoomDict, Resized, + ResizeD, + ResizeDict, Rotate90d, + Rotate90D, + Rotate90Dict, Rotated, + RotateD, + RotateDict, Spacingd, + SpacingD, + SpacingDict, Zoomd, + ZoomD, + ZoomDict, ) from .utility.array import ( AddChannel, @@ -216,27 +270,65 @@ ) from .utility.dictionary import ( AddChanneld, + AddChannelD, + AddChannelDict, AddExtremePointsChanneld, + AddExtremePointsChannelD, + AddExtremePointsChannelDict, AsChannelFirstd, + AsChannelFirstD, + AsChannelFirstDict, AsChannelLastd, + AsChannelLastD, + AsChannelLastDict, CastToTyped, + CastToTypeD, + CastToTypeDict, ConcatItemsd, + ConcatItemsD, + ConcatItemsDict, ConvertToMultiChannelBasedOnBratsClassesd, + ConvertToMultiChannelBasedOnBratsClassesD, + ConvertToMultiChannelBasedOnBratsClassesDict, CopyItemsd, + CopyItemsD, + CopyItemsDict, DataStatsd, + DataStatsD, + DataStatsDict, DeleteItemsd, + DeleteItemsD, + DeleteItemsDict, FgBgToIndicesd, + FgBgToIndicesD, + FgBgToIndicesDict, Identityd, + IdentityD, + IdentityDict, LabelToMaskd, + LabelToMaskD, + LabelToMaskDict, Lambdad, + LambdaD, + LambdaDict, RepeatChanneld, + RepeatChannelD, + RepeatChannelDict, SelectItemsd, SimulateDelayd, + SimulateDelayD, + SimulateDelayDict, SplitChanneld, + SplitChannelD, + SplitChannelDict, SqueezeDimd, + SqueezeDimD, + SqueezeDimDict, ToNumpyd, TorchVisiond, ToTensord, + ToTensorD, + ToTensorDict, ) from .utils import ( apply_transform, diff --git a/monai/transforms/io/dictionary.py b/monai/transforms/io/dictionary.py index 9f7cd821eba..474fbd0a504 100644 --- a/monai/transforms/io/dictionary.py +++ b/monai/transforms/io/dictionary.py @@ -24,7 +24,21 @@ from monai.transforms.compose import MapTransform from monai.transforms.io.array import LoadImage, LoadNifti, LoadNumpy, LoadPNG -__all__ = ["LoadImaged", "LoadDatad", "LoadNiftid", "LoadPNGd", "LoadNumpyd"] +__all__ = [ + "LoadImaged", + "LoadDatad", + "LoadNiftid", + "LoadPNGd", + "LoadNumpyd", + "LoadImageD", + "LoadImageDict", + "LoadNiftiD", + "LoadNiftiDict", + "LoadPNGD", + "LoadPNGDict", + "LoadNumpyD", + "LoadNumpyDict", +] class LoadImaged(MapTransform): diff --git a/monai/transforms/post/dictionary.py b/monai/transforms/post/dictionary.py index 779fcdc3976..60cda11a917 100644 --- a/monai/transforms/post/dictionary.py +++ b/monai/transforms/post/dictionary.py @@ -40,6 +40,18 @@ "Ensembled", "MeanEnsembled", "VoteEnsembled", + "ActivationsD", + "ActivationsDict", + "AsDiscreteD", + "AsDiscreteDict", + "KeepLargestConnectedComponentD", + "KeepLargestConnectedComponentDict", + "LabelToContourD", + "LabelToContourDict", + "MeanEnsembleD", + "MeanEnsembleDict", + "VoteEnsembleD", + "VoteEnsembleDict", ] diff --git a/monai/transforms/spatial/dictionary.py b/monai/transforms/spatial/dictionary.py index 5250bd1550f..2113f6c2d02 100644 --- a/monai/transforms/spatial/dictionary.py +++ b/monai/transforms/spatial/dictionary.py @@ -62,6 +62,34 @@ "RandRotated", "Zoomd", "RandZoomd", + "SpacingD", + "SpacingDict", + "OrientationD", + "OrientationDict", + "Rotate90D", + "Rotate90Dict", + "RandRotate90D", + "RandRotate90Dict", + "ResizeD", + "ResizeDict", + "RandAffineD", + "RandAffineDict", + "Rand2DElasticD", + "Rand2DElasticDict", + "Rand3DElasticD", + "Rand3DElasticDict", + "FlipD", + "FlipDict", + "RandFlipD", + "RandFlipDict", + "RotateD", + "RotateDict", + "RandRotateD", + "RandRotateDict", + "ZoomD", + "ZoomDict", + "RandZoomD", + "RandZoomDict", ] GridSampleModeSequence = Union[Sequence[Union[GridSampleMode, str]], GridSampleMode, str] diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 0ed328be0a0..7b95fdab9eb 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -68,7 +68,47 @@ "FgBgToIndicesd", "ConvertToMultiChannelBasedOnBratsClassesd", "AddExtremePointsChanneld", + "IdentityD", + "IdentityDict", + "AsChannelFirstD", + "AsChannelFirstDict", + "AsChannelLastD", + "AsChannelLastDict", + "AddChannelD", + "AddChannelDict", + "RepeatChannelD", + "RepeatChannelDict", + "SplitChannelD", + "SplitChannelDict", + "CastToTypeD", + "CastToTypeDict", + "ToTensorD", + "ToTensorDict", + "DeleteItemsD", + "DeleteItemsDict", + "SqueezeDimD", + "SqueezeDimDict", + "DataStatsD", + "DataStatsDict", + "SimulateDelayD", + "SimulateDelayDict", + "CopyItemsD", + "CopyItemsDict", + "ConcatItemsD", + "ConcatItemsDict", + "LambdaD", + "LambdaDict", + "LabelToMaskD", + "LabelToMaskDict", + "FgBgToIndicesD", + "FgBgToIndicesDict", + "ConvertToMultiChannelBasedOnBratsClassesD", + "ConvertToMultiChannelBasedOnBratsClassesDict", + "AddExtremePointsChannelD", + "AddExtremePointsChannelDict", "TorchVisiond", + "TorchVisionD", + "TorchVisionDict", ] diff --git a/monai/utils/aliases.py b/monai/utils/aliases.py index ce3c5134730..e4ef40da110 100644 --- a/monai/utils/aliases.py +++ b/monai/utils/aliases.py @@ -21,7 +21,7 @@ alias_lock = threading.RLock() GlobalAliases = {} -all = ["alias", "resolve_name"] +__all__ = ["alias", "resolve_name"] def alias(*names): diff --git a/monai/utils/decorators.py b/monai/utils/decorators.py index 78b510ad1c8..a3e6e3f9806 100644 --- a/monai/utils/decorators.py +++ b/monai/utils/decorators.py @@ -11,7 +11,7 @@ from functools import wraps -all = ["RestartGenerator", "MethodReplacer"] +__all__ = ["RestartGenerator", "MethodReplacer"] class RestartGenerator: diff --git a/monai/utils/enums.py b/monai/utils/enums.py index 7b01f2146b8..d1d2d3bccee 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -11,7 +11,7 @@ from enum import Enum -all = [ +__all__ = [ "NumpyPadMode", "GridSampleMode", "InterpolateMode", diff --git a/monai/utils/misc.py b/monai/utils/misc.py index 343e809f704..bf1ff60cbca 100644 --- a/monai/utils/misc.py +++ b/monai/utils/misc.py @@ -19,7 +19,7 @@ import numpy as np import torch -all = [ +__all__ = [ "zip_with", "star_zip_with", "first", diff --git a/tests/min_tests.py b/tests/min_tests.py index 0e2e8d39179..4cca8b5af8b 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -136,4 +136,4 @@ def run_testsuit(): # testing all modules test_runner = unittest.TextTestRunner(stream=sys.stdout, verbosity=2) result = test_runner.run(run_testsuit()) - exit(int(not result.wasSuccessful())) + sys.exit(int(not result.wasSuccessful())) diff --git a/tests/test_load_image.py b/tests/test_load_image.py index f549979de73..b7743f86ad9 100644 --- a/tests/test_load_image.py +++ b/tests/test_load_image.py @@ -124,7 +124,7 @@ def test_itk_dicom_series_reader(self, input_param, filenames, expected_shape): [0.0, 0.0, 0.0, 1.0], ] ), - ), + ) self.assertTupleEqual(result.shape, expected_shape) self.assertTupleEqual(tuple(header["spatial_shape"]), expected_shape) diff --git a/tests/test_occlusion_sensitivity.py b/tests/test_occlusion_sensitivity.py index ea21cd4fa82..47a13d01e1c 100644 --- a/tests/test_occlusion_sensitivity.py +++ b/tests/test_occlusion_sensitivity.py @@ -77,13 +77,13 @@ class TestComputeOcclusionSensitivity(unittest.TestCase): @parameterized.expand([TEST_CASE_0, TEST_CASE_1]) def test_shape(self, init_data, call_data, map_expected_shape, most_prob_expected_shape): occ_sens = OcclusionSensitivity(**init_data) - map, most_prob = occ_sens(**call_data) - self.assertTupleEqual(map.shape, map_expected_shape) + m, most_prob = occ_sens(**call_data) + self.assertTupleEqual(m.shape, map_expected_shape) self.assertTupleEqual(most_prob.shape, most_prob_expected_shape) # most probable class should be of type int, and should have min>=0, max