From 5690e24bba7b1cfdb8280c3a5cc606209e3aa90e Mon Sep 17 00:00:00 2001 From: 2260012471-eng <2260012471@qq.com> Date: Sun, 6 Sep 2026 13:31:06 +0800 Subject: [PATCH 1/6] Add instance ID support to synthetic test images --- monai/data/synthetic.py | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/monai/data/synthetic.py b/monai/data/synthetic.py index 97ed57ba7c3..58b62eaeb22 100644 --- a/monai/data/synthetic.py +++ b/monai/data/synthetic.py @@ -28,7 +28,8 @@ def create_test_image_2d( num_seg_classes: int = 5, channel_dim: int | None = None, random_state: np.random.RandomState | None = None, -) -> tuple[np.ndarray, np.ndarray]: + return_instance_id: bool = False, +) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, np.ndarray]: """ 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 @@ -48,9 +49,12 @@ def create_test_image_2d( channel_dim: if None, create an image without channel dimension, otherwise create 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`. + return_instance_id: if True, also return an instance ID mask where every generated + object is assigned a unique positive integer. Defaults to `False`. Returns: - Randomised Numpy array with shape (`height`, `width`) + A tuple of image and segmentation label arrays. If `return_instance_id=True`, also returns + an instance ID array as the third element. """ if rad_max <= rad_min: @@ -62,9 +66,10 @@ def create_test_image_2d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width)) + instance_ids = np.zeros((height, width), dtype=np.int32) rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore - for _ in range(num_objs): + for obj_id in range(1, num_objs + 1): x = rs.randint(rad_max, height - rad_max) y = rs.randint(rad_max, width - rad_max) rad = rs.randint(rad_min, rad_max) @@ -75,6 +80,7 @@ def create_test_image_2d( image[circle] = np.ceil(rs.random() * num_seg_classes) else: image[circle] = rs.random() * 0.5 + 0.5 + instance_ids[circle] = obj_id labels = np.ceil(image).astype(np.int32, copy=False) @@ -87,10 +93,14 @@ def create_test_image_2d( if channel_dim == 0: noisyimage = noisyimage[None] labels = labels[None] + instance_ids = instance_ids[None] else: noisyimage = noisyimage[..., None] labels = labels[..., None] + instance_ids = instance_ids[..., None] + if return_instance_id: + return noisyimage, labels, instance_ids return noisyimage, labels @@ -105,7 +115,8 @@ def create_test_image_3d( num_seg_classes: int = 5, channel_dim: int | None = None, random_state: np.random.RandomState | None = None, -) -> tuple[np.ndarray, np.ndarray]: + return_instance_id: bool = False, +) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, np.ndarray]: """ Return a noisy 3D image and segmentation. @@ -122,9 +133,12 @@ def create_test_image_3d( channel_dim: if None, create an image without channel dimension, otherwise create 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`. + return_instance_id: if True, also return an instance ID mask where every generated + object is assigned a unique positive integer. Defaults to `False`. Returns: - Randomised Numpy array with shape (`height`, `width`, `depth`) + A tuple of image and segmentation label arrays. If `return_instance_id=True`, also returns + an instance ID array as the third element. See also: :py:meth:`~create_test_image_2d` @@ -139,9 +153,10 @@ def create_test_image_3d( raise ValueError(f"the minimal size {min_size} of the image should be larger than `2 * rad_max` 2x{rad_max}.") image = np.zeros((height, width, depth)) + instance_ids = np.zeros((height, width, depth), dtype=np.int32) rs: np.random.RandomState = np.random.random.__self__ if random_state is None else random_state # type: ignore - for _ in range(num_objs): + for obj_id in range(1, num_objs + 1): x = rs.randint(rad_max, height - rad_max) y = rs.randint(rad_max, width - rad_max) z = rs.randint(rad_max, depth - rad_max) @@ -153,6 +168,7 @@ def create_test_image_3d( image[circle] = np.ceil(rs.random() * num_seg_classes) else: image[circle] = rs.random() * 0.5 + 0.5 + instance_ids[circle] = obj_id labels = np.ceil(image).astype(np.int32, copy=False) @@ -162,8 +178,15 @@ def create_test_image_3d( if channel_dim is not None: if not (isinstance(channel_dim, int) and channel_dim in (-1, 0, 3)): raise AssertionError("invalid channel dim.") - noisyimage, labels = ( - (noisyimage[None], labels[None]) if channel_dim == 0 else (noisyimage[..., None], labels[..., None]) - ) + if channel_dim == 0: + noisyimage = noisyimage[None] + labels = labels[None] + instance_ids = instance_ids[None] + else: + noisyimage = noisyimage[..., None] + labels = labels[..., None] + instance_ids = instance_ids[..., None] + if return_instance_id: + return noisyimage, labels, instance_ids return noisyimage, labels From 9e246770725493035da53180481f8eb4bc8b5ebe Mon Sep 17 00:00:00 2001 From: 2260012471-eng <2260012471@qq.com> Date: Sun, 6 Sep 2026 13:31:20 +0800 Subject: [PATCH 2/6] Test synthetic image instance IDs --- tests/data/test_synthetic.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/data/test_synthetic.py b/tests/data/test_synthetic.py index 4ab21445680..2677d811032 100644 --- a/tests/data/test_synthetic.py +++ b/tests/data/test_synthetic.py @@ -39,6 +39,22 @@ ], ] +INSTANCE_ID_CASES = [ + [2, {"width": 64, "height": 64, "num_objs": 5, "rad_max": 10, "rad_min": 4}], + [ + 3, + { + "width": 40, + "height": 40, + "depth": 40, + "num_objs": 4, + "rad_max": 8, + "rad_min": 3, + "channel_dim": -1, + }, + ], +] + class TestDiceCELoss(unittest.TestCase): @@ -54,6 +70,23 @@ def test_create_test_image(self, dim, input_param, expected_img, expected_seg, e 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) + @parameterized.expand(INSTANCE_ID_CASES) + def test_return_instance_id(self, dim, input_param): + set_determinism(seed=0) + if dim == 2: + img, seg, instance_ids = create_test_image_2d(**input_param, return_instance_id=True) + else: # dim == 3 + img, seg, instance_ids = create_test_image_3d(**input_param, return_instance_id=True) + + self.assertEqual(instance_ids.shape, seg.shape) + self.assertEqual(instance_ids.dtype, np.int32) + self.assertLessEqual(instance_ids.max(), input_param["num_objs"]) + np.testing.assert_array_equal(instance_ids > 0, seg > 0) + + def test_return_instance_id_default_false(self): + self.assertEqual(len(create_test_image_2d(32, 32, rad_max=5)), 2) + self.assertEqual(len(create_test_image_3d(32, 32, 32, rad_max=5)), 2) + def test_ill_radius(self): with self.assertRaisesRegex(ValueError, ""): img, seg = create_test_image_2d(32, 32, rad_max=20) From 536d23c65cabe9ad213d9078f1dd5c80c57222e4 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Sep 2026 05:40:36 +0000 Subject: [PATCH 3/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/data/test_synthetic.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/data/test_synthetic.py b/tests/data/test_synthetic.py index 2677d811032..0e0b90af5f5 100644 --- a/tests/data/test_synthetic.py +++ b/tests/data/test_synthetic.py @@ -41,18 +41,7 @@ INSTANCE_ID_CASES = [ [2, {"width": 64, "height": 64, "num_objs": 5, "rad_max": 10, "rad_min": 4}], - [ - 3, - { - "width": 40, - "height": 40, - "depth": 40, - "num_objs": 4, - "rad_max": 8, - "rad_min": 3, - "channel_dim": -1, - }, - ], + [3, {"width": 40, "height": 40, "depth": 40, "num_objs": 4, "rad_max": 8, "rad_min": 3, "channel_dim": -1}], ] From 6531a7679ef0f06bb4e115fbcbbae9539100f521 Mon Sep 17 00:00:00 2001 From: 2260012471-eng <2260012471@qq.com> Date: Sun, 6 Sep 2026 13:59:29 +0800 Subject: [PATCH 4/6] DCO Remediation Commit for 2260012471-eng <2260012471@qq.com> I, 2260012471-eng <2260012471@qq.com>, hereby add my Signed-off-by to this commit: 5690e24bba7b1cfdb8280c3a5cc606209e3aa90e I, 2260012471-eng <2260012471@qq.com>, hereby add my Signed-off-by to this commit: 9e246770725493035da53180481f8eb4bc8b5ebe From ad2fef9ac15770299d8c6d2699bc80d392a1be8b Mon Sep 17 00:00:00 2001 From: 2260012471-eng <2260012471@qq.com> Date: Sun, 6 Sep 2026 14:01:03 +0800 Subject: [PATCH 5/6] DCO Remediation Commit for 2260012471-eng <2260012471@qq.com> I, 2260012471-eng <2260012471@qq.com>, hereby add my Signed-off-by to this commit: 6531a7679ef0f06bb4e115fbcbbae9539100f521 Signed-off-by: 2260012471-eng <2260012471@qq.com> From a048fb30bf6141fc2e1f57446bd50d88ab17087b Mon Sep 17 00:00:00 2001 From: 2260012471-eng <2260012471@qq.com> Date: Sun, 6 Sep 2026 20:26:46 +0800 Subject: [PATCH 6/6] test: document synthetic image tests and fix radius inputs --- tests/data/test_synthetic.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/data/test_synthetic.py b/tests/data/test_synthetic.py index 0e0b90af5f5..7dd9bee03c2 100644 --- a/tests/data/test_synthetic.py +++ b/tests/data/test_synthetic.py @@ -49,6 +49,16 @@ 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): + """Verify synthetic image shapes, label classes, and deterministic means. + + Args: + dim: Spatial dimensionality of the generator. + input_param: Keyword arguments passed to the generator. + expected_img: Expected mean image intensity. + expected_seg: Expected mean segmentation label. + expected_shape: Expected image shape. + expected_max_cls: Expected maximum segmentation class. + """ set_determinism(seed=0) if dim == 2: img, seg = create_test_image_2d(**input_param) @@ -61,6 +71,12 @@ def test_create_test_image(self, dim, input_param, expected_img, expected_seg, e @parameterized.expand(INSTANCE_ID_CASES) def test_return_instance_id(self, dim, input_param): + """Verify instance mask shape, dtype, ID bounds, and foreground alignment. + + Args: + dim: Spatial dimensionality of the generator. + input_param: Keyword arguments passed to the generator. + """ set_determinism(seed=0) if dim == 2: img, seg, instance_ids = create_test_image_2d(**input_param, return_instance_id=True) @@ -73,10 +89,12 @@ def test_return_instance_id(self, dim, input_param): np.testing.assert_array_equal(instance_ids > 0, seg > 0) def test_return_instance_id_default_false(self): - self.assertEqual(len(create_test_image_2d(32, 32, rad_max=5)), 2) - self.assertEqual(len(create_test_image_3d(32, 32, 32, rad_max=5)), 2) + """Verify both generators return two arrays when instance IDs are not requested.""" + self.assertEqual(len(create_test_image_2d(32, 32, rad_max=5, rad_min=1)), 2) + self.assertEqual(len(create_test_image_3d(32, 32, 32, rad_max=5, rad_min=1)), 2) def test_ill_radius(self): + """Verify invalid radius bounds and image sizes raise ValueError.""" with self.assertRaisesRegex(ValueError, ""): img, seg = create_test_image_2d(32, 32, rad_max=20) with self.assertRaisesRegex(ValueError, ""):