Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions tests/test_wsireader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion tiatoolbox/data/remote_samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
124 changes: 94 additions & 30 deletions tiatoolbox/wsicore/wsireader.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,90 @@ 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 valid path to virtual 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:
"""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)
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.

Expand All @@ -228,7 +312,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,
Expand Down Expand Up @@ -279,7 +363,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)

Expand All @@ -301,35 +384,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)
Expand Down