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

Annotation exporting hannah #56

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,15 @@ API_KEY='<key-provided-on-request>'
python app.py
```

### Local tiled connection

To start local tiled connection:
1. Add `SERVE_LOCALLY=True` flag to `env` file (or to your environmental variables)
2. Start the app once, which will create `data/` directory and download 2 sample projects with 2 images each.
3. Open a second terminal and run `tiled serve directory --public data`.

The app will now connect to the local tiled server.

# Copyright
MLExchange Copyright (c) 2023, The Regents of the University of California, through Lawrence Berkeley National Laboratory (subject to receipt of any required approvals from the U.S. Dept. of Energy). All rights reserved.

Expand Down
5 changes: 3 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from dash import Dash
from dash import Dash, dcc
import dash_mantine_components as dmc
from components.control_bar import layout as control_bar_layout
from components.image_viewer import layout as image_viewer_layout
Expand All @@ -16,7 +16,8 @@
control_bar_layout(),
image_viewer_layout(),
],
)
),
dcc.Store(id="current-ann-mode"),
],
)

Expand Down
13 changes: 6 additions & 7 deletions assets/style.css
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
/* These css classes highlight the selected annotation color (class) */
.flex-row {
display: flex;
flex-direction: row;
}

.red>.red-icon,
.grape>.grape-icon,
.violet>.violet-icon,
.blue>.blue-icon,
.yellow>.yellow-icon {
border: 3px solid black;
.mantine-Grid-col>.mantine-1avyp1d {
margin: auto;
}
246 changes: 210 additions & 36 deletions callbacks/control_bar.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import dash_mantine_components as dmc
from dash_iconify import DashIconify
import json
from utils.data_utils import convert_hex_to_rgba, data
from utils.annotations import Annotations


Expand All @@ -26,6 +25,7 @@
Output("rectangle", "style"),
Output("drawing-off", "style"),
Output("annotation-store", "data", allow_duplicate=True),
Output("current-ann-mode", "data", allow_duplicate=True),
Input("open-freeform", "n_clicks"),
Input("closed-freeform", "n_clicks"),
Input("circle", "n_clicks"),
Expand All @@ -41,32 +41,34 @@ def annotation_mode(open, closed, circle, rect, off_mode, annotation_store):

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"}
active = {"border": "3px solid black"}
inactive = {"border": "1px solid"}
open_style = inactive
close_style = inactive
circle_style = inactive
rect_style = inactive
pan_style = inactive

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


Expand All @@ -92,58 +95,229 @@ def annotation_width(width_value):
return patched_figure


@callback(
Output("current-annotation-classes", "children"),
Input("annotation-class-selection", "children"),
prevent_initial_call=True,
)
def make_class_delete_modal(current_classes):
"""Creates buttons for the delete selected classes modal"""
for button in current_classes:
color = button["props"]["style"]["background-color"]
button["props"]["id"] = {"type": "annotation-delete-buttons", "index": color}
button["props"]["style"]["border"] = "1px solid"
return current_classes


@callback(
Output({"type": "annotation-delete-buttons", "index": ALL}, "style"),
Input({"type": "annotation-delete-buttons", "index": ALL}, "n_clicks"),
State({"type": "annotation-delete-buttons", "index": ALL}, "style"),
prevent_initial_call=True,
)
def highlight_selected_classes(selected_classes, current_styles):
for i in range(len(selected_classes)):
if selected_classes[i] is not None and selected_classes[i] % 2 != 0:
current_styles[i]["border"] = "3px solid black"
else:
current_styles[i]["border"] = "1px solid"
return current_styles


@callback(
Output("image-viewer", "figure", allow_duplicate=True),
Output("annotation-class-selection", "className"),
Output({"type": "annotation-color", "index": ALL}, "style"),
Input({"type": "annotation-color", "index": ALL}, "n_clicks"),
State({"type": "annotation-color", "index": ALL}, "style"),
prevent_initial_call=True,
)
def annotation_color(color_value):
def annotation_color(color_value, current_style):
"""
This callback is responsible for changing the color of the brush.
"""
color_name = json.loads(ctx.triggered[0]["prop_id"].split(".")[0])["index"]
hex_color = dmc.theme.DEFAULT_COLORS[color_name][7]
color = ctx.triggered_id["index"]
for i in range(len(current_style)):
if current_style[i]["background-color"] == color:
current_style[i]["border"] = "3px solid black"
else:
current_style[i]["border"] = "1px solid"
patched_figure = Patch()
patched_figure["layout"]["newshape"]["fillcolor"] = convert_hex_to_rgba(
hex_color, 0.3
)
patched_figure["layout"]["newshape"]["line"]["color"] = hex_color
return patched_figure, color_name
patched_figure["layout"]["newshape"]["fillcolor"] = color
patched_figure["layout"]["newshape"]["line"]["color"] = color
return patched_figure, current_style


@callback(
Output("delete-all-warning", "opened"),
Input("delete-all", "n_clicks"),
Input("modal-cancel-button", "n_clicks"),
Input("modal-delete-button", "n_clicks"),
State("delete-all-warning", "opened"),
prevent_initial_call=True,
)
def open_warning_modal(delete, cancel, delete_4_real, opened):
return not opened


@callback(
Output("generate-annotation-class-modal", "opened"),
Input("generate-annotation-class", "n_clicks"),
Input("create-annotation-class", "n_clicks"),
State("generate-annotation-class-modal", "opened"),
prevent_initial_call=True,
)
def open_annotation_class_modal(generate, create, opened):
return not opened


@callback(
Output("delete-annotation-class-modal", "opened"),
Input("delete-annotation-class", "n_clicks"),
Input("remove-annotation-class", "n_clicks"),
State("delete-annotation-class-modal", "opened"),
prevent_initial_call=True,
)
def open_delete_class_modal(delete, remove, opened):
return not opened


@callback(
Output("create-annotation-class", "disabled"),
Input("annotation-class-label", "value"),
)
def disable_class_creation(label):
if label is None or len(label) == 0:
return True
else:
return False


@callback(
Output("remove-annotation-class", "disabled"),
Output("at-least-one", "style"),
Input({"type": "annotation-delete-buttons", "index": ALL}, "style"),
prevent_initial_call=True,
)
def disable_class_deletion(highlighted):
num_selected = 0
for style in highlighted:
if style["border"] == "3px solid black":
num_selected += 1
if num_selected == 0:
return True, {"display": "none"}
if num_selected == len(highlighted):
return True, {"display": "initial"}
else:
return False, {"display": "none"}


@callback(
Output("annotation-class-selection", "children"),
Output("annotation-class-label", "value"),
Output("annotation-store", "data", allow_duplicate=True),
Input("create-annotation-class", "n_clicks"),
Input("remove-annotation-class", "n_clicks"),
State("annotation-class-label", "value"),
State("annotation-class-colorpicker", "value"),
State("annotation-class-selection", "children"),
State({"type": "annotation-delete-buttons", "index": ALL}, "style"),
State("annotation-store", "data"),
prevent_initial_call=True,
)
def add_new_class(
create,
remove,
class_label,
class_color,
current_classes,
classes_to_delete,
annotation_store,
):
"""Updates the list of available annotation classes"""
triggered = ctx.triggered_id
if triggered == "create-annotation-class":
annotation_store["label_mapping"][class_color] = class_label
if class_color is None:
class_color = "rgb(255, 255, 255)"
if class_color == "rgb(255, 255, 255)":
current_classes.append(
dmc.ActionIcon(
id={"type": "annotation-color", "index": class_color},
w=30,
variant="filled",
style={
"background-color": class_color,
"border": "1px solid",
"color": "black",
},
children=class_label,
)
)
else:
current_classes.append(
dmc.ActionIcon(
id={"type": "annotation-color", "index": class_color},
w=30,
variant="filled",
style={"background-color": class_color, "border": "1px solid"},
children=class_label,
)
)
output_classes = current_classes
else:
color_to_delete = []
color_to_keep = []
for color_opt in classes_to_delete:
if color_opt["border"] == "3px solid black":
color_to_delete.append(color_opt["background-color"])
for color in current_classes:
if color["props"]["id"]["index"] not in color_to_delete:
color_to_keep.append(color)
output_classes = color_to_keep
annotation_store["label_mapping"] = {
elem["props"]["style"]["background-color"]: elem["props"]["children"]
for elem in output_classes
}
return output_classes, "", annotation_store


@callback(
Output("annotation-store", "data", allow_duplicate=True),
Output("image-viewer", "figure", allow_duplicate=True),
Input("view-annotations", "checked"),
Input("modal-delete-button", "n_clicks"),
State("annotation-store", "data"),
State("image-viewer", "figure"),
State("image-selection-slider", "value"),
prevent_initial_call=True,
)
def annotation_visibility(checked, annotation_store, figure, image_idx):
def annotation_visibility(checked, delete_all, 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 - 1)
patched_figure = Patch()
if checked:
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:
new_annotation_data = (
[] if "shapes" not in figure["layout"] else figure["layout"]["shapes"]
)
annotation_store["visible"] = False
patched_figure["layout"]["dragmode"] = False
annotation_store["annotations"][image_idx] = new_annotation_data
if ctx.triggered_id == "modal-delete-button":
annotation_store["annotations"][image_idx] = []
patched_figure["layout"]["shapes"] = []
else:
if checked:
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:
new_annotation_data = (
[] if "shapes" not in figure["layout"] else figure["layout"]["shapes"]
)
annotation_store["visible"] = False
patched_figure["layout"]["dragmode"] = False
annotation_store["annotations"][image_idx] = new_annotation_data
patched_figure["layout"]["shapes"] = []

return annotation_store, patched_figure

Expand Down
Loading
Loading