From 53e27ac90c3948fb4a7324dd62d66f8c94dfdbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Mon, 21 Mar 2022 13:43:12 +0000 Subject: [PATCH 1/4] Fix missing channels dimension in normalization --- InnerEye/ML/photometric_normalization.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/InnerEye/ML/photometric_normalization.py b/InnerEye/ML/photometric_normalization.py index 4d34a8ac7..da399066f 100644 --- a/InnerEye/ML/photometric_normalization.py +++ b/InnerEye/ML/photometric_normalization.py @@ -83,6 +83,10 @@ def transform(self, image: Union[np.ndarray, torch.Tensor], else: mask = np.ones_like(image) + is3d = image.ndim == 3 + if is3d: + image = image[np.newaxis] + self.status_of_most_recent_call = None if self.norm_method == PhotometricNormalizationMethod.Unchanged: image_out = image @@ -117,6 +121,9 @@ def transform(self, image: Union[np.ndarray, torch.Tensor], logging.debug(f"Photonorm patient {patient_id}: {self.status_of_most_recent_call}") check_array_range(image_out, error_prefix="Normalized image") + if is3d: + image_out = image_out[0] + return image_out From 87f00b959a391e0a1d195477f64725f6e7e5fe45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Mon, 21 Mar 2022 17:21:12 +0000 Subject: [PATCH 2/4] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b122931f9..004741f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -97,6 +97,7 @@ gets uploaded to AzureML, by skipping all test folders. ### Fixed +- ([#701](https://github.com/microsoft/InnerEye-DeepLearning/pull/701)) Fix 3D images expected to be 4D for intensity normalization. - ([#682](https://github.com/microsoft/InnerEye-DeepLearning/pull/682)) Ensure the shape of input patches is compatible with model constraints. - ([#681](https://github.com/microsoft/InnerEye-DeepLearning/pull/681)) Pad model outputs if they are smaller than the inputs. - ([#683](https://github.com/microsoft/InnerEye-DeepLearning/pull/683)) Fix missing separator error in docs Makefile. From 478698e47305510dd4c7fb878ea6cf30e6057123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Wed, 8 Jun 2022 10:05:33 +0000 Subject: [PATCH 3/4] Add test for 3D and 4D input images --- InnerEye/ML/utils/image_util.py | 9 +++++---- Tests/ML/test_normalize.py | 32 ++++++++++++++++++++------------ 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/InnerEye/ML/utils/image_util.py b/InnerEye/ML/utils/image_util.py index dff703631..7c01332d1 100644 --- a/InnerEye/ML/utils/image_util.py +++ b/InnerEye/ML/utils/image_util.py @@ -389,18 +389,19 @@ def get_center_crop(image: NumpyOrTorch, crop_shape: TupleInt3) -> NumpyOrTorch: def check_array_range(data: np.ndarray, expected_range: Optional[Range] = None, - error_prefix: str = None) -> None: + error_prefix: Optional[str] = None) -> None: """ Checks if all values in the given array fall into the expected range. If not, raises a - ValueError, and prints out statistics about the values that fell outside the expected range. + ``ValueError``, and prints out statistics about the values that fell outside the expected range. If no range is provided, it checks that all values in the array are finite (that is, they are not - infinity and not np.nan + infinity and not ``np.nan``). :param data: The array to check. It can have any size. :param expected_range: The interval that all array elements must fall into. The first entry is the lower - bound, the second entry is the upper bound. + bound, the second entry is the upper bound. :param error_prefix: A string to use as the prefix for the error message. """ + data = np.asarray(data) if expected_range is None: valid_pixels = np.isfinite(data) else: diff --git a/Tests/ML/test_normalize.py b/Tests/ML/test_normalize.py index 12db1212c..5ad305943 100644 --- a/Tests/ML/test_normalize.py +++ b/Tests/ML/test_normalize.py @@ -36,14 +36,14 @@ @pytest.fixture -def image_rand_pos() -> Union[torch.Tensor, np.ndarray]: +def image_rand_pos() -> np.ndarray: torch.random.manual_seed(1) np.random.seed(0) return (np.random.rand(3, 4, 4, 4) * 1000.0).astype(ImageDataType.IMAGE.value) @pytest.fixture -def image_rand_pos_gpu(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> Union[torch.Tensor, np.ndarray]: +def image_rand_pos_gpu(image_rand_pos: np.ndarray) -> Union[torch.Tensor, np.ndarray]: return torch.tensor(image_rand_pos) if use_gpu else image_rand_pos @@ -56,7 +56,7 @@ def assert_image_out_datatype(image_out: np.ndarray) -> None: "datatype that we force images to have." -def test_simplenorm_half(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: +def test_simplenorm_half(image_rand_pos: np.ndarray) -> None: image_out = photometric_normalization.simple_norm(image_rand_pos, mask_half, debug_mode=True) assert np.mean(image_out, dtype=np.float) == approx(-0.05052318) for c in range(image_out.shape[0]): @@ -64,34 +64,42 @@ def test_simplenorm_half(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> Non assert_image_out_datatype(image_out) -def test_simplenorm_ones(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: +def test_simplenorm_ones(image_rand_pos: np.ndarray) -> None: image_out = photometric_normalization.simple_norm(image_rand_pos, mask_ones, debug_mode=True) assert np.mean(image_out) == approx(0, abs=1e-7) assert_image_out_datatype(image_out) -def test_mriwindowhalf(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: - image_out, status = photometric_normalization.mri_window(image_rand_pos, mask_half, (0, 1), sharpen, tail) +def test_3d_4d(image_rand_pos: np.ndarray) -> None: + normalization = photometric_normalization.PhotometricNormalization() + shape = image_rand_pos.shape + spatial_shape = shape[1:] + assert normalization.transform(image_rand_pos).shape == shape + assert normalization.transform(image_rand_pos[0]).shape == spatial_shape + + +def test_mriwindowhalf(image_rand_pos: np.ndarray) -> None: + image_out, _ = photometric_normalization.mri_window(image_rand_pos, mask_half, (0, 1), sharpen, tail) assert np.mean(image_out) == approx(0.2748852) assert_image_out_datatype(image_out) -def test_mriwindowones(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: - image_out, status = photometric_normalization.mri_window(image_rand_pos, mask_ones, (0.0, 1.0), sharpen, tail3) +def test_mriwindowones(image_rand_pos: np.ndarray) -> None: + image_out, _ = photometric_normalization.mri_window(image_rand_pos, mask_ones, (0.0, 1.0), sharpen, tail3) assert np.mean(image_out) == approx(0.2748852) assert_image_out_datatype(image_out) -def test_trimmed_norm_full(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: - image_out, status = photometric_normalization.normalize_trim(image_rand_pos, mask_ones, +def test_trimmed_norm_full(image_rand_pos: np.ndarray) -> None: + image_out, _ = photometric_normalization.normalize_trim(image_rand_pos, mask_ones, output_range=(-1, 1), sharpen=1, trim_percentiles=(1, 99)) assert np.mean(image_out, dtype=np.float) == approx(-0.08756259549409151) assert_image_out_datatype(image_out) -def test_trimmed_norm_half(image_rand_pos: Union[torch.Tensor, np.ndarray]) -> None: - image_out, status = photometric_normalization.normalize_trim(image_rand_pos, mask_half, +def test_trimmed_norm_half(image_rand_pos: np.ndarray) -> None: + image_out, _ = photometric_normalization.normalize_trim(image_rand_pos, mask_half, output_range=(-1, 1), sharpen=1, trim_percentiles=(1, 99)) assert np.mean(image_out, dtype=np.float) == approx(-0.4862089517215888) From 61515332d166efc7947d35ba62bcb8ec5e051c4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20P=C3=A9rez-Garc=C3=ADa?= Date: Mon, 13 Jun 2022 11:26:40 +0100 Subject: [PATCH 4/4] Move conversion to NumPy array --- InnerEye/ML/photometric_normalization.py | 2 +- InnerEye/ML/utils/image_util.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/InnerEye/ML/photometric_normalization.py b/InnerEye/ML/photometric_normalization.py index 66ec34fe0..558e93772 100644 --- a/InnerEye/ML/photometric_normalization.py +++ b/InnerEye/ML/photometric_normalization.py @@ -120,7 +120,7 @@ def transform(self, image: Union[np.ndarray, torch.Tensor], raise ValueError("Unknown normalization method {}".format(self.norm_method)) if patient_id is not None and self.status_of_most_recent_call is not None: logging.debug(f"Photonorm patient {patient_id}: {self.status_of_most_recent_call}") - check_array_range(image_out, error_prefix="Normalized image") + check_array_range(np.asarray(image_out), error_prefix="Normalized image") if is3d: image_out = image_out[0] diff --git a/InnerEye/ML/utils/image_util.py b/InnerEye/ML/utils/image_util.py index 7c01332d1..e0eb840c5 100644 --- a/InnerEye/ML/utils/image_util.py +++ b/InnerEye/ML/utils/image_util.py @@ -401,7 +401,6 @@ def check_array_range(data: np.ndarray, expected_range: Optional[Range] = None, bound, the second entry is the upper bound. :param error_prefix: A string to use as the prefix for the error message. """ - data = np.asarray(data) if expected_range is None: valid_pixels = np.isfinite(data) else: