diff --git a/monai/apps/pathology/data/datasets.py b/monai/apps/pathology/data/datasets.py index 71f3214ea4..a25e54e999 100644 --- a/monai/apps/pathology/data/datasets.py +++ b/monai/apps/pathology/data/datasets.py @@ -17,11 +17,12 @@ from monai.data import Dataset, SmartCacheDataset from monai.data.image_reader import WSIReader -from monai.utils import ensure_tuple_rep +from monai.utils import deprecated, ensure_tuple_rep __all__ = ["PatchWSIDataset", "SmartCachePatchWSIDataset", "MaskedInferenceWSIDataset"] +@deprecated(since="0.8", msg_suffix="use `monai.data.PatchWSIDataset` instead.") class PatchWSIDataset(Dataset): """ This dataset reads whole slide images, extracts regions, and creates patches. @@ -103,6 +104,7 @@ def __getitem__(self, index): return patches +@deprecated(since="0.8", msg_suffix="use `monai.data.SmartCacheDataset` with `monai.data.PatchWSIDataset` instead.") class SmartCachePatchWSIDataset(SmartCacheDataset): """Add SmartCache functionality to `PatchWSIDataset`. @@ -177,6 +179,7 @@ def __init__( ) +@deprecated(since="0.8", msg_suffix="use `monai.data.MaskedPatchWSIDataset` instead.") class MaskedInferenceWSIDataset(Dataset): """ This dataset load the provided foreground masks at an arbitrary resolution level, diff --git a/monai/apps/pathology/handlers/prob_map_producer.py b/monai/apps/pathology/handlers/prob_map_producer.py index 62507dc0cb..d5b1b50c47 100644 --- a/monai/apps/pathology/handlers/prob_map_producer.py +++ b/monai/apps/pathology/handlers/prob_map_producer.py @@ -27,7 +27,10 @@ @deprecated( since="0.8", - msg_suffix="use `monai.handler.ProbMapProducer` (with `monai.data.wsi_dataset.SlidingPatchWSIDataset`) instead.", + msg_suffix=( + "use `monai.handler.ProbMapProducer` (with `monai.data.wsi_dataset.MaskedPatchWSIDataset` or " + "`monai.data.wsi_dataset.SlidingPatchWSIDataset`) instead." + ), ) class ProbMapProducer: """ diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index ec644afaf6..904a5bb2d2 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -28,7 +28,7 @@ orientation_ras_lps, ) from monai.transforms.utility.array import EnsureChannelFirst -from monai.utils import MetaKeys, SpaceKeys, ensure_tuple, ensure_tuple_rep, optional_import, require_pkg +from monai.utils import MetaKeys, SpaceKeys, deprecated, ensure_tuple, ensure_tuple_rep, optional_import, require_pkg if TYPE_CHECKING: import itk @@ -1218,6 +1218,7 @@ def _get_spatial_shape(self, img): return np.asarray((img.width, img.height)) +@deprecated(since="0.8", msg_suffix="use `monai.wsi_reader.WSIReader` instead.") class WSIReader(ImageReader): """ Read whole slide images and extract patches. diff --git a/tests/min_tests.py b/tests/min_tests.py index f33af553f3..18e7b136fe 100644 --- a/tests/min_tests.py +++ b/tests/min_tests.py @@ -162,7 +162,6 @@ def run_testsuit(): "test_vitautoenc", "test_write_metrics_reports", "test_wsireader", - "test_wsireader_new", "test_zoom", "test_zoom_affine", "test_zoomd", diff --git a/tests/test_patch_wsi_dataset.py b/tests/test_patch_wsi_dataset.py index 20d7f22988..b08e380162 100644 --- a/tests/test_patch_wsi_dataset.py +++ b/tests/test_patch_wsi_dataset.py @@ -17,20 +17,25 @@ from numpy.testing import assert_array_equal from parameterized import parameterized -from monai.apps.pathology.data import PatchWSIDataset -from monai.utils import optional_import +from monai.apps.pathology.data import PatchWSIDataset as PatchWSIDatasetDeprecated +from monai.data import PatchWSIDataset +from monai.data.wsi_reader import CuCIMWSIReader, OpenSlideWSIReader +from monai.utils import deprecated, optional_import from tests.utils import download_url_or_skip_test, testing_data_config -_cucim, has_cim = optional_import("cucim") -has_cim = has_cim and hasattr(_cucim, "CuImage") -_, has_osl = optional_import("openslide") +cucim, has_cim = optional_import("cucim") +has_cim = has_cim and hasattr(cucim, "CuImage") +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 FILE_KEY = "wsi_img" FILE_URL = testing_data_config("images", FILE_KEY, "url") base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff" FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension) -TEST_CASE_0 = [ +TEST_CASE_DEP_0 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -41,7 +46,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_0_L1 = [ +TEST_CASE_DEP_0_L1 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -53,7 +58,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_0_L2 = [ +TEST_CASE_DEP_0_L2 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -66,7 +71,7 @@ ] -TEST_CASE_1 = [ +TEST_CASE_DEP_1 = [ { "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}], "region_size": (8, 8), @@ -83,7 +88,7 @@ ] -TEST_CASE_1_L0 = [ +TEST_CASE_DEP_1_L0 = [ { "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}], "region_size": (8, 8), @@ -101,7 +106,7 @@ ] -TEST_CASE_1_L1 = [ +TEST_CASE_DEP_1_L1 = [ { "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}], "region_size": (8, 8), @@ -117,7 +122,7 @@ {"image": np.array([[[246]], [[242]], [[243]]], dtype=np.uint8), "label": np.array([[[1]]])}, ], ] -TEST_CASE_2 = [ +TEST_CASE_DEP_2 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": 1, @@ -128,7 +133,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_3 = [ +TEST_CASE_DEP_3 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [[[0, 1], [1, 0]]]}], "region_size": 1, @@ -139,7 +144,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}], ] -TEST_CASE_OPENSLIDE_0 = [ +TEST_CASE_DEP_OPENSLIDE_0 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -150,7 +155,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_OPENSLIDE_0_L0 = [ +TEST_CASE_DEP_OPENSLIDE_0_L0 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -162,7 +167,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_OPENSLIDE_0_L1 = [ +TEST_CASE_DEP_OPENSLIDE_0_L1 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -175,7 +180,7 @@ ] -TEST_CASE_OPENSLIDE_0_L2 = [ +TEST_CASE_DEP_OPENSLIDE_0_L2 = [ { "data": [{"image": FILE_PATH, "location": [0, 0], "label": [1]}], "region_size": (1, 1), @@ -187,7 +192,7 @@ [{"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1]]])}], ] -TEST_CASE_OPENSLIDE_1 = [ +TEST_CASE_DEP_OPENSLIDE_1 = [ { "data": [{"image": FILE_PATH, "location": [10004, 20004], "label": [0, 0, 0, 1]}], "region_size": (8, 8), @@ -204,27 +209,99 @@ ] -class TestPatchWSIDataset(unittest.TestCase): - def setUp(self): - hash_type = testing_data_config("images", FILE_KEY, "hash_type") - hash_val = testing_data_config("images", FILE_KEY, "hash_val") - download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val) +TEST_CASE_0 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1], "patch_level": 0}], "patch_size": (1, 1)}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, +] + +TEST_CASE_0_L1 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, +] +TEST_CASE_0_L2 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, +] +TEST_CASE_1 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "patch_size": 1, "label": [1]}]}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, +] + +TEST_CASE_2 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": 1, "patch_level": 0}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, +] + +TEST_CASE_3 = [ + {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]}], "patch_size": 1}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, +] + +TEST_CASE_4 = [ + { + "data": [ + {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]}, + {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[1, 0], [0, 0]]]}, + ], + "patch_size": 1, + }, + [ + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1, 0], [0, 0]]])}, + ], +] + +TEST_CASE_5 = [ + { + "data": [ + { + "image": FILE_PATH, + "patch_location": [0, 0], + "label": [[[0, 1], [1, 0]]], + "patch_size": 1, + "patch_level": 1, + }, + { + "image": FILE_PATH, + "patch_location": [100, 100], + "label": [[[1, 0], [0, 0]]], + "patch_size": 1, + "patch_level": 1, + }, + ] + }, + [ + {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, + {"image": np.array([[[243]], [[243]], [[243]]], dtype=np.uint8), "label": np.array([[[1, 0], [0, 0]]])}, + ], +] + + +@skipUnless(has_cim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") +def setUpModule(): + hash_type = testing_data_config("images", FILE_KEY, "hash_type") + hash_val = testing_data_config("images", FILE_KEY, "hash_val") + download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val) + + +@deprecated(since="0.8", msg_suffix="use tests for `monai.data.PatchWSIDataset` instead, `PatchWSIDatasetTests`.") +class TestPatchWSIDatasetDeprecated(unittest.TestCase): @parameterized.expand( [ - TEST_CASE_0, - TEST_CASE_0_L1, - TEST_CASE_0_L2, - TEST_CASE_1, - TEST_CASE_1_L0, - TEST_CASE_1_L1, - TEST_CASE_2, - TEST_CASE_3, + TEST_CASE_DEP_0, + TEST_CASE_DEP_0_L1, + TEST_CASE_DEP_0_L2, + TEST_CASE_DEP_1, + TEST_CASE_DEP_1_L0, + TEST_CASE_DEP_1_L1, + TEST_CASE_DEP_2, + TEST_CASE_DEP_3, ] ) @skipUnless(has_cim, "Requires CuCIM") def test_read_patches_cucim(self, input_parameters, expected): - dataset = PatchWSIDataset(**input_parameters) + dataset = PatchWSIDatasetDeprecated(**input_parameters) samples = dataset[0] for i in range(len(samples)): self.assertTupleEqual(samples[i]["label"].shape, expected[i]["label"].shape) @@ -234,16 +311,16 @@ def test_read_patches_cucim(self, input_parameters, expected): @parameterized.expand( [ - TEST_CASE_OPENSLIDE_0, - TEST_CASE_OPENSLIDE_0_L0, - TEST_CASE_OPENSLIDE_0_L1, - TEST_CASE_OPENSLIDE_0_L2, - TEST_CASE_OPENSLIDE_1, + TEST_CASE_DEP_OPENSLIDE_0, + TEST_CASE_DEP_OPENSLIDE_0_L0, + TEST_CASE_DEP_OPENSLIDE_0_L1, + TEST_CASE_DEP_OPENSLIDE_0_L2, + TEST_CASE_DEP_OPENSLIDE_1, ] ) @skipUnless(has_osl, "Requires OpenSlide") def test_read_patches_openslide(self, input_parameters, expected): - dataset = PatchWSIDataset(**input_parameters) + dataset = PatchWSIDatasetDeprecated(**input_parameters) samples = dataset[0] for i in range(len(samples)): self.assertTupleEqual(samples[i]["label"].shape, expected[i]["label"].shape) @@ -252,5 +329,72 @@ def test_read_patches_openslide(self, input_parameters, expected): self.assertIsNone(assert_array_equal(samples[i]["image"], expected[i]["image"])) +class PatchWSIDatasetTests: + class Tests(unittest.TestCase): + backend = None + + @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + def test_read_patches_str(self, input_parameters, expected): + dataset = PatchWSIDataset(reader=self.backend, **input_parameters) + sample = dataset[0] + self.assertTupleEqual(sample["label"].shape, expected["label"].shape) + self.assertTupleEqual(sample["image"].shape, expected["image"].shape) + self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) + self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) + + @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + def test_read_patches_class(self, input_parameters, expected): + if self.backend == "openslide": + reader = OpenSlideWSIReader + elif self.backend == "cucim": + reader = CuCIMWSIReader + else: + raise ValueError("Unsupported backend: {self.backend}") + dataset = PatchWSIDataset(reader=reader, **input_parameters) + sample = dataset[0] + self.assertTupleEqual(sample["label"].shape, expected["label"].shape) + self.assertTupleEqual(sample["image"].shape, expected["image"].shape) + self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) + self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) + + @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) + def test_read_patches_object(self, input_parameters, expected): + if self.backend == "openslide": + reader = OpenSlideWSIReader(level=input_parameters.get("patch_level", 0)) + elif self.backend == "cucim": + reader = CuCIMWSIReader(level=input_parameters.get("patch_level", 0)) + else: + raise ValueError("Unsupported backend: {self.backend}") + dataset = PatchWSIDataset(reader=reader, **input_parameters) + sample = dataset[0] + self.assertTupleEqual(sample["label"].shape, expected["label"].shape) + self.assertTupleEqual(sample["image"].shape, expected["image"].shape) + self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) + self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) + + @parameterized.expand([TEST_CASE_4, TEST_CASE_5]) + def test_read_patches_str_multi(self, input_parameters, expected): + dataset = PatchWSIDataset(reader=self.backend, **input_parameters) + for i in range(len(dataset)): + self.assertTupleEqual(dataset[i]["label"].shape, expected[i]["label"].shape) + self.assertTupleEqual(dataset[i]["image"].shape, expected[i]["image"].shape) + self.assertIsNone(assert_array_equal(dataset[i]["label"], expected[i]["label"])) + self.assertIsNone(assert_array_equal(dataset[i]["image"], expected[i]["image"])) + + +@skipUnless(has_cim, "Requires cucim") +class TestPatchWSIDatasetCuCIM(PatchWSIDatasetTests.Tests): + @classmethod + def setUpClass(cls): + cls.backend = "cucim" + + +@skipUnless(has_osl, "Requires openslide") +class TestPatchWSIDatasetOpenSlide(PatchWSIDatasetTests.Tests): + @classmethod + def setUpClass(cls): + cls.backend = "openslide" + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_patch_wsi_dataset_new.py b/tests/test_patch_wsi_dataset_new.py deleted file mode 100644 index fee8a03068..0000000000 --- a/tests/test_patch_wsi_dataset_new.py +++ /dev/null @@ -1,181 +0,0 @@ -# Copyright (c) MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest -from unittest import skipUnless - -import numpy as np -from numpy.testing import assert_array_equal -from parameterized import parameterized - -from monai.data import PatchWSIDataset -from monai.data.wsi_reader import CuCIMWSIReader, OpenSlideWSIReader -from monai.utils import optional_import -from tests.utils import download_url_or_skip_test, testing_data_config - -cucim, has_cucim = optional_import("cucim") -has_cucim = has_cucim and hasattr(cucim, "CuImage") -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 - -FILE_KEY = "wsi_img" -FILE_URL = testing_data_config("images", FILE_KEY, "url") -base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff" -FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension) - -TEST_CASE_0 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1], "patch_level": 0}], "patch_size": (1, 1)}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, -] - -TEST_CASE_0_L1 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, -] - -TEST_CASE_0_L2 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": (1, 1), "patch_level": 1}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, -] -TEST_CASE_1 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "patch_size": 1, "label": [1]}]}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, -] - -TEST_CASE_2 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [1]}], "patch_size": 1, "patch_level": 0}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([1])}, -] - -TEST_CASE_3 = [ - {"data": [{"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]}], "patch_size": 1}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, -] - -TEST_CASE_4 = [ - { - "data": [ - {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[0, 1], [1, 0]]]}, - {"image": FILE_PATH, "patch_location": [0, 0], "label": [[[1, 0], [0, 0]]]}, - ], - "patch_size": 1, - }, - [ - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[1, 0], [0, 0]]])}, - ], -] - -TEST_CASE_5 = [ - { - "data": [ - { - "image": FILE_PATH, - "patch_location": [0, 0], - "label": [[[0, 1], [1, 0]]], - "patch_size": 1, - "patch_level": 1, - }, - { - "image": FILE_PATH, - "patch_location": [100, 100], - "label": [[[1, 0], [0, 0]]], - "patch_size": 1, - "patch_level": 1, - }, - ] - }, - [ - {"image": np.array([[[239]], [[239]], [[239]]], dtype=np.uint8), "label": np.array([[[0, 1], [1, 0]]])}, - {"image": np.array([[[243]], [[243]], [[243]]], dtype=np.uint8), "label": np.array([[[1, 0], [0, 0]]])}, - ], -] - - -@skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") -def setUpModule(): - hash_type = testing_data_config("images", FILE_KEY, "hash_type") - hash_val = testing_data_config("images", FILE_KEY, "hash_val") - download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val) - - -class PatchWSIDatasetTests: - class Tests(unittest.TestCase): - backend = None - - @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) - def test_read_patches_str(self, input_parameters, expected): - dataset = PatchWSIDataset(reader=self.backend, **input_parameters) - sample = dataset[0] - self.assertTupleEqual(sample["label"].shape, expected["label"].shape) - self.assertTupleEqual(sample["image"].shape, expected["image"].shape) - self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) - self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) - - @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) - def test_read_patches_class(self, input_parameters, expected): - if self.backend == "openslide": - reader = OpenSlideWSIReader - elif self.backend == "cucim": - reader = CuCIMWSIReader - else: - raise ValueError("Unsupported backend: {self.backend}") - dataset = PatchWSIDataset(reader=reader, **input_parameters) - sample = dataset[0] - self.assertTupleEqual(sample["label"].shape, expected["label"].shape) - self.assertTupleEqual(sample["image"].shape, expected["image"].shape) - self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) - self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) - - @parameterized.expand([TEST_CASE_0, TEST_CASE_0_L1, TEST_CASE_0_L2, TEST_CASE_1, TEST_CASE_2, TEST_CASE_3]) - def test_read_patches_object(self, input_parameters, expected): - if self.backend == "openslide": - reader = OpenSlideWSIReader(level=input_parameters.get("patch_level", 0)) - elif self.backend == "cucim": - reader = CuCIMWSIReader(level=input_parameters.get("patch_level", 0)) - else: - raise ValueError("Unsupported backend: {self.backend}") - dataset = PatchWSIDataset(reader=reader, **input_parameters) - sample = dataset[0] - self.assertTupleEqual(sample["label"].shape, expected["label"].shape) - self.assertTupleEqual(sample["image"].shape, expected["image"].shape) - self.assertIsNone(assert_array_equal(sample["label"], expected["label"])) - self.assertIsNone(assert_array_equal(sample["image"], expected["image"])) - - @parameterized.expand([TEST_CASE_4, TEST_CASE_5]) - def test_read_patches_str_multi(self, input_parameters, expected): - dataset = PatchWSIDataset(reader=self.backend, **input_parameters) - for i in range(len(dataset)): - self.assertTupleEqual(dataset[i]["label"].shape, expected[i]["label"].shape) - self.assertTupleEqual(dataset[i]["image"].shape, expected[i]["image"].shape) - self.assertIsNone(assert_array_equal(dataset[i]["label"], expected[i]["label"])) - self.assertIsNone(assert_array_equal(dataset[i]["image"], expected[i]["image"])) - - -@skipUnless(has_cucim, "Requires cucim") -class TestPatchWSIDatasetCuCIM(PatchWSIDatasetTests.Tests): - @classmethod - def setUpClass(cls): - cls.backend = "cucim" - - -@skipUnless(has_osl, "Requires openslide") -class TestPatchWSIDatasetOpenSlide(PatchWSIDatasetTests.Tests): - @classmethod - def setUpClass(cls): - cls.backend = "openslide" - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index a0a076b682..1576a16de4 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -14,16 +14,16 @@ from unittest import skipUnless import numpy as np -import torch from numpy.testing import assert_array_equal from parameterized import parameterized from monai.data import DataLoader, Dataset -from monai.data.image_reader import WSIReader +from monai.data.image_reader import WSIReader as WSIReaderDeprecated +from monai.data.wsi_reader import WSIReader from monai.transforms import Compose, FromMetaTensord, LoadImaged, ToTensord -from monai.utils import first, optional_import +from monai.utils import deprecated, first, optional_import from monai.utils.enums import PostFix -from tests.utils import download_url_or_skip_test, testing_data_config +from tests.utils import assert_allclose, download_url_or_skip_test, testing_data_config cucim, has_cucim = optional_import("cucim") has_cucim = has_cucim and hasattr(cucim, "CuImage") @@ -44,19 +44,19 @@ TEST_CASE_TRANSFORM_0 = [FILE_PATH, 4, (HEIGHT // 16, WIDTH // 16), (1, 3, HEIGHT // 16, WIDTH // 16)] -TEST_CASE_1 = [ +TEST_CASE_DEP_1 = [ FILE_PATH, {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0}, np.array([[[246], [246]], [[246], [246]], [[246], [246]]]), ] -TEST_CASE_2 = [ +TEST_CASE_DEP_2 = [ FILE_PATH, {"location": (0, 0), "size": (2, 1), "level": 2}, np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), ] -TEST_CASE_3 = [ +TEST_CASE_DEP_3 = [ FILE_PATH, {"location": (0, 0), "size": (8, 8), "level": 2, "grid_shape": (2, 1), "patch_size": 2}, np.array( @@ -67,18 +67,58 @@ ), ] -TEST_CASE_4 = [ +TEST_CASE_DEP_4 = [ FILE_PATH, {"location": (0, 0), "size": (8, 8), "level": 2, "grid_shape": (2, 1), "patch_size": 1}, np.array([[[[239]], [[239]], [[239]]], [[[243]], [[243]], [[243]]]]), ] -TEST_CASE_5 = [ +TEST_CASE_DEP_5 = [ FILE_PATH, {"location": (HEIGHT - 2, WIDTH - 2), "level": 0, "grid_shape": (1, 1)}, np.array([[[239, 239], [239, 239]], [[239, 239], [239, 239]], [[237, 237], [237, 237]]]), ] +TEST_CASE_1 = [ + FILE_PATH, + {}, + {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0}, + np.array([[[246], [246]], [[246], [246]], [[246], [246]]]), +] + +TEST_CASE_2 = [ + FILE_PATH, + {}, + {"location": (0, 0), "size": (2, 1), "level": 2}, + np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), +] + +TEST_CASE_3 = [ + FILE_PATH, + {"channel_dim": -1}, + {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0}, + np.moveaxis(np.array([[[246], [246]], [[246], [246]], [[246], [246]]]), 0, -1), +] + +TEST_CASE_4 = [ + FILE_PATH, + {"channel_dim": 2}, + {"location": (0, 0), "size": (2, 1), "level": 2}, + np.moveaxis(np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), 0, -1), +] + +TEST_CASE_MULTI_WSI = [ + [FILE_PATH, FILE_PATH], + {"location": (0, 0), "size": (2, 1), "level": 2}, + np.concatenate( + [ + np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), + np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), + ], + axis=0, + ), +] + TEST_CASE_RGB_0 = [np.ones((3, 2, 2), dtype=np.uint8)] # CHW @@ -108,6 +148,20 @@ 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) + + return filename + + @skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") def setUpModule(): hash_type = testing_data_config("images", FILE_KEY, "hash_type") @@ -115,21 +169,22 @@ def setUpModule(): download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val) -class WSIReaderTests: +@deprecated(since="0.8", msg_suffix="use tests for `monai.wsi_reader.WSIReader` instead, `WSIReaderTests`.") +class WSIReaderDeprecatedTests: class Tests(unittest.TestCase): backend = None @parameterized.expand([TEST_CASE_0]) def test_read_whole_image(self, file_path, level, expected_shape): - reader = WSIReader(self.backend, level=level) + reader = WSIReaderDeprecated(self.backend, level=level) with reader.read(file_path) as img_obj: img = reader.get_data(img_obj)[0] self.assertTupleEqual(img.shape, expected_shape) - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_5]) + @parameterized.expand([TEST_CASE_DEP_1, TEST_CASE_DEP_2, TEST_CASE_DEP_5]) def test_read_region(self, file_path, patch_info, expected_img): kwargs = {"name": None, "offset": None} if self.backend == "tifffile" else {} - reader = WSIReader(self.backend, **kwargs) + reader = WSIReaderDeprecated(self.backend, **kwargs) with reader.read(file_path, **kwargs) as img_obj: if self.backend == "tifffile": with self.assertRaises(ValueError): @@ -143,9 +198,9 @@ def test_read_region(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_3, TEST_CASE_4]) + @parameterized.expand([TEST_CASE_DEP_3, TEST_CASE_DEP_4]) def test_read_patches(self, file_path, patch_info, expected_img): - reader = WSIReader(self.backend) + reader = WSIReaderDeprecated(self.backend) with reader.read(file_path) as img_obj: if self.backend == "tifffile": with self.assertRaises(ValueError): @@ -155,6 +210,115 @@ 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_tiff, "Requires tifffile.") + def test_read_rgba(self, img_expected): + # skip for OpenSlide since not working with images without tiles + if self.backend == "openslide": + return + image = {} + reader = WSIReaderDeprecated(self.backend) + for mode in ["RGB", "RGBA"]: + file_path = save_rgba_tiff( + img_expected, + os.path.join(os.path.dirname(__file__), "testing_data", f"temp_tiff_image_{mode}.tiff"), + mode=mode, + ) + with reader.read(file_path) as img_obj: + 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)) + + @parameterized.expand([TEST_CASE_ERROR_0C, TEST_CASE_ERROR_1C, TEST_CASE_ERROR_2C, TEST_CASE_ERROR_3D]) + @skipUnless(has_tiff, "Requires tifffile.") + def test_read_malformats(self, img_expected): + if self.backend == "cucim" and (len(img_expected.shape) < 3 or img_expected.shape[2] == 1): + # Until cuCIM addresses https://github.com/rapidsai/cucim/issues/230 + return + reader = WSIReaderDeprecated(self.backend) + file_path = os.path.join(os.path.dirname(__file__), "testing_data", "temp_tiff_image_gray.tiff") + imwrite(file_path, img_expected, shape=img_expected.shape) + 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) + + @parameterized.expand([TEST_CASE_TRANSFORM_0]) + def test_with_dataloader(self, file_path, level, expected_spatial_shape, expected_shape): + train_transform = Compose( + [ + LoadImaged(keys=["image"], reader=WSIReaderDeprecated, backend=self.backend, level=level), + FromMetaTensord(keys=["image"]), + ToTensord(keys=["image"]), + ] + ) + dataset = Dataset([{"image": file_path}], transform=train_transform) + data_loader = DataLoader(dataset) + data: dict = first(data_loader) + for s in data[PostFix.meta("image")]["spatial_shape"]: + assert_allclose(s, expected_spatial_shape, type_test=False) + self.assertTupleEqual(data["image"].shape, expected_shape) + + +class WSIReaderTests: + class Tests(unittest.TestCase): + backend = None + + @parameterized.expand([TEST_CASE_0]) + def test_read_whole_image(self, file_path, level, expected_shape): + reader = WSIReader(self.backend, level=level) + with reader.read(file_path) as img_obj: + img, meta = reader.get_data(img_obj) + self.assertTupleEqual(img.shape, expected_shape) + self.assertEqual(meta["backend"], self.backend) + self.assertEqual(meta["path"], str(os.path.abspath(file_path))) + self.assertEqual(meta["patch_level"], level) + assert_array_equal(meta["patch_size"], expected_shape[1:]) + assert_array_equal(meta["patch_location"], (0, 0)) + + @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4]) + def test_read_region(self, file_path, kwargs, patch_info, expected_img): + reader = WSIReader(self.backend, **kwargs) + with reader.read(file_path) as img_obj: + if self.backend == "tifffile": + with self.assertRaises(ValueError): + reader.get_data(img_obj, **patch_info)[0] + else: + # Read twice to check multiple calls + img, meta = reader.get_data(img_obj, **patch_info) + img2 = reader.get_data(img_obj, **patch_info)[0] + self.assertTupleEqual(img.shape, img2.shape) + self.assertIsNone(assert_array_equal(img, img2)) + self.assertTupleEqual(img.shape, expected_img.shape) + self.assertIsNone(assert_array_equal(img, expected_img)) + self.assertEqual(meta["backend"], self.backend) + self.assertEqual(meta["path"], str(os.path.abspath(file_path))) + self.assertEqual(meta["patch_level"], patch_info["level"]) + assert_array_equal(meta["patch_size"], patch_info["size"]) + assert_array_equal(meta["patch_location"], patch_info["location"]) + + @parameterized.expand([TEST_CASE_MULTI_WSI]) + def test_read_region_multi_wsi(self, file_path_list, patch_info, expected_img): + kwargs = {"name": None, "offset": None} if self.backend == "tifffile" else {} + reader = WSIReader(self.backend, **kwargs) + img_obj_list = reader.read(file_path_list, **kwargs) + if self.backend == "tifffile": + with self.assertRaises(ValueError): + reader.get_data(img_obj_list, **patch_info)[0] + else: + # Read twice to check multiple calls + img, meta = reader.get_data(img_obj_list, **patch_info) + img2 = reader.get_data(img_obj_list, **patch_info)[0] + self.assertTupleEqual(img.shape, img2.shape) + self.assertIsNone(assert_array_equal(img, img2)) + self.assertTupleEqual(img.shape, expected_img.shape) + self.assertIsNone(assert_array_equal(img, expected_img)) + self.assertEqual(meta["backend"], self.backend) + self.assertEqual(meta["path"][0], str(os.path.abspath(file_path_list[0]))) + self.assertEqual(meta["patch_level"][0], patch_info["level"]) + assert_array_equal(meta["patch_size"][0], expected_img.shape[1:]) + assert_array_equal(meta["patch_location"][0], patch_info["location"]) + @parameterized.expand([TEST_CASE_RGB_0, TEST_CASE_RGB_1]) @skipUnless(has_tiff, "Requires tifffile.") def test_read_rgba(self, img_expected): @@ -201,30 +365,61 @@ def test_with_dataloader(self, file_path, level, expected_spatial_shape, expecte data_loader = DataLoader(dataset) data: dict = first(data_loader) for s in data[PostFix.meta("image")]["spatial_shape"]: - torch.testing.assert_allclose(s, expected_spatial_shape) + assert_allclose(s, expected_spatial_shape, type_test=False) self.assertTupleEqual(data["image"].shape, expected_shape) + @parameterized.expand([TEST_CASE_TRANSFORM_0]) + def test_with_dataloader_batch(self, file_path, level, expected_spatial_shape, expected_shape): + train_transform = Compose( + [ + LoadImaged(keys=["image"], reader=WSIReader, backend=self.backend, level=level), + FromMetaTensord(keys=["image"]), + ToTensord(keys=["image"]), + ] + ) + dataset = Dataset([{"image": file_path}, {"image": file_path}], transform=train_transform) + batch_size = 2 + data_loader = DataLoader(dataset, batch_size=batch_size) + data: dict = first(data_loader) + for s in data[PostFix.meta("image")]["spatial_shape"]: + assert_allclose(s, expected_spatial_shape, type_test=False) + self.assertTupleEqual(data["image"].shape, (batch_size, *expected_shape[1:])) + @skipUnless(has_cucim, "Requires cucim") -class TestCuCIM(WSIReaderTests.Tests): +class TestCuCIMDeprecated(WSIReaderDeprecatedTests.Tests): @classmethod def setUpClass(cls): cls.backend = "cucim" @skipUnless(has_osl, "Requires OpenSlide") -class TestOpenSlide(WSIReaderTests.Tests): +class TestOpenSlideDeprecated(WSIReaderDeprecatedTests.Tests): @classmethod def setUpClass(cls): cls.backend = "openslide" @skipUnless(has_tiff, "Requires TiffFile") -class TestTiffFile(WSIReaderTests.Tests): +class TestTiffFileDeprecated(WSIReaderDeprecatedTests.Tests): @classmethod def setUpClass(cls): cls.backend = "tifffile" +@skipUnless(has_cucim, "Requires cucim") +class TestCuCIM(WSIReaderTests.Tests): + @classmethod + def setUpClass(cls): + cls.backend = "cucim" + + +@skipUnless(has_osl, "Requires openslide") +class TestOpenSlide(WSIReaderTests.Tests): + @classmethod + def setUpClass(cls): + cls.backend = "openslide" + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_wsireader_new.py b/tests/test_wsireader_new.py deleted file mode 100644 index 0d5e5892e6..0000000000 --- a/tests/test_wsireader_new.py +++ /dev/null @@ -1,277 +0,0 @@ -# Copyright (c) MONAI Consortium -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# http://www.apache.org/licenses/LICENSE-2.0 -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import os -import unittest -from unittest import skipUnless - -import numpy as np -from numpy.testing import assert_array_equal -from parameterized import parameterized - -from monai.data import DataLoader, Dataset -from monai.data.wsi_reader import WSIReader -from monai.transforms import Compose, FromMetaTensord, LoadImaged, ToTensord -from monai.utils import first, optional_import -from monai.utils.enums import PostFix -from tests.utils import assert_allclose, download_url_or_skip_test, testing_data_config - -cucim, has_cucim = optional_import("cucim") -has_cucim = has_cucim and hasattr(cucim, "CuImage") -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 - -FILE_KEY = "wsi_img" -FILE_URL = testing_data_config("images", FILE_KEY, "url") -base_name, extension = os.path.basename(f"{FILE_URL}"), ".tiff" -FILE_PATH = os.path.join(os.path.dirname(__file__), "testing_data", "temp_" + base_name + extension) - -HEIGHT = 32914 -WIDTH = 46000 - -TEST_CASE_0 = [FILE_PATH, 2, (3, HEIGHT // 4, WIDTH // 4)] - -TEST_CASE_TRANSFORM_0 = [FILE_PATH, 4, (HEIGHT // 16, WIDTH // 16), (1, 3, HEIGHT // 16, WIDTH // 16)] - -TEST_CASE_1 = [ - FILE_PATH, - {}, - {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0}, - np.array([[[246], [246]], [[246], [246]], [[246], [246]]]), -] - -TEST_CASE_2 = [ - FILE_PATH, - {}, - {"location": (0, 0), "size": (2, 1), "level": 2}, - np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), -] - -TEST_CASE_3 = [ - FILE_PATH, - {"channel_dim": -1}, - {"location": (HEIGHT // 2, WIDTH // 2), "size": (2, 1), "level": 0}, - np.moveaxis(np.array([[[246], [246]], [[246], [246]], [[246], [246]]]), 0, -1), -] - -TEST_CASE_4 = [ - FILE_PATH, - {"channel_dim": 2}, - {"location": (0, 0), "size": (2, 1), "level": 2}, - np.moveaxis(np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), 0, -1), -] - -TEST_CASE_MULTI_WSI = [ - [FILE_PATH, FILE_PATH], - {"location": (0, 0), "size": (2, 1), "level": 2}, - np.concatenate( - [ - np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), - np.array([[[239], [239]], [[239], [239]], [[239], [239]]]), - ], - axis=0, - ), -] - - -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 - -TEST_CASE_ERROR_0C = [np.ones((16, 16), dtype=np.uint8)] # no color channel -TEST_CASE_ERROR_1C = [np.ones((16, 16, 1), dtype=np.uint8)] # one color channel -TEST_CASE_ERROR_2C = [np.ones((16, 16, 2), dtype=np.uint8)] # two color channels -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): - """ - Save numpy array into a TIFF RGB/RGBA file - - Args: - array: numpy ndarray with the shape of CxHxW and C==3 representing a RGB image - 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": - array = np.concatenate([array, 255 * np.ones_like(array[0])[np.newaxis]]).astype(np.uint8) - - img_rgb = array.transpose(1, 2, 0) - imwrite(filename, img_rgb, shape=img_rgb.shape, tile=(16, 16)) - - 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) - - return filename - - -@skipUnless(has_cucim or has_osl or has_tiff, "Requires cucim, openslide, or tifffile!") -def setUpModule(): - hash_type = testing_data_config("images", FILE_KEY, "hash_type") - hash_val = testing_data_config("images", FILE_KEY, "hash_val") - download_url_or_skip_test(FILE_URL, FILE_PATH, hash_type=hash_type, hash_val=hash_val) - - -class WSIReaderTests: - class Tests(unittest.TestCase): - backend = None - - @parameterized.expand([TEST_CASE_0]) - def test_read_whole_image(self, file_path, level, expected_shape): - reader = WSIReader(self.backend, level=level) - with reader.read(file_path) as img_obj: - img, meta = reader.get_data(img_obj) - self.assertTupleEqual(img.shape, expected_shape) - self.assertEqual(meta["backend"], self.backend) - self.assertEqual(meta["path"], str(os.path.abspath(file_path))) - self.assertEqual(meta["patch_level"], level) - assert_array_equal(meta["patch_size"], expected_shape[1:]) - assert_array_equal(meta["patch_location"], (0, 0)) - - @parameterized.expand([TEST_CASE_1, TEST_CASE_2, TEST_CASE_3, TEST_CASE_4]) - def test_read_region(self, file_path, kwargs, patch_info, expected_img): - reader = WSIReader(self.backend, **kwargs) - with reader.read(file_path) as img_obj: - if self.backend == "tifffile": - with self.assertRaises(ValueError): - reader.get_data(img_obj, **patch_info)[0] - else: - # Read twice to check multiple calls - img, meta = reader.get_data(img_obj, **patch_info) - img2 = reader.get_data(img_obj, **patch_info)[0] - self.assertTupleEqual(img.shape, img2.shape) - self.assertIsNone(assert_array_equal(img, img2)) - self.assertTupleEqual(img.shape, expected_img.shape) - self.assertIsNone(assert_array_equal(img, expected_img)) - self.assertEqual(meta["backend"], self.backend) - self.assertEqual(meta["path"], str(os.path.abspath(file_path))) - self.assertEqual(meta["patch_level"], patch_info["level"]) - assert_array_equal(meta["patch_size"], patch_info["size"]) - assert_array_equal(meta["patch_location"], patch_info["location"]) - - @parameterized.expand([TEST_CASE_MULTI_WSI]) - def test_read_region_multi_wsi(self, file_path_list, patch_info, expected_img): - kwargs = {"name": None, "offset": None} if self.backend == "tifffile" else {} - reader = WSIReader(self.backend, **kwargs) - img_obj_list = reader.read(file_path_list, **kwargs) - if self.backend == "tifffile": - with self.assertRaises(ValueError): - reader.get_data(img_obj_list, **patch_info)[0] - else: - # Read twice to check multiple calls - img, meta = reader.get_data(img_obj_list, **patch_info) - img2 = reader.get_data(img_obj_list, **patch_info)[0] - self.assertTupleEqual(img.shape, img2.shape) - self.assertIsNone(assert_array_equal(img, img2)) - self.assertTupleEqual(img.shape, expected_img.shape) - self.assertIsNone(assert_array_equal(img, expected_img)) - self.assertEqual(meta["backend"], self.backend) - self.assertEqual(meta["path"][0], str(os.path.abspath(file_path_list[0]))) - self.assertEqual(meta["patch_level"][0], patch_info["level"]) - assert_array_equal(meta["patch_size"][0], expected_img.shape[1:]) - assert_array_equal(meta["patch_location"][0], patch_info["location"]) - - @parameterized.expand([TEST_CASE_RGB_0, TEST_CASE_RGB_1]) - @skipUnless(has_tiff, "Requires tifffile.") - def test_read_rgba(self, img_expected): - # skip for OpenSlide since not working with images without tiles - if self.backend == "openslide": - return - image = {} - reader = WSIReader(self.backend) - for mode in ["RGB", "RGBA"]: - file_path = save_rgba_tiff( - img_expected, - os.path.join(os.path.dirname(__file__), "testing_data", f"temp_tiff_image_{mode}.tiff"), - mode=mode, - ) - with reader.read(file_path) as img_obj: - 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)) - - @parameterized.expand([TEST_CASE_ERROR_0C, TEST_CASE_ERROR_1C, TEST_CASE_ERROR_2C, TEST_CASE_ERROR_3D]) - @skipUnless(has_tiff, "Requires tifffile.") - def test_read_malformats(self, img_expected): - if self.backend == "cucim" and (len(img_expected.shape) < 3 or img_expected.shape[2] == 1): - # Until cuCIM addresses https://github.com/rapidsai/cucim/issues/230 - return - reader = WSIReader(self.backend) - file_path = os.path.join(os.path.dirname(__file__), "testing_data", "temp_tiff_image_gray.tiff") - imwrite(file_path, img_expected, shape=img_expected.shape) - 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) - - @parameterized.expand([TEST_CASE_TRANSFORM_0]) - def test_with_dataloader(self, file_path, level, expected_spatial_shape, expected_shape): - train_transform = Compose( - [ - LoadImaged(keys=["image"], reader=WSIReader, backend=self.backend, level=level), - FromMetaTensord(keys=["image"]), - ToTensord(keys=["image"]), - ] - ) - dataset = Dataset([{"image": file_path}], transform=train_transform) - data_loader = DataLoader(dataset) - data: dict = first(data_loader) - for s in data[PostFix.meta("image")]["spatial_shape"]: - assert_allclose(s, expected_spatial_shape, type_test=False) - self.assertTupleEqual(data["image"].shape, expected_shape) - - @parameterized.expand([TEST_CASE_TRANSFORM_0]) - def test_with_dataloader_batch(self, file_path, level, expected_spatial_shape, expected_shape): - train_transform = Compose( - [ - LoadImaged(keys=["image"], reader=WSIReader, backend=self.backend, level=level), - FromMetaTensord(keys=["image"]), - ToTensord(keys=["image"]), - ] - ) - dataset = Dataset([{"image": file_path}, {"image": file_path}], transform=train_transform) - batch_size = 2 - data_loader = DataLoader(dataset, batch_size=batch_size) - data: dict = first(data_loader) - for s in data[PostFix.meta("image")]["spatial_shape"]: - assert_allclose(s, expected_spatial_shape, type_test=False) - self.assertTupleEqual(data["image"].shape, (batch_size, *expected_shape[1:])) - - -@skipUnless(has_cucim, "Requires cucim") -class TestCuCIM(WSIReaderTests.Tests): - @classmethod - def setUpClass(cls): - cls.backend = "cucim" - - -@skipUnless(has_osl, "Requires openslide") -class TestOpenSlide(WSIReaderTests.Tests): - @classmethod - def setUpClass(cls): - cls.backend = "openslide" - - -if __name__ == "__main__": - unittest.main()