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

Cross slice annotation saving #32

Merged
merged 2 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 37 additions & 12 deletions callbacks/control_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
ALL,
ctx,
clientside_callback,
no_update,
)
from dash.exceptions import PreventUpdate
import dash_mantine_components as dmc
import json
from utils.data_utils import convert_hex_to_rgba, data
Expand All @@ -21,38 +21,57 @@
Output("circle", "style"),
Output("rectangle", "style"),
Output("drawing-off", "style"),
Output("annotation-store", "data", allow_duplicate=True),
Input("open-freeform", "n_clicks"),
Input("closed-freeform", "n_clicks"),
Input("circle", "n_clicks"),
Input("rectangle", "n_clicks"),
Input("drawing-off", "n_clicks"),
State("annotation-store", "data"),
prevent_initial_call=True,
)
def annotation_mode(open, closed, circle, rect, off_mode):
def annotation_mode(open, closed, circle, rect, off_mode, annotation_store):
"""This callback determines which drawing mode the graph is in"""
if not annotation_store["visible"]:
raise PreventUpdate

patched_figure = Patch()
triggered = ctx.triggered_id
open_style = {"border": "1px solid"}
close_style = {"border": "1px solid"}
circle_style = {"border": "1px solid"}
rect_style = {"border": "1px solid"}
pan_style = {"border": "1px solid"}

if triggered == "open-freeform" and open > 0:
patched_figure["layout"]["dragmode"] = "drawopenpath"
annotation_store["dragmode"] = "drawopenpath"
open_style = {"border": "3px solid black"}
if triggered == "closed-freeform" and closed > 0:
patched_figure["layout"]["dragmode"] = "drawclosedpath"
annotation_store["dragmode"] = "drawclosedpath"
close_style = {"border": "3px solid black"}
if triggered == "circle" and circle > 0:
patched_figure["layout"]["dragmode"] = "drawcircle"
annotation_store["dragmode"] = "drawcircle"
circle_style = {"border": "3px solid black"}
if triggered == "rectangle" and rect > 0:
patched_figure["layout"]["dragmode"] = "drawrect"
annotation_store["dragmode"] = "drawrect"
rect_style = {"border": "3px solid black"}
if triggered == "drawing-off" and off_mode > 0:
patched_figure["layout"]["dragmode"] = "pan"
annotation_store["dragmode"] = "pan"
pan_style = {"border": "3px solid black"}
return patched_figure, open_style, close_style, circle_style, rect_style, pan_style
return (
patched_figure,
open_style,
close_style,
circle_style,
rect_style,
pan_style,
annotation_store,
)


@callback(
Expand Down Expand Up @@ -90,33 +109,39 @@ def annotation_color(color_value):


@callback(
Output("annotation-store", "data"),
Output("annotation-store", "data", allow_duplicate=True),
Output("image-viewer", "figure", allow_duplicate=True),
Input("view-annotations", "checked"),
State("annotation-store", "data"),
State("image-viewer", "figure"),
State("image-selection-slider", "value"),
prevent_initial_call=True,
)
def annotation_visibility(checked, store, figure, image_idx):
def annotation_visibility(checked, annotation_store, figure, image_idx):
"""
This callback is responsible for toggling the visibility of the annotation layer.
It also saves the annotation data to the store when the layer is hidden, and then loads it back in when the layer is shown again.
"""
image_idx = str(image_idx)

image_idx = str(image_idx - 1)
patched_figure = Patch()
if checked:
store["visible"] = True
patched_figure["layout"]["shapes"] = store[image_idx]
annotation_store["visible"] = True
patched_figure["layout"]["visible"] = True
if str(image_idx) in annotation_store["annotations"]:
patched_figure["layout"]["shapes"] = annotation_store["annotations"][
image_idx
]
patched_figure["layout"]["dragmode"] = annotation_store["dragmode"]
else:
annotation_data = (
new_annotation_data = (
[] if "shapes" not in figure["layout"] else figure["layout"]["shapes"]
)
store[image_idx] = annotation_data
annotation_store["visible"] = False
patched_figure["layout"]["dragmode"] = False
annotation_store["annotations"][image_idx] = new_annotation_data
patched_figure["layout"]["shapes"] = []

return store, patched_figure
return annotation_store, patched_figure


clientside_callback(
Expand Down
43 changes: 40 additions & 3 deletions callbacks/image_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from tifffile import imread
import plotly.express as px
import numpy as np
from utils import data_utils
from utils.data_utils import convert_hex_to_rgba, data


Expand All @@ -13,12 +12,14 @@
State("project-name-src", "value"),
State("paintbrush-width", "value"),
State("annotation-class-selection", "className"),
State("annotation-store", "data"),
)
def render_image(
image_idx,
project_name,
annotation_width,
annotation_color,
annotation_store,
):
if image_idx:
image_idx -= 1 # slider starts at 1, so subtract 1 to get the correct index
Expand All @@ -42,24 +43,58 @@ def render_image(
hex_color = dmc.theme.DEFAULT_COLORS[annotation_color][7]
fig.update_layout(
newshape=dict(
line=dict(color=annotation_color, width=annotation_width),
line=dict(
color=convert_hex_to_rgba(hex_color, 0.3), width=annotation_width
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

adressess #19

),
fillcolor=convert_hex_to_rgba(hex_color, 0.3),
)
)
if annotation_store:
if not annotation_store["visible"]:
fig["layout"]["shapes"] = []
fig["layout"]["dragmode"] = False
return fig

fig["layout"]["dragmode"] = annotation_store["dragmode"]

if str(image_idx) in annotation_store["annotations"]:
fig["layout"]["shapes"] = annotation_store["annotations"][str(image_idx)]

return fig


@callback(
Output("annotation-store", "data", allow_duplicate=True),
Input("image-viewer", "relayoutData"),
State("image-selection-slider", "value"),
State("annotation-store", "data"),
prevent_initial_call=True,
)
def locally_store_annotations(relayout_data, img_idx, annotation_store):
"""
Upon finishing relayout event (drawing, but it also includes panning, zooming),
this function takes the annotation shapes, and stores it in the dcc.Store, which is then used elsewhere
to preserve drawn annotations, or to save them.
"""
if "shapes" in relayout_data:
annotation_store["annotations"][str(img_idx - 1)] = relayout_data["shapes"]
return annotation_store


@callback(
Output("image-selection-slider", "min"),
Output("image-selection-slider", "max"),
Output("image-selection-slider", "value"),
Output("image-selection-slider", "disabled"),
Output("annotation-store", "data"),
Input("project-name-src", "value"),
State("annotation-store", "data"),
)
def update_slider_values(project_name):
def update_slider_values(project_name, annotation_store):
"""
When the data source is loaded, this callback will set the slider values and chain call
"update_selection_and_image" callback which will update image and slider selection component
It also resets "annotation-store" data to {} so that existing annotations don't carry over to the new project.

## todo - change Input("project-name-src", "data") to value when image-src will contain buckets of data and not just one image
## todo - eg, when a different image source is selected, update slider values which is then used to select image within that source
Expand All @@ -71,11 +106,13 @@ def update_slider_values(project_name):
min_slider_value = 0 if disable_slider else 1
max_slider_value = 0 if disable_slider else len(tiff_file)
slider_value = 0 if disable_slider else 1
annotation_store["annotations"] = {}
return (
min_slider_value,
max_slider_value,
slider_value,
disable_slider,
annotation_store,
)


Expand Down
5 changes: 4 additions & 1 deletion components/control_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,10 @@ def layout():
),
],
),
dcc.Store(id="annotation-store", data={}),
dcc.Store(
id="annotation-store",
data={"dragmode": "drawopenpath", "visible": True, "annotations": {}},
),
dcc.Store(id="project-data"),
html.Div(id="dummy-output"),
],
Expand Down
Loading