From 30b6ea8798f66d0b6fc07fd914e814574c925b3c Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Sat, 27 Mar 2021 18:29:23 -0400 Subject: [PATCH 1/6] Update WSIReader for CuCIM Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/data/image_reader.py | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 67425c0f47..685fa787ee 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -713,14 +713,6 @@ def get_data( grid_shape: (row, columns) tuple define a grid to extract patches on that patch_size: (heigsht, width) the size of extracted patches at the given level """ - if size is None: - if location == (0, 0): - # the maximum size is set to WxH - size = (img.shape[0] // (2 ** level), img.shape[1] // (2 ** level)) - print(f"Reading the whole image at level={level} with shape={size}") - else: - raise ValueError("Size need to be provided to extract the region!") - region = self._extract_region(img, location=location, size=size, level=level, dtype=dtype) metadata: Dict = {} @@ -740,19 +732,36 @@ def get_data( def _extract_region( self, img_obj, - size: Tuple[int, int], + size: Optional[Tuple[int, int]], location: Tuple[int, int] = (0, 0), level: int = 0, dtype: DtypeLike = np.uint8, ): # reverse the order of dimensions for size and location to be compatible with image shape - size = size[::-1] location = location[::-1] - region = img_obj.read_region(location=location, size=size, level=level) + 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 + + def convert_to_rgb_array(self, region, dtype): + """Convert to RGB mode and numpy array""" if self.reader_lib == "openslide": + # convert to RGB region = region.convert("RGB") - # convert to numpy - region = np.asarray(region, dtype=dtype) + # convert to numpy + region = np.asarray(region, dtype=dtype) + elif self.reader_lib == "cucim": + num_channels = len(region.channel_names) + # convert to numpy + region = np.asarray(region, dtype=dtype) + # remove alpha channel if exist (RGBA) + if num_channels > 3: + region = region[:, :, :3] return region From b1abbff8febd6f39ca7ca37088872d046729be08 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Mon, 29 Mar 2021 21:39:15 -0400 Subject: [PATCH 2/6] Add tests for RGBA with CuCIM Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- tests/test_cuimage_reader.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_cuimage_reader.py b/tests/test_cuimage_reader.py index 036d5ad1ae..1b0293f159 100644 --- a/tests/test_cuimage_reader.py +++ b/tests/test_cuimage_reader.py @@ -11,7 +11,7 @@ from monai.utils import optional_import _, has_cim = optional_import("cucim") - +PILImage, has_pil = optional_import("PIL.Image") FILE_URL = "http://openslide.cs.cmu.edu/download/openslide-testdata/Generic-TIFF/CMU-1.tiff" FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", os.path.basename(FILE_URL)) @@ -62,6 +62,14 @@ np.array([[[[239]], [[239]], [[239]]], [[[243]], [[243]], [[243]]]]), ] +TEST_CASE_RGB_0 = [ + np.ones((3, 2, 2), dtype=np.uint8), # CHW +] + +TEST_CASE_RGB_1 = [ + np.ones((3, 100, 100), dtype=np.uint8), # CHW +] + class TestCuCIMReader(unittest.TestCase): @skipUnless(has_cim, "Requires CuCIM") @@ -91,6 +99,32 @@ def test_read_patches(self, file_path, patch_info, expected_img): self.assertTupleEqual(img.shape, expected_img.shape) self.assertIsNone(assert_array_equal(img, expected_img)) + @parameterized.expand([TEST_CASE_RGB_0, TEST_CASE_RGB_1]) + @skipUnless(has_pil, "Requires PIL") + def test_read_rgba(self, img_expected): + image = {} + reader = WSIReader("cuCIM") + for mode in ["RGB", "RGBA"]: + file_path = self.create_rgba_image(img_expected, "test_cu_tiff_image", mode=mode) + img_obj = reader.read(file_path) + image[mode], _ = reader.get_data(img_obj) + + self.assertIsNone(assert_array_equal(image["RGB"], img_expected)) + self.assertIsNone(assert_array_equal(image["RGBA"], img_expected)) + + def create_rgba_image(self, array: np.ndarray, filename_prefix: str, mode: str): + file_path = os.path.join(os.path.dirname(__file__), "testing_data", f"{filename_prefix}_{mode}.tiff") + + if mode == "RGBA": + array = np.concatenate([array, 255 * np.ones_like(array[0])[np.newaxis]]).astype(np.uint8) + + img_rgb = array.transpose(1, 2, 0) + + image = PILImage.fromarray(img_rgb, mode=mode) + image.save(file_path) + + return file_path + if __name__ == "__main__": unittest.main() From 393954cdb8613a26f97017f6c3331b2390174d8a Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Tue, 30 Mar 2021 11:27:56 -0400 Subject: [PATCH 3/6] Address issues Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/data/image_reader.py | 40 ++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 685fa787ee..13d334a329 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -20,7 +20,7 @@ from monai.config import DtypeLike, KeysCollection from monai.data.utils import correct_nifti_header_if_necessary from monai.transforms.utility.array import EnsureChannelFirst -from monai.utils import ensure_tuple, optional_import +from monai.utils import ensure_tuple, ensure_tuple_rep, optional_import from .utils import is_supported_format @@ -697,7 +697,7 @@ def get_data( level: int = 0, dtype: DtypeLike = np.uint8, grid_shape: Tuple[int, int] = (1, 1), - patch_size: Optional[int] = None, + patch_size: Optional[Union[int, Tuple[int, int]]] = None, ): """ Extract regions as numpy array from WSI image and return them. @@ -711,8 +711,16 @@ def get_data( level: the level number, or list of level numbers (default=0) dtype: the data type of output image grid_shape: (row, columns) tuple define a grid to extract patches on that - patch_size: (heigsht, width) the size of extracted patches at the given level + patch_size: (height, width) the size of extracted patches at the given level """ + + if self.reader_lib == "openslide" and size is None: + # the maximum size is set to WxH + size = ( + img.shape[0] // (2 ** level) - location[0], + img.shape[1] // (2 ** level) - location[1], + ) + region = self._extract_region(img, location=location, size=size, level=level, dtype=dtype) metadata: Dict = {} @@ -723,8 +731,12 @@ def get_data( if patch_size is None: patches = region else: + tuple_patch_size = ensure_tuple_rep(patch_size, 2) patches = self._extract_patches( - region, patch_size=(patch_size, patch_size), grid_shape=grid_shape, dtype=dtype + region, + patch_size=tuple_patch_size, # type: ignore + grid_shape=grid_shape, + dtype=dtype, ) return patches, metadata @@ -736,7 +748,7 @@ def _extract_region( location: Tuple[int, int] = (0, 0), level: int = 0, dtype: DtypeLike = np.uint8, - ): + ) -> np.ndarray: # reverse the order of dimensions for size and location to be compatible with image shape location = location[::-1] if size is None: @@ -748,22 +760,26 @@ def _extract_region( region = self.convert_to_rgb_array(region, dtype) return region - def convert_to_rgb_array(self, region, dtype): + def convert_to_rgb_array( + self, + raw_region, + dtype: DtypeLike = np.uint8, + ) -> np.ndarray: """Convert to RGB mode and numpy array""" if self.reader_lib == "openslide": # convert to RGB - region = region.convert("RGB") + raw_region = raw_region.convert("RGB") # convert to numpy - region = np.asarray(region, dtype=dtype) + raw_region = np.asarray(raw_region, dtype=dtype) elif self.reader_lib == "cucim": - num_channels = len(region.channel_names) + num_channels = len(raw_region.channel_names) # convert to numpy - region = np.asarray(region, dtype=dtype) + raw_region = np.asarray(raw_region, dtype=dtype) # remove alpha channel if exist (RGBA) if num_channels > 3: - region = region[:, :, :3] + raw_region = raw_region[:, :, :3] - return region + return raw_region def _extract_patches( self, From 4bec87b7e2e355bf4ae5e1679a347c27510b94f5 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Tue, 30 Mar 2021 12:25:41 -0400 Subject: [PATCH 4/6] Minor change for typing Signed-off-by: Behrooz <3968947+behxyz@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 cbd1ec7528..59b0b43b85 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -771,14 +771,14 @@ def convert_to_rgb_array( raw_region = raw_region.convert("RGB") # convert to numpy raw_region = np.asarray(raw_region, dtype=dtype) - elif self.reader_lib == "cucim": + 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] - + return raw_region def _extract_patches( From 54b19054c6bfbeafecc8994ca0f4cf2fb3b270e6 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:30:00 -0400 Subject: [PATCH 5/6] Fix format Signed-off-by: Behrooz <3968947+behxyz@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 59b0b43b85..40dfedfbe3 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -778,7 +778,7 @@ def convert_to_rgb_array( # remove alpha channel if exist (RGBA) if num_channels > 3: raw_region = raw_region[:, :, :3] - + return raw_region def _extract_patches( From 5fbe12dcd6d3db21b2a74fde50aff17bf162aa15 Mon Sep 17 00:00:00 2001 From: Behrooz <3968947+behxyz@users.noreply.github.com> Date: Tue, 30 Mar 2021 13:47:03 -0400 Subject: [PATCH 6/6] Remove ndarray type hint Signed-off-by: Behrooz <3968947+behxyz@users.noreply.github.com> --- monai/data/image_reader.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 40dfedfbe3..d41b779a1e 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -253,7 +253,7 @@ def _get_meta_dict(self, img) -> Dict: meta_dict["direction"] = itk.array_from_matrix(img.GetDirection()) return meta_dict - def _get_affine(self, img) -> np.ndarray: + def _get_affine(self, img): """ Get or construct the affine matrix of the image, it can be used to correct spacing, orientation or execute spatial transforms. @@ -274,7 +274,7 @@ def _get_affine(self, img) -> np.ndarray: affine[(slice(-1), -1)] = origin return affine - def _get_spatial_shape(self, img) -> np.ndarray: + def _get_spatial_shape(self, img): """ Get the spatial shape of image data, it doesn't contain the channel dim. @@ -406,7 +406,7 @@ def _get_meta_dict(self, img) -> Dict: """ return dict(img.header) - def _get_affine(self, img) -> np.ndarray: + def _get_affine(self, img): """ Get the affine matrix of the image, it can be used to correct spacing, orientation or execute spatial transforms. @@ -417,7 +417,7 @@ def _get_affine(self, img) -> np.ndarray: """ return np.array(img.affine, copy=True) - def _get_spatial_shape(self, img) -> np.ndarray: + def _get_spatial_shape(self, img): """ Get the spatial shape of image data, it doesn't contain the channel dim. @@ -430,7 +430,7 @@ def _get_spatial_shape(self, img) -> np.ndarray: # the img data should have no channel dim or the last dim is channel return np.asarray(img.header["dim"][1 : spatial_rank + 1]) - def _get_array_data(self, img) -> np.ndarray: + def _get_array_data(self, img): """ Get the raw array data of the image, converted to Numpy array. @@ -623,7 +623,7 @@ def _get_meta_dict(self, img) -> Dict: "height": img.height, } - def _get_spatial_shape(self, img) -> np.ndarray: + def _get_spatial_shape(self, img): """ Get the spatial shape of image data, it doesn't contain the channel dim. Args: @@ -748,7 +748,7 @@ def _extract_region( location: Tuple[int, int] = (0, 0), level: int = 0, dtype: DtypeLike = np.uint8, - ) -> np.ndarray: + ): # reverse the order of dimensions for size and location to be compatible with image shape location = location[::-1] if size is None: @@ -764,7 +764,7 @@ def convert_to_rgb_array( self, raw_region, dtype: DtypeLike = np.uint8, - ) -> np.ndarray: + ): """Convert to RGB mode and numpy array""" if self.reader_lib == "openslide": # convert to RGB