From f672d7a08a1223cb88541e9ef9d0e77a0fa52e54 Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Fri, 16 Jul 2021 23:44:49 +0800 Subject: [PATCH 1/4] fix synthetic bugs Signed-off-by: Yiheng Wang --- monai/data/synthetic.py | 46 ++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index 1a8b2a6fbb..ee0b75166a 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -23,23 +23,25 @@ def create_test_image_2d( height: int, num_objs: int = 12, rad_max: int = 30, + rad_min: int = 5, noise_max: float = 0.0, num_seg_classes: int = 5, channel_dim: Optional[int] = None, random_state: Optional[np.random.RandomState] = None, ) -> Tuple[np.ndarray, np.ndarray]: """ - Return a noisy 2D image with `num_objs` circles and a 2D mask image. The maximum radius of the circles is given as - `rad_max`. The mask will have `num_seg_classes` number of classes for segmentations labeled sequentially from 1, plus a - background class represented as 0. If `noise_max` is greater than 0 then noise will be added to the image taken from - the uniform distribution on range `[0,noise_max)`. If `channel_dim` is None, will create an image without channel - dimension, otherwise create an image with channel dimension as first dim or last dim. + Return a noisy 2D image with `num_objs` circles and a 2D mask image. The maximum and minimum radiuses of the circles + are given as `rad_max` and `rad_min`. The mask will have `num_seg_classes` number of classes for segmentations labeled + sequentially from 1, plus a background class represented as 0. If `noise_max` is greater than 0 then noise will be + added to the image taken from the uniform distribution on range `[0,noise_max)`. If `channel_dim` is None, will create + an image without channel dimension, otherwise create an image with channel dimension as first dim or last dim. Args: - width: width of the image. - height: height of the image. + width: width of the image. The value should be larger than `2 * rad_max`. + height: height of the image. The value should be larger than `2 * rad_max`. num_objs: number of circles to generate. Defaults to `12`. rad_max: maximum circle radius. Defaults to `30`. + rad_min: minimum circle radius. Defaults to `5`. noise_max: if greater than 0 then noise will be added to the image taken from the uniform distribution on range `[0,noise_max)`. Defaults to `0`. num_seg_classes: number of classes for segmentations. Defaults to `5`. @@ -47,13 +49,22 @@ def create_test_image_2d( an image with channel dimension as first dim or last dim. Defaults to `None`. random_state: the random generator to use. Defaults to `np.random`. """ + + if rad_max <= rad_min: + raise ValueError("`rad_min` should be less than `rad_max`.") + if rad_min < 1: + raise ValueError("`rad_min` should be no less than 1.") + min_size = min(width, height) + if min_size <= 2 * rad_max: + raise ValueError("the minimal size of the image should be larger than `2 * rad_max`.") + image = np.zeros((width, height)) rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore for _ in range(num_objs): x = rs.randint(rad_max, width - rad_max) y = rs.randint(rad_max, height - rad_max) - rad = rs.randint(5, rad_max) + rad = rs.randint(rad_min, rad_max) spy, spx = np.ogrid[-x : width - x, -y : height - y] circle = (spx * spx + spy * spy) <= rad * rad @@ -86,6 +97,7 @@ def create_test_image_3d( depth: int, num_objs: int = 12, rad_max: int = 30, + rad_min: int = 5, noise_max: float = 0.0, num_seg_classes: int = 5, channel_dim: Optional[int] = None, @@ -95,11 +107,12 @@ def create_test_image_3d( Return a noisy 3D image and segmentation. Args: - height: height of the image. - width: width of the image. - depth: depth of the image. + height: height of the image. The value should be larger than `2 * rad_max`. + width: width of the image. The value should be larger than `2 * rad_max`. + depth: depth of the image. The value should be larger than `2 * rad_max`. num_objs: number of circles to generate. Defaults to `12`. rad_max: maximum circle radius. Defaults to `30`. + rad_min: minimum circle radius. Defaults to `5`. noise_max: if greater than 0 then noise will be added to the image taken from the uniform distribution on range `[0,noise_max)`. Defaults to `0`. num_seg_classes: number of classes for segmentations. Defaults to `5`. @@ -110,6 +123,15 @@ def create_test_image_3d( See also: :py:meth:`~create_test_image_2d` """ + + if rad_max <= rad_min: + raise ValueError("`rad_min` should be less than `rad_max`.") + if rad_min < 1: + raise ValueError("`rad_min` should be no less than 1.") + min_size = min(width, height, depth) + if min_size <= 2 * rad_max: + raise ValueError("the minimal size of the image should be larger than `2 * rad_max`.") + image = np.zeros((width, height, depth)) rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore @@ -117,7 +139,7 @@ def create_test_image_3d( x = rs.randint(rad_max, width - rad_max) y = rs.randint(rad_max, height - rad_max) z = rs.randint(rad_max, depth - rad_max) - rad = rs.randint(5, rad_max) + rad = rs.randint(rad_min, rad_max) spy, spx, spz = np.ogrid[-x : width - x, -y : height - y, -z : depth - z] circle = (spx * spx + spy * spy + spz * spz) <= rad * rad From a3f069587aa0eb9877eae79c72fe9b8364099c05 Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Sat, 17 Jul 2021 00:42:04 +0800 Subject: [PATCH 2/4] add unittest Signed-off-by: Yiheng Wang --- tests/test_synthetic.py | 91 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 tests/test_synthetic.py diff --git a/tests/test_synthetic.py b/tests/test_synthetic.py new file mode 100644 index 0000000000..97ab12a588 --- /dev/null +++ b/tests/test_synthetic.py @@ -0,0 +1,91 @@ +# 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.data import create_test_image_2d, create_test_image_3d +from monai.utils import set_determinism + +TEST_CASES = [ + [ + 2, + { + "width": 64, + "height": 64, + "rad_max": 10, + "rad_min": 4, + }, + 0.1479004, + 0.739502, + (64, 64), + 5, + ], + [ + 2, + { + "width": 32, + "height": 28, + "num_objs": 3, + "rad_max": 5, + "rad_min": 1, + "noise_max": 0.2, + }, + 0.1709315, + 0.4040179, + (32, 28), + 5, + ], + [ + 3, + { + "width": 64, + "height": 64, + "depth": 45, + "num_seg_classes": 3, + "channel_dim": -1, + "rad_max": 10, + "rad_min": 4, + }, + 0.025132, + 0.0753961, + (64, 64, 45, 1), + 3, + ], +] + + +class TestDiceCELoss(unittest.TestCase): + @parameterized.expand(TEST_CASES) + def test_create_test_image(self, dim, input_param, expected_img, expected_seg, expected_shape, expected_max_cls): + set_determinism(seed=0) + if dim == 2: + img, seg = create_test_image_2d(**input_param) + elif dim == 3: + img, seg = create_test_image_3d(**input_param) + self.assertEqual(img.shape, expected_shape) + self.assertEqual(seg.max(), expected_max_cls) + np.testing.assert_allclose(img.mean(), expected_img, atol=1e-7, rtol=1e-7) + np.testing.assert_allclose(seg.mean(), expected_seg, atol=1e-7, rtol=1e-7) + + def test_ill_radius(self): + with self.assertRaisesRegex(ValueError, ""): + img, seg = create_test_image_2d(32, 32, rad_max=20) + with self.assertRaisesRegex(ValueError, ""): + img, seg = create_test_image_3d(32, 32, 32, rad_max=10, rad_min=11) + with self.assertRaisesRegex(ValueError, ""): + img, seg = create_test_image_2d(32, 32, rad_max=10, rad_min=0) + + +if __name__ == "__main__": + unittest.main() From 05912209f252f9202cebf88f08a8bf2b821163ca Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Mon, 19 Jul 2021 21:46:24 +0800 Subject: [PATCH 3/4] add prameter keys Signed-off-by: Yiheng Wang --- monai/data/synthetic.py | 2 +- tests/utils.py | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index ee0b75166a..20a7829cab 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -30,7 +30,7 @@ def create_test_image_2d( random_state: Optional[np.random.RandomState] = None, ) -> Tuple[np.ndarray, np.ndarray]: """ - Return a noisy 2D image with `num_objs` circles and a 2D mask image. The maximum and minimum radiuses of the circles + Return a noisy 2D image with `num_objs` circles and a 2D mask image. The maximum and minimum radii of the circles are given as `rad_max` and `rad_min`. The mask will have `num_seg_classes` number of classes for segmentations labeled sequentially from 1, plus a background class represented as 0. If `noise_max` is greater than 0 then noise will be added to the image taken from the uniform distribution on range `[0,noise_max)`. If `channel_dim` is None, will create diff --git a/tests/utils.py b/tests/utils.py index 5970e65d9d..68ae8e4ec9 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -473,7 +473,9 @@ class NumpyImageTestCase2D(unittest.TestCase): num_classes = 3 def setUp(self): - im, msk = create_test_image_2d(self.im_shape[0], self.im_shape[1], 4, 20, 0, self.num_classes) + im, msk = create_test_image_2d( + self.im_shape[0], self.im_shape[1], num_objs=4, rad_max=20, noise_max=0.0, num_seg_classes=self.num_classes + ) self.imt = im[None, None] self.seg1 = (msk[None, None] > 0).astype(np.float32) @@ -495,7 +497,15 @@ class NumpyImageTestCase3D(unittest.TestCase): num_classes = 3 def setUp(self): - im, msk = create_test_image_3d(self.im_shape[0], self.im_shape[1], self.im_shape[2], 4, 20, 0, self.num_classes) + im, msk = create_test_image_3d( + self.im_shape[0], + self.im_shape[1], + self.im_shape[2], + num_objs=4, + rad_max=20, + noise_max=0.0, + num_seg_classes=self.num_classes, + ) self.imt = im[None, None] self.seg1 = (msk[None, None] > 0).astype(np.float32) From d0bba112a62f5add8a1b4c0ec4a85a247e8453ae Mon Sep 17 00:00:00 2001 From: Yiheng Wang Date: Mon, 19 Jul 2021 22:00:05 +0800 Subject: [PATCH 4/4] add parameter keys Signed-off-by: Yiheng Wang --- tests/test_gibbs_noise.py | 2 +- tests/test_gibbs_noised.py | 2 +- tests/test_k_space_spike_noise.py | 2 +- tests/test_k_space_spike_noised.py | 2 +- tests/test_rand_gibbs_noise.py | 2 +- tests/test_rand_gibbs_noised.py | 2 +- tests/test_rand_k_space_spike_noise.py | 2 +- tests/test_rand_k_space_spike_noised.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_gibbs_noise.py b/tests/test_gibbs_noise.py index c853d4686a..83cba56938 100644 --- a/tests/test_gibbs_noise.py +++ b/tests/test_gibbs_noise.py @@ -38,7 +38,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - im = create_test_image(*im_shape, 4, 20, 0, 5)[0][None] + im = create_test_image(*im_shape, num_objs=4, rad_max=20, noise_max=0.0, num_seg_classes=5)[0][None] return torch.Tensor(im) if as_tensor_input else im @parameterized.expand(TEST_CASES) diff --git a/tests/test_gibbs_noised.py b/tests/test_gibbs_noised.py index b0db79fe4f..0e02feb341 100644 --- a/tests/test_gibbs_noised.py +++ b/tests/test_gibbs_noised.py @@ -40,7 +40,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - ims = create_test_image(*im_shape, 4, 20, 0, 5) + ims = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5) ims = [torch.Tensor(im) for im in ims] if as_tensor_input else ims return {k: v for k, v in zip(KEYS, ims)} diff --git a/tests/test_k_space_spike_noise.py b/tests/test_k_space_spike_noise.py index 1b26d8b040..53661d5fcb 100644 --- a/tests/test_k_space_spike_noise.py +++ b/tests/test_k_space_spike_noise.py @@ -39,7 +39,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - im = create_test_image(*im_shape, 4, 20, 0, 5)[0][None] + im = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5)[0][None] return torch.Tensor(im) if as_tensor_input else im @parameterized.expand(TEST_CASES) diff --git a/tests/test_k_space_spike_noised.py b/tests/test_k_space_spike_noised.py index af0bba0152..e5d2dfb6f8 100644 --- a/tests/test_k_space_spike_noised.py +++ b/tests/test_k_space_spike_noised.py @@ -41,7 +41,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - ims = create_test_image(*im_shape, 4, 20, 0, 5) + ims = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5) ims = [im[None] for im in ims] ims = [torch.Tensor(im) for im in ims] if as_tensor_input else ims return {k: v for k, v in zip(KEYS, ims)} diff --git a/tests/test_rand_gibbs_noise.py b/tests/test_rand_gibbs_noise.py index ef2fe25eb4..94948c5a0d 100644 --- a/tests/test_rand_gibbs_noise.py +++ b/tests/test_rand_gibbs_noise.py @@ -38,7 +38,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - im = create_test_image(*im_shape, 4, 20, 0, 5)[0][None] + im = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5)[0][None] return torch.Tensor(im) if as_tensor_input else im @parameterized.expand(TEST_CASES) diff --git a/tests/test_rand_gibbs_noised.py b/tests/test_rand_gibbs_noised.py index 82ce220d89..986f4c02ae 100644 --- a/tests/test_rand_gibbs_noised.py +++ b/tests/test_rand_gibbs_noised.py @@ -40,7 +40,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - ims = create_test_image(*im_shape, 4, 20, 0, 5) + ims = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5) ims = [torch.Tensor(im) for im in ims] if as_tensor_input else ims return {k: v for k, v in zip(KEYS, ims)} diff --git a/tests/test_rand_k_space_spike_noise.py b/tests/test_rand_k_space_spike_noise.py index fb094c124f..ba9156c5b2 100644 --- a/tests/test_rand_k_space_spike_noise.py +++ b/tests/test_rand_k_space_spike_noise.py @@ -39,7 +39,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - im = create_test_image(*im_shape, 4, 20, 0, 5)[0][None] + im = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5)[0][None] return torch.Tensor(im) if as_tensor_input else im @parameterized.expand(TEST_CASES) diff --git a/tests/test_rand_k_space_spike_noised.py b/tests/test_rand_k_space_spike_noised.py index c245fe0afc..3cb49f1c08 100644 --- a/tests/test_rand_k_space_spike_noised.py +++ b/tests/test_rand_k_space_spike_noised.py @@ -40,7 +40,7 @@ def tearDown(self): @staticmethod def get_data(im_shape, as_tensor_input): create_test_image = create_test_image_2d if len(im_shape) == 2 else create_test_image_3d - ims = create_test_image(*im_shape, 4, 20, 0, 5) + ims = create_test_image(*im_shape, rad_max=20, noise_max=0.0, num_seg_classes=5) ims = [im[None] for im in ims] ims = [torch.Tensor(im) for im in ims] if as_tensor_input else ims return {k: v for k, v in zip(KEYS, ims)}