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/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/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..a68851cb 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,13 @@ import shutil import os +import threading from supervisely import Application -from src.ui.ui import layout +from src.ui.ui import layout, update_loop 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) + +update_loop.start() diff --git a/src/ui/dtl/Action.py b/src/ui/dtl/Action.py index 560ec092..06946a50 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,8 @@ 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -23,18 +18,18 @@ 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")] @classmethod def create_outputs(cls): - return [NodesFlow.Node.Output("destination", "Destination")] + return [NodesFlow.Node.Output("destination", "Output")] @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}

"), ] ) diff --git a/src/ui/dtl/Layer.py b/src/ui/dtl/Layer.py index 95d5ae20..4984465e 100644 --- a/src/ui/dtl/Layer.py +++ b/src/ui/dtl/Layer.py @@ -1,40 +1,38 @@ -import copy +import time +from typing import Optional +import markdown +import random +import numpy as np + from supervisely import Annotation from supervisely.app.widgets import LabeledImage, NodesFlow 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 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,29 +40,20 @@ def __init__( self.output_meta = None - self._preview_img_url = f"static/{self._id}.jpg" - self._ann = None - - self._add_info_option() - self._add_preview_option() - - def _add_info_option(self): - self._options = [ - NodesFlow.Node.Option( - name="Info", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - self.action.create_info_widget() - ) - ), + # info option + self._info_option = NodesFlow.Node.Option( + name="Info", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.HtmlOptionComponent( + markdown.markdown(self.action.md_description) + ) ), - *self._options, - ] - - def _add_preview_option(self): - self._preview_widget = LabeledImage(enable_zoom=True) - self._options = [ - *self._options, + ) + # preview option + self._preview_img_url = f"static/{self.id}.jpg" + self._ann = None + self._preview_widget = LabeledImage(enable_zoom=True, view_height=250) + self._preview_options = [ NodesFlow.Node.Option( name="preview_text", option_component=NodesFlow.TextOptionComponent("Preview") ), @@ -74,6 +63,16 @@ def _add_preview_option(self): ), ] + 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 +81,104 @@ 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) return NodesFlow.Node( - id=self._id, + id=self.id, name=self.action.title, width=self.action.width, - options=self._options, + options=[ + self._info_option, + get_separator(0), + *options["src"], + *options["dst"], + *options["settings"], + *self._preview_options, + ], inputs=self._inputs, outputs=self._outputs, + inputs_up=True, ) - 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 update_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=f"{self._preview_img_url}?{time.time()}", ann=self._ann + ) + + 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 = [] - - def clear_settings(self): - self._settings = {} - - def clear(self): - self.clear_sources() - self.clear_destinations() - self.clear_settings() + return [self._connection_name(self.id, output.name) for output in self._outputs] - 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..8a019c4a 100644 --- a/src/ui/dtl/__init__.py +++ b/src/ui/dtl/__init__.py @@ -39,49 +39,59 @@ 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, + 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, diff --git a/src/ui/dtl/actions/approx_vector.py b/src/ui/dtl/actions/approx_vector.py index fc4cf0f3..8c2801d9 100644 --- a/src/ui/dtl/actions/approx_vector.py +++ b/src/ui/dtl/actions/approx_vector.py @@ -1,10 +1,14 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta from supervisely import Polygon, Polyline, AnyGeometry + +import src.globals as g from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview +from src.ui.dtl.utils import get_separator class ApproxVectorAction(Action): @@ -14,36 +18,24 @@ class ApproxVectorAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/approx_vector" ) description = "This layer (approx_vector) approximates vector figures: lines and polygons. The operation decreases number of vertices with Douglas-Peucker algorithm." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_button = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [obj_class.name for obj_class in classes.get_selected_classes()], + "classes": saved_classes_settings, "epsilon": options_json["epsilon"], } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] - obj_class_names = settings["classes"] - classes.loading = True - classes.select(obj_class_names) - classes.loading = False - node_state["epsilon"] = settings["epsilon"] - return node_state - def meta_changed_cb(project_meta: ProjectMeta): + nonlocal saved_classes_settings classes.loading = True classes.set( [ @@ -52,40 +44,68 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Polygon, Polyline, AnyGeometry] ] ) + saved_classes_settings = [ + obj_class.name for obj_class in classes.get_selected_classes() + ] + classes.loading = False + + def _save_classes_setting(): + nonlocal saved_classes_settings + selected_classes = classes.get_selected_classes() + saved_classes_settings = [obj_class.name for obj_class in selected_classes] + classes_preview.set(selected_classes) + g.updater("metas") + + classes_save_button.click(_save_classes_setting) + + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) + classes.loading = True + classes.select(obj_class_names) + _save_classes_setting() classes.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes"), - ), - NodesFlow.Node.Option( - name="Select Classes", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + epsilon_val = settings.get("epsilon", 3) + if epsilon_val < 1: + raise ValueError("Epsilon must be greater than 0") + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_button]) + ) + ), + ), + NodesFlow.Node.Option( + name="classes_preview_text", + option_component=NodesFlow.WidgetOptionComponent(classes_preview), + ), + get_separator(1), + NodesFlow.Node.Option( + name="epsilon_text", + option_component=NodesFlow.TextOptionComponent("Epsilon"), ), - ), - NodesFlow.Node.Option( - name="epsilon_text", - option_component=NodesFlow.TextOptionComponent("Epsilon"), - ), - NodesFlow.Node.Option( - name="epsilon", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=3), - ), - ] + NodesFlow.Node.Option( + name="epsilon", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=epsilon_val + ), + ), + get_separator(2), + ] + 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..e9a5b380 100644 --- a/src/ui/dtl/actions/background.py +++ b/src/ui/dtl/actions/background.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -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..89258728 100644 --- a/src/ui/dtl/actions/bbox.py +++ b/src/ui/dtl/actions/bbox.py @@ -1,9 +1,11 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class BBoxAction(Action): @@ -11,17 +13,14 @@ class BBoxAction(Action): title = "Bounding Box" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox" description = "Bounding Box layer (bbox) converts annotations of specified classes to bounding boxes. Annotations would be replaced with new objects of shape rectangle." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -32,16 +31,32 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + obj_classes = [ + obj_class + for obj_class in classes_mapping_widget.get_classes() + if obj_class.name in saved_classes_mapping_settings + ] + classes_mapping_preview.set(classes=obj_classes, mapping=saved_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""" + def meta_changed_cb(project_meta: ProjectMeta): + classes_mapping_widget.loading = True + classes_mapping_widget.set(project_meta.obj_classes) + _save_classes_mapping_setting() + classes_mapping_widget.loading = False + + def _set_settings_from_json(settings): + if "classes_mapping" not in settings: + return 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(): @@ -57,38 +72,37 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) + _save_classes_mapping_setting() classes_mapping_widget.loading = False - return node_state - def meta_changed_cb(project_meta: ProjectMeta): - classes_mapping_widget.loading = True - classes_mapping_widget.set(project_meta.obj_classes) - classes_mapping_widget.loading = False + classes_mapping_save_btn.click(_save_classes_mapping_setting) - 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 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), + ), + 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..94c29669 100644 --- a/src/ui/dtl/actions/bbox2poly.py +++ b/src/ui/dtl/actions/bbox2poly.py @@ -1,9 +1,11 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta, Rectangle, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class BboxToPolyAction(Action): @@ -13,17 +15,14 @@ class BboxToPolyAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox2poly" ) description = 'This layer (bbox2poly) converts rectangles ("bounding boxes") to polygons.' - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -37,9 +36,19 @@ def _get_classes_mapping_value(): 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 _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + obj_classes = [ + obj_class + for obj_class in classes_mapping_widget.get_classes() + if obj_class.name in saved_classes_mapping_settings + ] + classes_mapping_preview.set(classes=obj_classes, mapping=saved_classes_mapping_settings) + def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True classes_mapping_widget.set( @@ -49,12 +58,13 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Rectangle, AnyGeometry] ] ) + _save_classes_mapping_setting() 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""" + def _set_settings_from_json(settings): + if "classes_mapping" not in settings: + return 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(): @@ -70,33 +80,37 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) + _save_classes_mapping_setting() 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) + classes_mapping_save_btn.click(_save_classes_mapping_setting) + + 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ), + ), ), - ), - ] + 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..cacd4ca0 100644 --- a/src/ui/dtl/actions/bitmap2lines.py +++ b/src/ui/dtl/actions/bitmap2lines.py @@ -1,11 +1,12 @@ -import copy -import json from typing import Optional + from supervisely import ProjectMeta, Bitmap, AnyGeometry -from supervisely.app.widgets import NodesFlow +from supervisely.app.widgets import NodesFlow, Button, Container + +import src.globals as g from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class Bitmap2LinesAction(Action): @@ -15,17 +16,14 @@ class Bitmap2LinesAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitmap2lines" ) description = "This layer (bitmap2lines) converts thinned (skeletonized) bitmaps to lines. It is extremely useful if you have some raster objects representing lines or edges, maybe forming some tree or net structure, and want to work with vector objects. Each input bitmap should be already thinned (use Skeletonize layer to do it), and for single input mask a number of lines will be produced. Resulting lines may have very many vertices, so consider applying Approx Vector layer to results of this layer. Internally the layer builds a graph of 8-connected pixels, determines minimum spanning tree(s), then greedely extracts diameters from connected components of the tree." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_button = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -39,13 +37,38 @@ def _get_classes_mapping_value(): 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": _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"] + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + obj_classes = [ + obj_class + for obj_class in classes_mapping_widget.get_classes() + if obj_class.name in saved_classes_mapping_settings + ] + classes_mapping_preview.set(classes=obj_classes, mapping=saved_classes_mapping_settings) + g.updater("metas") + + def meta_changed_cb(project_meta: ProjectMeta): + classes_mapping_widget.loading = True + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Bitmap, AnyGeometry] + ] + classes_mapping_widget.set(obj_classes) + _save_classes_mapping_setting() + classes_mapping_widget.loading = False + + classes_mapping_save_button.click(_save_classes_mapping_setting) + + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -61,57 +84,56 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping_setting() - def meta_changed_cb(project_meta: ProjectMeta): - classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Bitmap, AnyGeometry] - ] - ) - 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 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="classes_mapping_text", + # option_component=NodesFlow.TextOptionComponent("Classes Mapping"), + # ), + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes_mapping_widget, classes_mapping_save_button]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt_text", - option_component=NodesFlow.TextOptionComponent("Min Points Count."), - ), - NodesFlow.Node.Option( - name="min_points_cnt_description", - option_component=NodesFlow.TextOptionComponent( - "Min number of vertices for each output line. Other lines will be dropped." + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt", - option_component=NodesFlow.IntegerOptionComponent(min=2, default_value=2), - ), - ] + # NodesFlow.Node.Option( + # name="min_points_cnt_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 + ), + ), + ] + + 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..32813d29 100644 --- a/src/ui/dtl/actions/bitwise_masks.py +++ b/src/ui/dtl/actions/bitwise_masks.py @@ -1,9 +1,11 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview class BitwiseMasksAction(Action): @@ -13,32 +15,44 @@ class BitwiseMasksAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitwise_masks" ) description = "Bitwise Masks - make bitwise operations between bitmap annotations." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "class_mask": None, - "classes_to_correct": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): class_mask_widget = ClassesList() classes_to_correct_widget = ClassesList(multiple=True) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + class_mask_preview = ClassesListPreview() + classes_to_correct_preview = ClassesListPreview() + + save_class_mask_btn = Button("Save", icon="zmdi zmdi-floppy") + save_classes_to_correct_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_class_mask_settings = "" + saved_classes_to_correct_settings = [] + + def _save_class_mask_setting(): + nonlocal saved_class_mask_settings try: - class_mask = class_mask_widget.get_selected_classes()[0].name + class_mask_obj = class_mask_widget.get_selected_classes()[0] + class_mask = class_mask_obj.name except: + class_mask_obj = None class_mask = "" + saved_class_mask_settings = class_mask + class_mask_preview.set([] if class_mask_obj is None else [class_mask_obj]) + + def _save_classes_to_correct_setting(): + nonlocal saved_classes_to_correct_settings + selected_classes = classes_to_correct_widget.get_selected_classes() + saved_classes_to_correct_settings = [cls.name for cls in selected_classes] + classes_to_correct_preview.set(selected_classes) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" return { "type": options_json["type"], - "class_mask": class_mask, - "classes_to_correct": [ - cls.name for cls in classes_to_correct_widget.get_selected_classes() - ], + "class_mask": saved_class_mask_settings, + "classes_to_correct": saved_classes_to_correct_settings, } def meta_changed_cb(project_meta: ProjectMeta): @@ -46,68 +60,85 @@ def meta_changed_cb(project_meta: ProjectMeta): classes_to_correct_widget.loading = True class_mask_widget.set(project_meta.obj_classes) classes_to_correct_widget.set(project_meta.obj_classes) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def _set_settings_from_json(settings): class_mask_widget.loading = True classes_to_correct_widget.loading = True - class_mask_widget.select([settings["class_mask"]]) - classes_to_correct_widget.select(settings["classes_to_correct"]) + class_mask_widget.select([settings["class_mask"]] if "class_mask" in settings else []) + classes_to_correct_widget.select( + settings["classes_to_correct"] if "classes_to_correct" in settings else [] + ) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - return node_state + + save_class_mask_btn.click(_save_class_mask_setting) + save_classes_to_correct_btn.click(_save_classes_to_correct_setting) type_items = [NodesFlow.SelectOptionComponent.Item(t, t) for t in ("nor", "and", "or")] - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="type_text", - option_component=NodesFlow.TextOptionComponent("Operation type"), - ), - NodesFlow.Node.Option( - name="type", - option_component=NodesFlow.SelectOptionComponent( - items=type_items, default_value=type_items[0].value + def create_options(src: list, dst: list, settings: dict) -> dict: + type_val = settings.get("type", "nor") + if type_val not in ("nor", "and", "or"): + raise ValueError("Type must be one of: nor, and, or") + + _set_settings_from_json(settings) + + settings_options = [ + NodesFlow.Node.Option( + name="type_text", + option_component=NodesFlow.TextOptionComponent("Operation type"), + ), + NodesFlow.Node.Option( + name="type", + option_component=NodesFlow.SelectOptionComponent( + items=type_items, default_value=type_val + ), ), - ), - NodesFlow.Node.Option( - name="class_mask_text", - option_component=NodesFlow.TextOptionComponent( - "Class Mask. First element of bitwise operation" + NodesFlow.Node.Option( + name="class_mask_text", + option_component=NodesFlow.TextOptionComponent( + "Class Mask. First element of bitwise operation" + ), ), - ), - NodesFlow.Node.Option( - name="Select Class Mask", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(class_mask_widget) + NodesFlow.Node.Option( + name="Select Class Mask", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[class_mask_widget, save_class_mask_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes to correct"), - ), - NodesFlow.Node.Option( - name="Select Classes to correct", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_to_correct_widget) + NodesFlow.Node.Option( + name="classes_text", + option_component=NodesFlow.TextOptionComponent("Classes to correct"), ), - ), - ] + NodesFlow.Node.Option( + name="Select Classes to correct", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container( + widgets=[classes_to_correct_widget, save_classes_to_correct_btn] + ) + ) + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/blur.py b/src/ui/dtl/actions/blur.py index 7e061062..1723cc29 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,7 +9,9 @@ Field, Flexbox, Text, + Button, ) + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -18,15 +21,6 @@ class BlurAction(Action): 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): @@ -57,12 +51,35 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ), ] ) + 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("", color="white") + params_preview = Text("", color="white") + 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..e62eb3f7 100644 --- a/src/ui/dtl/actions/color_class.py +++ b/src/ui/dtl/actions/color_class.py @@ -1,10 +1,12 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta +from supervisely.imaging.color import hex2rgb, rgb2hex + from src.ui.dtl import Action 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): @@ -13,70 +15,75 @@ class ColorClassAction(Action): docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/color_class" ) - description = "This layer (color_class) used for coloring classes as you wish. Add this class at the end of graph, before data saving." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_color_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_colors = ClassesColorMapping() + classes_colors_preview = ClassesMappingPreview() + classes_colors_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_colors_settings = {} + + def _save_classes_colors_setting(): + nonlocal saved_classes_colors_settings + mapping = classes_colors.get_mapping() + saved_classes_colors_settings = { + cls_name: hex2rgb(value["value"]) for cls_name, value in mapping.items() + } + classes_colors_preview.set_mapping(mapping) 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 meta_changed_cb(project_meta: ProjectMeta): + classes_colors.loading = True + classes_colors.set(project_meta.obj_classes) + classes_colors_preview.set(project_meta.obj_classes, classes_colors.get_mapping()) + _save_classes_colors_setting() + classes_colors.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: 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) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) - 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) + 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..df99987e 100644 --- a/src/ui/dtl/actions/contrast_brightness.py +++ b/src/ui/dtl/actions/contrast_brightness.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -11,14 +13,6 @@ class ContrastBrightnessAction(Action): 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..e138c8e3 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,7 +10,10 @@ Flexbox, OneOf, Checkbox, + Button, + Text, ) + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -19,11 +23,6 @@ class CropAction(Action): title = "Crop" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/crop" description = "This layer (crop) is used to crop part of image with its annotations. This layer has several modes: it may crop fixed given part of image or random one." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {"sides": None, "random_part": None} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -142,6 +141,14 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ] ) + mode_preview = Text("", color="white") + params_preview = Text("", color="white") + settings_preview = Container(widgets=[mode_preview, params_preview], gap=1) + + save_settings_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_settings = {} + def _set_sides(settings: dict): mode_select.set_value("sides") top_value = settings["sides"]["top"] @@ -212,46 +219,70 @@ def _get_random(): "keep_aspect_ratio": crop_random_keep_aspect_ratio.is_checked(), } - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" - settings = {} - if mode_select.get_value() == "sides": + def _update_preview(): + mode = saved_settings.get("mode", "") + if mode == "": + mode_preview.text = "" + params_preview.text = "" + elif mode == "sides": + sides = _get_sides() + mode_preview.text = "Mode: Sides" + params_preview.text = f"Top: {sides['top']}; Left: {sides['left']}; Right: {sides['right']}; Bottom: {sides['bottom']}" + elif mode == "random_part": + random_part = _get_random() + mode_preview.text = "Mode: Random part" + params_preview.text = f"Height: {random_part['height']['min_percent']} - {random_part['height']['max_percent']}; Width: {random_part['width']['min_percent']} - {random_part['width']['max_percent']}; Keep aspect ratio: {random_part['keep_aspect_ratio']}" + + def _save_settings(): + nonlocal saved_settings + settings = { + "mode": mode_select.get_value(), + } + if settings["mode"] == "sides": settings["sides"] = _get_sides() else: settings["random_part"] = _get_random() - return settings + saved_settings = settings + _update_preview() - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return saved_settings + + def _set_settings_from_json(settings): if "sides" in settings: _set_sides(settings) - else: + elif "random_part" in settings: _set_random(settings) - return node_state + _save_settings() + + save_settings_btn.click(_save_settings) - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Crop settings"), - ), - NodesFlow.Node.Option( - name="Set Settings", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - Container(widgets=[mode_select, OneOf(mode_select)]) - ) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Settings", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[mode_select, OneOf(mode_select), save_settings_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/data.py b/src/ui/dtl/actions/data.py index af9a779e..888b393d 100644 --- a/src/ui/dtl/actions/data.py +++ b/src/ui/dtl/actions/data.py @@ -1,15 +1,23 @@ import copy -import traceback from typing import List, Optional +import requests + +from supervisely.app.content import StateJson +from supervisely.app.widgets import NodesFlow, SelectDataset, Text, Button, Container, Flexbox +from supervisely import ProjectType, ProjectMeta, ObjClassCollection import src.utils as utils +import src.globals as g from src.ui.dtl import Action 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_separator, +) class DataAction(Action): @@ -17,13 +25,10 @@ class DataAction(Action): 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 +36,26 @@ 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( + "Select Datasets", + icon="zmdi zmdi-folder", + button_size="mini", + emit_on_click="openSidebar", + ) + src_save_btn = Button("Save", icon="zmdi zmdi-floppy") + src_preview_widget = Text("", color="white") + 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 +64,271 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ] = g.WORKSPACE_ID select_datasets._project_selector._ws_selector.disable() StateJson().send_changes() + + # Settings widgets classes_mapping_widget = ClassesMapping() + 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, + ), + ] + ) + + default_classes_mapping_settings = "default" + saved_classes_mapping_settings = "default" + + def _set_src_preview(): + src_preview_widget.text = str(saved_src) + + def _save_src(): + def read_src_from_widget(): + ids = select_datasets.get_selected_ids() + if ids is None or len(ids) == 0 or ids[0] is None: + ids = [] + project_info = None + dataset_names = [] + for id in ids: + dataset_info = utils.get_dataset_by_id(id=id) + if project_info is None: + project_info = utils.get_project_by_id(id=dataset_info.project_id) + dataset_names.append(dataset_info.name) + if project_info is None: + return [] + if project_info.datasets_count == len(dataset_names): + return [f"{project_info.name}/*"] + return [f"{project_info.name}/{name}" for name in dataset_names] + + nonlocal saved_src + saved_src = read_src_from_widget() 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"] - ] - 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 + return get_classes_mapping_value( + classes_mapping_widget, + default_action="keep", + ignore_action="keep", + other_allowed=True, + default_allowed=True, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="copy", + ignore_action="keep", + ) + + def _save_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + print(saved_classes_mapping_settings) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True + old_obj_classes = classes_mapping_widget.get_classes() + + # set classes to widget classes_mapping_widget.set(project_meta.obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + project_meta.obj_classes, + default_action="keep", + ignore_action="keep", + other_allowed=True, + ) + if saved_classes_mapping_settings == {}: + saved_classes_mapping_settings = "default" + + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False def get_src(options_json: dict) -> List[str]: - ids = select_datasets.get_selected_ids() - if ids is None or len(ids) == 0 or ids[0] is None: - ids = [] - project_info = None - dataset_names = [] - for id in ids: - dataset_info = utils.get_dataset_by_id(id=id) - if project_info is None: - project_info = utils.get_project_by_id(id=dataset_info.project_id) - dataset_names.append(dataset_info.name) - if project_info is None: - return [] - if project_info.datasets_count == len(dataset_names): - return [f"{project_info.name}/*"] - return [f"{project_info.name}/{name}" for name in dataset_names] + return saved_src 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""" - # set sources - srcs = json_data["src"] - if type(srcs) is str: - srcs = [srcs] - all_datasets = [] - first_project_name = None - select_all_datasets_flag = False - for src in srcs: - project_name, dataset_name = src.split("/") - if first_project_name is None: - first_project_name = project_name - elif first_project_name != project_name: - raise RuntimeError("All datasets should be from the same project") - project_info = utils.get_project_by_name(name=project_name) - if dataset_name == "*": - select_all_datasets_flag = True - datasets = utils.get_all_datasets(project_info.id) - else: - datasets = [utils.get_dataset_by_name(dataset_name, project_info.id)] - all_datasets.extend(datasets) - - workspace_info = utils.get_workspace_by_id(project_info.workspace_id) - StateJson()[select_datasets._project_selector._ws_selector._team_selector.widget_id][ - "teamId" - ] = workspace_info.team_id - StateJson()[select_datasets._project_selector._ws_selector.widget_id][ - "workspaceId" - ] = project_info.workspace_id - StateJson()[select_datasets._project_selector.widget_id]["projectId"] = project_info.id - StateJson()[select_datasets.widget_id]["datasets"] = [ds.id for ds in all_datasets] - if select_all_datasets_flag: - select_datasets._all_datasets_checkbox.check() - else: + def _set_src_from_json(srcs: List[str]): + nonlocal saved_src + if len(srcs) == 0: + # set empty src to widget + StateJson()[select_datasets._project_selector.widget_id]["projectId"] = None + StateJson()[select_datasets.widget_id]["datasets"] = [] select_datasets._all_datasets_checkbox.uncheck() - StateJson().send_changes() + StateJson().send_changes() + + # set empty project meta + project_meta = ProjectMeta() + else: + # get all datasets + first_project_name = None + datasets = [] + for src in srcs: + project_name, dataset_name = src.split("/") + if first_project_name is None: + first_project_name = project_name + elif first_project_name != project_name: + raise RuntimeError("All datasets should be from the same project") + project_info = utils.get_project_by_name(name=project_name) + if dataset_name == "*": + datasets.extend(utils.get_all_datasets(project_info.id)) + else: + datasets.append(utils.get_dataset_by_name(dataset_name, project_info.id)) + + # set datasets to widget + StateJson()[select_datasets._project_selector.widget_id][ + "projectId" + ] = project_info.id + StateJson()[select_datasets.widget_id]["datasets"] = [ds.id for ds in datasets] + if len(datasets) == project_info.datasets_count: + select_datasets._all_datasets_checkbox.check() + else: + select_datasets._all_datasets_checkbox.uncheck() + StateJson().send_changes() + + # get project meta + project_meta = utils.get_project_meta(project_info.id) - # load meta before setting classes mapping - meta_changed_cb(utils.get_project_meta(project_info.id)) + # save src + _save_src() + # set src preview + _set_src_preview() + # update meta + meta_changed_cb(project_meta) - # set settings - settings = json_data["settings"] - if settings["classes_mapping"] == "default": + def _set_settings_from_json(settings: dict): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": classes_mapping_widget.set_default() else: 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] + other_ignore = settings["classes_mapping"].get("__other__", None) == "__ignore__" + obj_classes = classes_mapping_widget.get_classes() + + # check that all the classes from settings are present in meta + if isinstance(obj_classes, ObjClassCollection): + in_func = lambda cls_name: obj_classes.has_key(cls_name) + else: + in_func = lambda cls_name: cls_name in [ + obj_class.name for obj_class in obj_classes + ] + for cls_name in settings["classes_mapping"].keys(): + if cls_name != "__other__" and not in_func(cls_name): + raise RuntimeError(f'Bad settings. "{cls_name}" not found in meta') + + # set classes mapping to widget + for obj_class in obj_classes: + if obj_class.name in settings["classes_mapping"]: + value = settings["classes_mapping"][obj_class.name] if value == "__default__": - value = cls.name + value = obj_class.name if value == "__ignore__": value = "" - classes_mapping[cls.name] = value + classes_mapping[obj_class.name] = value elif other_default: - classes_mapping[cls.name] = cls.name + classes_mapping[obj_class.name] = obj_class.name + elif other_ignore: + classes_mapping[obj_class.name] = "" else: - classes_mapping[cls.name] = "" + raise RuntimeError( + f'Bad settings. "{obj_class.name}" not found in classes_mapping' + ) classes_mapping_widget.set_mapping(classes_mapping) - return node_state - - @select_datasets.value_changed - def select_datasets_changed_cb(value): - if value is None or len(value) == 0 or value[0] is None: - return - try: - dataset_info = utils.get_dataset_by_id(id=value[0]) - project_info = utils.get_project_by_id(id=dataset_info.project_id) - project_meta = utils.get_project_meta(project_id=project_info.id) - meta_changed_cb(project_meta) - except Exception: - traceback.print_exc() - - options = [ - NodesFlow.Node.Option( - name="source_text", - option_component=NodesFlow.TextOptionComponent("Source"), - ), - NodesFlow.Node.Option( - name="Select Datasets", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(select_datasets) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @src_save_btn.click + def src_save_btn_cb(): + _save_src() + _set_src_preview() + g.updater("metas") + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: List[str], dst: List[str], settings: dict) -> dict: + _set_src_from_json(src) + _set_settings_from_json(settings) + + src_options = [ + NodesFlow.Node.Option( + name="Select Datasets", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent(src_widgets_container) + ), + # option_component=NodesFlow.WidgetOptionComponent( + # widget=Flexbox( + # widgets=[Text("Select Datasets", color="white"), select_datasets_btn] + # ), + # sidebar_component=NodesFlow.WidgetOptionComponent(src_widgets_container), + # ), + ), + NodesFlow.Node.Option( + name="src_preview", + option_component=NodesFlow.WidgetOptionComponent(src_preview_widget), ), - ), - 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) + get_separator(1), + ] + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ) + ), + ), + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + get_separator(2), + ] + return {"src": src_options, "dst": [], "settings": settings_options} return Layer( action=cls, id=layer_id, - options=options, + create_options=create_options, get_src=get_src, - get_dst=None, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/dataset.py b/src/ui/dtl/actions/dataset.py index 21d5c25d..3572c68b 100644 --- a/src/ui/dtl/actions/dataset.py +++ b/src/ui/dtl/actions/dataset.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -11,14 +13,6 @@ class DatasetAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/approx_vector" ) description = "This layer (dataset) places every image that it sees to dataset with a specified name. Put name of the future dataset in the field name." - # 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 = { - "rule": None, - "name": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -34,41 +28,38 @@ def get_settings(options_json: dict) -> dict: "name": name, } - 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: if "rule" in settings: - node_state["Save original"] = True + save_orig_val = True + name_val = "" else: - node_state["Save original"] = False - node_state["name"] = settings["name"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="rule_text", - option_component=NodesFlow.TextOptionComponent("Rule: Save Original"), - ), - NodesFlow.Node.Option( - name="Save original", option_component=NodesFlow.CheckboxOptionComponent() - ), - NodesFlow.Node.Option( - name="name_text", option_component=NodesFlow.TextOptionComponent("Name") - ), - NodesFlow.Node.Option(name="name", option_component=NodesFlow.InputOptionComponent()), - ] + save_orig_val = False + name_val = settings.get("name", "") + settings_options = [ + NodesFlow.Node.Option( + name="rule_text", + option_component=NodesFlow.TextOptionComponent("Rule: Save Original"), + ), + NodesFlow.Node.Option( + name="Save original", + option_component=NodesFlow.CheckboxOptionComponent(save_orig_val), + ), + NodesFlow.Node.Option( + name="name_text", option_component=NodesFlow.TextOptionComponent("Name") + ), + NodesFlow.Node.Option( + name="name", option_component=NodesFlow.InputOptionComponent(name_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/drop_lines_by_length.py b/src/ui/dtl/actions/drop_lines_by_length.py index c51a3de3..96d9a449 100644 --- a/src/ui/dtl/actions/drop_lines_by_length.py +++ b/src/ui/dtl/actions/drop_lines_by_length.py @@ -1,9 +1,11 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow, Button, Container +from supervisely import ProjectMeta, Polyline, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList -from supervisely.app.widgets import NodesFlow -from supervisely import ProjectMeta, Polyline, AnyGeometry +from src.ui.widgets import ClassesList, ClassesListPreview class DropLinesByLengthAction(Action): @@ -11,31 +13,34 @@ class DropLinesByLengthAction(Action): title = "Drop Lines by Length" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/drop_lines_by_length" description = "Layer drop_lines_by_length - remove too long or to short lines. Also this layer can drop lines with length in range. Lines with more than two points also supported. For multi-lines total length is calculated as sum of sections." - # 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 = { - "lines_class": None, - "resolution_compensation": "Resolution Compensation", - "invert": "Invert", - "min_length": None, - "max_length": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): _current_meta = ProjectMeta() classes = ClassesList(multiple=False) + classes_preview = ClassesListPreview() + classes_save_btn = Button("Save", icon="zmdi zmdi-floppy") - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + saved_classes_settings = "" + + def _save_classes_setting(): + nonlocal saved_classes_settings try: - lines_class = classes.get_selected_classes()[0].name + lines_class_obj = classes.get_selected_classes()[0] + line_class = lines_class_obj.name except: - lines_class = "" + lines_class_obj = None + line_class = "" + saved_classes_settings = line_class + if lines_class_obj is None: + classes_preview.set([]) + else: + classes_preview.set([lines_class_obj]) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" return { - "lines_class": lines_class, + "lines_class": saved_classes_settings, "resolution_compensation": bool(options_json["Resolution Compensation"]), "invert": bool(options_json["Invert"]), "min_length": options_json["min_length"] if options_json["Min Length"] else 0, @@ -54,70 +59,86 @@ def meta_changed_cb(project_meta: ProjectMeta): ] ) classes.loading = False + _save_classes_setting() _current_meta = project_meta - 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: dict): + if "lines_class" not in settings: + return obj_class_names = [settings["lines_class"]] classes.loading = True classes.select(obj_class_names) + _save_classes_setting() classes.loading = False - min_length = settings["min_length"] - node_state["min_length"] = min_length if min_length is not None else 1 - node_state["Min Length"] = min_length is not None - max_length = settings["max_length"] - node_state["max_length"] = max_length if max_length is not None else 1 - node_state["Max Length"] = max_length is not None - return node_state - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Line Class"), - ), - NodesFlow.Node.Option( - name="Select Class", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes) + classes_save_btn.click(_save_classes_setting) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + + min_length_flag = False + min_length_val = 1 + if "min_length" in settings: + min_length_flag = True + min_length_val = settings.get("min_length", 1) + max_length_flag = False + max_length_val = 1 + if "max_length" in settings: + max_length_flag = True + max_length_val = settings.get("max_length", 1) + invert_val = settings.get("invert", False) + resolution_compensation_val = settings.get("resolution_compensation", False) + + settings_options = [ + NodesFlow.Node.Option( + name="Select Class", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="Resolution Compensation", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=resolution_compensation_val + ), + ), + NodesFlow.Node.Option( + name="Invert", + option_component=NodesFlow.CheckboxOptionComponent(default_value=invert_val), + ), + NodesFlow.Node.Option( + name="Min Length", + option_component=NodesFlow.CheckboxOptionComponent(min_length_flag), + ), + NodesFlow.Node.Option( + name="min_length", + option_component=NodesFlow.IntegerOptionComponent( + min=0, default_value=min_length_val + ), + ), + NodesFlow.Node.Option( + name="Max Length", + option_component=NodesFlow.CheckboxOptionComponent(max_length_flag), ), - ), - NodesFlow.Node.Option( - name="Resolution Compensation", - option_component=NodesFlow.CheckboxOptionComponent(default_value=False), - ), - NodesFlow.Node.Option( - name="Invert", - option_component=NodesFlow.CheckboxOptionComponent(default_value=False), - ), - NodesFlow.Node.Option( - name="Min Length", - option_component=NodesFlow.CheckboxOptionComponent(), - ), - NodesFlow.Node.Option( - name="min_length", - option_component=NodesFlow.IntegerOptionComponent(min=0, default_value=1), - ), - NodesFlow.Node.Option( - name="Max Length", - option_component=NodesFlow.CheckboxOptionComponent(), - ), - NodesFlow.Node.Option( - name="max_length", - option_component=NodesFlow.IntegerOptionComponent(min=0, default_value=1), - ), - ] + NodesFlow.Node.Option( + name="max_length", + option_component=NodesFlow.IntegerOptionComponent( + min=0, default_value=max_length_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } + return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/drop_noise.py b/src/ui/dtl/actions/drop_noise.py index 1857950c..4e7df9df 100644 --- a/src/ui/dtl/actions/drop_noise.py +++ b/src/ui/dtl/actions/drop_noise.py @@ -1,9 +1,20 @@ from typing import Optional + +from supervisely.app.widgets import ( + NodesFlow, + Switch, + InputNumber, + OneOf, + Flexbox, + Button, + Container, + Text, +) +from supervisely import ProjectMeta, Bitmap, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList -from supervisely.app.widgets import NodesFlow, Switch, InputNumber, OneOf, Flexbox -from supervisely import ProjectMeta, Bitmap, AnyGeometry +from src.ui.widgets import ClassesList, ClassesListPreview class DropNoiseAction(Action): @@ -11,20 +22,11 @@ class DropNoiseAction(Action): title = "Drop Noise" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/drop_noise_from_bitmap" description = "This layer (drop_noise) removes connected components smaller than the specified size from bitmap annotations. This can be useful to eliminate noise after running neural network." - # 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, - "min_area": None, - "src_type": "Source type", - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): _current_meta = ProjectMeta() - classes = ClassesList(multiple=False) + classes = ClassesList(multiple=True) input_px = InputNumber(min=0) input_percent = InputNumber(min=0, max=100) px_or_percent_switch = Switch( @@ -38,11 +40,31 @@ def create_new_layer(cls, layer_id: Optional[str] = None): input_value = OneOf(px_or_percent_switch) min_area_widgets = Flexbox(widgets=[input_value, px_or_percent_switch]) + classes_preview = ClassesListPreview() + save_classes_btn = Button("Save", icon="zmdi zmdi-floppy") + + min_area_preview = Text("", color="white") + save_min_area_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] + saved_min_area_settings = "" + + def _save_classes_setting(): + nonlocal saved_classes_settings + selected_classes = classes.get_selected_classes() + saved_classes_settings = [cls.name for cls in selected_classes] + classes_preview.set(selected_classes) + + def _save_min_area_setting(): + nonlocal saved_min_area_settings + saved_min_area_settings = _get_min_area() + min_area_preview.text = f"Min Area: {saved_min_area_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": [cls.name for cls in classes.get_selected_classes()], - "min_area": _get_min_area(), + "classes": saved_classes_settings, + "min_area": saved_min_area_settings, "src_type": options_json["Source type"], } @@ -58,6 +80,7 @@ def meta_changed_cb(project_meta: ProjectMeta): ] ) classes.loading = False + _save_classes_setting() _current_meta = project_meta def _get_min_area(): @@ -74,57 +97,62 @@ def _set_min_area(value): px_or_percent_switch.off() input_percent.value = int(value[:-1]) - 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"] + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) classes.loading = True classes.select(obj_class_names) + _save_classes_setting() classes.loading = False min_area_widgets.loading = True - _set_min_area(settings["min_area"]) + _set_min_area(settings.get("min_area", "2%")) + _save_min_area_setting() min_area_widgets.loading = False - return node_state src_type_option_items = [ NodesFlow.SelectOptionComponent.Item("image", "Image"), NodesFlow.SelectOptionComponent.Item("bbox", "Bounding Box"), ] - 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) + + save_classes_btn.click(_save_classes_setting) + save_min_area_btn.click(_save_min_area_setting) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + src_type_val = settings.get("src_type", "image") + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, save_classes_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="Min Area", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(min_area_widgets) + NodesFlow.Node.Option( + name="Min Area", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[min_area_widgets, save_min_area_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="Source type", - option_component=NodesFlow.SelectOptionComponent( - items=src_type_option_items, default_value="image" + NodesFlow.Node.Option( + name="Source type", + option_component=NodesFlow.SelectOptionComponent( + items=src_type_option_items, default_value=src_type_val + ), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } + return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/drop_obj_by_class.py b/src/ui/dtl/actions/drop_obj_by_class.py index f3d0c89a..6dd0e827 100644 --- a/src/ui/dtl/actions/drop_obj_by_class.py +++ b/src/ui/dtl/actions/drop_obj_by_class.py @@ -1,9 +1,11 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow, Button, Container +from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList -from supervisely.app.widgets import NodesFlow -from supervisely import ProjectMeta +from src.ui.widgets import ClassesList, ClassesListPreview class DropByClassAction(Action): @@ -11,23 +13,26 @@ class DropByClassAction(Action): title = "Drop by Class" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/drop_obj_by_class" description = "This layer (drop_obj_by_class) simply removes annotations of specified classes. You can also use data layer and map unnecessary classes to __ignore__." - # 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): _current_meta = ProjectMeta() classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + save_classes_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes = [] + + def _save_classes_settings(): + nonlocal saved_classes + selected_classes = classes.get_selected_classes() + saved_classes = [cls.name for cls in selected_classes] + classes_preview.set(selected_classes) def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [obj_class.name for obj_class in classes.get_selected_classes()], + "classes": saved_classes, } def meta_changed_cb(project_meta: ProjectMeta): @@ -35,41 +40,41 @@ def meta_changed_cb(project_meta: ProjectMeta): if project_meta.obj_classes != _current_meta.obj_classes: classes.loading = True classes.set(project_meta.obj_classes) + _save_classes_settings() classes.loading = False _current_meta = project_meta - 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"] + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) classes.loading = True classes.select(obj_class_names) + _save_classes_settings() classes.loading = False - return node_state - 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) + save_classes_btn.click(_save_classes_settings) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, save_classes_btn]) + ) + ), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } + return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/dummy.py b/src/ui/dtl/actions/dummy.py index 6086be10..f8390f59 100644 --- a/src/ui/dtl/actions/dummy.py +++ b/src/ui/dtl/actions/dummy.py @@ -1,7 +1,7 @@ from typing import Optional + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely.app.widgets import NodesFlow class DummyAction(Action): @@ -9,26 +9,22 @@ class DummyAction(Action): title = "Dummy" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/dummy" description = "This layer (dummy) does nothing. Literally. Dummy layer can be useful when you have many destinations of other layers you want to merge into 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" return {} - options = [] + def create_options(src: list, dst: list, settings: dict) -> dict: + return { + "src": [], + "dst": [], + "settings": [], + } + return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/duplicate_objects.py b/src/ui/dtl/actions/duplicate_objects.py index de868d16..d66a746f 100644 --- a/src/ui/dtl/actions/duplicate_objects.py +++ b/src/ui/dtl/actions/duplicate_objects.py @@ -1,10 +1,11 @@ -import copy from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class DuplicateObjectsAction(Action): @@ -12,17 +13,14 @@ class DuplicateObjectsAction(Action): title = "Duplicate Objects" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/duplicate_objects" description = "This layer (duplicate_objects) clones figures of required classes." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -33,20 +31,29 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_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 saved_classes_mapping_settings classes_mapping_widget.loading = True classes_mapping_widget.set(project_meta.obj_classes) + classes_mapping_value = _get_classes_mapping_value() + saved_classes_mapping_settings = classes_mapping_value + classes_mapping_preview.set(project_meta.obj_classes, classes_mapping_value) 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""" - settings = copy.deepcopy(json_data["settings"]) + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -62,32 +69,32 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping_setting() - 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) + classes_mapping_save_btn.click(_save_classes_mapping_setting) + + 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/find_contours.py b/src/ui/dtl/actions/find_contours.py index 15195375..cf32e025 100644 --- a/src/ui/dtl/actions/find_contours.py +++ b/src/ui/dtl/actions/find_contours.py @@ -1,10 +1,11 @@ -import copy from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta, Bitmap, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class FindContoursAction(Action): @@ -16,17 +17,19 @@ class FindContoursAction(Action): description = ( "This layer (find_contours) extracts contours from bitmaps and stores results as polygons." ) - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_classes_mapping_settings) def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -40,23 +43,23 @@ def _get_classes_mapping_value(): def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Bitmap, AnyGeometry] - ] - ) + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Bitmap, AnyGeometry] + ] + classes_mapping_widget.set(obj_classes) + classes_mapping_preview.set(obj_classes, _get_classes_mapping_value()) 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""" - settings = copy.deepcopy(json_data["settings"]) + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -72,32 +75,36 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping_setting() - 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) + classes_mapping_save_btn.click(_save_classes_mapping_setting) + + 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="Classes Mapping Preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/flip.py b/src/ui/dtl/actions/flip.py index 7bef6034..4cec2046 100644 --- a/src/ui/dtl/actions/flip.py +++ b/src/ui/dtl/actions/flip.py @@ -1,7 +1,9 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely.app.widgets import NodesFlow class FlipAction(Action): @@ -11,11 +13,6 @@ class FlipAction(Action): description = ( "Flip layer (flip) simply flips data (image + annotation) vertically or horizontally." ) - # 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -29,29 +26,30 @@ def get_settings(options_json: dict) -> dict: NodesFlow.SelectOptionComponent.Item("vertical", "Vertical"), NodesFlow.SelectOptionComponent.Item("horizontal", "Horizontal"), ] - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="axis_text", - option_component=NodesFlow.TextOptionComponent("Axis"), - ), - NodesFlow.Node.Option( - name="axis", - option_component=NodesFlow.SelectOptionComponent( - axis_option_items, default_value=axis_option_items[0].value + + def create_options(src: list, dst: list, settings: dict) -> dict: + axis_val = settings.get("axis", "vertical") + settings_options = [ + NodesFlow.Node.Option( + name="axis_text", + option_component=NodesFlow.TextOptionComponent("Axis"), ), - ), - ] + NodesFlow.Node.Option( + name="axis", + option_component=NodesFlow.SelectOptionComponent( + axis_option_items, default_value=axis_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=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/if_action.py b/src/ui/dtl/actions/if_action.py index de88ac04..a6840f9a 100644 --- a/src/ui/dtl/actions/if_action.py +++ b/src/ui/dtl/actions/if_action.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely.app.widgets import ( NodesFlow, Select, @@ -8,11 +9,14 @@ Field, Input, OneOf, + Button, + Text, ) from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview, TagMetasPreview class IfAction(Action): @@ -22,23 +26,20 @@ class IfAction(Action): description = ( "This layer (if) is used to split input data to several flows with a specified criterion." ) - # 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 = { - "condition": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): class Condition: - def __init__(self, name, title, widget, get_func, set_func): + def __init__( + self, name, title, widget, get_func, set_func, preview_widget, set_preview_func + ): self.name = name self.title = title self.widget = widget self.get_func = get_func self.set_func = set_func + self.preview_widget = preview_widget + self.set_preview_func = set_preview_func def item(self): return Select.Item(self.name, self.title, self.widget) @@ -50,6 +51,12 @@ def set(self, value): self.set_func(value) select_condition.set_value(self.name) + def preview_item(self): + return Select.Item(self.name, self.title, self.preview_widget) + + def set_preview(self): + self.set_preview_func() + # probabilty _prob_input = InputNumber(min=0, max=100, precision=3) _probability_condition_widget = Field( @@ -64,12 +71,19 @@ def _get_prob_value(): def _set_prob_value(condition_json): _prob_input.value = condition_json["probability"] * 100 + _prob_preview_widget = Text("", color="white") + + def _set_prob_preview(): + _prob_preview_widget.text = f"Probability: {_get_prob_value()}" + probability_condition = Condition( name="probability", title="Probability", widget=_probability_condition_widget, get_func=_get_prob_value, set_func=_set_prob_value, + preview_widget=_prob_preview_widget, + set_preview_func=_set_prob_preview, ) # min objects count @@ -83,12 +97,21 @@ def _set_prob_value(condition_json): def _set_min_obj_count_value(condition_json): _min_objects_count_input.value = condition_json["min_objects_count"] + _min_objects_preview_widget = Text("", color="white") + + def _set_min_obj_preview(): + _min_objects_preview_widget.text = ( + f"Min objects count: {_min_objects_count_input.get_value()}" + ) + min_objects_count_condition = Condition( name="min_objects_count", title="Min objects count", widget=_min_objects_count_condition_widget, get_func=_min_objects_count_input.get_value, set_func=_set_min_obj_count_value, + preview_widget=_min_objects_preview_widget, + set_preview_func=_set_min_obj_preview, ) # min height @@ -102,12 +125,19 @@ def _set_min_obj_count_value(condition_json): def _set_min_height_value(condition_json): _min_height_input.value = condition_json["min_height"] + _min_height_preview_widget = Text("", color="white") + + def _min_height_preview(): + _min_height_preview_widget.text = f"Min height: {_min_height_input.get_value()}" + min_height_condition = Condition( name="min_height", title="Min height", widget=_min_height_condition_widget, get_func=_min_height_input.get_value, set_func=_set_min_height_value, + preview_widget=_min_height_preview_widget, + set_preview_func=_min_height_preview, ) # tags @@ -121,12 +151,27 @@ def _set_min_height_value(condition_json): def _set_tags_value(condition_json): _select_tags_input.set_names(condition_json["tags"]) + _tags_preview_widget = TagMetasPreview() + + def _set_tags_preview(): + names = _get_tags_value() + _tags_preview_widget.set( + [_select_tags_input.get_tag_meta_by_name(name) for name in names] + ) + + def _get_tags_value(): + return [name for name in _select_tags_input.get_selected_names() if name] + select_tags_condition = Condition( name="tags", title="Tags", widget=_select_tags_widget, - get_func=lambda: [tm.name for tm in _select_tags_input.get_selected_items()], + get_func=_get_tags_value, set_func=_set_tags_value, + preview_widget=Container( + widgets=[Text("Include Tags", color="white"), _tags_preview_widget], gap=1 + ), + set_preview_func=_set_tags_preview, ) # include classes @@ -140,12 +185,22 @@ def _set_tags_value(condition_json): def _set_include_classes_value(condition_json): _include_classes_input.select(condition_json["include_classes"]) + _include_classes_preview_widget = ClassesListPreview() + + def _set_include_classes_preview(): + _include_classes_preview_widget.set(_include_classes_input.get_selected_classes()) + select_classes_condition = Condition( name="include_classes", title="Include classes", widget=_include_classes_widget, get_func=lambda: [oc.name for oc in _include_classes_input.get_selected_classes()], set_func=_set_include_classes_value, + preview_widget=Container( + widgets=[Text("Include Classes:", color="white"), _include_classes_preview_widget], + gap=1, + ), + set_preview_func=_set_include_classes_preview, ) # name in range @@ -172,18 +227,36 @@ def _set_names_in_range_value(condition_json): _names_in_range_inputs["name_to"].set_value(name_to) _names_in_range_inputs["step"].value = condition_json["frame_step"] - names_in_range_condition = Condition( - name="name_in_range", - title="Name in range", - widget=_names_in_range_widget, - get_func=lambda: { + def _get_names_in_range_value(): + return { "name_in_range": [ _names_in_range_inputs["name_from"].get_value(), _names_in_range_inputs["name_to"].get_value(), ], "frame_step": _names_in_range_inputs["step"].get_value(), - }, + } + + _names_in_range_preview_range = Text("", color="white") + _names_in_range_preview_step = Text("", color="white") + _names_in_range_preview_widget = Container( + widgets=[_names_in_range_preview_range, _names_in_range_preview_step], gap=1 + ) + + def _set_names_in_range_preview(): + value = _get_names_in_range_value() + _names_in_range_preview_range.text = ( + f"Name in range: from {value['name_in_range'][0]} to {value['name_in_range'][1]}" + ) + _names_in_range_preview_step.text = f"Step: {value['frame_step']}" + + names_in_range_condition = Condition( + name="name_in_range", + title="Name in range", + widget=_names_in_range_widget, + get_func=_get_names_in_range_value, set_func=_set_names_in_range_value, + preview_widget=_names_in_range_preview_widget, + set_preview_func=_set_names_in_range_preview, ) conditions = { @@ -201,10 +274,19 @@ def _set_names_in_range_value(condition_json): select_condition_items = [condition.item() for condition in conditions.values()] select_condition = Select(items=select_condition_items) condition_input = OneOf(select_condition) + + preview_items = [condition.preview_item() for condition in conditions.values()] + _select_preview = Select(items=preview_items) + settings_preview = OneOf(_select_preview) + save_settings_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_settings = {} + widget = Container( widgets=[ Field(title="Condition", content=select_condition), Field(title="Condition value", content=condition_input), + save_settings_btn, ] ) @@ -212,61 +294,74 @@ def _get_condition_value(condition_name: str): condition = conditions[condition_name] return condition.get() - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + def _set_preview(condition_name: str): + condition = conditions[condition_name] + condition.set_preview() + + def _save_settings(): + nonlocal saved_settings condition_name = select_condition.get_value() condition_value = _get_condition_value(condition_name) - return { + saved_settings = { "condition": {condition_name: condition_value}, } + _set_preview(condition_name) + _select_preview.set_value(condition_name) + + 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 meta_changed_cb(project_meta: ProjectMeta): _include_classes_input.loading = True _select_tags_input.loading = True _include_classes_input.set(project_meta.obj_classes) - # _select_tags_input.set(project_meta=project_meta) # TODO: Add set() method to SelectTagMeta widget in SDK + _select_tags_input.set_project_meta(project_meta=project_meta) _include_classes_input.loading = False _select_tags_input.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""" - condition_json = json_data["settings"]["condition"] - condition_name, _ = list(json_data["settings"]["condition"].items())[0] + def _set_settings_from_json(settings: dict): + if "condition" not in settings: + return + condition_json = settings["condition"] + condition_name, _ = list(settings["condition"].items())[0] condition = conditions[condition_name] condition.set(condition_json) - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="condition_text", - option_component=NodesFlow.TextOptionComponent("Condition"), - ), - NodesFlow.Node.Option( - name="Set Condition", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(widget) + _save_settings() + + 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 Condition", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent(widget) + ), ), - ), - ] + NodesFlow.Node.Option( + name="Settings Preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) @classmethod def create_outputs(cls): return [ - NodesFlow.Node.Output("destination_true", "Destination True"), - NodesFlow.Node.Output("destination_false", "Destination False"), + NodesFlow.Node.Output("destination_true", "Output True"), + NodesFlow.Node.Output("destination_false", "Output False"), ] diff --git a/src/ui/dtl/actions/instances_crop.py b/src/ui/dtl/actions/instances_crop.py index aac71d7f..7b4eb632 100644 --- a/src/ui/dtl/actions/instances_crop.py +++ b/src/ui/dtl/actions/instances_crop.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely import ProjectMeta from supervisely.app.widgets import ( NodesFlow, @@ -8,10 +9,13 @@ Field, Flexbox, OneOf, + Button, + Text, ) + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview class InstancesCropAction(Action): @@ -21,18 +25,12 @@ class InstancesCropAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/instances_crop" ) description = "This layer (instances_crop) crops objects of specified classes from image with configurable padding. So from one image there can be produced multiple images, each with one target object: other objects are removed from crop." - # 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, - "pad": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_btn = Button("Save", icon="zmdi zmdi-floppy") padding_top_px = InputNumber(min=0) padding_top_percent = InputNumber(min=0, max=100) @@ -74,6 +72,8 @@ def create_new_layer(cls, layer_id: Optional[str] = None): on_content=padding_bot_px, off_content=padding_bot_percent, ) + padding_preview = Text("", color="white") + save_padding_btn = Button("Save", icon="zmdi zmdi-floppy") padding_container = Container( widgets=[ Field( @@ -92,6 +92,7 @@ def create_new_layer(cls, layer_id: Optional[str] = None): title="bottom", content=Flexbox(widgets=[OneOf(padding_bot_switch), padding_bot_switch]), ), + save_padding_btn, ] ) @@ -114,6 +115,8 @@ def _get_padding(): } def _set_padding(settings: dict): + if "pad" not in settings: + return padding = settings["pad"]["sides"] top_value = padding["top"] if top_value.endswith("px"): @@ -144,62 +147,82 @@ def _set_padding(settings: dict): padding_bot_percent.value = int(bot_value[:-1]) padding_bot_switch.off() + saved_classes_settings = [] + saved_padding_settings = {} + + def _save_classes(): + nonlocal saved_classes_settings + obj_classes = classes.get_selected_classes() + saved_classes_settings = [cls.name for cls in obj_classes] + classes_preview.set(obj_classes) + + def _save_padding(): + nonlocal saved_padding_settings + saved_padding_settings = _get_padding() + padding_preview.text = f'Top: {saved_padding_settings["top"], "Left: ", saved_padding_settings["left"], "Right: ", saved_padding_settings["right"], "Bottom: ", saved_padding_settings["bottom"]}' + def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [cls.name for cls in classes.get_selected_classes()], - "pad": _get_padding(), + "classes": saved_classes_settings, + "pad": saved_padding_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: dict): classes.loading = True padding_container.loading = True - classes.select(settings["classes"]) + classes.select(settings.get("classes", [])) _set_padding(settings) + _save_classes() + _save_padding() classes.loading = False padding_container.loading = False - return node_state def meta_changed_cb(project_meta: ProjectMeta): classes.loading = True classes.set(project_meta.obj_classes) + _save_classes() 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) + save_padding_btn.click(_save_padding) + classes_save_btn.click(_save_classes) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="classes_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_preview), ), - ), - NodesFlow.Node.Option( - name="padding_text", - option_component=NodesFlow.TextOptionComponent("Padding"), - ), - NodesFlow.Node.Option( - name="Set Padding", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(padding_container) + NodesFlow.Node.Option( + name="Set Padding", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent(padding_container) + ), ), - ), - ] + NodesFlow.Node.Option( + name="padding_preview", + option_component=NodesFlow.WidgetOptionComponent(padding_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/line2bitmap.py b/src/ui/dtl/actions/line2bitmap.py index 34e2861f..410b5559 100644 --- a/src/ui/dtl/actions/line2bitmap.py +++ b/src/ui/dtl/actions/line2bitmap.py @@ -1,10 +1,11 @@ -import copy from typing import Optional + from supervisely import ProjectMeta, Polyline, AnyGeometry -from supervisely.app.widgets import NodesFlow +from supervisely.app.widgets import NodesFlow, Button, Container + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class LineToBitmapAction(Action): @@ -14,17 +15,14 @@ class LineToBitmapAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/line2bitmap" ) description = "This layer (line2bitmap) converts geometry figures (lines) to bitmaps." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -35,16 +33,21 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping(): + nonlocal saved_classes_mapping + saved_classes_mapping = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_classes_mapping) + 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, "width": options_json["width"], } - 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: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -60,51 +63,50 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping() def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Polyline, AnyGeometry] - ] - ) + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Polyline, AnyGeometry] + ] + classes_mapping_widget.set(obj_classes) + classes_mapping_preview.set(obj_classes, _get_classes_mapping_value()) 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) + classes_mapping_save_btn.click(_save_classes_mapping) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + width_val = settings.get("width", 1) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="width_text", - option_component=NodesFlow.TextOptionComponent("Width"), - ), - NodesFlow.Node.Option( - name="width", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=1), - ), - ] + NodesFlow.Node.Option( + name="width", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=width_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/merge_bitmaps.py b/src/ui/dtl/actions/merge_bitmaps.py index d3e0d353..2c3a6e89 100644 --- a/src/ui/dtl/actions/merge_bitmaps.py +++ b/src/ui/dtl/actions/merge_bitmaps.py @@ -1,10 +1,12 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta from supervisely import Bitmap, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview class MergeBitmapsAction(Action): @@ -14,36 +16,38 @@ class MergeBitmapsAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/merge_masks" ) description = "This layer (merge_bitmap_masks) takes all bitmap annotations which has same class name and merge it into single bitmap annotation." - # 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": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=False) + classes_preview = ClassesListPreview() + classes_save_btn = Button("Save", icon="zmdi zmdi-floppy") - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + saved_classes_settings = "" + + def _save_classes_settings(): + nonlocal saved_classes_settings try: - class_name = classes.get_selected_classes()[0].name + selected_class = classes.get_selected_classes()[0] + class_name = selected_class.name except: - class_name = None + selected_class = None + class_name = "" + saved_classes_settings = class_name + classes_preview.set([selected_class] if selected_class else []) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" return { - "class": class_name, + "class": saved_classes_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"] - obj_class_names = [settings["class"]] + def _set_settings_from_json(settings: dict): + obj_class_names = [settings["class"]] if "class" in settings else [] classes.loading = True classes.select(obj_class_names) + _save_classes_settings() classes.loading = False - return node_state def meta_changed_cb(project_meta: ProjectMeta): classes.loading = True @@ -54,32 +58,37 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Bitmap, AnyGeometry] ] ) + _save_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) + classes_save_btn.click(_save_classes_settings) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_btn]) + ) + ), ), - ), - ] + NodesFlow.Node.Option( + name="classes_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_preview), + ), + ] + 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/multiply.py b/src/ui/dtl/actions/multiply.py index d88c3b2f..e1d2ac33 100644 --- a/src/ui/dtl/actions/multiply.py +++ b/src/ui/dtl/actions/multiply.py @@ -1,7 +1,9 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely.app.widgets import NodesFlow class MultiplyAction(Action): @@ -9,11 +11,6 @@ class MultiplyAction(Action): title = "Multiply" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/multiply" description = "This layer (multiply) duplicates data (image + annotation)." - # 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -23,27 +20,29 @@ def get_settings(options_json: dict) -> dict: "multiply": options_json["multiply"], } - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="multiply_text", - option_component=NodesFlow.TextOptionComponent("Multiply number"), - ), - NodesFlow.Node.Option( - name="multiply", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=1), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + multiply_val = settings.get("multiply", 1) + settings_options = [ + NodesFlow.Node.Option( + name="multiply_text", + option_component=NodesFlow.TextOptionComponent("Multiply number"), + ), + NodesFlow.Node.Option( + name="multiply", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=multiply_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=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/noise.py b/src/ui/dtl/actions/noise.py index 30c7f415..aa56df97 100644 --- a/src/ui/dtl/actions/noise.py +++ b/src/ui/dtl/actions/noise.py @@ -1,7 +1,9 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely.app.widgets import NodesFlow class NoiseAction(Action): @@ -9,11 +11,6 @@ class NoiseAction(Action): title = "Noise" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/noise" description = "Noise layer (noise) adds noise of Gaussian distribution to the 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -24,35 +21,36 @@ def get_settings(options_json: dict) -> dict: "std": options_json["std"], } - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="mean_text", - option_component=NodesFlow.TextOptionComponent("Mean"), - ), - NodesFlow.Node.Option( - name="mean", - option_component=NodesFlow.NumberOptionComponent(default_value=10), - ), - NodesFlow.Node.Option( - name="std_text", - option_component=NodesFlow.TextOptionComponent("Spread"), - ), - NodesFlow.Node.Option( - name="std", - option_component=NodesFlow.NumberOptionComponent(default_value=50), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + mean_val = settings.get("mean", 10) + std_val = settings.get("std", 50) + settings_options = [ + NodesFlow.Node.Option( + name="mean_text", + option_component=NodesFlow.TextOptionComponent("Mean"), + ), + NodesFlow.Node.Option( + name="mean", + option_component=NodesFlow.NumberOptionComponent(default_value=mean_val), + ), + NodesFlow.Node.Option( + name="std_text", + option_component=NodesFlow.TextOptionComponent("Spread"), + ), + NodesFlow.Node.Option( + name="std", + option_component=NodesFlow.NumberOptionComponent(default_value=std_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=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/objects_filter.py b/src/ui/dtl/actions/objects_filter.py index 9fd4df63..567ac163 100644 --- a/src/ui/dtl/actions/objects_filter.py +++ b/src/ui/dtl/actions/objects_filter.py @@ -1,9 +1,20 @@ from typing import Optional + +from supervisely import ProjectMeta +from supervisely.app.widgets import ( + NodesFlow, + Select, + Container, + InputNumber, + Field, + OneOf, + Button, + Text, +) + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely import ProjectMeta -from supervisely.app.widgets import NodesFlow, Select, Container, InputNumber, Field, OneOf -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview class ObjectsFilterAction(Action): @@ -13,13 +24,6 @@ class ObjectsFilterAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/objects_filter" ) description = "This layer (objects_filter) deletes annotations less (or greater) than specified size or percentage of image area." - # 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 = { - "filter_by": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -81,36 +85,102 @@ def create_new_layer(cls, layer_id: Optional[str] = None): filter_by_select = Select(filter_items) filter_by_inputs = OneOf(filter_by_select) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + filter_by_preview_text = Text("", color="white") + filter_preview_classes_text = Text("Classes:", color="white") + classes_preview = ClassesListPreview() + area_size_preview = Text("", color="white") + comparator_preview = Text("", color="white") + action_preview = Text("", color="white") + + filter_by_name_preview_container = Container( + widgets=[filter_by_preview_text, filter_preview_classes_text, classes_preview], gap=1 + ) + filter_by_size_preview_container = Container( + widgets=[ + filter_by_preview_text, + filter_preview_classes_text, + classes_preview, + area_size_preview, + comparator_preview, + action_preview, + ], + gap=1, + ) + + _settings_preview_select = Select( + items=[ + Select.Item("names", "names", filter_by_name_preview_container), + Select.Item("polygon_sizes", "polygon_sizes", filter_by_size_preview_container), + ] + ) + settings_preview = OneOf(_settings_preview_select) + + settings_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_settings = {} + + def _set_preview(): + nonlocal saved_settings + if "filter_by" not in saved_settings: + return + filter_by = saved_settings["filter_by"] + if "names" in filter_by: + _settings_preview_select.set_value("names") + names = saved_settings["filter_by"]["names"] + obj_classes = [cls for cls in classes.get_all_classes() if cls.name in names] + classes_preview.set(obj_classes) + else: + _settings_preview_select.set_value("polygon_sizes") + names = saved_settings["filter_by"]["polygon_sizes"]["filtering_classes"] + obj_classes = [cls for cls in classes.get_all_classes() if cls.name in names] + classes_preview.set(obj_classes) + comparator_preview.text = ( + f"Comparator: {saved_settings['filter_by']['polygon_sizes']['comparator']}" + ) + action_preview.text = ( + f"Action: {saved_settings['filter_by']['polygon_sizes']['action']}" + ) + if "percent" in filter_by["polygon_sizes"]["area_size"]: + area_size_preview.text = f"Area size: {saved_settings['filter_by']['polygon_sizes']['area_size']['percent']}%" + else: + area_size_preview.text = f"Area size: width = {saved_settings['filter_by']['polygon_sizes']['area_size']['width']} x height = {saved_settings['filter_by']['polygon_sizes']['area_size']['height']}" + + def _save_settings(): + nonlocal saved_settings settings = {} filter_by = filter_by_select.get_value() if filter_by == "names": settings["filter_by"] = { "names": [cls.name for cls in classes.get_selected_classes()], } - return settings - settings["filter_by"] = { - "polygon_sizes": { - "filtering_classes": [cls.name for cls in classes.get_selected_classes()], - "action": action_select.get_value(), - "comparator": comparator_select.get_value(), - }, - } - if filter_by == "area_percent": - settings["filter_by"]["polygon_sizes"]["area_size"] = { - "percent": percent_input.get_value(), - } - elif filter_by == "bbox_size": - settings["filter_by"]["polygon_sizes"]["area_size"] = { - "width": width_input.get_value(), - "height": height_input.get_value(), + saved_settings = settings + else: + settings["filter_by"] = { + "polygon_sizes": { + "filtering_classes": [cls.name for cls in classes.get_selected_classes()], + "action": action_select.get_value(), + "comparator": comparator_select.get_value(), + }, } - return settings + if filter_by == "area_percent": + settings["filter_by"]["polygon_sizes"]["area_size"] = { + "percent": percent_input.get_value(), + } + elif filter_by == "bbox_size": + settings["filter_by"]["polygon_sizes"]["area_size"] = { + "width": width_input.get_value(), + "height": height_input.get_value(), + } + saved_settings = settings + _set_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: dict): + if "filter_by" not in settings: + return if "names" in settings["filter_by"]: classes.loading = True filter_by_select.loading = True @@ -144,7 +214,7 @@ def set_settings_from_json(json_data: dict, node_state: dict): percent_input.loading = False classes.loading = False comparator_select.loading = False - return node_state + _save_settings() prev_project_meta = ProjectMeta() @@ -154,39 +224,44 @@ def meta_changed_cb(project_meta: ProjectMeta): return classes.loading = True classes.set(project_meta.obj_classes) + _save_settings() classes.loading = False prev_project_meta = project_meta - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="filter_by_text", - option_component=NodesFlow.TextOptionComponent("Objects Filter settings:"), - ), - NodesFlow.Node.Option( - name="Set Filter", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - Container( - widgets=[ - Field(title="Filter by", content=filter_by_select), - filter_by_inputs, - ] + settings_save_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 Filter", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container( + widgets=[ + Field(title="Filter by", content=filter_by_select), + filter_by_inputs, + settings_save_btn, + ] + ) ) - ) + ), ), - ), - ] + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } + return Layer( action=cls, - options=options, + 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=None, - id=layer_id, ) diff --git a/src/ui/dtl/actions/poly2bitmap.py b/src/ui/dtl/actions/poly2bitmap.py index c23674da..d9a9f6ef 100644 --- a/src/ui/dtl/actions/poly2bitmap.py +++ b/src/ui/dtl/actions/poly2bitmap.py @@ -1,10 +1,11 @@ -import copy from typing import Optional + from supervisely import ProjectMeta, Polygon, AnyGeometry -from supervisely.app.widgets import NodesFlow +from supervisely.app.widgets import NodesFlow, Button, Container + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class PolygonToBitmapAction(Action): @@ -14,17 +15,14 @@ class PolygonToBitmapAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/poly2bitmap" ) description = "This layer (poly2bitmap) converts annotations of shape polygon to shape bitmap." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -35,15 +33,20 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_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""" - settings = json_data["settings"] + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -59,43 +62,47 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping() def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Polygon, AnyGeometry] - ] - ) + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Polygon, AnyGeometry] + ] + classes_mapping_widget.set(obj_classes) + classes_mapping_preview.set(obj_classes, _get_classes_mapping_value()) 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) + classes_mapping_save_btn.click(_save_classes_mapping) + + def create_options(src: dict, dst: dict, 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ), + ), ), - ), - ] + 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/random_color.py b/src/ui/dtl/actions/random_color.py index 43515c16..fb3b14bf 100644 --- a/src/ui/dtl/actions/random_color.py +++ b/src/ui/dtl/actions/random_color.py @@ -1,7 +1,9 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from supervisely.app.widgets import NodesFlow class RandomColorsAction(Action): @@ -13,11 +15,6 @@ class RandomColorsAction(Action): description = ( "This layer (random_color) changes image colors by random moving each of RGB components." ) - # 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 = {} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -27,27 +24,29 @@ def get_settings(options_json: dict) -> dict: "strength": options_json["strength"], } - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="strength_text", - option_component=NodesFlow.TextOptionComponent("Strength"), - ), - NodesFlow.Node.Option( - name="strength", - option_component=NodesFlow.SliderOptionComponent(min=0, max=1, default_value=0.25), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + str_val = settings.get("strength", 0.25) + settings_options = [ + NodesFlow.Node.Option( + name="strength_text", + option_component=NodesFlow.TextOptionComponent("Strength"), + ), + NodesFlow.Node.Option( + name="strength", + option_component=NodesFlow.SliderOptionComponent( + min=0, max=1, default_value=str_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=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/rasterize.py b/src/ui/dtl/actions/rasterize.py index 4a94967a..8fdaf909 100644 --- a/src/ui/dtl/actions/rasterize.py +++ b/src/ui/dtl/actions/rasterize.py @@ -1,10 +1,11 @@ -import copy from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class RasterizeAction(Action): @@ -14,17 +15,14 @@ class RasterizeAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/rasterize" ) description = "This layer (rasterize) converts all geometry figures to bitmaps." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -35,20 +33,26 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_classes_mapping_settings) + def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True classes_mapping_widget.set(project_meta.obj_classes) + classes_mapping_preview.set(project_meta.obj_classes, _get_classes_mapping_value()) 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""" - settings = copy.deepcopy(json_data["settings"]) + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -64,32 +68,36 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping() - 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) + classes_mapping_save_btn.click(_save_classes_mapping) + + 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/rename.py b/src/ui/dtl/actions/rename.py index f0f601d1..17e84b8f 100644 --- a/src/ui/dtl/actions/rename.py +++ b/src/ui/dtl/actions/rename.py @@ -1,11 +1,11 @@ from typing import Optional from supervisely import ProjectMeta -from supervisely.app.widgets import NodesFlow +from supervisely.app.widgets import NodesFlow, Button, Container from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview class RenameAction(Action): @@ -13,15 +13,14 @@ class RenameAction(Action): title = "Rename" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/rename" description = "This layer (rename) re-maps existing classes." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {"classes_mapping": None} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_mapping_settings = {} def _get_classes_mapping_value(): mapping = classes_mapping_widget.get_mapping() @@ -32,15 +31,20 @@ def _get_classes_mapping_value(): } return values + def _save_classes_mapping(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + classes_mapping_preview.set_mapping(saved_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""" - settings = json_data["settings"] + def _set_settings_from_json(settings: dict): + if "classes_mapping" not in settings: + return classes_mapping = {} other_default = settings["classes_mapping"].get("__other__", None) == "__default__" for cls in classes_mapping_widget.get_classes(): @@ -56,37 +60,42 @@ def set_settings_from_json(json_data: dict, node_state: dict): else: classes_mapping[cls.name] = "" classes_mapping_widget.set_mapping(classes_mapping) - return node_state + _save_classes_mapping() def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True classes_mapping_widget.set(project_meta.obj_classes) + classes_mapping_preview.set(project_meta.obj_classes, _get_classes_mapping_value()) 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) + classes_mapping_save_btn.click(_save_classes_mapping) + + 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( + Container(widgets=[classes_mapping_widget, classes_mapping_save_btn]) + ) + ), + ), + 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/resize.py b/src/ui/dtl/actions/resize.py index 5ecc75f8..b2912aac 100644 --- a/src/ui/dtl/actions/resize.py +++ b/src/ui/dtl/actions/resize.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -9,13 +11,6 @@ class ResizeAction(Action): title = "Resize" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/resize" description = "Resize layer (resize) resizes data (image + annotation) to the certain size." - # 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 = { - "aspect_ratio": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -43,46 +38,47 @@ def get_settings(options_json: dict) -> dict: }, } - 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"] - node_state["Keep aspect ratio"] = settings["aspect_ratio"]["keep"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="width_text", - option_component=NodesFlow.TextOptionComponent("Width"), - ), - NodesFlow.Node.Option( - name="width", - option_component=NodesFlow.IntegerOptionComponent(min=-1, default_value=1), - ), - NodesFlow.Node.Option( - name="height_text", - option_component=NodesFlow.TextOptionComponent("Height"), - ), - NodesFlow.Node.Option( - name="height", - option_component=NodesFlow.IntegerOptionComponent(min=-1, default_value=1), - ), - NodesFlow.Node.Option( - name="Keep aspect ratio", - option_component=NodesFlow.CheckboxOptionComponent(default_value=False), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + width_val = settings.get("width", 1) + height_val = settings.get("height", 1) + keep_aspect_ratio = settings.get("aspect_ratio", {}).get("keep", False) + settings_options = [ + NodesFlow.Node.Option( + name="width_text", + option_component=NodesFlow.TextOptionComponent("Width"), + ), + NodesFlow.Node.Option( + name="width", + option_component=NodesFlow.IntegerOptionComponent( + min=-1, default_value=width_val + ), + ), + NodesFlow.Node.Option( + name="height_text", + option_component=NodesFlow.TextOptionComponent("Height"), + ), + NodesFlow.Node.Option( + name="height", + option_component=NodesFlow.IntegerOptionComponent( + min=-1, default_value=height_val + ), + ), + NodesFlow.Node.Option( + name="Keep aspect ratio", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=keep_aspect_ratio + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, - meta_changed_cb=None, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/rotate.py b/src/ui/dtl/actions/rotate.py index 1a6ab05a..1230629c 100644 --- a/src/ui/dtl/actions/rotate.py +++ b/src/ui/dtl/actions/rotate.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -9,14 +11,6 @@ class RotateAction(Action): title = "Rotate" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/rotate" description = "Rotate layer (rotate) rotates images and its 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 = { - "rotate_angles": None, - "black_regions": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -30,62 +24,60 @@ def get_settings(options_json: dict) -> dict: "black_regions": {"mode": options_json["black_regions"]}, } - 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"] - node_state["min_degrees"] = settings["rotate_angles"]["min_degrees"] - node_state["max_degrees"] = settings["rotate_angles"]["max_degrees"] - node_state["black_regions"] = settings["black_regions"]["mode"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="rotate_angles_text", - option_component=NodesFlow.TextOptionComponent("Rotate Angles"), - ), - NodesFlow.Node.Option( - name="min_degrees_text", - option_component=NodesFlow.TextOptionComponent("Min Degrees"), - ), - NodesFlow.Node.Option( - name="min_degrees", - option_component=NodesFlow.IntegerOptionComponent(default_value=45), - ), - NodesFlow.Node.Option( - name="max_degrees_text", - option_component=NodesFlow.TextOptionComponent("Max Degrees"), - ), - NodesFlow.Node.Option( - name="max_degrees", - option_component=NodesFlow.IntegerOptionComponent(default_value=45), - ), - NodesFlow.Node.Option( - name="black_regions_text", - option_component=NodesFlow.TextOptionComponent("Black Regions"), - ), - NodesFlow.Node.Option( - name="black_regions", - option_component=NodesFlow.SelectOptionComponent( - items=[ - NodesFlow.SelectOptionComponent.Item("keep", "Keep"), - NodesFlow.SelectOptionComponent.Item("crop", "Crop"), - NodesFlow.SelectOptionComponent.Item("preserve_size", "Preserve Size"), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + min_degrees_val = settings.get("rotate_angles", {}).get("min_degrees", 45) + max_degrees_val = settings.get("rotate_angles", {}).get("max_degrees", 45) + black_regions_val = settings.get("black_regions", {}).get("mode", "keep") + settings_options = [ + NodesFlow.Node.Option( + name="rotate_angles_text", + option_component=NodesFlow.TextOptionComponent("Rotate Angles"), + ), + NodesFlow.Node.Option( + name="min_degrees_text", + option_component=NodesFlow.TextOptionComponent("Min Degrees"), + ), + NodesFlow.Node.Option( + name="min_degrees", + option_component=NodesFlow.IntegerOptionComponent( + default_value=min_degrees_val + ), + ), + NodesFlow.Node.Option( + name="max_degrees_text", + option_component=NodesFlow.TextOptionComponent("Max Degrees"), ), - ), - ] + NodesFlow.Node.Option( + name="max_degrees", + option_component=NodesFlow.IntegerOptionComponent( + default_value=max_degrees_val + ), + ), + NodesFlow.Node.Option( + name="black_regions_text", + option_component=NodesFlow.TextOptionComponent("Black Regions"), + ), + NodesFlow.Node.Option( + name="black_regions", + option_component=NodesFlow.SelectOptionComponent( + items=[ + NodesFlow.SelectOptionComponent.Item("keep", "Keep"), + NodesFlow.SelectOptionComponent.Item("crop", "Crop"), + NodesFlow.SelectOptionComponent.Item("preserve_size", "Preserve Size"), + ], + default_value=black_regions_val, + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, - meta_changed_cb=None, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/save.py b/src/ui/dtl/actions/save.py index 4fbcec2b..9c5e9beb 100644 --- a/src/ui/dtl/actions/save.py +++ b/src/ui/dtl/actions/save.py @@ -1,5 +1,7 @@ import json from typing import Optional + +import requests from src.ui.dtl import Action from src.ui.dtl.Layer import Layer from supervisely.app.widgets import NodesFlow @@ -17,13 +19,10 @@ class SaveAction(Action): "representations of all annotated objects on top of your images by setting " "visualize to true." ) - # 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 = { - "visualize": "Visualize", - } + md_description_url = ( + "https://raw.githubusercontent.com/supervisely/docs/master/data-manipulation/dtl/save.md" + ) + md_description = requests.get(md_description_url).text @classmethod def create_new_layer(cls, layer_id: Optional[str] = None) -> Layer: @@ -45,30 +44,43 @@ def get_dst(options_json: dict) -> dict: dst = [dst.strip("'\"")] return dst - options = [ - NodesFlow.Node.Option( - name="destination_text", - option_component=NodesFlow.TextOptionComponent("Destination"), - ), - NodesFlow.Node.Option(name="dst", option_component=NodesFlow.InputOptionComponent()), - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="Visualize", - option_component=NodesFlow.CheckboxOptionComponent(), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + try: + dst_value = dst[0] + except IndexError: + dst_value = "" + try: + visualize_value = settings["visualize"] + except KeyError: + visualize_value = False + + dst_options = [ + NodesFlow.Node.Option( + name="destination_text", + option_component=NodesFlow.TextOptionComponent("Destination"), + ), + NodesFlow.Node.Option( + name="dst", option_component=NodesFlow.InputOptionComponent(dst_value) + ), + ] + settings_options = [ + NodesFlow.Node.Option( + name="Visualize", + option_component=NodesFlow.CheckboxOptionComponent(visualize_value), + ), + ] + return { + "src": [], + "dst": dst_options, + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=get_dst, id=layer_id, + create_options=create_options, + get_dst=get_dst, + get_settings=get_settings, ) @classmethod diff --git a/src/ui/dtl/actions/save_masks.py b/src/ui/dtl/actions/save_masks.py index 811b6d5b..824b6a1d 100644 --- a/src/ui/dtl/actions/save_masks.py +++ b/src/ui/dtl/actions/save_masks.py @@ -1,11 +1,13 @@ from typing import Optional import json -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta +from supervisely.imaging.color import hex2rgb, rgb2hex + from src.ui.dtl import Action 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 SaveMasksAction(Action): @@ -13,39 +15,71 @@ class SaveMasksAction(Action): title = "Save Masks" docs_url = "https://docs.supervisely.com/data-manipulation/index/save-layers/save_masks" description = "Save masks layer (save_masks) gives you an opportunity to get masked representations of data besides just images and annotations that you can get using save layer. It includes machine and human representations. In machine masks each of listed classes are colored in shades of gray that you specify. Note that black color [0, 0, 0] is automatically assigned with the background. In human masks you would get stacked original images with that images having class colors above." - # 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 = { - "masks_human": "Add human masks", - "masks_machine": "Add machine masks", - "gt_human_color": None, - "gt_machine_color": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None) -> Layer: human_classes_colors = ClassesColorMapping() machine_classes_colors = ClassesColorMapping() + human_classes_colors_preview = ClassesMappingPreview() + machine_classes_colors_preview = ClassesMappingPreview() + + human_classes_colors_save_btn = Button("Save", icon="zmdi zmdi-floppy") + machine_classes_colors_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_human_classes_colors_settings = {} + saved_machine_classes_colors_settings = {} + + def _get_human_classes_colors_value(): + mapping = human_classes_colors.get_mapping() + values = { + name: values["value"] for name, values in mapping.items() if not values["ignore"] + } + return values + + def _get_machine_classes_colors_value(): + mapping = machine_classes_colors.get_mapping() + values = { + name: values["value"] for name, values in mapping.items() if not values["ignore"] + } + return values + + def _save_human_classes_colors(): + nonlocal saved_human_classes_colors_settings + saved_human_classes_colors_settings = { + cls_name: hex2rgb(value) + for cls_name, value in _get_human_classes_colors_value().items() + } + human_classes_colors_preview.set_mapping( + { + cls_name: rgb2hex(color) + for cls_name, color in saved_human_classes_colors_settings.items() + } + ) + + def _save_machine_classes_colors(): + nonlocal saved_machine_classes_colors_settings + saved_machine_classes_colors_settings = { + cls_name: hex2rgb(value) + for cls_name, value in _get_machine_classes_colors_value().items() + } + machine_classes_colors_preview.set_mapping( + { + cls_name: rgb2hex(color) + for cls_name, color in saved_machine_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""" masks_human = options_json["Add human masks"] gt_human_color = {} if masks_human: - gt_human_color = { - cls_name: hex2rgb(value["value"]) - for cls_name, value in human_classes_colors.get_mapping().items() - } + gt_human_color = saved_human_classes_colors_settings masks_machine = options_json["Add machine masks"] gt_machine_color = {} if masks_machine: - gt_machine_color = { - cls_name: hex2rgb(value["value"]) - for cls_name, value in machine_classes_colors.get_mapping().items() - } + gt_machine_color = saved_machine_classes_colors_settings return { "masks_human": masks_human, @@ -72,76 +106,99 @@ def get_dst(options_json: dict) -> dict: dst = [dst.strip("'\"")] return dst - 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"] - human_colors = settings["gt_human_color"] - current_colors_mapping = human_classes_colors.get_mapping() - human_classes_colors.set_colors( - [ - human_colors.get(cls, hex2rgb(hex_color)) - for cls, hex_color in current_colors_mapping.items() - ] - ) - machine_colors = settings["gt_machine_color"] - current_colors_mapping = machine_classes_colors.get_mapping() - machine_classes_colors.set_colors( - [ - machine_colors.get(cls, hex2rgb(hex_color)) - for cls, hex_color in current_colors_mapping.items() - ] - ) - return node_state - - options = [ - NodesFlow.Node.Option( - name="destination_text", - option_component=NodesFlow.TextOptionComponent("Destination"), - ), - NodesFlow.Node.Option(name="dst", option_component=NodesFlow.InputOptionComponent()), - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="gt_human_color_text", - option_component=NodesFlow.TextOptionComponent("Human masks colors"), - ), - NodesFlow.Node.Option( - name="Add human masks", - option_component=NodesFlow.CheckboxOptionComponent(), - ), - NodesFlow.Node.Option( - name="Set human masks colors", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(human_classes_colors) + def _set_settings_from_json(settings): + if "gt_human_color" in settings: + human_colors = settings["gt_human_color"] + current_colors_mapping = human_classes_colors.get_mapping() + human_classes_colors.set_colors( + [ + human_colors.get(cls, hex2rgb(hex_color)) + for cls, hex_color in current_colors_mapping.items() + ] + ) + _save_human_classes_colors() + if "gt_machine_color" in settings: + machine_colors = settings["gt_machine_color"] + current_colors_mapping = machine_classes_colors.get_mapping() + machine_classes_colors.set_colors( + [ + machine_colors.get(cls, hex2rgb(hex_color)) + for cls, hex_color in current_colors_mapping.items() + ] + ) + _save_machine_classes_colors() + + human_classes_colors_save_btn.click(_save_human_classes_colors) + machine_classes_colors_save_btn.click(_save_machine_classes_colors) + + def create_options(src: list, dst: list, settings: dict) -> dict: + try: + dst_value = dst[0] + except IndexError: + dst_value = "" + dst_options = [ + NodesFlow.Node.Option( + name="destination_text", + option_component=NodesFlow.TextOptionComponent("Destination"), + ), + NodesFlow.Node.Option( + name="dst", option_component=NodesFlow.InputOptionComponent(dst_value) + ), + ] + masks_human_val = settings.get("masks_human", False) + masks_machine_val = settings.get("masks_machine", False) + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Add human masks", + option_component=NodesFlow.CheckboxOptionComponent(masks_human_val), + ), + NodesFlow.Node.Option( + name="Set human masks colors", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[human_classes_colors, human_classes_colors_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="human_colors_preview", + option_component=NodesFlow.WidgetOptionComponent(human_classes_colors_preview), ), - ), - NodesFlow.Node.Option( - name="gt_machine_color_text", - option_component=NodesFlow.TextOptionComponent("Machine masks colors"), - ), - NodesFlow.Node.Option( - name="Add machine masks", - option_component=NodesFlow.CheckboxOptionComponent(), - ), - NodesFlow.Node.Option( - name="Set machine masks colors", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(machine_classes_colors) + NodesFlow.Node.Option( + name="Add machine masks", + option_component=NodesFlow.CheckboxOptionComponent(masks_machine_val), ), - ), - ] + NodesFlow.Node.Option( + name="Set machine masks colors", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container( + widgets=[machine_classes_colors, machine_classes_colors_save_btn] + ) + ) + ), + ), + NodesFlow.Node.Option( + name="machine_colors_preview", + option_component=NodesFlow.WidgetOptionComponent( + machine_classes_colors_preview + ), + ), + ] + return { + "src": [], + "dst": dst_options, + "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=get_dst, - set_settings_from_json=set_settings_from_json, - id=layer_id, + meta_changed_cb=meta_changed_cb, ) @classmethod diff --git a/src/ui/dtl/actions/skeletonize.py b/src/ui/dtl/actions/skeletonize.py index 15321f4c..ac0ab9f6 100644 --- a/src/ui/dtl/actions/skeletonize.py +++ b/src/ui/dtl/actions/skeletonize.py @@ -1,9 +1,11 @@ from typing import Optional + +from supervisely.app.widgets import NodesFlow, Button, Container +from supervisely import ProjectMeta, Bitmap, AnyGeometry + from src.ui.dtl.Action import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList -from supervisely.app.widgets import NodesFlow -from supervisely import ProjectMeta, Bitmap, AnyGeometry +from src.ui.widgets import ClassesList, ClassesListPreview class SkeletonizeAction(Action): @@ -13,17 +15,15 @@ class SkeletonizeAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/skeletonize" ) description = "This layer (skeletonize) extracts skeletons from bitmap figures." - # 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) -> Layer: classes_widget = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] + _current_meta = ProjectMeta() methods = [ @@ -33,10 +33,16 @@ def create_new_layer(cls, layer_id: Optional[str] = None) -> Layer: ] items = [NodesFlow.SelectOptionComponent.Item(*method) for method in methods] + def _save_classes(): + nonlocal saved_classes_settings + selected_classes = classes_widget.get_selected_classes() + saved_classes_settings = [cls.name for cls in selected_classes] + classes_preview.set(saved_classes_settings) + def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [cls.name for cls in classes_widget.get_selected_classes()], + "classes": saved_classes_settings, "method": options_json["method"], } @@ -44,55 +50,58 @@ def meta_changed_cb(project_meta: ProjectMeta): nonlocal _current_meta if project_meta.obj_classes != _current_meta.obj_classes: classes_widget.loading = True - classes_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Bitmap, AnyGeometry] - ] - ) + obj_classes = [ + cls + for cls in project_meta.obj_classes + if cls.geometry_type in [Bitmap, AnyGeometry] + ] + classes_widget.set(obj_classes) + classes_preview.set(obj_classes, saved_classes_settings) classes_widget.loading = False _current_meta = project_meta - 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"] + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) classes_widget.loading = True classes_widget.select(obj_class_names) + _save_classes() classes_widget.loading = False - return node_state - options = [ - 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_widget) + classes_save_btn.click(_save_classes) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + method_val = settings.get("method", "skeletonization") + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes_widget, classes_save_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="method_text", - option_component=NodesFlow.TextOptionComponent("Method"), - ), - NodesFlow.Node.Option( - name="method", - option_component=NodesFlow.SelectOptionComponent( - items=items, default_value=items[0].value + NodesFlow.Node.Option( + name="method_text", + option_component=NodesFlow.TextOptionComponent("Method"), ), - ), - ] + NodesFlow.Node.Option( + name="method", + option_component=NodesFlow.SelectOptionComponent( + items=items, default_value=method_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, meta_changed_cb=meta_changed_cb, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/sliding_window.py b/src/ui/dtl/actions/sliding_window.py index 8ec0aa5d..ba09568d 100644 --- a/src/ui/dtl/actions/sliding_window.py +++ b/src/ui/dtl/actions/sliding_window.py @@ -1,5 +1,7 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -11,14 +13,6 @@ class SlidingWindowAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/sliding_window" ) description = "This layer (sliding_window) is used to crop part of image with its annotations by sliding of window from left to rigth, from top to bottom." - # 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 = { - "window": None, - "min_overlap": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -35,69 +29,70 @@ def get_settings(options_json: dict) -> dict: }, } - 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"] - node_state["window_width"] = settings["window"]["width"] - node_state["window_height"] = settings["window"]["height"] - node_state["min_overlap_x"] = settings["min_overlap"]["x"] - node_state["min_overlap_y"] = settings["min_overlap"]["y"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="window_text", - option_component=NodesFlow.TextOptionComponent("Window"), - ), - NodesFlow.Node.Option( - name="window_width_text", - option_component=NodesFlow.TextOptionComponent("Width"), - ), - NodesFlow.Node.Option( - name="window_width", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=128), - ), - NodesFlow.Node.Option( - name="window_height_text", - option_component=NodesFlow.TextOptionComponent("Height"), - ), - NodesFlow.Node.Option( - name="window_height", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=128), - ), - NodesFlow.Node.Option( - name="min_overlap_text", - option_component=NodesFlow.TextOptionComponent("Min Overlap"), - ), - NodesFlow.Node.Option( - name="min_overlap_x_text", - option_component=NodesFlow.TextOptionComponent("X"), - ), - NodesFlow.Node.Option( - name="min_overlap_x", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=32), - ), - NodesFlow.Node.Option( - name="min_overlap_y_text", - option_component=NodesFlow.TextOptionComponent("Y"), - ), - NodesFlow.Node.Option( - name="min_overlap_y", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=32), - ), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + width_val = settings.get("window", {}).get("width", 128) + height_val = settings.get("window", {}).get("height", 128) + min_overlap_x_val = settings.get("min_overlap", {}).get("x", 32) + min_overlap_y_val = settings.get("min_overlap", {}).get("y", 32) + settings_options = [ + NodesFlow.Node.Option( + name="window_text", + option_component=NodesFlow.TextOptionComponent("Window"), + ), + NodesFlow.Node.Option( + name="window_width_text", + option_component=NodesFlow.TextOptionComponent("Width"), + ), + NodesFlow.Node.Option( + name="window_width", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=width_val + ), + ), + NodesFlow.Node.Option( + name="window_height_text", + option_component=NodesFlow.TextOptionComponent("Height"), + ), + NodesFlow.Node.Option( + name="window_height", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=height_val + ), + ), + NodesFlow.Node.Option( + name="min_overlap_text", + option_component=NodesFlow.TextOptionComponent("Min Overlap"), + ), + NodesFlow.Node.Option( + name="min_overlap_x_text", + option_component=NodesFlow.TextOptionComponent("X"), + ), + NodesFlow.Node.Option( + name="min_overlap_x", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=min_overlap_x_val + ), + ), + NodesFlow.Node.Option( + name="min_overlap_y_text", + option_component=NodesFlow.TextOptionComponent("Y"), + ), + NodesFlow.Node.Option( + name="min_overlap_y", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=min_overlap_y_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, id=layer_id, - options=options, - get_src=None, - get_dst=None, + create_options=create_options, get_settings=get_settings, - meta_changed_cb=None, - set_settings_from_json=set_settings_from_json, ) diff --git a/src/ui/dtl/actions/split_masks.py b/src/ui/dtl/actions/split_masks.py index 8d459c00..0d2386de 100644 --- a/src/ui/dtl/actions/split_masks.py +++ b/src/ui/dtl/actions/split_masks.py @@ -1,10 +1,12 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta from supervisely import Bitmap, AnyGeometry + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview class SplitMasksAction(Action): @@ -14,31 +16,32 @@ class SplitMasksAction(Action): "https://docs.supervisely.com/data-manipulation/index/transformation-layers/split_masks" ) description = "This layer (split_masks) takes one bitmap annotation and split it into few bitmap masks if it contain non-connected components." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] + + def _save_classes_settings(): + nonlocal saved_classes_settings + selected_classes = classes.get_selected_classes() + saved_classes_settings = [cls.name for cls in selected_classes] + classes_preview.set(saved_classes_settings) def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [cls.name for cls in classes.get_selected_classes()], + "classes": saved_classes_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: dict): classes.loading = True - classes.select(settings["classes"]) + classes.select(settings.get("classes", [])) + _save_classes_settings() classes.loading = False - return node_state def meta_changed_cb(project_meta: ProjectMeta): classes.loading = True @@ -49,32 +52,33 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Bitmap, AnyGeometry] ] ) + classes_preview.set(classes.get_selected_classes()) 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) + classes_save_btn.click(_save_classes_settings) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_btn]) + ) + ), ), - ), - ] + ] + 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/supervisely.py b/src/ui/dtl/actions/supervisely.py index 9f6b4bd3..5b9d3a7e 100644 --- a/src/ui/dtl/actions/supervisely.py +++ b/src/ui/dtl/actions/supervisely.py @@ -1,6 +1,8 @@ from typing import Optional import json + from supervisely.app.widgets import NodesFlow + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer @@ -24,22 +26,31 @@ def get_dst(options_json: dict) -> dict: return dst - options = [ - NodesFlow.Node.Option( - name="destination_text", - option_component=NodesFlow.TextOptionComponent("Destination"), - ), - NodesFlow.Node.Option(name="dst", option_component=NodesFlow.InputOptionComponent()), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + try: + dst_value = dst[0] + except IndexError: + dst_value = "" + dst_options = [ + NodesFlow.Node.Option( + name="destination_text", + option_component=NodesFlow.TextOptionComponent("Destination"), + ), + NodesFlow.Node.Option( + name="dst", option_component=NodesFlow.InputOptionComponent(dst_value) + ), + ] + return { + "src": [], + "dst": dst_options, + "settings": [], + } return Layer( action=cls, - options=options, - get_settings=None, - get_src=None, - meta_changed_cb=None, - get_dst=get_dst, id=layer_id, + create_options=create_options, + get_dst=get_dst, ) @classmethod diff --git a/src/ui/dtl/actions/tag.py b/src/ui/dtl/actions/tag.py index 1b6addf5..0faf31a3 100644 --- a/src/ui/dtl/actions/tag.py +++ b/src/ui/dtl/actions/tag.py @@ -1,10 +1,12 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow, SelectTagMeta, Container + +from supervisely.app.widgets import NodesFlow, SelectTagMeta, Container, Button, Text from supervisely import ProjectMeta, TagMeta, Tag from supervisely.annotation.tag_meta import TagValueType + from src.ui.dtl import Action from src.ui.dtl.Layer import Layer -from src.ui.widgets import InputTag +from src.ui.widgets import InputTag, TagMetasPreview class TagAction(Action): @@ -12,14 +14,6 @@ class TagAction(Action): title = "Tag" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/tag" description = "Tag layer (tag) adds or removes tags from images. Tags are used for several things, e.g. to split images by folders in save layers or to filter images by tag." - # 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 = { - "tag": None, - "action": "Select Action", - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -28,6 +22,12 @@ def create_new_layer(cls, layer_id: Optional[str] = None): select_tag_meta = SelectTagMeta(project_meta=prev_project_meta) input_tag = InputTag(TagMeta(name="", value_type=TagValueType.NONE)) + tag_preview_meta = TagMetasPreview() + tag_preview_value = Text("", color="white") + save_tag_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_tag_setting = None + last_tag_meta = None @select_tag_meta.value_changed @@ -38,8 +38,7 @@ def select_tag_meta_value_changed(tag_meta: TagMeta): input_tag.set_tag_meta(tag_meta) input_tag.loading = False - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + def _get_tag_value(): selected_tag = input_tag.get_tag() selected_tag: Tag if selected_tag is None: @@ -51,11 +50,31 @@ def get_settings(options_json: dict) -> dict: "name": selected_tag.meta.name, "value": selected_tag.value, } - return {"tag": tag, "action": options_json["Select Action"]} + return tag + + def _save_tag(): + nonlocal saved_tag_setting + saved_tag_setting = _get_tag_value() + tag_preview_value.text = ( + "" + if isinstance(saved_tag_setting, str) or saved_tag_setting is None + else str(saved_tag_setting["value"]) + ) + if saved_tag_setting is not None: + if isinstance(saved_tag_setting, str): + tag_name = saved_tag_setting + else: + tag_name = saved_tag_setting["name"] + tag_meta = select_tag_meta.get_tag_meta_by_name(tag_name) + tag_preview_meta.set([tag_meta]) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return {"tag": saved_tag_setting, "action": options_json["Select Action"]} - 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: dict): + if "tag" not in settings: + return select_tag_meta.loading = True input_tag.loading = True if isinstance(settings["tag"], str): @@ -70,8 +89,7 @@ def set_settings_from_json(json_data: dict, node_state: dict): input_tag.value = tag_value select_tag_meta.loading = False input_tag.loading = False - node_state["action"] = settings["action"] - return node_state + _save_tag() def meta_changed_cb(project_meta: ProjectMeta): nonlocal prev_project_meta @@ -92,46 +110,51 @@ def meta_changed_cb(project_meta: ProjectMeta): select_tag_meta.loading = False prev_project_meta = project_meta - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="input_tag_text", - option_component=NodesFlow.TextOptionComponent("Tag"), - ), - NodesFlow.Node.Option( - name="Input Tag", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - Container(widgets=[select_tag_meta, input_tag]) - ) + save_tag_btn.click(_save_tag) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + action_val = settings.get("action", "add") + settings_options = [ + NodesFlow.Node.Option( + name="Input Tag", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[select_tag_meta, input_tag, save_tag_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="action_text", - option_component=NodesFlow.TextOptionComponent("Action"), - ), - NodesFlow.Node.Option( - name="Select Action", - option_component=NodesFlow.SelectOptionComponent( - items=[ - NodesFlow.SelectOptionComponent.Item("add", "Add"), - NodesFlow.SelectOptionComponent.Item("delete", "Delete"), - ], - default_value="add", + NodesFlow.Node.Option( + name="tag_preview", + option_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[tag_preview_meta, tag_preview_value], gap=1) + ), ), - ), - ] + NodesFlow.Node.Option( + name="action_text", + option_component=NodesFlow.TextOptionComponent("Action"), + ), + NodesFlow.Node.Option( + name="Select Action", + option_component=NodesFlow.SelectOptionComponent( + items=[ + NodesFlow.SelectOptionComponent.Item("add", "Add"), + NodesFlow.SelectOptionComponent.Item("delete", "Delete"), + ], + default_value=action_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/utils.py b/src/ui/dtl/utils.py new file mode 100644 index 00000000..21aeb10a --- /dev/null +++ b/src/ui/dtl/utils.py @@ -0,0 +1,212 @@ +from typing import List, Literal, Union + +from supervisely import ObjClass, ObjClassCollection +from supervisely.app.widgets import NodesFlow + +from src.ui.widgets import ClassesMapping, ClassesMappingPreview + + +# Classes Mapping utils + + +def _unpack_classes_mapping_settings(mapping: dict, all_obj_classes_names): + unpacked = {} + other = mapping.get("__other__", None) + for obj_class_name in all_obj_classes_names: + old_value = mapping.get(obj_class_name, other) + if old_value is None: + old_value = obj_class_name + unpacked[obj_class_name] = old_value + return unpacked + + +def _pack_classes_mapping_settings( + mapping: dict, + default_action: Literal["skip", "keep", "copy"] = "skip", + ignore_action: Literal["skip", "keep", "empty"] = "skip", + other_allowed: bool = False, +): + new_mapping = {} + for name, value in mapping.items(): + if value == "__default__": + if default_action == "skip": + continue + elif default_action == "keep": + new_mapping[name] = "__default__" + elif default_action == "copy": + new_mapping[name] = name + elif value == "__ignore__": + if ignore_action == "skip": + continue + elif ignore_action == "keep": + new_mapping[name] = "__ignore__" + elif ignore_action == "empty": + new_mapping[name] = "" + if other_allowed: + default = [name for name, value in new_mapping.items() if value == "__default__"] + ignore = [name for name, value in new_mapping.items() if value == "__ignore__"] + if len(ignore) > 0: + for name in ignore: + del new_mapping[name] + new_mapping["__other__"] = "__ignore__" + elif len(default) > 0: + for name in default: + del new_mapping[name] + new_mapping["__other__"] = "__default__" + return new_mapping + + +def get_classes_mapping_value( + classes_mapping_widget: ClassesMapping, + default_action: Literal["skip", "keep", "copy"] = "skip", + ignore_action: Literal["skip", "keep", "empty"] = "skip", + other_allowed: bool = False, + default_allowed: bool = False, +): + mapping = classes_mapping_widget.get_mapping() + default = [cls_name for cls_name, cls_values in mapping.items() if cls_values["default"]] + + if default_allowed: + classes = classes_mapping_widget.get_classes() + if len(default) == len(classes): + return "default" + + ignore = [cls_name for cls_name, cls_values in mapping.items() if cls_values["ignore"]] + + result_mapping = {} + for name, values in mapping.items(): + if values["ignore"]: + if ignore_action == "skip": + continue + elif ignore_action == "keep": + result_mapping[name] = "__ignore__" + elif ignore_action == "empty": + result_mapping[name] = "" + elif values["default"]: + if default_action == "skip": + continue + elif default_action == "keep": + result_mapping[name] = "__default__" + elif default_action == "copy": + result_mapping[name] = name + else: + result_mapping[name] = values["value"] + + if other_allowed: + if len(ignore) > 0: + result_mapping = { + name: value for name, value in result_mapping.items() if value != "__ignore__" + } + result_mapping["__other__"] = "__ignore__" + elif len(default) > 0: + result_mapping = { + name: value for name, value in result_mapping.items() if value != "__default__" + } + result_mapping["__other__"] = "__default__" + + return result_mapping + + +def classes_mapping_settings_changed_meta( + settings: dict, + old_obj_classes: Union[List[ObjClass], ObjClassCollection], + new_obj_classes: Union[List[ObjClass], ObjClassCollection], + default_action: Literal["skip", "keep", "copy"] = "skip", + ignore_action: Literal["skip", "keep", "empty"] = "skip", + other_allowed: bool = False, +): + if settings == "default": + return "default" + new_settings = {} + + old_obj_classes_names = {obj_class.name for obj_class in old_obj_classes} + new_obj_classes_names = {obj_class.name for obj_class in new_obj_classes} + + # unpack old settings + new_settings = _unpack_classes_mapping_settings(settings, old_obj_classes_names) + + # remove obj classes that are not in new obj classes + for obj_class_name in old_obj_classes_names: + if obj_class_name not in new_obj_classes_names: + del new_settings[obj_class_name] + + # add new object classes + for obj_class_name in new_obj_classes_names: + if obj_class_name not in old_obj_classes_names: + new_settings[obj_class_name] = "__default__" + + # pack new settings + new_settings = _pack_classes_mapping_settings( + new_settings, + default_action=default_action, + ignore_action=ignore_action, + other_allowed=other_allowed, + ) + return new_settings + + +def set_classes_mapping_preview( + classes_mapping_widget: ClassesMapping, + classes_mapping_preview_widget: ClassesMappingPreview, + classes_mapping_settings: dict, + default_action: Literal["skip", "keep", "copy"] = "skip", + ignore_action: Literal["skip", "keep", "empty"] = "skip", +): + obj_classes = classes_mapping_widget.get_classes() + mapping = {} + if classes_mapping_settings == "default": + for obj_class in obj_classes: + mapping[obj_class.name] = obj_class.name + classes_mapping_preview_widget.set(obj_classes, mapping) + else: + displayed_obj_classes = [] + other_default = classes_mapping_settings.get("__other__", None) == "__default__" + other_ignore = classes_mapping_settings.get("__other__", None) == "__ignore__" + for obj_class in obj_classes: + if obj_class.name in classes_mapping_settings: + value = classes_mapping_settings[obj_class.name] + if value == "__default__": + if default_action == "copy": + value = obj_class.name + elif default_action == "keep": + value = "__default__" + elif default_action == "skip": + continue + if value == "__ignore__": + if ignore_action == "empty": + value = "" + elif ignore_action == "keep": + value = "__ignore__" + elif ignore_action == "skip": + continue + mapping[obj_class.name] = value + displayed_obj_classes.append(obj_class) + elif other_default: + if default_action == "copy": + displayed_obj_classes.append(obj_class) + mapping[obj_class.name] = obj_class.name + elif default_action == "keep": + displayed_obj_classes.append(obj_class) + mapping[obj_class.name] = "__default__" + elif default_action == "skip": + continue + elif other_ignore: + if ignore_action == "empty": + displayed_obj_classes.append(obj_class) + mapping[obj_class.name] = "" + elif ignore_action == "keep": + displayed_obj_classes.append(obj_class) + mapping[obj_class.name] = "__ignore__" + elif ignore_action == "skip": + continue + classes_mapping_preview_widget.set(obj_classes, mapping) + + +# Options utils + + +def get_separator(idx: int = 0): + return NodesFlow.Node.Option( + name=f"separator {idx}", + option_component=NodesFlow.HtmlOptionComponent("
"), + ) diff --git a/src/ui/ui.py b/src/ui/ui.py index 40bc87fd..a035d4c3 100644 --- a/src/ui/ui.py +++ b/src/ui/ui.py @@ -1,6 +1,5 @@ import json from pathlib import Path -import queue import random import threading import time @@ -19,6 +18,7 @@ ReloadableArea, TaskLogs, Empty, + Dialog, ) from supervisely.app import show_dialog from supervisely import ProjectMeta @@ -28,7 +28,7 @@ from src.ui.dtl import actions_list, actions, Action from src.ui.dtl.Layer import Layer -from src.ui.dtl import DATA_ACTIONS +from src.ui.dtl import SOURCE_ACTIONS from src.compute.main import main as compute_dtls import src.globals as g from src.compute.Net import Net @@ -51,7 +51,20 @@ size="large", ) add_layer_btn = Button("Add Layer") -nodes_flow = NodesFlow(height="70vh") +context_menu_items = [ + {"label": "Select..."}, + *[ + {"label": group_name, "items": [{"label": action_name} for action_name in group_actions]} + for group_name, group_actions in actions_list.items() + ], +] +add_layer_from_dialog_btn = Button("Add Layer") +add_layer_dialog = Dialog( + title="Add Layer", + content=Flexbox(widgets=[select_action_name, add_layer_from_dialog_btn]), + size="tiny", +) +nodes_flow = NodesFlow(height="75vh", context_menu=context_menu_items) run_btn = Button("Run", icon="zmdi zmdi-play") json_editor = Editor(height_lines=100) update_previews_btn = Button("Update previews") @@ -68,18 +81,20 @@ add_layer_card = Card( title="Add new layer", content=Flexbox(widgets=[select_action_name, add_layer_btn]) ) +add_layer_card.hide() download_archives_urls = Text("") results = ReloadableArea(Empty()) results.hide() nodes_flow_card = Card( title="DTL Graph", content=Container(widgets=[nodes_flow, run_btn, progress, results]), - content_top_right=update_previews_btn, + content_top_right=Flexbox(widgets=[select_action_name, add_layer_btn, update_previews_btn]), ) layout_widgets = [ add_layer_card, nodes_flow_card, json_editor_card, + add_layer_dialog, ] logs = None if not sly.is_development(): @@ -135,7 +150,6 @@ def load_json(): dst_to_layer_id = {} unknown_actions = set() missing_settings = set() - bad_settings = set() ids = [] layers_jsons_idxs = [] data_layers_ids = [] @@ -157,15 +171,13 @@ def load_json(): dst_layer = action.create_new_layer(id) g.layers[id] = dst_layer ids.append(id) - if action_name in actions_list[DATA_ACTIONS]: + if action_name in actions_list[SOURCE_ACTIONS]: data_layers_ids.append(id) data_layer_id_to_json_idx[id] = i layers_jsons_idxs.append(i) - # create nodes - for layer_id in ids: - node = g.layers[layer_id].create_node() - nodes_flow.add_node(node) + if len(ids) == 0: + raise RuntimeError("No layers found") # update src and dst for layer_id, json_idx in zip(ids, layers_jsons_idxs): @@ -186,6 +198,46 @@ def load_json(): for d in dst: dst_to_layer_id.setdefault(d, []).append(layer_id) + # init metas for data layers + for layer_id in data_layers_ids: + data_layer = g.layers[layer_id] + src = data_layer.get_src() + if src is None or len(src) == 0: + # Skip if no sources specified for data layer + continue + + # Add project meta + project_name, dataset_name = src[0].split("/") + project_info = utils.get_project_by_name(project_name) + project_meta = utils.get_project_meta(project_info.id) + + data_layer.update_project_meta(project_meta) + layer_json = dtl_json[data_layer_id_to_json_idx[layer_id]] + + # init metas for all layers + net = Net(dtl_json, g.RESULTS_DIR) + utils.init_output_metas(net, ids) + for layer_id in ids: + if layer_id in data_layers_ids: + continue + layer = g.layers[layer_id] + layer_input_meta = utils.merge_input_metas( + [g.layers[utils.find_layer_id_by_dst(src)].output_meta for src in layer.get_src()] + ) + layer.update_project_meta(layer_input_meta) + + # update settings + for layer_id, json_idx in zip(ids, layers_jsons_idxs): + layer_json = dtl_json[json_idx] + layer = g.layers[layer_id] + layer.from_json(layer_json) + + # create nodes + for layer_id in ids: + layer = g.layers[layer_id] + node = layer.create_node() + nodes_flow.add_node(node) + # create node connections nodes_flow_edges = [] for dst_layer_id in ids: @@ -214,74 +266,10 @@ def load_json(): pass nodes_flow.set_edges(nodes_flow_edges) - # get metas for data layers - # PROBLEM: nodes_flow_state is empty. need to get post request from client to update StateJson - nodes_flow_state = nodes_flow.get_flow_state() - input_metas = {} - for layer_id in data_layers_ids: - data_layer = g.layers[layer_id] - src = data_layer.get_src() - if src is None or len(src) == 0: - # Skip if no sources specified for data layer - continue - - # Add project meta - project_name, dataset_name = src[0].split("/") - project_info = utils.get_project_by_name(project_name) - project_meta = utils.get_project_meta(project_info.id) - input_metas[project_name] = project_meta - - data_layer.meta_changed_cb(project_meta) - layer_json = dtl_json[data_layer_id_to_json_idx[layer_id]] - try: - new_state = data_layer.set_settings_from_json(layer_json, {}) - if new_state is not None and len(new_state) > 0: - nodes_flow_state[layer_id] = new_state - except: - logger.debug( - "Error setting settings from json for data layer", - exc_info=traceback.format_exc(), - ) - bad_settings.add(data_layer.action.name) - continue - - net = Net(dtl_json, g.RESULTS_DIR) - net.calc_metas() - for layer_id, net_layer in zip(ids, net.layers): - g.layers[layer_id].output_meta = net_layer.output_meta - - # update settings - for layer_id, json_idx, net_layer in zip(ids, layers_jsons_idxs, net.layers): - layer_json = dtl_json[json_idx] - layer = g.layers[layer_id] - - # update settings - if layer.action.name not in actions_list[DATA_ACTIONS]: - layer_input_meta = ProjectMeta() - for src in layer.get_src(): - src_layer_id = utils.find_layer_id_by_dst(src) - if src_layer_id is None: - continue - src_layer = g.layers[src_layer_id] - if src_layer.output_meta is not None: - layer_input_meta = utils.merge_input_metas( - [layer_input_meta, src_layer.output_meta] - ) - if layer.meta_changed_cb is not None: - layer.meta_changed_cb(layer_input_meta) - try: - # state = nodes_flow_state[layer_id] - new_state = layer.set_settings_from_json(layer_json, {}) - if new_state is not None: - nodes_flow_state[layer_id] = new_state - except: - logger.debug( - "Error updating settings from json", exc_info=traceback.format_exc() - ) - bad_settings.add(layer.action.name) - continue - - nodes_flow.update_nodes_state(nodes_flow_state) + except: + logger.error("Error loading json", exc_info=traceback.format_exc()) + show_dialog(title="Error", description="Error loading json", status="error") + finally: def actions_str(actions: Iterable): return ", ".join([f'"{a}"' for a in actions]) @@ -296,18 +284,12 @@ def actions_str(actions: Iterable): if len(missing_settings) > 0 else "" ) - problems_reading += ( - f"Bad settings in actions: {actions_str(bad_settings)}" if len(bad_settings) > 0 else "" - ) if len(problems_reading) > 0: show_dialog( title="Errors reading from json", description=problems_reading, status="warning", ) - except: - logger.debug("Error loading json", exc_info=traceback.format_exc()) - finally: nodes_flow.loading = False @@ -332,6 +314,31 @@ def add_layer_btn_cb(): nodes_flow.add_node(node) +@add_layer_from_dialog_btn.click +def add_layer_from_dialog_btn_cb(): + position = g.context_menu_position + action_name = select_action_name.get_value() + try: + action = actions.get(action_name) + except: + show_dialog( + title="Error", + description=f"Action {action_name} not found", + status="error", + ) + return + action: Action + g.layers_count += 1 + id = action_name + "_" + str(g.layers_count) + layer = action.create_new_layer(id) + g.layers[id] = layer + node = layer.create_node() + node.set_position(position) + nodes_flow.add_node(node) + add_layer_dialog.hide() + g.context_menu_position = None + + @run_btn.click def run(): edges = nodes_flow.get_edges_json() @@ -371,7 +378,7 @@ def run(): utils.create_data_dir() except: - logger.debug("Error initializing layers", exc_info=traceback.format_exc()) + logger.error("Error initializing layers", exc_info=traceback.format_exc()) progress.hide() run_btn.show() return @@ -380,7 +387,7 @@ def run(): utils.save_dtl_json(dtl_json) net = compute_dtls(progress) except: - logger.debug("Error computing dtls", exc_info=traceback.format_exc()) + logger.error("Error computing dtls", exc_info=traceback.format_exc()) file_infos = [] # with progress( # total=len([p for p in Path(g.RESULTS_DIR).iterdir() if p.is_dir()]), @@ -412,19 +419,15 @@ def run(): if not sly.is_development(): g.api.task.set_output_archive(sly.env.task_id(), file_info.id, file_info.name) except Exception as e: - logger.debug("Error uploading results", exc_info=traceback.format_exc()) + logger.error("Error uploading results", exc_info=traceback.format_exc()) show_dialog(title="Error uploading results", description=str(e), status="error") try: - logger.debug( - "Creating results widget", - extra={"file_infos": file_infos, "file_infos_length": len(file_infos)}, - ) supervisely_layers = [l for l in net.layers if isinstance(l, SuperviselyLayer)] results.set_content(utils.create_results_widget(file_infos, supervisely_layers)) results.reload() results.show() except: - logger.debug("Error creating results widget", exc_info=traceback.format_exc()) + logger.error("Error creating results widget", exc_info=traceback.format_exc()) finally: progress.hide() run_btn.show() @@ -451,10 +454,23 @@ def update_nodes(): net = Net(dtl_json, g.RESULTS_DIR) utils.init_output_metas(net, all_layers_ids) + # Call meta changed callbacks for Data layers + for layer_id in data_layers_ids: + layer = g.layers[layer_id] + layer: Layer + src = layer.get_src() + layer_input_meta = ProjectMeta() + if src: + project_name, _ = src[0].split("/") + layer_input_meta = utils.get_project_meta( + utils.get_project_by_name(project_name).id + ) + layer.update_project_meta(layer_input_meta) + # Call meta changed callbacks to update ui for layer_id in all_layers_ids: layer = g.layers[layer_id] - if layer.action.name not in actions_list[DATA_ACTIONS]: + if layer.action.name not in actions_list[SOURCE_ACTIONS]: layer_input_meta = ProjectMeta() for src in layer.get_src(): src_layer_id = utils.find_layer_id_by_dst(src) @@ -463,18 +479,18 @@ def update_nodes(): layer_input_meta = utils.merge_input_metas( [layer_input_meta, src_layer.output_meta] ) - layer.meta_changed_cb(layer_input_meta) + layer.update_project_meta(layer_input_meta) # Load preview for data layers utils.delete_preview_dir() utils.create_preview_dir() - utils.set_preview(data_layers_ids) + utils.update_preview(data_layers_ids) # Update preview utils.update_previews(net, data_layers_ids, all_layers_ids) except: - logger.debug("Error updating nodes", exc_info=traceback.format_exc()) + logger.error("Error updating nodes", exc_info=traceback.format_exc()) show_dialog(title="Error", description="Error updating nodes", status="error") finally: nodes_flow.loading = False @@ -488,6 +504,7 @@ def update_metas(): # Init layers data layers_ids = utils.init_layers(nodes_state) all_layers_ids = layers_ids["all_layers_ids"] + data_layers_ids = layers_ids["data_layers_ids"] # Init sources utils.init_src(edges) @@ -499,10 +516,23 @@ def update_metas(): net = Net(dtl_json, g.RESULTS_DIR) utils.init_output_metas(net, all_layers_ids) + # Call meta changed callbacks for Data layers + for layer_id in data_layers_ids: + layer = g.layers[layer_id] + layer: Layer + src = layer.get_src() + layer_input_meta = ProjectMeta() + if src: + project_name, _ = src[0].split("/") + layer_input_meta = utils.get_project_meta( + utils.get_project_by_name(project_name).id + ) + layer.update_project_meta(layer_input_meta) + # Call meta changed callbacks to update ui for layer_id in all_layers_ids: layer = g.layers[layer_id] - if layer.action.name not in actions_list[DATA_ACTIONS]: + if layer.action.name not in actions_list[SOURCE_ACTIONS]: layer_input_meta = ProjectMeta() for src in layer.get_src(): src_layer_id = utils.find_layer_id_by_dst(src) @@ -511,21 +541,18 @@ def update_metas(): layer_input_meta = utils.merge_input_metas( [layer_input_meta, src_layer.output_meta] ) - layer.meta_changed_cb(layer_input_meta) + layer.update_project_meta(layer_input_meta) except: - logger.debug("Error updating metas", exc_info=traceback.format_exc()) - - -_update_queue = queue.Queue() + show_dialog(title="Error", description="Error updating metas", status="error") + logger.error("Error updating metas", exc_info=traceback.format_exc()) def _update_f(): - global _update_queue while True: updates = [] - while not _update_queue.empty(): - updates.append(_update_queue.get()) + while not g.update_queue.empty(): + updates.append(g.update_queue.get()) if len(updates) == 0: time.sleep(0.1) continue @@ -538,36 +565,56 @@ def _update_f(): update_metas() finally: for _ in range(len(updates)): - _update_queue.task_done() + g.update_queue.task_done() time.sleep(0.1) -_update_loop = threading.Thread( +update_loop = threading.Thread( target=_update_f, name="App update loop", daemon=True, -).start() - - -def updater(update: str): - global _update_queue - _update_queue.put(update) +) def update_nodes_cb(): - updater("nodes") + g.updater("nodes") def update_metas_cb(): - updater("metas") + g.updater("metas") def load_json_cb(): - updater("load_json") + g.updater("load_json") + + +@nodes_flow.context_menu_clicked +def context_menu_clicked_cb(item): + position = item["position"] + g.context_menu_position = position + action_name = item["item"]["label"] + if action_name == "Select...": + add_layer_dialog.show() + return + action = actions.get(action_name) + action: Action + g.layers_count += 1 + id = action_name + "_" + str(g.layers_count) + layer = action.create_new_layer(id) + g.layers[id] = layer + node = layer.create_node() + node.set_position(position) + nodes_flow.add_node(node) + g.context_menu_position = None + + +@nodes_flow.sidebar_toggled +def sidebar_toggled_cb(): + print("sidebar toggled") nodes_flow.flow_changed(update_metas_cb) nodes_flow.flow_state_changed(update_metas_cb) -nodes_flow.on_save(update_nodes_cb) +nodes_flow.on_save(update_metas_cb) update_previews_btn.click(update_nodes_cb) load_json_button.click(load_json_cb) diff --git a/src/ui/widgets/__init__.py b/src/ui/widgets/__init__.py index 1da768a6..9a47bf55 100644 --- a/src/ui/widgets/__init__.py +++ b/src/ui/widgets/__init__.py @@ -3,3 +3,6 @@ from .classes_mapping import ClassesMapping from .classes_color_mapping import ClassesColorMapping from .input_tag import InputTag +from .classes_mapping_preview import ClassesMappingPreview +from .classes_list_preview import ClassesListPreview +from .tag_metas_preview import TagMetasPreview diff --git a/src/ui/widgets/classes_list/classes_list.py b/src/ui/widgets/classes_list/classes_list.py index 2a122f52..223539dd 100644 --- a/src/ui/widgets/classes_list/classes_list.py +++ b/src/ui/widgets/classes_list/classes_list.py @@ -111,3 +111,6 @@ def select(self, names: List[str]): selected = [cls.name in names for cls in self._classes] StateJson()[self.widget_id]["selected"] = selected StateJson().send_changes() + + def get_all_classes(self): + return self._classes diff --git a/src/ui/widgets/classes_list_preview/__init__.py b/src/ui/widgets/classes_list_preview/__init__.py new file mode 100644 index 00000000..dea9b639 --- /dev/null +++ b/src/ui/widgets/classes_list_preview/__init__.py @@ -0,0 +1 @@ +from .classes_list_preview import ClassesListPreview diff --git a/src/ui/widgets/classes_list_preview/classes_list_preview.py b/src/ui/widgets/classes_list_preview/classes_list_preview.py new file mode 100644 index 00000000..45475049 --- /dev/null +++ b/src/ui/widgets/classes_list_preview/classes_list_preview.py @@ -0,0 +1,29 @@ +from typing import Optional, Union, List + +from supervisely import ObjClass, ObjClassCollection +from supervisely.app import StateJson +from supervisely.app.widgets import Widget + + +class ClassesListPreview(Widget): + def __init__( + self, + classes: Optional[Union[List[ObjClass], ObjClassCollection]] = [], + widget_id: Optional[str] = None, + ): + self._classes = classes + super().__init__(widget_id=widget_id, file_path=__file__) + + def get_json_data(self): + return {} + + def get_json_state(self): + return {"classes": [cls.to_json() for cls in self._classes]} + + def set(self, classes: Union[List[ObjClass], ObjClassCollection]): + self._classes = classes + StateJson()[self.widget_id] = self.get_json_state() + StateJson().send_changes() + + def get(self): + return self._classes diff --git a/src/ui/widgets/classes_list_preview/template.html b/src/ui/widgets/classes_list_preview/template.html new file mode 100644 index 00000000..b916214a --- /dev/null +++ b/src/ui/widgets/classes_list_preview/template.html @@ -0,0 +1,21 @@ +
+ +
+ + + {{obj_class.title}} + + {{obj_class.shape_text}} + + +
+
+ + \ No newline at end of file diff --git a/src/ui/widgets/classes_mapping_preview/__init__.py b/src/ui/widgets/classes_mapping_preview/__init__.py new file mode 100644 index 00000000..c386bf0c --- /dev/null +++ b/src/ui/widgets/classes_mapping_preview/__init__.py @@ -0,0 +1 @@ +from .classes_mapping_preview import ClassesMappingPreview diff --git a/src/ui/widgets/classes_mapping_preview/classes_mapping_preview.py b/src/ui/widgets/classes_mapping_preview/classes_mapping_preview.py new file mode 100644 index 00000000..1a0462ae --- /dev/null +++ b/src/ui/widgets/classes_mapping_preview/classes_mapping_preview.py @@ -0,0 +1,39 @@ +from typing import Optional, Union, List + +from supervisely import ObjClass, ObjClassCollection +from supervisely.app import StateJson +from supervisely.app.widgets import Widget + + +class ClassesMappingPreview(Widget): + def __init__( + self, + classes: Optional[Union[List[ObjClass], ObjClassCollection]] = [], + mapping: Optional[dict] = {}, + widget_id: Optional[str] = None, + ): + self._classes = classes + self._mapping = mapping + super().__init__(widget_id=widget_id, file_path=__file__) + + def get_json_data(self): + return {} + + def get_json_state(self): + return { + "mapping": [ + {"class": cls.to_json(), "value": self._mapping.get(cls.name, "")} + for cls in self._classes + ] + } + + def set(self, classes: Union[List[ObjClass], ObjClassCollection], mapping: dict): + self._classes = classes + self._mapping = mapping + StateJson()[self.widget_id] = self.get_json_state() + StateJson().send_changes() + + def set_mapping(self, mapping: dict): + self._mapping = mapping + StateJson()[self.widget_id] = self.get_json_state() + StateJson().send_changes() diff --git a/src/ui/widgets/classes_mapping_preview/template.html b/src/ui/widgets/classes_mapping_preview/template.html new file mode 100644 index 00000000..e9280e59 --- /dev/null +++ b/src/ui/widgets/classes_mapping_preview/template.html @@ -0,0 +1,31 @@ +
+ +
+ + + {{mapping.class.title}} + + {{mapping.class.shape_text}} + + + + + + {{mapping.value}} + +
+
+ + \ No newline at end of file diff --git a/src/ui/widgets/tag_metas_preview/__init__.py b/src/ui/widgets/tag_metas_preview/__init__.py new file mode 100644 index 00000000..60c1cac0 --- /dev/null +++ b/src/ui/widgets/tag_metas_preview/__init__.py @@ -0,0 +1 @@ +from .tag_metas_preview import TagMetasPreview diff --git a/src/ui/widgets/tag_metas_preview/tag_metas_preview.py b/src/ui/widgets/tag_metas_preview/tag_metas_preview.py new file mode 100644 index 00000000..e64c6f7c --- /dev/null +++ b/src/ui/widgets/tag_metas_preview/tag_metas_preview.py @@ -0,0 +1,55 @@ +from typing import List, Union, Dict + +from supervisely.app.widgets import Widget +from supervisely import TagMeta, TagMetaCollection +from supervisely.annotation.tag_meta import TagValueType +from supervisely.app.content import StateJson +from supervisely.imaging.color import rgb2hex + + +VALUE_TYPE_NAME = { + str(TagValueType.NONE): "NONE", + str(TagValueType.ANY_STRING): "TEXT", + str(TagValueType.ONEOF_STRING): "ONE OF", + str(TagValueType.ANY_NUMBER): "NUMBER", +} + + +class TagMetasPreview(Widget): + def __init__( + self, + tag_metas: Union[List[TagMeta], TagMetaCollection] = [], + max_width: int = 300, + widget_id: int = None, + ): + self._tag_metas = tag_metas + self._max_width = self._get_max_width(max_width) + + super().__init__(widget_id=widget_id, file_path=__file__) + + def _get_max_width(self, value): + if value < 150: + value = 150 + return f"{value}px" + + def get_json_data(self): + return { + "maxWidth": self._max_width, + } + + def get_json_state(self) -> Dict: + return { + "tags": [ + { + "name": f"{tag_meta.name}", + "valueType": VALUE_TYPE_NAME[tag_meta.value_type], + "color": rgb2hex(tag_meta.color), + } + for tag_meta in self._tag_metas + ] + } + + def set(self, tag_metas: Union[List[TagMeta], TagMetaCollection]): + self._tag_metas = tag_metas + StateJson()[self.widget_id] = self.get_json_state() + StateJson().send_changes() diff --git a/src/ui/widgets/tag_metas_preview/template.html b/src/ui/widgets/tag_metas_preview/template.html new file mode 100644 index 00000000..5d6673d4 --- /dev/null +++ b/src/ui/widgets/tag_metas_preview/template.html @@ -0,0 +1,29 @@ +
+
+ +
+ + + + + + + +
+
+
diff --git a/src/utils.py b/src/utils.py index 8cfecfa1..efad5dad 100644 --- a/src/utils.py +++ b/src/utils.py @@ -225,7 +225,15 @@ def create_preview_dir(): def init_layers(nodes_state: dict): - from src.ui.dtl import DATA_ACTIONS, SAVE_ACTIONS, TRANSFORMATION_ACTIONS, actions_list + from src.ui.dtl import ( + SOURCE_ACTIONS, + SAVE_ACTIONS, + PIXEL_LEVEL_TRANSFORMS, + SPATIAL_LEVEL_TRANSFORMS, + ANNOTATION_TRANSFORMS, + OTHER, + actions_list, + ) data_layers_ids = [] save_layers_ids = [] @@ -235,14 +243,22 @@ def init_layers(nodes_state: dict): layer = g.layers[node_id] layer: Layer - layer.clear() layer.parse_options(node_options) - if layer.action.name in actions_list[DATA_ACTIONS]: + if layer.action.name in actions_list[SOURCE_ACTIONS]: data_layers_ids.append(node_id) if layer.action.name in actions_list[SAVE_ACTIONS]: save_layers_ids.append(node_id) - if layer.action.name in actions_list[TRANSFORMATION_ACTIONS]: + if layer.action.name in [ + action + for group in ( + PIXEL_LEVEL_TRANSFORMS, + SPATIAL_LEVEL_TRANSFORMS, + ANNOTATION_TRANSFORMS, + OTHER, + ) + for action in actions_list[group] + ]: transform_layers_ids.append(node_id) all_layers_ids.append(node_id) @@ -281,7 +297,7 @@ def get_input_metas(data_layers_ids): return input_metas -def set_preview(data_layers_ids): +def update_preview(data_layers_ids): for data_layer_id in data_layers_ids: data_layer = g.layers[data_layer_id] src = data_layer.get_src() @@ -297,7 +313,7 @@ def set_preview(data_layers_ids): preview_img = sly.image.read(preview_img_path) with open(preview_ann_path, "r") as f: preview_ann = sly.Annotation.from_json(json.load(f), project_meta) - data_layer.set_preview(preview_img, preview_ann) + data_layer.update_preview(preview_img, preview_ann) def init_output_metas(net, all_layers_ids: list): @@ -320,6 +336,8 @@ def init_output_metas(net, all_layers_ids: list): def update_previews(net, data_layers_ids: list, all_layers_ids: list): + for layer in g.layers.values(): + layer.clear_preview() updated = set() net.preprocess() for data_layer_id in data_layers_ids: @@ -356,7 +374,7 @@ def update_previews(net, data_layers_ids: list, all_layers_ids: list): img_desc, ann, _ = data_el else: img_desc, ann = data_el - layer.set_preview(img_desc.read_image(), ann) + layer.update_preview(img_desc.read_image(), ann) updated.add(layer_indx) @@ -415,3 +433,13 @@ def create_results_widget(file_infos, supervisely_layers): ) ) return Container(widgets=widgets) + + +def get_layer_id(action_name: str): + g.layers_count += 1 + id = action_name + "_" + str(g.layers_count) + return id + + +def register_layer(layer: Layer): + g.layers[layer.id] = layer