Skip to content
This repository has been archived by the owner on Mar 21, 2024. It is now read-only.

Commit

Permalink
BUG: Fix missing channels dimension in normalization (#701)
Browse files Browse the repository at this point in the history
* Fix missing channels dimension in normalization

* Update CHANGELOG

* Add test for 3D and 4D input images

* Move conversion to NumPy array
  • Loading branch information
fepegar committed Jun 13, 2022
1 parent edc72ed commit a9fd52c
Show file tree
Hide file tree
Showing 4 changed files with 33 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 8 additions & 1 deletion InnerEye/ML/photometric_normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions InnerEye/ML/utils/image_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
32 changes: 20 additions & 12 deletions Tests/ML/test_normalize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -56,42 +56,50 @@ 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]):
assert np.mean(image_out[c, mask_half > 0.5], dtype=np.float) == approx(0, abs=1e-7)
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)
Expand Down

0 comments on commit a9fd52c

Please sign in to comment.