From 439d8f4fb5f1480adeb17d3ed4a4441f5c23cdfa Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Mon, 15 Nov 2021 19:05:57 +0000 Subject: [PATCH 1/7] Use matplotlib to load pngs --- InnerEye/ML/utils/io_util.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/InnerEye/ML/utils/io_util.py b/InnerEye/ML/utils/io_util.py index cc13155c1..c631a1f40 100644 --- a/InnerEye/ML/utils/io_util.py +++ b/InnerEye/ML/utils/io_util.py @@ -12,6 +12,7 @@ import SimpleITK as sitk import h5py +import matplotlib.image as mpimg import numpy as np import pandas as pd import pydicom as dicom @@ -495,8 +496,15 @@ def load_image(path: PathOrString, image_type: Optional[Type] = float) -> ImageW header = get_unit_image_header() return ImageWithHeader(image, header) elif is_png(path): - import imageio - image = imageio.imread(path).astype(np.float) + # https://matplotlib.org/stable/api/image_api.html#matplotlib.image.imread + # numpy_array is a numpy.array of shape: (H, W), (H, W, 3), or (H, W, 4) + # where H = height, W = width + nd = mpimg.imread(path) + if len(nd.shape) == 2: + # if loaded a greyscale image, then it is of shape (H, W) so add in an extra axis + nd = np.expand_dims(nd, 2) + # transpose to shape (C, H, W) + image = np.transpose(nd, (2, 0, 1)) header = get_unit_image_header() return ImageWithHeader(image, header) raise ValueError(f"Invalid file type {path}") From d9e5cbb7349d8c30bd2e93e82e9941a60469fb18 Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Mon, 15 Nov 2021 19:10:56 +0000 Subject: [PATCH 2/7] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index de3851de4..823150273 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ jobs that run in AzureML. - ([#559](https://github.com/microsoft/InnerEye-DeepLearning/pull/559)) Adding the accompanying code for the ["Active label cleaning: Improving dataset quality under resource constraints"](https://arxiv.org/abs/2109.00574) paper. The code can be found in the [InnerEye-DataQuality](InnerEye-DataQuality/README.md) subfolder. It provides tools for training noise robust models, running label cleaning simulation and loading our label cleaning benchmark datasets. ### Changed +- ([#588](https://github.com/microsoft/InnerEye-DeepLearning/pull/588)) Replace SciPy with matplotlib to load png files. - ([#576](https://github.com/microsoft/InnerEye-DeepLearning/pull/576)) The console output is no longer written to stdout.txt because AzureML handles that better now - ([#531](https://github.com/microsoft/InnerEye-DeepLearning/pull/531)) Updated PL to 1.3.8, torchmetrics and pl-bolts and changed relevant metrics and SSL code API. - ([#555](https://github.com/microsoft/InnerEye-DeepLearning/pull/555)) Make the SSLContainer compatible with new datasets From a3f7e3be4638af82a7b29bdbdc9a96aae94fc697 Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Tue, 16 Nov 2021 17:10:23 +0000 Subject: [PATCH 3/7] Use png image plugin directly --- InnerEye/ML/utils/io_util.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/InnerEye/ML/utils/io_util.py b/InnerEye/ML/utils/io_util.py index c631a1f40..94ecd3c20 100644 --- a/InnerEye/ML/utils/io_util.py +++ b/InnerEye/ML/utils/io_util.py @@ -15,6 +15,7 @@ import matplotlib.image as mpimg import numpy as np import pandas as pd +import PIL.PngImagePlugin import pydicom as dicom import torch from numpy.lib.npyio import NpzFile @@ -496,15 +497,8 @@ def load_image(path: PathOrString, image_type: Optional[Type] = float) -> ImageW header = get_unit_image_header() return ImageWithHeader(image, header) elif is_png(path): - # https://matplotlib.org/stable/api/image_api.html#matplotlib.image.imread - # numpy_array is a numpy.array of shape: (H, W), (H, W, 3), or (H, W, 4) - # where H = height, W = width - nd = mpimg.imread(path) - if len(nd.shape) == 2: - # if loaded a greyscale image, then it is of shape (H, W) so add in an extra axis - nd = np.expand_dims(nd, 2) - # transpose to shape (C, H, W) - image = np.transpose(nd, (2, 0, 1)) + with PIL.PngImagePlugin.PngImageFile(path) as pil_png: + image = np.asarray(pil_png, np.float) header = get_unit_image_header() return ImageWithHeader(image, header) raise ValueError(f"Invalid file type {path}") From 46d6e20c920fcf28bc99d94091828b9223bfdd50 Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Tue, 16 Nov 2021 17:13:07 +0000 Subject: [PATCH 4/7] Remove unused import --- InnerEye/ML/utils/io_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/InnerEye/ML/utils/io_util.py b/InnerEye/ML/utils/io_util.py index 94ecd3c20..c1b6b91f7 100644 --- a/InnerEye/ML/utils/io_util.py +++ b/InnerEye/ML/utils/io_util.py @@ -12,7 +12,6 @@ import SimpleITK as sitk import h5py -import matplotlib.image as mpimg import numpy as np import pandas as pd import PIL.PngImagePlugin From 9975968de903109239144156137c327da9de5000 Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Thu, 18 Nov 2021 11:43:00 +0000 Subject: [PATCH 5/7] Add round trip png tests --- Tests/ML/utils/test_io_util.py | 37 ++++++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/Tests/ML/utils/test_io_util.py b/Tests/ML/utils/test_io_util.py index d8083e3b3..c2c45134b 100644 --- a/Tests/ML/utils/test_io_util.py +++ b/Tests/ML/utils/test_io_util.py @@ -9,11 +9,14 @@ from unittest import mock import zipfile +import imageio +import PIL.PngImagePlugin import SimpleITK as sitk import numpy as np import pydicom import pytest import torch +from PIL import Image from skimage.transform import resize from InnerEye.Common.fixed_paths_for_tests import full_ml_test_data_path @@ -278,6 +281,7 @@ def write_test_dicom(array: np.ndarray, path: Path, is_monochrome2: bool = True, ds.BitsStored = bits_stored ds.save_as(path) + @pytest.mark.parametrize("is_signed", [True, False]) @pytest.mark.parametrize("is_monochrome2", [True, False]) def test_load_dicom_image_ones(test_output_dirs: OutputFolderForTests, @@ -405,8 +409,8 @@ def test_load_and_stack_with_crop() -> None: # values of 1.0 are in the image image = np.zeros(image_size) image[center_start[0]:center_start[0] + crop_shape[0], - center_start[1]:center_start[1] + crop_shape[1], - center_start[2]:center_start[2] + crop_shape[2]] = 1 + center_start[1]:center_start[1] + crop_shape[1], + center_start[2]:center_start[2] + crop_shape[2]] = 1 segmentation = image * 2 mock_return = ImageAndSegmentations(image, segmentation) with mock.patch("InnerEye.ML.utils.io_util.load_image_in_known_formats", return_value=mock_return): @@ -689,3 +693,32 @@ def test_zip_random_dicom_series(test_output_dirs: OutputFolderForTests) -> None # GetSize returns (width, height, depth) assert loaded_image.GetSize() == reverse_tuple_float3(test_shape) assert loaded_image.GetSpacing() == test_spacing + + +def test_load_png_images(test_output_dirs: OutputFolderForTests) -> None: + """ + Test load of png image files in a variety of formats using SciPy and PIL.PngImagePlugin + """ + def save_and_reload_image(data: np.array, # type: ignore + mode: str) -> None: + path = test_output_dirs.root_dir / f"{mode}.png" + image = Image.fromarray(data, mode=mode) + image.save(path) + + scipy_image = imageio.imread(path).astype(np.float) + assert scipy_image.dtype == np.float # type: ignore + assert np.array_equal(data, scipy_image) + + with PIL.PngImagePlugin.PngImageFile(path) as pil_png: + pil_image = np.asarray(pil_png, np.float) + assert pil_image.dtype == np.float # type: ignore + assert np.array_equal(data, pil_image) + + data_l = np.random.randint(2**8, size=(300, 200), dtype=np.uint8) + save_and_reload_image(data_l, 'L') + + data_rgb = np.random.randint(2**8, size=(300, 200, 3), dtype=np.uint8) + save_and_reload_image(data_rgb, 'RGB') + + data_rgba = np.random.randint(2**8, size=(300, 200, 4), dtype=np.uint8) + save_and_reload_image(data_rgba, 'RGBA') From 101900bef5febb10dade50afe5d5fb1f6f1b1acb Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Thu, 18 Nov 2021 16:31:56 +0000 Subject: [PATCH 6/7] Call load_image --- CHANGELOG.md | 2 +- Tests/ML/utils/test_io_util.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e584c7627..e711f5ff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ jobs that run in AzureML. hook to enable overriding `AzureConfig` parameters from a container (e.g. `experiment_name`, `cluster`, `num_nodes`). ### Changed -- ([#588](https://github.com/microsoft/InnerEye-DeepLearning/pull/588)) Replace SciPy with matplotlib to load png files. +- ([#588](https://github.com/microsoft/InnerEye-DeepLearning/pull/588)) Replace SciPy with PIL.PngImagePlugin.PngImageFile to load png files. - ([#576](https://github.com/microsoft/InnerEye-DeepLearning/pull/576)) The console output is no longer written to stdout.txt because AzureML handles that better now - ([#531](https://github.com/microsoft/InnerEye-DeepLearning/pull/531)) Updated PL to 1.3.8, torchmetrics and pl-bolts and changed relevant metrics and SSL code API. - ([#555](https://github.com/microsoft/InnerEye-DeepLearning/pull/555)) Make the SSLContainer compatible with new datasets diff --git a/Tests/ML/utils/test_io_util.py b/Tests/ML/utils/test_io_util.py index c2c45134b..608c7c6f7 100644 --- a/Tests/ML/utils/test_io_util.py +++ b/Tests/ML/utils/test_io_util.py @@ -709,10 +709,9 @@ def save_and_reload_image(data: np.array, # type: ignore assert scipy_image.dtype == np.float # type: ignore assert np.array_equal(data, scipy_image) - with PIL.PngImagePlugin.PngImageFile(path) as pil_png: - pil_image = np.asarray(pil_png, np.float) - assert pil_image.dtype == np.float # type: ignore - assert np.array_equal(data, pil_image) + image_with_header = io_util.load_image(path) + assert image_with_header.image.dtype == np.float # type: ignore + assert np.array_equal(data, image_with_header.image) data_l = np.random.randint(2**8, size=(300, 200), dtype=np.uint8) save_and_reload_image(data_l, 'L') From 3d91d12b6c274a855503e9a21934ab429cc11af2 Mon Sep 17 00:00:00 2001 From: Jonathan Tripp Date: Thu, 18 Nov 2021 17:01:49 +0000 Subject: [PATCH 7/7] Unused import --- Tests/ML/utils/test_io_util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/ML/utils/test_io_util.py b/Tests/ML/utils/test_io_util.py index 608c7c6f7..7abf53849 100644 --- a/Tests/ML/utils/test_io_util.py +++ b/Tests/ML/utils/test_io_util.py @@ -10,7 +10,6 @@ import zipfile import imageio -import PIL.PngImagePlugin import SimpleITK as sitk import numpy as np import pydicom