Skip to content

Commit 333e20b

Browse files
authored
NEW: Zoomify Tile Generation (#182)
- Add classes which can generate zoomify tiles from a WSIReader object. Example -------- ```python from tiatoolbox.wsicore.wsireader import WSReader from tiatoolbox.tool.pyramid import ZoomifyGenerator wsi = WSReader.open("slide.svs") zoomify_gen = ZoomifyGenerator(wsi) # Defaults to 256x256 tiles tile = zoomify_gen.get_tile(0, 0, 0) # PIL Image ``` Co-authored-by: @John-P
1 parent d98e091 commit 333e20b

File tree

9 files changed

+1281
-240
lines changed

9 files changed

+1281
-240
lines changed

docs/usage.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ Patch Extraction
100100
.. autoclass:: SlidingWindowPatchExtractor
101101
:show-inheritance:
102102

103+
^^^^^^^^^^^^^^^^^^^^^^^
104+
Tile Pyramid Generation
105+
^^^^^^^^^^^^^^^^^^^^^^^
106+
107+
.. automodule:: tiatoolbox.tools.pyramid
108+
:members:
109+
110+
.. autoclass:: TilePyramidGenerator
111+
:show-inheritance:
112+
113+
.. autoclass:: DeepZoomGenerator
114+
:show-inheritance:
115+
116+
.. autoclass:: ZoomifyGenerator
117+
:show-inheritance:
118+
103119
^^^^^^^^^^^^^^^^^^^^
104120
Deep Learning Models
105121
^^^^^^^^^^^^^^^^^^^^

requirements.txt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,10 @@ pyyaml>=5.1
1515
requests
1616
scikit-image
1717
scikit-learn>=0.23.2
18+
shapely
1819
sphinx==4.1.2
1920
tifffile
2021
torchvision==0.10.1
2122
torch==1.9.1
2223
tqdm==4.60.0
23-
jupyterlab
24-
shapely
25-
requests
2624
zarr

tests/test_pyramid.py

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Tests for tile pyramid generation."""
2+
import re
3+
from pathlib import Path
4+
5+
import numpy as np
6+
import pytest
7+
from PIL import Image
8+
from skimage import data
9+
from skimage.metrics import peak_signal_noise_ratio
10+
11+
from tiatoolbox.tools import pyramid
12+
from tiatoolbox.utils.image import imresize
13+
from tiatoolbox.wsicore import wsireader
14+
15+
16+
def test_zoomify_tile_path():
17+
"""Test Zoomify tile path generation."""
18+
array = np.ones((1024, 1024))
19+
wsi = wsireader.VirtualWSIReader(array)
20+
dz = pyramid.ZoomifyGenerator(wsi)
21+
path = dz.tile_path(0, 0, 0)
22+
assert isinstance(path, Path)
23+
assert len(path.parts) == 2
24+
assert "TileGroup" in path.parts[0]
25+
assert re.match(pattern=r"TileGroup\d+", string=path.parts[0]) is not None
26+
assert re.match(pattern=r"\d+-\d+-\d+\.jpg", string=path.parts[1]) is not None
27+
28+
29+
def test_zoomify_len():
30+
"""Test __len__ for ZoomifyGenerator."""
31+
array = np.ones((1024, 1024))
32+
wsi = wsireader.VirtualWSIReader(array)
33+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
34+
assert len(dz) == (4 * 4) + (2 * 2) + 1
35+
36+
37+
def test_zoomify_iter():
38+
"""Test __iter__ for ZoomifyGenerator."""
39+
array = np.ones((1024, 1024))
40+
wsi = wsireader.VirtualWSIReader(array)
41+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
42+
for tile in dz:
43+
assert isinstance(tile, Image.Image)
44+
assert tile.size == (256, 256)
45+
46+
47+
def test_tile_grid_size_invalid_level():
48+
"""Test tile_grid_size for IndexError on invalid levels."""
49+
array = np.ones((1024, 1024))
50+
wsi = wsireader.VirtualWSIReader(array)
51+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
52+
with pytest.raises(IndexError):
53+
dz.tile_grid_size(level=-1)
54+
with pytest.raises(IndexError):
55+
dz.tile_grid_size(level=100)
56+
dz.tile_grid_size(level=0)
57+
58+
59+
def test_get_tile_negative_level():
60+
"""Test for IndexError on negative levels."""
61+
array = np.ones((1024, 1024))
62+
wsi = wsireader.VirtualWSIReader(array)
63+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
64+
with pytest.raises(IndexError):
65+
dz.get_tile(-1, 0, 0)
66+
67+
68+
def test_get_tile_large_level():
69+
"""Test for IndexError on too large a level."""
70+
array = np.ones((1024, 1024))
71+
wsi = wsireader.VirtualWSIReader(array)
72+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
73+
with pytest.raises(IndexError):
74+
dz.get_tile(100, 0, 0)
75+
76+
77+
def test_get_tile_large_xy():
78+
"""Test for IndexError on too large an xy index."""
79+
array = np.ones((1024, 1024))
80+
wsi = wsireader.VirtualWSIReader(array)
81+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
82+
with pytest.raises(IndexError):
83+
dz.get_tile(0, 100, 100)
84+
85+
86+
def test_zoomify_tile_group_index_error():
87+
"""Test IndexError for Zoomify tile groups."""
88+
array = np.ones((1024, 1024))
89+
wsi = wsireader.VirtualWSIReader(array)
90+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=256)
91+
with pytest.raises(IndexError):
92+
dz.tile_group(0, 100, 100)
93+
94+
95+
def test_zoomify_dump_options_combinations(tmp_path): # noqa: CCR001
96+
"""Test for no fatal errors on all option combinations for dump."""
97+
array = data.camera()
98+
wsi = wsireader.VirtualWSIReader(array)
99+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=64)
100+
101+
for container in [None, "zip", "tar"]:
102+
compression_methods = [None, "deflate", "gzip", "bz2", "lzma"]
103+
if container == "zip":
104+
compression_methods.remove("gzip")
105+
if container == "tar":
106+
compression_methods.remove("deflate")
107+
if container is None:
108+
compression_methods = [None]
109+
for compression in compression_methods:
110+
out_path = tmp_path / f"{compression}-pyramid"
111+
if container is not None:
112+
out_path = out_path.with_suffix(f".{container}")
113+
dz.dump(out_path, container=container, compression=compression)
114+
assert out_path.exists()
115+
116+
117+
def test_zoomify_dump_compression_error(tmp_path):
118+
"""Test ValueError is raised on invalid compression modes."""
119+
array = data.camera()
120+
wsi = wsireader.VirtualWSIReader(array)
121+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=64)
122+
out_path = tmp_path / "pyramid_dump"
123+
124+
with pytest.raises(ValueError, match="Unsupported compression for container None"):
125+
dz.dump(out_path, container=None, compression="deflate")
126+
127+
with pytest.raises(ValueError, match="Unsupported compression for zip"):
128+
dz.dump(out_path, container="zip", compression="gzip")
129+
130+
with pytest.raises(ValueError, match="Unsupported compression for tar"):
131+
dz.dump(out_path, container="tar", compression="deflate")
132+
133+
134+
def test_zoomify_dump_container_error(tmp_path):
135+
"""Test ValueError is raised on invalid containers."""
136+
array = data.camera()
137+
wsi = wsireader.VirtualWSIReader(array)
138+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=64)
139+
out_path = tmp_path / "pyramid_dump"
140+
141+
with pytest.raises(ValueError, match="Unsupported container"):
142+
dz.dump(out_path, container="foo")
143+
144+
145+
def test_zoomify_dump(tmp_path):
146+
"""Test dumping to directory."""
147+
array = data.camera()
148+
wsi = wsireader.VirtualWSIReader(array)
149+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=64)
150+
out_path = tmp_path / "pyramid_dump"
151+
dz.dump(out_path)
152+
assert out_path.exists()
153+
assert len(list((out_path / "TileGroup0").glob("0-*"))) == 1
154+
assert Image.open(out_path / "TileGroup0" / "0-0-0.jpg").size == (64, 64)
155+
156+
157+
def test_get_thumb_tile():
158+
"""Test getting a thumbnail tile (whole WSI in one tile)."""
159+
array = data.camera()
160+
wsi = wsireader.VirtualWSIReader(array)
161+
dz = pyramid.ZoomifyGenerator(wsi, tile_size=224)
162+
thumb = dz.get_thumb_tile()
163+
assert thumb.size == (224, 224)
164+
cv2_thumb = imresize(array, output_size=(224, 224))
165+
psnr = peak_signal_noise_ratio(cv2_thumb, np.array(thumb.convert("L")))
166+
assert np.isinf(psnr) or psnr < 40
167+
168+
169+
def test_sub_tile_levels():
170+
"""Test sub-tile level generation."""
171+
array = data.camera()
172+
wsi = wsireader.VirtualWSIReader(array)
173+
174+
class MockTileGenerator(pyramid.TilePyramidGenerator):
175+
def tile_path(self, level: int, x: int, y: int) -> Path:
176+
return Path(level, x, y)
177+
178+
@property
179+
def sub_tile_level_count(self):
180+
return 1
181+
182+
dz = MockTileGenerator(wsi, tile_size=224)
183+
184+
tile = dz.get_tile(0, 0, 0)
185+
assert tile.size == (112, 112)

0 commit comments

Comments
 (0)