From 19bdb8ff2cba7881570dfc315ad146b52286c818 Mon Sep 17 00:00:00 2001 From: Nikolai Petukhov Date: Mon, 18 Sep 2023 18:25:17 -0300 Subject: [PATCH] test --- requirements.txt | 4 +- src/compute/Layer.py | 138 +++-- src/compute/Net.py | 55 +- src/compute/layers/data/DataLayer.py | 13 +- src/compute/layers/processing/BlurLayer.py | 58 +- .../processing/ContrastBrightnessLayer.py | 2 + .../layers/processing/InstancesCrop.py | 1 + .../layers/processing/ObjectsFilterLayer.py | 1 + src/compute/layers/processing/RotateLayer.py | 18 +- src/compute/layers/save/SaveLayer.py | 111 ++-- src/compute/layers/save/SaveMasksLayer.py | 94 +-- src/compute/layers/save/SuperviselyLayer.py | 34 +- src/compute/main.py | 21 +- 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 | 200 ++++-- src/ui/dtl/actions/background.py | 40 +- src/ui/dtl/actions/bbox.py | 207 +++++-- src/ui/dtl/actions/bbox2poly.py | 217 +++++-- src/ui/dtl/actions/bitmap2lines.py | 236 ++++--- src/ui/dtl/actions/bitwise_masks.py | 298 ++++++--- src/ui/dtl/actions/blur.py | 114 ++-- src/ui/dtl/actions/color_class.py | 111 ++-- src/ui/dtl/actions/contrast_brightness.py | 140 ++--- src/ui/dtl/actions/crop.py | 103 ++-- src/ui/dtl/actions/data.py | 421 ++++++++----- src/ui/dtl/actions/dataset.py | 73 +-- src/ui/dtl/actions/drop_lines_by_length.py | 266 +++++--- src/ui/dtl/actions/drop_noise.py | 255 ++++++-- src/ui/dtl/actions/drop_obj_by_class.py | 175 ++++-- src/ui/dtl/actions/dummy.py | 28 +- src/ui/dtl/actions/duplicate_objects.py | 207 +++++-- src/ui/dtl/actions/find_contours.py | 215 +++++-- 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 | 197 ++++-- src/ui/dtl/actions/instances_crop.py | 213 +++++-- src/ui/dtl/actions/line2bitmap.py | 229 ++++--- src/ui/dtl/actions/merge_bitmaps.py | 181 ++++-- src/ui/dtl/actions/multiply.py | 55 +- src/ui/dtl/actions/noise.py | 70 ++- src/ui/dtl/actions/objects_filter.py | 207 +++++-- src/ui/dtl/actions/poly2bitmap.py | 216 +++++-- src/ui/dtl/actions/random_color.py | 55 +- src/ui/dtl/actions/rasterize.py | 204 +++++-- src/ui/dtl/actions/rename.py | 201 ++++-- 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 | 234 ++++--- src/ui/dtl/actions/skeletonize.py | 197 ++++-- src/ui/dtl/actions/sliding_window.py | 135 ++-- src/ui/dtl/actions/split_masks.py | 177 ++++-- src/ui/dtl/actions/supervisely.py | 39 +- src/ui/dtl/actions/tag.py | 145 +++-- src/ui/dtl/utils.py | 261 ++++++++ src/ui/tabs/configure.py | 271 ++++++++ src/ui/tabs/json_preview.py | 202 ++++++ src/ui/tabs/run.py | 94 +++ src/ui/ui.py | 578 +----------------- src/ui/utils.py | 377 ++++++++++++ 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 +----- 81 files changed, 6478 insertions(+), 3314 deletions(-) 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/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..64e4e60e 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: @@ -51,7 +52,12 @@ class Layer: "properties": { "action": {"type": "string"}, "src": {"$ref": "#/definitions/connections"}, - "dst": {"type": "string"}, + "dst": { + "oneOf": [ + {"type": "array"}, + {"type": "string"}, + ] + }, }, } @@ -84,9 +90,21 @@ 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: + extra = {"layer_config": self._config} + + v = jsonschema.Draft7Validator(self.params) + for error in v.iter_errors(self._config): + extra.setdefault("errors", []).append(str(error)) + raise GraphError("Layer not valid", error=e, extra=extra) + 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 +128,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 +148,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 +184,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 +205,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 +228,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 +247,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 +296,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 +304,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/ContrastBrightnessLayer.py b/src/compute/layers/processing/ContrastBrightnessLayer.py index 4868aab8..94d42e36 100644 --- a/src/compute/layers/processing/ContrastBrightnessLayer.py +++ b/src/compute/layers/processing/ContrastBrightnessLayer.py @@ -41,6 +41,8 @@ def __init__(self, config): Layer.__init__(self, config) def validate(self): + super().validate() + def check_min_max(dictionary, text): if dictionary["min"] > dictionary["max"]: raise RuntimeError('"min" should be <= than "max" for "{}".'.format(text)) diff --git a/src/compute/layers/processing/InstancesCrop.py b/src/compute/layers/processing/InstancesCrop.py index a3a0eadc..977683a7 100644 --- a/src/compute/layers/processing/InstancesCrop.py +++ b/src/compute/layers/processing/InstancesCrop.py @@ -51,6 +51,7 @@ def __init__(self, config): self.classes_to_crop, self.classes_to_save = self._get_cls_lists() def validate(self): + super().validate() if len(self.classes_to_crop) == 0: raise ValueError("InstancesCropLayer: classes array can not be empty") if len(set(self.classes_to_crop) & set(self.classes_to_save)) > 0: diff --git a/src/compute/layers/processing/ObjectsFilterLayer.py b/src/compute/layers/processing/ObjectsFilterLayer.py index accc0023..acb327bf 100644 --- a/src/compute/layers/processing/ObjectsFilterLayer.py +++ b/src/compute/layers/processing/ObjectsFilterLayer.py @@ -101,6 +101,7 @@ def __init__(self, config): Layer.__init__(self, config) def validate(self): + super().validate() if self.settings["filter_by"]["polygon_sizes"]["action"] != "delete": raise NotImplementedError("Class remapping is NIY here.") diff --git a/src/compute/layers/processing/RotateLayer.py b/src/compute/layers/processing/RotateLayer.py index 862e94a0..d2b21b1c 100644 --- a/src/compute/layers/processing/RotateLayer.py +++ b/src/compute/layers/processing/RotateLayer.py @@ -45,6 +45,7 @@ def __init__(self, config): Layer.__init__(self, config) def validate(self): + super().validate() if ( self.settings["rotate_angles"]["min_degrees"] > self.settings["rotate_angles"]["max_degrees"] @@ -76,8 +77,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 +102,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..c69c1a91 100644 --- a/src/compute/layers/save/SaveLayer.py +++ b/src/compute/layers/save/SaveLayer.py @@ -5,14 +5,15 @@ import cv2 import numpy as np + +import supervisely as sly +from supervisely.project.project import Dataset + from src.compute.utils import imaging from src.compute.utils import os_utils from src.compute.dtl_utils.image_descriptor import ImageDescriptor - from src.compute.Layer import Layer - -import supervisely as sly -from supervisely.project.project import Dataset +from src.exceptions import GraphError # save to archive @@ -66,8 +67,12 @@ 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.") + raise GraphError( + "Output meta is not set. Check that node is connected", extra={"layer": self.action} + ) dst = self.dsts[0] self.out_project = sly.Project( directory=f"{self.output_folder}/{dst}", mode=sly.OpenMode.CREATE @@ -84,52 +89,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..f5c84145 100644 --- a/src/compute/layers/save/SaveMasksLayer.py +++ b/src/compute/layers/save/SaveMasksLayer.py @@ -1,18 +1,18 @@ # coding: utf-8 +from typing import Tuple import json import os import os.path as osp -from typing import Tuple - import cv2 import numpy as np -from src.compute.dtl_utils.image_descriptor import ImageDescriptor - -from src.compute.Layer import Layer import supervisely as sly +from src.compute.dtl_utils.image_descriptor import ImageDescriptor +from src.compute.Layer import Layer +from src.exceptions import GraphError + # save to archive, with GTs and checks class SaveMasksLayer(Layer): @@ -86,6 +86,7 @@ def validate_dest_connections(self): pass def validate(self): + super().validate() if "gt_machine_color" in self.settings: for cls in self.settings["gt_machine_color"]: col = self.settings["gt_machine_color"][cls] @@ -111,6 +112,12 @@ def validate(self): ) def preprocess(self): + if self.net.preview_mode: + return + if self.output_meta is None: + raise GraphError( + "Output meta is not set. Check that node is connected", extra={"layer": self.action} + ) dst = self.dsts[0] self.out_project = sly.Project( directory=f"{self.output_folder}/{dst}", mode=sly.OpenMode.CREATE @@ -127,55 +134,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..28211c17 100644 --- a/src/compute/layers/save/SuperviselyLayer.py +++ b/src/compute/layers/save/SuperviselyLayer.py @@ -1,13 +1,14 @@ # coding: utf-8 -import json from typing import Tuple + +import supervisely as sly + from src.compute.dtl_utils.image_descriptor import ImageDescriptor from src.compute.Layer import Layer +from src.exceptions import GraphError import src.globals as g -import supervisely as sly - class SuperviselyLayer(Layer): action = "supervisely" @@ -29,6 +30,14 @@ 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 + if self.output_meta is None: + raise GraphError( + "Output meta is not set. Check that node is connected", extra={"layer": self.action} + ) + if len(self.dsts) == 0: + raise GraphError("Destination is not set", extra={"layer_config": self.config}) dst = self.dsts[0] self.out_project_name = dst @@ -50,14 +59,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..7d6e96c7 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 BadSettingsError, CustomException +from src.utils import LegacyProjectItem def make_legacy_project_item(project: sly.Project, dataset, item_name): @@ -79,11 +72,17 @@ 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 total = net.get_total_elements() + if total == 0: + raise BadSettingsError("There are no elements to process") elements_generator = net.get_elements_generator() results_counter = 0 with progress(message=f"Processing items...", total=total) as pbar: 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..37afc05f 100644 --- a/src/ui/dtl/actions/approx_vector.py +++ b/src/ui/dtl/actions/approx_vector.py @@ -1,91 +1,171 @@ +import copy from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox 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 ( + classes_list_settings_changed_meta, + get_classes_list_value, + set_classes_list_preview, + set_classes_list_settings_from_json, +) -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) + _current_meta = ProjectMeta() + classes_list_widget = ClassesList(multiple=True) + classes_list_preview = ClassesListPreview() + classes_list_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_list_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_list_widgets_container = Container( + widgets=[ + classes_list_widget, + Flexbox( + widgets=[ + classes_list_save_btn, + classes_list_set_default_btn, + ], + gap=105, + ), + ] + ) + + saved_classes_settings = [] + default_classes_settings = [] + + def _get_classes_list_value(): + return get_classes_list_value(classes_list_widget, multiple=True) + + def _set_classes_list_preview(): + set_classes_list_preview( + classes_list_widget, classes_list_preview, saved_classes_settings + ) + + def _save_classes_list_settings(): + nonlocal saved_classes_settings + saved_classes_settings = _get_classes_list_value() + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_settings + saved_classes_settings = copy.deepcopy(default_classes_settings) + + def meta_changed_cb(project_meta: ProjectMeta): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta + + classes_list_widget.loading = True + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Polygon, Polyline, AnyGeometry] + ] + + # set classes to widget + classes_list_widget.set(obj_classes) + + # update settings according to new meta + nonlocal saved_classes_settings + saved_classes_settings = classes_list_settings_changed_meta( + saved_classes_settings, obj_classes + ) + + # update settings preview + _set_classes_list_preview() + + classes_list_widget.loading = False 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 _set_settings_from_json(settings: dict): + classes_list_widget.loading = True + classes_list_settings = settings.get("classes", []) + set_classes_list_settings_from_json( + classes_list_widget=classes_list_widget, settings=classes_list_settings + ) + # save settings + _save_classes_list_settings() + # update settings preview + _set_classes_list_preview() + classes_list_widget.loading = False - def meta_changed_cb(project_meta: ProjectMeta): - classes.loading = True - classes.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Polygon, Polyline, AnyGeometry] - ] + @classes_list_save_btn.click + def classes_list_save_btn_cb(): + _save_classes_list_settings() + _set_classes_list_preview() + g.updater("metas") + + @classes_list_set_default_btn.click + def classes_list_set_default_btn_cb(): + _set_default_classes_mapping_setting() + set_classes_list_settings_from_json( + classes_list_widget=classes_list_widget, settings=saved_classes_settings ) - 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) + _set_classes_list_preview() + g.updater("metas") + + 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( + classes_list_widgets_container + ), + sidebar_width=380, + ), ), - ), - 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="classes_preview_text", + option_component=NodesFlow.WidgetOptionComponent(classes_list_preview), + ), + NodesFlow.Node.Option( + name="epsilon_text", + option_component=NodesFlow.TextOptionComponent("Epsilon"), + ), + 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..f292c1e1 100644 --- a/src/ui/dtl/actions/bbox.py +++ b/src/ui/dtl/actions/bbox.py @@ -1,94 +1,179 @@ +import copy 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): + _current_meta = ProjectMeta() 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 = copy.deepcopy(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): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta 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_settings_from_json( + classes_mapping_widget, + saved_classes_mapping_settings, + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + _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 + ), + sidebar_width=630, + ), + ), + 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..23e07032 100644 --- a/src/ui/dtl/actions/bbox2poly.py +++ b/src/ui/dtl/actions/bbox2poly.py @@ -1,102 +1,185 @@ +import copy 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): + _current_meta = ProjectMeta() 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 = copy.deepcopy(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): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta 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_settings_from_json( + classes_mapping_widget, + saved_classes_mapping_settings, + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + _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..d5a2d207 100644 --- a/src/ui/dtl/actions/bitmap2lines.py +++ b/src/ui/dtl/actions/bitmap2lines.py @@ -1,117 +1,195 @@ 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): + _current_meta = ProjectMeta() 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 = copy.deepcopy(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): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta 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_settings_from_json( + classes_mapping_widget, + saved_classes_mapping_settings, + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + _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 + ), + sidebar_width=630, + ), + ), + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - 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="min_points_cnt", + option_component=NodesFlow.IntegerOptionComponent( + min=2, default_value=min_points_cnt_val + ), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt", - option_component=NodesFlow.IntegerOptionComponent(min=2, default_value=2), - ), - ] + ] + + 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..121f047a 100644 --- a/src/ui/dtl/actions/bitwise_masks.py +++ b/src/ui/dtl/actions/bitwise_masks.py @@ -1,113 +1,273 @@ +import copy 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 ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview +from src.ui.dtl.utils import ( + classes_list_settings_changed_meta, + get_classes_list_value, + set_classes_list_preview, + set_classes_list_settings_from_json, +) +from src.exceptions import BadSettingsError +import src.globals as g -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): + _current_meta = ProjectMeta() class_mask_widget = ClassesList() classes_to_correct_widget = ClassesList(multiple=True) + 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") + + set_default_class_mask_btn = Button("Set Default", icon="zmdi zmdi-refresh") + set_default_classes_to_correct_btn = Button("Set Default", icon="zmdi zmdi-refresh") + + class_mask_widgets_container = Container( + widgets=[ + class_mask_widget, + Flexbox( + widgets=[ + save_class_mask_btn, + set_default_class_mask_btn, + ], + gap=105, + ), + ] + ) + classes_to_correct_widgets_container = Container( + widgets=[ + classes_to_correct_widget, + Flexbox( + widgets=[ + save_classes_to_correct_btn, + set_default_classes_to_correct_btn, + ], + gap=105, + ), + ] + ) + + saved_class_mask_settings = "" + saved_classes_to_correct_settings = [] + + default_class_mask_settings = "" + default_classes_to_correct_settings = [] + + def _get_class_mask_value(): + return get_classes_list_value(class_mask_widget, multiple=False) + + def _set_class_mask_preview(): + set_classes_list_preview( + class_mask_widget, class_mask_preview, saved_class_mask_settings + ) + + def _save_class_mask_settings(): + nonlocal saved_class_mask_settings + saved_class_mask_settings = _get_class_mask_value() + + def _set_default_class_mask_setting(): + # save setting to var + nonlocal saved_class_mask_settings + saved_class_mask_settings = copy.deepcopy(default_class_mask_settings) + + def _get_classes_to_correct_value(): + return get_classes_list_value(classes_to_correct_widget, multiple=True) + + def _set_classes_to_correct_preview(): + set_classes_list_preview( + classes_to_correct_widget, + classes_to_correct_preview, + saved_classes_to_correct_settings, + ) + + def _save_classes_to_correct_settings(): + nonlocal saved_classes_to_correct_settings + saved_classes_to_correct_settings = _get_classes_to_correct_value() + + def _set_default_classes_to_correct_setting(): + # save setting to var + nonlocal saved_classes_to_correct_settings + saved_classes_to_correct_settings = copy.deepcopy(default_classes_to_correct_settings) + def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" - try: - class_mask = class_mask_widget.get_selected_classes()[0].name - except: - class_mask = "" 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): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta + class_mask_widget.loading = True - classes_to_correct_widget.loading = True - class_mask_widget.set(project_meta.obj_classes) - classes_to_correct_widget.set(project_meta.obj_classes) + obj_classes = [cls for cls in project_meta.obj_classes] + # set classes to widget + class_mask_widget.set(obj_classes) + # update settings according to new meta + nonlocal saved_class_mask_settings + saved_class_mask_settings = classes_list_settings_changed_meta( + saved_class_mask_settings, obj_classes + ) + # update settings preview + _set_class_mask_preview() class_mask_widget.loading = False + + classes_to_correct_widget.loading = True + obj_classes = [cls for cls in project_meta.obj_classes] + # set classes to widget + classes_to_correct_widget.set(obj_classes) + # update settings according to new meta + nonlocal saved_classes_to_correct_settings + saved_classes_to_correct_settings = classes_list_settings_changed_meta( + saved_classes_to_correct_settings, obj_classes + ) + # update settings preview + _set_classes_to_correct_preview() 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"]) + classes_list_settings = settings.get("class_mask", "") + set_classes_list_settings_from_json( + classes_list_widget=class_mask_widget, settings=classes_list_settings + ) + # save settings + _save_class_mask_settings() + # update settings preview + _set_class_mask_preview() class_mask_widget.loading = False + + classes_to_correct_widget.loading = True + classes_list_settings = settings.get("classes_to_correct", []) + set_classes_list_settings_from_json( + classes_list_widget=classes_to_correct_widget, settings=classes_list_settings + ) + # save settings + _save_classes_to_correct_settings() + # update settings preview + _set_classes_to_correct_preview() classes_to_correct_widget.loading = False - return node_state + + @save_class_mask_btn.click + def save_class_mask_btn_cb(): + _save_class_mask_settings() + _set_class_mask_preview() + g.updater("metas") + + @set_default_class_mask_btn.click + def set_default_class_mask_btn_cb(): + _set_default_class_mask_setting() + set_classes_list_settings_from_json( + classes_list_widget=class_mask_widget, settings=saved_class_mask_settings + ) + _set_class_mask_preview() + g.updater("metas") + + @save_classes_to_correct_btn.click + def save_classes_to_correct_btn_cb(): + _save_classes_to_correct_settings() + _set_classes_to_correct_preview() + g.updater("metas") + + @set_default_classes_to_correct_btn.click + def set_default_classes_to_correct_btn_cb(): + _set_default_classes_to_correct_setting() + set_classes_list_settings_from_json( + classes_list_widget=classes_to_correct_widget, + settings=saved_classes_to_correct_settings, + ) + _set_classes_to_correct_preview() + g.updater("metas") 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 BadSettingsError("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="Select Class Mask", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + class_mask_widgets_container + ), + sidebar_width=380, + ), ), - ), - NodesFlow.Node.Option( - name="class_mask_text", - option_component=NodesFlow.TextOptionComponent( - "Class Mask. First element of bitwise operation" + NodesFlow.Node.Option( + name="class_mask_preview", + option_component=NodesFlow.WidgetOptionComponent(class_mask_preview), ), - ), - NodesFlow.Node.Option( - name="Select Class Mask", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(class_mask_widget) + NodesFlow.Node.Option( + name="classes_text", + option_component=NodesFlow.TextOptionComponent("Classes to correct"), ), - ), - 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="Select Classes to correct", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_to_correct_widgets_container + ), + sidebar_width=380, + ), ), - ), - ] + NodesFlow.Node.Option( + name="classes_to_correct_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_to_correct_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/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..fdd76e72 100644 --- a/src/ui/dtl/actions/color_class.py +++ b/src/ui/dtl/actions/color_class.py @@ -1,82 +1,97 @@ 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): + _current_meta = ProjectMeta() 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): + nonlocal _current_meta + if project_meta == _current_meta: + return + _current_meta = project_meta + 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..6eaac36a 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,16 @@ 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") + + settings_container = Container(widgets=[mode_select, OneOf(mode_select), save_settings_btn]) + + saved_settings = {} + def _set_sides(settings: dict): mode_select.set_value("sides") top_value = settings["sides"]["top"] @@ -212,46 +221,68 @@ 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 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(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): if "sides" in settings: _set_sides(settings) - else: + elif "random_part" in settings: _set_random(settings) - return node_state + _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)]) - ) + save_settings_btn.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_container) + ), + ), + 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..b87a5313 100644 --- a/src/ui/dtl/actions/data.py +++ b/src/ui/dtl/actions/data.py @@ -1,29 +1,44 @@ -import copy -import traceback from typing import List, Optional +import copy +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 +46,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 +76,262 @@ 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 + _current_meta = ProjectMeta() + 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'