From 4a0f50e4cf326a7d391a0d3007599b63fafc51e7 Mon Sep 17 00:00:00 2001 From: Egor Panfilov Date: Sun, 16 Feb 2020 15:59:42 +0200 Subject: [PATCH 1/4] Added IntensityRemap --- setup.py | 2 +- solt/transforms/__init__.py | 2 ++ solt/transforms/_transforms.py | 51 ++++++++++++++++++++++++++++++++++ tests/test_transforms.py | 44 +++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f10509..53df9c9 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import find_packages, setup -requirements = ("numpy", "opencv-python-headless", "torch", "torchvision", "pyyaml") +requirements = ("numpy", "scipy", "opencv-python-headless", "torch", "torchvision", "pyyaml") setup_requirements = () diff --git a/solt/transforms/__init__.py b/solt/transforms/__init__.py index ab1e392..b0ac892 100644 --- a/solt/transforms/__init__.py +++ b/solt/transforms/__init__.py @@ -12,6 +12,7 @@ SaltAndPepper, Blur, HSV, + IntensityRemap, CvtColor, Resize, Contrast, @@ -36,6 +37,7 @@ "SaltAndPepper", "Blur", "HSV", + "IntensityRemap", "CvtColor", "Resize", "Contrast", diff --git a/solt/transforms/_transforms.py b/solt/transforms/_transforms.py index 189e8cc..76047d0 100644 --- a/solt/transforms/_transforms.py +++ b/solt/transforms/_transforms.py @@ -2,6 +2,8 @@ import cv2 import numpy as np +import scipy +import scipy.signal from ..core import ( BaseTransform, @@ -1212,6 +1214,55 @@ def _apply_img(self, img: np.ndarray, settings: dict): return cv2.LUT(img, self.state_dict["LUT"]) +class IntensityRemap(ImageTransform): + """Performs random intensity remapping. + + Parameters + ---------- + p : float + Probability of applying this transform, + kernel_size: int + Size of medial filter kernel used during the generation of intensity mapping. + Higher value yield more monotonic mapping. + data_indices : tuple or None + Indices of the images within the data container to which this transform needs to be applied. + Every element within the tuple must be integer numbers. + If None, then the transform will be applied to all the images withing the DataContainer. + + References + ---------- + .. [1] Hesse, L. S., Kuling, G., Veta, M., & Martel, A. L. (2019). + Intensity augmentation for domain transfer of whole breast + segmentation in MRI. https://arxiv.org/abs/1909.02642 + """ + + serializable_name = "intensity_remap" + """How the class should be stored in the registry""" + + def __init__(self, kernel_size=9, data_indices=None, p=0.5): + super(IntensityRemap, self).__init__(p=p, data_indices=data_indices) + self.kernel_size = kernel_size + + def sample_transform(self, data): + m = random.sample(range(256), k=256) + m = scipy.signal.medfilt(m, kernel_size=self.kernel_size) + m = m + np.linspace(0, 255, 256) + + m = m - min(m) + m = m / max(m) * 255 + m = np.floor(m).astype(np.uint8) + + self.state_dict = {"LUT": m} + + @img_shape_checker + def _apply_img(self, img: np.ndarray, settings: dict): + if img.dtype != np.uint8: + raise ValueError("IntensityRemap supports uint8 ndarrays only") + if img.ndim == 3 and img.shape[-1] != 1: + raise ValueError("Only grayscale 2D images are supported") + return cv2.LUT(img, self.state_dict["LUT"]) + + class CvtColor(ImageTransform): """RGB to grayscale or grayscale to RGB image conversion. diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 0e933fa..771c65b 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -1,5 +1,6 @@ import copy import random +from contextlib import ExitStack as does_not_raise import cv2 import numpy as np @@ -1057,6 +1058,49 @@ def test_hsv_doesnt_work_for_1_channel(img_6x6): trf(dc) +def test_intensity_remap_values(): + trf = slt.IntensityRemap(p=1) + img = np.arange(0, 256, 1, dtype=np.uint8).reshape((16, 16, 1)) + dc = slc.DataContainer(img, "I") + dc_res = trf(dc) + + img_expected = trf.state_dict["LUT"].reshape((16, 16, 1)) + np.testing.assert_array_equal(dc_res.data[0], img_expected) + + +@pytest.mark.parametrize( + "img, expected", + [ + (img_3x3(), does_not_raise()), + (img_3x3_rgb(), pytest.raises(ValueError)), + ], +) +def test_intensity_remap_channels(img, expected): + trf = slt.IntensityRemap(p=1) + dc = slc.DataContainer(img.astype(np.uint8), "I") + + with expected: + trf(dc) + + +@pytest.mark.parametrize( + "dtype, expected", + [ + (np.uint8, does_not_raise()), + (np.int8, pytest.raises(ValueError)), + (np.uint16, pytest.raises(ValueError)), + (np.float, pytest.raises(ValueError)), + (np.bool_, pytest.raises(ValueError)), + ], +) +def test_intensity_remap_dtypes(dtype, expected): + trf = slt.IntensityRemap(p=1) + dc = slc.DataContainer(img_3x3().astype(dtype), "I") + + with expected: + trf(dc) + + @pytest.mark.parametrize( "mode, img, expected", [ From 17d91192d8cee2ce7f0a29ec4b95c23e7c795a52 Mon Sep 17 00:00:00 2001 From: Egor Panfilov Date: Mon, 24 Feb 2020 21:34:26 +0200 Subject: [PATCH 2/4] Added more tests for intensity remap, review comments --- tests/test_transforms.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 771c65b..55794d4 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -1062,10 +1062,23 @@ def test_intensity_remap_values(): trf = slt.IntensityRemap(p=1) img = np.arange(0, 256, 1, dtype=np.uint8).reshape((16, 16, 1)) dc = slc.DataContainer(img, "I") - dc_res = trf(dc) + out = trf(dc).dc_res.data[0] + # Mapping is applied correctly img_expected = trf.state_dict["LUT"].reshape((16, 16, 1)) - np.testing.assert_array_equal(dc_res.data[0], img_expected) + np.testing.assert_array_equal(out, img_expected) + + # Mapping has a positive trendline + assert np.diff(out.astype(np.float).ravel()) > 0 + + # Higher kernel size yields more monotonic mapping + trf_noisy = slt.IntensityRemap(p=1, kernel_size=1) + trf_low_pass = slt.IntensityRemap(p=1, kernel_size=5) + out_noisy = trf_noisy(dc).data[0].astype(np.float) + out_low_pass = trf_low_pass(dc).data[0].astype(np.float) + std_noisy = np.std(np.diff(out_noisy.ravel())) + std_low_pass = np.std(np.diff(out_low_pass.ravel())) + assert std_low_pass < std_noisy @pytest.mark.parametrize( @@ -1077,7 +1090,7 @@ def test_intensity_remap_values(): ) def test_intensity_remap_channels(img, expected): trf = slt.IntensityRemap(p=1) - dc = slc.DataContainer(img.astype(np.uint8), "I") + dc = slc.DataContainer(img, "I") with expected: trf(dc) @@ -1086,7 +1099,6 @@ def test_intensity_remap_channels(img, expected): @pytest.mark.parametrize( "dtype, expected", [ - (np.uint8, does_not_raise()), (np.int8, pytest.raises(ValueError)), (np.uint16, pytest.raises(ValueError)), (np.float, pytest.raises(ValueError)), From 285a9ccb22be7ee7223f832c59455f113326b66e Mon Sep 17 00:00:00 2001 From: Egor Panfilov Date: Tue, 25 Feb 2020 21:57:45 +0200 Subject: [PATCH 3/4] Fix copypaste --- tests/test_transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 55794d4..2375c96 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -1062,7 +1062,7 @@ def test_intensity_remap_values(): trf = slt.IntensityRemap(p=1) img = np.arange(0, 256, 1, dtype=np.uint8).reshape((16, 16, 1)) dc = slc.DataContainer(img, "I") - out = trf(dc).dc_res.data[0] + out = trf(dc).data[0] # Mapping is applied correctly img_expected = trf.state_dict["LUT"].reshape((16, 16, 1)) From 5c41bead9ba848afe2cf821ea1d71051590145c9 Mon Sep 17 00:00:00 2001 From: Egor Panfilov Date: Tue, 25 Feb 2020 22:06:05 +0200 Subject: [PATCH 4/4] Fix trendline test --- tests/test_transforms.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_transforms.py b/tests/test_transforms.py index 2375c96..b1bd4c9 100644 --- a/tests/test_transforms.py +++ b/tests/test_transforms.py @@ -1069,7 +1069,7 @@ def test_intensity_remap_values(): np.testing.assert_array_equal(out, img_expected) # Mapping has a positive trendline - assert np.diff(out.astype(np.float).ravel()) > 0 + assert np.sum(np.diff(out.astype(np.float).ravel())) > 0 # Higher kernel size yields more monotonic mapping trf_noisy = slt.IntensityRemap(p=1, kernel_size=1)