From efdf79044844f3725ea724465b3deeea408388f8 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:58:25 +0100 Subject: [PATCH 01/15] address review comments --- tiatoolbox/annotation/storage.py | 14 +++++++++ tiatoolbox/utils/visualization.py | 36 ++++++++++++++++++++-- tiatoolbox/visualization/bokeh_app/main.py | 12 +++++--- tiatoolbox/visualization/tileserver.py | 2 +- 4 files changed, 56 insertions(+), 8 deletions(-) diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index 3936efe60..eac20333d 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1990,6 +1990,20 @@ def transform_geometry(geom: Geometry) -> Geometry: ) for feature in geojson["features"] ] + # check for presence of 'nucleusGeometry' key in features + # if present, add them (qupath export format) + annotations += [ + transform( + Annotation( + transform_geometry( + feature2geometry(feature["nucleusGeometry"]), + ), + {}, + ), + ) + for feature in geojson["features"] + if "nucleusGeometry" in feature + ] logger.info("Adding %d annotations.", len(annotations)) self.append_many(annotations) diff --git a/tiatoolbox/utils/visualization.py b/tiatoolbox/utils/visualization.py index 142b6e061..007093809 100644 --- a/tiatoolbox/utils/visualization.py +++ b/tiatoolbox/utils/visualization.py @@ -843,11 +843,41 @@ def render_pt( top_left, scale, )[0][0], - np.maximum(self.edge_thickness, 1), + np.maximum(int(16 / scale**0.5), 1), col, - thickness=self.thickness, + thickness=-1, ) + def render_pts( + self: AnnotationRenderer, + tile: np.ndarray, + annotation: Annotation, + top_left: tuple[float, float], + scale: float, + ) -> None: + """Render a multipoint annotation onto a tile using cv2. + + Args: + tile (ndarray): + The rgb(a) tile image to render onto. + annotation (Annotation): + The annotation to render. + top_left (tuple): + The top left corner of the tile in wsi. + scale (float): + The zoom scale at which we are rendering. + + """ + col = self.get_color(annotation, edge=False) + for pt in annotation.coords: + cv2.circle( + tile, + self.to_tile_coords([pt], top_left, scale)[0][0], + np.maximum(int(16 / scale**0.5), 1), + col, + thickness=-1, + ) + def render_line( self: AnnotationRenderer, tile: np.ndarray, @@ -1052,5 +1082,7 @@ def render_by_type( self.render_poly(tile, annotation, top_left, scale) elif geom_type == GeometryType.LINE_STRING: self.render_line(tile, annotation, top_left, scale) + elif geom_type == GeometryType.MULTI_POINT: + self.render_pts(tile, annotation, top_left, scale) else: logger.warning("Unknown geometry: %s", geom_type, stacklevel=3) diff --git a/tiatoolbox/visualization/bokeh_app/main.py b/tiatoolbox/visualization/bokeh_app/main.py index e86168506..a033844ef 100644 --- a/tiatoolbox/visualization/bokeh_app/main.py +++ b/tiatoolbox/visualization/bokeh_app/main.py @@ -14,6 +14,10 @@ import numpy as np import requests import torch +from matplotlib import colormaps +from PIL import Image +from requests.adapters import HTTPAdapter, Retry + from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from bokeh.io import curdoc from bokeh.layouts import column, row @@ -58,9 +62,6 @@ from bokeh.models.tiles import WMTSTileSource from bokeh.plotting import figure from bokeh.util import token -from matplotlib import colormaps -from PIL import Image -from requests.adapters import HTTPAdapter, Retry # GitHub actions seems unable to find TIAToolbox unless this is here sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) @@ -511,7 +512,6 @@ def add_layer(lname: str) -> None: end=1, value=0.75, step=0.01, - title=lname, height=40, width=100, max_width=90, @@ -1053,7 +1053,9 @@ def layer_slider_cb( UI["vstate"].layer_dict[obj.name.split("_")[0]] ].glyph.line_alpha = new else: - UI["p"].renderers[UI["vstate"].layer_dict[obj.name.split("_")[0]]].alpha = new + UI["p"].renderers[ + UI["vstate"].layer_dict["_".join(obj.name.split("_")[0:-1])] + ].alpha = new def color_input_cb( diff --git a/tiatoolbox/visualization/tileserver.py b/tiatoolbox/visualization/tileserver.py index b0a427502..5e2d5ad06 100644 --- a/tiatoolbox/visualization/tileserver.py +++ b/tiatoolbox/visualization/tileserver.py @@ -511,7 +511,7 @@ def change_overlay(self: TileServer) -> str: overlay_path = self.decode_safe_name(overlay_path) if overlay_path.suffix in [".jpg", ".png", ".tiff", ".svs", ".ndpi", ".mrxs"]: - layer = f"layer{len(self.pyramids[session_id])}" + layer = overlay_path.stem if overlay_path.suffix == ".tiff": self.layers[session_id][layer] = OpenSlideWSIReader( overlay_path, From 3e7938b91df77850751d70b59263d02895b165fc Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:08:55 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tiatoolbox/visualization/bokeh_app/main.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tiatoolbox/visualization/bokeh_app/main.py b/tiatoolbox/visualization/bokeh_app/main.py index a033844ef..0b40dd3bb 100644 --- a/tiatoolbox/visualization/bokeh_app/main.py +++ b/tiatoolbox/visualization/bokeh_app/main.py @@ -14,10 +14,6 @@ import numpy as np import requests import torch -from matplotlib import colormaps -from PIL import Image -from requests.adapters import HTTPAdapter, Retry - from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from bokeh.io import curdoc from bokeh.layouts import column, row @@ -62,6 +58,9 @@ from bokeh.models.tiles import WMTSTileSource from bokeh.plotting import figure from bokeh.util import token +from matplotlib import colormaps +from PIL import Image +from requests.adapters import HTTPAdapter, Retry # GitHub actions seems unable to find TIAToolbox unless this is here sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) From f4b3c906c5bac77cebc44c0684740d1882d1212f Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:33:50 +0100 Subject: [PATCH 03/15] qupath measurements support --- tiatoolbox/annotation/storage.py | 4 ++-- tiatoolbox/visualization/tileserver.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/tiatoolbox/annotation/storage.py b/tiatoolbox/annotation/storage.py index eac20333d..fe0d6b1e9 100644 --- a/tiatoolbox/annotation/storage.py +++ b/tiatoolbox/annotation/storage.py @@ -1991,14 +1991,14 @@ def transform_geometry(geom: Geometry) -> Geometry: for feature in geojson["features"] ] # check for presence of 'nucleusGeometry' key in features - # if present, add them (qupath export format) + # if present, add them (support qupath export format) annotations += [ transform( Annotation( transform_geometry( feature2geometry(feature["nucleusGeometry"]), ), - {}, + {"type": "nucleus"}, ), ) for feature in geojson["features"] diff --git a/tiatoolbox/visualization/tileserver.py b/tiatoolbox/visualization/tileserver.py index 5e2d5ad06..3a08a7b13 100644 --- a/tiatoolbox/visualization/tileserver.py +++ b/tiatoolbox/visualization/tileserver.py @@ -30,6 +30,7 @@ if TYPE_CHECKING: # pragma: no cover from matplotlib.colors import Colormap + from tiatoolbox.annotation.storage import Annotation from tiatoolbox.wsicore import WSIMeta @@ -529,7 +530,19 @@ def change_overlay(self: TileServer) -> str: ) return json.dumps(layer) if overlay_path.suffix == ".geojson": - sq = SQLiteStore.from_geojson(overlay_path) + + def unpack_qupath(ann: Annotation) -> Annotation: + # Helper function to unpack QuPath measurements if present. + props = ann.properties + if "measurements" in props: + measurements = props.pop("measurements") + for k, v in measurements.items(): + props[k] = v + if "objectType" in props: + props["type"] = props.pop("objectType") + return ann + + sq = SQLiteStore.from_geojson(overlay_path, transform=unpack_qupath) elif overlay_path.suffix == ".dat": sq = store_from_dat(overlay_path) if overlay_path.suffix == ".db": From 908120a66f37c494794aebe4bd713fb8b87c483e Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Thu, 29 May 2025 15:33:50 +0100 Subject: [PATCH 04/15] update tests --- tests/test_annotation_tilerendering.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_annotation_tilerendering.py b/tests/test_annotation_tilerendering.py index 0ee34b17b..abfdb495a 100644 --- a/tests/test_annotation_tilerendering.py +++ b/tests/test_annotation_tilerendering.py @@ -270,15 +270,37 @@ def sub_tile_level_count(self: MockTileGenerator) -> int: assert tile.size == (112, 112) +def test_multi_point() -> None: + """Test multi-point rendering.""" + renderer = AnnotationRenderer(max_scale=8, edge_thickness=0) + tile = np.zeros((256, 256, 3), dtype=np.uint8) + renderer.render_by_type( + tile=tile, + annotation=Annotation(MultiPoint([(5.0, 5.0), (10.0, 10.0)])), + top_left=(0, 0), + scale=1, + ) + # check point locations are now non-zero + assert np.any(tile[5, 5, :] > 0) + assert np.any(tile[10, 10, :] > 0) + # check a non-point region still zero + assert np.all(tile[100:150, 100:150, :] == 0) + + 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) + unknown_ann = Annotation( + MultiPolygon( + [Polygon.from_bounds(0, 0, 1, 1), Polygon.from_bounds(2, 2, 3, 3)] + ), + {}, + ) renderer.render_by_type( tile=np.zeros((256, 256, 3), dtype=np.uint8), - annotation=Annotation(MultiPoint([(5.0, 5.0), (10.0, 10.0)])), + annotation=unknown_ann, top_left=(0, 0), scale=1, ) From 68b67ad0800a5e609562878880770e5321523d84 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Thu, 29 May 2025 16:14:42 +0100 Subject: [PATCH 05/15] tests fix --- tests/test_app_bokeh.py | 9 +++++---- tiatoolbox/tools/pyramid.py | 4 +++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index cb99a3df3..000fb4d94 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -404,7 +404,8 @@ def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> N # trigger an event to select the image overlay click = MenuItemClick(layer_drop, str(data_path["img_overlay"])) layer_drop._trigger_event(click) - layer_slider = doc.get_model_by_name("layer2_slider") + l_name = "CMU-1-Small-Region_rendered_annotations" + layer_slider = doc.get_model_by_name(f"{l_name}_slider") assert layer_slider is not None # check alpha controls @@ -413,14 +414,14 @@ def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> N assert type_column_list[-1].active # toggle off and check alpha is 0 type_column_list[-1].active = False - assert main.UI["p"].renderers[main.UI["vstate"].layer_dict["layer2"]].alpha == 0 + assert main.UI["p"].renderers[main.UI["vstate"].layer_dict[l_name]].alpha == 0 # toggle back on and check alpha is back to default 0.75 type_column_list[-1].active = True - assert main.UI["p"].renderers[main.UI["vstate"].layer_dict["layer2"]].alpha == 0.75 + assert main.UI["p"].renderers[main.UI["vstate"].layer_dict[l_name]].alpha == 0.75 # set alpha to 0.4 layer_slider.value = 0.4 # check that the alpha values have been set correctly - assert main.UI["p"].renderers[main.UI["vstate"].layer_dict["layer2"]].alpha == 0.4 + assert main.UI["p"].renderers[main.UI["vstate"].layer_dict[l_name]].alpha == 0.4 def test_hovernet_on_box(doc: Document, data_path: pytest.TempPathFactory) -> None: diff --git a/tiatoolbox/tools/pyramid.py b/tiatoolbox/tools/pyramid.py index bacc8edf0..07ef4dff0 100644 --- a/tiatoolbox/tools/pyramid.py +++ b/tiatoolbox/tools/pyramid.py @@ -362,7 +362,9 @@ def save_tile(tile_path: Path, tile: Image.Image) -> None: msg = "Unsupported compression for tar." raise ValueError(msg) - tar_archive = tarfile.TarFile.open(path, mode=compression2mode[compression]) + tar_archive = tarfile.TarFile.open( + str(path), mode=compression2mode[compression] + ) def save_tile(tile_path: Path, tile: Image.Image) -> None: """Write the tile to the output zip.""" From 71b9baa6b2cc2f9ac38232bef1c4979a71016c2b Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Thu, 29 May 2025 16:58:07 +0100 Subject: [PATCH 06/15] handhold mypy through types --- tests/test_tileserver.py | 6 +++--- tiatoolbox/tools/pyramid.py | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_tileserver.py b/tests/test_tileserver.py index c4632b403..7f0368004 100644 --- a/tests/test_tileserver.py +++ b/tests/test_tileserver.py @@ -461,7 +461,7 @@ def test_change_overlay( # noqa: PLR0915 assert response.status_code == 200 assert response.content_type == "text/html; charset=utf-8" # check that the overlay has been correctly added - lname = f"layer{len(empty_app.pyramids[session_id]) - 1}" + lname = Path(overlay_path).stem layer = empty_app.pyramids[session_id][lname] assert layer.wsi.info.file_path == overlay_path @@ -493,7 +493,7 @@ def test_change_overlay( # noqa: PLR0915 data={"overlay_path": safe_str(jpg_path)}, ) # check that the overlay has been correctly added - lname = f"layer{len(empty_app.pyramids[session_id]) - 1}" + lname = Path(jpg_path).stem layer = empty_app.pyramids[session_id][lname] assert np.all(layer.wsi.img == imread(jpg_path)) @@ -517,7 +517,7 @@ def test_change_overlay( # noqa: PLR0915 data={"overlay_path": safe_str(tiff_path)}, ) # check that the overlay has been correctly added - lname = f"layer{len(empty_app.pyramids[session_id]) - 1}" + lname = Path(tiff_path).stem layer = empty_app.pyramids[session_id][lname] assert layer.wsi.info.file_path == tiff_path diff --git a/tiatoolbox/tools/pyramid.py b/tiatoolbox/tools/pyramid.py index 07ef4dff0..08675bec8 100644 --- a/tiatoolbox/tools/pyramid.py +++ b/tiatoolbox/tools/pyramid.py @@ -29,6 +29,7 @@ if TYPE_CHECKING: # pragma: no cover from collections.abc import Iterator + from typing import Literal from tiatoolbox.annotation import AnnotationStore from tiatoolbox.wsicore.wsireader import WSIMeta, WSIReader @@ -352,7 +353,9 @@ def save_tile(tile_path: Path, tile: Image.Image) -> None: ) else: # container == "tar": - compression2mode = { + compression2mode: dict[ + str | None, Literal["w", "w:gz", "w:bz2", "w:xz"] + ] = { None: "w", "gzip": "w:gz", "bz2": "w:bz2", From 777531ddf2ea9dec6c16290c6230fe9bc21d08df Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 14 Aug 2025 19:24:24 +0000 Subject: [PATCH 07/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_app_bokeh.py | 8 ++++---- tiatoolbox/visualization/bokeh_app/main.py | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index ce10a8d6f..d391aaaaf 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -11,19 +11,19 @@ from pathlib import Path from typing import TYPE_CHECKING +import bokeh.models as bkmodels import matplotlib.pyplot as plt import numpy as np import pytest import requests +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from flask_cors import CORS from matplotlib import colormaps from PIL import Image from scipy.ndimage import label -import bokeh.models as bkmodels -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from tiatoolbox.data import _fetch_remote_sample from tiatoolbox.visualization.bokeh_app import main from tiatoolbox.visualization.tileserver import TileServer diff --git a/tiatoolbox/visualization/bokeh_app/main.py b/tiatoolbox/visualization/bokeh_app/main.py index 204fa115d..81395300d 100644 --- a/tiatoolbox/visualization/bokeh_app/main.py +++ b/tiatoolbox/visualization/bokeh_app/main.py @@ -14,10 +14,6 @@ import numpy as np import requests import torch -from matplotlib import colormaps -from PIL import Image -from requests.adapters import HTTPAdapter, Retry - from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from bokeh.io import curdoc from bokeh.layouts import column, row @@ -62,6 +58,9 @@ from bokeh.models.tiles import WMTSTileSource from bokeh.plotting import figure from bokeh.util import token +from matplotlib import colormaps +from PIL import Image +from requests.adapters import HTTPAdapter, Retry # GitHub actions seems unable to find TIAToolbox unless this is here sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent)) From d9b2ffc8ed57ce234d5c4b58c4f9e293a7f2cab5 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Thu, 14 Aug 2025 20:59:18 +0100 Subject: [PATCH 08/15] address comments --- tests/test_app_bokeh.py | 8 +++++++- tiatoolbox/tools/pyramid.py | 1 - tiatoolbox/visualization/tileserver.py | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index ce10a8d6f..313d89de1 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -434,7 +434,7 @@ def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> N # trigger an event to select the image overlay click = MenuItemClick(layer_drop, str(data_path["img_overlay"])) layer_drop._trigger_event(click) - l_name = "CMU-1-Small-Region_rendered_annotations" + l_name = data_path["img_overlay"].stem layer_slider = doc.get_model_by_name(f"{l_name}_slider") assert layer_slider is not None @@ -453,6 +453,12 @@ def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> N # check that the alpha values have been set correctly assert main.UI["p"].renderers[main.UI["vstate"].layer_dict[l_name]].alpha == 0.4 + # check loading a new layer with same stem uses full file name to disambiguate + click = MenuItemClick(layer_drop, str(data_path["img_overlay"])) + layer_drop._trigger_event(click) + full_name = data_path["img_overlay"].name + assert full_name in main.UI["vstate"].layer_dict + def test_hovernet_on_box(doc: Document, data_path: pytest.TempPathFactory) -> None: """Test running hovernet on a box.""" diff --git a/tiatoolbox/tools/pyramid.py b/tiatoolbox/tools/pyramid.py index 47579c26a..3483a38cb 100644 --- a/tiatoolbox/tools/pyramid.py +++ b/tiatoolbox/tools/pyramid.py @@ -29,7 +29,6 @@ if TYPE_CHECKING: # pragma: no cover from collections.abc import Iterator - from typing import Literal from tiatoolbox.annotation import AnnotationStore from tiatoolbox.wsicore.wsireader import WSIMeta, WSIReader diff --git a/tiatoolbox/visualization/tileserver.py b/tiatoolbox/visualization/tileserver.py index 7ff4f77ce..ee61dfb62 100644 --- a/tiatoolbox/visualization/tileserver.py +++ b/tiatoolbox/visualization/tileserver.py @@ -587,6 +587,9 @@ def _apply_transform(source_fp: str, target_fp: str) -> None: def _add_image_overlay(self, session_id: str, overlay_path: Path) -> str: layer = overlay_path.stem + if layer in self.layers[session_id]: + # use full file name to disambiguate + layer = overlay_path.name if overlay_path.suffix == ".tiff": self.layers[session_id][layer] = OpenSlideWSIReader( overlay_path, From 233aba31641f533c072873d2a9c9a5ef8192a832 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 15 Aug 2025 01:14:31 +0100 Subject: [PATCH 09/15] adjust tests for new naming convention --- tiatoolbox/visualization/tileserver.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tiatoolbox/visualization/tileserver.py b/tiatoolbox/visualization/tileserver.py index ee61dfb62..b7c750938 100644 --- a/tiatoolbox/visualization/tileserver.py +++ b/tiatoolbox/visualization/tileserver.py @@ -539,7 +539,7 @@ def _handle_registration_overlay( self, session_id: str, overlay_path: Path, - other_session_id: str, + other_session_id: str | None, ) -> str: def _apply_transform(source_fp: str, target_fp: str) -> None: # loading a registration transformation @@ -560,10 +560,11 @@ def _apply_transform(source_fp: str, target_fp: str) -> None: ) return json.dumps("slide") - layer_keys = [k for k in self.layers[session_id] if "layer" in k] - for key in sorted( - layer_keys, key=lambda x: int(x.replace("layer", "")), reverse=True - ): + layer_keys = [ + k for k in self.layers[session_id] if k not in ["slide", "overlay"] + ] + layer_keys.reverse() # try newest first + for key in layer_keys: target_fp = self.layers[session_id][key].info.file_path if Path(target_fp).suffix in [".tiff", ".svs", ".ndpi", ".mrxs"]: logger.warning( From b36581eee919af3b559077ea80f03169394d8c8d Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Tue, 2 Sep 2025 03:24:10 +0100 Subject: [PATCH 10/15] test update --- tests/test_app_bokeh.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index 84cebee7e..c776affe5 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -11,19 +11,19 @@ from pathlib import Path from typing import TYPE_CHECKING -import bokeh.models as bkmodels import matplotlib.pyplot as plt import numpy as np import pytest import requests -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from flask_cors import CORS from matplotlib import colormaps from PIL import Image from scipy.ndimage import label +import bokeh.models as bkmodels +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from tiatoolbox.data import _fetch_remote_sample from tiatoolbox.visualization.bokeh_app import main from tiatoolbox.visualization.tileserver import TileServer @@ -454,6 +454,8 @@ def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> N assert main.UI["p"].renderers[main.UI["vstate"].layer_dict[l_name]].alpha == 0.4 # check loading a new layer with same stem uses full file name to disambiguate + click = MenuItemClick(layer_drop, str(data_path["annotations"])) + layer_drop._trigger_event(click) click = MenuItemClick(layer_drop, str(data_path["img_overlay"])) layer_drop._trigger_event(click) full_name = data_path["img_overlay"].name From 97146428da8a8ebe296d6d2b9bba17eb77f62e81 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 15:34:10 +0000 Subject: [PATCH 11/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_app_bokeh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index c776affe5..02fb7ee2b 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -11,19 +11,19 @@ from pathlib import Path from typing import TYPE_CHECKING +import bokeh.models as bkmodels import matplotlib.pyplot as plt import numpy as np import pytest import requests +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from flask_cors import CORS from matplotlib import colormaps from PIL import Image from scipy.ndimage import label -import bokeh.models as bkmodels -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from tiatoolbox.data import _fetch_remote_sample from tiatoolbox.visualization.bokeh_app import main from tiatoolbox.visualization.tileserver import TileServer From 16ef98559545f3174b322c04434510754234a139 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 12 Sep 2025 00:09:30 +0100 Subject: [PATCH 12/15] update test --- tests/test_app_bokeh.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index 02fb7ee2b..eccb62af9 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -11,19 +11,19 @@ from pathlib import Path from typing import TYPE_CHECKING -import bokeh.models as bkmodels import matplotlib.pyplot as plt import numpy as np import pytest import requests -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from flask_cors import CORS from matplotlib import colormaps from PIL import Image from scipy.ndimage import label +import bokeh.models as bkmodels +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from tiatoolbox.data import _fetch_remote_sample from tiatoolbox.visualization.bokeh_app import main from tiatoolbox.visualization.tileserver import TileServer @@ -285,7 +285,7 @@ def test_add_annotation_layer(doc: Document, data_path: pytest.TempPathFactory) # trigger an event to select the geojson file click = MenuItemClick(layer_drop, str(data_path["geojson_anns"])) layer_drop._trigger_event(click) - assert main.UI["vstate"].types == ["annotation"] + assert main.UI["vstate"].types == ["nucleus", "cell", "annotation"] # test the name2type function. assert main.name2type("annotation") == '"annotation"' From f31fff773b7c01448a6d6d750f42b4be56de53d1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:11:00 +0000 Subject: [PATCH 13/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- tests/test_app_bokeh.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index eccb62af9..e90d09671 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -11,19 +11,19 @@ from pathlib import Path from typing import TYPE_CHECKING +import bokeh.models as bkmodels import matplotlib.pyplot as plt import numpy as np import pytest import requests +from bokeh.application import Application +from bokeh.application.handlers import FunctionHandler +from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from flask_cors import CORS from matplotlib import colormaps from PIL import Image from scipy.ndimage import label -import bokeh.models as bkmodels -from bokeh.application import Application -from bokeh.application.handlers import FunctionHandler -from bokeh.events import ButtonClick, DoubleTap, MenuItemClick from tiatoolbox.data import _fetch_remote_sample from tiatoolbox.visualization.bokeh_app import main from tiatoolbox.visualization.tileserver import TileServer From 578cebd7ed085e30cf28037c029e175ca08caae4 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 12 Sep 2025 00:34:07 +0100 Subject: [PATCH 14/15] fix test --- tests/test_app_bokeh.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_app_bokeh.py b/tests/test_app_bokeh.py index eccb62af9..5c6775bf8 100644 --- a/tests/test_app_bokeh.py +++ b/tests/test_app_bokeh.py @@ -285,7 +285,7 @@ def test_add_annotation_layer(doc: Document, data_path: pytest.TempPathFactory) # trigger an event to select the geojson file click = MenuItemClick(layer_drop, str(data_path["geojson_anns"])) layer_drop._trigger_event(click) - assert main.UI["vstate"].types == ["nucleus", "cell", "annotation"] + assert set(main.UI["vstate"].types) == {"nucleus", "cell", "annotation"} # test the name2type function. assert main.name2type("annotation") == '"annotation"' From caab28ddf7ae89389d022c9d771061b4bbf387b0 Mon Sep 17 00:00:00 2001 From: measty <20169086+measty@users.noreply.github.com> Date: Fri, 12 Sep 2025 02:26:55 +0100 Subject: [PATCH 15/15] add even more tests --- tests/test_tileserver.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/tests/test_tileserver.py b/tests/test_tileserver.py index a4f75e31a..add9f4981 100644 --- a/tests/test_tileserver.py +++ b/tests/test_tileserver.py @@ -100,11 +100,7 @@ def app(remote_sample: Callable, tmp_path: Path) -> TileServer: imwrite(thumb_path, thumb) sample_store = Path(remote_sample("annotation_store_svs_1")) - store = SQLiteStore(sample_store) - geo_path = tmp_path / "test.geojson" - store.to_geojson(geo_path) - store.commit() - store.close() + geo_path = Path(remote_sample("geojson_cmu_1")) # make tileserver with layers representing all the types # of things it should be able to handle @@ -115,7 +111,7 @@ def app(remote_sample: Callable, tmp_path: Path) -> TileServer: "tile": str(thumb_path), "im_array": np.zeros(wsi.slide_dimensions(1.25, "power"), dtype=np.uint8).T, "overlay": str(sample_store), - "store_geojson": tmp_path / "test.geojson", + "store_geojson": str(geo_path), }, ) app.config.from_mapping({"TESTING": True}) @@ -446,7 +442,7 @@ def test_change_overlay( # noqa: PLR0915 assert response.status_code == 200 response = client.put( "/tileserver/overlay", - data={"overlay_path": safe_str(geo_path)}, + data={"overlay_path": safe_str(sample_store)}, ) assert response.status_code == 200 assert response.content_type == "text/html; charset=utf-8" @@ -454,6 +450,14 @@ def test_change_overlay( # noqa: PLR0915 # check that the annotations have been correctly loaded assert len(empty_app.pyramids[session_id]["overlay"].store) == num_annotations + # check loading a geojson from qupath with cells/nuclei + qpath_geo_path = Path(remote_sample("geojson_cmu_1")) + response = client.put( + "/tileserver/overlay", + data={"overlay_path": safe_str(qpath_geo_path)}, + ) + assert response.status_code == 200 + # add another image layer response = client.put( "/tileserver/overlay", @@ -466,6 +470,14 @@ def test_change_overlay( # noqa: PLR0915 layer = empty_app.pyramids[session_id][lname] assert layer.wsi.info.file_path == overlay_path + # add same image again to check if duplicate names triggers disambiguation + response = client.put( + "/tileserver/overlay", + data={"overlay_path": safe_str(overlay_path)}, + ) + assert response.status_code == 200 + assert Path(overlay_path).name in empty_app.pyramids[session_id] + # replace existing store overlay response = client.put( "/tileserver/overlay",