Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🆕 Add Reading Slide-level info & Simple Plugins for Visualization Tool #789

Draft
wants to merge 43 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
9a7de48
add reading slide-level info from provided csv
measty Feb 21, 2024
b811b62
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 21, 2024
a692c59
add documentation entry
measty Feb 21, 2024
79ab016
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Feb 21, 2024
bcd5131
fix typo
measty Feb 21, 2024
d0995cc
improve test coverage
measty Feb 21, 2024
8b24409
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 21, 2024
ad57393
Merge branch 'develop' into add-slide-data
shaneahmed Mar 1, 2024
2fd714d
Merge branch 'develop' into add-slide-data
shaneahmed Mar 12, 2024
cda3110
Merge branch 'develop' into add-slide-data
shaneahmed Mar 13, 2024
ab711ff
add simple plugins
measty Mar 13, 2024
e45fb3c
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Mar 13, 2024
8755f27
fix test
measty Mar 13, 2024
b7715f6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 14, 2024
4ca17e1
change circle size->radius for bokeh 3.4 compatibility
measty Mar 14, 2024
3cec0aa
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Mar 14, 2024
1e8df62
remove refs to size
measty Mar 15, 2024
96ccd97
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Mar 15, 2024
cd9e3bd
update img size
measty Mar 15, 2024
f52ee5e
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Mar 15, 2024
d2a6ec4
try dialog
measty Mar 15, 2024
1bec5b6
Merge branch 'develop' into add-slide-data
shaneahmed Mar 19, 2024
7b40965
replace dropdown with select
measty Mar 20, 2024
08fcb59
Merge branch 'develop' into add-slide-data
shaneahmed Mar 22, 2024
646848e
once and per slide options
measty Apr 4, 2024
4a2e48f
update tests for switch to layer select
measty Apr 8, 2024
27a9d1d
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty Apr 12, 2024
880e4a6
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Apr 12, 2024
c8ffd30
add missing docstrings
measty Apr 12, 2024
c5165be
Merge branch 'develop' into add-slide-data
shaneahmed Apr 19, 2024
721ad20
Merge branch 'develop' into add-slide-data
shaneahmed May 10, 2024
f849dee
Merge branch 'develop' into add-slide-data
shaneahmed May 17, 2024
e15f6bb
add tests
measty May 28, 2024
23796bc
Merge branch 'develop' into add-slide-data
shaneahmed Jun 14, 2024
90d69b3
Merge branch 'develop' into add-slide-data
shaneahmed Jun 21, 2024
7768414
Merge branch 'develop' into add-slide-data
shaneahmed Jun 28, 2024
12147da
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Oct 3, 2024
c9e7ea1
Merge branch 'develop' of https://github.com/TissueImageAnalytics/tia…
measty Oct 3, 2024
0bfb5f0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 3, 2024
c39fe41
fix test
measty Oct 4, 2024
43cac84
Merge branch 'add-slide-data' of https://github.com/TissueImageAnalyt…
measty Oct 4, 2024
33a1068
Merge branch 'develop' into add-slide-data
shaneahmed Nov 22, 2024
59b4a2c
Merge branch 'develop' into add-slide-data
shaneahmed Jan 24, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,10 @@ Additional features can be added to nodes by adding extra keys to the dictionary

It will be possible to color the nodes by these features in the interface, and the top 10 will appear in a tooltip when hovering over a node (you will have to turn on the hovertool in the small toolbar to the right of the main window to enable this, it is disabled by default.)

Slide Level Information
^^^^^^^^^^^^^^^^^^^^^^^

If you have slide-level predictions, ground truth labels, or other metadata you wish to be able to see associated with slides in the interface, this can be provided as a .csv formatted table placed in the slides folder, with "Image File" as the first column. The other columns can be anything you like. When loading a slide in the UI, if the slide name appears in the "Image File" column of the provided .csv, any other entries in that row will be displayed in the interface below the main view window when the slide is selected.

.. _examples:

Expand Down Expand Up @@ -422,7 +426,7 @@ and the ability to toggle on or off specific UI elements:

::

"UI_elements_1": { # controls which UI elements are visible
"ui_elements_1": { # controls which UI elements are visible
"slide_select": 1, # slide select box
"layer_drop": 1, # overlay select drop down
"slide_row": 1, # slide alpha toggle and slider
Expand All @@ -437,7 +441,7 @@ and the ability to toggle on or off specific UI elements:

::

"UI_elements_2": { # controls visible UI elements on second tab in UI
"ui_elements_2": { # controls visible UI elements on second tab in UI
"opt_buttons": 1, # UI elements providing a few options including if annotations should be filled/outline only
"pt_size_spinner": 1, # control for point size and graph node size
"edge_size_spinner": 1, # control for edge thickness
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,6 +608,7 @@ def data_path(tmp_path_factory: pytest.TempPathFactory) -> dict[str, object]:
"""Set up a temporary data directory for testing visualization UI."""
tmp_path = tmp_path_factory.mktemp("data")
(tmp_path / "slides").mkdir()
(tmp_path / "slides" / "CMU-1_files").mkdir()
(tmp_path / "overlays").mkdir()
return {"base_path": tmp_path}

Expand Down
52 changes: 30 additions & 22 deletions tests/test_app_bokeh.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import requests
from bokeh.application import Application
from bokeh.application.handlers import FunctionHandler
from bokeh.events import ButtonClick, DoubleTap, MenuItemClick
from bokeh.events import ButtonClick, DoubleTap
from flask_cors import CORS
from matplotlib import colormaps
from PIL import Image
Expand Down Expand Up @@ -101,6 +101,10 @@ def annotation_path(data_path: dict[str, Path]) -> dict[str, object]:
"patch-extraction-vf",
data_path["base_path"] / "slides",
)
data_path["meta"] = _fetch_remote_sample(
"test_meta",
data_path["base_path"] / "slides",
)
data_path["annotations"] = _fetch_remote_sample(
"annotation_store_svs_1",
data_path["base_path"] / "overlays",
Expand Down Expand Up @@ -129,6 +133,14 @@ def annotation_path(data_path: dict[str, Path]) -> dict[str, object]:
"config_2",
data_path["base_path"] / "overlays",
)
data_path["plugin_img"] = _fetch_remote_sample(
"stainnorm-source",
data_path["base_path"] / "slides" / "CMU-1_files",
)
data_path["plugin_csv"] = _fetch_remote_sample(
"test_csv",
data_path["base_path"] / "slides" / "CMU-1_files",
)
return data_path


Expand Down Expand Up @@ -181,8 +193,8 @@ def test_get_level_by_extent() -> None:

def test_roots(doc: Document) -> None:
"""Test that the document has the correct number of roots."""
# should be 4 roots: main window, controls, slide_info, popup table
assert len(doc.roots) == 4
# should be 5 roots: main window, controls, slide_info, popup, extra_layout
assert len(doc.roots) == 5


def test_config_loaded(data_path: pytest.TempPathFactory) -> None:
Expand Down Expand Up @@ -210,6 +222,11 @@ def test_slide_select(doc: Document, data_path: pytest.TempPathFactory) -> None:
slide_select.value = ["CMU-1.ndpi"]
assert main.UI["vstate"].slide_path == data_path["slide2"]

# check the slide metadata is loaded from csv
desc = doc.get_model_by_name("description")
assert "valA" in desc.text
assert "valB" not in desc.text

# check selecting nothing has no effect
slide_select.value = []
assert main.UI["vstate"].slide_path == data_path["slide2"]
Expand Down Expand Up @@ -253,8 +270,7 @@ def test_add_annotation_layer(doc: Document, data_path: pytest.TempPathFactory)
slide_select.value = [data_path["slide2"].name]
layer_drop = doc.get_model_by_name("layer_drop0")
# trigger an event to select the geojson file
click = MenuItemClick(layer_drop, str(data_path["geojson_anns"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["geojson_anns"])
assert main.UI["vstate"].types == ["annotation"]

# test the name2type function.
Expand All @@ -263,11 +279,10 @@ def test_add_annotation_layer(doc: Document, data_path: pytest.TempPathFactory)
# test loading an annotation store
slide_select.value = [data_path["slide1"].name]
layer_drop = doc.get_model_by_name("layer_drop0")
assert len(layer_drop.menu) == 5
assert len(layer_drop.options) == 5
n_renderers = len(doc.get_model_by_name("slide_windows").children[0].renderers)
# trigger an event to select the annotation .db file
click = MenuItemClick(layer_drop, str(data_path["annotations"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["annotations"])
# should be one more renderer now
assert len(doc.get_model_by_name("slide_windows").children[0].renderers) == (
n_renderers + 1
Expand Down Expand Up @@ -360,8 +375,7 @@ def test_load_graph(doc: Document, data_path: pytest.TempPathFactory) -> None:
"""Test loading a graph."""
layer_drop = doc.get_model_by_name("layer_drop0")
# trigger an event to select the graph file
click = MenuItemClick(layer_drop, str(data_path["graph"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["graph"])
# we should have 2144 nodes in the node_source now
assert len(main.UI["node_source"].data["x_"]) == 2144

Expand All @@ -370,8 +384,7 @@ def test_graph_with_feats(doc: Document, data_path: pytest.TempPathFactory) -> N
"""Test loading a graph with features."""
layer_drop = doc.get_model_by_name("layer_drop0")
# trigger an event to select the graph .json file
click = MenuItemClick(layer_drop, str(data_path["graph_feats"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["graph_feats"])
# we should have keys for the features in node data source now
for i in range(10):
assert f"feat_{i}" in main.UI["node_source"].data
Expand All @@ -393,17 +406,15 @@ def test_graph_with_feats(doc: Document, data_path: pytest.TempPathFactory) -> N
)

# test graph overlay option remains on loading new overlay
click = MenuItemClick(layer_drop, str(data_path["annotations"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["annotations"])
assert "graph_overlay" in cmap_select.options


def test_load_img_overlay(doc: Document, data_path: pytest.TempPathFactory) -> None:
"""Test loading an image overlay."""
layer_drop = doc.get_model_by_name("layer_drop0")
# trigger an event to select the image overlay
click = MenuItemClick(layer_drop, str(data_path["img_overlay"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["img_overlay"])
layer_slider = doc.get_model_by_name("layer2_slider")
assert layer_slider is not None

Expand Down Expand Up @@ -465,8 +476,7 @@ def test_hovernet_on_box(doc: Document, data_path: pytest.TempPathFactory) -> No
cprop_select = doc.get_model_by_name("cprop0")
cprop_select.value = ["prob"]
layer_drop = doc.get_model_by_name("layer_drop0")
click = MenuItemClick(layer_drop, str(data_path["dat_anns"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["dat_anns"])
assert main.UI["vstate"].types == ["annotation"]
# check the per-type ui controls have been updated
assert len(main.UI["color_column"].children) == 1
Expand Down Expand Up @@ -507,8 +517,7 @@ def test_type_select(doc: Document, data_path: pytest.TempPathFactory) -> None:
"""Test selecting/deselecting specific types."""
# load annotation layer
layer_drop = doc.get_model_by_name("layer_drop0")
click = MenuItemClick(layer_drop, str(data_path["annotations"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["annotations"])
time.sleep(1)
im = get_tile("overlay", 4, 8, 4, show=False)
_, num_before = label(np.any(im[:, :, :3], axis=2))
Expand Down Expand Up @@ -560,8 +569,7 @@ def test_node_and_edge_alpha(doc: Document, data_path: pytest.TempPathFactory) -
"""Test sliders for adjusting graph node and edge alpha."""
layer_drop = doc.get_model_by_name("layer_drop0")
# trigger an event to select the graph .db file
click = MenuItemClick(layer_drop, str(data_path["graph"]))
layer_drop._trigger_event(click)
layer_drop.value = str(data_path["graph"])

type_column_list = doc.get_model_by_name("type_column0").children
color_column_list = doc.get_model_by_name("color_column0").children
Expand Down
18 changes: 16 additions & 2 deletions tests/test_json_config_bokeh.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from threading import Thread
from typing import TYPE_CHECKING

import pandas as pd
import pytest
import requests
from bokeh.client.session import ClientSession, pull_session
Expand All @@ -29,6 +30,15 @@ def annotation_path(data_path: dict[str, Path]) -> dict[str, Path]:
"ndpi-1",
data_path["base_path"] / "slides",
)
data_path["meta"] = _fetch_remote_sample(
"test_meta",
data_path["base_path"] / "slides",
)
meta_df = pd.read_csv(data_path["meta"])
# change 'Image File' column name to 'Wrong Name'
meta_df = meta_df.rename(columns={"Image File": "Wrong Name"})
# save so we can test behaviour if required column isn't there
meta_df.to_csv(data_path["meta"], index=False)
data_path["annotations"] = _fetch_remote_sample(
"annotation_store_svs_1",
data_path["base_path"] / "overlays",
Expand Down Expand Up @@ -77,10 +87,14 @@ def test_slides_available(bk_session: ClientSession) -> None:
assert slide_select.value[0] == "CMU-1-Small-Region.svs"

layer_drop = doc.get_model_by_name("layer_drop0")
assert len(layer_drop.menu) == 2
assert len(layer_drop.options) == 2
# check that the overlays are available.
slide_select.value = ["CMU-1.ndpi"]
assert len(layer_drop.menu) == 2
assert len(layer_drop.options) == 2

# check the metadata wasnt found as the column name was wrong
desc = doc.get_model_by_name("description")
assert "Metadata:" not in desc.text

bk_session.document.clear()
assert len(bk_session.document.roots) == 0
Expand Down
2 changes: 1 addition & 1 deletion tests/test_server_bokeh.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def test_slides_available(bk_session: ClientSession) -> None:
# check that the overlays are available.
slide_select.value = ["CMU-1-Small-region.svs"]
layer_drop = doc.get_model_by_name("layer_drop0")
assert len(layer_drop.menu) == 2
assert len(layer_drop.options) == 2

bk_session.document.clear()
assert len(bk_session.document.roots) == 0
Expand Down
12 changes: 11 additions & 1 deletion tiatoolbox/cli/visualize.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ def run_bokeh(img_input: list[str], port: int, *, noshow: bool) -> None:
This option must be used in conjunction with --slides.
The --base-path option should not be used in this case.""",
)
@click.option(
"--plugin",
multiple=True,
help=r"""Path to a file to define an extra layout containing extra
resources such as graphs below each slide. Some pre-built plugins are
available in the tiatoolbox\visualization\templates folder. Can pass
multiple instances of this option to add multiple ui additions.""",
)
@click.option(
"--port",
type=int,
Expand All @@ -88,6 +96,7 @@ def visualize(
base_path: str,
slides: str,
overlays: str,
plugin: list[str],
port: int,
*,
noshow: bool,
Expand All @@ -101,6 +110,7 @@ def visualize(
base_path (str): Path to base directory containing images to be displayed.
slides (str): Path to directory containing slides to be displayed.
overlays (str): Path to directory containing overlays to be displayed.
plugin (list): Paths to files containing ui plugins.
port (int): Port to launch the visualization tool on.
noshow (bool): Do not launch in browser (mainly intended for testing).

Expand All @@ -109,7 +119,7 @@ def visualize(
if base_path is None and (slides is None or overlays is None):
msg = "Must specify either base-path or both slides and overlays."
raise ValueError(msg)
img_input = [base_path, slides, overlays]
img_input = [base_path, slides, overlays, *list(plugin)]
img_input = [p for p in img_input if p is not None]
# check that the input paths exist
for input_path in img_input:
Expand Down
4 changes: 4 additions & 0 deletions tiatoolbox/data/remote_samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ files:
url: [ *testdata, "annotation/test1_config.json"]
config_2:
url: [ *testdata, "annotation/test2_config.json"]
test_meta:
url: [ *testdata, "annotation/test_meta.csv"]
test_csv:
url: [ *testdata, "annotation/metrics_10.csv"]
patch_annotations:
url: [ *testdata, "annotation/sample_wsi_patch_preds.db"]
nuclick-output:
Expand Down
Loading
Loading