Skip to content

Commit

Permalink
🐛 Fix TIFFWSIReader read_bound (#777)
Browse files Browse the repository at this point in the history
![image](https://github.com/TissueImageAnalytics/tiatoolbox/assets/24943262/20c0fda0-19f6-46cb-97f8-9ab125d3b3be)

Emergency bugfix per @John-P request.
The culprit is reading bound doesn't use the adjusted bounds as have been done in OpenSlideReader.

---------

Co-authored-by: Abdol <a@fkrtech.com>
Co-authored-by: measty <20169086+measty@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Shan E Ahmed Raza <13048456+shaneahmed@users.noreply.github.com>
  • Loading branch information
5 people authored Jul 5, 2024
1 parent 6643e7a commit 82e9d8f
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 30 deletions.
12 changes: 11 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,17 @@ def sample_ome_tiff(remote_sample: Callable) -> Path:
Download ome-tiff image for pytest.
"""
return remote_sample("ome-brightfield-pyramid-1-small")
return remote_sample("ome-brightfield-small-pyramid")


@pytest.fixture(scope="session")
def sample_ome_tiff_level_0(remote_sample: Callable) -> Path:
"""Sample pytest fixture for ome-tiff image with one level.
Download ome-tiff image for pytest.
"""
return remote_sample("ome-brightfield-small-level-0")


@pytest.fixture(scope="session")
Expand Down
8 changes: 4 additions & 4 deletions tests/test_tiffreader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def test_ome_missing_instrument_ref(
remote_sample: Callable,
) -> None:
"""Test that an OME-TIFF can be read without instrument reference."""
sample = remote_sample("ome-brightfield-pyramid-1-small")
sample = remote_sample("ome-brightfield-small-level-0")
wsi = wsireader.TIFFWSIReader(sample)
page = wsi.tiff.pages[0]
description = page.description
Expand All @@ -37,7 +37,7 @@ def test_ome_missing_physicalsize(
remote_sample: Callable,
) -> None:
"""Test that an OME-TIFF can be read without physical size."""
sample = remote_sample("ome-brightfield-pyramid-1-small")
sample = remote_sample("ome-brightfield-small-level-0")
wsi = wsireader.TIFFWSIReader(sample)
page = wsi.tiff.pages[0]
description = page.description
Expand All @@ -62,7 +62,7 @@ def test_ome_missing_physicalsizey(
remote_sample: Callable,
) -> None:
"""Test that an OME-TIFF can be read without physical size."""
sample = remote_sample("ome-brightfield-pyramid-1-small")
sample = remote_sample("ome-brightfield-small-level-0")
wsi = wsireader.TIFFWSIReader(sample)
page = wsi.tiff.pages[0]
description = page.description
Expand All @@ -86,7 +86,7 @@ def test_tiffreader_non_tiled_metadata(
remote_sample: Callable,
) -> None:
"""Test that fetching metadata for non-tiled TIFF works."""
sample = remote_sample("ome-brightfield-pyramid-1-small")
sample = remote_sample("ome-brightfield-small-level-0")
wsi = wsireader.TIFFWSIReader(sample)
monkeypatch.setattr(wsi.tiff, "is_ome", False)
monkeypatch.setattr(
Expand Down
18 changes: 13 additions & 5 deletions tests/test_wsireader.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,15 +959,23 @@ def test_read_bounds_interpolated(sample_svs: Path) -> None:


def test_read_bounds_level_consistency_openslide(sample_ndpi: Path) -> None:
"""Test read_bounds produces the same visual field across resolution levels."""
"""Test read_bounds produces the same visual field across resolution levels.
with OpenSlideWSIReader.
"""
wsi = wsireader.OpenSlideWSIReader(sample_ndpi)
bounds = NDPI_TEST_TISSUE_BOUNDS

read_bounds_level_consistency(wsi, bounds)


def test_read_bounds_level_consistency_jp2(sample_jp2: Path) -> None:
"""Test read_bounds produces the same visual field across resolution levels."""
"""Test read_bounds produces the same visual field across resolution levels.
Using JP2WSIReader.
"""
bounds = JP2_TEST_TISSUE_BOUNDS
wsi = wsireader.JP2WSIReader(sample_jp2)

Expand Down Expand Up @@ -1883,11 +1891,11 @@ def test_tiffwsireader_invalid_svs_metadata(


def test_tiffwsireader_invalid_ome_metadata(
sample_ome_tiff: Path,
sample_ome_tiff_level_0: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Test exception raised for invalid OME-XML metadata instrument."""
wsi = wsireader.TIFFWSIReader(sample_ome_tiff)
wsi = wsireader.TIFFWSIReader(sample_ome_tiff_level_0)
monkeypatch.setattr(
wsi.tiff.pages[0],
"description",
Expand Down Expand Up @@ -2545,7 +2553,7 @@ def test_jp2_no_header(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
},
{
"reader_class": TIFFWSIReader,
"sample_key": "ome-brightfield-pyramid-1-small",
"sample_key": "ome-brightfield-small-pyramid",
"kwargs": {},
},
{
Expand Down
6 changes: 4 additions & 2 deletions tiatoolbox/data/remote_samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ files:
url: [*wsis, "CMU-1-Small-Region.jpeg.tiff"]
tiled-tiff-1-small-jp2k:
url: [*wsis, "CMU-1-Small-Region.jp2k.tiff"]
ome-brightfield-pyramid-1-small:
url: [*wsis, "CMU-1-Small-Region.ome.tiff"]
ome-brightfield-small-level-0:
url: [*wsis, "CMU-1-Small-Region-Level-0.ome.tiff"]
ome-brightfield-small-pyramid:
url: [*wsis, "CMU-1-Small-Region-Pyramid.ome.tif"]
two-tiled-pages:
url: [*wsis, "two-tiled-pages.tiff"]
ventana-tif:
Expand Down
2 changes: 1 addition & 1 deletion tiatoolbox/visualization/bokeh_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2086,7 +2086,7 @@ def setup_doc(self: DocConfig, base_doc: Document) -> tuple[Row, Tabs]:

# Set initial slide to first one in base folder
slide_list = []
for ext in ["*.svs", "*ndpi", "*.tiff", "*.mrxs", "*.png", "*.jpg"]:
for ext in ["*.svs", "*ndpi", "*.tiff", "*.tif", "*.mrxs", "*.png", "*.jpg"]:
slide_list.extend(list(doc_config["slide_folder"].glob(ext)))
slide_list.extend(
list(doc_config["slide_folder"].glob(str(Path("*") / ext))),
Expand Down
48 changes: 31 additions & 17 deletions tiatoolbox/wsicore/wsireader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from __future__ import annotations

import copy
import json
import logging
import math
Expand Down Expand Up @@ -380,7 +379,9 @@ def open( # noqa: PLR0911
)
return NGFFWSIReader(input_path, mpp=mpp, power=power)

if suffixes[-2:] in ([".ome", ".tiff"],):
if suffixes[-2:] in ([".ome", ".tiff"],) or suffixes[-2:] in (
[".ome", ".tif"],
):
return TIFFWSIReader(input_path, mpp=mpp, power=power)

if last_suffix in (".tif", ".tiff"):
Expand Down Expand Up @@ -477,7 +478,7 @@ def info(self: WSIReader) -> WSIMeta:
"""
if self._m_info is not None:
return copy.deepcopy(self._m_info)
return self._m_info
self._m_info = self._info()
if self._manual_mpp:
self._m_info.mpp = np.array(self._manual_mpp)
Expand Down Expand Up @@ -3472,7 +3473,14 @@ def __init__(
len(self.tiff.pages) == 1,
],
)
if not any([self.tiff.is_svs, self.tiff.is_ome, is_single_page_tiled]):
if not any(
[
self.tiff.is_svs,
self.tiff.is_ome,
is_single_page_tiled,
self.tiff.is_bigtiff,
]
):
msg = "Unsupported TIFF WSI format."
raise ValueError(msg)

Expand Down Expand Up @@ -3503,9 +3511,16 @@ def page_area(page: tifffile.TiffPage) -> float:
group[0] = self._zarr_group
self._zarr_group = group
self.level_arrays = {
int(key): ArrayView(array, axes=self.info.axes)
int(key): ArrayView(array, axes=self._axes)
for key, array in self._zarr_group.items()
}
# ensure level arrays are sorted by descending area
self.level_arrays = dict(
sorted(
self.level_arrays.items(),
key=lambda x: -np.prod(self._canonical_shape(x[1].array.shape[:2])),
)
)

def _canonical_shape(self: TIFFWSIReader, shape: IntPair) -> tuple:
"""Make a level shape tuple in YXS order.
Expand Down Expand Up @@ -3761,10 +3776,10 @@ def _info(self: TIFFWSIReader) -> WSIMeta:
Containing metadata.
"""
level_count = len(self._zarr_group)
level_count = len(self.level_arrays)
level_dimensions = [
np.array(self._canonical_shape(p.shape)[:2][::-1])
for p in self._zarr_group.values()
np.array(self._canonical_shape(p.array.shape)[:2][::-1])
for p in self.level_arrays.values()
]
slide_dimensions = level_dimensions[0]
level_downsamples = [(level_dimensions[0] / x)[0] for x in level_dimensions]
Expand Down Expand Up @@ -4007,10 +4022,10 @@ def read_rect(
# Find parameters for optimal read
(
read_level,
_,
_,
level_read_location,
level_read_size,
post_read_scale,
baseline_read_size,
_,
) = self.find_read_rect_params(
location=location,
size=size,
Expand All @@ -4019,16 +4034,15 @@ def read_rect(
)

bounds = utils.transforms.locsize2bounds(
location=location,
size=baseline_read_size,
location=level_read_location,
size=level_read_size,
)
im_region = utils.image.safe_padded_read(
image=self.level_arrays[read_level],
bounds=bounds,
pad_mode=pad_mode,
pad_constant_values=pad_constant_values,
)

im_region = utils.transforms.imresize(
img=im_region,
scale_factor=post_read_scale,
Expand Down Expand Up @@ -4163,7 +4177,7 @@ class docstrings for more information.
# but base image is of different scale)
(
read_level,
_,
bounds_at_read_level,
_,
post_read_scale,
) = self._find_read_bounds_params(
Expand All @@ -4175,7 +4189,7 @@ class docstrings for more information.
# Find parameters for optimal read
(
read_level,
_,
bounds_at_read_level,
size_at_requested,
post_read_scale,
) = self._find_read_bounds_params(
Expand All @@ -4186,7 +4200,7 @@ class docstrings for more information.

im_region = utils.image.sub_pixel_read(
image=self.level_arrays[read_level],
bounds=bounds_at_baseline,
bounds=bounds_at_read_level,
output_size=size_at_requested,
interpolation=interpolation,
pad_mode=pad_mode,
Expand Down

0 comments on commit 82e9d8f

Please sign in to comment.