Repository navigation
TiffFileWSIReader reads the wrong region of Aperio SVS slides at level >= 1 #9225
Description
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_patchreads pixels withwsi.asarray(level=level), which indexes the pyramid levels of the first series (wsi.series[0].levels).get_level_count,get_size,get_downsample_ratioandget_mppusewsi.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
- Download
Aperio-CMU-1.svsfrom https://huggingface.co/datasets/MONAI/testing_data/resolve/main/Aperio-CMU-1.svs pip install monai tifffile imagecodecs openslide-python openslide-bin- 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