Skip to content

Commit 451bdac

Browse files
John-Pmeastyshaneahmedpre-commit-ci[bot]
authored
✨ Extend Annotation to Support Init from WKB (#639)
I have created this as a proof of concept in regard to my comments on #632. This PR extends the `Annotation` class to enable the creation from either a set of coordinates and a geometry type (int) for direct decoding of the WKB OR from a shapely geometry object. The shapely geometry is created lazily, and vice versa; the coordinates can be accessed lazily if the Annotation was created from a Shapely object. # Summary 1. Enables init of `Annotation` from WKB bytes instead of from Shapely geometry object. This can shorten the path from database WKB -> Shapely -> Numpy Array for applications which just need the raw coordinates and not any Shapely/geos methods. 1. Move `decode_wkb` to an Annotation static method. This could potentially be optimized with numba or similar in future. 1. Lazily create a Shapely geometry object, WKB, or raw numpy coordinates as needed. This can avoid converting WKB to shapely and then to coordinates and go directly from WKB to coordinates. 1. Add tests. 1. Update code relying on `as_wkb`. 1. Bug fixes including fixing exceptions on appending unsupported geometry and generation of coordinates from WKB in `decode_wkb`. 2. Ensures the coordinates generates from Shapely and from `decode_wkb` via `Annotation.coords` are in the same format as each other. I.e. 1x2 array for points, nx2 array for lines, list of nx2 arrays for poylgons and lists for the multi forms. --------- Co-authored-by: measty <[email protected]> Co-authored-by: Shan E Ahmed Raza <[email protected]> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent f26387f commit 451bdac

File tree

8 files changed

+807
-234
lines changed

8 files changed

+807
-234
lines changed

tests/test_annotation_stores.py

Lines changed: 306 additions & 21 deletions
Large diffs are not rendered by default.

tests/test_annotation_tilerendering.py

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -257,21 +257,17 @@ def sub_tile_level_count(self):
257257

258258

259259
def test_unknown_geometry(
260-
fill_store,
261-
tmp_path: Path,
260+
fill_store, # noqa: ARG001
262261
caplog: pytest.LogCaptureFixture,
263262
) -> None:
264263
"""Test warning when unknown geometries cannot be rendered."""
265-
array = np.ones((1024, 1024))
266-
wsi = wsireader.VirtualWSIReader(array)
267-
_, store = fill_store(SQLiteStore, tmp_path / "test.db")
268-
store.append(
269-
Annotation(geometry=MultiPoint([(5.0, 5.0), (10.0, 10.0)]), properties={}),
270-
)
271-
store.commit()
272264
renderer = AnnotationRenderer(max_scale=8, edge_thickness=0)
273-
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
274-
tg.get_tile(0, 0, 0)
265+
renderer.render_by_type(
266+
tile=np.zeros((256, 256, 3), dtype=np.uint8),
267+
annotation=Annotation(MultiPoint([(5.0, 5.0), (10.0, 10.0)])),
268+
top_left=(0, 0),
269+
scale=1,
270+
)
275271
assert "Unknown geometry" in caplog.text
276272

277273

@@ -431,16 +427,16 @@ def test_unfilled_polys(fill_store, tmp_path: Path) -> None:
431427
assert np.sum(tile_filled) > 2 * np.sum(tile_outline)
432428

433429

434-
def test_multipolygon_render(cell_grid, tmp_path: Path) -> None:
430+
def test_multipolygon_render(cell_grid) -> None:
435431
"""Test multipolygon rendering."""
436-
array = np.ones((1024, 1024))
437-
wsi = wsireader.VirtualWSIReader(array, mpp=(1, 1))
438-
store = SQLiteStore(tmp_path / "test.db")
439-
# add a multi-polygon
440-
store.append(Annotation(MultiPolygon(cell_grid), {"color": (1, 0, 0)}))
441432
renderer = AnnotationRenderer(score_prop="color", edge_thickness=0)
442-
tg = AnnotationTileGenerator(wsi.info, store, renderer, tile_size=256)
443-
tile = np.array(tg.get_tile(1, 0, 0))
433+
tile = np.zeros((1024, 1024, 3), dtype=np.uint8)
434+
renderer.render_multipoly(
435+
tile=tile,
436+
annotation=Annotation(MultiPolygon(cell_grid), {"color": (1, 0, 0)}),
437+
top_left=(0, 0),
438+
scale=1,
439+
)
444440
_, num = label(np.array(tile)[:, :, 0])
445441
assert num == 25 # expect 25 red objects
446442

tests/test_enums.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Tests for the enumerated types used by TIAToolbox."""
2+
import pytest
3+
4+
from tiatoolbox.enums import GeometryType
5+
6+
7+
def test_geometrytype_missing() -> None:
8+
"""Test that GeometryType.MISSING is returned when given None."""
9+
with pytest.raises(ValueError, match="not a valid GeometryType"):
10+
GeometryType(None)
11+
12+
13+
def test_geometrytype_point_from_string() -> None:
14+
"""Init GeometryType.POINT from string."""
15+
assert GeometryType("Point") == GeometryType.POINT
16+
17+
18+
def test_geometrytype_linestring_from_string() -> None:
19+
"""Init GeometryType.LINE_STRING from string."""
20+
assert GeometryType("LineString") == GeometryType.LINE_STRING
21+
22+
23+
def test_geometrytype_polygon_from_string() -> None:
24+
"""Init GeometryType.POLYGON from string."""
25+
assert GeometryType("Polygon") == GeometryType.POLYGON
26+
27+
28+
def test_geometrytype_multipoint_from_string() -> None:
29+
"""Init GeometryType.MULTI_POINT from string."""
30+
assert GeometryType("MultiPoint") == GeometryType.MULTI_POINT
31+
32+
33+
def test_geometrytype_multilinestring_from_string() -> None:
34+
"""Init GeometryType.MULTI_LINE_STRING from string."""
35+
assert GeometryType("MultiLineString") == GeometryType.MULTI_LINE_STRING
36+
37+
38+
def test_geometrytype_multipolygon_from_string() -> None:
39+
"""Init GeometryType.MULTI_POLYGON from string."""
40+
assert GeometryType("MultiPolygon") == GeometryType.MULTI_POLYGON

tests/test_visualization.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
Polygon,
1818
)
1919

20+
from tiatoolbox.annotation.storage import Annotation
21+
from tiatoolbox.enums import GeometryType
2022
from tiatoolbox.utils.visualization import (
21-
AnnotationRenderer,
2223
overlay_prediction_contours,
2324
overlay_prediction_mask,
2425
overlay_probability_map,
@@ -27,7 +28,7 @@
2728
from tiatoolbox.wsicore.wsireader import WSIReader
2829

2930

30-
def test_overlay_prediction_mask(sample_wsi_dict) -> None:
31+
def test_overlay_prediction_mask(sample_wsi_dict: dict) -> None:
3132
"""Test for overlaying merged patch prediction of wsi."""
3233
mini_wsi_svs = Path(sample_wsi_dict["wsi2_4k_4k_svs"])
3334
mini_wsi_pred = Path(sample_wsi_dict["wsi2_4k_4k_pred"])
@@ -127,7 +128,7 @@ def test_overlay_prediction_mask(sample_wsi_dict) -> None:
127128
_ = overlay_prediction_mask(thumb_float, merged, min_val=0.5, return_ax=False)
128129

129130

130-
def test_overlay_probability_map(sample_wsi_dict) -> None:
131+
def test_overlay_probability_map(sample_wsi_dict: dict) -> None:
131132
"""Test functional run for overlaying merged patch prediction of wsi."""
132133
mini_wsi_svs = Path(sample_wsi_dict["wsi2_4k_4k_svs"])
133134
reader = WSIReader.open(mini_wsi_svs)
@@ -216,8 +217,9 @@ def test_overlay_instance_prediction() -> None:
216217
type_colours=type_colours,
217218
line_thickness=1,
218219
)
220+
ref_value = -12
219221
assert np.sum(canvas[..., 0].astype(np.int32) - inst_map) == 0
220-
assert np.sum(canvas[..., 1].astype(np.int32) - inst_map) == -12
222+
assert np.sum(canvas[..., 1].astype(np.int32) - inst_map) == ref_value
221223
assert np.sum(canvas[..., 2].astype(np.int32) - inst_map) == 0
222224
canvas = overlay_prediction_contours(
223225
canvas,
@@ -276,8 +278,6 @@ def test_plot_graph() -> None:
276278

277279
def test_decode_wkb() -> None:
278280
"""Test decoding of WKB geometries."""
279-
renderer = AnnotationRenderer()
280-
281281
# Create some Shapely geometries of supported types
282282
point = Point(0, 0)
283283
line = LineString([(0, 0), (1, 1), (2, 0)])
@@ -289,9 +289,18 @@ def test_decode_wkb() -> None:
289289
polygon_wkb = polygon.wkb
290290

291291
# Decode the WKB geometries
292-
point_contours = renderer.decode_wkb(point_wkb, 1).reshape(-1, 2)
293-
line_contours = renderer.decode_wkb(line_wkb, 2).reshape(-1, 2)
294-
polygon_contours = renderer.decode_wkb(polygon_wkb, 3).reshape(-1, 2)
292+
point_contours = Annotation.decode_wkb(
293+
point_wkb,
294+
GeometryType.POINT,
295+
)
296+
line_contours = Annotation.decode_wkb(
297+
line_wkb,
298+
GeometryType.LINE_STRING,
299+
)
300+
polygon_contours = Annotation.decode_wkb(
301+
polygon_wkb,
302+
GeometryType.POLYGON,
303+
)
295304

296305
# Check that the decoded contours are as expected
297306
assert np.all(point_contours == np.array([[0, 0]]))
@@ -314,9 +323,9 @@ def test_decode_wkb() -> None:
314323
multiline_wkb = multiline.wkb
315324
multipolygon_wkb = multipolygon.wkb
316325

317-
multipoint_contours = renderer.decode_wkb(multipoint_wkb, 4).reshape(3, -1, 2)
318-
multiline_contours = renderer.decode_wkb(multiline_wkb, 5).reshape(2, -1, 2)
319-
multipolygon_contours = renderer.decode_wkb(multipolygon_wkb, 6).reshape(2, -1, 2)
326+
multipoint_contours = Annotation.decode_wkb(multipoint_wkb, 4)
327+
multiline_contours = Annotation.decode_wkb(multiline_wkb, 5)
328+
multipolygon_contours = Annotation.decode_wkb(multipolygon_wkb, 6)
320329

321330
assert np.all(multipoint_contours == np.array([[[0, 0]], [[1, 1]], [[2, 0]]]))
322331
assert np.all(
@@ -335,4 +344,4 @@ def test_decode_wkb() -> None:
335344

336345
# test unknown geometry type
337346
with pytest.raises(ValueError, match=r"Unknown geometry type"):
338-
renderer.decode_wkb(multipolygon_wkb, 7)
347+
Annotation.decode_wkb(multipolygon_wkb, 7)

0 commit comments

Comments
 (0)