diff --git a/CHANGELOG.md b/CHANGELOG.md index 3edea227d..276d2a993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,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. - ([#704](https://github.com/microsoft/InnerEye-DeepLearning/pull/704)) Add submodules to sys.path to fix autodoc's warning. - ([#699](https://github.com/microsoft/InnerEye-DeepLearning/pull/699)) Fix Sphinx warnings. - ([#682](https://github.com/microsoft/InnerEye-DeepLearning/pull/682)) Ensure the shape of input patches is compatible with model constraints. diff --git a/InnerEye/ML/photometric_normalization.py b/InnerEye/ML/photometric_normalization.py index 38eddf8d7..558e93772 100644 --- a/InnerEye/ML/photometric_normalization.py +++ b/InnerEye/ML/photometric_normalization.py @@ -84,6 +84,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 @@ -116,7 +120,10 @@ 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] return image_out diff --git a/InnerEye/ML/utils/image_util.py b/InnerEye/ML/utils/image_util.py index dff703631..e0eb840c5 100644 --- a/InnerEye/ML/utils/image_util.py +++ b/InnerEye/ML/utils/image_util.py @@ -389,16 +389,16 @@ 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. """ if expected_range is None: 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)