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

Re-enable Linux build #655

Merged
merged 9 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ in inference-only runs when using lightning containers.
- ([#638](https://github.com/microsoft/InnerEye-DeepLearning/pull/638)) SimClr cosine LR scheduler was using wrong length information when using with long linear head datasets
- ([#612](https://github.com/microsoft/InnerEye-DeepLearning/pull/612)) SSL online evaluator was not doing distributed training
- ([#652](https://github.com/microsoft/InnerEye-DeepLearning/pull/652)) Run pytest build on Windows after Linux agent version upgrade
- ([#655](https://github.com/microsoft/InnerEye-DeepLearning/pull/655)) Run pytest on Linux again, but with Ubuntu 20.04

### Removed

Expand Down
1 change: 0 additions & 1 deletion InnerEye/ML/Histopathology/preprocessing/loading.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ def _get_bounding_box(self, slide_obj: 'CuImage') -> Tuple[box_utils.Box, float]
return bbox, threshold

def __call__(self, data: Dict) -> Dict:
from cucim import CuImage
image_obj: CuImage = self.reader.read(data[self.image_key])

level0_bbox, threshold = self._get_bounding_box(image_obj)
Expand Down
45 changes: 25 additions & 20 deletions Tests/ML/augmentations/test_augmentation_for_segmentation_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
# ------------------------------------------------------------------------------------------

from typing import Any, List
from typing import Any, List, Tuple

import numpy as np
import pytest
Expand All @@ -14,24 +14,28 @@

from Tests.ML.util import DummyPatientMetadata

ml_util.set_random_seed(1)

image_size = (8, 8, 8)
valid_image_4d = np.random.uniform(size=((5,) + image_size)) * 10
valid_mask = np.random.randint(2, size=image_size)
number_of_classes = 5
class_assignments = np.random.randint(2, size=image_size)
valid_labels = np.zeros((number_of_classes,) + image_size)
for c in range(number_of_classes):
valid_labels[c, class_assignments == c] = 1
valid_crop_size = (2, 2, 2)
valid_full_crop_size = image_size
valid_class_weights = [0.5] + [0.5 / (number_of_classes - 1)] * (number_of_classes - 1)
crop_size_requires_padding = (9, 8, 12)


def create_valid_image() -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
ml_util.set_random_seed(1)
valid_image_4d = np.random.uniform(size=((5,) + image_size)) * 10
valid_mask = np.random.randint(2, size=image_size)
class_assignments = np.random.randint(2, size=image_size)
valid_labels = np.zeros((number_of_classes,) + image_size)
for c in range(number_of_classes):
valid_labels[c, class_assignments == c] = 1
return valid_image_4d, valid_labels, valid_mask


def test_valid_full_crop() -> None:
metadata = DummyPatientMetadata
valid_image_4d, valid_labels, valid_mask = create_valid_image()
sample, _ = random_crop(sample=Sample(image=valid_image_4d,
labels=valid_labels,
mask=valid_mask,
Expand All @@ -45,25 +49,23 @@ def test_valid_full_crop() -> None:
assert sample.metadata == metadata


@pytest.mark.parametrize("image", [None, list(), valid_image_4d])
@pytest.mark.parametrize("labels", [None, list(), valid_labels])
@pytest.mark.parametrize("mask", [None, list(), valid_mask])
@pytest.mark.parametrize("class_weights", [[0, 0, 0], [0], [-1, 0, 1], [-1, -2, -3], valid_class_weights])
@pytest.mark.parametrize("image", [None, list()])
@pytest.mark.parametrize("labels", [None, list()])
@pytest.mark.parametrize("mask", [None, list()])
@pytest.mark.parametrize("class_weights", [[0, 0, 0], [0], [-1, 0, 1], [-1, -2, -3]])
def test_invalid_arrays(image: Any, labels: Any, mask: Any, class_weights: Any) -> None:
"""
Tests failure cases of the random_crop function for invalid image, labels, mask or class
weights arguments.
"""
# Skip the final combination, because it is valid
if not (np.array_equal(image, valid_image_4d) and np.array_equal(labels, valid_labels)
and np.array_equal(mask, valid_mask) and class_weights == valid_class_weights):
with pytest.raises(Exception):
random_crop(Sample(metadata=DummyPatientMetadata, image=image, labels=labels, mask=mask),
valid_crop_size, class_weights)
with pytest.raises(Exception):
random_crop(Sample(metadata=DummyPatientMetadata, image=image, labels=labels, mask=mask),
valid_crop_size, class_weights)


@pytest.mark.parametrize("crop_size", [None, ["a"], 5])
def test_invalid_crop_arg(crop_size: Any) -> None:
valid_image_4d, valid_labels, valid_mask = create_valid_image()
with pytest.raises(Exception):
random_crop(
Sample(metadata=DummyPatientMetadata, image=valid_image_4d, labels=valid_labels, mask=valid_mask),
Expand All @@ -72,13 +74,15 @@ def test_invalid_crop_arg(crop_size: Any) -> None:

@pytest.mark.parametrize("crop_size", [[2, 2], [2, 2, 2, 2], [10, 10, 10]])
def test_invalid_crop_size(crop_size: Any) -> None:
valid_image_4d, valid_labels, valid_mask = create_valid_image()
with pytest.raises(Exception):
random_crop(
Sample(metadata=DummyPatientMetadata, image=valid_image_4d, labels=valid_labels, mask=valid_mask),
crop_size, valid_class_weights)


def test_random_crop_no_fg() -> None:
valid_image_4d, valid_labels, valid_mask = create_valid_image()
with pytest.raises(Exception):
random_crop(Sample(metadata=DummyPatientMetadata, image=valid_image_4d, labels=valid_labels,
mask=np.zeros_like(valid_mask)),
Expand All @@ -92,6 +96,7 @@ def test_random_crop_no_fg() -> None:

@pytest.mark.parametrize("crop_size", [valid_crop_size])
def test_random_crop(crop_size: Any) -> None:
valid_image_4d, valid_labels, valid_mask = create_valid_image()
labels = valid_labels
# create labels such that there are no foreground voxels in a particular class
# this should ne handled gracefully (class being ignored from sampling)
Expand Down Expand Up @@ -119,7 +124,7 @@ def test_valid_class_weights(class_weights: List[float]) -> None:
"""
Produce a large number of crops and make sure the crop center class proportions respect class weights
"""
ml_util.set_random_seed(1)
valid_image_4d, valid_labels, valid_mask = create_valid_image()
num_classes = len(valid_labels)
image = np.zeros_like(valid_image_4d)
labels = np.zeros_like(valid_labels)
Expand Down
81 changes: 52 additions & 29 deletions Tests/ML/augmentations/test_image_transforms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,71 +17,92 @@

image_size = (256, 256)

test_tensor_1channel_1slice = torch.ones([1, 1, *image_size], dtype=torch.float)
test_tensor_1channel_1slice[..., 100:150, 100:200] = 1 / 255.
min_test_image, max_test_image = test_tensor_1channel_1slice.min(), test_tensor_1channel_1slice.max()
test_tensor_2channels_2slices = torch.ones([2, 2, *image_size], dtype=torch.float)
test_tensor_2channels_2slices[..., 100:150, 100:200] = 1 / 255.
invalid_test_tensor = torch.ones([1, *image_size])
test_pil_image = Image.open(str(full_ml_test_data_path() / "image_and_contour.png")).convert("RGB")
test_image_as_tensor = to_tensor(test_pil_image).unsqueeze(0) # put in a [1, C, H, W] format

@pytest.fixture(scope="module")
def tensor_1channel_1slice() -> torch.Tensor:
torch.manual_seed(10)
t = torch.ones([1, 1, *image_size], dtype=torch.float)
t[..., 100:150, 100:200] = 1 / 255.
return t


@pytest.fixture(scope="module")
def tensor_2channels_2slices() -> torch.Tensor:
torch.manual_seed(10)
t = torch.ones([2, 2, *image_size], dtype=torch.float)
t[..., 100:150, 100:200] = 1 / 255.
return t


@pytest.fixture(scope="module")
def image_as_tensor() -> torch.Tensor:
test_pil_image = Image.open(str(full_ml_test_data_path() / "image_and_contour.png")).convert("RGB")
return to_tensor(test_pil_image).unsqueeze(0) # put in a [1, C, H, W] format


@pytest.fixture(scope="module")
def invalid_test_tensor() -> torch.Tensor:
return torch.ones([1, *image_size])


def test_add_gaussian_noise() -> None:
def test_add_gaussian_noise(tensor_1channel_1slice: torch.Tensor,
tensor_2channels_2slices: torch.Tensor) -> None:
"""
Tests functionality of add gaussian noise
"""
min_test_image, max_test_image = tensor_1channel_1slice.min(), tensor_1channel_1slice.max()
# Test case of image with 1 channel, 1 slice (2D)
torch.manual_seed(10)
transformed = AddGaussianNoise(std=0.05, p_apply=1)(test_tensor_1channel_1slice.clone())
transformed = AddGaussianNoise(std=0.05, p_apply=1)(tensor_1channel_1slice)
torch.manual_seed(10)
noise = torch.randn(size=(1, *image_size)) * 0.05
assert torch.isclose(
torch.clamp(test_tensor_1channel_1slice + noise, min_test_image, max_test_image), # type: ignore
torch.clamp(tensor_1channel_1slice + noise, min_test_image, max_test_image), # type: ignore
transformed).all()

# Test p_apply = 0
untransformed = AddGaussianNoise(std=0.05, p_apply=0)(test_tensor_1channel_1slice.clone())
assert torch.isclose(untransformed, test_tensor_1channel_1slice).all()
untransformed = AddGaussianNoise(std=0.05, p_apply=0)(tensor_1channel_1slice)
assert torch.isclose(untransformed, tensor_1channel_1slice).all()

# Check that it applies the same transform to all slices if number of slices > 1
torch.manual_seed(10)
transformed = AddGaussianNoise(std=0.05, p_apply=1)(test_tensor_2channels_2slices.clone())
transformed = AddGaussianNoise(std=0.05, p_apply=1)(tensor_2channels_2slices)
assert torch.isclose(
torch.clamp(test_tensor_2channels_2slices + noise, min_test_image, max_test_image), # type: ignore
torch.clamp(tensor_2channels_2slices + noise, min_test_image, max_test_image), # type: ignore
transformed).all()


def test_elastic_transform() -> None:
def test_elastic_transform(image_as_tensor: torch.Tensor) -> None:
"""
Tests elastic transform
"""
np.random.seed(7)
transformed_image = ElasticTransform(sigma=4, alpha=34, p_apply=1.0)(test_image_as_tensor.clone())
transformed_image = ElasticTransform(sigma=4, alpha=34, p_apply=1.0)(image_as_tensor)
transformed_pil = to_pil_image(transformed_image.squeeze(0))
expected_pil_image = Image.open(full_ml_test_data_path() / "elastic_transformed_image_and_contour.png").convert(
"RGB")
assert expected_pil_image == transformed_pil
untransformed_image = ElasticTransform(sigma=4, alpha=34, p_apply=0.0)(test_image_as_tensor.clone())
assert torch.isclose(test_image_as_tensor, untransformed_image).all()
untransformed_image = ElasticTransform(sigma=4, alpha=34, p_apply=0.0)(image_as_tensor)
assert torch.isclose(image_as_tensor, untransformed_image).all()


def test_expand_channels() -> None:
def test_invalid_tensors(invalid_test_tensor: torch.Tensor) -> None:
# This is invalid input (expects 4 dimensions)
with pytest.raises(ValueError):
ExpandChannels()(invalid_test_tensor)
with pytest.raises(ValueError):
RandomGamma(scale=(0.3, 3))(invalid_test_tensor)

tensor_img = ExpandChannels()(test_tensor_1channel_1slice.clone())

def test_expand_channels(tensor_1channel_1slice: torch.Tensor) -> None:
tensor_img = ExpandChannels()(tensor_1channel_1slice)
assert tensor_img.shape == torch.Size([1, 3, *image_size])


def test_random_gamma() -> None:
# This is invalid input (expects 4 dimensions)
with pytest.raises(ValueError):
RandomGamma(scale=(0.3, 3))(invalid_test_tensor)

def test_random_gamma(tensor_1channel_1slice: torch.Tensor) -> None:
random.seed(0)
transformed_1 = RandomGamma(scale=(0.3, 3))(test_tensor_1channel_1slice.clone())
assert transformed_1.shape == test_tensor_1channel_1slice.shape
transformed_1 = RandomGamma(scale=(0.3, 3))(tensor_1channel_1slice.clone())
assert transformed_1.shape == tensor_1channel_1slice.shape

tensor_img = torch.ones([2, 3, *image_size])
transformed_2 = RandomGamma(scale=(0.3, 3))(tensor_img)
Expand All @@ -91,6 +112,8 @@ def test_random_gamma() -> None:
assert torch.isclose(transformed_2[0, 1], transformed_2[0, 2]).all() and \
torch.isclose(transformed_2[0, 0], transformed_2[0, 2]).all()

human_readable_transformed = to_pil_image(RandomGamma(scale=(2, 3))(test_image_as_tensor).squeeze(0))

def test_random_gamma_image(image_as_tensor: torch.Tensor) -> None:
human_readable_transformed = to_pil_image(RandomGamma(scale=(2, 3))(image_as_tensor).squeeze(0))
expected_pil_image = Image.open(full_ml_test_data_path() / "gamma_transformed_image_and_contour.png").convert("RGB")
assert expected_pil_image == human_readable_transformed
54 changes: 29 additions & 25 deletions Tests/ML/augmentations/test_transform_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
# ------------------------------------------------------------------------------------------
import random
from typing import Tuple

import PIL
import pytest
Expand Down Expand Up @@ -36,27 +37,29 @@

image_size = (32, 32)
crop_size = 24
test_image_as_array = np.ones(list(image_size)) * 255.0
test_image_as_array[10:15, 10:20] = 1
test_image_as_pil = PIL.Image.fromarray(test_image_as_array).convert("L")
test_2d_image_as_CHW_tensor = to_tensor(test_image_as_array)

test_2d_image_as_ZCHW_tensor = test_2d_image_as_CHW_tensor.unsqueeze(0)

test_4d_scan_as_tensor = torch.ones([5, 4, *image_size]) * 255.0
test_4d_scan_as_tensor[..., 10:15, 10:20] = 1
def create_test_images() -> Tuple[np.ndarray, torch.Tensor, torch.Tensor, torch.Tensor]:
test_image_as_array = np.ones(list(image_size)) * 255.0
test_image_as_array[10:15, 10:20] = 1
image_as_pil = PIL.Image.fromarray(test_image_as_array).convert("L")
image_2d_as_CHW_tensor = to_tensor(test_image_as_array)
image_2d_as_ZCHW_tensor = image_2d_as_CHW_tensor.unsqueeze(0)
scan_4d_as_tensor = torch.ones([5, 4, *image_size]) * 255.0
scan_4d_as_tensor[..., 10:15, 10:20] = 1
return image_as_pil, image_2d_as_CHW_tensor, image_2d_as_ZCHW_tensor, scan_4d_as_tensor


@pytest.mark.parametrize("use_different_transformation_per_channel", [True, False])
def test_torchvision_on_various_input(
use_different_transformation_per_channel: bool,
use_different_transformation_per_channel: bool,
) -> None:
"""
This tests that we can run transformation pipeline with out of the box torchvision transforms on various types
of input: PIL image, 3D tensor, 4D tensors. Tests that use_different_transformation_per_channel has the correct
behavior.
"""

image_as_pil, image_2d_as_CHW_tensor, image_2d_as_ZCHW_tensor, scan_4d_as_tensor = create_test_images()
transform = ImageTransformationPipeline(
[
CenterCrop(crop_size),
Expand All @@ -67,41 +70,42 @@ def test_torchvision_on_various_input(
)

# Test PIL image input
transformed = transform(test_image_as_pil)
transformed = transform(image_as_pil)
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == torch.Size([1, crop_size, crop_size])

# Test image as [C, H. W] tensor
transformed = transform(test_2d_image_as_CHW_tensor.clone())
transformed = transform(image_2d_as_CHW_tensor.clone())
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == torch.Size([1, crop_size, crop_size])

# Test image as [1, 1, H, W]
transformed = transform(test_2d_image_as_ZCHW_tensor)
transformed = transform(image_2d_as_ZCHW_tensor)
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == torch.Size([1, 1, crop_size, crop_size])

# Test with a fake 4D scan [C, Z, H, W] -> [25, 34, 32, 32]
transformed = transform(test_4d_scan_as_tensor)
transformed = transform(scan_4d_as_tensor)
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == torch.Size([5, 4, crop_size, crop_size])

# Same transformation should be applied to all slices and channels.
assert (
torch.isclose(transformed[0, 0], transformed[1, 1]).all()
!= use_different_transformation_per_channel
torch.isclose(transformed[0, 0], transformed[1, 1]).all()
!= use_different_transformation_per_channel
)


@pytest.mark.parametrize("use_different_transformation_per_channel", [True, False])
def test_custom_tf_on_various_input(
use_different_transformation_per_channel: bool,
use_different_transformation_per_channel: bool,
) -> None:
"""
This tests that we can run transformation pipeline with our custom transforms on various types
of input: PIL image, 3D tensor, 4D tensors. Tests that use_different_transformation_per_channel has the correct
behavior. The transforms are test individually in test_image_transforms.py
"""
image_as_pil, image_2d_as_CHW_tensor, image_2d_as_ZCHW_tensor, scan_4d_as_tensor = create_test_images()
pipeline = ImageTransformationPipeline(
[
ElasticTransform(sigma=4, alpha=34, p_apply=1),
Expand All @@ -112,27 +116,27 @@ def test_custom_tf_on_various_input(
)

# Test PIL image input
transformed = pipeline(test_image_as_pil)
assert transformed.shape == test_2d_image_as_CHW_tensor.shape
transformed = pipeline(image_as_pil)
assert transformed.shape == image_2d_as_CHW_tensor.shape

# Test image as [C, H, W] tensor
pipeline(test_2d_image_as_CHW_tensor)
assert transformed.shape == test_2d_image_as_CHW_tensor.shape
pipeline(image_2d_as_CHW_tensor)
assert transformed.shape == image_2d_as_CHW_tensor.shape

# Test image as [1, 1, H, W]
transformed = pipeline(test_2d_image_as_ZCHW_tensor)
transformed = pipeline(image_2d_as_ZCHW_tensor)
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == torch.Size([1, 1, *image_size])

# Test with a fake scan [C, Z, H, W] -> [25, 34, 32, 32]
transformed = pipeline(test_4d_scan_as_tensor)
transformed = pipeline(scan_4d_as_tensor)
assert isinstance(transformed, torch.Tensor)
assert transformed.shape == test_4d_scan_as_tensor.shape
assert transformed.shape == scan_4d_as_tensor.shape

# Same transformation should be applied to all slices and channels.
assert (
torch.isclose(transformed[0, 0], transformed[1, 1]).all()
!= use_different_transformation_per_channel
torch.isclose(transformed[0, 0], transformed[1, 1]).all()
!= use_different_transformation_per_channel
)


Expand Down
Loading