From c38943cc4fc866c6d9058e4629e1583d077938e4 Mon Sep 17 00:00:00 2001 From: Nikolai Petukhov Date: Mon, 18 Sep 2023 18:25:17 -0300 Subject: [PATCH] test --- edges.json | 57 ++ graph.json | 50 ++ nodes.json | 431 +++++++++++++ nodes_state.json | 47 ++ requirements.txt | 4 +- src/compute/Layer.py | 126 ++-- src/compute/Net.py | 55 +- src/compute/layers/data/DataLayer.py | 13 +- src/compute/layers/processing/BlurLayer.py | 58 +- src/compute/layers/processing/RotateLayer.py | 17 +- src/compute/layers/save/SaveLayer.py | 98 +-- src/compute/layers/save/SaveMasksLayer.py | 79 +-- src/compute/layers/save/SuperviselyLayer.py | 21 +- src/compute/main.py | 19 +- src/exceptions.py | 117 ++++ src/globals.py | 12 + src/main.py | 34 ++ src/ui/dtl/Action.py | 49 +- src/ui/dtl/Layer.py | 274 +++++---- src/ui/dtl/__init__.py | 57 +- src/ui/dtl/actions/approx_vector.py | 119 ++-- src/ui/dtl/actions/background.py | 40 +- src/ui/dtl/actions/bbox.py | 194 ++++-- src/ui/dtl/actions/bbox2poly.py | 205 +++++-- src/ui/dtl/actions/bitmap2lines.py | 225 ++++--- src/ui/dtl/actions/bitwise_masks.py | 159 +++-- src/ui/dtl/actions/blur.py | 114 ++-- src/ui/dtl/actions/color_class.py | 106 ++-- src/ui/dtl/actions/contrast_brightness.py | 140 ++--- src/ui/dtl/actions/crop.py | 103 ++-- src/ui/dtl/actions/data.py | 411 ++++++++----- src/ui/dtl/actions/dataset.py | 73 +-- src/ui/dtl/actions/drop_lines_by_length.py | 171 +++--- src/ui/dtl/actions/drop_noise.py | 134 ++-- src/ui/dtl/actions/drop_obj_by_class.py | 85 +-- src/ui/dtl/actions/dummy.py | 28 +- src/ui/dtl/actions/duplicate_objects.py | 194 ++++-- src/ui/dtl/actions/find_contours.py | 202 ++++-- src/ui/dtl/actions/flip.py | 57 -- src/ui/dtl/actions/flip/flip.py | 55 ++ src/ui/dtl/actions/flip/readme.md | 31 + src/ui/dtl/actions/if_action.py | 191 ++++-- src/ui/dtl/actions/instances_crop.py | 127 ++-- src/ui/dtl/actions/line2bitmap.py | 216 ++++--- src/ui/dtl/actions/merge_bitmaps.py | 91 +-- src/ui/dtl/actions/multiply.py | 55 +- src/ui/dtl/actions/noise.py | 70 ++- src/ui/dtl/actions/objects_filter.py | 197 ++++-- src/ui/dtl/actions/poly2bitmap.py | 204 +++++-- src/ui/dtl/actions/random_color.py | 55 +- src/ui/dtl/actions/rasterize.py | 191 ++++-- src/ui/dtl/actions/rename.py | 188 ++++-- src/ui/dtl/actions/resize.py | 88 ++- src/ui/dtl/actions/rotate.py | 116 ++-- src/ui/dtl/actions/save.py | 70 ++- src/ui/dtl/actions/save_masks.py | 229 ++++--- src/ui/dtl/actions/skeletonize.py | 107 ++-- src/ui/dtl/actions/sliding_window.py | 135 ++-- src/ui/dtl/actions/split_masks.py | 78 +-- src/ui/dtl/actions/supervisely.py | 39 +- src/ui/dtl/actions/tag.py | 133 ++-- src/ui/dtl/utils.py | 212 +++++++ src/ui/tabs/configure.py | 270 ++++++++ src/ui/tabs/json_preview.py | 202 ++++++ src/ui/tabs/run.py | 94 +++ src/ui/ui.py | 578 +----------------- src/ui/utils.py | 374 ++++++++++++ src/ui/widgets/__init__.py | 3 + src/ui/widgets/classes_list/classes_list.py | 12 +- src/ui/widgets/classes_list/template.html | 5 +- .../widgets/classes_list_preview/__init__.py | 1 + .../classes_list_preview.py | 33 + .../classes_list_preview/template.html | 21 + .../classes_mapping/classes_mapping.py | 9 +- src/ui/widgets/classes_mapping/template.html | 5 +- .../classes_mapping_preview/__init__.py | 1 + .../classes_mapping_preview.py | 43 ++ .../classes_mapping_preview/template.html | 31 + src/ui/widgets/tag_metas_preview/__init__.py | 1 + .../tag_metas_preview/tag_metas_preview.py | 55 ++ .../widgets/tag_metas_preview/template.html | 29 + src/utils.py | 189 +----- 82 files changed, 6025 insertions(+), 3187 deletions(-) create mode 100644 edges.json create mode 100644 graph.json create mode 100644 nodes.json create mode 100644 nodes_state.json create mode 100644 src/exceptions.py delete mode 100644 src/ui/dtl/actions/flip.py create mode 100644 src/ui/dtl/actions/flip/flip.py create mode 100644 src/ui/dtl/actions/flip/readme.md create mode 100644 src/ui/dtl/utils.py create mode 100644 src/ui/tabs/configure.py create mode 100644 src/ui/tabs/json_preview.py create mode 100644 src/ui/tabs/run.py create mode 100644 src/ui/utils.py create mode 100644 src/ui/widgets/classes_list_preview/__init__.py create mode 100644 src/ui/widgets/classes_list_preview/classes_list_preview.py create mode 100644 src/ui/widgets/classes_list_preview/template.html create mode 100644 src/ui/widgets/classes_mapping_preview/__init__.py create mode 100644 src/ui/widgets/classes_mapping_preview/classes_mapping_preview.py create mode 100644 src/ui/widgets/classes_mapping_preview/template.html create mode 100644 src/ui/widgets/tag_metas_preview/__init__.py create mode 100644 src/ui/widgets/tag_metas_preview/tag_metas_preview.py create mode 100644 src/ui/widgets/tag_metas_preview/template.html diff --git a/edges.json b/edges.json new file mode 100644 index 00000000..1a0b0e06 --- /dev/null +++ b/edges.json @@ -0,0 +1,57 @@ +[ + { + "id": "169143702805114", + "output": { + "node": "data_1", + "interface": "destination" + }, + "input": { + "node": "if_3", + "interface": "source" + } + }, + { + "id": "169143703364517", + "output": { + "node": "if_3", + "interface": "destination_true" + }, + "input": { + "node": "approx_vector_2", + "interface": "source" + } + }, + { + "id": "169143703561020", + "output": { + "node": "if_3", + "interface": "destination_false" + }, + "input": { + "node": "duplicate_objects_4", + "interface": "source" + } + }, + { + "id": "169143704716225", + "output": { + "node": "approx_vector_2", + "interface": "destination" + }, + "input": { + "node": "save_5", + "interface": "source" + } + }, + { + "id": "169143704898828", + "output": { + "node": "duplicate_objects_4", + "interface": "destination" + }, + "input": { + "node": "save_5", + "interface": "source" + } + } +] \ No newline at end of file diff --git a/graph.json b/graph.json new file mode 100644 index 00000000..c2fd016c --- /dev/null +++ b/graph.json @@ -0,0 +1,50 @@ +[ + { + "action": "data", + "src": [ + "Lemons for exam PRED/*" + ], + "dst": "$data_1", + "settings": { + "classes_mapping": "default" + } + }, + { + "action": "if", + "src": [ + "$data_1" + ], + "dst": [ + "$if_2_true", + "$if_2_false" + ], + "settings": { + "condition": { + "probability": 0.66 + } + } + }, + { + "action": "drop_obj_by_class", + "src": [ + "$if_2_true" + ], + "dst": "$drop_obj_by_class_3", + "settings": { + "classes": [ + "kiwi" + ] + } + }, + { + "action": "save", + "src": [ + "$if_2_false", + "$drop_obj_by_class_3" + ], + "dst": "lemons_transformed", + "settings": { + "visualize": true + } + } +] \ No newline at end of file diff --git a/nodes.json b/nodes.json new file mode 100644 index 00000000..0ddd9195 --- /dev/null +++ b/nodes.json @@ -0,0 +1,431 @@ +[ + { + "id": "data_1", + "name": "Data", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "source_text", + "component": "TextOption", + "value": "Source", + "sidebarComponent": null, + "options": {} + }, + { + "name": "src", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping_text", + "component": "TextOption", + "value": "Classes Mapping", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [], + "outputs": [ + { + "name": "destination", + "options": { + "displayName": "Destination" + } + } + ], + "position": { + "x": 157, + "y": 24 + } + }, + { + "id": "data_2", + "name": "Data", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "source_text", + "component": "TextOption", + "value": "Source", + "sidebarComponent": null, + "options": {} + }, + { + "name": "src", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping_text", + "component": "TextOption", + "value": "Classes Mapping", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [], + "outputs": [ + { + "name": "destination", + "options": { + "displayName": "Destination" + } + } + ], + "position": { + "x": 158, + "y": 262 + } + }, + { + "id": "if_3", + "name": "If", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "condition_text", + "component": "TextOption", + "value": "Condition", + "sidebarComponent": null, + "options": {} + }, + { + "name": "condition", + "component": "SelectOption", + "value": "probability", + "sidebarComponent": null, + "options": { + "items": [ + { + "text": "Probability", + "value": "probability" + }, + { + "text": "Min objects count", + "value": "min_objects_count" + }, + { + "text": "Min height", + "value": "min_height" + }, + { + "text": "Tags", + "value": "tags" + }, + { + "text": "Include classes", + "value": "include_classes" + }, + { + "text": "Name in range", + "value": "name_in_range" + } + ] + } + }, + { + "name": "condition_value_text", + "component": "TextOption", + "value": "Condition Value", + "sidebarComponent": null, + "options": {} + }, + { + "name": "condition_value", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [ + { + "name": "source", + "options": { + "displayName": "Source" + } + } + ], + "outputs": [ + { + "name": "destination_true", + "options": { + "displayName": "Destination True" + } + }, + { + "name": "destination_false", + "options": { + "displayName": "Destination False" + } + } + ], + "position": { + "x": 507, + "y": 9 + } + }, + { + "id": "approx_vector_4", + "name": "Approx Vector", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "classes_text", + "component": "TextOption", + "value": "Classes", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + }, + { + "name": "epsilon_text", + "component": "TextOption", + "value": "Epsilon", + "sidebarComponent": null, + "options": {} + }, + { + "name": "epsilon", + "component": "IntegerOption", + "value": 3, + "sidebarComponent": null, + "options": { + "min": 1 + } + } + ], + "inputs": [ + { + "name": "source", + "options": { + "displayName": "Source" + } + } + ], + "outputs": [ + { + "name": "destination", + "options": { + "displayName": "Destination" + } + } + ], + "position": { + "x": 825.6461641025293, + "y": -95.97889858536985 + } + }, + { + "id": "bitmap2lines_5", + "name": "Bitmap to Lines", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "min_points_cnt_text", + "component": "TextOption", + "value": "Min Points Count", + "sidebarComponent": null, + "options": {} + }, + { + "name": "min_points_cnt", + "component": "IntegerOption", + "value": 2, + "sidebarComponent": null, + "options": { + "min": 2 + } + }, + { + "name": "classes_mapping_text", + "component": "TextOption", + "value": "Classes Mapping", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [ + { + "name": "source", + "options": { + "displayName": "Source" + } + } + ], + "outputs": [ + { + "name": "destination", + "options": { + "displayName": "Destination" + } + } + ], + "position": { + "x": 826.5391525197994, + "y": 165.55185526083622 + } + }, + { + "id": "find_contours_6", + "name": "Find Contours", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "classes_mapping_text", + "component": "TextOption", + "value": "Classes Mapping", + "sidebarComponent": null, + "options": {} + }, + { + "name": "classes_mapping", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [ + { + "name": "source", + "options": { + "displayName": "Source" + } + } + ], + "outputs": [ + { + "name": "destination", + "options": { + "displayName": "Destination" + } + } + ], + "position": { + "x": 824.5764388232484, + "y": 435.5822485673756 + } + }, + { + "id": "supervisely_7", + "name": "Supervisely", + "options": [ + { + "name": "Info", + "component": "ButtonOption", + "value": null, + "sidebarComponent": "SlyFlowOptionRenderer", + "options": { + "template": "
\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n\n\n\n\n
\n
\n
" + } + }, + { + "name": "destination_text", + "component": "TextOption", + "value": "Destination", + "sidebarComponent": null, + "options": {} + }, + { + "name": "dst", + "component": "InputOption", + "value": "", + "sidebarComponent": null, + "options": {} + } + ], + "inputs": [ + { + "name": "source", + "options": { + "displayName": "Source" + } + } + ], + "outputs": [], + "position": { + "x": 1210.947875092936, + "y": 149.59682625218753 + } + } +] \ No newline at end of file diff --git a/nodes_state.json b/nodes_state.json new file mode 100644 index 00000000..62436409 --- /dev/null +++ b/nodes_state.json @@ -0,0 +1,47 @@ +{ + "data_1": { + "Info": null, + "source_text": "Source", + "src": "src1", + "classes_mapping_text": "Classes Mapping", + "classes_mapping": "\"default\"" + }, + "data_2": { + "Info": null, + "source_text": "Source", + "src": "src2", + "classes_mapping_text": "Classes Mapping", + "classes_mapping": "{\"cls2\":\"cls2_copy\"}" + }, + "if_3": { + "Info": null, + "condition_text": "Condition", + "condition": "probability", + "condition_value_text": "Condition Value", + "condition_value": "0.8" + }, + "approx_vector_4": { + "Info": null, + "classes_text": "Classes", + "classes": "cls1", + "epsilon_text": "Epsilon", + "epsilon": 3 + }, + "bitmap2lines_5": { + "Info": null, + "min_points_cnt_text": "Min Points Count", + "min_points_cnt": 2, + "classes_mapping_text": "Classes Mapping", + "classes_mapping": "{\"cls1\": \"cls1_line\"}" + }, + "find_contours_6": { + "Info": null, + "classes_mapping_text": "Classes Mapping", + "classes_mapping": "{\"cls2_copy\":\"cls2_copy_cont\"}" + }, + "supervisely_7": { + "Info": null, + "destination_text": "Destination", + "dst": "result" + } +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index cc4e411a..6a9d8886 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,6 @@ git+https://github.com/supervisely/supervisely.git@NikolaiPetukhov jsonschema networkx scikit-image>=0.17.1, <1.0.0 -cacheout \ No newline at end of file +cacheout +markdown +json2html \ No newline at end of file diff --git a/src/compute/Layer.py b/src/compute/Layer.py index 438f2b8d..c8f40f98 100644 --- a/src/compute/Layer.py +++ b/src/compute/Layer.py @@ -18,6 +18,7 @@ from supervisely.imaging.color import hex2rgb from src.compute.classes_utils import ClassConstants +from src.exceptions import CustomException, GraphError, CreateMetaError, UnexpectedError def maybe_wrap_in_list(v): @@ -26,9 +27,9 @@ def maybe_wrap_in_list(v): def check_connection_name(connection_name): if len(connection_name) == 0: - raise RuntimeError("Connection name should be non empty.") + raise GraphError("Connection name should be non empty.") if connection_name[0] != "$" and connection_name != Layer.null: - raise RuntimeError('Connection name should be "%s" or start with "$".' % Layer.null) + raise GraphError(f'Connection name should be "{Layer.null}" or start with "$".') class Layer: @@ -84,9 +85,16 @@ def __init__(self, config): self.output_meta = None def validate(self): - jsonschema.validate(self._config, self.params) - self.validate_source_connections() - self.validate_dest_connections() + try: + jsonschema.validate(self._config, self.params) + except jsonschema.ValidationError as e: + raise GraphError("Layer not valid", error=e, extra={"layer_config": self._config}) + try: + self.validate_source_connections() + self.validate_dest_connections() + except GraphError as e: + e.extra["layer_config"] = self._config + raise e @property def config(self): @@ -110,7 +118,7 @@ def requires_image(self): def validate_source_connections(self): for src in self.srcs: if src == Layer.null: - raise RuntimeError('"%s" cannot be in "src".' % Layer.null) + raise GraphError(f'"Layer.null" cannot be in "src"') check_connection_name(src) def validate_dest_connections(self): @@ -130,17 +138,31 @@ def make_output_meta(self, input_metas_dict): if existing_obj_class is None: full_input_meta = full_input_meta.add_obj_class(inp_obj_class) elif existing_obj_class.geometry_type != inp_obj_class.geometry_type: - raise RuntimeError( - f"Trying to add new class ({inp_obj_class.name}) with shape ({inp_obj_class.geometry_type.geometry_name()}). Same class with different shape ({existing_obj_class.geometry_type.geometry_name()}) exists." + raise CreateMetaError( + "Trying to add existing ObjClass with different geometry type", + extra={ + "existing_class": existing_obj_class.to_json(), + "new_class": inp_obj_class.to_json(), + }, ) + # raise RuntimeError( + # f"Trying to add new class ({inp_obj_class.name}) with shape ({inp_obj_class.geometry_type.geometry_name()}). Same class with different shape ({existing_obj_class.geometry_type.geometry_name()}) exists." + # ) for inp_tag_meta in inp_meta.tag_metas: existing_tag_meta = full_input_meta.tag_metas.get(inp_tag_meta.name, None) if existing_tag_meta is None: full_input_meta = full_input_meta.add_tag_meta(inp_tag_meta) elif not existing_tag_meta.is_compatible(inp_tag_meta): - raise RuntimeError( - f"Trying to add new tag ({inp_tag_meta.name}) with type ({inp_tag_meta.value_type}) and possible values ({inp_tag_meta.possible_values}). Same tag with different type ({existing_tag_meta.value_type}) or possible values ({existing_tag_meta.possible_values}) exists." + raise CreateMetaError( + "Trying to add existing TagMeta with different type or possible values", + extra={ + "existing_tag_meta": existing_tag_meta.to_json(), + "new_tag_meta": inp_tag_meta.to_json(), + }, ) + # raise RuntimeError( + # f"Trying to add new tag ({inp_tag_meta.name}) with type ({inp_tag_meta.value_type}) and possible values ({inp_tag_meta.possible_values}). Same tag with different type ({existing_tag_meta.value_type}) or possible values ({existing_tag_meta.possible_values}) exists." + # ) res_meta = deepcopy(full_input_meta) in_class_titles = set((obj_class.name for obj_class in full_input_meta.obj_classes)) @@ -152,9 +174,17 @@ def make_output_meta(self, input_metas_dict): self.cls_mapping[oclass] = self.cls_mapping[ClassConstants.OTHER] del self.cls_mapping[ClassConstants.OTHER] - missed_classes = in_class_titles - set(self.cls_mapping.keys()) - if len(missed_classes) != 0: - raise RuntimeError("Some classes in mapping are missed: {}".format(missed_classes)) + missing_classes = in_class_titles - set(self.cls_mapping.keys()) + if len(missing_classes) != 0: + raise CreateMetaError( + "Some classes in input meta are missing in mapping", + extra={ + "missing_classes": [ + res_meta.obj_classes.get(obj_class_name) + for obj_class_name in missing_classes + ] + }, + ) for src_class_title, dst_class in self.cls_mapping.items(): # __new__ -> [ list of classes ] @@ -165,15 +195,22 @@ def make_output_meta(self, input_metas_dict): new_name = new_cls_dict["title"] new_shape = new_cls_dict["shape"] new_geometry_type = GET_GEOMETRY_FROM_STR(new_shape) + inp_obj_class = ObjClass(new_name, new_geometry_type) if res_meta.obj_classes.has_key(new_name): - existing_cls = res_meta.obj_classes.get(new_name) - if existing_cls.geometry_type != new_geometry_type: + existing_obj_class = res_meta.obj_classes.get(new_name) + if existing_obj_class.geometry_type != new_geometry_type: + raise CreateMetaError( + "Trying to add existing ObjClass with different geometry type", + extra={ + "existing_class": existing_obj_class.to_json(), + "new_class": inp_obj_class.to_json(), + }, + ) raise RuntimeError( f"Trying to add new class ({new_name}) with shape ({new_shape}). Same class with different shape ({existing_cls.geometry_type.geometry_name()}) exists." ) else: - new_cls = ObjClass(new_name, new_geometry_type) - res_meta = res_meta.add_obj_class(new_cls) + res_meta = res_meta.add_obj_class(inp_obj_class) # __clone__ -> dict {parent_cls_name: child_cls_name} elif src_class_title == ClassConstants.CLONE: @@ -181,12 +218,14 @@ def make_output_meta(self, input_metas_dict): raise RuntimeError("Internal class mapping error in layer (CLONE spec).") for src_title, dst_title in dst_class.items(): - real_src_cls = full_input_meta.obj_classes.get(src_title, None) + real_src_cls = res_meta.obj_classes.get(src_title, None) if real_src_cls is None: - raise RuntimeError( - 'Class mapping error, source class "{}" not found.'.format( - src_title - ) + raise CreateMetaError( + "Class not found in input meta", + extra={ + "class_name": src_title, + "existing_classes": res_meta.obj_classes.to_json(), + }, ) real_dst_cls = real_src_cls.clone(name=dst_title) res_meta = res_meta.add_obj_class(real_dst_cls) @@ -198,21 +237,24 @@ def make_output_meta(self, input_metas_dict): for cls_dct in dst_class: title = cls_dct["title"] existing_class = res_meta.obj_classes.get(title, None) - if existing_class is not None: - new_shape = cls_dct.get("shape", None) - new_geometry_type = ( - GET_GEOMETRY_FROM_STR(new_shape) if new_shape else None + if existing_class is None: + raise CreateMetaError( + "Class not found in input meta", + extra={ + "class_name": title, + "existing_classes": res_meta.obj_classes.to_json(), + }, ) - new_color = cls_dct.get("color", None) - if new_color is not None and new_color[0] == "#": - new_color = hex2rgb(new_color) - new_obj_cls = existing_class.clone( - name=title, geometry_type=new_geometry_type, color=new_color - ) - res_meta = res_meta.delete_obj_class(title) - res_meta = res_meta.add_obj_class(new_obj_cls) - else: - raise RuntimeError("Can not update class {}. Not found".format(title)) + new_shape = cls_dct.get("shape", None) + new_geometry_type = GET_GEOMETRY_FROM_STR(new_shape) if new_shape else None + new_color = cls_dct.get("color", None) + if new_color is not None and new_color[0] == "#": + new_color = hex2rgb(new_color) + new_obj_cls = existing_class.clone( + name=title, geometry_type=new_geometry_type, color=new_color + ) + res_meta = res_meta.delete_obj_class(title) + res_meta = res_meta.add_obj_class(new_obj_cls) # smth -> __default__ elif dst_class == ClassConstants.DEFAULT: @@ -244,7 +286,6 @@ def make_output_meta(self, input_metas_dict): res_meta = res_meta.delete_obj_class(src_class_title) res_meta = res_meta.add_obj_class(obj_cls) - # TODO switch to get added / removed tags to be TagMeta instances. rm_imtags = [TagMeta.from_json(tag) for tag in self.get_removed_tag_metas()] res_meta = res_meta.clone( tag_metas=[tm for tm in res_meta.tag_metas if tm not in rm_imtags] @@ -253,19 +294,18 @@ def make_output_meta(self, input_metas_dict): new_imtags_exist = [ tm for tm in res_meta.tag_metas.intersection(TagMetaCollection(new_imtags)) ] - # new_imtags_exist = res_meta.tags.intersection(new_imtags).to_list() if len(new_imtags_exist) != 0: exist_tag_names = [t.name for t in new_imtags_exist] logger.warn("Tags {} already exist.".format(exist_tag_names)) res_meta.clone(tag_metas=new_imtags) self.output_meta = res_meta + except CustomException as e: + raise e except Exception as e: - logger.error( - "Meta-error occurred in layer '{}' with config: {}".format( - self.action, self._config - ) + raise UnexpectedError( + "Unexpected error occurred while creating meta", + error=e, ) - raise e return self.output_meta diff --git a/src/compute/Net.py b/src/compute/Net.py index 6ffd339d..89f4e689 100644 --- a/src/compute/Net.py +++ b/src/compute/Net.py @@ -5,7 +5,7 @@ import numpy as np -from supervisely import Annotation, rand_str, ProjectMeta, DatasetInfo +from supervisely import Annotation, rand_str, ProjectMeta from src.compute.Layer import Layer from src.compute import layers # to register layers @@ -20,11 +20,13 @@ ) import src.globals as g from src.utils import LegacyProjectItem +from src.exceptions import ActionNotFoundError, BadSettingsError, CreateMetaError, GraphError class Net: def __init__(self, graph_desc, output_folder): self.layers = [] + self.preview_mode = False if type(graph_desc) is str: graph_path = graph_desc @@ -32,16 +34,18 @@ def __init__(self, graph_desc, output_folder): if not os.path.exists(graph_path): raise RuntimeError('No such config file "%s"' % graph_path) else: - graph = json.load(open(graph_path, "r")) + self.graph = json.load(open(graph_path, "r")) else: - graph = graph_desc + self.graph = graph_desc - for layer_config in graph: + for layer_config in self.graph: if "action" not in layer_config: - raise RuntimeError('No "action" field in layer "{}".'.format(layer_config)) + raise BadSettingsError( + 'Missing "action" field in layer config', extra={"layer_config": layer_config} + ) action = layer_config["action"] if action not in Layer.actions_mapping: - raise RuntimeError('Unrecognized action "{}".'.format(action)) + raise ActionNotFoundError(action) layer_cls = Layer.actions_mapping[action] if layer_cls.type == "data": layer = layer_cls(layer_config) @@ -73,11 +77,11 @@ def validate(self): graph_has_savel = True if graph_has_datal is False: - raise RuntimeError("Graph error: missing data layer.") + raise GraphError("Missing data layer") if graph_has_savel is False: - raise RuntimeError("Graph error: missing save layer.") + raise GraphError("Missing save layer") if len(self.layers) < 2: - raise RuntimeError("Graph error: less than two layers.") + raise GraphError("Less than two layers") self.check_connections() def get_input_project_metas(self): @@ -127,7 +131,7 @@ def check_connections(self, indx=-1): else: color = self.layers[indx].color if color == "visiting": - raise RuntimeError("Loop in layers structure.") + raise GraphError("Loop in layers structure.") if color == "visited": return self.layers[indx].color = "visiting" @@ -203,20 +207,23 @@ def start(self, data_el): for output in output_generator: yield output - def start_iterate(self, data_el): + def start_iterate(self, data_el, layer_idx: int = None, skip_save_layers=False): img_pr_name = data_el[0].get_pr_name() img_ds_name = data_el[0].get_ds_name() - start_layer_indxs = set() - for idx, layer in enumerate(self.layers): - if layer.type != "data": - continue - if layer.project_name == img_pr_name and ( - "*" in layer.dataset_names or img_ds_name in layer.dataset_names - ): - start_layer_indxs.add(idx) - if len(start_layer_indxs) == 0: - raise RuntimeError("Can not find data layer for the image: {}".format(data_el)) + if layer_idx is not None: + start_layer_indxs = [layer_idx] + else: + start_layer_indxs = set() + for idx, layer in enumerate(self.layers): + if layer.type != "data": + continue + if layer.project_name == img_pr_name and ( + "*" in layer.dataset_names or img_ds_name in layer.dataset_names + ): + start_layer_indxs.add(idx) + if len(start_layer_indxs) == 0: + raise RuntimeError("Can not find data layer for the image: {}".format(data_el)) for start_layer_indx in start_layer_indxs: output_generator = self.process_iterate(start_layer_indx, data_el) @@ -400,7 +407,11 @@ def layer_input_metas_are_calculated(the_layer): processed_layers.add(cur_layer) # TODO no need for dict here? cur_layer_input_metas = {src: datalevel_metas[src] for src in cur_layer.srcs} - cur_layer_res_meta = cur_layer.make_output_meta(cur_layer_input_metas) + try: + cur_layer_res_meta = cur_layer.make_output_meta(cur_layer_input_metas) + except CreateMetaError as e: + e.extra["layer_config"] = cur_layer.config + raise e for dst in cur_layer.dsts: datalevel_metas[dst] = cur_layer_res_meta diff --git a/src/compute/layers/data/DataLayer.py b/src/compute/layers/data/DataLayer.py index c1921b39..a7999e32 100644 --- a/src/compute/layers/data/DataLayer.py +++ b/src/compute/layers/data/DataLayer.py @@ -1,8 +1,6 @@ # coding: utf-8 from typing import Tuple -from copy import deepcopy - from supervisely import Annotation, Label, ProjectMeta from src.compute.Layer import Layer @@ -10,6 +8,7 @@ from src.compute.dtl_utils.image_descriptor import ImageDescriptor from src.compute.dtl_utils import apply_to_labels from src.utils import get_project_by_name, get_project_meta +from src.exceptions import BadSettingsError class DataLayer(Layer): @@ -48,9 +47,9 @@ def _split_data_src(cls, src): src_components = src.strip("/").split("/") if src_components == [""] or len(src_components) > 2: # Empty name or too many components. - raise ValueError( - 'Wrong "data" layer source path "{}", use "project_name/dataset_name" or "project_name/*" ' - "format of the path:".format(src) + raise BadSettingsError( + 'Wrong "data" layer source path. Use "project_name/dataset_name" or "project_name/*"', + extra={"layer_config": cls.config}, ) if len(src_components) == 1: # Only the project is specified, append '*' for the datasets. @@ -65,7 +64,9 @@ def _define_layer_project(self): if self.project_name is None: self.project_name = project_name elif self.project_name != project_name: - raise ValueError("Data Layer can only work with one project") + raise BadSettingsError( + "Data Layer can only work with one project", extra={"layer_config": self.config} + ) dataset_names.add(dataset_name) self.dataset_names = list(dataset_names) diff --git a/src/compute/layers/processing/BlurLayer.py b/src/compute/layers/processing/BlurLayer.py index e844df3a..9361fc19 100644 --- a/src/compute/layers/processing/BlurLayer.py +++ b/src/compute/layers/processing/BlurLayer.py @@ -11,8 +11,7 @@ class BlurLayer(Layer): - - action = 'blur' + action = "blur" layer_settings = { "required": ["settings"], @@ -22,16 +21,13 @@ class BlurLayer(Layer): "oneOf": [ { "type": "object", - "required": [ - "name", - "sigma" - ], + "required": ["name", "sigma"], "properties": { "name": { "type": "string", "enum": [ "gaussian", - ] + ], }, "sigma": { "type": "object", @@ -39,45 +35,39 @@ class BlurLayer(Layer): "properties": { "min": {"type": "number", "minimum": 0.01}, "max": {"type": "number", "minimum": 0.01}, - } - } - } + }, + }, + }, }, { "type": "object", - "required": [ - "name", - "kernel" - ], + "required": ["name", "kernel"], "properties": { "name": { "type": "string", "enum": [ "median", - ] + ], }, - "kernel": { - "type": "integer", - "minimum": 3 - } - } - } - ] + "kernel": {"type": "integer", "minimum": 3}, + }, + }, + ], } - } + }, } def __init__(self, config): Layer.__init__(self, config) - if (self.settings['name'] == 'median') and (self.settings['kernel'] % 2 == 0): - raise RuntimeError('Kernel for median blur must be odd.') + if (self.settings["name"] == "median") and (self.settings["kernel"] % 2 == 0): + raise RuntimeError("Kernel for median blur must be odd.") def check_min_max(dictionary, text): - if dictionary['min'] > dictionary['max']: + if dictionary["min"] > dictionary["max"]: raise RuntimeError('"min" should be <= than "max" for "{}".'.format(text)) - if self.settings['name'] == 'gaussian': - check_min_max(self.settings['sigma'], 'sigma') + if self.settings["name"] == "gaussian": + check_min_max(self.settings["sigma"], "sigma") def requires_image(self): return True @@ -86,13 +76,13 @@ def process(self, data_el: Tuple[ImageDescriptor, Annotation]): img_desc, ann = data_el img = img_desc.read_image() - img = img.astype(np.float32) - if self.settings['name'] == 'gaussian': - sigma_b = self.settings['sigma'] - sigma_value = np.random.uniform(sigma_b['min'], sigma_b['max']) + img = img.astype(np.uint8) + if self.settings["name"] == "gaussian": + sigma_b = self.settings["sigma"] + sigma_value = np.random.uniform(sigma_b["min"], sigma_b["max"]) res_img = cv2.GaussianBlur(img, ksize=(0, 0), sigmaX=sigma_value) - elif self.settings['name'] == 'median': - res_img = cv2.medianBlur(img, ksize=self.settings['kernel']) + elif self.settings["name"] == "median": + res_img = cv2.medianBlur(img, ksize=self.settings["kernel"]) else: raise NotImplementedError() diff --git a/src/compute/layers/processing/RotateLayer.py b/src/compute/layers/processing/RotateLayer.py index 862e94a0..f66317ac 100644 --- a/src/compute/layers/processing/RotateLayer.py +++ b/src/compute/layers/processing/RotateLayer.py @@ -76,8 +76,6 @@ def expand_image_with_rect(img: np.ndarray, req_rect: Rectangle): def process(self, data_el: Tuple[ImageDescriptor, Annotation]): img_desc, ann = data_el - aug.rotate(mode=aug.RotationModes.KEEP) - angle_dct = self.settings["rotate_angles"] min_degrees, max_degrees = angle_dct["min_degrees"], angle_dct["max_degrees"] rotate_degrees = np.random.uniform(min_degrees, max_degrees) @@ -103,9 +101,20 @@ def process(self, data_el: Tuple[ImageDescriptor, Annotation]): if black_reg_mode == "preserve_size": rect_to_crop = Rectangle.from_array(img) new_img, (delta_x, delta_y) = self.expand_image_with_rect(new_img, rect_to_crop) - new_ann.img_size = new_img.shape[:2] - new_ann = apply_to_labels(ann, lambda x: x.translate(delta_x, delta_y)) + top_pad = max((new_img.shape[0] - ann.img_size[0]) // 2, 0) + lefet_pad = max((new_img.shape[1] - ann.img_size[1]) // 2, 0) + new_img, new_ann = aug.crop( + new_img, + new_ann, + top_pad=top_pad, + bottom_pad=new_img.shape[0] - top_pad - ann.img_size[0], + left_pad=lefet_pad, + right_pad=new_img.shape[1] - lefet_pad - ann.img_size[1], + ) + new_ann.clone(img_size=new_img.shape[:2]) + + new_ann = apply_to_labels(new_ann, lambda x: [x.translate(delta_x, delta_y)]) if new_img is None: return # no yield diff --git a/src/compute/layers/save/SaveLayer.py b/src/compute/layers/save/SaveLayer.py index 29d6099e..2fc7252f 100644 --- a/src/compute/layers/save/SaveLayer.py +++ b/src/compute/layers/save/SaveLayer.py @@ -66,6 +66,8 @@ def validate_dest_connections(self): pass def preprocess(self): + if self.net.preview_mode: + return if self.output_meta is None: sly.logger.warning("Save Layer: output meta is None. Skipped.") dst = self.dsts[0] @@ -84,52 +86,54 @@ def preprocess(self): def process(self, data_el: Tuple[ImageDescriptor, sly.Annotation]): img_desc, ann = data_el - free_name = self.net.get_free_name(img_desc, self.out_project.name) - new_dataset_name = img_desc.get_res_ds_name() - - if self.settings.get("visualize"): - out_meta = self.output_meta - out_meta: sly.ProjectMeta - cls_mapping = {} - for obj_class in out_meta.obj_classes: - color = obj_class.color - if color is None: - color = sly.color.random_rgb() - cls_mapping[obj_class.name] = color - - # hack to draw 'black' regions - cls_mapping = {k: (1, 1, 1) if max(v) == 0 else v for k, v in cls_mapping.items()} - - vis_img = self.draw_colored_mask(ann, cls_mapping) - orig_img = img_desc.read_image() - comb_img = imaging.overlay_images(orig_img, vis_img, 0.5) - - sep = np.array([[[0, 255, 0]]] * orig_img.shape[0], dtype=np.uint8) - img = np.hstack((orig_img, sep, comb_img)) - - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - output_img_path = osp.join( - self.output_folder, - self.out_project.name, - new_dataset_name, - "visualize", - free_name + ".png", - ) - os_utils.ensure_base_path(output_img_path) - cv2.imwrite(output_img_path, img) - - dataset_name = img_desc.get_res_ds_name() - if not self.out_project.datasets.has_key(dataset_name): - self.out_project.create_dataset(dataset_name) - out_dataset = self.out_project.datasets.get(dataset_name) - - out_item_name = free_name + img_desc.get_image_ext() - - # net _always_ downloads images - if img_desc.need_write(): - out_dataset: Dataset - out_dataset.add_item_np(out_item_name, img_desc.image_data, ann=ann) - else: - out_dataset.add_item_file(out_item_name, img_desc.get_img_path(), ann=ann) + + if not self.net.preview_mode: + free_name = self.net.get_free_name(img_desc, self.out_project.name) + new_dataset_name = img_desc.get_res_ds_name() + + if self.settings.get("visualize"): + out_meta = self.output_meta + out_meta: sly.ProjectMeta + cls_mapping = {} + for obj_class in out_meta.obj_classes: + color = obj_class.color + if color is None: + color = sly.color.random_rgb() + cls_mapping[obj_class.name] = color + + # hack to draw 'black' regions + cls_mapping = {k: (1, 1, 1) if max(v) == 0 else v for k, v in cls_mapping.items()} + + vis_img = self.draw_colored_mask(ann, cls_mapping) + orig_img = img_desc.read_image() + comb_img = imaging.overlay_images(orig_img, vis_img, 0.5) + + sep = np.array([[[0, 255, 0]]] * orig_img.shape[0], dtype=np.uint8) + img = np.hstack((orig_img, sep, comb_img)) + + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + output_img_path = osp.join( + self.output_folder, + self.out_project.name, + new_dataset_name, + "visualize", + free_name + ".png", + ) + os_utils.ensure_base_path(output_img_path) + cv2.imwrite(output_img_path, img) + + dataset_name = img_desc.get_res_ds_name() + if not self.out_project.datasets.has_key(dataset_name): + self.out_project.create_dataset(dataset_name) + out_dataset = self.out_project.datasets.get(dataset_name) + + out_item_name = free_name + img_desc.get_image_ext() + + # net _always_ downloads images + if img_desc.need_write(): + out_dataset: Dataset + out_dataset.add_item_np(out_item_name, img_desc.image_data, ann=ann) + else: + out_dataset.add_item_file(out_item_name, img_desc.get_img_path(), ann=ann) yield ([img_desc, ann],) diff --git a/src/compute/layers/save/SaveMasksLayer.py b/src/compute/layers/save/SaveMasksLayer.py index a6fd36ef..de1ca0ea 100644 --- a/src/compute/layers/save/SaveMasksLayer.py +++ b/src/compute/layers/save/SaveMasksLayer.py @@ -111,6 +111,8 @@ def validate(self): ) def preprocess(self): + if self.net.preview_mode: + return dst = self.dsts[0] self.out_project = sly.Project( directory=f"{self.output_folder}/{dst}", mode=sly.OpenMode.CREATE @@ -127,55 +129,58 @@ def preprocess(self): def process(self, data_el: Tuple[ImageDescriptor, sly.Annotation]): img_desc, ann = data_el - free_name = self.net.get_free_name(img_desc, self.out_project.name) - new_dataset_name = img_desc.get_res_ds_name() + if not self.net.preview_mode: + free_name = self.net.get_free_name(img_desc, self.out_project.name) + new_dataset_name = img_desc.get_res_ds_name() - for out_dir, flag_name, mapping_name in self.odir_flag_mapping: - if not self.settings[flag_name]: - continue - cls_mapping = self.settings[mapping_name] + for out_dir, flag_name, mapping_name in self.odir_flag_mapping: + if not self.settings[flag_name]: + continue + cls_mapping = self.settings[mapping_name] - # hack to draw 'black' regions - if flag_name == "masks_human": - cls_mapping = {k: (1, 1, 1) if max(v) == 0 else v for k, v in cls_mapping.items()} + # hack to draw 'black' regions + if flag_name == "masks_human": + cls_mapping = { + k: (1, 1, 1) if max(v) == 0 else v for k, v in cls_mapping.items() + } - img = self.draw_colored_mask(ann, cls_mapping) + img = self.draw_colored_mask(ann, cls_mapping) - if flag_name == "masks_human": - orig_img = img_desc.read_image() - comb_img = self.overlay_images(orig_img, img, 0.5) + if flag_name == "masks_human": + orig_img = img_desc.read_image() + comb_img = self.overlay_images(orig_img, img, 0.5) - sep = np.array([[[0, 255, 0]]] * orig_img.shape[0], dtype=np.uint8) - img = np.hstack((orig_img, sep, comb_img)) + sep = np.array([[[0, 255, 0]]] * orig_img.shape[0], dtype=np.uint8) + img = np.hstack((orig_img, sep, comb_img)) - img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - output_img_path = osp.join( - self.out_project.directory, new_dataset_name, out_dir, free_name + ".png" - ) + img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) + output_img_path = osp.join( + self.out_project.directory, new_dataset_name, out_dir, free_name + ".png" + ) - dst_dir = osp.split(output_img_path)[0] + dst_dir = osp.split(output_img_path)[0] - def ensure_dir(dst_dir): - if not osp.exists(dst_dir): - parent, _ = osp.split(dst_dir) - ensure_dir(parent) - os.mkdir(dst_dir) + def ensure_dir(dst_dir): + if not osp.exists(dst_dir): + parent, _ = osp.split(dst_dir) + ensure_dir(parent) + os.mkdir(dst_dir) - ensure_dir(dst_dir) + ensure_dir(dst_dir) - cv2.imwrite(output_img_path, img) + cv2.imwrite(output_img_path, img) - dataset_name = img_desc.get_res_ds_name() - if not self.out_project.datasets.has_key(dataset_name): - self.out_project.create_dataset(dataset_name) - out_dataset = self.out_project.datasets.get(dataset_name) + dataset_name = img_desc.get_res_ds_name() + if not self.out_project.datasets.has_key(dataset_name): + self.out_project.create_dataset(dataset_name) + out_dataset = self.out_project.datasets.get(dataset_name) - out_item_name = free_name + img_desc.get_image_ext() + out_item_name = free_name + img_desc.get_image_ext() - # net _always_ downloads images - if img_desc.need_write(): - out_dataset.add_item_np(out_item_name, img_desc.image_data, ann=ann) - else: - out_dataset.add_item_file(out_item_name, img_desc.get_img_path(), ann=ann) + # net _always_ downloads images + if img_desc.need_write(): + out_dataset.add_item_np(out_item_name, img_desc.image_data, ann=ann) + else: + out_dataset.add_item_file(out_item_name, img_desc.get_img_path(), ann=ann) yield ([img_desc, ann],) diff --git a/src/compute/layers/save/SuperviselyLayer.py b/src/compute/layers/save/SuperviselyLayer.py index 01331e82..05282dbe 100644 --- a/src/compute/layers/save/SuperviselyLayer.py +++ b/src/compute/layers/save/SuperviselyLayer.py @@ -29,6 +29,8 @@ def validate_dest_connections(self): raise ValueError("Destination name in '{}' layer is empty!".format(self.action)) def preprocess(self): + if self.net.preview_mode: + return dst = self.dsts[0] self.out_project_name = dst @@ -50,14 +52,17 @@ def get_or_create_dataset(self, dataset_name): def process(self, data_el: Tuple[ImageDescriptor, sly.Annotation]): img_desc, ann = data_el - dataset_name = img_desc.get_res_ds_name() - out_item_name = ( - self.net.get_free_name(img_desc, self.out_project_name) + img_desc.get_image_ext() - ) + if not self.net.preview_mode: + dataset_name = img_desc.get_res_ds_name() + out_item_name = ( + self.net.get_free_name(img_desc, self.out_project_name) + img_desc.get_image_ext() + ) - if self.sly_project_info is not None: - dataset_info = self.get_or_create_dataset(dataset_name) - image_info = g.api.image.upload_np(dataset_info.id, out_item_name, img_desc.image_data) - g.api.annotation.upload_ann(image_info.id, ann) + if self.sly_project_info is not None: + dataset_info = self.get_or_create_dataset(dataset_name) + image_info = g.api.image.upload_np( + dataset_info.id, out_item_name, img_desc.read_image() + ) + g.api.annotation.upload_ann(image_info.id, ann) yield ([img_desc, ann],) diff --git a/src/compute/main.py b/src/compute/main.py index 682333ca..d6ce310f 100644 --- a/src/compute/main.py +++ b/src/compute/main.py @@ -1,25 +1,18 @@ # coding: utf-8 import os -import re -from src.utils import LegacyProjectItem +import supervisely as sly from supervisely import sly_logger -from supervisely.app.widgets.sly_tqdm.sly_tqdm import CustomTqdm, Progress +from supervisely.app.widgets.sly_tqdm.sly_tqdm import Progress from supervisely.sly_logger import logger, EventType from src.compute.dtl_utils.dtl_helper import DtlHelper, DtlPaths -from src.compute.dtl_utils.image_descriptor import ImageDescriptor - from src.compute.tasks import task_helpers -from src.compute.tasks import progress_counter - -from src.compute.utils import json_utils from src.compute.utils import logging_utils - -import supervisely as sly - from src.compute.Net import Net +from src.exceptions import CustomException +from src.utils import LegacyProjectItem def make_legacy_project_item(project: sly.Project, dataset, item_name): @@ -79,6 +72,10 @@ def main(progress: Progress): net.calc_metas() net.preprocess() datasets_conflict_map = calculate_datasets_conflict_map(helper) + except CustomException as e: + logger.error("Error occurred on DTL-graph initialization step!") + e.log() + raise e except Exception as e: logger.error("Error occurred on DTL-graph initialization step!") raise e diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 00000000..c89bdb04 --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,117 @@ +from typing import Optional +import traceback + +from supervisely import logger +from supervisely import ProjectMeta + + +class CustomException(Exception): + def __init__( + self, message: str, error: Optional[Exception] = None, extra: Optional[dict] = None + ): + super().__init__(message) + self.message = message + self.error = error + self.extra = extra + + def __str__(self): + return self.message + + def log(self): + exc_info = ( + traceback.format_tb(self.error.__traceback__) + if self.error + else traceback.format_tb(self.__traceback__) + ) + logger.error(self.message, exc_info=exc_info, extra=self.extra) + + +class ActionNotFoundError(CustomException): + def __init__(self, action_name: str, extra: Optional[dict] = {}): + self.action_name = action_name + extra["action_name"] = action_name + super().__init__("Action not found", extra=extra) + + +class CreateLayerError(CustomException): + def __init__(self, action_name: str, error: Exception, extra: Optional[dict] = {}): + self.action_name = action_name + extra["action_name"] = action_name + super().__init__(f"Error creating Layer", error=error, extra=extra) + + +class LayerNotFoundError(CustomException): + def __init__(self, layer_id: str, extra: Optional[dict] = {}): + self.layer_id = layer_id + extra["layer_id"] = layer_id + super().__init__("Layer not found", extra=extra) + + +class CreateNodeError(CustomException): + def __init__(self, layer_name, error: Exception, extra: Optional[dict] = {}): + self.layer_name = layer_name + extra["layer_name"] = layer_name + super().__init__(f"Error creating Node", error=error, extra=extra) + + +class UnexpectedError(CustomException): + def __init__( + self, message: str = "Unexpected error", error: Exception = None, extra: Optional[dict] = {} + ): + super().__init__(message, error=error, extra=extra) + + +class UpdateMetaError(CustomException): + def __init__( + self, + layer_name: str, + project_meta: ProjectMeta, + error: Exception, + extra: Optional[dict] = {}, + ): + self.layer_name = layer_name + self.project_meta = project_meta + extra["layer_name"] = layer_name + extra["project_meta"] = project_meta.to_json() + super().__init__( + f"Error updating project meta", + error=error, + extra=extra, + ) + + +class BadSettingsError(CustomException): + def __init__( + self, + message, + error: Exception = None, + extra: Optional[dict] = {}, + ): + message = "Bad settings. " + message + super().__init__(message, error, extra=extra) + + +class GraphError(CustomException): + def __init__(self, message, error: Exception = None, extra: Optional[dict] = {}): + message = "Graph Error. " + message + super().__init__(message, error=error, extra=extra) + + +class CreateMetaError(CustomException): + def __init__(self, message, error: Exception = None, extra: Optional[dict] = {}): + message = "Create Meta Error. " + message + super().__init__(message, error=error, extra=extra) + + +def handle_exception(func): + """Decorator to log exception and silence it""" + + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except CustomException as e: + e.log() + except Exception as e: + logger.error("Unexpected error", exc_info=traceback.format_exc()) + + return inner diff --git a/src/globals.py b/src/globals.py index ff44bd81..fc1fac28 100644 --- a/src/globals.py +++ b/src/globals.py @@ -1,4 +1,5 @@ import os +import queue from dotenv import load_dotenv import supervisely as sly @@ -29,3 +30,14 @@ layers_count = 0 layers = {} + + +update_queue = queue.Queue() + + +def updater(update: str): + global update_queue + update_queue.put(update) + + +context_menu_position = None diff --git a/src/main.py b/src/main.py index aff9a189..93c64a42 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,44 @@ import shutil import os +import threading +import time from supervisely import Application from src.ui.ui import layout +from src.ui.tabs.configure import update_metas, update_nodes +from src.ui.tabs.json_preview import load_json import src.globals as g shutil.rmtree(g.STATIC_DIR, ignore_errors=True) os.mkdir(g.STATIC_DIR) app = Application(layout=layout, static_dir=g.STATIC_DIR) + + +def _update_f(): + while True: + updates = [] + while not g.update_queue.empty(): + updates.append(g.update_queue.get()) + if len(updates) == 0: + time.sleep(0.1) + continue + try: + if "load_json" in updates: + load_json() + elif "nodes" in updates: + update_nodes() + else: + update_metas() + finally: + for _ in range(len(updates)): + g.update_queue.task_done() + time.sleep(0.1) + + +update_loop = threading.Thread( + target=_update_f, + name="App update loop", + daemon=True, +) + +update_loop.start() diff --git a/src/ui/dtl/Action.py b/src/ui/dtl/Action.py index 560ec092..6d290550 100644 --- a/src/ui/dtl/Action.py +++ b/src/ui/dtl/Action.py @@ -2,7 +2,6 @@ from typing import Optional from supervisely.app.widgets import NodesFlow, Container, Text -import src.globals as g class Action: @@ -10,12 +9,10 @@ class Action: title = None docs_url = None description = None + md_description = "" width = 340 - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {} + header_color = None + header_text_color = None @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -23,18 +20,48 @@ def create_new_layer(cls, layer_id: Optional[str] = None): @classmethod def create_inputs(cls): - return [NodesFlow.Node.Input("source", "Source")] + return [NodesFlow.Node.Input("source", "Input", color="#000000")] @classmethod def create_outputs(cls): - return [NodesFlow.Node.Output("destination", "Destination")] + return [NodesFlow.Node.Output("destination", "Output", color="#000000")] @classmethod def create_info_widget(cls): return Container( widgets=[ - Text(f"

{cls.title}

", color="white"), - Text(f'Docs'), - Text(f"

{cls.description}

", color="white"), + Text(f"

{cls.title}

"), + Text(f'Docs'), + Text(f"

{cls.description}

"), ] ) + + +class SourceAction(Action): + header_color = "#13ce66" + header_text_color = "#000000" + + +class PixelLevelAction(Action): + header_color = "#c9a5fa" + header_text_color = "#000000" + + +class SpatialLevelAction(Action): + header_color = "#fcd068" + header_text_color = "#000000" + + +class AnnotationAction(Action): + header_color = "#90ddf5" + header_text_color = "#000000" + + +class OtherAction(Action): + header_color = "#cfcfcf" + header_text_color = "#000000" + + +class OutputAction(Action): + header_color = "#ff5e90" + header_text_color = "#000000" diff --git a/src/ui/dtl/Layer.py b/src/ui/dtl/Layer.py index 95d5ae20..2d051163 100644 --- a/src/ui/dtl/Layer.py +++ b/src/ui/dtl/Layer.py @@ -1,40 +1,48 @@ -import copy +import time +from typing import Optional +import random + from supervisely import Annotation -from supervisely.app.widgets import LabeledImage, NodesFlow +from supervisely.app.widgets import ( + LabeledImage, + NodesFlow, + Markdown, + Button, + Text, +) from supervisely.imaging.image import write as write_image -from src.ui.dtl.Action import Action - -import numpy as np - - -import random -from typing import List, Optional +from src.ui.dtl.Action import Action +from src.ui.dtl.utils import ( + get_separator, + get_set_settings_button_style, + get_set_settings_container, +) +import src.globals as g +from src.compute.dtl_utils.image_descriptor import ImageDescriptor class Layer: def __init__( self, action: Action, - options: List[NodesFlow.Node.Option], - get_settings: callable, + create_options: callable, get_src: Optional[callable] = None, - meta_changed_cb: Optional[callable] = None, + get_settings: Optional[callable] = None, get_dst: Optional[callable] = None, - set_settings_from_json: callable = None, + meta_changed_cb: Optional[callable] = None, id: Optional[str] = None, ): self.action = action - self._id = id - if self._id is None: - self._id = action.name + "_" + "".join(random.choice("0123456789") for _ in range(8)) + self.id = id + if self.id is None: + self.id = action.name + "_" + "".join(random.choice("0123456789") for _ in range(8)) - self._options = options + self._create_options = create_options self._get_settings = get_settings self._get_src = get_src - self._meta_changed_cb = meta_changed_cb self._get_dst = get_dst - self._set_settings_from_json = set_settings_from_json + self._meta_changed_cb = meta_changed_cb self._src = [] self._settings = {} @@ -42,38 +50,61 @@ def __init__( self.output_meta = None - self._preview_img_url = f"static/{self._id}.jpg" + md_description = self.action.md_description.replace( + r"../../assets", r"https://raw.githubusercontent.com/supervisely/docs/master/assets" + ) + + # info option + self._info_option = NodesFlow.Node.Option( + name="sidebarNodeInfo", + option_component=NodesFlow.SidebarNodeInfoOptionComponent( + sidebar_template=Markdown(md_description).to_html(), + sidebar_width=600, + ), + ) + # preview option + self._preview_img_url = f"static/{self.id}.jpg" self._ann = None + self._img_desc = None + self._preview_widget = LabeledImage(enable_zoom=True) + self._update_preview_button = Button( + text="Update", + icon="zmdi zmdi-refresh", + button_type="text", + button_size="small", + style=get_set_settings_button_style(), + ) - self._add_info_option() - self._add_preview_option() + @self._update_preview_button.click + def _update_preview_btn_click_cb(): + g.updater("nodes") - def _add_info_option(self): - self._options = [ + self._preview_options = [ + # NodesFlow.Node.Option( + # name="preview_text", option_component=NodesFlow.TextOptionComponent("Preview") + # ), NodesFlow.Node.Option( - name="Info", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - self.action.create_info_widget() - ) + name="update_preview_btn", + option_component=NodesFlow.WidgetOptionComponent( + widget=get_set_settings_container(Text("Preview"), self._update_preview_button) ), ), - *self._options, - ] - - def _add_preview_option(self): - self._preview_widget = LabeledImage(enable_zoom=True) - self._options = [ - *self._options, - NodesFlow.Node.Option( - name="preview_text", option_component=NodesFlow.TextOptionComponent("Preview") - ), NodesFlow.Node.Option( name="preview", option_component=NodesFlow.WidgetOptionComponent(widget=self._preview_widget), ), ] + def get_src(self) -> list: + return self._src + + def get_dst(self) -> list: + return self._dst + + def get_settings(self) -> dict: + return self._settings + + # JSON def to_json(self) -> dict: return { "action": self.action.name, @@ -82,103 +113,130 @@ def to_json(self) -> dict: "settings": self._settings, } - def get_destination_name(self, dst_index: int): - outputs = self.action.create_outputs() - return outputs[dst_index].name - - def set_settings_from_json(self, json_data: dict, node_state: dict): - node_state = copy.deepcopy(node_state) - settings = json_data["settings"] - for settings_key, value in settings.items(): - node_state_key = self.action._settings_mapping.get(settings_key, settings_key) - if node_state_key is not None: - node_state[node_state_key] = value - if self._set_settings_from_json is not None: - node_state = self._set_settings_from_json(json_data, node_state) - return node_state - + def from_json(self, json_data: dict = {}) -> None: + """Init src, dst and settings from json data""" + src = json_data.get("src", []) + if isinstance(src, str): + src = [src] + self._src = src + dst = json_data.get("dst", []) + if isinstance(dst, str): + dst = [dst] + self._dst = dst + self._settings = json_data.get("settings", {}) + + # NodesFlow.Node def create_node(self) -> NodesFlow.Node: + """creates node from src, dst and settings""" self._inputs = self.action.create_inputs() self._outputs = self.action.create_outputs() + options = self._create_options(src=self._src, dst=self._dst, settings=self._settings) + + def combine_options(options: list): + result_options = [ + self._info_option, + get_separator(0), + ] + if len(options["src"]) > 0: + result_options.extend(options["src"]) + result_options.append(get_separator(1)) + + if len(options["dst"]) > 0: + result_options.extend(options["dst"]) + result_options.append(get_separator(2)) + + if len(options["settings"]) > 0: + result_options.extend(options["settings"]) + result_options.append(get_separator(3)) + + return [ + *result_options, + *self._preview_options, + ] + return NodesFlow.Node( - id=self._id, + id=self.id, name=self.action.title, width=self.action.width, - options=self._options, + options=combine_options(options), inputs=self._inputs, outputs=self._outputs, + inputs_up=True, + header_color=self.action.header_color, + header_text_color=self.action.header_text_color, ) - def update_src(self, node_options: dict): - if self._get_src is not None: - self._src = self._get_src(options_json=node_options) - else: - self._src = [] - - def update_dst(self, node_options: dict): - if self._get_dst is not None: - self._dst = self._get_dst(options_json=node_options) - else: - self._dst = self._create_destinations() - - def update_settings(self, node_options: dict): - if self._get_settings is not None: - self._settings = self._get_settings(options_json=node_options) - else: - self._settings = {} - def parse_options(self, node_options: dict): - self.update_src(node_options) - self.update_dst(node_options) - self.update_settings(node_options) + """Read node options and init src, dst and settings""" + self._update_src(node_options) + self._update_dst(node_options) + self._update_settings(node_options) def add_source(self, from_node_id, from_node_interface): src_name = self._connection_name(from_node_id, from_node_interface) self._src.append(src_name) + def clear_preview(self): + self._preview_widget.clean_up() + + def get_preview_img_desc(self): + return self._img_desc + + def update_preview(self, img_desc: ImageDescriptor, ann: Annotation): + self._img_desc = img_desc + write_image(self._preview_img_url, img_desc.read_image()) + self._ann = ann + self._preview_widget.set( + title=None, image_url=f"{self._preview_img_url}?{time.time()}", ann=self._ann + ) + + def set_preview_loading(self, val: bool): + self._preview_widget.loading = val + self._update_preview_button.loading = val + + def get_ann(self): + return self._ann + + def update_project_meta(self, project_meta): + if self._meta_changed_cb is not None: + self._meta_changed_cb(project_meta) + + # Utils + def get_destination_name(self, dst_index: int): + outputs = self.action.create_outputs() + return outputs[dst_index].name + def _connection_name(self, name: str, interface: str): interface_str = "_".join( [ *[ part for part in interface.split("_") - if part not in ["", "source", "destination"] + if part not in ["", "source", "destination", "input", "output"] ], ] ) return "$" + name + (f"__{interface_str}" if interface_str else "") def _create_destinations(self): - return [self._connection_name(self._id, output.name) for output in self._outputs] - - def clear_sources(self): - self._src = [] - - def clear_destinations(self): - self._dst = [] + return [self._connection_name(self.id, output.name) for output in self._outputs] - def clear_settings(self): - self._settings = {} - - def clear(self): - self.clear_sources() - self.clear_destinations() - self.clear_settings() - - def get_src(self): - return self._src - - def get_dst(self): - return self._dst - - def set_preview(self, img: np.ndarray, ann: Annotation): - write_image(self._preview_img_url, img) - self._ann = ann - self._preview_widget.set(title=None, image_url=self._preview_img_url, ann=self._ann) + def _update_src(self, node_options: dict): + if self._get_src is not None: + self._src = self._get_src(options_json=node_options) + else: + self._src = [] - def get_ann(self): - return self._ann + def _update_dst(self, node_options: dict): + """Read node options and init dst""" + if self._get_dst is not None: + self._dst = self._get_dst(options_json=node_options) + else: + self._dst = self._create_destinations() - def meta_changed_cb(self, project_meta): - if self._meta_changed_cb is not None: - self._meta_changed_cb(project_meta) + def _update_settings(self, node_options: dict): + """Read node options and init settings""" + if self._get_settings is not None: + self._settings = self._get_settings(options_json=node_options) + else: + self._settings = {} diff --git a/src/ui/dtl/__init__.py b/src/ui/dtl/__init__.py index 725f77d2..4919759a 100644 --- a/src/ui/dtl/__init__.py +++ b/src/ui/dtl/__init__.py @@ -1,4 +1,12 @@ -from .Action import Action +from .Action import ( + Action, + SourceAction, + PixelLevelAction, + SpatialLevelAction, + AnnotationAction, + OtherAction, + OutputAction, +) from .actions.data import DataAction from .actions.approx_vector import ApproxVectorAction from .actions.background import BackgroundAction @@ -17,7 +25,7 @@ from .actions.dummy import DummyAction from .actions.duplicate_objects import DuplicateObjectsAction from .actions.find_contours import FindContoursAction -from .actions.flip import FlipAction +from .actions.flip.flip import FlipAction from .actions.if_action import IfAction from .actions.instances_crop import InstancesCropAction from .actions.line2bitmap import LineToBitmapAction @@ -30,6 +38,7 @@ from .actions.rename import RenameAction from .actions.rasterize import RasterizeAction from .actions.resize import ResizeAction +from .actions.rotate import RotateAction from .actions.skeletonize import SkeletonizeAction from .actions.sliding_window import SlidingWindowAction from .actions.split_masks import SplitMasksAction @@ -39,49 +48,60 @@ from .actions.supervisely import SuperviselyAction -DATA_ACTIONS = "Data actions" -TRANSFORMATION_ACTIONS = "Transformation actions" +SOURCE_ACTIONS = "Source actions" +# TRANSFORMATION_ACTIONS = "Transformation actions" +PIXEL_LEVEL_TRANSFORMS = "Pixel-level transforms" +SPATIAL_LEVEL_TRANSFORMS = "Spatial-level transforms" +ANNOTATION_TRANSFORMS = "Annotation transforms" +OTHER = "Other" SAVE_ACTIONS = "Save actions" actions_list = { - DATA_ACTIONS: [DataAction.name], - TRANSFORMATION_ACTIONS: [ + SOURCE_ACTIONS: [DataAction.name], + PIXEL_LEVEL_TRANSFORMS: [ + BlurAction.name, + ContrastBrightnessAction.name, + NoiseAction.name, + ], + SPATIAL_LEVEL_TRANSFORMS: [ + CropAction.name, + FlipAction.name, + InstancesCropAction.name, + MultiplyAction.name, + ResizeAction.name, + RotateAction.name, + SlidingWindowAction.name, + ], + ANNOTATION_TRANSFORMS: [ ApproxVectorAction.name, BackgroundAction.name, BBoxAction.name, BboxToPolyAction.name, Bitmap2LinesAction.name, BitwiseMasksAction.name, - BlurAction.name, ColorClassAction.name, - ContrastBrightnessAction.name, - CropAction.name, - DatasetAction.name, DropByClassAction.name, DropLinesByLengthAction.name, DropNoiseAction.name, - DummyAction.name, DuplicateObjectsAction.name, FindContoursAction.name, - FlipAction.name, - IfAction.name, - InstancesCropAction.name, LineToBitmapAction.name, MergeBitmapsAction.name, - MultiplyAction.name, - NoiseAction.name, ObjectsFilterAction.name, PolygonToBitmapAction.name, RandomColorsAction.name, RasterizeAction.name, RenameAction.name, - ResizeAction.name, SkeletonizeAction.name, - SlidingWindowAction.name, SplitMasksAction.name, TagAction.name, ], + OTHER: [ + DatasetAction.name, + DummyAction.name, + IfAction.name, + ], SAVE_ACTIONS: [ SaveAction.name, SaveMasksAction.name, @@ -123,6 +143,7 @@ RasterizeAction.name: RasterizeAction, RenameAction.name: RenameAction, ResizeAction.name: ResizeAction, + RotateAction.name: RotateAction, SkeletonizeAction.name: SkeletonizeAction, SlidingWindowAction.name: SlidingWindowAction, SplitMasksAction.name: SplitMasksAction, diff --git a/src/ui/dtl/actions/approx_vector.py b/src/ui/dtl/actions/approx_vector.py index fc4cf0f3..5156a6c1 100644 --- a/src/ui/dtl/actions/approx_vector.py +++ b/src/ui/dtl/actions/approx_vector.py @@ -1,49 +1,41 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta from supervisely import Polygon, Polyline, AnyGeometry -from src.ui.dtl import Action + +import src.globals as g +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview +from src.ui.dtl.utils import get_separator -class ApproxVectorAction(Action): +class ApproxVectorAction(AnnotationAction): name = "approx_vector" title = "Approx Vector" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/approx_vector" ) description = "This layer (approx_vector) approximates vector figures: lines and polygons. The operation decreases number of vertices with Douglas-Peucker algorithm." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_button = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [obj_class.name for obj_class in classes.get_selected_classes()], + "classes": saved_classes_settings, "epsilon": options_json["epsilon"], } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] - obj_class_names = settings["classes"] - classes.loading = True - classes.select(obj_class_names) - classes.loading = False - node_state["epsilon"] = settings["epsilon"] - return node_state - def meta_changed_cb(project_meta: ProjectMeta): + nonlocal saved_classes_settings classes.loading = True classes.set( [ @@ -52,40 +44,67 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Polygon, Polyline, AnyGeometry] ] ) + saved_classes_settings = [ + obj_class.name for obj_class in classes.get_selected_classes() + ] + classes.loading = False + + def _save_classes_setting(): + nonlocal saved_classes_settings + selected_classes = classes.get_selected_classes() + saved_classes_settings = [obj_class.name for obj_class in selected_classes] + classes_preview.set(selected_classes) + g.updater("metas") + + classes_save_button.click(_save_classes_setting) + + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) + classes.loading = True + classes.select(obj_class_names) + _save_classes_setting() classes.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes"), - ), - NodesFlow.Node.Option( - name="Select Classes", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + epsilon_val = settings.get("epsilon", 3) + if epsilon_val < 1: + raise ValueError("Epsilon must be greater than 0") + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_button]) + ) + ), + ), + NodesFlow.Node.Option( + name="classes_preview_text", + option_component=NodesFlow.WidgetOptionComponent(classes_preview), + ), + get_separator(10), + NodesFlow.Node.Option( + name="epsilon_text", + option_component=NodesFlow.TextOptionComponent("Epsilon"), ), - ), - NodesFlow.Node.Option( - name="epsilon_text", - option_component=NodesFlow.TextOptionComponent("Epsilon"), - ), - NodesFlow.Node.Option( - name="epsilon", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=3), - ), - ] + NodesFlow.Node.Option( + name="epsilon", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=epsilon_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - set_settings_from_json=set_settings_from_json, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - id=layer_id, ) diff --git a/src/ui/dtl/actions/background.py b/src/ui/dtl/actions/background.py index 06201918..a67f2df3 100644 --- a/src/ui/dtl/actions/background.py +++ b/src/ui/dtl/actions/background.py @@ -1,10 +1,12 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -class BackgroundAction(Action): +class BackgroundAction(AnnotationAction): name = "background" title = "Background" docs_url = ( @@ -20,24 +22,26 @@ def get_settings(options_json: dict) -> dict: "class": options_json["class"] if options_json["class"] else "", } - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="class_text", - option_component=NodesFlow.TextOptionComponent("Background Class name"), - ), - NodesFlow.Node.Option(name="class", option_component=NodesFlow.InputOptionComponent()), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + class_val = settings.get("class", "") + settings_options = [ + NodesFlow.Node.Option( + name="class_text", + option_component=NodesFlow.TextOptionComponent("Background Class name"), + ), + NodesFlow.Node.Option( + name="class", option_component=NodesFlow.InputOptionComponent(class_val) + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/bbox.py b/src/ui/dtl/actions/bbox.py index b37525b5..606869ad 100644 --- a/src/ui/dtl/actions/bbox.py +++ b/src/ui/dtl/actions/bbox.py @@ -1,94 +1,166 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox from supervisely import ProjectMeta -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) +import src.globals as g -class BBoxAction(Action): +class BBoxAction(AnnotationAction): name = "bbox" title = "Bounding Box" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox" description = "Bounding Box layer (bbox) converts annotations of specified classes to bounding boxes. Annotations would be replaced with new objects of shape rectangle." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - classes_mapping_widget.loading = True - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) - classes_mapping_widget.loading = False - return node_state - def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True + old_obj_classes = classes_mapping_widget.get_classes() + + # set classes to widget classes_mapping_widget.set(project_meta.obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + project_meta.obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, + ) + + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Classes Mapping"), - ), - NodesFlow.Node.Option( - name="Set Classes Mapping", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ) + ), + ), + NodesFlow.Node.Option( + name="Classes Mapping Preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bbox2poly.py b/src/ui/dtl/actions/bbox2poly.py index 7b41ac14..331c4f7e 100644 --- a/src/ui/dtl/actions/bbox2poly.py +++ b/src/ui/dtl/actions/bbox2poly.py @@ -1,102 +1,173 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox from supervisely import ProjectMeta, Rectangle, AnyGeometry -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) +import src.globals as g -class BboxToPolyAction(Action): +class BboxToPolyAction(AnnotationAction): name = "bbox2poly" title = "BBox to Polygon" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox2poly" ) description = 'This layer (bbox2poly) converts rectangles ("bounding boxes") to polygons.' - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Rectangle, AnyGeometry] - ] + old_obj_classes = classes_mapping_widget.get_classes() + new_obj_classes = [ + obj_class + for obj_class in project_meta.obj_classes + if obj_class.geometry_type in [Rectangle, AnyGeometry] + ] + + # set classes to widget + classes_mapping_widget.set(new_obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + new_obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, ) - classes_mapping_widget.loading = False - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - classes_mapping_widget.loading = True - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="class_text", - option_component=NodesFlow.TextOptionComponent("Class"), - ), - NodesFlow.Node.Option( - name="Set Classes", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ), + ), + ), + NodesFlow.Node.Option( + name="Classes Mapping Preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bitmap2lines.py b/src/ui/dtl/actions/bitmap2lines.py index 0f0b40eb..206202df 100644 --- a/src/ui/dtl/actions/bitmap2lines.py +++ b/src/ui/dtl/actions/bitmap2lines.py @@ -1,117 +1,182 @@ -import copy -import json from typing import Optional + from supervisely import ProjectMeta, Bitmap, AnyGeometry -from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox + +import src.globals as g +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) -class Bitmap2LinesAction(Action): +class Bitmap2LinesAction(AnnotationAction): name = "bitmap2lines" title = "Bitmap to Lines" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitmap2lines" ) description = "This layer (bitmap2lines) converts thinned (skeletonized) bitmaps to lines. It is extremely useful if you have some raster objects representing lines or edges, maybe forming some tree or net structure, and want to work with vector objects. Each input bitmap should be already thinned (use Skeletonize layer to do it), and for single input mask a number of lines will be produced. Resulting lines may have very many vertices, so consider applying Approx Vector layer to results of this layer. Internally the layer builds a graph of 8-connected pixels, determines minimum spanning tree(s), then greedely extracts diameters from connected components of the tree." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, "min_points_cnt": options_json["min_points_cnt"], } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) - return node_state - def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Bitmap, AnyGeometry] - ] + old_obj_classes = classes_mapping_widget.get_classes() + new_obj_classes = [ + obj_class + for obj_class in project_meta.obj_classes + if obj_class.geometry_type in [Bitmap, AnyGeometry] + ] + + # set classes to widget + classes_mapping_widget.set(new_obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + new_obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, ) + + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_mapping_text", - option_component=NodesFlow.TextOptionComponent("Classes Mapping"), - ), - NodesFlow.Node.Option( - name="Set Classes Mapping", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + min_points_cnt_val = settings.get("min_points_cnt", 2) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ) + ), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt_text", - option_component=NodesFlow.TextOptionComponent("Min Points Count."), - ), - NodesFlow.Node.Option( - name="min_points_cnt_description", - option_component=NodesFlow.TextOptionComponent( - "Min number of vertices for each output line. Other lines will be dropped." + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt", - option_component=NodesFlow.IntegerOptionComponent(min=2, default_value=2), - ), - ] + NodesFlow.Node.Option( + name="min_points_cnt", + option_component=NodesFlow.IntegerOptionComponent( + min=2, default_value=min_points_cnt_val + ), + ), + ] + + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bitwise_masks.py b/src/ui/dtl/actions/bitwise_masks.py index 298253d8..23148f72 100644 --- a/src/ui/dtl/actions/bitwise_masks.py +++ b/src/ui/dtl/actions/bitwise_masks.py @@ -1,44 +1,58 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview -class BitwiseMasksAction(Action): +class BitwiseMasksAction(AnnotationAction): name = "bitwise_masks" title = "Bitwise Masks" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitwise_masks" ) description = "Bitwise Masks - make bitwise operations between bitmap annotations." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "class_mask": None, - "classes_to_correct": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): class_mask_widget = ClassesList() classes_to_correct_widget = ClassesList(multiple=True) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + class_mask_preview = ClassesListPreview() + classes_to_correct_preview = ClassesListPreview() + + save_class_mask_btn = Button("Save", icon="zmdi zmdi-floppy") + save_classes_to_correct_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_class_mask_settings = "" + saved_classes_to_correct_settings = [] + + def _save_class_mask_setting(): + nonlocal saved_class_mask_settings try: - class_mask = class_mask_widget.get_selected_classes()[0].name + class_mask_obj = class_mask_widget.get_selected_classes()[0] + class_mask = class_mask_obj.name except: + class_mask_obj = None class_mask = "" + saved_class_mask_settings = class_mask + class_mask_preview.set([] if class_mask_obj is None else [class_mask_obj]) + + def _save_classes_to_correct_setting(): + nonlocal saved_classes_to_correct_settings + selected_classes = classes_to_correct_widget.get_selected_classes() + saved_classes_to_correct_settings = [cls.name for cls in selected_classes] + classes_to_correct_preview.set(selected_classes) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" return { "type": options_json["type"], - "class_mask": class_mask, - "classes_to_correct": [ - cls.name for cls in classes_to_correct_widget.get_selected_classes() - ], + "class_mask": saved_class_mask_settings, + "classes_to_correct": saved_classes_to_correct_settings, } def meta_changed_cb(project_meta: ProjectMeta): @@ -46,68 +60,85 @@ def meta_changed_cb(project_meta: ProjectMeta): classes_to_correct_widget.loading = True class_mask_widget.set(project_meta.obj_classes) classes_to_correct_widget.set(project_meta.obj_classes) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def _set_settings_from_json(settings): class_mask_widget.loading = True classes_to_correct_widget.loading = True - class_mask_widget.select([settings["class_mask"]]) - classes_to_correct_widget.select(settings["classes_to_correct"]) + class_mask_widget.select([settings["class_mask"]] if "class_mask" in settings else []) + classes_to_correct_widget.select( + settings["classes_to_correct"] if "classes_to_correct" in settings else [] + ) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - return node_state + + save_class_mask_btn.click(_save_class_mask_setting) + save_classes_to_correct_btn.click(_save_classes_to_correct_setting) type_items = [NodesFlow.SelectOptionComponent.Item(t, t) for t in ("nor", "and", "or")] - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="type_text", - option_component=NodesFlow.TextOptionComponent("Operation type"), - ), - NodesFlow.Node.Option( - name="type", - option_component=NodesFlow.SelectOptionComponent( - items=type_items, default_value=type_items[0].value + def create_options(src: list, dst: list, settings: dict) -> dict: + type_val = settings.get("type", "nor") + if type_val not in ("nor", "and", "or"): + raise ValueError("Type must be one of: nor, and, or") + + _set_settings_from_json(settings) + + settings_options = [ + NodesFlow.Node.Option( + name="type_text", + option_component=NodesFlow.TextOptionComponent("Operation type"), + ), + NodesFlow.Node.Option( + name="type", + option_component=NodesFlow.SelectOptionComponent( + items=type_items, default_value=type_val + ), ), - ), - NodesFlow.Node.Option( - name="class_mask_text", - option_component=NodesFlow.TextOptionComponent( - "Class Mask. First element of bitwise operation" + NodesFlow.Node.Option( + name="class_mask_text", + option_component=NodesFlow.TextOptionComponent( + "Class Mask. First element of bitwise operation" + ), ), - ), - NodesFlow.Node.Option( - name="Select Class Mask", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(class_mask_widget) + NodesFlow.Node.Option( + name="Select Class Mask", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[class_mask_widget, save_class_mask_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes to correct"), - ), - NodesFlow.Node.Option( - name="Select Classes to correct", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_to_correct_widget) + NodesFlow.Node.Option( + name="classes_text", + option_component=NodesFlow.TextOptionComponent("Classes to correct"), ), - ), - ] + NodesFlow.Node.Option( + name="Select Classes to correct", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container( + widgets=[classes_to_correct_widget, save_classes_to_correct_btn] + ) + ) + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/blur.py b/src/ui/dtl/actions/blur.py index 7e061062..b0665d06 100644 --- a/src/ui/dtl/actions/blur.py +++ b/src/ui/dtl/actions/blur.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely.app.widgets import ( NodesFlow, Container, @@ -8,25 +9,18 @@ Field, Flexbox, Text, + Button, ) -from src.ui.dtl import Action + +from src.ui.dtl import PixelLevelAction from src.ui.dtl.Layer import Layer -class BlurAction(Action): +class BlurAction(PixelLevelAction): name = "blur" title = "Blur" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/blur" description = 'Blur layer ("action": "blur") applies blur filter to the image. To use median blur (cv2.medianBlur) set name to median and kernel to odd number. To use gaussian blur (cv2.GaussianBlur) set name to gaussian and sigma to object with two numbers: min and max.' - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "name": None, - "kernel": None, - "sigma": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -49,20 +43,43 @@ def create_new_layer(cls, layer_id: Optional[str] = None): title="Sigma", content=Container( widgets=[ - Flexbox(widgets=[Text("Min", color="white"), sigma_min_input]), - Flexbox(widgets=[Text("Max", color="white"), sigma_max_input]), + Flexbox(widgets=[Text("Min"), sigma_min_input]), + Flexbox(widgets=[Text("Max"), sigma_max_input]), ] ), ), ), ] ) + save_settings_button = Button("Save", icon="zmdi zmdi-floppy") settings_widget = Container( - widgets=[Field(title="Blur type", content=select_name), OneOf(select_name)] + widgets=[ + Field(title="Blur type", content=select_name), + OneOf(select_name), + save_settings_button, + ] ) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + type_preview = Text("") + params_preview = Text("") + settings_preview = Container(widgets=[type_preview, params_preview], gap=1) + + saved_settings = {} + + def _update_preview(): + blur_type = saved_settings.get("name", "") + type_preview.text = f"Blur type: {blur_type}" + if blur_type == "": + params_preview.text = "" + elif blur_type == "median": + params_preview.text = f"kernel = {saved_settings.get('kernel')}" + elif blur_type == "gaussian": + params_preview.text = ( + f'sigma = {saved_settings["sigma"]["min"]} - {saved_settings["sigma"]["max"]}' + ) + + def _save_settings(): + nonlocal saved_settings settings = { "name": select_name.get_value(), } @@ -73,41 +90,52 @@ def get_settings(options_json: dict) -> dict: "min": sigma_min_input.get_value(), "max": sigma_max_input.get_value(), } - return settings + saved_settings = settings + _update_preview() - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return saved_settings + + def _set_settings_from_json(settings: dict): settings_widget.loading = True - select_name.set_value(settings["name"]) - if settings["name"] == "median": - kernel_input.value = settings["kernel"] + name = settings.get("name", "median") + select_name.set_value(name) + if name == "median": + kernel_input.value = settings.get("kernel", 5) else: - sigma_min_input.value = settings["sigma"]["min"] - sigma_max_input.value = settings["sigma"]["max"] + sigma_min_v = settings.get("sigma", {}).get("min", 3) + sigma_max_v = settings.get("sigma", {}).get("max", 50) + sigma_min_input.value = sigma_min_v + sigma_max_input.value = sigma_max_v + _save_settings() settings_widget.loading = False - return node_state - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="Set Settings", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(settings_widget) + save_settings_button.click(_save_settings) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Settings", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent(settings_widget) + ), + ), + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/color_class.py b/src/ui/dtl/actions/color_class.py index d62cb80e..705d7d2d 100644 --- a/src/ui/dtl/actions/color_class.py +++ b/src/ui/dtl/actions/color_class.py @@ -1,82 +1,92 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta -from src.ui.dtl import Action +from supervisely.imaging.color import hex2rgb, rgb2hex + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesColorMapping -from supervisely.imaging.color import hex2rgb +from src.ui.widgets import ClassesColorMapping, ClassesMappingPreview -class ColorClassAction(Action): +class ColorClassAction(AnnotationAction): name = "color_class" title = "Color Class" + description = "This layer (color_class) used for coloring classes as you wish. Add this class at the end of graph, before data saving." docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/color_class" ) - description = "This layer (color_class) used for coloring classes as you wish. Add this class at the end of graph, before data saving." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_color_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_colors = ClassesColorMapping() + classes_colors_preview = ClassesMappingPreview() + classes_colors_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_colors_settings = {} + + def _save_classes_colors_setting(): + nonlocal saved_classes_colors_settings + mapping = classes_colors.get_mapping() + saved_classes_colors_settings = { + cls_name: hex2rgb(value["value"]) for cls_name, value in mapping.items() + } + obj_classes = classes_colors.get_classes() + classes_colors_preview.set( + obj_classes, {k: rgb2hex(v) for k, v in saved_classes_colors_settings.items()} + ) def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" - return { - "classes_color_mapping": { - cls_name: hex2rgb(value["value"]) - for cls_name, value in classes_colors.get_mapping().items() - } - } + return {"classes_color_mapping": saved_classes_colors_settings} - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def meta_changed_cb(project_meta: ProjectMeta): + classes_colors.loading = True + classes_colors.set(project_meta.obj_classes) + _save_classes_colors_setting() + classes_colors.loading = False + + def _set_settings_from_json(settings: dict): + colors = settings.get("classes_color_mapping", {}) classes_colors.loading = True classes_colors.set_colors( [ - settings.get(cls, hex2rgb(value["value"])) + colors.get(cls, hex2rgb(value["value"])) for cls, value in classes_colors.get_mapping().items() ] ) + _save_classes_colors_setting() classes_colors.loading = False - return node_state - def meta_changed_cb(project_meta: ProjectMeta): - classes_colors.loading = True - classes_colors.set(project_meta.obj_classes) - classes_colors.loading = False + classes_colors_save_btn.click(_save_classes_colors_setting) - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_colors_text", - option_component=NodesFlow.TextOptionComponent("Classes Colors"), - ), - NodesFlow.Node.Option( - name="Set Colors", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_colors) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + + settings_options = [ + NodesFlow.Node.Option( + name="Set Colors", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes_colors, classes_colors_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="colors_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_colors_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/contrast_brightness.py b/src/ui/dtl/actions/contrast_brightness.py index def52f99..d47002ff 100644 --- a/src/ui/dtl/actions/contrast_brightness.py +++ b/src/ui/dtl/actions/contrast_brightness.py @@ -1,24 +1,18 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action + +from src.ui.dtl import PixelLevelAction from src.ui.dtl.Layer import Layer -class ContrastBrightnessAction(Action): +class ContrastBrightnessAction(PixelLevelAction): name = "contrast_brightness" title = "Contrast / Brightness" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/contrast_brightness" description = ( "This layer (contrast_brightness) randomly changes contrast and brightness of images. " ) - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "contrast": None, - "brightness": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -51,72 +45,80 @@ def get_settings(options_json: dict) -> dict: } return settings - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def create_options(src: list, dst: list, settings: dict) -> dict: + contrast_val = False + contrast_min_val = 1 + contrast_max_val = 2 + center_grey_val = False if "contrast" in settings: - node_state["Contrast"] = True - node_state["Contrast Min"] = settings["contrast"]["min"] - node_state["Contrast Max"] = settings["contrast"]["max"] - node_state["Center grey"] = settings["contrast"]["center_grey"] + contrast_val = True + contrast_min_val = settings["contrast"].get("min", 1) + contrast_max_val = settings["contrast"].get("max", 2) + center_grey_val = settings["contrast"].get("center_grey", False) + brightness_val = False + brightness_min_val = -50 + brightness_max_val = 50 if "brightness" in settings: - node_state["Brightness"] = True - node_state["Brightness Min"] = settings["brightness"]["min"] - node_state["Brightness Max"] = settings["brightness"]["max"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="Contrast", - option_component=NodesFlow.CheckboxOptionComponent(default_value=True), - ), - NodesFlow.Node.Option( - name="Contrast Min", - option_component=NodesFlow.SliderOptionComponent(min=0, max=10, default_value=1), - ), - NodesFlow.Node.Option( - name="Contrast Max", - option_component=NodesFlow.SliderOptionComponent(min=0, max=10, default_value=2), - ), - NodesFlow.Node.Option( - name="Center grey", - option_component=NodesFlow.CheckboxOptionComponent(default_value=False), - ), - NodesFlow.Node.Option( - name="center_grey_text", - option_component=NodesFlow.TextOptionComponent( - '*To center colors of images (subtract 128) first, set "Center grey" to true' + brightness_val = True + brightness_min_val = settings["brightness"].get("min", -50) + brightness_max_val = settings["brightness"].get("max", 50) + settings_options = [ + NodesFlow.Node.Option( + name="Contrast", + option_component=NodesFlow.CheckboxOptionComponent(default_value=contrast_val), + ), + NodesFlow.Node.Option( + name="Contrast Min", + option_component=NodesFlow.SliderOptionComponent( + min=0, max=10, default_value=contrast_min_val + ), + ), + NodesFlow.Node.Option( + name="Contrast Max", + option_component=NodesFlow.SliderOptionComponent( + min=0, max=10, default_value=contrast_max_val + ), ), - ), - NodesFlow.Node.Option( - name="Brightness", - option_component=NodesFlow.CheckboxOptionComponent(default_value=True), - ), - NodesFlow.Node.Option( - name="Brightness Min", - option_component=NodesFlow.SliderOptionComponent( - min=-255, max=255, default_value=-50 + NodesFlow.Node.Option( + name="Center grey", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=center_grey_val + ), ), - ), - NodesFlow.Node.Option( - name="Brightness Max", - option_component=NodesFlow.SliderOptionComponent( - min=-255, max=255, default_value=50 + NodesFlow.Node.Option( + name="center_grey_text", + option_component=NodesFlow.TextOptionComponent( + '*To center colors of images (subtract 128) first, set "Center grey" to true' + ), ), - ), - ] + NodesFlow.Node.Option( + name="Brightness", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=brightness_val + ), + ), + NodesFlow.Node.Option( + name="Brightness Min", + option_component=NodesFlow.SliderOptionComponent( + min=-255, max=255, default_value=brightness_min_val + ), + ), + NodesFlow.Node.Option( + name="Brightness Max", + option_component=NodesFlow.SliderOptionComponent( + min=-255, max=255, default_value=brightness_max_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/crop.py b/src/ui/dtl/actions/crop.py index eaf6f8fd..d273a6c3 100644 --- a/src/ui/dtl/actions/crop.py +++ b/src/ui/dtl/actions/crop.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely.app.widgets import ( NodesFlow, Select, @@ -9,21 +10,19 @@ Flexbox, OneOf, Checkbox, + Button, + Text, ) -from src.ui.dtl import Action + +from src.ui.dtl import SpatialLevelAction from src.ui.dtl.Layer import Layer -class CropAction(Action): +class CropAction(SpatialLevelAction): name = "crop" title = "Crop" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/crop" description = "This layer (crop) is used to crop part of image with its annotations. This layer has several modes: it may crop fixed given part of image or random one." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {"sides": None, "random_part": None} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -142,6 +141,14 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ] ) + mode_preview = Text("") + params_preview = Text("") + settings_preview = Container(widgets=[mode_preview, params_preview], gap=1) + + save_settings_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_settings = {} + def _set_sides(settings: dict): mode_select.set_value("sides") top_value = settings["sides"]["top"] @@ -212,46 +219,70 @@ def _get_random(): "keep_aspect_ratio": crop_random_keep_aspect_ratio.is_checked(), } - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" - settings = {} - if mode_select.get_value() == "sides": + def _update_preview(): + mode = saved_settings.get("mode", "") + if mode == "": + mode_preview.text = "" + params_preview.text = "" + elif mode == "sides": + sides = _get_sides() + mode_preview.text = "Mode: Sides" + params_preview.text = f"Top: {sides['top']}; Left: {sides['left']}; Right: {sides['right']}; Bottom: {sides['bottom']}" + elif mode == "random_part": + random_part = _get_random() + mode_preview.text = "Mode: Random part" + params_preview.text = f"Height: {random_part['height']['min_percent']} - {random_part['height']['max_percent']}; Width: {random_part['width']['min_percent']} - {random_part['width']['max_percent']}; Keep aspect ratio: {random_part['keep_aspect_ratio']}" + + def _save_settings(): + nonlocal saved_settings + settings = { + "mode": mode_select.get_value(), + } + if settings["mode"] == "sides": settings["sides"] = _get_sides() else: settings["random_part"] = _get_random() - return settings + saved_settings = settings + _update_preview() - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return saved_settings + + def _set_settings_from_json(settings): if "sides" in settings: _set_sides(settings) - else: + elif "random_part" in settings: _set_random(settings) - return node_state + _save_settings() + + save_settings_btn.click(_save_settings) - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Crop settings"), - ), - NodesFlow.Node.Option( - name="Set Settings", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - Container(widgets=[mode_select, OneOf(mode_select)]) - ) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Settings", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[mode_select, OneOf(mode_select), save_settings_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/data.py b/src/ui/dtl/actions/data.py index af9a779e..11efba4c 100644 --- a/src/ui/dtl/actions/data.py +++ b/src/ui/dtl/actions/data.py @@ -1,29 +1,43 @@ -import copy -import traceback from typing import List, Optional +import requests + +from supervisely.app.content import StateJson +from supervisely.app.widgets import ( + NodesFlow, + SelectDataset, + Text, + Button, + Container, + Flexbox, + NotificationBox, +) +from supervisely import ProjectType, ProjectMeta, ObjClassCollection import src.utils as utils -from src.ui.dtl import Action +import src.globals as g +from src.ui.dtl import SourceAction from src.ui.dtl.Layer import Layer -from supervisely.app.content import StateJson -from supervisely.app.widgets import NodesFlow, SelectDataset -from supervisely import ProjectType, ProjectMeta from src.ui.widgets.classes_mapping import ClassesMapping -import src.globals as g +from src.ui.widgets.classes_mapping_preview import ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + get_set_settings_container, + get_set_settings_button_style, + set_classes_mapping_settings_from_json, +) -class DataAction(Action): +class DataAction(SourceAction): name = "data" title = "Data" docs_url = "https://docs.supervisely.com/data-manipulation/index/data-layers/data" description = "Data layer (data) is used to specify project and its datasets that will participate in data transformation process." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } + md_description_url = ( + "https://raw.githubusercontent.com/supervisely/docs/master/data-manipulation/dtl/data.md" + ) + md_description = requests.get(md_description_url).text @classmethod def create_inputs(self): @@ -31,12 +45,28 @@ def create_inputs(self): @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): + # Src widgets select_datasets = SelectDataset( multiselect=True, select_all_datasets=True, allowed_project_types=[ProjectType.IMAGES], compact=False, ) + select_datasets_btn = Button( + text="SELECT", + icon="zmdi zmdi-folder", + button_type="text", + button_size="small", + emit_on_click="openSidebar", + style=get_set_settings_button_style(), + ) + src_save_btn = Button("Save", icon="zmdi zmdi-floppy") + src_preview_widget = Text("") + src_widgets_container = Container(widgets=[select_datasets, src_save_btn]) + + saved_src = [] + + # fix team and workspace for SelectDataset widget StateJson()[select_datasets._project_selector._ws_selector._team_selector.widget_id][ "teamId" ] = g.TEAM_ID @@ -45,166 +75,251 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ] = g.WORKSPACE_ID select_datasets._project_selector._ws_selector.disable() StateJson().send_changes() - classes_mapping_widget = ClassesMapping() - def _get_classes_mapping_value(): - classes = classes_mapping_widget.get_classes() - mapping = classes_mapping_widget.get_mapping() - default = [ - cls_name for cls_name, cls_values in mapping.items() if cls_values["default"] + # Settings widgets + empty_src_notification = NotificationBox( + title="No classes", + description="Choose datasets and ensure that source project have classes.", + ) + classes_mapping_widget = ClassesMapping(empty_notification=empty_src_notification) + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), ] - if len(default) == len(classes): - return "default" - ignore = [cls_name for cls_name, cls_values in mapping.items() if cls_values["ignore"]] - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - if len(ignore) > 0: - values["__other__"] = "__ignore__" - values.update({name: "__default__" for name in default}) - elif len(default) > 0: - values["__other__"] = "__default__" - return values + ) + + default_classes_mapping_settings = "default" + saved_classes_mapping_settings = "default" + + def _set_src_preview(): + src_preview_text = "".join(f"
  • {src.replace('/', ' / ')}
  • " for src in saved_src) + src_preview_text = ( + f'