From 4317bbe1acf91ad1290e41a1d5173e32ca63ea39 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 12:45:31 +0000 Subject: [PATCH 1/6] Check images dimensions and color channels Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 13fa47b0af..28917aaf39 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -923,6 +923,19 @@ def convert_to_rgb_array(self, raw_region, dtype: DtypeLike = np.uint8): # convert to numpy (if not already in numpy) raw_region = np.asarray(raw_region, dtype=dtype) + + # # check if the image has three dimensions (2D + color) + if raw_region.ndim != 3: + raise ValueError( + f"The input image dimension should be 3 but {raw_region.ndim} is given. " + "`WSIReader` is designed to work only with 2D colored images." + ) + + # check if the color channel is 3 (RGB) or 4 (RGBA) + if raw_region.shape[-1] not in [3, 4]: + raise ValueError( + f"There should be three or four color channels but {raw_region.shape[-1]} is given. " + "`WSIReader` is designed to work only with 2D colored images.") # remove alpha channel if exist (RGBA) if raw_region.shape[-1] > 3: From 9cd057fba9a15c2a5190edd801e17dbc9b60627b Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 12:46:22 +0000 Subject: [PATCH 2/6] Change imsave (depricated) to imwrite Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 589bd32369..1bfb0e458d 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -28,7 +28,7 @@ cucim, has_cucim = optional_import("cucim") has_cucim = has_cucim and hasattr(cucim, "CuImage") _, has_osl = optional_import("openslide") -imsave, has_tiff = optional_import("tifffile", name="imsave") +imwrite, has_tiff = optional_import("tifffile", name="imwrite") _, has_codec = optional_import("imagecodecs") has_tiff = has_tiff and has_codec @@ -98,7 +98,7 @@ def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): array = np.concatenate([array, 255 * np.ones_like(array[0])[np.newaxis]]).astype(np.uint8) img_rgb = array.transpose(1, 2, 0) - imsave(filename, img_rgb, shape=img_rgb.shape, tile=(16, 16)) + imwrite(filename, img_rgb, shape=img_rgb.shape, tile=(16, 16)) return filename From a8fc41c763d8ae244049e39303194111c1ab7608 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:02:11 +0000 Subject: [PATCH 3/6] Remove extra # Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 28917aaf39..2d2e876c92 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -924,7 +924,7 @@ def convert_to_rgb_array(self, raw_region, dtype: DtypeLike = np.uint8): # convert to numpy (if not already in numpy) raw_region = np.asarray(raw_region, dtype=dtype) - # # check if the image has three dimensions (2D + color) + # check if the image has three dimensions (2D + color) if raw_region.ndim != 3: raise ValueError( f"The input image dimension should be 3 but {raw_region.ndim} is given. " From 82093c1550f75998d560128b2832b6006daa97d8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:02:45 +0000 Subject: [PATCH 4/6] Add unittests for errors Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 1bfb0e458d..21ffe54595 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -27,7 +27,7 @@ cucim, has_cucim = optional_import("cucim") has_cucim = has_cucim and hasattr(cucim, "CuImage") -_, has_osl = optional_import("openslide") +openslide, has_osl = optional_import("openslide") imwrite, has_tiff = optional_import("tifffile", name="imwrite") _, has_codec = optional_import("imagecodecs") has_tiff = has_tiff and has_codec @@ -84,6 +84,8 @@ TEST_CASE_RGB_1 = [np.ones((3, 100, 100), dtype=np.uint8)] # CHW +TEST_CASE_ERROR_GRAY = [np.ones((16, 16), dtype=np.uint8)] # no color channel +TEST_CASE_ERROR_3D = [np.ones((16, 16, 16, 3), dtype=np.uint8)] # 3D + color def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): """ @@ -91,7 +93,7 @@ def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): Args: array: numpy ndarray with the shape of CxHxW and C==3 representing a RGB image - file_prefix: the filename to be used for the tiff file. '_RGB.tiff' or '_RGBA.tiff' will be appended to this filename. + filename: the filename to be used for the tiff file. '_RGB.tiff' or '_RGBA.tiff' will be appended to this filename. mode: RGB or RGBA """ if mode == "RGBA": @@ -102,6 +104,18 @@ def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): return filename +def save_gray_tiff(array: np.ndarray, filename: str): + """ + Save numpy array into a TIFF file + + Args: + array: numpy ndarray with any shape + filename: the filename to be used for the tiff file. + """ + img_gray = array + imwrite(filename, img_gray, shape=img_gray.shape, photometric='rgb') + + return filename @skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") def setUpModule(): # noqa: N802 @@ -170,6 +184,20 @@ def test_read_rgba(self, img_expected): self.assertIsNone(assert_array_equal(image["RGB"], img_expected)) self.assertIsNone(assert_array_equal(image["RGBA"], img_expected)) + + @parameterized.expand([TEST_CASE_ERROR_GRAY, TEST_CASE_ERROR_3D]) + @skipUnless(has_tiff, "Requires tifffile.") + def test_read_malformats(self, img_expected): + reader = WSIReader(self.backend) + file_path = save_gray_tiff( + img_expected, + os.path.join(os.path.dirname(__file__), "testing_data", f"temp_tiff_image_gray.tiff"), + ) + with self.assertRaises((RuntimeError, ValueError, openslide.OpenSlideError)): + with reader.read(file_path) as img_obj: + reader.get_data(img_obj) + + @parameterized.expand([TEST_CASE_TRANSFORM_0]) def test_with_dataloader(self, file_path, level, expected_spatial_shape, expected_shape): train_transform = Compose( From fc8b8e4a0c24b8ea2ea28f7747e186735aa7df72 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 14:44:31 +0000 Subject: [PATCH 5/6] Fix formatting Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 7 ++++--- tests/test_wsireader.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 2d2e876c92..a41615c908 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -923,19 +923,20 @@ def convert_to_rgb_array(self, raw_region, dtype: DtypeLike = np.uint8): # convert to numpy (if not already in numpy) raw_region = np.asarray(raw_region, dtype=dtype) - + # check if the image has three dimensions (2D + color) if raw_region.ndim != 3: raise ValueError( f"The input image dimension should be 3 but {raw_region.ndim} is given. " "`WSIReader` is designed to work only with 2D colored images." - ) + ) # check if the color channel is 3 (RGB) or 4 (RGBA) if raw_region.shape[-1] not in [3, 4]: raise ValueError( f"There should be three or four color channels but {raw_region.shape[-1]} is given. " - "`WSIReader` is designed to work only with 2D colored images.") + "`WSIReader` is designed to work only with 2D colored images." + ) # remove alpha channel if exist (RGBA) if raw_region.shape[-1] > 3: diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 21ffe54595..cb1590026d 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -84,8 +84,9 @@ TEST_CASE_RGB_1 = [np.ones((3, 100, 100), dtype=np.uint8)] # CHW -TEST_CASE_ERROR_GRAY = [np.ones((16, 16), dtype=np.uint8)] # no color channel -TEST_CASE_ERROR_3D = [np.ones((16, 16, 16, 3), dtype=np.uint8)] # 3D + color +TEST_CASE_ERROR_GRAY = [np.ones((16, 16), dtype=np.uint8)] # no color channel +TEST_CASE_ERROR_3D = [np.ones((16, 16, 16, 3), dtype=np.uint8)] # 3D + color + def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): """ @@ -104,19 +105,21 @@ def save_rgba_tiff(array: np.ndarray, filename: str, mode: str): return filename + def save_gray_tiff(array: np.ndarray, filename: str): """ Save numpy array into a TIFF file Args: - array: numpy ndarray with any shape + array: numpy ndarray with any shape filename: the filename to be used for the tiff file. """ img_gray = array - imwrite(filename, img_gray, shape=img_gray.shape, photometric='rgb') + imwrite(filename, img_gray, shape=img_gray.shape, photometric="rgb") return filename + @skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") def setUpModule(): # noqa: N802 hash_type = testing_data_config("images", FILE_KEY, "hash_type") @@ -184,20 +187,18 @@ def test_read_rgba(self, img_expected): self.assertIsNone(assert_array_equal(image["RGB"], img_expected)) self.assertIsNone(assert_array_equal(image["RGBA"], img_expected)) - @parameterized.expand([TEST_CASE_ERROR_GRAY, TEST_CASE_ERROR_3D]) @skipUnless(has_tiff, "Requires tifffile.") def test_read_malformats(self, img_expected): reader = WSIReader(self.backend) file_path = save_gray_tiff( img_expected, - os.path.join(os.path.dirname(__file__), "testing_data", f"temp_tiff_image_gray.tiff"), + os.path.join(os.path.dirname(__file__), "testing_data", "temp_tiff_image_gray.tiff"), ) with self.assertRaises((RuntimeError, ValueError, openslide.OpenSlideError)): with reader.read(file_path) as img_obj: reader.get_data(img_obj) - @parameterized.expand([TEST_CASE_TRANSFORM_0]) def test_with_dataloader(self, file_path, level, expected_spatial_shape, expected_shape): train_transform = Compose( From 8b38e900c916851f3a842a77cb392a93692a2ed8 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Tue, 22 Mar 2022 17:17:02 +0000 Subject: [PATCH 6/6] Remove test dependency on openslide Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index cb1590026d..6ee02143b8 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -192,10 +192,9 @@ def test_read_rgba(self, img_expected): def test_read_malformats(self, img_expected): reader = WSIReader(self.backend) file_path = save_gray_tiff( - img_expected, - os.path.join(os.path.dirname(__file__), "testing_data", "temp_tiff_image_gray.tiff"), + img_expected, os.path.join(os.path.dirname(__file__), "testing_data", "temp_tiff_image_gray.tiff") ) - with self.assertRaises((RuntimeError, ValueError, openslide.OpenSlideError)): + with self.assertRaises((RuntimeError, ValueError, openslide.OpenSlideError if has_osl else ValueError)): with reader.read(file_path) as img_obj: reader.get_data(img_obj)