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 45 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
45 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
12b4d62
Merge branch 'develop' into add-slide-data
shaneahmed Feb 7, 2025
ab9451e
Merge branch 'develop' into add-slide-data
shaneahmed Feb 21, 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
15 changes: 12 additions & 3 deletions tests/test_app_bokeh.py
Original file line number Diff line number Diff line change
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 @@ -181,8 +185,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 +214,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 @@ -618,7 +627,7 @@ def test_pt_size_spinner(doc: Document) -> None:
pt_size_spinner.value = 10
# check that the point size has been set correctly
assert (
main.UI["p"].renderers[main.UI["vstate"].layer_dict["nodes"]].glyph.size
main.UI["p"].renderers[main.UI["vstate"].layer_dict["nodes"]].glyph.radius
== 2 * 10
)

Expand Down
14 changes: 14 additions & 0 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 @@ -82,6 +92,10 @@ def test_slides_available(bk_session: ClientSession) -> None:
slide_select.value = ["CMU-1.ndpi"]
assert len(layer_drop.menu) == 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
bk_session.close()
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
2 changes: 2 additions & 0 deletions tiatoolbox/data/remote_samples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,7 @@ files:
url: [ *testdata, "annotation/test1_config.json"]
config_2:
url: [ *testdata, "annotation/test2_config.json"]
test_meta:
url: [ *testdata, "annotation/test_meta.csv"]
nuclick-output:
url: [*modelroot, "predictions/nuclei_mask/nuclick-output.npy"]
103 changes: 86 additions & 17 deletions tiatoolbox/visualization/bokeh_app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from __future__ import annotations

import importlib
import json
import sys
import tempfile
Expand All @@ -12,6 +13,7 @@
from typing import TYPE_CHECKING, Any, Callable, SupportsFloat

import numpy as np
import pandas as pd
import requests
import torch
from bokeh.events import ButtonClick, DoubleTap, MenuItemClick
Expand Down Expand Up @@ -70,7 +72,7 @@
)
from tiatoolbox.tools.pyramid import ZoomifyGenerator
from tiatoolbox.utils.visualization import random_colors
from tiatoolbox.visualization.ui_utils import get_level_by_extent
from tiatoolbox.visualization.ui_utils import UIWrapper, get_level_by_extent
from tiatoolbox.wsicore.wsireader import WSIReader

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -117,23 +119,27 @@
self.item = val


class UIWrapper:
"""Wrapper class to access ui elements."""

def __init__(self: UIWrapper) -> None:
"""Initialize the class."""
self.active = 0

def __getitem__(self: UIWrapper, key: str) -> Any: # noqa: ANN401
"""Gets ui element for the active window."""
return win_dicts[self.active][key]
def to_camel(name: str) -> str:
"""Convert a string to upper camel case."""
parts = name.split("_")

Check warning on line 124 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L124

Added line #L124 was not covered by tests
return "".join([p.capitalize() for p in parts])


def format_info(info: dict[str, Any]) -> str:
"""Format the slide info for display."""
info_str = f"<b>Slide Name: {info.pop('file_path').name}</b><br>"
slide_name = info.pop("file_path").name
info_str = f"<b>Slide Name: {slide_name}</b><br>"
for k, v in info.items():
info_str += f"{k}: {v}<br>"
# if there is metadata, add it
if doc_config.metadata is not None:
try:
row_data = doc_config.metadata.loc[slide_name]
info_str += "<br><b>Metadata:</b><br>"
for k, v in row_data.items():
info_str += f"{k}: {v}<br>"
except KeyError:
info_str += "<br><b>No metadata found.</b><br>"
return info_str


Expand Down Expand Up @@ -778,7 +784,7 @@

def pt_size_cb(attr: str, old: float, new: float) -> None: # noqa: ARG001
"""Callback to change the size of the points."""
UI["vstate"].graph_node.size = 2 * new
UI["vstate"].graph_node.radius = 2 * new


def edge_size_cb(attr: str, old: float, new: float) -> None: # noqa: ARG001
Expand Down Expand Up @@ -848,6 +854,13 @@
if len(new) == 0:
return
slide_path = Path(doc_config["slide_folder"]) / Path(new[0])
if doc_config["extra_layout"] is not None:
# create the extra layout if we can
extras = []

Check warning on line 859 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L859

Added line #L859 was not covered by tests
for cl in doc_config["extra_layout"]:
extras.extend(cl.create_extra_layout(slide_path, extra_layout.children))
cl.add_to_ui()
extra_layout.children = extras

Check warning on line 863 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L861-L863

Added lines #L861 - L863 were not covered by tests
# Reset the data sources for glyph overlays
UI["pt_source"].data = {"x": [], "y": []}
UI["box_source"].data = {"x": [], "y": [], "width": [], "height": []}
Expand Down Expand Up @@ -1272,6 +1285,11 @@
height=200,
sizing_mode="stretch_width",
)
extra_layout = column(
children=[],
name="extra_layout",
sizing_mode="stretch_both",
)


def gather_ui_elements( # noqa: PLR0915
Expand Down Expand Up @@ -1771,7 +1789,14 @@
box_source = ColumnDataSource({"x": [], "y": [], "width": [], "height": []})
pt_source = ColumnDataSource({"x": [], "y": []})
r = p.rect("x", "y", "width", "height", source=box_source, fill_alpha=0)
c = p.circle("x", "y", source=pt_source, color="red", size=5)
c = p.circle(
"x",
"y",
source=pt_source,
color="red",
radius=3,
radius_units="screen",
)
p.add_tools(BoxEditTool(renderers=[r], num_objects=1))
p.add_tools(PointDrawTool(renderers=[c]))
p.add_tools(TapTool())
Expand All @@ -1783,7 +1808,13 @@
# Add graph stuff
node_source = ColumnDataSource({"x_": [], "y_": [], "node_color_": []})
edge_source = ColumnDataSource({"x0_": [], "y0_": [], "x1_": [], "y1_": []})
vstate.graph_node = Circle(x="x_", y="y_", fill_color="node_color_", size=5)
vstate.graph_node = Circle(
x="x_",
y="y_",
fill_color="node_color_",
radius=3,
radius_units="screen",
)
vstate.graph_edge = Segment(x0="x0_", y0="y0_", x1="x1_", y1="y1_")
p.add_glyph(node_source, vstate.graph_node)
node_source.selected.on_change("indices", node_select_cb)
Expand Down Expand Up @@ -1853,14 +1884,16 @@
"hover": hover,
"user": user,
"color_bar": color_bar,
"ui_layout": ui_layout,
"extra_options": extra_options,
}


# Main ui containers
UI = UIWrapper()
windows = []
controls = []
win_dicts = []
UI = UIWrapper(win_dicts)

# Popup for annotation viewing on double click
popup_div = Div(
Expand Down Expand Up @@ -2004,6 +2037,7 @@
"overlay_folder": Path("/app_data").joinpath("overlays"),
}
self.sys_args = None
self.metadata = None

def __getitem__(self: DocConfig, key: str) -> Any: # noqa: ANN401
"""Get an item from the config."""
Expand Down Expand Up @@ -2033,6 +2067,23 @@
base_folder = slide_folder.parent
overlay_folder = Path(sys_args[2])

plugins = None
if len(sys_args) > 3: # noqa: PLR2004
# also passed a path to a plugin file defining extra ui elements
plugins = []

Check warning on line 2073 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L2073

Added line #L2073 was not covered by tests
for p in sys_args[3:]:
plugin_path = Path(p)

Check warning on line 2075 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L2075

Added line #L2075 was not covered by tests

# should have class named (in camel case) after the filename, that
# handles the extra ui elements
spec = importlib.util.spec_from_file_location(

Check warning on line 2079 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L2079

Added line #L2079 was not covered by tests
"plugin_mod",
plugin_path,
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module) # Execute the module
plugins.append(module.__getattribute__(to_camel(plugin_path.stem))(UI))

Check warning on line 2085 in tiatoolbox/visualization/bokeh_app/main.py

View check run for this annotation

Codecov / codecov/patch

tiatoolbox/visualization/bokeh_app/main.py#L2083-L2085

Added lines #L2083 - L2085 were not covered by tests

# Load a color_dict and/or slide initial view windows from a json file
config_file = list(overlay_folder.glob("*config.json"))
config = self.config
Expand All @@ -2045,6 +2096,7 @@
config["base_folder"] = base_folder
config["slide_folder"] = slide_folder
config["overlay_folder"] = overlay_folder
config["extra_layout"] = plugins
config["demo_name"] = self.config["demo_name"]
if "initial_views" not in config:
config["initial_views"] = {}
Expand All @@ -2070,10 +2122,26 @@

"""
self._get_config()
# see if there's a metadata .csv file in slides folder
metadata_file = list(doc_config["slide_folder"].glob("*.csv"))
if len(metadata_file) > 0:
metadata_file = metadata_file[0]
with metadata_file.open() as f:
metadata = pd.read_csv(f, encoding="utf-8")
# must have an 'Image File' column to associate with slides
if "Image File" in metadata.columns:
metadata = metadata.set_index("Image File")
else:
# can't use it so set to None
metadata = None
else:
# no metadata file
metadata = None
self.metadata = metadata

# Set initial slide to first one in base folder
slide_list = []
for ext in ["*.svs", "*ndpi", "*.tiff", "*.mrxs", "*.png", "*.jpg"]:
for ext in ["*.svs", "*ndpi", "*.tiff", "*.mrxs", "*.png", "*.jpg", "*.tif"]:
slide_list.extend(list(doc_config["slide_folder"].glob(ext)))
slide_list.extend(
list(doc_config["slide_folder"].glob(str(Path("*") / ext))),
Expand Down Expand Up @@ -2102,6 +2170,7 @@
base_doc.add_root(control_tabs)
base_doc.add_root(popup_table)
base_doc.add_root(slide_info)
base_doc.add_root(extra_layout)
base_doc.title = "Tiatoolbox Visualization Tool"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
base_doc.title = "Tiatoolbox Visualization Tool"
base_doc.title = "TIAToolbox Visualization Tool"

return slide_wins, control_tabs

Expand Down
1 change: 1 addition & 0 deletions tiatoolbox/visualization/bokeh_app/templates/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""plugins and templates for bokeh visualization app."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""plugins and templates for bokeh visualization app."""
"""Plugins and templates for bokeh visualization app."""

Loading
Loading