Skip to content

Commit 08dedf9

Browse files
Fix/intensity remap range (#9155)
IntensityRemap is supposed to keep an images intensities in the same [min, max] range but it rescaled with * max + min so the output ended up in [min, min + max] instead. For example a CT scan in [-1000, 3000] came out in [-1000, 2000] and a image in [10, 20] came out in [10, 30]. The fix is one line, multiply by (max - min) instead and a second commit makes RandIntensityRemap use its own seeded generator instead of numpys global one so setting the seed actually makes the output repeatable. These are the first tests these two transforms have ever had, without the fixes 6 of 9 fail and with them all 9 pass. --------- Signed-off-by: devteamaegis <devteam.aegis@gmail.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 76015b5 commit 08dedf9

2 files changed

Lines changed: 76 additions & 8 deletions

File tree

‎monai/transforms/intensity/array.py‎

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2740,7 +2740,7 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor:
27402740
grid = torch.arange(len(noise)) / len(noise)
27412741
noise += self.slope * grid
27422742
# rescale
2743-
noise = (noise - noise.min()) / (noise.max() - noise.min()) * img_.max() + img_.min()
2743+
noise = (noise - noise.min()) / (noise.max() - noise.min()) * (img_.max() - img_.min()) + img_.min()
27442744

27452745
# intensity remapping function
27462746
index_img = torch.bucketize(img_, torch.tensor(vals_to_sample))
@@ -2787,17 +2787,18 @@ def __call__(self, img: torch.Tensor) -> torch.Tensor:
27872787
img = convert_to_tensor(img, track_meta=get_track_meta())
27882788
if self._do_transform:
27892789
if self.channel_wise:
2790-
img = torch.stack(
2791-
[
2792-
IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img[i])
2793-
for i in range(len(img))
2794-
]
2795-
)
2790+
img = torch.stack([self._remap(img[i]) for i in range(len(img))])
27962791
else:
2797-
img = IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))(img)
2792+
img = self._remap(img)
27982793

27992794
return img
28002795

2796+
def _remap(self, img: torch.Tensor) -> torch.Tensor:
2797+
# share this transform's random state so that `set_random_state` also controls the remapping curve
2798+
remap = IntensityRemap(self.kernel_size, self.R.choice([-self.slope, self.slope]))
2799+
remap.set_random_state(state=self.R)
2800+
return remap(img)
2801+
28012802

28022803
class ForegroundMask(Transform):
28032804
"""
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Copyright (c) MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
from __future__ import annotations
13+
14+
import unittest
15+
16+
import torch
17+
from parameterized import parameterized
18+
19+
from monai.transforms import IntensityRemap, RandIntensityRemap
20+
from tests.test_utils import assert_allclose
21+
22+
# input ranges with a negative, zero and positive minimum
23+
TEST_RANGES = [(-1000.0, 3000.0), (0.0, 255.0), (10.0, 20.0)]
24+
25+
26+
class TestIntensityRemap(unittest.TestCase):
27+
@parameterized.expand(TEST_RANGES)
28+
def test_output_range(self, low, high):
29+
img = torch.linspace(low, high, 500).reshape(1, 10, 50)
30+
remap = IntensityRemap(kernel_size=10, slope=0.7)
31+
remap.set_random_state(seed=0)
32+
result = remap(img)
33+
self.assertEqual(result.shape, img.shape)
34+
assert_allclose(result.min(), low, type_test=False, atol=1e-3, rtol=1e-5)
35+
assert_allclose(result.max(), high, type_test=False, atol=1e-3, rtol=1e-5)
36+
37+
38+
class TestRandIntensityRemap(unittest.TestCase):
39+
@parameterized.expand(TEST_RANGES)
40+
def test_output_range(self, low, high):
41+
img = torch.stack([torch.linspace(low, high, 500).reshape(10, 50)] * 2)
42+
remap = RandIntensityRemap(prob=1.0, kernel_size=10, channel_wise=True)
43+
remap.set_random_state(seed=0)
44+
result = remap(img)
45+
self.assertEqual(result.shape, img.shape)
46+
for c in range(img.shape[0]):
47+
assert_allclose(result[c].min(), low, type_test=False, atol=1e-3, rtol=1e-5)
48+
assert_allclose(result[c].max(), high, type_test=False, atol=1e-3, rtol=1e-5)
49+
50+
@parameterized.expand([[True], [False]])
51+
def test_set_random_state(self, channel_wise):
52+
img = torch.stack([torch.linspace(0.0, 100.0, 500).reshape(10, 50)] * 2)
53+
results = []
54+
for _ in range(2):
55+
remap = RandIntensityRemap(prob=1.0, kernel_size=10, channel_wise=channel_wise)
56+
remap.set_random_state(seed=0)
57+
results.append(remap(img))
58+
assert_allclose(results[0], results[1], type_test=False)
59+
60+
def test_prob_zero(self):
61+
img = torch.linspace(-1.0, 1.0, 500).reshape(1, 10, 50)
62+
remap = RandIntensityRemap(prob=0.0)
63+
assert_allclose(remap(img), img, type_test=False)
64+
65+
66+
if __name__ == "__main__":
67+
unittest.main()

0 commit comments

Comments
 (0)