From a7476ca36b8d40afa8e48eeef8fd378eaf827e66 Mon Sep 17 00:00:00 2001 From: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com> Date: Fri, 3 May 2024 16:15:33 +0100 Subject: [PATCH 1/3] :white_check_mark: Fix test coverage. --- tests/conftest.py | 10 ++++++++++ tests/test_wsireader.py | 4 ++++ tiatoolbox/data/remote_samples.yaml | 4 +++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6b39e3ee4..5665b80f6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -130,6 +130,16 @@ def sample_ventana_tif(remote_sample: Callable) -> Path: return remote_sample("ventana-tif") +@pytest.fixture(scope="session") +def sample_regular_tif(remote_sample: Callable) -> Path: + """Sample pytest fixture for non-tiled tif Ventana images. + + Download Ventana tif image for pytest. + + """ + return remote_sample("regular-tif") + + @pytest.fixture(scope="session") def sample_jp2(remote_sample: Callable) -> Path: """Sample pytest fixture for JP2 images. diff --git a/tests/test_wsireader.py b/tests/test_wsireader.py index c99aa0a48..e8d0f28da 100644 --- a/tests/test_wsireader.py +++ b/tests/test_wsireader.py @@ -1473,6 +1473,7 @@ def test_wsireader_open( sample_jp2: Path, sample_ome_tiff: Path, sample_ventana_tif: Path, + sample_regular_tif: Path, source_image: Path, tmp_path: pytest.TempPathFactory, ) -> None: @@ -1498,6 +1499,9 @@ def test_wsireader_open( wsi = WSIReader.open(sample_ventana_tif) assert isinstance(wsi, wsireader.OpenSlideWSIReader) + wsi = WSIReader.open(sample_regular_tif) + assert isinstance(wsi, wsireader.VirtualWSIReader) + wsi = WSIReader.open(Path(source_image)) assert isinstance(wsi, wsireader.VirtualWSIReader) diff --git a/tiatoolbox/data/remote_samples.yaml b/tiatoolbox/data/remote_samples.yaml index 6931ea94b..58fff49d3 100644 --- a/tiatoolbox/data/remote_samples.yaml +++ b/tiatoolbox/data/remote_samples.yaml @@ -30,7 +30,9 @@ files: two-tiled-pages: url: [*wsis, "two-tiled-pages.tiff"] ventana-tif: - url: [*wsis, "ventana_sample.tif"] + url: [*wsis, "ventana-sample.tif"] + regular-tif: + url: [*wsis, "sample-regular.tif"] jp2-omnyx-small: url: [*wsis, "CMU-1-Small-Region.omnyx.jp2"] jp2-omnyx-1: From 683241e17cf04c24162afa7da59371fc81b50be8 Mon Sep 17 00:00:00 2001 From: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com> Date: Fri, 3 May 2024 16:41:57 +0100 Subject: [PATCH 2/3] :bug: Fix code complexity --- tiatoolbox/wsicore/wsireader.py | 106 +++++++++++++++++++++++--------- 1 file changed, 76 insertions(+), 30 deletions(-) diff --git a/tiatoolbox/wsicore/wsireader.py b/tiatoolbox/wsicore/wsireader.py index 417a37282..6d871d690 100644 --- a/tiatoolbox/wsicore/wsireader.py +++ b/tiatoolbox/wsicore/wsireader.py @@ -205,6 +205,72 @@ def is_ngff( # noqa: PLR0911 return is_zarr(path) +def _handle_virtual_wsi( + last_suffix: str, + input_path: Path, + mpp: tuple[Number, Number] | None, + power: Number | None, +) -> VirtualWSIReader | None: + """Handle virtual WSI cases. + + Args: + last_suffix (str): + Suffix of the file to read. + input_path (Path): + Input path to virtual WSI. + mpp (:obj:`tuple` or :obj:`list` or :obj:`None`, optional): + The MPP of the WSI. If not provided, the MPP is approximated + from the objective power. + power (:obj:`float` or :obj:`None`, optional): + The objective power of the WSI. If not provided, the power + is approximated from the MPP. + + Returns: + VirtualWSIReader | None: + :class:`VirtualWSIReader` if input_path is valide path to viruatl WSI + otherwise None. + + """ + + # Handle homogeneous cases (based on final suffix) + def np_virtual_wsi( + input_path: np.ndarray, + *args: Number | tuple | str | WSIMeta | None, + **kwargs: dict, + ) -> VirtualWSIReader: + """Create a virtual WSI from a numpy array.""" + return VirtualWSIReader(input_path, *args, **kwargs) + + suffix_to_reader = { + ".npy": np_virtual_wsi, + ".jp2": JP2WSIReader, + ".jpeg": VirtualWSIReader, + ".jpg": VirtualWSIReader, + ".png": VirtualWSIReader, + ".tif": VirtualWSIReader, + ".tiff": VirtualWSIReader, + } + + if last_suffix in suffix_to_reader: + return suffix_to_reader[last_suffix](input_path, mpp=mpp, power=power) + + return None + + +def _handle_tiff_wsi( + input_path: Path, mpp: tuple[Number, Number] | None, power: Number | None +) -> TIFFWSIReader | OpenSlideWSIReader | None: + if openslide.OpenSlide.detect_format(input_path) is not None: + try: + return OpenSlideWSIReader(input_path, mpp=mpp, power=power) + except openslide.OpenSlideError: + pass + if is_tiled_tiff(input_path): + return TIFFWSIReader(input_path, mpp=mpp, power=power) + + return None + + class WSIReader: """Base whole slide image (WSI) reader class. @@ -228,7 +294,7 @@ class WSIReader: """ @staticmethod - def open( # noqa: PLR0911, PLR0912, C901 + def open( # noqa: PLR0911 input_img: str | Path | np.ndarray | WSIReader, mpp: tuple[Number, Number] | None = None, power: Number | None = None, @@ -279,7 +345,6 @@ def open( # noqa: PLR0911, PLR0912, C901 WSIReader.verify_supported_wsi(input_path) # Handle special cases first (DICOM, Zarr/NGFF, OME-TIFF) - if is_dicom(input_path): return DICOMWSIReader(input_path, mpp=mpp, power=power) @@ -301,35 +366,16 @@ def open( # noqa: PLR0911, PLR0912, C901 return TIFFWSIReader(input_path, mpp=mpp, power=power) if last_suffix in (".tif", ".tiff"): - if openslide.OpenSlide.detect_format(input_path) is not None: - try: - return OpenSlideWSIReader(input_path, mpp=mpp, power=power) - except openslide.OpenSlideError: - pass - if is_tiled_tiff(input_path): - return TIFFWSIReader(input_path, mpp=mpp, power=power) - - # Handle homogeneous cases (based on final suffix) - def np_virtual_wsi( - input_path: np.ndarray, - *args: Number | tuple | str | WSIMeta | None, - **kwargs: dict, - ) -> VirtualWSIReader: - """Create a virtual WSI from a numpy array.""" - return VirtualWSIReader(input_path, *args, **kwargs) - - suffix_to_reader = { - ".npy": np_virtual_wsi, - ".jp2": JP2WSIReader, - ".jpeg": VirtualWSIReader, - ".jpg": VirtualWSIReader, - ".png": VirtualWSIReader, - ".tif": VirtualWSIReader, - ".tiff": VirtualWSIReader, - } + tiff_wsi = _handle_tiff_wsi(input_path, mpp=mpp, power=power) + if tiff_wsi is not None: + return tiff_wsi + + virtual_wsi = _handle_virtual_wsi( + last_suffix=last_suffix, input_path=input_path, mpp=mpp, power=power + ) - if last_suffix in suffix_to_reader: - return suffix_to_reader[last_suffix](input_path, mpp=mpp, power=power) + if virtual_wsi is not None: + return virtual_wsi # Try openslide last return OpenSlideWSIReader(input_path, mpp=mpp, power=power) From 6dda71e3d9a0818e6ca88a9ef001a8b64a287cae Mon Sep 17 00:00:00 2001 From: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com> Date: Fri, 3 May 2024 16:45:42 +0100 Subject: [PATCH 3/3] :memo: Fix documentation --- tiatoolbox/wsicore/wsireader.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tiatoolbox/wsicore/wsireader.py b/tiatoolbox/wsicore/wsireader.py index 6d871d690..00e8b7673 100644 --- a/tiatoolbox/wsicore/wsireader.py +++ b/tiatoolbox/wsicore/wsireader.py @@ -227,7 +227,7 @@ def _handle_virtual_wsi( Returns: VirtualWSIReader | None: - :class:`VirtualWSIReader` if input_path is valide path to viruatl WSI + :class:`VirtualWSIReader` if input_path is valid path to virtual WSI otherwise None. """ @@ -260,6 +260,24 @@ def np_virtual_wsi( def _handle_tiff_wsi( input_path: Path, mpp: tuple[Number, Number] | None, power: Number | None ) -> TIFFWSIReader | OpenSlideWSIReader | None: + """Handle TIFF WSI cases. + + Args: + input_path (Path): + Input path to virtual WSI. + mpp (:obj:`tuple` or :obj:`list` or :obj:`None`, optional): + The MPP of the WSI. If not provided, the MPP is approximated + from the objective power. + power (:obj:`float` or :obj:`None`, optional): + The objective power of the WSI. If not provided, the power + is approximated from the MPP. + + Returns: + OpenSlideWSIReader | TIFFWSIReader | None: + :class:`OpenSlideWSIReader` or :class:`TIFFWSIReader` if input_path is + valid path to tiff WSI otherwise None. + + """ if openslide.OpenSlide.detect_format(input_path) is not None: try: return OpenSlideWSIReader(input_path, mpp=mpp, power=power)