From 43b3e5c2e18bd9c5d9821ee48094523125b412fc Mon Sep 17 00:00:00 2001 From: "Patel, Nilaykumar K" Date: Mon, 28 Sep 2026 02:24:10 -0400 Subject: [PATCH 1/2] fix(handlers): coerce probability-map locations to int for numpy 2.x `SlidingPatchWSIDataset` builds `ProbMapKeys.LOCATION` with `np.round(...)` and never casts back to int, so `ProbMapProducer` indexes the probability map with floats and numpy 2.x raises `IndexError: only integers ... are valid indices`. Cast at the source so the metadata matches `MaskedPatchWSIDataset`, which already calls `.astype(int)`, and keep a defensive coercion in the handler for datasets that still supply floats. The existing tests only supply integer locations; add a float-location variant of each, which reproduces the error without the handler change. Signed-off-by: Patel, Nilaykumar K --- monai/data/wsi_datasets.py | 2 +- monai/handlers/probability_maps.py | 4 ++- .../test_handler_prob_map_producer.py | 28 ++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py index b1830018358..76ace77fc29 100644 --- a/monai/data/wsi_datasets.py +++ b/monai/data/wsi_datasets.py @@ -306,7 +306,7 @@ def _evaluate_patch_locations(self, sample): ) ) # convert locations to mask_location - mask_locations = np.round((patch_locations + patch_size_0 // 2) / float(mask_ratio)) + mask_locations = np.round((patch_locations + patch_size_0 // 2) / float(mask_ratio)).astype(int) # fill out samples with location and metadata sample[WSIPatchKeys.SIZE.value] = patch_size diff --git a/monai/handlers/probability_maps.py b/monai/handlers/probability_maps.py index e21bd199f8e..7311734b930 100644 --- a/monai/handlers/probability_maps.py +++ b/monai/handlers/probability_maps.py @@ -108,7 +108,9 @@ def __call__(self, engine: Engine) -> None: locs = engine.state.batch[CommonKeys.IMAGE].meta[ProbMapKeys.LOCATION] probs = engine.state.output[self.prob_key] for name, loc, prob in zip(names, locs, probs): - self.prob_map[name][tuple(loc)] = prob + # `SlidingPatchWSIDataset` derives these locations with `np.round` and never casts + # back to int, and numpy 2.x rejects non-integer array indices. + self.prob_map[name][tuple(int(i) for i in loc)] = prob with self.lock: self.counter[name] -= 1 if self.counter[name] == 0: diff --git a/tests/handlers/test_handler_prob_map_producer.py b/tests/handlers/test_handler_prob_map_producer.py index 063f7984cb8..89fc317923e 100644 --- a/tests/handlers/test_handler_prob_map_producer.py +++ b/tests/handlers/test_handler_prob_map_producer.py @@ -65,6 +65,21 @@ def __getitem__(self, index): return {"image": MetaTensor(x=image, meta=metadata), "pred": index + 1} +class FloatLocationDataset(TestDataset): + """A dataset whose probability-map locations are floats. + + `SlidingPatchWSIDataset` builds them with `np.round(...)` and never casts back to int, + so this is what the handler actually receives in the sliding-window WSI pipeline. + """ + + __test__ = False # indicate to pytest that this class is not intended for collection + + def __init__(self, name, size): + super().__init__(name, size) + for sample in self.data: + sample[ProbMapKeys.LOCATION.value] = sample[ProbMapKeys.LOCATION.value].astype(float) + + class TestEvaluator(Evaluator): __test__ = False # indicate to pytest that this class is not intended for collection @@ -73,10 +88,7 @@ def _iteration(self, engine, batchdata): class TestHandlerProbMapGenerator(unittest.TestCase): - @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2]) - def test_prob_map_generator(self, name, size): - # set up dataset - dataset = TestDataset(name, size) + def run_and_check(self, dataset, name, size): batch_size = 2 data_loader = DataLoader(dataset, batch_size=batch_size) @@ -106,6 +118,14 @@ def inference(engine, batch): self.assertListEqual(np.vstack(prob_map.nonzero()).T.tolist(), [[i, i + 1] for i in range(size)]) self.assertListEqual(prob_map[prob_map.nonzero()].tolist(), [i + 1 for i in range(size)]) + @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2]) + def test_prob_map_generator(self, name, size): + self.run_and_check(TestDataset(name, size), name, size) + + @parameterized.expand([TEST_CASE_0, TEST_CASE_1, TEST_CASE_2]) + def test_prob_map_generator_float_locations(self, name, size): + self.run_and_check(FloatLocationDataset(name, size), name, size) + if __name__ == "__main__": unittest.main() From 7190b6f7714bec5e219e1b35f7ac055817957e90 Mon Sep 17 00:00:00 2001 From: "Patel, Nilaykumar K" Date: Tue, 29 Sep 2026 00:21:15 -0400 Subject: [PATCH 2/2] tests(data): assert integer location dtype and clarify comments Add a regression assertion for the `ProbMapKeys.LOCATION` dtype emitted by `SlidingPatchWSIDataset`. The float-location handler test cannot catch a regression here because the handler coerces each coordinate before indexing. Reword the handler comment and the test fixture docstring, which described the sliding dataset as emitting floats; that is no longer true after the cast. Addresses review feedback on #9136. Signed-off-by: Patel, Nilaykumar K --- monai/handlers/probability_maps.py | 4 ++-- tests/data/test_sliding_patch_wsi_dataset.py | 5 ++++- tests/handlers/test_handler_prob_map_producer.py | 6 +++--- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/monai/handlers/probability_maps.py b/monai/handlers/probability_maps.py index 7311734b930..eb0a51c6147 100644 --- a/monai/handlers/probability_maps.py +++ b/monai/handlers/probability_maps.py @@ -108,8 +108,8 @@ def __call__(self, engine: Engine) -> None: locs = engine.state.batch[CommonKeys.IMAGE].meta[ProbMapKeys.LOCATION] probs = engine.state.output[self.prob_key] for name, loc, prob in zip(names, locs, probs): - # `SlidingPatchWSIDataset` derives these locations with `np.round` and never casts - # back to int, and numpy 2.x rejects non-integer array indices. + # numpy 2.x rejects non-integer array indices, so coerce here for datasets that + # still supply float locations. The bundled WSI datasets already emit integers. self.prob_map[name][tuple(int(i) for i in loc)] = prob with self.lock: self.counter[name] -= 1 diff --git a/tests/data/test_sliding_patch_wsi_dataset.py b/tests/data/test_sliding_patch_wsi_dataset.py index 8e27f7ad0e4..aab390d298a 100644 --- a/tests/data/test_sliding_patch_wsi_dataset.py +++ b/tests/data/test_sliding_patch_wsi_dataset.py @@ -21,7 +21,7 @@ from parameterized import parameterized from monai.data import SlidingPatchWSIDataset -from monai.utils import WSIPatchKeys, optional_import, set_determinism +from monai.utils import ProbMapKeys, WSIPatchKeys, optional_import, set_determinism from tests.test_utils import download_url_or_skip_test, testing_data_config set_determinism(0) @@ -250,6 +250,9 @@ def test_read_patches_large(self, input_parameters, expected): steps = [round(expected[i]["ratio"] * s) for s in expected[i]["patch_size"]] expected_location = tuple(expected[i]["step_loc"][j] * steps[j] for j in range(len(steps))) assert_array_equal(sample["image"].meta[WSIPatchKeys.LOCATION], expected_location) + # `ProbMapProducer` uses these as probability-map indices, which numpy 2.x + # only accepts as integers. + self.assertTrue(np.issubdtype(sample["image"].meta[ProbMapKeys.LOCATION].dtype, np.integer)) @skipUnless(has_cucim, "Requires cucim") diff --git a/tests/handlers/test_handler_prob_map_producer.py b/tests/handlers/test_handler_prob_map_producer.py index 89fc317923e..cb7dbb8a94b 100644 --- a/tests/handlers/test_handler_prob_map_producer.py +++ b/tests/handlers/test_handler_prob_map_producer.py @@ -66,10 +66,10 @@ def __getitem__(self, index): class FloatLocationDataset(TestDataset): - """A dataset whose probability-map locations are floats. + """A test fixture whose probability-map locations are floats. - `SlidingPatchWSIDataset` builds them with `np.round(...)` and never casts back to int, - so this is what the handler actually receives in the sliding-window WSI pipeline. + The bundled WSI datasets emit integer locations, so this stands in for a third-party + dataset that does not, which the handler still has to index the probability map with. """ __test__ = False # indicate to pytest that this class is not intended for collection