From c494b5426f71c58443de49def8ec9678c606ae5f Mon Sep 17 00:00:00 2001 From: Aarni Koskela <akx@iki.fi> Date: Wed, 14 Aug 2024 00:03:22 +0300 Subject: [PATCH 1/5] Reorder toolbar semantically --- labelme/app.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index 7bbce4936..ee010b526 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -826,13 +826,14 @@ def __init__( save, deleteFile, None, + undo, + None, createMode, editMode, duplicate, delete, - undo, - brightnessContrast, None, + brightnessContrast, fitWindow, zoom, None, From e46cdc44d972f50e39e63452de1ddb28145f9068 Mon Sep 17 00:00:00 2001 From: Aarni Koskela <akx@iki.fi> Date: Wed, 14 Aug 2024 00:06:59 +0300 Subject: [PATCH 2/5] Move action collections out of `actions` namespace Let's have the `actions` object just hold single actions, not lists of them. --- labelme/app.py | 187 +++++++++++++++++++++++-------------------------- 1 file changed, 86 insertions(+), 101 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index ee010b526..c186dbc02 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -576,15 +576,6 @@ def __init__( self.tr("Adjust brightness and contrast"), enabled=False, ) - # Group zoom controls into a list for easier toggling. - zoomActions = ( - self.zoomWidget, - zoomIn, - zoomOut, - zoomOrg, - fitWindow, - fitWidth, - ) self.zoomMode = self.FIT_WINDOW fitWindow.setChecked(Qt.Checked) self.scalers = { @@ -657,60 +648,78 @@ def __init__( fitWindow=fitWindow, fitWidth=fitWidth, brightnessContrast=brightnessContrast, - zoomActions=zoomActions, openNextImg=openNextImg, openPrevImg=openPrevImg, - fileMenuActions=(open_, opendir, save, saveAs, close, quit), - tool=(), - # XXX: need to add some actions here to activate the shortcut - editMenu=( - edit, - duplicate, - copy, - paste, - delete, - None, - undo, - undoLastPoint, - None, - removePoint, - None, - toggle_keep_prev_mode, - ), - # menu shown at right click - menu=( - createMode, - createRectangleMode, - createCircleMode, - createLineMode, - createPointMode, - createLineStripMode, - createAiPolygonMode, - createAiMaskMode, - editMode, - edit, - duplicate, - copy, - paste, - delete, - undo, - undoLastPoint, - removePoint, - ), - onLoadActive=( - close, - createMode, - createRectangleMode, - createCircleMode, - createLineMode, - createPointMode, - createLineStripMode, - createAiPolygonMode, - createAiMaskMode, - editMode, - brightnessContrast, - ), - onShapesPresent=(saveAs, hideAll, showAll, toggleAll), + ) + self.on_shapes_present_actions = (saveAs, hideAll, showAll, toggleAll) + + self.draw_actions = { + "polygon": createMode, + "rectangle": createRectangleMode, + "circle": createCircleMode, + "point": createPointMode, + "line": createLineMode, + "linestrip": createLineStripMode, + "ai_polygon": createAiPolygonMode, + "ai_mask": createAiMaskMode, + } + + # Group zoom controls into a list for easier toggling. + self.zoom_actions = ( + self.zoomWidget, + zoomIn, + zoomOut, + zoomOrg, + fitWindow, + fitWidth, + ) + self.on_load_active_actions = ( + close, + createMode, + createRectangleMode, + createCircleMode, + createLineMode, + createPointMode, + createLineStripMode, + createAiPolygonMode, + createAiMaskMode, + editMode, + brightnessContrast, + ) + # menu shown at right click + self.context_menu_actions = ( + createMode, + createRectangleMode, + createCircleMode, + createLineMode, + createPointMode, + createLineStripMode, + createAiPolygonMode, + createAiMaskMode, + editMode, + edit, + duplicate, + copy, + paste, + delete, + undo, + undoLastPoint, + removePoint, + ) + # XXX: need to add some actions here to activate the shortcut + self.edit_menu_actions = ( + edit, + duplicate, + copy, + paste, + delete, + None, + undo, + undoLastPoint, + None, + removePoint, + None, + toggle_keep_prev_mode, ) self.canvas.vertexSelected.connect(self.actions.removePoint.setEnabled) @@ -773,7 +782,7 @@ def __init__( self.menus.file.aboutToShow.connect(self.updateFileMenu) # Custom context menu for the canvas widget: - utils.addActions(self.canvas.menus[0], self.actions.menu) + utils.addActions(self.canvas.menus[0], self.context_menu_actions) utils.addActions( self.canvas.menus[1], ( @@ -818,7 +827,7 @@ def __init__( ai_prompt_action.setDefaultWidget(self._ai_prompt_widget) self.tools = self.toolbar("Tools") - self.actions.tool = ( + self.toolbar_actions = ( open_, opendir, openPrevImg, @@ -929,24 +938,17 @@ def noShapes(self): return not len(self.labelList) def populateModeActions(self): - tool, menu = self.actions.tool, self.actions.menu self.tools.clear() - utils.addActions(self.tools, tool) + utils.addActions(self.tools, self.toolbar_actions) self.canvas.menus[0].clear() - utils.addActions(self.canvas.menus[0], menu) + utils.addActions(self.canvas.menus[0], self.context_menu_actions) self.menus.edit.clear() actions = ( - self.actions.createMode, - self.actions.createRectangleMode, - self.actions.createCircleMode, - self.actions.createLineMode, - self.actions.createPointMode, - self.actions.createLineStripMode, - self.actions.createAiPolygonMode, - self.actions.createAiMaskMode, + *self.draw_actions.values(), self.actions.editMode, + *self.edit_menu_actions, ) - utils.addActions(self.menus.edit, actions + self.actions.editMenu) + utils.addActions(self.menus.edit, actions) def setDirty(self): # Even if we autosave the file, we keep the ability to undo @@ -969,14 +971,8 @@ def setDirty(self): def setClean(self): self.dirty = False self.actions.save.setEnabled(False) - self.actions.createMode.setEnabled(True) - self.actions.createRectangleMode.setEnabled(True) - self.actions.createCircleMode.setEnabled(True) - self.actions.createLineMode.setEnabled(True) - self.actions.createPointMode.setEnabled(True) - self.actions.createLineStripMode.setEnabled(True) - self.actions.createAiPolygonMode.setEnabled(True) - self.actions.createAiMaskMode.setEnabled(True) + for action in self.draw_actions.values(): + action.setEnabled(True) title = __appname__ if self.filename is not None: title = "{} - {}".format(title, self.filename) @@ -989,9 +985,9 @@ def setClean(self): def toggleActions(self, value=True): """Enable/Disable widgets which depend on an opened image.""" - for z in self.actions.zoomActions: + for z in self.zoom_actions: z.setEnabled(value) - for action in self.actions.onLoadActive: + for action in self.on_load_active_actions: action.setEnabled(value) def queueEvent(self, function): @@ -1105,24 +1101,13 @@ def toggleDrawingSensitive(self, drawing=True): self.actions.delete.setEnabled(not drawing) def toggleDrawMode(self, edit=True, createMode="polygon"): - draw_actions = { - "polygon": self.actions.createMode, - "rectangle": self.actions.createRectangleMode, - "circle": self.actions.createCircleMode, - "point": self.actions.createPointMode, - "line": self.actions.createLineMode, - "linestrip": self.actions.createLineStripMode, - "ai_polygon": self.actions.createAiPolygonMode, - "ai_mask": self.actions.createAiMaskMode, - } - self.canvas.setEditing(edit) self.canvas.createMode = createMode if edit: - for draw_action in draw_actions.values(): + for draw_action in self.draw_actions.values(): draw_action.setEnabled(True) else: - for draw_mode, draw_action in draw_actions.items(): + for draw_mode, draw_action in self.draw_actions.items(): draw_action.setEnabled(createMode != draw_mode) self.actions.editMode.setEnabled(not edit) @@ -1323,7 +1308,7 @@ def addLabel(self, shape): rgb = self._get_rgb_by_label(shape.label) self.uniqLabelList.setItemLabel(item, shape.label, rgb) self.labelDialog.addLabelHistory(shape.label) - for action in self.actions.onShapesPresent: + for action in self.on_shapes_present_actions: action.setEnabled(True) self._update_shape_color(shape) @@ -2109,7 +2094,7 @@ def removeSelectedPoint(self): self.canvas.deleteShape(self.canvas.hShape) self.remLabels([self.canvas.hShape]) if self.noShapes(): - for action in self.actions.onShapesPresent: + for action in self.on_shapes_present_actions: action.setEnabled(False) self.setDirty() @@ -2124,7 +2109,7 @@ def deleteSelectedShape(self): self.remLabels(self.canvas.deleteSelected()) self.setDirty() if self.noShapes(): - for action in self.actions.onShapesPresent: + for action in self.on_shapes_present_actions: action.setEnabled(False) def copyShape(self): From 1eecec1dae2f78528710d624e0891a937a95b644 Mon Sep 17 00:00:00 2001 From: Aarni Koskela <akx@iki.fi> Date: Wed, 14 Aug 2024 00:22:39 +0300 Subject: [PATCH 3/5] Show all draw modes in toolbar --- labelme/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/labelme/app.py b/labelme/app.py index c186dbc02..4d41808eb 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -837,7 +837,8 @@ def __init__( None, undo, None, - createMode, + *self.draw_actions.values(), + None, editMode, duplicate, delete, From 9e9f849ac9ce350e6a753cb8ecf2345e454b4e53 Mon Sep 17 00:00:00 2001 From: Aarni Koskela <akx@iki.fi> Date: Wed, 14 Aug 2024 00:41:30 +0300 Subject: [PATCH 4/5] Improve dock feature configuration * Don't leak `features` in loop * Don't crash if configuration is missing --- labelme/app.py | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index 4d41808eb..26b5cd8ad 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -191,17 +191,10 @@ def __init__( self.setCentralWidget(scrollArea) - features = QtWidgets.QDockWidget.DockWidgetFeatures() - for dock in ["flag_dock", "label_dock", "shape_dock", "file_dock"]: - if self._config[dock]["closable"]: - features = features | QtWidgets.QDockWidget.DockWidgetClosable - if self._config[dock]["floatable"]: - features = features | QtWidgets.QDockWidget.DockWidgetFloatable - if self._config[dock]["movable"]: - features = features | QtWidgets.QDockWidget.DockWidgetMovable - getattr(self, dock).setFeatures(features) - if self._config[dock]["show"] is False: - getattr(self, dock).setVisible(False) + self._configure_dock("flag_dock", self.flag_dock) + self._configure_dock("label_dock", self.label_dock) + self._configure_dock("shape_dock", self.shape_dock) + self._configure_dock("file_dock", self.file_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.flag_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock) @@ -917,6 +910,24 @@ def __init__( # if self.firstStart: # QWhatsThis.enterWhatsThisMode() + def _configure_dock(self, dock_name: str, dock_obj: QtWidgets.QDockWidget) -> None: + dock_config = self._config.get(dock_name, None) + if dock_config is None: + # Missing configuration? Never mind then... + return + + features = QtWidgets.QDockWidget.DockWidgetFeatures() + + if dock_config.get("closable"): + features = features | QtWidgets.QDockWidget.DockWidgetClosable + if dock_config.get("floatable"): + features = features | QtWidgets.QDockWidget.DockWidgetFloatable + if dock_config.get("movable"): + features = features | QtWidgets.QDockWidget.DockWidgetMovable + dock_obj.setFeatures(features) + if dock_config.get("show") is False: + dock_obj.setVisible(False) + def menu(self, title, actions=None): menu = self.menuBar().addMenu(title) if actions: From b70c54e4ac459865250d1534866d035d2c7a05af Mon Sep 17 00:00:00 2001 From: Aarni Koskela <akx@iki.fi> Date: Wed, 14 Aug 2024 00:32:12 +0300 Subject: [PATCH 5/5] Move AI configuration to dock --- labelme/app.py | 96 +++++++++++++----------------- labelme/config/default_config.yaml | 5 ++ 2 files changed, 48 insertions(+), 53 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index 26b5cd8ad..13f5a627e 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -161,6 +161,10 @@ def __init__( fileListWidget.setLayout(fileListLayout) self.file_dock.setWidget(fileListWidget) + self.ai_dock = QtWidgets.QDockWidget(self.tr("AI"), self) + self.ai_dock.setWidget(self.configure_ai_dock()) + self.ai_dock.setObjectName("AI") + self.zoomWidget = ZoomWidget() self.setAcceptDrops(True) @@ -200,6 +204,7 @@ def __init__( self.addDockWidget(Qt.RightDockWidgetArea, self.label_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.shape_dock) self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock) + self.addDockWidget(Qt.RightDockWidgetArea, self.ai_dock) # Actions action = functools.partial(utils.newAction, self) @@ -367,13 +372,7 @@ def __init__( self.tr("Start drawing ai_polygon. Ctrl+LeftClick ends creation."), enabled=False, ) - createAiPolygonMode.changed.connect( - lambda: self.canvas.initializeAiModel( - name=self._selectAiModelComboBox.currentText() - ) - if self.canvas.createMode == "ai_polygon" - else None - ) + createAiPolygonMode.changed.connect(self._initialize_ai_model_if_needed) createAiMaskMode = action( self.tr("Create AI-Mask"), lambda: self.toggleDrawMode(False, createMode="ai_mask"), @@ -382,13 +381,7 @@ def __init__( self.tr("Start drawing ai_mask. Ctrl+LeftClick ends creation."), enabled=False, ) - createAiMaskMode.changed.connect( - lambda: self.canvas.initializeAiModel( - name=self._selectAiModelComboBox.currentText() - ) - if self.canvas.createMode == "ai_mask" - else None - ) + createAiMaskMode.changed.connect(self._initialize_ai_model_if_needed) editMode = action( self.tr("Edit Polygons"), self.setEditMode, @@ -784,41 +777,6 @@ def __init__( ), ) - selectAiModel = QtWidgets.QWidgetAction(self) - selectAiModel.setDefaultWidget(QtWidgets.QWidget()) - selectAiModel.defaultWidget().setLayout(QtWidgets.QVBoxLayout()) - # - selectAiModelLabel = QtWidgets.QLabel(self.tr("AI Mask Model")) - selectAiModelLabel.setAlignment(QtCore.Qt.AlignCenter) - selectAiModel.defaultWidget().layout().addWidget(selectAiModelLabel) - # - self._selectAiModelComboBox = QtWidgets.QComboBox() - selectAiModel.defaultWidget().layout().addWidget(self._selectAiModelComboBox) - model_names = [model.name for model in MODELS] - self._selectAiModelComboBox.addItems(model_names) - if self._config["ai"]["default"] in model_names: - model_index = model_names.index(self._config["ai"]["default"]) - else: - logger.warning( - "Default AI model is not found: %r", - self._config["ai"]["default"], - ) - model_index = 0 - self._selectAiModelComboBox.setCurrentIndex(model_index) - self._selectAiModelComboBox.currentIndexChanged.connect( - lambda: self.canvas.initializeAiModel( - name=self._selectAiModelComboBox.currentText() - ) - if self.canvas.createMode in ["ai_polygon", "ai_mask"] - else None - ) - - self._ai_prompt_widget: QtWidgets.QWidget = AiPromptWidget( - on_submit=self._submit_ai_prompt, parent=self - ) - ai_prompt_action = QtWidgets.QWidgetAction(self) - ai_prompt_action.setDefaultWidget(self._ai_prompt_widget) - self.tools = self.toolbar("Tools") self.toolbar_actions = ( open_, @@ -839,10 +797,6 @@ def __init__( brightnessContrast, fitWindow, zoom, - None, - selectAiModel, - None, - ai_prompt_action, ) self.statusBar().showMessage(str(self.tr("%s started.")) % __appname__) @@ -928,6 +882,42 @@ def _configure_dock(self, dock_name: str, dock_obj: QtWidgets.QDockWidget) -> No if dock_config.get("show") is False: dock_obj.setVisible(False) + def configure_ai_dock(self) -> QtWidgets.QWidget: + select_ai_model_combo_box = QtWidgets.QComboBox(self) + model_names = [model.name for model in MODELS] + select_ai_model_combo_box.addItems(model_names) + default_ai_model = self._config["ai"]["default"] + if default_ai_model in model_names: + model_index = model_names.index(default_ai_model) + else: + logger.warning( + "Default AI model is not found: %r", + default_ai_model, + ) + model_index = 0 + select_ai_model_combo_box.setCurrentIndex(model_index) + select_ai_model_combo_box.currentIndexChanged.connect( + self._initialize_ai_model_if_needed, + ) + self._selectAiModelComboBox = select_ai_model_combo_box + self._ai_prompt_widget: QtWidgets.QWidget = AiPromptWidget( + on_submit=self._submit_ai_prompt, parent=self + ) + + ai_dock_layout = QtWidgets.QVBoxLayout() + ai_dock_widget = QtWidgets.QWidget() + ai_dock_widget.setLayout(ai_dock_layout) + ai_dock_layout.addWidget(QtWidgets.QLabel(self.tr("AI Mask Model"))) + ai_dock_layout.addWidget(select_ai_model_combo_box) + ai_dock_layout.addWidget(self._ai_prompt_widget) + return ai_dock_widget + + def _initialize_ai_model_if_needed(self) -> None: + if self.canvas.createMode in ("ai_polygon", "ai_mask"): + self.canvas.initializeAiModel( + name=self._selectAiModelComboBox.currentText() + ) + def menu(self, title, actions=None): menu = self.menuBar().addMenu(title) if actions: diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index 128dc6d6a..cee853c78 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -54,6 +54,11 @@ file_dock: closable: true movable: true floatable: true +ai_dock: + show: true + closable: true + movable: true + floatable: true # label_dialog show_label_text_field: true