From af092137598936863743520ed9f21f6bbdbf5759 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 6 Nov 2021 12:35:33 +0000 Subject: [PATCH 1/9] Add tifffile support to WSIReader Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 40 +++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index ddf6d9e563..04b2952b74 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -683,8 +683,10 @@ def __init__(self, backend: str = "OpenSlide", level: int = 0): self.wsi_reader, *_ = optional_import("openslide", name="OpenSlide") elif self.backend == "cucim": self.wsi_reader, *_ = optional_import("cucim", name="CuImage") + elif self.backend == "tifffile": + self.wsi_reader, *_ = optional_import("tifffile", name="TiffFile") else: - raise ValueError('`backend` should be either "cuCIM" or "OpenSlide"') + raise ValueError('`backend` should be "cuCIM", "OpenSlide", or "TiffFile') self.level = level def verify_suffix(self, filename: Union[Sequence[str], str]) -> bool: @@ -774,13 +776,22 @@ def _extract_region( level: int = 0, dtype: DtypeLike = np.uint8, ): - # reverse the order of dimensions for size and location to be compatible with image shape - location = location[::-1] - if size is None: - region = img_obj.read_region(location=location, level=level) + if self.backend == "tifffile": + region = img_obj.asarray(level=level) + img_obj.close() + if size is None: + region = region[location[0] :, location[1] :] + else: + region = region[location[0] : location[0] + size[0], location[1] : location[1] + size[1]] + else: - size = size[::-1] - region = img_obj.read_region(location=location, size=size, level=level) + # reverse the order of dimensions for size and location to be compatible with image shape + location = location[::-1] + if size is None: + region = img_obj.read_region(location=location, level=level) + else: + size = size[::-1] + region = img_obj.read_region(location=location, size=size, level=level) region = self.convert_to_rgb_array(region, dtype) return region @@ -790,15 +801,12 @@ def convert_to_rgb_array(self, raw_region, dtype: DtypeLike = np.uint8): if self.backend == "openslide": # convert to RGB raw_region = raw_region.convert("RGB") - # convert to numpy - raw_region = np.asarray(raw_region, dtype=dtype) - else: - num_channels = len(raw_region.channel_names) - # convert to numpy - raw_region = np.asarray(raw_region, dtype=dtype) - # remove alpha channel if exist (RGBA) - if num_channels > 3: - raw_region = raw_region[:, :, :3] + + # convert to numpy (if not already in numpy) + raw_region = np.asarray(raw_region, dtype=dtype) + # remove alpha channel if exist (RGBA) + if raw_region.shape[-1] > 3: + raw_region = raw_region[..., :3] return raw_region From 2876cc668db95f3413b2d5d7edb0e4972c4fff64 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 6 Nov 2021 12:35:51 +0000 Subject: [PATCH 2/9] Update unittests Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 7cd9efbf06..6354c8b2ec 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -170,5 +170,12 @@ def setUpClass(cls): cls.backend = "openslide" +@skipUnless(has_osl, "Requires OpenSlide") +class TestTiffFile(WSIReaderTests.Tests): + @classmethod + def setUpClass(cls): + cls.backend = "openslide" + + if __name__ == "__main__": unittest.main() From 19e60a848ef446657ea196ed54cc80bf809ae32c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 6 Nov 2021 12:36:32 +0000 Subject: [PATCH 3/9] Fix a typo Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/apps/pathology/transforms/spatial/array.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/apps/pathology/transforms/spatial/array.py b/monai/apps/pathology/transforms/spatial/array.py index b47e6fd015..4edf987610 100644 --- a/monai/apps/pathology/transforms/spatial/array.py +++ b/monai/apps/pathology/transforms/spatial/array.py @@ -28,9 +28,9 @@ class SplitOnGrid(Transform): If it's an integer, the value will be repeated for each dimension. Default is 2x2 patch_size: a tuple or an integer that defines the output patch sizes. If it's an integer, the value will be repeated for each dimension. - The default is (0, 0), where the patch size will be infered from the grid shape. + The default is (0, 0), where the patch size will be inferred from the grid shape. - Note: the shape of the input image is infered based on the first image used. + Note: the shape of the input image is inferred based on the first image used. """ def __init__( From 94d1e372fc6a30e25cab2de71c2c0e6149b5446a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 6 Nov 2021 13:09:32 +0000 Subject: [PATCH 4/9] Update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 04b2952b74..dee60dbd34 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -670,10 +670,14 @@ class WSIReader(ImageReader): Read whole slide images and extract patches. Args: - backend: backend library to load the images, available options: "OpenSlide" or "cuCIM". + backend: backend library to load the images, available options: "cuCIM", "OpenSlide" and "Tifffile". level: the whole slide image level at which the image is extracted. (default=0) - Note that this is overridden by the level argument in `get_data`. + This is overridden if the level argument is provided `get_data`. + Note: While ``cuCIM`` and ``OpenSlide`` both can load patches from large whole slide images + without loading the entire image into memory, ``TiffFile` needs to load the entire image into memory + before extracting any patch; thus, memory consideration is needed when using ``TiffFile` backend for + patch extraction. """ def __init__(self, backend: str = "OpenSlide", level: int = 0): From 1e1956d845aae407f914056d997cfbf8ac7de6ed Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sat, 6 Nov 2021 23:23:35 +0000 Subject: [PATCH 5/9] Update docstring Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index dee60dbd34..71da7c8625 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -674,9 +674,10 @@ class WSIReader(ImageReader): level: the whole slide image level at which the image is extracted. (default=0) This is overridden if the level argument is provided `get_data`. - Note: While ``cuCIM`` and ``OpenSlide`` both can load patches from large whole slide images - without loading the entire image into memory, ``TiffFile` needs to load the entire image into memory - before extracting any patch; thus, memory consideration is needed when using ``TiffFile` backend for + Note: + While "cucim" and "OpenSlide" backends both can load patches from large whole slide images + without loading the entire image into memory, "Tifffile" backend needs to load the entire image into memory + before extracting any patch; thus, memory consideration is needed when using "Tifffile" backend for patch extraction. """ From f3c93ff4263207e0571fb5f7ad6aeee0c2df4699 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 7 Nov 2021 10:18:06 -0500 Subject: [PATCH 6/9] Fix the backend name Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 2 +- tests/test_wsireader.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 00fef8848d..98a763a9e1 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -678,7 +678,7 @@ class WSIReader(ImageReader): Args: backend: backend library to load the images, available options: "cuCIM", "OpenSlide" and "Tifffile". level: the whole slide image level at which the image is extracted. (default=0) - This is overridden if the level argument is provided `get_data`. + This is overridden if the level argument is provided in `get_data`. Note: While "cucim" and "OpenSlide" backends both can load patches from large whole slide images diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 089fcd4289..29f1a76da7 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -175,7 +175,7 @@ def setUpClass(cls): class TestTiffFile(WSIReaderTests.Tests): @classmethod def setUpClass(cls): - cls.backend = "openslide" + cls.backend = "tifffile" if __name__ == "__main__": From 2989ae0136fe22ee51ae0e29f307d7b242ea499a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Sun, 7 Nov 2021 10:19:38 -0500 Subject: [PATCH 7/9] Fix a typo Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index 29f1a76da7..e100138c6e 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -171,7 +171,7 @@ def setUpClass(cls): cls.backend = "openslide" -@skipUnless(has_osl, "Requires OpenSlide") +@skipUnless(has_tiff, "Requires TiffFile") class TestTiffFile(WSIReaderTests.Tests): @classmethod def setUpClass(cls): From 9c9b30a9b772f626916f5b2e29bdeabc42f80de9 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 10 Nov 2021 21:45:34 +0000 Subject: [PATCH 8/9] Add test for multiple get_data call Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- tests/test_wsireader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index e100138c6e..61eb2d82ce 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -112,6 +112,8 @@ def test_read_whole_image(self, file_path, level, expected_shape): def test_read_region(self, file_path, patch_info, expected_img): reader = WSIReader(self.backend) img_obj = reader.read(file_path) + # Read twice to check multiple calls + img = reader.get_data(img_obj, **patch_info)[0] img = reader.get_data(img_obj, **patch_info)[0] self.assertTupleEqual(img.shape, expected_img.shape) self.assertIsNone(assert_array_equal(img, expected_img)) From ef8146dedf12eb81baa57032c1cd1a19b55c2ffd Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+drbeh@users.noreply.github.com> Date: Wed, 10 Nov 2021 21:45:58 +0000 Subject: [PATCH 9/9] Change to context manager for tifffile object Signed-off-by: Behrooz <3968947+drbeh@users.noreply.github.com> --- monai/data/image_reader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 98a763a9e1..c7d77e0781 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -788,8 +788,8 @@ def _extract_region( dtype: DtypeLike = np.uint8, ): if self.backend == "tifffile": - region = img_obj.asarray(level=level) - img_obj.close() + with img_obj: + region = img_obj.asarray(level=level) if size is None: region = region[location[0] :, location[1] :] else: