From 4cb5df9b1f78afd17c28555bdb59348be9ffee85 Mon Sep 17 00:00:00 2001 From: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:42:41 -0700 Subject: [PATCH 1/2] Fix NormalizeIntensity inverse for non-contiguous inputs I found that nonzero intensity normalization could not be inverted for transposed images because its restoration mask was not contiguous. I make the mask contiguous before updating its flattened indices and add round-trip tests for both transform variants and channel-wise modes. Assisted-by: Claude Assisted-by: GPT-6 Astra Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> --- monai/transforms/intensity/array.py | 2 +- tests/transforms/test_normalize_intensity.py | 13 +++++++++++++ tests/transforms/test_normalize_intensityd.py | 13 +++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/monai/transforms/intensity/array.py b/monai/transforms/intensity/array.py index a80f94727e..d7b405d096 100644 --- a/monai/transforms/intensity/array.py +++ b/monai/transforms/intensity/array.py @@ -1055,7 +1055,7 @@ def _restore(x, sub, div, zeroed_idx=None): div, *_ = convert_to_dst_type(div, x) return x * div + sub # nonzero=True: the forward mask is the output's non-zero voxels plus the recorded zeroed ones - mask = x != 0 + mask = (x != 0).contiguous() zeroed_idx, *_ = convert_to_dst_type(zeroed_idx, mask, dtype=torch.long) mask.view(-1)[zeroed_idx] = True vals = x[mask] diff --git a/tests/transforms/test_normalize_intensity.py b/tests/transforms/test_normalize_intensity.py index 53fea19d33..6586a98974 100644 --- a/tests/transforms/test_normalize_intensity.py +++ b/tests/transforms/test_normalize_intensity.py @@ -176,6 +176,19 @@ def test_inverse_nonzero_value_equal_to_mean(self, _, args): inv = normalizer.inverse(out) assert_allclose(inv, img, type_test=False, rtol=0, atol=0) + @parameterized.expand([[False], [True]]) + def test_inverse_nonzero_noncontiguous(self, channel_wise): + """Restore transposed inputs, including non-zero voxels normalized to zero.""" + self.addCleanup(set_track_meta, get_track_meta()) + set_track_meta(True) + img = MetaTensor(torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 0.0], [0.0, 2.0]]])).transpose(1, 2) + normalizer = NormalizeIntensity(nonzero=True, channel_wise=channel_wise) + out = normalizer(img.clone()) + self.assertFalse(out.is_contiguous()) + inv = normalizer.inverse(out) + assert_allclose(inv, img, type_test=False, rtol=1e-6, atol=1e-6) + self.assertEqual(len(inv.applied_operations), 0) + def test_inverse_nonzero_in_compose(self): self.addCleanup(set_track_meta, get_track_meta()) set_track_meta(True) diff --git a/tests/transforms/test_normalize_intensityd.py b/tests/transforms/test_normalize_intensityd.py index 50a9c302db..2a186394d5 100644 --- a/tests/transforms/test_normalize_intensityd.py +++ b/tests/transforms/test_normalize_intensityd.py @@ -98,6 +98,19 @@ def test_inverse(self, _, args): inv = normalizer.inverse(out) assert_allclose(inv[key], original, type_test=False, rtol=1e-4, atol=1e-4) + @parameterized.expand([[False], [True]]) + def test_inverse_nonzero_noncontiguous(self, channel_wise): + """Restore transposed dictionary values without losing their zeroed voxels.""" + self.addCleanup(set_track_meta, get_track_meta()) + set_track_meta(True) + img = MetaTensor(torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 0.0], [0.0, 2.0]]])).transpose(1, 2) + normalizer = NormalizeIntensityd(keys="img", nonzero=True, channel_wise=channel_wise) + out = normalizer({"img": img.clone()}) + self.assertFalse(out["img"].is_contiguous()) + inv = normalizer.inverse(out) + assert_allclose(inv["img"], img, type_test=False, rtol=1e-6, atol=1e-6) + self.assertEqual(len(inv["img"].applied_operations), 0) + def test_invertd_nonzero(self): self.addCleanup(set_track_meta, get_track_meta()) set_track_meta(True) From 2e4d272e90cb73872d42f1c188d2c330b88a06c5 Mon Sep 17 00:00:00 2001 From: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> Date: Sat, 17 Oct 2026 02:55:46 -0700 Subject: [PATCH 2/2] Document channel_wise in the new inverse tests Assisted-by: Claude Assisted-by: GPT-6 Astra Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com> --- tests/transforms/test_normalize_intensity.py | 6 +++++- tests/transforms/test_normalize_intensityd.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/transforms/test_normalize_intensity.py b/tests/transforms/test_normalize_intensity.py index 6586a98974..49d9491ac0 100644 --- a/tests/transforms/test_normalize_intensity.py +++ b/tests/transforms/test_normalize_intensity.py @@ -178,7 +178,11 @@ def test_inverse_nonzero_value_equal_to_mean(self, _, args): @parameterized.expand([[False], [True]]) def test_inverse_nonzero_noncontiguous(self, channel_wise): - """Restore transposed inputs, including non-zero voxels normalized to zero.""" + """Restore transposed inputs, including non-zero voxels normalized to zero. + + Args: + channel_wise: whether each channel is normalized independently. + """ self.addCleanup(set_track_meta, get_track_meta()) set_track_meta(True) img = MetaTensor(torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 0.0], [0.0, 2.0]]])).transpose(1, 2) diff --git a/tests/transforms/test_normalize_intensityd.py b/tests/transforms/test_normalize_intensityd.py index 2a186394d5..992f8db67c 100644 --- a/tests/transforms/test_normalize_intensityd.py +++ b/tests/transforms/test_normalize_intensityd.py @@ -100,7 +100,11 @@ def test_inverse(self, _, args): @parameterized.expand([[False], [True]]) def test_inverse_nonzero_noncontiguous(self, channel_wise): - """Restore transposed dictionary values without losing their zeroed voxels.""" + """Restore transposed dictionary values without losing their zeroed voxels. + + Args: + channel_wise: whether each channel is normalized independently. + """ self.addCleanup(set_track_meta, get_track_meta()) set_track_meta(True) img = MetaTensor(torch.tensor([[[0.0, 1.0], [2.0, 3.0]], [[0.0, 0.0], [0.0, 2.0]]])).transpose(1, 2)