Skip to content

TiffFileWSIReader reads the wrong region of Aperio SVS slides at level >= 1 #9225

Open

Description

@ontdepart

Describe the bug

WSIReader(backend="tifffile") returns wrong patches from Aperio SVS files at any level other than 0, and it doesn't raise an error. A patch on tissue comes back as background, a level-2 patch comes back empty, and a whole-level read is cropped.

The cause is that TiffFileWSIReader mixes up two ways of indexing levels:

  • _get_patch reads pixels with wsi.asarray(level=level), which indexes the pyramid levels of the first series (wsi.series[0].levels).
  • get_level_count, get_size, get_downsample_ratio and get_mpp use wsi.pages[level], i.e. raw TIFF pages.

The two only line up when every page is a pyramid level. That holds for the generic tiled TIFF used in the tests, but not for SVS. An Aperio file stores the baseline, then a thumbnail as the second page, then the reduced levels, then the label and macro images. On Aperio-CMU-1.svs (the slide MONAI's WSI tests use):

page shape what it is
0 32914 × 46000 level 0
1 732 × 1024 thumbnail
2 8228 × 11500 level 1
3 2057 × 2875 level 2
4 463 × 387 label
5 431 × 1280 macro

So for level=1 the reader takes the size and downsample ratio (≈45 instead of 4) from the thumbnail but reads pixels from the real level 1. The patch location is then scaled by the wrong factor. For level=2 the factor is 4 instead of 16, so the location can fall outside the level and the slice comes back with a width of 0. level=3 points at the label page and fails with IndexError: list index out of range instead of the usual "maximum level" error.

This also affects PatchWSIDataset, SlidingPatchWSIDataset and MaskedPatchWSIDataset, because they all rely on the reader's get_data, get_size or get_downsample_ratio. With SlidingPatchWSIDataset, inference at level 1 covers a thumbnail-sized corner of the slide (1 patch instead of 352).

SVS is listed in TiffFileWSIReader.supported_suffixes. The existing tests don't catch this because test_read_region skips all SVS cases for the tifffile backend.

To Reproduce

  1. Download Aperio-CMU-1.svs from https://huggingface.co/datasets/MONAI/testing_data/resolve/main/Aperio-CMU-1.svs
  2. pip install monai tifffile imagecodecs openslide-python openslide-bin
  3. Run:
from monai.data import SlidingPatchWSIDataset, WSIReader

path = "Aperio-CMU-1.svs"
location = (5120, 36864)  # level-0 (y, x) of a tissue region

for backend in ("tifffile", "openslide"):
    reader = WSIReader(backend=backend)
    with reader.read(path) as wsi:
        n_levels = reader.get_level_count(wsi)
        print(f"{backend}: {n_levels} levels, sizes {[reader.get_size(wsi, level) for level in range(n_levels)]}")
        for level in (1, 2):
            patch, _ = reader.get_data(wsi, location=location, size=(256, 256), level=level)
            print(f"  level {level} patch: shape={tuple(patch.shape)}, mean={patch.mean():.1f}")
        whole, _ = reader.get_data(wsi, level=1)
        print(f"  whole level 1: shape={tuple(whole.shape)}")
    dataset = SlidingPatchWSIDataset([{"image": path}], patch_size=512, patch_level=1, reader=backend)
    print(f"  SlidingPatchWSIDataset at level 1: {len(dataset)} patches")

Output:

tifffile: 6 levels, sizes [(32914, 46000), (732, 1024), (8228, 11500), (2057, 2875), (463, 387), (431, 1280)]
  level 1 patch: shape=(3, 256, 256), mean=243.5
  level 2 patch: shape=(3, 256, 0), mean=nan
  whole level 1: shape=(3, 732, 1024)
  SlidingPatchWSIDataset at level 1: 1 patches
openslide: 3 levels, sizes [(32914, 46000), (8228, 11500), (2057, 2875)]
  level 1 patch: shape=(3, 256, 256), mean=91.8
  level 2 patch: shape=(3, 256, 256), mean=150.5
  whole level 1: shape=(3, 8228, 11500)
  SlidingPatchWSIDataset at level 1: 352 patches

The level-1 patch from tifffile is white background (mean 243.5), while the same request through openslide is tissue (mean 91.8).

Expected behavior

The tifffile backend should expose the same 3 pyramid levels as openslide, and return the same regions.

Environment

Reproduced on dev at 874343b. The same code is in the 1.6.1 release.

MONAI version: 1.6.1rc0+51.g874343b
Numpy version: 2.5.4
Pytorch version: 2.14.1+cpu
Pillow version: 13.0.0
tifffile version: 2026.10.15
imagecodecs version: 2026.10.10
openslide-python version: 1.4.6 (OpenSlide 4.0.1)
Python version: 3.12.7
Platform: Windows-11-10.0.26200-SP0

Additional context

A fix is to take all level metadata from wsi.series[0].levels[level].keyframe, the same levels asarray(level=...) reads. For the generic TIFFs in the test data, every level's keyframe is the same page as before, so nothing changes for them. I have this ready with tests and can open a PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions