-
Notifications
You must be signed in to change notification settings - Fork 86
/
Copy pathtest_annotation_tilerendering.py
495 lines (411 loc) · 18.1 KB
/
test_annotation_tilerendering.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
"""Test for rendering tile annotations.
Test for annotation rendering using AnnotationRenderer and AnnotationTileGenerator.
"""
from __future__ import annotations
from pathlib import Path
from typing import Callable
import matplotlib.pyplot as plt
import numpy as np
import pytest
from matplotlib import colormaps
from PIL import Image, ImageFilter
from scipy.ndimage import label
from shapely.geometry import LineString, MultiPoint, MultiPolygon, Polygon
from shapely.geometry.point import Point
from skimage import data
from tests.test_annotation_stores import cell_polygon
from tiatoolbox.annotation import Annotation, AnnotationStore, SQLiteStore
from tiatoolbox.tools.pyramid import AnnotationTileGenerator
from tiatoolbox.utils.env_detection import running_on_travis
from tiatoolbox.utils.visualization import AnnotationRenderer, _find_minimum_mpp_sf
from tiatoolbox.wsicore import wsireader
RNG = np.random.default_rng(0) # Numpy Random Generator
@pytest.fixture(scope="session")
def cell_grid() -> list[Polygon]:
"""Generate a grid of fake cell boundary polygon annotations."""
return [
cell_polygon(((i + 0.5) * 100, (j + 0.5) * 100), radius=13)
for i, j in np.ndindex(5, 5)
]
@pytest.fixture(scope="session")
def points_grid(spacing: float = 60) -> list[Point]:
"""Generate a grid of fake point annotations."""
return [Point((600 + i * spacing, 600 + j * spacing)) for i, j in np.ndindex(7, 7)]
@pytest.fixture(scope="session")
def fill_store(
cell_grid: list[Polygon],
points_grid: list[Point],
) -> Callable[[list[str]], AnnotationStore]:
"""Factory fixture to fill stores with test data."""
def _fill_store(
store_class: AnnotationStore,
path: str | Path,
) -> tuple[list[str], AnnotationStore]:
"""Fills store with random variety of annotations."""
store = store_class(path)
cells = [
Annotation(
cell,
{"type": "cell", "prob": RNG.random(1)[0], "color": (0, 1, 0)},
)
for cell in cell_grid
]
points = [
Annotation(
point,
{"type": "pt", "prob": RNG.random(1)[0], "color": (1, 0, 0)},
)
for point in points_grid
]
lines = [
Annotation(
LineString((x, x + 500) for x in range(100, 400, 10)),
{"type": "line", "prob": 0.75, "color": (0, 0, 1)},
),
]
annotations = cells + points + lines
keys = store.append_many(annotations)
return keys, store
return _fill_store
def test_tile_generator_len(fill_store: Callable, tmp_path: Path) -> None:
"""Test __len__ for AnnotationTileGenerator."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
tg = AnnotationTileGenerator(wsi.info, store, tile_size=256)
assert len(tg) == (4 * 4) + (2 * 2) + 1
def test_tile_generator_iter(fill_store: Callable, tmp_path: Path) -> None:
"""Test __iter__ for AnnotationTileGenerator."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
tg = AnnotationTileGenerator(wsi.info, store, tile_size=256)
for tile in tg:
assert isinstance(tile, Image.Image)
assert tile.size == (256, 256)
@pytest.mark.skipif(running_on_travis(), reason="no display on travis.")
def test_show_generator_iter(fill_store: Callable, tmp_path: Path) -> None:
"""Show tiles with example annotations (if not travis)."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer("prob")
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
for i, tile in enumerate(tg):
if i > 5:
break
assert isinstance(tile, Image.Image)
assert tile.size == (256, 256)
plt.imshow(tile)
plt.show(block=False)
def test_correct_number_rendered(fill_store: Callable, tmp_path: Path) -> None:
"""Test that the expected number of annotations are rendered."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer)
thumb = tg.get_thumb_tile()
_, num = label(np.array(thumb)[:, :, 1]) # default color is green
assert num == 75 # expect 75 rendered objects
def test_correct_color_rendered(fill_store: Callable, tmp_path: Path) -> None:
"""Test color mapping."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(
"type",
{"cell": (1, 0, 0, 1), "pt": (0, 1, 0, 1), "line": (0, 0, 1, 1)},
edge_thickness=0,
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_thumb_tile()
_, num = label(np.array(thumb)[:, :, 1])
assert num == 49 # expect 49 green objects
_, num = label(np.array(thumb)[:, :, 0])
assert num == 25 # expect 25 red objects
_, num = label(np.array(thumb)[:, :, 2])
assert num == 1 # expect 1 blue objects
def test_filter_by_expression(fill_store: Callable, tmp_path: Path) -> None:
"""Test filtering using a where expression."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(
where='props["type"] == "cell"',
edge_thickness=0,
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_thumb_tile()
_, num = label(np.array(thumb)[:, :, 1])
assert num == 25 # expect 25 cell objects
def test_zoomed_out_rendering(fill_store: Callable, tmp_path: Path) -> None:
"""Test that the expected number of annotations are rendered."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
small_annotation = Annotation(
Polygon([(9, 9), (9, 10), (10, 10), (10, 9)]),
{"type": "cell", "prob": 0.75, "color": (0, 0, 1)},
)
store.append(small_annotation)
renderer = AnnotationRenderer(
max_scale=1,
edge_thickness=0,
zoomed_out_strat="scale",
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_tile(1, 0, 0)
_, num = label(np.array(thumb)[:, :, 1]) # default color is green
assert num == 25 # expect 25 cells in top left quadrant (added one too small)
def test_decimation(fill_store: Callable, tmp_path: Path) -> None:
"""Test decimation."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(max_scale=1, zoomed_out_strat="decimate")
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_tile(1, 1, 1)
plt.imshow(thumb)
plt.show(block=False)
_, num = label(np.array(thumb)[:, :, 1]) # default color is green
assert num == 17 # expect 17 pts in bottom right quadrant
def test_get_tile_negative_level(fill_store: Callable, tmp_path: Path) -> None:
"""Test for IndexError on negative levels."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array)
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(max_scale=1, edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
with pytest.raises(IndexError):
tg.get_tile(-1, 0, 0)
def test_get_tile_large_level(fill_store: Callable, tmp_path: Path) -> None:
"""Test for IndexError on too large a level."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array)
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(max_scale=1, edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
with pytest.raises(IndexError):
tg.get_tile(100, 0, 0)
def test_get_tile_large_xy(fill_store: Callable, tmp_path: Path) -> None:
"""Test for IndexError on too large an xy index."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array)
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(max_scale=1)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
with pytest.raises(IndexError):
tg.get_tile(0, 100, 100)
def test_sub_tile_levels(fill_store: Callable, tmp_path: Path) -> None:
"""Test sub-tile level generation."""
array = data.camera()
wsi = wsireader.VirtualWSIReader(array)
class MockTileGenerator(AnnotationTileGenerator):
"""Mock generator with specific subtile_level."""
def tile_path(
self: MockTileGenerator,
level: int,
x: int,
y: int,
) -> Path: # skipcq: PYL-R0201
"""Tile path."""
return Path(level, x, y)
@property
def sub_tile_level_count(self: MockTileGenerator) -> int:
return 1
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
tg = MockTileGenerator(wsi.info, store, tile_size=224)
tile = tg.get_tile(0, 0, 0)
assert tile.size == (112, 112)
def test_unknown_geometry(
fill_store: Callable, # noqa: ARG001
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test warning when unknown geometries cannot be rendered."""
renderer = AnnotationRenderer(max_scale=8, edge_thickness=0)
renderer.render_by_type(
tile=np.zeros((256, 256, 3), dtype=np.uint8),
annotation=Annotation(MultiPoint([(5.0, 5.0), (10.0, 10.0)])),
top_left=(0, 0),
scale=1,
)
assert "Unknown geometry" in caplog.text
def test_interp_pad_warning(
fill_store: Callable,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test warning when providing unused options."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array)
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
tg = AnnotationTileGenerator(wsi.info, store, tile_size=256)
tg.get_tile(0, 0, 0, pad_mode="constant")
assert "interpolation, pad_mode are unused" in caplog.text
def test_user_provided_cm(fill_store: Callable, tmp_path: Path) -> None:
"""Test correct color mapping for user-provided cm name."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(
"prob",
"viridis",
edge_thickness=0,
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tile = np.array(tg.get_tile(1, 0, 1)) # line here with prob=0.75
color = tile[np.any(tile, axis=2), :3]
color = color[0, :]
viridis_mapper = colormaps["viridis"]
assert np.all(
np.equal(color, (np.array(viridis_mapper(0.75)) * 255)[:3].astype(np.uint8)),
) # expect rendered color to be viridis(0.75)
def test_random_mapper() -> None:
"""Test random color map dict for list."""
test_list = ["line", "pt", "cell"]
renderer = AnnotationRenderer(mapper=test_list)
# check all the colors are valid rgba values
for ann_type in test_list:
rgba = renderer.mapper(ann_type)
assert isinstance(rgba, tuple)
assert len(rgba) == 4
for val in rgba:
assert 0 <= val <= 1
def test_categorical_mapper(fill_store: Callable, tmp_path: Path) -> None:
"""Test categorical mapper option to ease cli usage."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(score_prop="type", mapper="categorical")
AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
# check correct keys exist and all colors are valid rgba values
for ann_type in ["line", "pt", "cell"]:
rgba = renderer.mapper(ann_type)
assert isinstance(rgba, tuple)
assert len(rgba) == 4
for val in rgba:
assert 0 <= val <= 1
def test_color_prop_warnings(
fill_store: Callable,
tmp_path: Path,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test warning with inappropriate property.
Test warning is correctly shown when rendering annotations when the provided
score_prop does not exist, or its type does not match the mapper.
"""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(score_prop="nonexistant_prop")
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tg.get_tile(1, 0, 0)
assert "not found in properties" in caplog.text
renderer = AnnotationRenderer(score_prop="type", mapper="jet")
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tg.get_tile(1, 0, 0)
assert "property value type incompatable" in caplog.text
def test_blur(fill_store: Callable, tmp_path: Path) -> None:
"""Test blur."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(blur_radius=5, edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tile_blurred = tg.get_tile(1, 0, 0)
renderer = AnnotationRenderer(edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tile = tg.get_tile(1, 0, 0)
blur_filter = ImageFilter.GaussianBlur(5)
# blurring our un-blurred tile should give almost same result
assert np.allclose(tile_blurred, tile.filter(blur_filter), atol=1)
def test_direct_color(fill_store: Callable, tmp_path: Path) -> None:
"""Test direct color."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(score_prop="color", edge_thickness=0)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_thumb_tile()
_, num = label(np.array(thumb)[:, :, 1])
assert num == 25 # expect 25 green objects
_, num = label(np.array(thumb)[:, :, 0])
assert num == 49 # expect 49 red objects
_, num = label(np.array(thumb)[:, :, 2])
assert num == 1 # expect 1 blue objects
def test_secondary_cmap(fill_store: Callable, tmp_path: Path) -> None:
"""Test secondary cmap."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
cmap_dict = {"type": "line", "score_prop": "prob", "mapper": colormaps["viridis"]}
renderer = AnnotationRenderer(
score_prop="type",
secondary_cmap=cmap_dict,
edge_thickness=0,
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tile = np.array(tg.get_tile(1, 0, 1)) # line here with prob=0.75
color = tile[np.any(tile, axis=2), :3]
color = color[0, :]
viridis_mapper = colormaps["viridis"]
assert np.all(
np.equal(color, (np.array(viridis_mapper(0.75)) * 255)[:3].astype(np.uint8)),
) # expect rendered color to be viridis(0.75)
def test_unfilled_polys(fill_store: Callable, tmp_path: Path) -> None:
"""Test unfilled polygons."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
renderer = AnnotationRenderer(thickness=1)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
tile_outline = np.array(tg.get_tile(1, 0, 0))
tg.renderer.thickness = -1
tile_filled = np.array(tg.get_tile(1, 0, 0))
# expect sum of filled polys to be much greater than sum of outlines
assert np.sum(tile_filled) > 2 * np.sum(tile_outline)
def test_multipolygon_render(cell_grid: list[Polygon]) -> None:
"""Test multipolygon rendering."""
renderer = AnnotationRenderer(score_prop="color", edge_thickness=0)
tile = np.zeros((1024, 1024, 3), dtype=np.uint8)
renderer.render_multipoly(
tile=tile,
annotation=Annotation(MultiPolygon(cell_grid), {"color": (1, 0, 0)}),
top_left=(0, 0),
scale=1,
)
_, num = label(np.array(tile)[:, :, 0])
assert num == 25 # expect 25 red objects
def test_function_mapper(fill_store: Callable, tmp_path: Path) -> None:
"""Test function mapper."""
array = np.ones((1024, 1024))
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
def color_fn(props: dict[str, str]) -> tuple[int, int, int]:
"""Tests Red for cells, otherwise green."""
# simple test function that returns red for cells, otherwise green.
if props["type"] == "cell":
return 1, 0, 0
return 0, 1, 0
renderer = AnnotationRenderer(
score_prop="type",
function_mapper=color_fn,
edge_thickness=0,
)
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
thumb = tg.get_thumb_tile()
_, num = label(np.array(thumb)[:, :, 0])
assert num == 25 # expect 25 red objects
_, num = label(np.array(thumb)[:, :, 1])
assert num == 50 # expect 50 green objects
_, num = label(np.array(thumb)[:, :, 2])
assert num == 0 # expect 0 blue objects
def test_minimum_mpp_sf() -> None:
"""Test minimum mpp_sf."""
mpp_sf = _find_minimum_mpp_sf((0.5, 0.5))
assert mpp_sf == 1.0
mpp_sf = _find_minimum_mpp_sf((0.20, 0.20))
assert mpp_sf == 0.20 / 0.25
mpp_sf = _find_minimum_mpp_sf(None)
assert mpp_sf == 1.0