diff --git a/monai/data/transforms/intensity_normalizer.py b/monai/data/transforms/intensity_normalizer.py new file mode 100644 index 0000000000..953498ab3d --- /dev/null +++ b/monai/data/transforms/intensity_normalizer.py @@ -0,0 +1,49 @@ +# Copyright 2020 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 numpy as np +import monai + +export = monai.utils.export("monai.data.transforms") + + +@export +class IntensityNormalizer: + """Normalize input based on provided args, using calculated mean and std if not provided + (shape of subtrahend and divisor must match. if 0, entire volume uses same subtrahend and + divisor, otherwise the shape can have dimension 1 for channels). + Current implementation can only support 'channel_last' format data. + + Args: + subtrahend (ndarray): the amount to subtract by (usually the mean) + divisor (ndarray): the amount to divide by (usually the standard deviation) + dtype: output data format + """ + + def __init__(self, subtrahend=None, divisor=None, dtype=np.float32): + if subtrahend is not None or divisor is not None: + assert isinstance(subtrahend, np.ndarray) and isinstance(divisor, np.ndarray), \ + 'subtrahend and divisor must be set in pair and in numpy array.' + self.subtrahend = subtrahend + self.divisor = divisor + self.dtype = dtype + + def __call__(self, img): + if self.subtrahend is not None and self.divisor is not None: + img -= self.subtrahend + img /= self.divisor + else: + img -= np.mean(img) + img /= np.std(img) + + if self.dtype != img.dtype: + img = img.astype(self.dtype) + return img diff --git a/tests/test_convolutions.py b/tests/test_convolutions.py index 14b189ccdd..70644c8a9a 100644 --- a/tests/test_convolutions.py +++ b/tests/test_convolutions.py @@ -9,13 +9,12 @@ # See the License for the specific language governing permissions and # limitations under the License. - -from .utils import ImageTestCase +from .utils import TorchImageTestCase2D from monai.networks.layers.convolutions import Convolution, ResidualUnit -class TestConvolution2D(ImageTestCase): +class TestConvolution2D(TorchImageTestCase2D): def test_conv1(self): conv = Convolution(2, self.input_channels, self.output_channels) out = conv(self.imt) @@ -59,7 +58,7 @@ def test_transpose2(self): self.assertEqual(out.shape, expected_shape) -class TestResidualUnit2D(ImageTestCase): +class TestResidualUnit2D(TorchImageTestCase2D): def test_conv_only1(self): conv = ResidualUnit(2, 1, self.output_channels) out = conv(self.imt) diff --git a/tests/test_intensity_normalizer.py b/tests/test_intensity_normalizer.py new file mode 100644 index 0000000000..f8c09b53bd --- /dev/null +++ b/tests/test_intensity_normalizer.py @@ -0,0 +1,30 @@ +# Copyright 2020 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 monai.data.transforms.intensity_normalizer import IntensityNormalizer +from tests.utils import NumpyImageTestCase2D + + +class IntensityNormTestCase(NumpyImageTestCase2D): + + def test_image_normalizer_default(self): + normalizer = IntensityNormalizer() + normalised = normalizer(self.imt) + expected = (self.imt - np.mean(self.imt)) / np.std(self.imt) + self.assertTrue(np.allclose(normalised, expected)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/utils.py b/tests/utils.py index f780220b77..c7f55c7d2b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -9,11 +9,11 @@ # See the License for the specific language governing permissions and # limitations under the License. - import os import unittest -import torch + import numpy as np +import torch from monai.utils.arrayutils import rescale_array @@ -55,7 +55,7 @@ def create_test_image(width, height, num_objs=12, rad_max=30, noise_max=0.0, num return noisyimage, labels -class ImageTestCase(unittest.TestCase): +class NumpyImageTestCase2D(unittest.TestCase): im_shape = (128, 128) input_channels = 1 output_channels = 4 @@ -64,7 +64,15 @@ class ImageTestCase(unittest.TestCase): def setUp(self): im, msk = create_test_image(self.im_shape[0], self.im_shape[1], 4, 20, 0, self.num_classes) - self.imt = torch.tensor(im[None, None]) + self.imt = im[None, None] + self.seg1 = (msk[None, None] > 0).astype(np.float32) + self.segn = msk[None, None] - self.seg1 = torch.tensor((msk[None, None] > 0).astype(np.float32)) - self.segn = torch.tensor(msk[None, None]) + +class TorchImageTestCase2D(NumpyImageTestCase2D): + + def setUp(self): + NumpyImageTestCase2D.setUp(self) + self.imt = torch.tensor(self.imt) + self.seg1 = torch.tensor(self.seg1) + self.segn = torch.tensor(self.segn)