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/Net.py b/src/compute/Net.py index 6ffd339d..b6f31e56 100644 --- a/src/compute/Net.py +++ b/src/compute/Net.py @@ -32,11 +32,11 @@ def __init__(self, graph_desc, output_folder): if not os.path.exists(graph_path): raise RuntimeError('No such config file "%s"' % graph_path) else: - graph = json.load(open(graph_path, "r")) + self.graph = json.load(open(graph_path, "r")) else: - graph = graph_desc + self.graph = graph_desc - for layer_config in graph: + for layer_config in self.graph: if "action" not in layer_config: raise RuntimeError('No "action" field in layer "{}".'.format(layer_config)) action = layer_config["action"] @@ -203,20 +203,23 @@ def start(self, data_el): for output in output_generator: yield output - def start_iterate(self, data_el): + def start_iterate(self, data_el, layer_idx: int = None): img_pr_name = data_el[0].get_pr_name() img_ds_name = data_el[0].get_ds_name() - start_layer_indxs = set() - for idx, layer in enumerate(self.layers): - if layer.type != "data": - continue - if layer.project_name == img_pr_name and ( - "*" in layer.dataset_names or img_ds_name in layer.dataset_names - ): - start_layer_indxs.add(idx) - if len(start_layer_indxs) == 0: - raise RuntimeError("Can not find data layer for the image: {}".format(data_el)) + if layer_idx is not None: + start_layer_indxs = [layer_idx] + else: + start_layer_indxs = set() + for idx, layer in enumerate(self.layers): + if layer.type != "data": + continue + if layer.project_name == img_pr_name and ( + "*" in layer.dataset_names or img_ds_name in layer.dataset_names + ): + start_layer_indxs.add(idx) + if len(start_layer_indxs) == 0: + raise RuntimeError("Can not find data layer for the image: {}".format(data_el)) for start_layer_indx in start_layer_indxs: output_generator = self.process_iterate(start_layer_indx, data_el) diff --git a/src/compute/layers/processing/BlurLayer.py b/src/compute/layers/processing/BlurLayer.py index e844df3a..9361fc19 100644 --- a/src/compute/layers/processing/BlurLayer.py +++ b/src/compute/layers/processing/BlurLayer.py @@ -11,8 +11,7 @@ class BlurLayer(Layer): - - action = 'blur' + action = "blur" layer_settings = { "required": ["settings"], @@ -22,16 +21,13 @@ class BlurLayer(Layer): "oneOf": [ { "type": "object", - "required": [ - "name", - "sigma" - ], + "required": ["name", "sigma"], "properties": { "name": { "type": "string", "enum": [ "gaussian", - ] + ], }, "sigma": { "type": "object", @@ -39,45 +35,39 @@ class BlurLayer(Layer): "properties": { "min": {"type": "number", "minimum": 0.01}, "max": {"type": "number", "minimum": 0.01}, - } - } - } + }, + }, + }, }, { "type": "object", - "required": [ - "name", - "kernel" - ], + "required": ["name", "kernel"], "properties": { "name": { "type": "string", "enum": [ "median", - ] + ], }, - "kernel": { - "type": "integer", - "minimum": 3 - } - } - } - ] + "kernel": {"type": "integer", "minimum": 3}, + }, + }, + ], } - } + }, } def __init__(self, config): Layer.__init__(self, config) - if (self.settings['name'] == 'median') and (self.settings['kernel'] % 2 == 0): - raise RuntimeError('Kernel for median blur must be odd.') + if (self.settings["name"] == "median") and (self.settings["kernel"] % 2 == 0): + raise RuntimeError("Kernel for median blur must be odd.") def check_min_max(dictionary, text): - if dictionary['min'] > dictionary['max']: + if dictionary["min"] > dictionary["max"]: raise RuntimeError('"min" should be <= than "max" for "{}".'.format(text)) - if self.settings['name'] == 'gaussian': - check_min_max(self.settings['sigma'], 'sigma') + if self.settings["name"] == "gaussian": + check_min_max(self.settings["sigma"], "sigma") def requires_image(self): return True @@ -86,13 +76,13 @@ def process(self, data_el: Tuple[ImageDescriptor, Annotation]): img_desc, ann = data_el img = img_desc.read_image() - img = img.astype(np.float32) - if self.settings['name'] == 'gaussian': - sigma_b = self.settings['sigma'] - sigma_value = np.random.uniform(sigma_b['min'], sigma_b['max']) + img = img.astype(np.uint8) + if self.settings["name"] == "gaussian": + sigma_b = self.settings["sigma"] + sigma_value = np.random.uniform(sigma_b["min"], sigma_b["max"]) res_img = cv2.GaussianBlur(img, ksize=(0, 0), sigmaX=sigma_value) - elif self.settings['name'] == 'median': - res_img = cv2.medianBlur(img, ksize=self.settings['kernel']) + elif self.settings["name"] == "median": + res_img = cv2.medianBlur(img, ksize=self.settings["kernel"]) else: raise NotImplementedError() diff --git a/src/compute/layers/processing/RotateLayer.py b/src/compute/layers/processing/RotateLayer.py index 862e94a0..f66317ac 100644 --- a/src/compute/layers/processing/RotateLayer.py +++ b/src/compute/layers/processing/RotateLayer.py @@ -76,8 +76,6 @@ def expand_image_with_rect(img: np.ndarray, req_rect: Rectangle): def process(self, data_el: Tuple[ImageDescriptor, Annotation]): img_desc, ann = data_el - aug.rotate(mode=aug.RotationModes.KEEP) - angle_dct = self.settings["rotate_angles"] min_degrees, max_degrees = angle_dct["min_degrees"], angle_dct["max_degrees"] rotate_degrees = np.random.uniform(min_degrees, max_degrees) @@ -103,9 +101,20 @@ def process(self, data_el: Tuple[ImageDescriptor, Annotation]): if black_reg_mode == "preserve_size": rect_to_crop = Rectangle.from_array(img) new_img, (delta_x, delta_y) = self.expand_image_with_rect(new_img, rect_to_crop) - new_ann.img_size = new_img.shape[:2] - new_ann = apply_to_labels(ann, lambda x: x.translate(delta_x, delta_y)) + top_pad = max((new_img.shape[0] - ann.img_size[0]) // 2, 0) + lefet_pad = max((new_img.shape[1] - ann.img_size[1]) // 2, 0) + new_img, new_ann = aug.crop( + new_img, + new_ann, + top_pad=top_pad, + bottom_pad=new_img.shape[0] - top_pad - ann.img_size[0], + left_pad=lefet_pad, + right_pad=new_img.shape[1] - lefet_pad - ann.img_size[1], + ) + new_ann.clone(img_size=new_img.shape[:2]) + + new_ann = apply_to_labels(new_ann, lambda x: [x.translate(delta_x, delta_y)]) if new_img is None: return # no yield diff --git a/src/exceptions.py b/src/exceptions.py new file mode 100644 index 00000000..3af5e529 --- /dev/null +++ b/src/exceptions.py @@ -0,0 +1,117 @@ +from typing import Optional, Union +import traceback + +from supervisely import logger +from supervisely import ProjectMeta + + +class CustomException(Exception): + def __init__( + self, message: str, error: Optional[Exception] = None, extra: Optional[dict] = None + ): + super().__init__(message) + self.message = message + self.error = error + self.extra = extra + + def __str__(self): + return self.message + + def log(self): + exc_info = ( + traceback.format_tb(self.error.__traceback__) + if self.error + else traceback.format_tb(self.__traceback__) + ) + logger.error(self.message, exc_info=exc_info, extra=self.extra) + + +class ActionNotFoundError(CustomException): + def __init__(self, action_name: str, extra: Optional[dict] = {}): + self.action_name = action_name + extra["action_name"] = action_name + super().__init__("Action not found", extra=extra) + + +class CreateLayerError(CustomException): + def __init__(self, action_name: str, error: Exception, extra: Optional[dict] = {}): + self.action_name = action_name + extra["action_name"] = action_name + super().__init__(f"Error creating Layer", error=error, extra=extra) + + +class LayerNotFoundError(CustomException): + def __init__(self, layer_id: str, extra: Optional[dict] = {}): + self.layer_id = layer_id + extra["layer_id"] = layer_id + super().__init__("Layer not found", extra=extra) + + +class CreateNodeError(CustomException): + def __init__(self, layer_name, error: Exception, extra: Optional[dict] = {}): + self.layer_name = layer_name + extra["layer_name"] = layer_name + super().__init__(f"Error creating Node", error=error, extra=extra) + + +class UpdateMetaError(CustomException): + def __init__( + self, + layer_name: str, + project_meta: ProjectMeta, + error: Exception, + extra: Optional[dict] = {}, + ): + self.layer_name = layer_name + self.project_meta = project_meta + extra["layer_name"] = layer_name + extra["project_meta"] = project_meta.to_json() + super().__init__( + f"Error updating project meta", + error=error, + extra=extra, + ) + + +class GraphCalculationError(CustomException): + def __init__( + self, + dtl_json: list, + error: Exception, + layer_name: Optional[str] = "", + extra: Optional[dict] = {}, + ): + extra["dtl_json"] = dtl_json + extra["layer_name"] = layer_name + super().__init__( + "Error performing transformations", + error=error, + extra=extra, + ) + + +class BadSettingsError(CustomException): + def __init__( + self, + layer_name: str, + settings_json: dict, + error: Exception = None, + extra: Optional[dict] = {}, + ): + extra["layer_name"] = layer_name + extra["settings"] = settings_json + super().__init__("Bad settings", error, extra=extra) + + +def handle_exception(func): + """Decorator to log exception and silence it""" + + def inner(*args, **kwargs): + try: + return func(*args, **kwargs) + except CustomException as e: + e.log() + except Exception as e: + logger.error("Unexpected error", exc_info=traceback.format_exc()) + + return inner diff --git a/src/globals.py b/src/globals.py index ff44bd81..fc1fac28 100644 --- a/src/globals.py +++ b/src/globals.py @@ -1,4 +1,5 @@ import os +import queue from dotenv import load_dotenv import supervisely as sly @@ -29,3 +30,14 @@ layers_count = 0 layers = {} + + +update_queue = queue.Queue() + + +def updater(update: str): + global update_queue + update_queue.put(update) + + +context_menu_position = None diff --git a/src/main.py b/src/main.py index aff9a189..93c64a42 100644 --- a/src/main.py +++ b/src/main.py @@ -1,10 +1,44 @@ import shutil import os +import threading +import time from supervisely import Application from src.ui.ui import layout +from src.ui.tabs.configure import update_metas, update_nodes +from src.ui.tabs.json_preview import load_json import src.globals as g shutil.rmtree(g.STATIC_DIR, ignore_errors=True) os.mkdir(g.STATIC_DIR) app = Application(layout=layout, static_dir=g.STATIC_DIR) + + +def _update_f(): + while True: + updates = [] + while not g.update_queue.empty(): + updates.append(g.update_queue.get()) + if len(updates) == 0: + time.sleep(0.1) + continue + try: + if "load_json" in updates: + load_json() + elif "nodes" in updates: + update_nodes() + else: + update_metas() + finally: + for _ in range(len(updates)): + g.update_queue.task_done() + time.sleep(0.1) + + +update_loop = threading.Thread( + target=_update_f, + name="App update loop", + daemon=True, +) + +update_loop.start() diff --git a/src/ui/dtl/Action.py b/src/ui/dtl/Action.py index 560ec092..6d290550 100644 --- a/src/ui/dtl/Action.py +++ b/src/ui/dtl/Action.py @@ -2,7 +2,6 @@ from typing import Optional from supervisely.app.widgets import NodesFlow, Container, Text -import src.globals as g class Action: @@ -10,12 +9,10 @@ class Action: title = None docs_url = None description = None + md_description = "" width = 340 - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {} + header_color = None + header_text_color = None @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -23,18 +20,48 @@ def create_new_layer(cls, layer_id: Optional[str] = None): @classmethod def create_inputs(cls): - return [NodesFlow.Node.Input("source", "Source")] + return [NodesFlow.Node.Input("source", "Input", color="#000000")] @classmethod def create_outputs(cls): - return [NodesFlow.Node.Output("destination", "Destination")] + return [NodesFlow.Node.Output("destination", "Output", color="#000000")] @classmethod def create_info_widget(cls): return Container( widgets=[ - Text(f"
{cls.description}
", color="white"), + Text(f"{cls.description}
"), ] ) + + +class SourceAction(Action): + header_color = "#13ce66" + header_text_color = "#000000" + + +class PixelLevelAction(Action): + header_color = "#c9a5fa" + header_text_color = "#000000" + + +class SpatialLevelAction(Action): + header_color = "#fcd068" + header_text_color = "#000000" + + +class AnnotationAction(Action): + header_color = "#90ddf5" + header_text_color = "#000000" + + +class OtherAction(Action): + header_color = "#cfcfcf" + header_text_color = "#000000" + + +class OutputAction(Action): + header_color = "#ff5e90" + header_text_color = "#000000" diff --git a/src/ui/dtl/Layer.py b/src/ui/dtl/Layer.py index 95d5ae20..2d051163 100644 --- a/src/ui/dtl/Layer.py +++ b/src/ui/dtl/Layer.py @@ -1,40 +1,48 @@ -import copy +import time +from typing import Optional +import random + from supervisely import Annotation -from supervisely.app.widgets import LabeledImage, NodesFlow +from supervisely.app.widgets import ( + LabeledImage, + NodesFlow, + Markdown, + Button, + Text, +) from supervisely.imaging.image import write as write_image -from src.ui.dtl.Action import Action - -import numpy as np - - -import random -from typing import List, Optional +from src.ui.dtl.Action import Action +from src.ui.dtl.utils import ( + get_separator, + get_set_settings_button_style, + get_set_settings_container, +) +import src.globals as g +from src.compute.dtl_utils.image_descriptor import ImageDescriptor class Layer: def __init__( self, action: Action, - options: List[NodesFlow.Node.Option], - get_settings: callable, + create_options: callable, get_src: Optional[callable] = None, - meta_changed_cb: Optional[callable] = None, + get_settings: Optional[callable] = None, get_dst: Optional[callable] = None, - set_settings_from_json: callable = None, + meta_changed_cb: Optional[callable] = None, id: Optional[str] = None, ): self.action = action - self._id = id - if self._id is None: - self._id = action.name + "_" + "".join(random.choice("0123456789") for _ in range(8)) + self.id = id + if self.id is None: + self.id = action.name + "_" + "".join(random.choice("0123456789") for _ in range(8)) - self._options = options + self._create_options = create_options self._get_settings = get_settings self._get_src = get_src - self._meta_changed_cb = meta_changed_cb self._get_dst = get_dst - self._set_settings_from_json = set_settings_from_json + self._meta_changed_cb = meta_changed_cb self._src = [] self._settings = {} @@ -42,38 +50,61 @@ def __init__( self.output_meta = None - self._preview_img_url = f"static/{self._id}.jpg" + md_description = self.action.md_description.replace( + r"../../assets", r"https://raw.githubusercontent.com/supervisely/docs/master/assets" + ) + + # info option + self._info_option = NodesFlow.Node.Option( + name="sidebarNodeInfo", + option_component=NodesFlow.SidebarNodeInfoOptionComponent( + sidebar_template=Markdown(md_description).to_html(), + sidebar_width=600, + ), + ) + # preview option + self._preview_img_url = f"static/{self.id}.jpg" self._ann = None + self._img_desc = None + self._preview_widget = LabeledImage(enable_zoom=True) + self._update_preview_button = Button( + text="Update", + icon="zmdi zmdi-refresh", + button_type="text", + button_size="small", + style=get_set_settings_button_style(), + ) - self._add_info_option() - self._add_preview_option() + @self._update_preview_button.click + def _update_preview_btn_click_cb(): + g.updater("nodes") - def _add_info_option(self): - self._options = [ + self._preview_options = [ + # NodesFlow.Node.Option( + # name="preview_text", option_component=NodesFlow.TextOptionComponent("Preview") + # ), NodesFlow.Node.Option( - name="Info", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - self.action.create_info_widget() - ) + name="update_preview_btn", + option_component=NodesFlow.WidgetOptionComponent( + widget=get_set_settings_container(Text("Preview"), self._update_preview_button) ), ), - *self._options, - ] - - def _add_preview_option(self): - self._preview_widget = LabeledImage(enable_zoom=True) - self._options = [ - *self._options, - NodesFlow.Node.Option( - name="preview_text", option_component=NodesFlow.TextOptionComponent("Preview") - ), NodesFlow.Node.Option( name="preview", option_component=NodesFlow.WidgetOptionComponent(widget=self._preview_widget), ), ] + def get_src(self) -> list: + return self._src + + def get_dst(self) -> list: + return self._dst + + def get_settings(self) -> dict: + return self._settings + + # JSON def to_json(self) -> dict: return { "action": self.action.name, @@ -82,103 +113,130 @@ def to_json(self) -> dict: "settings": self._settings, } - def get_destination_name(self, dst_index: int): - outputs = self.action.create_outputs() - return outputs[dst_index].name - - def set_settings_from_json(self, json_data: dict, node_state: dict): - node_state = copy.deepcopy(node_state) - settings = json_data["settings"] - for settings_key, value in settings.items(): - node_state_key = self.action._settings_mapping.get(settings_key, settings_key) - if node_state_key is not None: - node_state[node_state_key] = value - if self._set_settings_from_json is not None: - node_state = self._set_settings_from_json(json_data, node_state) - return node_state - + def from_json(self, json_data: dict = {}) -> None: + """Init src, dst and settings from json data""" + src = json_data.get("src", []) + if isinstance(src, str): + src = [src] + self._src = src + dst = json_data.get("dst", []) + if isinstance(dst, str): + dst = [dst] + self._dst = dst + self._settings = json_data.get("settings", {}) + + # NodesFlow.Node def create_node(self) -> NodesFlow.Node: + """creates node from src, dst and settings""" self._inputs = self.action.create_inputs() self._outputs = self.action.create_outputs() + options = self._create_options(src=self._src, dst=self._dst, settings=self._settings) + + def combine_options(options: list): + result_options = [ + self._info_option, + get_separator(0), + ] + if len(options["src"]) > 0: + result_options.extend(options["src"]) + result_options.append(get_separator(1)) + + if len(options["dst"]) > 0: + result_options.extend(options["dst"]) + result_options.append(get_separator(2)) + + if len(options["settings"]) > 0: + result_options.extend(options["settings"]) + result_options.append(get_separator(3)) + + return [ + *result_options, + *self._preview_options, + ] + return NodesFlow.Node( - id=self._id, + id=self.id, name=self.action.title, width=self.action.width, - options=self._options, + options=combine_options(options), inputs=self._inputs, outputs=self._outputs, + inputs_up=True, + header_color=self.action.header_color, + header_text_color=self.action.header_text_color, ) - def update_src(self, node_options: dict): - if self._get_src is not None: - self._src = self._get_src(options_json=node_options) - else: - self._src = [] - - def update_dst(self, node_options: dict): - if self._get_dst is not None: - self._dst = self._get_dst(options_json=node_options) - else: - self._dst = self._create_destinations() - - def update_settings(self, node_options: dict): - if self._get_settings is not None: - self._settings = self._get_settings(options_json=node_options) - else: - self._settings = {} - def parse_options(self, node_options: dict): - self.update_src(node_options) - self.update_dst(node_options) - self.update_settings(node_options) + """Read node options and init src, dst and settings""" + self._update_src(node_options) + self._update_dst(node_options) + self._update_settings(node_options) def add_source(self, from_node_id, from_node_interface): src_name = self._connection_name(from_node_id, from_node_interface) self._src.append(src_name) + def clear_preview(self): + self._preview_widget.clean_up() + + def get_preview_img_desc(self): + return self._img_desc + + def update_preview(self, img_desc: ImageDescriptor, ann: Annotation): + self._img_desc = img_desc + write_image(self._preview_img_url, img_desc.read_image()) + self._ann = ann + self._preview_widget.set( + title=None, image_url=f"{self._preview_img_url}?{time.time()}", ann=self._ann + ) + + def set_preview_loading(self, val: bool): + self._preview_widget.loading = val + self._update_preview_button.loading = val + + def get_ann(self): + return self._ann + + def update_project_meta(self, project_meta): + if self._meta_changed_cb is not None: + self._meta_changed_cb(project_meta) + + # Utils + def get_destination_name(self, dst_index: int): + outputs = self.action.create_outputs() + return outputs[dst_index].name + def _connection_name(self, name: str, interface: str): interface_str = "_".join( [ *[ part for part in interface.split("_") - if part not in ["", "source", "destination"] + if part not in ["", "source", "destination", "input", "output"] ], ] ) return "$" + name + (f"__{interface_str}" if interface_str else "") def _create_destinations(self): - return [self._connection_name(self._id, output.name) for output in self._outputs] - - def clear_sources(self): - self._src = [] - - def clear_destinations(self): - self._dst = [] + return [self._connection_name(self.id, output.name) for output in self._outputs] - def clear_settings(self): - self._settings = {} - - def clear(self): - self.clear_sources() - self.clear_destinations() - self.clear_settings() - - def get_src(self): - return self._src - - def get_dst(self): - return self._dst - - def set_preview(self, img: np.ndarray, ann: Annotation): - write_image(self._preview_img_url, img) - self._ann = ann - self._preview_widget.set(title=None, image_url=self._preview_img_url, ann=self._ann) + def _update_src(self, node_options: dict): + if self._get_src is not None: + self._src = self._get_src(options_json=node_options) + else: + self._src = [] - def get_ann(self): - return self._ann + def _update_dst(self, node_options: dict): + """Read node options and init dst""" + if self._get_dst is not None: + self._dst = self._get_dst(options_json=node_options) + else: + self._dst = self._create_destinations() - def meta_changed_cb(self, project_meta): - if self._meta_changed_cb is not None: - self._meta_changed_cb(project_meta) + def _update_settings(self, node_options: dict): + """Read node options and init settings""" + if self._get_settings is not None: + self._settings = self._get_settings(options_json=node_options) + else: + self._settings = {} diff --git a/src/ui/dtl/__init__.py b/src/ui/dtl/__init__.py index 725f77d2..4919759a 100644 --- a/src/ui/dtl/__init__.py +++ b/src/ui/dtl/__init__.py @@ -1,4 +1,12 @@ -from .Action import Action +from .Action import ( + Action, + SourceAction, + PixelLevelAction, + SpatialLevelAction, + AnnotationAction, + OtherAction, + OutputAction, +) from .actions.data import DataAction from .actions.approx_vector import ApproxVectorAction from .actions.background import BackgroundAction @@ -17,7 +25,7 @@ from .actions.dummy import DummyAction from .actions.duplicate_objects import DuplicateObjectsAction from .actions.find_contours import FindContoursAction -from .actions.flip import FlipAction +from .actions.flip.flip import FlipAction from .actions.if_action import IfAction from .actions.instances_crop import InstancesCropAction from .actions.line2bitmap import LineToBitmapAction @@ -30,6 +38,7 @@ from .actions.rename import RenameAction from .actions.rasterize import RasterizeAction from .actions.resize import ResizeAction +from .actions.rotate import RotateAction from .actions.skeletonize import SkeletonizeAction from .actions.sliding_window import SlidingWindowAction from .actions.split_masks import SplitMasksAction @@ -39,49 +48,60 @@ from .actions.supervisely import SuperviselyAction -DATA_ACTIONS = "Data actions" -TRANSFORMATION_ACTIONS = "Transformation actions" +SOURCE_ACTIONS = "Source actions" +# TRANSFORMATION_ACTIONS = "Transformation actions" +PIXEL_LEVEL_TRANSFORMS = "Pixel-level transforms" +SPATIAL_LEVEL_TRANSFORMS = "Spatial-level transforms" +ANNOTATION_TRANSFORMS = "Annotation transforms" +OTHER = "Other" SAVE_ACTIONS = "Save actions" actions_list = { - DATA_ACTIONS: [DataAction.name], - TRANSFORMATION_ACTIONS: [ + SOURCE_ACTIONS: [DataAction.name], + PIXEL_LEVEL_TRANSFORMS: [ + BlurAction.name, + ContrastBrightnessAction.name, + NoiseAction.name, + ], + SPATIAL_LEVEL_TRANSFORMS: [ + CropAction.name, + FlipAction.name, + InstancesCropAction.name, + MultiplyAction.name, + ResizeAction.name, + RotateAction.name, + SlidingWindowAction.name, + ], + ANNOTATION_TRANSFORMS: [ ApproxVectorAction.name, BackgroundAction.name, BBoxAction.name, BboxToPolyAction.name, Bitmap2LinesAction.name, BitwiseMasksAction.name, - BlurAction.name, ColorClassAction.name, - ContrastBrightnessAction.name, - CropAction.name, - DatasetAction.name, DropByClassAction.name, DropLinesByLengthAction.name, DropNoiseAction.name, - DummyAction.name, DuplicateObjectsAction.name, FindContoursAction.name, - FlipAction.name, - IfAction.name, - InstancesCropAction.name, LineToBitmapAction.name, MergeBitmapsAction.name, - MultiplyAction.name, - NoiseAction.name, ObjectsFilterAction.name, PolygonToBitmapAction.name, RandomColorsAction.name, RasterizeAction.name, RenameAction.name, - ResizeAction.name, SkeletonizeAction.name, - SlidingWindowAction.name, SplitMasksAction.name, TagAction.name, ], + OTHER: [ + DatasetAction.name, + DummyAction.name, + IfAction.name, + ], SAVE_ACTIONS: [ SaveAction.name, SaveMasksAction.name, @@ -123,6 +143,7 @@ RasterizeAction.name: RasterizeAction, RenameAction.name: RenameAction, ResizeAction.name: ResizeAction, + RotateAction.name: RotateAction, SkeletonizeAction.name: SkeletonizeAction, SlidingWindowAction.name: SlidingWindowAction, SplitMasksAction.name: SplitMasksAction, diff --git a/src/ui/dtl/actions/approx_vector.py b/src/ui/dtl/actions/approx_vector.py index fc4cf0f3..5156a6c1 100644 --- a/src/ui/dtl/actions/approx_vector.py +++ b/src/ui/dtl/actions/approx_vector.py @@ -1,49 +1,41 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta from supervisely import Polygon, Polyline, AnyGeometry -from src.ui.dtl import Action + +import src.globals as g +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview +from src.ui.dtl.utils import get_separator -class ApproxVectorAction(Action): +class ApproxVectorAction(AnnotationAction): name = "approx_vector" title = "Approx Vector" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/approx_vector" ) description = "This layer (approx_vector) approximates vector figures: lines and polygons. The operation decreases number of vertices with Douglas-Peucker algorithm." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes = ClassesList(multiple=True) + classes_preview = ClassesListPreview() + classes_save_button = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_settings = [] def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes": [obj_class.name for obj_class in classes.get_selected_classes()], + "classes": saved_classes_settings, "epsilon": options_json["epsilon"], } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] - obj_class_names = settings["classes"] - classes.loading = True - classes.select(obj_class_names) - classes.loading = False - node_state["epsilon"] = settings["epsilon"] - return node_state - def meta_changed_cb(project_meta: ProjectMeta): + nonlocal saved_classes_settings classes.loading = True classes.set( [ @@ -52,40 +44,67 @@ def meta_changed_cb(project_meta: ProjectMeta): if cls.geometry_type in [Polygon, Polyline, AnyGeometry] ] ) + saved_classes_settings = [ + obj_class.name for obj_class in classes.get_selected_classes() + ] + classes.loading = False + + def _save_classes_setting(): + nonlocal saved_classes_settings + selected_classes = classes.get_selected_classes() + saved_classes_settings = [obj_class.name for obj_class in selected_classes] + classes_preview.set(selected_classes) + g.updater("metas") + + classes_save_button.click(_save_classes_setting) + + def _set_settings_from_json(settings: dict): + obj_class_names = settings.get("classes", []) + classes.loading = True + classes.select(obj_class_names) + _save_classes_setting() classes.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes"), - ), - NodesFlow.Node.Option( - name="Select Classes", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + epsilon_val = settings.get("epsilon", 3) + if epsilon_val < 1: + raise ValueError("Epsilon must be greater than 0") + settings_options = [ + NodesFlow.Node.Option( + name="Select Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes, classes_save_button]) + ) + ), + ), + NodesFlow.Node.Option( + name="classes_preview_text", + option_component=NodesFlow.WidgetOptionComponent(classes_preview), + ), + get_separator(10), + NodesFlow.Node.Option( + name="epsilon_text", + option_component=NodesFlow.TextOptionComponent("Epsilon"), ), - ), - NodesFlow.Node.Option( - name="epsilon_text", - option_component=NodesFlow.TextOptionComponent("Epsilon"), - ), - NodesFlow.Node.Option( - name="epsilon", - option_component=NodesFlow.IntegerOptionComponent(min=1, default_value=3), - ), - ] + NodesFlow.Node.Option( + name="epsilon", + option_component=NodesFlow.IntegerOptionComponent( + min=1, default_value=epsilon_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - set_settings_from_json=set_settings_from_json, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - id=layer_id, ) diff --git a/src/ui/dtl/actions/background.py b/src/ui/dtl/actions/background.py index 06201918..a67f2df3 100644 --- a/src/ui/dtl/actions/background.py +++ b/src/ui/dtl/actions/background.py @@ -1,10 +1,12 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -class BackgroundAction(Action): +class BackgroundAction(AnnotationAction): name = "background" title = "Background" docs_url = ( @@ -20,24 +22,26 @@ def get_settings(options_json: dict) -> dict: "class": options_json["class"] if options_json["class"] else "", } - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="class_text", - option_component=NodesFlow.TextOptionComponent("Background Class name"), - ), - NodesFlow.Node.Option(name="class", option_component=NodesFlow.InputOptionComponent()), - ] + def create_options(src: list, dst: list, settings: dict) -> dict: + class_val = settings.get("class", "") + settings_options = [ + NodesFlow.Node.Option( + name="class_text", + option_component=NodesFlow.TextOptionComponent("Background Class name"), + ), + NodesFlow.Node.Option( + name="class", option_component=NodesFlow.InputOptionComponent(class_val) + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/bbox.py b/src/ui/dtl/actions/bbox.py index b37525b5..606869ad 100644 --- a/src/ui/dtl/actions/bbox.py +++ b/src/ui/dtl/actions/bbox.py @@ -1,94 +1,166 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox from supervisely import ProjectMeta -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) +import src.globals as g -class BBoxAction(Action): +class BBoxAction(AnnotationAction): name = "bbox" title = "Bounding Box" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox" description = "Bounding Box layer (bbox) converts annotations of specified classes to bounding boxes. Annotations would be replaced with new objects of shape rectangle." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - classes_mapping_widget.loading = True - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) - classes_mapping_widget.loading = False - return node_state - def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True + old_obj_classes = classes_mapping_widget.get_classes() + + # set classes to widget classes_mapping_widget.set(project_meta.obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + project_meta.obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, + ) + + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Classes Mapping"), - ), - NodesFlow.Node.Option( - name="Set Classes Mapping", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ) + ), + ), + NodesFlow.Node.Option( + name="Classes Mapping Preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bbox2poly.py b/src/ui/dtl/actions/bbox2poly.py index 7b41ac14..331c4f7e 100644 --- a/src/ui/dtl/actions/bbox2poly.py +++ b/src/ui/dtl/actions/bbox2poly.py @@ -1,102 +1,173 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox from supervisely import ProjectMeta, Rectangle, AnyGeometry -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) +import src.globals as g -class BboxToPolyAction(Action): +class BboxToPolyAction(AnnotationAction): name = "bbox2poly" title = "BBox to Polygon" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bbox2poly" ) description = 'This layer (bbox2poly) converts rectangles ("bounding boxes") to polygons.' - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, } def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Rectangle, AnyGeometry] - ] + old_obj_classes = classes_mapping_widget.get_classes() + new_obj_classes = [ + obj_class + for obj_class in project_meta.obj_classes + if obj_class.geometry_type in [Rectangle, AnyGeometry] + ] + + # set classes to widget + classes_mapping_widget.set(new_obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + new_obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, ) - classes_mapping_widget.loading = False - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - classes_mapping_widget.loading = True - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="class_text", - option_component=NodesFlow.TextOptionComponent("Class"), - ), - NodesFlow.Node.Option( - name="Set Classes", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ), + ), + ), + NodesFlow.Node.Option( + name="Classes Mapping Preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bitmap2lines.py b/src/ui/dtl/actions/bitmap2lines.py index 0f0b40eb..206202df 100644 --- a/src/ui/dtl/actions/bitmap2lines.py +++ b/src/ui/dtl/actions/bitmap2lines.py @@ -1,117 +1,182 @@ -import copy -import json from typing import Optional + from supervisely import ProjectMeta, Bitmap, AnyGeometry -from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action +from supervisely.app.widgets import NodesFlow, Button, Container, Flexbox + +import src.globals as g +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesMapping +from src.ui.widgets import ClassesMapping, ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + set_classes_mapping_settings_from_json, +) -class Bitmap2LinesAction(Action): +class Bitmap2LinesAction(AnnotationAction): name = "bitmap2lines" title = "Bitmap to Lines" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitmap2lines" ) description = "This layer (bitmap2lines) converts thinned (skeletonized) bitmaps to lines. It is extremely useful if you have some raster objects representing lines or edges, maybe forming some tree or net structure, and want to work with vector objects. Each input bitmap should be already thinned (use Skeletonize layer to do it), and for single input mask a number of lines will be produced. Resulting lines may have very many vertices, so consider applying Approx Vector layer to results of this layer. Internally the layer builds a graph of 8-connected pixels, determines minimum spanning tree(s), then greedely extracts diameters from connected components of the tree." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_mapping_widget = ClassesMapping() + classes_mapping_preview = ClassesMappingPreview() + classes_mapping_save_btn = Button("Save", icon="zmdi zmdi-floppy") + classes_mapping_set_default_btn = Button("Set Default", icon="zmdi zmdi-refresh") + classes_mapping_widgets_container = Container( + widgets=[ + classes_mapping_widget, + Flexbox( + widgets=[ + classes_mapping_save_btn, + classes_mapping_set_default_btn, + ], + gap=355, + ), + ] + ) + + saved_classes_mapping_settings = {} + default_classes_mapping_settings = {} def _get_classes_mapping_value(): - mapping = classes_mapping_widget.get_mapping() - values = { - name: values["value"] - for name, values in mapping.items() - if not values["ignore"] and not values["default"] - } - return values + return get_classes_mapping_value( + classes_mapping_widget, + default_action="skip", + ignore_action="skip", + other_allowed=False, + default_allowed=False, + ) + + def _set_classes_mapping_preview(): + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _save_classes_mapping_setting(): + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = _get_classes_mapping_value() + set_classes_mapping_preview( + classes_mapping_widget, + classes_mapping_preview, + saved_classes_mapping_settings, + default_action="skip", + ignore_action="skip", + ) + + def _set_default_classes_mapping_setting(): + # save setting to var + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = default_classes_mapping_settings def get_settings(options_json: dict) -> dict: """This function is used to get settings from options json we get from NodesFlow widget""" return { - "classes_mapping": _get_classes_mapping_value(), + "classes_mapping": saved_classes_mapping_settings, "min_points_cnt": options_json["min_points_cnt"], } - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] - classes_mapping = {} - other_default = settings["classes_mapping"].get("__other__", None) == "__default__" - for cls in classes_mapping_widget.get_classes(): - if cls.name in settings["classes_mapping"]: - value = settings["classes_mapping"][cls.name] - if value == "__default__": - value = cls.name - if value == "__ignore__": - value = "" - classes_mapping[cls.name] = value - elif other_default: - classes_mapping[cls.name] = cls.name - else: - classes_mapping[cls.name] = "" - classes_mapping_widget.set_mapping(classes_mapping) - return node_state - def meta_changed_cb(project_meta: ProjectMeta): classes_mapping_widget.loading = True - classes_mapping_widget.set( - [ - cls - for cls in project_meta.obj_classes - if cls.geometry_type in [Bitmap, AnyGeometry] - ] + old_obj_classes = classes_mapping_widget.get_classes() + new_obj_classes = [ + obj_class + for obj_class in project_meta.obj_classes + if obj_class.geometry_type in [Bitmap, AnyGeometry] + ] + + # set classes to widget + classes_mapping_widget.set(new_obj_classes) + + # update settings according to new meta + nonlocal saved_classes_mapping_settings + saved_classes_mapping_settings = classes_mapping_settings_changed_meta( + saved_classes_mapping_settings, + old_obj_classes, + new_obj_classes, + default_action="skip", + ignore_action="skip", + other_allowed=False, ) + + # update settings preview + _set_classes_mapping_preview() + classes_mapping_widget.loading = False - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_mapping_text", - option_component=NodesFlow.TextOptionComponent("Classes Mapping"), - ), - NodesFlow.Node.Option( - name="Set Classes Mapping", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_mapping_widget) + def _set_settings_from_json(settings): + # if settings is empty, set default + if settings.get("classes_mapping", "default") == "default": + classes_mapping_widget.set_default() + else: + set_classes_mapping_settings_from_json( + classes_mapping_widget, + settings["classes_mapping"], + missing_in_settings_action="ignore", + missing_in_meta_action="ignore", + ) + + # save settings + _save_classes_mapping_setting() + # update settings preview + _set_classes_mapping_preview() + + @classes_mapping_save_btn.click + def classes_mapping_save_btn_cb(): + _save_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + @classes_mapping_set_default_btn.click + def classes_mapping_set_default_btn_cb(): + _set_default_classes_mapping_setting() + _set_classes_mapping_preview() + g.updater("metas") + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + min_points_cnt_val = settings.get("min_points_cnt", 2) + settings_options = [ + NodesFlow.Node.Option( + name="Set Classes Mapping", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + classes_mapping_widgets_container + ) + ), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt_text", - option_component=NodesFlow.TextOptionComponent("Min Points Count."), - ), - NodesFlow.Node.Option( - name="min_points_cnt_description", - option_component=NodesFlow.TextOptionComponent( - "Min number of vertices for each output line. Other lines will be dropped." + NodesFlow.Node.Option( + name="classes_mapping_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_mapping_preview), ), - ), - NodesFlow.Node.Option( - name="min_points_cnt", - option_component=NodesFlow.IntegerOptionComponent(min=2, default_value=2), - ), - ] + NodesFlow.Node.Option( + name="min_points_cnt", + option_component=NodesFlow.IntegerOptionComponent( + min=2, default_value=min_points_cnt_val + ), + ), + ] + + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/bitwise_masks.py b/src/ui/dtl/actions/bitwise_masks.py index 298253d8..23148f72 100644 --- a/src/ui/dtl/actions/bitwise_masks.py +++ b/src/ui/dtl/actions/bitwise_masks.py @@ -1,44 +1,58 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta -from src.ui.dtl import Action + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesList +from src.ui.widgets import ClassesList, ClassesListPreview -class BitwiseMasksAction(Action): +class BitwiseMasksAction(AnnotationAction): name = "bitwise_masks" title = "Bitwise Masks" docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/bitwise_masks" ) description = "Bitwise Masks - make bitwise operations between bitmap annotations." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "class_mask": None, - "classes_to_correct": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): class_mask_widget = ClassesList() classes_to_correct_widget = ClassesList(multiple=True) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + class_mask_preview = ClassesListPreview() + classes_to_correct_preview = ClassesListPreview() + + save_class_mask_btn = Button("Save", icon="zmdi zmdi-floppy") + save_classes_to_correct_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_class_mask_settings = "" + saved_classes_to_correct_settings = [] + + def _save_class_mask_setting(): + nonlocal saved_class_mask_settings try: - class_mask = class_mask_widget.get_selected_classes()[0].name + class_mask_obj = class_mask_widget.get_selected_classes()[0] + class_mask = class_mask_obj.name except: + class_mask_obj = None class_mask = "" + saved_class_mask_settings = class_mask + class_mask_preview.set([] if class_mask_obj is None else [class_mask_obj]) + + def _save_classes_to_correct_setting(): + nonlocal saved_classes_to_correct_settings + selected_classes = classes_to_correct_widget.get_selected_classes() + saved_classes_to_correct_settings = [cls.name for cls in selected_classes] + classes_to_correct_preview.set(selected_classes) + + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" return { "type": options_json["type"], - "class_mask": class_mask, - "classes_to_correct": [ - cls.name for cls in classes_to_correct_widget.get_selected_classes() - ], + "class_mask": saved_class_mask_settings, + "classes_to_correct": saved_classes_to_correct_settings, } def meta_changed_cb(project_meta: ProjectMeta): @@ -46,68 +60,85 @@ def meta_changed_cb(project_meta: ProjectMeta): classes_to_correct_widget.loading = True class_mask_widget.set(project_meta.obj_classes) classes_to_correct_widget.set(project_meta.obj_classes) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def _set_settings_from_json(settings): class_mask_widget.loading = True classes_to_correct_widget.loading = True - class_mask_widget.select([settings["class_mask"]]) - classes_to_correct_widget.select(settings["classes_to_correct"]) + class_mask_widget.select([settings["class_mask"]] if "class_mask" in settings else []) + classes_to_correct_widget.select( + settings["classes_to_correct"] if "classes_to_correct" in settings else [] + ) + _save_class_mask_setting() + _save_classes_to_correct_setting() class_mask_widget.loading = False classes_to_correct_widget.loading = False - return node_state + + save_class_mask_btn.click(_save_class_mask_setting) + save_classes_to_correct_btn.click(_save_classes_to_correct_setting) type_items = [NodesFlow.SelectOptionComponent.Item(t, t) for t in ("nor", "and", "or")] - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="type_text", - option_component=NodesFlow.TextOptionComponent("Operation type"), - ), - NodesFlow.Node.Option( - name="type", - option_component=NodesFlow.SelectOptionComponent( - items=type_items, default_value=type_items[0].value + def create_options(src: list, dst: list, settings: dict) -> dict: + type_val = settings.get("type", "nor") + if type_val not in ("nor", "and", "or"): + raise ValueError("Type must be one of: nor, and, or") + + _set_settings_from_json(settings) + + settings_options = [ + NodesFlow.Node.Option( + name="type_text", + option_component=NodesFlow.TextOptionComponent("Operation type"), + ), + NodesFlow.Node.Option( + name="type", + option_component=NodesFlow.SelectOptionComponent( + items=type_items, default_value=type_val + ), ), - ), - NodesFlow.Node.Option( - name="class_mask_text", - option_component=NodesFlow.TextOptionComponent( - "Class Mask. First element of bitwise operation" + NodesFlow.Node.Option( + name="class_mask_text", + option_component=NodesFlow.TextOptionComponent( + "Class Mask. First element of bitwise operation" + ), ), - ), - NodesFlow.Node.Option( - name="Select Class Mask", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(class_mask_widget) + NodesFlow.Node.Option( + name="Select Class Mask", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[class_mask_widget, save_class_mask_btn]) + ) + ), ), - ), - NodesFlow.Node.Option( - name="classes_text", - option_component=NodesFlow.TextOptionComponent("Classes to correct"), - ), - NodesFlow.Node.Option( - name="Select Classes to correct", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_to_correct_widget) + NodesFlow.Node.Option( + name="classes_text", + option_component=NodesFlow.TextOptionComponent("Classes to correct"), ), - ), - ] + NodesFlow.Node.Option( + name="Select Classes to correct", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container( + widgets=[classes_to_correct_widget, save_classes_to_correct_btn] + ) + ) + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/blur.py b/src/ui/dtl/actions/blur.py index 7e061062..b0665d06 100644 --- a/src/ui/dtl/actions/blur.py +++ b/src/ui/dtl/actions/blur.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely.app.widgets import ( NodesFlow, Container, @@ -8,25 +9,18 @@ Field, Flexbox, Text, + Button, ) -from src.ui.dtl import Action + +from src.ui.dtl import PixelLevelAction from src.ui.dtl.Layer import Layer -class BlurAction(Action): +class BlurAction(PixelLevelAction): name = "blur" title = "Blur" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/blur" description = 'Blur layer ("action": "blur") applies blur filter to the image. To use median blur (cv2.medianBlur) set name to median and kernel to odd number. To use gaussian blur (cv2.GaussianBlur) set name to gaussian and sigma to object with two numbers: min and max.' - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "name": None, - "kernel": None, - "sigma": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -49,20 +43,43 @@ def create_new_layer(cls, layer_id: Optional[str] = None): title="Sigma", content=Container( widgets=[ - Flexbox(widgets=[Text("Min", color="white"), sigma_min_input]), - Flexbox(widgets=[Text("Max", color="white"), sigma_max_input]), + Flexbox(widgets=[Text("Min"), sigma_min_input]), + Flexbox(widgets=[Text("Max"), sigma_max_input]), ] ), ), ), ] ) + save_settings_button = Button("Save", icon="zmdi zmdi-floppy") settings_widget = Container( - widgets=[Field(title="Blur type", content=select_name), OneOf(select_name)] + widgets=[ + Field(title="Blur type", content=select_name), + OneOf(select_name), + save_settings_button, + ] ) - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" + type_preview = Text("") + params_preview = Text("") + settings_preview = Container(widgets=[type_preview, params_preview], gap=1) + + saved_settings = {} + + def _update_preview(): + blur_type = saved_settings.get("name", "") + type_preview.text = f"Blur type: {blur_type}" + if blur_type == "": + params_preview.text = "" + elif blur_type == "median": + params_preview.text = f"kernel = {saved_settings.get('kernel')}" + elif blur_type == "gaussian": + params_preview.text = ( + f'sigma = {saved_settings["sigma"]["min"]} - {saved_settings["sigma"]["max"]}' + ) + + def _save_settings(): + nonlocal saved_settings settings = { "name": select_name.get_value(), } @@ -73,41 +90,52 @@ def get_settings(options_json: dict) -> dict: "min": sigma_min_input.get_value(), "max": sigma_max_input.get_value(), } - return settings + saved_settings = settings + _update_preview() - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return saved_settings + + def _set_settings_from_json(settings: dict): settings_widget.loading = True - select_name.set_value(settings["name"]) - if settings["name"] == "median": - kernel_input.value = settings["kernel"] + name = settings.get("name", "median") + select_name.set_value(name) + if name == "median": + kernel_input.value = settings.get("kernel", 5) else: - sigma_min_input.value = settings["sigma"]["min"] - sigma_max_input.value = settings["sigma"]["max"] + sigma_min_v = settings.get("sigma", {}).get("min", 3) + sigma_max_v = settings.get("sigma", {}).get("max", 50) + sigma_min_input.value = sigma_min_v + sigma_max_input.value = sigma_max_v + _save_settings() settings_widget.loading = False - return node_state - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="Set Settings", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(settings_widget) + save_settings_button.click(_save_settings) + + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Settings", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent(settings_widget) + ), + ), + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/color_class.py b/src/ui/dtl/actions/color_class.py index d62cb80e..8a97aa4d 100644 --- a/src/ui/dtl/actions/color_class.py +++ b/src/ui/dtl/actions/color_class.py @@ -1,82 +1,90 @@ from typing import Optional -from supervisely.app.widgets import NodesFlow + +from supervisely.app.widgets import NodesFlow, Button, Container from supervisely import ProjectMeta -from src.ui.dtl import Action +from supervisely.imaging.color import hex2rgb, rgb2hex + +from src.ui.dtl import AnnotationAction from src.ui.dtl.Layer import Layer -from src.ui.widgets import ClassesColorMapping -from supervisely.imaging.color import hex2rgb +from src.ui.widgets import ClassesColorMapping, ClassesMappingPreview -class ColorClassAction(Action): +class ColorClassAction(AnnotationAction): name = "color_class" title = "Color Class" + description = "This layer (color_class) used for coloring classes as you wish. Add this class at the end of graph, before data saving." docs_url = ( "https://docs.supervisely.com/data-manipulation/index/transformation-layers/color_class" ) - description = "This layer (color_class) used for coloring classes as you wish. Add this class at the end of graph, before data saving." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_color_mapping": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): classes_colors = ClassesColorMapping() + classes_colors_preview = ClassesMappingPreview() + classes_colors_save_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_classes_colors_settings = {} + + def _save_classes_colors_setting(): + nonlocal saved_classes_colors_settings + mapping = classes_colors.get_mapping() + saved_classes_colors_settings = { + cls_name: hex2rgb(value["value"]) for cls_name, value in mapping.items() + } + 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) - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="classes_colors_text", - option_component=NodesFlow.TextOptionComponent("Classes Colors"), - ), - NodesFlow.Node.Option( - name="Set Colors", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent(classes_colors) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + + settings_options = [ + NodesFlow.Node.Option( + name="Set Colors", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[classes_colors, classes_colors_save_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="colors_preview", + option_component=NodesFlow.WidgetOptionComponent(classes_colors_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, + id=layer_id, + create_options=create_options, get_settings=get_settings, - get_src=None, meta_changed_cb=meta_changed_cb, - get_dst=None, - set_settings_from_json=set_settings_from_json, - id=layer_id, ) diff --git a/src/ui/dtl/actions/contrast_brightness.py b/src/ui/dtl/actions/contrast_brightness.py index def52f99..d47002ff 100644 --- a/src/ui/dtl/actions/contrast_brightness.py +++ b/src/ui/dtl/actions/contrast_brightness.py @@ -1,24 +1,18 @@ from typing import Optional + from supervisely.app.widgets import NodesFlow -from src.ui.dtl import Action + +from src.ui.dtl import PixelLevelAction from src.ui.dtl.Layer import Layer -class ContrastBrightnessAction(Action): +class ContrastBrightnessAction(PixelLevelAction): name = "contrast_brightness" title = "Contrast / Brightness" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/contrast_brightness" description = ( "This layer (contrast_brightness) randomly changes contrast and brightness of images. " ) - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "contrast": None, - "brightness": None, - } @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -51,72 +45,80 @@ def get_settings(options_json: dict) -> dict: } return settings - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def create_options(src: list, dst: list, settings: dict) -> dict: + contrast_val = False + contrast_min_val = 1 + contrast_max_val = 2 + center_grey_val = False if "contrast" in settings: - node_state["Contrast"] = True - node_state["Contrast Min"] = settings["contrast"]["min"] - node_state["Contrast Max"] = settings["contrast"]["max"] - node_state["Center grey"] = settings["contrast"]["center_grey"] + contrast_val = True + contrast_min_val = settings["contrast"].get("min", 1) + contrast_max_val = settings["contrast"].get("max", 2) + center_grey_val = settings["contrast"].get("center_grey", False) + brightness_val = False + brightness_min_val = -50 + brightness_max_val = 50 if "brightness" in settings: - node_state["Brightness"] = True - node_state["Brightness Min"] = settings["brightness"]["min"] - node_state["Brightness Max"] = settings["brightness"]["max"] - return node_state - - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Settings"), - ), - NodesFlow.Node.Option( - name="Contrast", - option_component=NodesFlow.CheckboxOptionComponent(default_value=True), - ), - NodesFlow.Node.Option( - name="Contrast Min", - option_component=NodesFlow.SliderOptionComponent(min=0, max=10, default_value=1), - ), - NodesFlow.Node.Option( - name="Contrast Max", - option_component=NodesFlow.SliderOptionComponent(min=0, max=10, default_value=2), - ), - NodesFlow.Node.Option( - name="Center grey", - option_component=NodesFlow.CheckboxOptionComponent(default_value=False), - ), - NodesFlow.Node.Option( - name="center_grey_text", - option_component=NodesFlow.TextOptionComponent( - '*To center colors of images (subtract 128) first, set "Center grey" to true' + brightness_val = True + brightness_min_val = settings["brightness"].get("min", -50) + brightness_max_val = settings["brightness"].get("max", 50) + settings_options = [ + NodesFlow.Node.Option( + name="Contrast", + option_component=NodesFlow.CheckboxOptionComponent(default_value=contrast_val), + ), + NodesFlow.Node.Option( + name="Contrast Min", + option_component=NodesFlow.SliderOptionComponent( + min=0, max=10, default_value=contrast_min_val + ), + ), + NodesFlow.Node.Option( + name="Contrast Max", + option_component=NodesFlow.SliderOptionComponent( + min=0, max=10, default_value=contrast_max_val + ), ), - ), - NodesFlow.Node.Option( - name="Brightness", - option_component=NodesFlow.CheckboxOptionComponent(default_value=True), - ), - NodesFlow.Node.Option( - name="Brightness Min", - option_component=NodesFlow.SliderOptionComponent( - min=-255, max=255, default_value=-50 + NodesFlow.Node.Option( + name="Center grey", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=center_grey_val + ), ), - ), - NodesFlow.Node.Option( - name="Brightness Max", - option_component=NodesFlow.SliderOptionComponent( - min=-255, max=255, default_value=50 + NodesFlow.Node.Option( + name="center_grey_text", + option_component=NodesFlow.TextOptionComponent( + '*To center colors of images (subtract 128) first, set "Center grey" to true' + ), ), - ), - ] + NodesFlow.Node.Option( + name="Brightness", + option_component=NodesFlow.CheckboxOptionComponent( + default_value=brightness_val + ), + ), + NodesFlow.Node.Option( + name="Brightness Min", + option_component=NodesFlow.SliderOptionComponent( + min=-255, max=255, default_value=brightness_min_val + ), + ), + NodesFlow.Node.Option( + name="Brightness Max", + option_component=NodesFlow.SliderOptionComponent( + min=-255, max=255, default_value=brightness_max_val + ), + ), + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/crop.py b/src/ui/dtl/actions/crop.py index eaf6f8fd..d273a6c3 100644 --- a/src/ui/dtl/actions/crop.py +++ b/src/ui/dtl/actions/crop.py @@ -1,4 +1,5 @@ from typing import Optional + from supervisely.app.widgets import ( NodesFlow, Select, @@ -9,21 +10,19 @@ Flexbox, OneOf, Checkbox, + Button, + Text, ) -from src.ui.dtl import Action + +from src.ui.dtl import SpatialLevelAction from src.ui.dtl.Layer import Layer -class CropAction(Action): +class CropAction(SpatialLevelAction): name = "crop" title = "Crop" docs_url = "https://docs.supervisely.com/data-manipulation/index/transformation-layers/crop" description = "This layer (crop) is used to crop part of image with its annotations. This layer has several modes: it may crop fixed given part of image or random one." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = {"sides": None, "random_part": None} @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): @@ -142,6 +141,14 @@ def create_new_layer(cls, layer_id: Optional[str] = None): ] ) + mode_preview = Text("") + params_preview = Text("") + settings_preview = Container(widgets=[mode_preview, params_preview], gap=1) + + save_settings_btn = Button("Save", icon="zmdi zmdi-floppy") + + saved_settings = {} + def _set_sides(settings: dict): mode_select.set_value("sides") top_value = settings["sides"]["top"] @@ -212,46 +219,70 @@ def _get_random(): "keep_aspect_ratio": crop_random_keep_aspect_ratio.is_checked(), } - def get_settings(options_json: dict) -> dict: - """This function is used to get settings from options json we get from NodesFlow widget""" - settings = {} - if mode_select.get_value() == "sides": + def _update_preview(): + mode = saved_settings.get("mode", "") + if mode == "": + mode_preview.text = "" + params_preview.text = "" + elif mode == "sides": + sides = _get_sides() + mode_preview.text = "Mode: Sides" + params_preview.text = f"Top: {sides['top']}; Left: {sides['left']}; Right: {sides['right']}; Bottom: {sides['bottom']}" + elif mode == "random_part": + random_part = _get_random() + mode_preview.text = "Mode: Random part" + params_preview.text = f"Height: {random_part['height']['min_percent']} - {random_part['height']['max_percent']}; Width: {random_part['width']['min_percent']} - {random_part['width']['max_percent']}; Keep aspect ratio: {random_part['keep_aspect_ratio']}" + + def _save_settings(): + nonlocal saved_settings + settings = { + "mode": mode_select.get_value(), + } + if settings["mode"] == "sides": settings["sides"] = _get_sides() else: settings["random_part"] = _get_random() - return settings + saved_settings = settings + _update_preview() - def set_settings_from_json(json_data: dict, node_state: dict): - """This function is used to set options from settings we get from dlt json input""" - settings = json_data["settings"] + def get_settings(options_json: dict) -> dict: + """This function is used to get settings from options json we get from NodesFlow widget""" + return saved_settings + + def _set_settings_from_json(settings): if "sides" in settings: _set_sides(settings) - else: + elif "random_part" in settings: _set_random(settings) - return node_state + _save_settings() + + save_settings_btn.click(_save_settings) - options = [ - NodesFlow.Node.Option( - name="settings_text", - option_component=NodesFlow.TextOptionComponent("Crop settings"), - ), - NodesFlow.Node.Option( - name="Set Settings", - option_component=NodesFlow.ButtonOptionComponent( - sidebar_component=NodesFlow.WidgetOptionComponent( - Container(widgets=[mode_select, OneOf(mode_select)]) - ) + def create_options(src: list, dst: list, settings: dict) -> dict: + _set_settings_from_json(settings) + settings_options = [ + NodesFlow.Node.Option( + name="Set Settings", + option_component=NodesFlow.ButtonOptionComponent( + sidebar_component=NodesFlow.WidgetOptionComponent( + Container(widgets=[mode_select, OneOf(mode_select), save_settings_btn]) + ) + ), + ), + NodesFlow.Node.Option( + name="settings_preview", + option_component=NodesFlow.WidgetOptionComponent(settings_preview), ), - ), - ] + ] + return { + "src": [], + "dst": [], + "settings": settings_options, + } return Layer( action=cls, - options=options, - get_settings=get_settings, - get_src=None, - meta_changed_cb=None, - get_dst=None, - set_settings_from_json=set_settings_from_json, id=layer_id, + create_options=create_options, + get_settings=get_settings, ) diff --git a/src/ui/dtl/actions/data.py b/src/ui/dtl/actions/data.py index af9a779e..4e27752a 100644 --- a/src/ui/dtl/actions/data.py +++ b/src/ui/dtl/actions/data.py @@ -1,29 +1,35 @@ -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 -from src.ui.dtl import Action +import src.globals as g +from src.ui.dtl import SourceAction from src.ui.dtl.Layer import Layer -from supervisely.app.content import StateJson -from supervisely.app.widgets import NodesFlow, SelectDataset -from supervisely import ProjectType, ProjectMeta from src.ui.widgets.classes_mapping import ClassesMapping -import src.globals as g +from src.ui.widgets.classes_mapping_preview import ClassesMappingPreview +from src.ui.dtl.utils import ( + get_classes_mapping_value, + classes_mapping_settings_changed_meta, + set_classes_mapping_preview, + get_set_settings_container, + get_set_settings_button_style, + set_classes_mapping_settings_from_json, +) -class DataAction(Action): +class DataAction(SourceAction): name = "data" title = "Data" docs_url = "https://docs.supervisely.com/data-manipulation/index/data-layers/data" description = "Data layer (data) is used to specify project and its datasets that will participate in data transformation process." - # when setting options from settings json, values from _settings_mapping will be mapped to options. - # If there is no option mapped directly to setting, set this option mapping to None and set the option value - # in set_settings_from_json function. If option name is different from setting name - set mapping in - # _settings_mapping below. If option name is the same as setting name - no need to set mapping. - _settings_mapping = { - "classes_mapping": None, - } + md_description_url = ( + "https://raw.githubusercontent.com/supervisely/docs/master/data-manipulation/dtl/data.md" + ) + md_description = requests.get(md_description_url).text @classmethod def create_inputs(self): @@ -31,12 +37,28 @@ def create_inputs(self): @classmethod def create_new_layer(cls, layer_id: Optional[str] = None): + # Src widgets select_datasets = SelectDataset( multiselect=True, select_all_datasets=True, allowed_project_types=[ProjectType.IMAGES], compact=False, ) + select_datasets_btn = Button( + text="SELECT", + icon="zmdi zmdi-folder", + button_type="text", + button_size="small", + emit_on_click="openSidebar", + style=get_set_settings_button_style(), + ) + src_save_btn = Button("Save", icon="zmdi zmdi-floppy") + src_preview_widget = Text("") + src_widgets_container = Container(widgets=[select_datasets, src_save_btn]) + + saved_src = [] + + # fix team and workspace for SelectDataset widget StateJson()[select_datasets._project_selector._ws_selector._team_selector.widget_id][ "teamId" ] = g.TEAM_ID @@ -45,166 +67,247 @@ 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_text = "".join(f"