From fc6f941b2a17b05fdddb3032e58d36502a902fa3 Mon Sep 17 00:00:00 2001 From: Tammy Do <61042740+tamidodo@users.noreply.github.com> Date: Tue, 25 Jul 2023 11:36:07 -0700 Subject: [PATCH] restricts user to creating a class that doesn't already exist --- callbacks/control_bar.py | 76 +++++++++++++++++++++++++++++++++----- components/control_bar.py | 14 ++++--- components/image_viewer.py | 15 +------- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/callbacks/control_bar.py b/callbacks/control_bar.py index 2884e9f3..d85eb186 100644 --- a/callbacks/control_bar.py +++ b/callbacks/control_bar.py @@ -179,13 +179,35 @@ def open_delete_class_modal(delete, remove, opened): @callback( Output("create-annotation-class", "disabled"), + Output("bad-label-color", "children"), Input("annotation-class-label", "value"), + Input("annotation-class-colorpicker", "value"), + State({"type": "annotation-color", "index": ALL}, "children"), + State({"type": "annotation-color", "index": ALL}, "style"), + prevent_initial_call=True, ) -def disable_class_creation(label): - if label is None or len(label) == 0: - return True +def disable_class_creation(label, color, current_labels, current_colors): + if color is None: + color = "rgb(255, 255, 255)" + current_colors = [style["background-color"] for style in current_colors] + warning_text = [] + if label in current_labels: + warning_text.append( + dmc.Text("This annotation class label is already in use.", color="red") + ) + if color in current_colors: + warning_text.append( + dmc.Text("This annotation class color is already in use.", color="red") + ) + if ( + label is None + or len(label) == 0 + or label in current_labels + or color in current_colors + ): + return True, warning_text else: - return False + return False, warning_text @callback( @@ -210,19 +232,30 @@ def disable_class_deletion(highlighted): @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({"type": "annotation-delete-buttons", "index": ALL}, "children"), + State("annotation-store", "data"), prevent_initial_call=True, ) def add_new_class( - create, remove, class_label, class_color, current_classes, classes_to_delete + create, + remove, + class_label, + class_color, + current_classes, + classes_to_delete, + class_names, + annotation_store, ): """Updates the list of available annotation classes""" triggered = ctx.triggered_id + current_stored_classes = annotation_store["classes"] if triggered == "create-annotation-class": if class_color is None: class_color = "rgb(255, 255, 255)" @@ -250,17 +283,26 @@ def add_new_class( children=class_label, ) ) - return current_classes, "" + new_idx = int(max(list(current_stored_classes.keys()))) + current_stored_classes[new_idx + 1] = class_label + annotation_store["classes"] = current_stored_classes + return current_classes, "", annotation_store 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 i in range(len(classes_to_delete)): + if classes_to_delete[i]["border"] == "3px solid black": + color_to_delete.append(classes_to_delete[i]["background-color"]) + current_stored_classes = { + key: val + for key, val in current_stored_classes.items() + if val != class_names[i] + } for color in current_classes: if color["props"]["id"]["index"] not in color_to_delete: color_to_keep.append(color) - return color_to_keep, "" + annotation_store["classes"] = current_stored_classes + return color_to_keep, "", annotation_store @callback( @@ -330,3 +372,17 @@ def reset_filters(n_clicks): default_brightness = 100 default_contrast = 100 return default_brightness, default_contrast + + +clientside_callback( + """ + function eraseShape(_, graph_id) { + Plotly.eraseActiveShape(graph_id) + return dash_clientside.no_update + } + """, + Output("image-viewer", "id", allow_duplicate=True), + Input("eraser", "n_clicks"), + State("image-viewer", "id"), + prevent_initial_call=True, +) diff --git a/components/control_bar.py b/components/control_bar.py index b56a3599..8c9b1ef4 100644 --- a/components/control_bar.py +++ b/components/control_bar.py @@ -188,7 +188,7 @@ def layout(): color="gray", children=DashIconify(icon="ph:eraser"), ), - label="Eraser: click on shapes to delete them", + label="Eraser: click on the shape to erase then click this button to delete the selected shape", multiline=True, ), dmc.Tooltip( @@ -316,6 +316,7 @@ def layout(): variant="light", ), ), + html.Div(id="bad-label-color"), ], ), dmc.Modal( @@ -338,10 +339,12 @@ def layout(): ), ] ), - dmc.Text( - "There must be at least one annotation class!", - color="red", - id="at-least-one", + dmc.Center( + dmc.Text( + "There must be at least one annotation class!", + color="red", + id="at-least-one", + ), ), ], ), @@ -373,6 +376,7 @@ def layout(): "annotations": {}, "view": {}, "image_size": [], + "classes": {1: "1"}, }, ), dcc.Store(id="project-data"), diff --git a/components/image_viewer.py b/components/image_viewer.py index 5a6f2d79..7be91b1a 100644 --- a/components/image_viewer.py +++ b/components/image_viewer.py @@ -13,21 +13,8 @@ } FIGURE_CONFIG = { - "modeBarButtonsToAdd": [ - "drawopenpath", - "drawclosedpath", - "eraseshape", - "drawline", - "drawcircle", - "drawrect", - ], + "displayModeBar": False, "scrollZoom": True, - "modeBarButtonsToRemove": [ - "zoom", - "zoomin", - "zoomout", - "autoscale", - ], }