From efb226b119b884de38909e96a192d95276646e51 Mon Sep 17 00:00:00 2001 From: Artemy Gladkov Date: Thu, 29 Aug 2024 14:42:25 +0300 Subject: [PATCH 1/8] fixing start --- .gitignore | 2 ++ README.md | 24 ++++++++++++++++++++++++ requirements.txt | 13 +++++++++++++ start.py | 4 ++++ 4 files changed, 43 insertions(+) create mode 100644 requirements.txt create mode 100644 start.py diff --git a/.gitignore b/.gitignore index b2ddc7143..90a9899d8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ .DS_Store .idea/ + +/venv/* diff --git a/README.md b/README.md index 3803af29a..d09e8228e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ +

+ Гайд для установки +

+Разрабы не смогли нормально написать что необходимо для запуска программы и какие зависимости необходимо подгрузить, так что придётся это делать мне. + +Для начала нужен питон с официального сайта (у меня работает на 3.11.9). + +Далее рекомендую создать vevn: +``` +python -m venv venv + +.\venv\Scripts\activate +``` + +Далее устанавливаем зависимости: +``` +pip install requirements.txt + +pip install requirements-dev.txt +``` + +Для запуска приложения нужно запустить файл start.py. + +


labelme

diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 000000000..801df0784 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,13 @@ +gdown +imgviz>=1.7.5 +matplotlib +natsort>=7.1.0 +numpy +onnxruntime>=1.14.1,!=1.16.0 +osam>=0.2.2 +Pillow>=2.8 +PyYAML +qtpy!=1.11.2 +pyqt5 +scikit-image +termcolor \ No newline at end of file diff --git a/start.py b/start.py new file mode 100644 index 000000000..8ba6f9298 --- /dev/null +++ b/start.py @@ -0,0 +1,4 @@ +from labelme.__main__ import main + +if __name__ == "__main__": + main() From bbd31e5f20b7f0c99dcfc33adf5d1644b288a02f Mon Sep 17 00:00:00 2001 From: Artemy Gladkov Date: Thu, 29 Aug 2024 15:12:19 +0300 Subject: [PATCH 2/8] updated readme --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 58a0395fa..143f8f704 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,13 @@ Для начала нужен питон с официального сайта (у меня работает на 3.11.9). +Клонируем репозиторий: +``` +git clone https://github.com/DemidNeuroLab/NeuroLabel + +cd ./NeuroLabel +``` + Далее рекомендую создать vevn: ``` python -m venv venv @@ -14,13 +21,17 @@ python -m venv venv Далее устанавливаем зависимости: ``` -pip install requirements.txt +pip install -r requirements.txt -pip install requirements-dev.txt +pip install -r requirements-dev.txt ``` Для запуска приложения нужно запустить файл start.py. +``` +python start.py +``` +# Далее ридми от разрабов:


labelme From 19f9e9bc304f77354358ec52635dc64b97091163 Mon Sep 17 00:00:00 2001 From: Artemy Gladkov <52081678+Nasochec@users.noreply.github.com> Date: Sun, 13 Oct 2024 12:57:45 +0300 Subject: [PATCH 3/8] MARK-8. Removed the Save With Image Data function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * MARK-8. Removed the Save With Image Data function * Удаление функции enableSaveImageWithData и поля ImageData в json * Удалил остатки imageData и пофиксил табуляции --------- Co-authored-by: Ro0ngo --- labelme/app.py | 20 ++------------------ labelme/config/default_config.yaml | 1 - labelme/label_file.py | 20 ++++---------------- 3 files changed, 6 insertions(+), 35 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index 7bbce4936..39f4774d9 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -36,7 +36,7 @@ from labelme.widgets import UniqueLabelQListWidget from labelme.widgets import ZoomWidget -from . import utils +from labelme import utils # FIXME # - [medium] Set max zoom value to something big enough for FitWidth/Window @@ -292,14 +292,6 @@ def __init__( ) saveAuto.setChecked(self._config["auto_save"]) - saveWithImageData = action( - text=self.tr("Save With Image Data"), - slot=self.enableSaveImageWithData, - tip=self.tr("Save image data in label file"), - checkable=True, - checked=self._config["store_data"], - ) - close = action( self.tr("&Close"), self.closeFile, @@ -624,7 +616,6 @@ def __init__( # Store actions for further handling. self.actions = utils.struct( saveAuto=saveAuto, - saveWithImageData=saveWithImageData, changeOutputDir=changeOutputDir, save=save, saveAs=saveAs, @@ -736,7 +727,6 @@ def __init__( saveAs, saveAuto, changeOutputDir, - saveWithImageData, close, deleteFile, None, @@ -1451,14 +1441,12 @@ def format_shape(s): flags[key] = flag try: imagePath = osp.relpath(self.imagePath, osp.dirname(filename)) - imageData = self.imageData if self._config["store_data"] else None if osp.dirname(filename) and not osp.exists(osp.dirname(filename)): os.makedirs(osp.dirname(filename)) lf.save( filename=filename, shapes=shapes, imagePath=imagePath, - imageData=imageData, imageHeight=self.image.height(), imageWidth=self.image.width(), otherData=self.otherData, @@ -1815,10 +1803,6 @@ def scaleFitWidth(self): w = self.centralWidget().width() - 2.0 return w / self.canvas.pixmap.width() - def enableSaveImageWithData(self, enabled): - self._config["store_data"] = enabled - self.actions.saveWithImageData.setChecked(enabled) - def closeEvent(self, event): if not self.mayContinue(): event.ignore() @@ -2118,7 +2102,7 @@ def deleteSelectedShape(self): "You are about to permanently delete {} polygons, " "proceed anyway?" ).format(len(self.canvas.selectedShapes)) if yes == QtWidgets.QMessageBox.warning( - self, self.tr("Attention"), msg, yes | no, yes + self, self.tr("Attention"), msg, yes | no, yes ): self.remLabels(self.canvas.deleteSelected()) self.setDirty() diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index 128dc6d6a..f41039707 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -1,6 +1,5 @@ auto_save: false display_label_popup: true -store_data: true keep_prev: false keep_prev_scale: false keep_prev_brightness: false diff --git a/labelme/label_file.py b/labelme/label_file.py index 3c1f31530..262947550 100644 --- a/labelme/label_file.py +++ b/labelme/label_file.py @@ -68,7 +68,6 @@ def load_image_file(filename): def load(self, filename): keys = [ "version", - "imageData", "imagePath", "shapes", # polygonal annotations "flags", # image level flags @@ -88,14 +87,10 @@ def load(self, filename): with open(filename, "r") as f: data = json.load(f) - if data["imageData"] is not None: - imageData = base64.b64decode(data["imageData"]) - if PY2 and QT4: - imageData = utils.img_data_to_png_data(imageData) - else: - # relative path from label file to relative path from cwd - imagePath = osp.join(osp.dirname(filename), data["imagePath"]) - imageData = self.load_image_file(imagePath) + + imagePath = osp.join(osp.dirname(filename), data["imagePath"]) + imageData = self.load_image_file(imagePath) + flags = data.get("flags") or {} imagePath = data["imagePath"] self._check_image_height_and_width( @@ -158,15 +153,9 @@ def save( imagePath, imageHeight, imageWidth, - imageData=None, otherData=None, flags=None, ): - if imageData is not None: - imageData = base64.b64encode(imageData).decode("utf-8") - imageHeight, imageWidth = self._check_image_height_and_width( - imageData, imageHeight, imageWidth - ) if otherData is None: otherData = {} if flags is None: @@ -176,7 +165,6 @@ def save( flags=flags, shapes=shapes, imagePath=imagePath, - imageData=imageData, imageHeight=imageHeight, imageWidth=imageWidth, ) From 14a3e2eabb8410aee57a2ca16526e8970fbb6c59 Mon Sep 17 00:00:00 2001 From: Delone2002 <94550855+Delone2002@users.noreply.github.com> Date: Sun, 13 Oct 2024 13:33:15 +0300 Subject: [PATCH 4/8] =?UTF-8?q?MARK-9=20=D0=A0=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20Edit?= =?UTF-8?q?=20Menu=20(#4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Mark-9 Удалил CreateCircle из меню Edit * MARK-9 Убрал всё лишнее из меню Edit и поставил autosave в программе --------- Co-authored-by: Leonid Bystrov --- labelme/app.py | 184 +---------------------------- labelme/config/default_config.yaml | 2 +- labelme/shape.py | 46 +------- labelme/widgets/canvas.py | 141 ++-------------------- 4 files changed, 21 insertions(+), 352 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index 39f4774d9..ee5cc5bdd 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -96,8 +96,6 @@ def __init__( self._noSelectionSlot = False - self._copied_shapes = None - # Main widgets and related state. self.labelDialog = LabelDialog( parent=self, @@ -300,24 +298,6 @@ def __init__( self.tr("Close current file"), ) - toggle_keep_prev_mode = action( - self.tr("Keep Previous Annotation"), - self.toggleKeepPrevMode, - shortcuts["toggle_keep_prev_mode"], - None, - self.tr('Toggle "keep previous annotation" mode'), - checkable=True, - ) - toggle_keep_prev_mode.setChecked(self._config["keep_prev"]) - - createMode = action( - self.tr("Create Polygons"), - lambda: self.toggleDrawMode(False, createMode="polygon"), - shortcuts["create_polygon"], - "objects", - self.tr("Start drawing polygons"), - enabled=False, - ) createRectangleMode = action( self.tr("Create Rectangle"), lambda: self.toggleDrawMode(False, createMode="rectangle"), @@ -326,38 +306,6 @@ def __init__( self.tr("Start drawing rectangles"), enabled=False, ) - createCircleMode = action( - self.tr("Create Circle"), - lambda: self.toggleDrawMode(False, createMode="circle"), - shortcuts["create_circle"], - "objects", - self.tr("Start drawing circles"), - enabled=False, - ) - createLineMode = action( - self.tr("Create Line"), - lambda: self.toggleDrawMode(False, createMode="line"), - shortcuts["create_line"], - "objects", - self.tr("Start drawing lines"), - enabled=False, - ) - createPointMode = action( - self.tr("Create Point"), - lambda: self.toggleDrawMode(False, createMode="point"), - shortcuts["create_point"], - "objects", - self.tr("Start drawing points"), - enabled=False, - ) - createLineStripMode = action( - self.tr("Create LineStrip"), - lambda: self.toggleDrawMode(False, createMode="linestrip"), - shortcuts["create_linestrip"], - "objects", - self.tr("Start drawing linestrip. Ctrl+LeftClick ends creation."), - enabled=False, - ) createAiPolygonMode = action( self.tr("Create AI-Polygon"), lambda: self.toggleDrawMode(False, createMode="ai_polygon"), @@ -373,21 +321,6 @@ def __init__( if self.canvas.createMode == "ai_polygon" else None ) - createAiMaskMode = action( - self.tr("Create AI-Mask"), - lambda: self.toggleDrawMode(False, createMode="ai_mask"), - None, - "objects", - 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 - ) editMode = action( self.tr("Edit Polygons"), self.setEditMode, @@ -405,30 +338,6 @@ def __init__( self.tr("Delete the selected polygons"), enabled=False, ) - duplicate = action( - self.tr("Duplicate Polygons"), - self.duplicateSelectedShape, - shortcuts["duplicate_polygon"], - "copy", - self.tr("Create a duplicate of the selected polygons"), - enabled=False, - ) - copy = action( - self.tr("Copy Polygons"), - self.copySelectedShape, - shortcuts["copy_polygon"], - "copy_clipboard", - self.tr("Copy selected polygons to clipboard"), - enabled=False, - ) - paste = action( - self.tr("Paste Polygons"), - self.pasteSelectedShape, - shortcuts["paste_polygon"], - "paste", - self.tr("Paste copied polygons"), - enabled=False, - ) undoLastPoint = action( self.tr("Undo last point"), self.canvas.undoLastPoint, @@ -622,24 +531,14 @@ def __init__( open=open_, close=close, deleteFile=deleteFile, - toggleKeepPrevMode=toggle_keep_prev_mode, delete=delete, edit=edit, - duplicate=duplicate, - copy=copy, - paste=paste, undoLastPoint=undoLastPoint, undo=undo, removePoint=removePoint, - createMode=createMode, editMode=editMode, createRectangleMode=createRectangleMode, - createCircleMode=createCircleMode, - createLineMode=createLineMode, - createPointMode=createPointMode, - createLineStripMode=createLineStripMode, createAiPolygonMode=createAiPolygonMode, - createAiMaskMode=createAiMaskMode, zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, @@ -656,9 +555,6 @@ def __init__( # XXX: need to add some actions here to activate the shortcut editMenu=( edit, - duplicate, - copy, - paste, delete, None, undo, @@ -666,23 +562,13 @@ def __init__( 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, @@ -690,14 +576,8 @@ def __init__( ), onLoadActive=( close, - createMode, createRectangleMode, - createCircleMode, - createLineMode, - createPointMode, - createLineStripMode, - createAiPolygonMode, - createAiMaskMode, + # createAiPolygonMode, editMode, brightnessContrast, ), @@ -797,7 +677,7 @@ def __init__( lambda: self.canvas.initializeAiModel( name=self._selectAiModelComboBox.currentText() ) - if self.canvas.createMode in ["ai_polygon", "ai_mask"] + if self.canvas.createMode in ["ai_polygon"] else None ) @@ -816,9 +696,7 @@ def __init__( save, deleteFile, None, - createMode, editMode, - duplicate, delete, undo, brightnessContrast, @@ -925,14 +803,8 @@ def populateModeActions(self): utils.addActions(self.canvas.menus[0], menu) 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.actions.editMode, ) utils.addActions(self.menus.edit, actions + self.actions.editMenu) @@ -958,14 +830,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) + self.actions.createAiPolygonMode.setEnabled(False) title = __appname__ if self.filename is not None: title = "{} - {}".format(title, self.filename) @@ -1093,16 +959,10 @@ def toggleDrawingSensitive(self, drawing=True): self.actions.undo.setEnabled(not drawing) self.actions.delete.setEnabled(not drawing) - def toggleDrawMode(self, edit=True, createMode="polygon"): + def toggleDrawMode(self, edit=True, createMode="rectangle"): 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) @@ -1295,8 +1155,6 @@ def shapeSelectionChanged(self, selected_shapes): self._noSelectionSlot = False n_selected = len(selected_shapes) self.actions.delete.setEnabled(n_selected) - self.actions.duplicate.setEnabled(n_selected) - self.actions.copy.setEnabled(n_selected) self.actions.edit.setEnabled(n_selected) def addLabel(self, shape): @@ -1467,20 +1325,6 @@ def format_shape(s): ) return False - def duplicateSelectedShape(self): - added_shapes = self.canvas.duplicateSelectedShapes() - for shape in added_shapes: - self.addLabel(shape) - self.setDirty() - - def pasteSelectedShape(self): - self.loadShapes(self._copied_shapes, replace=False) - self.setDirty() - - def copySelectedShape(self): - self._copied_shapes = [s.copy() for s in self.canvas.selectedShapes] - self.actions.paste.setEnabled(len(self._copied_shapes) > 0) - def labelSelectionChanged(self): if self._noSelectionSlot: return @@ -1706,8 +1550,6 @@ def loadFile(self, filename=None): return False self.image = image self.filename = filename - if self._config["keep_prev"]: - prev_shapes = self.canvas.shapes self.canvas.loadPixmap(QtGui.QPixmap.fromImage(image)) flags = {k: False for k in self._config["flags"] or []} if self.labelFile: @@ -1715,11 +1557,7 @@ def loadFile(self, filename=None): if self.labelFile.flags is not None: flags.update(self.labelFile.flags) self.loadFlags(flags) - if self._config["keep_prev"] and self.noShapes(): - self.loadShapes(prev_shapes, replace=False) - self.setDirty() - else: - self.setClean() + self.setClean() self.canvas.setEnabled(True) # set zoom values is_initial_load = not self.zoom_values @@ -1861,14 +1699,7 @@ def openPrevImg(self, _value=False): if filename: self.loadFile(filename) - self._config["keep_prev"] = keep_prev - def openNextImg(self, _value=False, load=True): - keep_prev = self._config["keep_prev"] - if QtWidgets.QApplication.keyboardModifiers() == ( - Qt.ControlModifier | Qt.ShiftModifier - ): - self._config["keep_prev"] = True if not self.mayContinue(): return @@ -1890,8 +1721,6 @@ def openNextImg(self, _value=False, load=True): if self.filename and load: self.loadFile(self.filename) - self._config["keep_prev"] = keep_prev - def openFile(self, _value=False): if not self.mayContinue(): return @@ -2082,9 +1911,6 @@ def errorMessage(self, title, message): def currentPath(self): return osp.dirname(str(self.filename)) if self.filename else "." - def toggleKeepPrevMode(self): - self._config["keep_prev"] = not self._config["keep_prev"] - def removeSelectedPoint(self): self.canvas.removeSelectedPoint() self.canvas.update() diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index f41039707..6c0c6fb0a 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -1,4 +1,4 @@ -auto_save: false +auto_save: true display_label_popup: true keep_prev: false keep_prev_scale: false diff --git a/labelme/shape.py b/labelme/shape.py index 0f1fd9fdb..046b5fc53 100644 --- a/labelme/shape.py +++ b/labelme/shape.py @@ -101,15 +101,9 @@ def shape_type(self): @shape_type.setter def shape_type(self, value): if value is None: - value = "polygon" + value = "rectangle" if value not in [ - "polygon", "rectangle", - "point", - "line", - "circle", - "linestrip", - "points", "mask", ]: raise ValueError("Unexpected shape_type: {}".format(value)) @@ -126,7 +120,7 @@ def addPoint(self, point, label=1): self.point_labels.append(label) def canAddPoint(self): - return self.shape_type in ["polygon", "linestrip"] + return self.shape_type in ["polygon"] def popPoint(self): if self.points: @@ -155,14 +149,6 @@ def removePoint(self, i): ) return - if self.shape_type == "linestrip" and len(self.points) <= 2: - logger.warning( - "Cannot remove point from: shape_type=%r, len(points)=%d", - self.shape_type, - len(self.points), - ) - return - self.points.pop(i) self.point_labels.pop(i) @@ -228,29 +214,6 @@ def paint(self, painter): if self.shape_type == "rectangle": for i in range(len(self.points)): self.drawVertex(vrtx_path, i) - elif self.shape_type == "circle": - assert len(self.points) in [1, 2] - if len(self.points) == 2: - raidus = labelme.utils.distance( - self._scale_point(self.points[0] - self.points[1]) - ) - line_path.addEllipse( - self._scale_point(self.points[0]), raidus, raidus - ) - for i in range(len(self.points)): - self.drawVertex(vrtx_path, i) - elif self.shape_type == "linestrip": - line_path.moveTo(self._scale_point(self.points[0])) - for i, p in enumerate(self.points): - line_path.lineTo(self._scale_point(p)) - self.drawVertex(vrtx_path, i) - elif self.shape_type == "points": - assert len(self.points) == len(self.point_labels) - for i, point_label in enumerate(self.point_labels): - if point_label == 1: - self.drawVertex(vrtx_path, i) - else: - self.drawVertex(negative_vrtx_path, i) else: line_path.moveTo(self._scale_point(self.points[0])) # Uncommenting the following line will draw 2 paths @@ -343,11 +306,6 @@ def makePath(self): path = QtGui.QPainterPath() if len(self.points) == 2: path.addRect(QtCore.QRectF(self.points[0], self.points[1])) - elif self.shape_type == "circle": - path = QtGui.QPainterPath() - if len(self.points) == 2: - raidus = labelme.utils.distance(self.points[0] - self.points[1]) - path.addEllipse(self.points[0], raidus, raidus) else: path = QtGui.QPainterPath(self.points[0]) for p in self.points[1:]: diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py index a78f073d3..f9239a2ce 100644 --- a/labelme/widgets/canvas.py +++ b/labelme/widgets/canvas.py @@ -35,7 +35,7 @@ class Canvas(QtWidgets.QWidget): CREATE, EDIT = 0, 1 # polygon, rectangle, line, or point - _createMode = "polygon" + _createMode = "rectangle" _fill_drawing = False @@ -50,14 +50,8 @@ def __init__(self, *args, **kwargs): self._crosshair = kwargs.pop( "crosshair", { - "polygon": False, "rectangle": True, - "circle": False, - "line": False, - "point": False, - "linestrip": False, "ai_polygon": False, - "ai_mask": False, }, ) super(Canvas, self).__init__(*args, **kwargs) @@ -116,14 +110,8 @@ def createMode(self): @createMode.setter def createMode(self, value): if value not in [ - "polygon", "rectangle", - "circle", - "line", - "point", - "linestrip", "ai_polygon", - "ai_mask", ]: raise ValueError("Unsupported createMode: %s" % value) self._createMode = value @@ -244,7 +232,7 @@ def mouseMoveEvent(self, ev): # Polygon drawing. if self.drawing(): - if self.createMode in ["ai_polygon", "ai_mask"]: + if self.createMode in ["ai_polygon"]: self.line.shape_type = "points" else: self.line.shape_type = self.createMode @@ -258,21 +246,7 @@ def mouseMoveEvent(self, ev): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) - elif ( - self.snapping - and len(self.current) > 1 - and self.createMode == "polygon" - and self.closeEnough(pos, self.current[0]) - ): - # Attract line to starting point and - # colorise to alert the user. - pos = self.current[0] - self.overrideCursor(CURSOR_POINT) - self.current.highlightVertex(0, Shape.NEAR_VERTEX) - if self.createMode in ["polygon", "linestrip"]: - self.line.points = [self.current[-1], pos] - self.line.point_labels = [1, 1] - elif self.createMode in ["ai_polygon", "ai_mask"]: + if self.createMode in ["ai_polygon"]: self.line.points = [self.current.points[-1], pos] self.line.point_labels = [ self.current.point_labels[-1], @@ -282,18 +256,6 @@ def mouseMoveEvent(self, ev): self.line.points = [self.current[0], pos] self.line.point_labels = [1, 1] self.line.close() - elif self.createMode == "circle": - self.line.points = [self.current[0], pos] - self.line.point_labels = [1, 1] - self.line.shape_type = "circle" - elif self.createMode == "line": - self.line.points = [self.current[0], pos] - self.line.point_labels = [1, 1] - self.line.close() - elif self.createMode == "point": - self.line.points = [self.current[0]] - self.line.point_labels = [1] - self.line.close() assert len(self.line.points) == len(self.line.point_labels) self.repaint() self.current.highlightClear() @@ -413,21 +375,11 @@ def mousePressEvent(self, ev): if self.drawing(): if self.current: # Add point to existing shape. - if self.createMode == "polygon": - self.current.addPoint(self.line[1]) - self.line[0] = self.current[-1] - if self.current.isClosed(): - self.finalise() - elif self.createMode in ["rectangle", "circle", "line"]: + if self.createMode in ["rectangle"]: assert len(self.current.points) == 1 self.current.points = self.line.points self.finalise() - elif self.createMode == "linestrip": - self.current.addPoint(self.line[1]) - self.line[0] = self.current[-1] - if int(ev.modifiers()) == QtCore.Qt.ControlModifier: - self.finalise() - elif self.createMode in ["ai_polygon", "ai_mask"]: + elif self.createMode in ["ai_polygon"]: self.current.addPoint( self.line.points[1], label=self.line.point_labels[1], @@ -440,23 +392,19 @@ def mousePressEvent(self, ev): # Create new shape. self.current = Shape( shape_type="points" - if self.createMode in ["ai_polygon", "ai_mask"] + if self.createMode in ["ai_polygon"] else self.createMode ) self.current.addPoint(pos, label=0 if is_shift_pressed else 1) - if self.createMode == "point": - self.finalise() - elif ( - self.createMode in ["ai_polygon", "ai_mask"] + if ( + self.createMode in ["ai_polygon"] and ev.modifiers() & QtCore.Qt.ControlModifier ): self.finalise() else: - if self.createMode == "circle": - self.current.shape_type = "circle" self.line.points = [pos, pos] if ( - self.createMode in ["ai_polygon", "ai_mask"] + self.createMode in ["ai_polygon"] and is_shift_pressed ): self.line.point_labels = [0, 0] @@ -545,16 +493,14 @@ def setHiding(self, enable=True): def canCloseShape(self): return self.drawing() and ( (self.current and len(self.current) > 2) - or self.createMode in ["ai_polygon", "ai_mask"] + or self.createMode in ["ai_polygon"] ) def mouseDoubleClickEvent(self, ev): if self.double_click != "close": return - if ( - self.createMode == "polygon" and self.canCloseShape() - ) or self.createMode in ["ai_polygon", "ai_mask"]: + if self.createMode in ["ai_polygon"]: self.finalise() def selectShapes(self, shapes): @@ -663,13 +609,6 @@ def deleteShape(self, shape): self.storeShapes() self.update() - def duplicateSelectedShapes(self): - if self.selectedShapes: - self.selectedShapesCopy = [s.copy() for s in self.selectedShapes] - self.boundedShiftShapes(self.selectedShapesCopy) - self.endMove(copy=True) - return self.selectedShapes - def boundedShiftShapes(self, shapes): # Try to move in one direction, and if it fails in another. # Give up if both fail. @@ -731,23 +670,7 @@ def paintEvent(self, event): for s in self.selectedShapesCopy: s.paint(p) - if ( - self.fillDrawing() - and self.createMode == "polygon" - and self.current is not None - and len(self.current.points) >= 2 - ): - drawing_shape = self.current.copy() - if drawing_shape.fill_color.getRgb()[3] == 0: - logger.warning( - "fill_drawing=true, but fill_color is transparent," - " so forcing to be opaque." - ) - drawing_shape.fill_color.setAlpha(64) - drawing_shape.addPoint(self.line[1]) - drawing_shape.fill = True - drawing_shape.paint(p) - elif self.createMode == "ai_polygon" and self.current is not None: + if self.createMode == "ai_polygon" and self.current is not None: drawing_shape = self.current.copy() drawing_shape.addPoint( point=self.line.points[1], @@ -766,26 +689,6 @@ def paintEvent(self, event): drawing_shape.fill = self.fillDrawing() drawing_shape.selected = True drawing_shape.paint(p) - elif self.createMode == "ai_mask" and self.current is not None: - drawing_shape = self.current.copy() - drawing_shape.addPoint( - point=self.line.points[1], - label=self.line.point_labels[1], - ) - mask = self._ai_model.predict_mask_from_points( - points=[[point.x(), point.y()] for point in drawing_shape.points], - point_labels=drawing_shape.point_labels, - ) - y1, x1, y2, x2 = imgviz.instances.masks_to_bboxes([mask])[0].astype(int) - drawing_shape.setShapeRefined( - shape_type="mask", - points=[QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)], - point_labels=[1, 1], - mask=mask[y1 : y2 + 1, x1 : x2 + 1], - ) - drawing_shape.selected = True - drawing_shape.paint(p) - p.end() def transformPos(self, point): @@ -819,20 +722,6 @@ def finalise(self): point_labels=[1] * len(points), shape_type="polygon", ) - elif self.createMode == "ai_mask": - # convert points to mask by an AI model - assert self.current.shape_type == "points" - mask = self._ai_model.predict_mask_from_points( - points=[[point.x(), point.y()] for point in self.current.points], - point_labels=self.current.point_labels, - ) - y1, x1, y2, x2 = imgviz.instances.masks_to_bboxes([mask])[0].astype(int) - self.current.setShapeRefined( - shape_type="mask", - points=[QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)], - point_labels=[1, 1], - mask=mask[y1 : y2 + 1, x1 : x2 + 1], - ) self.current.close() self.shapes.append(self.current) @@ -998,12 +887,8 @@ def undoLastLine(self): self.current = self.shapes.pop() self.current.setOpen() self.current.restoreShapeRaw() - if self.createMode in ["polygon", "linestrip"]: - self.line.points = [self.current[-1], self.current[0]] - elif self.createMode in ["rectangle", "line", "circle"]: + if self.createMode in ["rectangle"]: self.current.points = self.current.points[0:1] - elif self.createMode == "point": - self.current = None self.drawingPolygon.emit(True) def undoLastPoint(self): From 3509230d58b333803ba13d480a78d4cd41c4e7d9 Mon Sep 17 00:00:00 2001 From: DLlord47 Date: Sun, 13 Oct 2024 20:06:24 +0300 Subject: [PATCH 5/8] MARK-3. Removed Brightness contrast (#3) * MARK-3. Removed Brightness contrast * MARK-3. Removed Brightness contrast. * fix default_config --- labelme/app.py | 60 ----------------- labelme/config/default_config.yaml | 2 - labelme/widgets/brightness_contrast_dialog.py | 67 ------------------- 3 files changed, 129 deletions(-) delete mode 100644 labelme/widgets/brightness_contrast_dialog.py diff --git a/labelme/app.py b/labelme/app.py index ee5cc5bdd..f86a1bd42 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -26,7 +26,6 @@ from labelme.logger import logger from labelme.shape import Shape from labelme.widgets import AiPromptWidget -from labelme.widgets import BrightnessContrastDialog from labelme.widgets import Canvas from labelme.widgets import FileDialogPreview from labelme.widgets import LabelDialog @@ -469,14 +468,6 @@ def __init__( checkable=True, enabled=False, ) - brightnessContrast = action( - self.tr("&Brightness Contrast"), - self.brightnessContrast, - None, - "color", - self.tr("Adjust brightness and contrast"), - enabled=False, - ) # Group zoom controls into a list for easier toggling. zoomActions = ( self.zoomWidget, @@ -546,7 +537,6 @@ def __init__( keepPrevScale=keepPrevScale, fitWindow=fitWindow, fitWidth=fitWidth, - brightnessContrast=brightnessContrast, zoomActions=zoomActions, openNextImg=openNextImg, openPrevImg=openPrevImg, @@ -579,7 +569,6 @@ def __init__( createRectangleMode, # createAiPolygonMode, editMode, - brightnessContrast, ), onShapesPresent=(saveAs, hideAll, showAll, toggleAll), ) @@ -636,7 +625,6 @@ def __init__( fitWindow, fitWidth, None, - brightnessContrast, ), ) @@ -699,7 +687,6 @@ def __init__( editMode, delete, undo, - brightnessContrast, None, fitWindow, zoom, @@ -730,7 +717,6 @@ def __init__( self.zoom_level = 100 self.fit_window = False self.zoom_values = {} # key=filename, value=(zoom_mode, zoom_value) - self.brightnessContrast_values = {} self.scroll_values = { Qt.Horizontal: {}, Qt.Vertical: {}, @@ -1451,28 +1437,6 @@ def enableKeepPrevScale(self, enabled): self._config["keep_prev_scale"] = enabled self.actions.keepPrevScale.setChecked(enabled) - def onNewBrightnessContrast(self, qimage): - self.canvas.loadPixmap(QtGui.QPixmap.fromImage(qimage), clear_shapes=False) - - def brightnessContrast(self, value): - dialog = BrightnessContrastDialog( - utils.img_data_to_pil(self.imageData), - self.onNewBrightnessContrast, - parent=self, - ) - brightness, contrast = self.brightnessContrast_values.get( - self.filename, (None, None) - ) - if brightness is not None: - dialog.slider_brightness.setValue(brightness) - if contrast is not None: - dialog.slider_contrast.setValue(contrast) - dialog.exec_() - - brightness = dialog.slider_brightness.value() - contrast = dialog.slider_contrast.value() - self.brightnessContrast_values[self.filename] = (brightness, contrast) - def togglePolygons(self, value): flag = value for item in self.labelList: @@ -1572,30 +1536,6 @@ def loadFile(self, filename=None): self.setScroll( orientation, self.scroll_values[orientation][self.filename] ) - # set brightness contrast values - dialog = BrightnessContrastDialog( - utils.img_data_to_pil(self.imageData), - self.onNewBrightnessContrast, - parent=self, - ) - brightness, contrast = self.brightnessContrast_values.get( - self.filename, (None, None) - ) - if self._config["keep_prev_brightness"] and self.recentFiles: - brightness, _ = self.brightnessContrast_values.get( - self.recentFiles[0], (None, None) - ) - if self._config["keep_prev_contrast"] and self.recentFiles: - _, contrast = self.brightnessContrast_values.get( - self.recentFiles[0], (None, None) - ) - if brightness is not None: - dialog.slider_brightness.setValue(brightness) - if contrast is not None: - dialog.slider_contrast.setValue(contrast) - self.brightnessContrast_values[self.filename] = (brightness, contrast) - if brightness is not None or contrast is not None: - dialog.onNewValue(None) self.paintCanvas() self.addRecentFile(self.filename) self.toggleActions(True) diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index 6c0c6fb0a..1ed66ce56 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -2,8 +2,6 @@ auto_save: true display_label_popup: true keep_prev: false keep_prev_scale: false -keep_prev_brightness: false -keep_prev_contrast: false logger_level: info flags: null diff --git a/labelme/widgets/brightness_contrast_dialog.py b/labelme/widgets/brightness_contrast_dialog.py deleted file mode 100644 index 47f5d8ec3..000000000 --- a/labelme/widgets/brightness_contrast_dialog.py +++ /dev/null @@ -1,67 +0,0 @@ -import PIL.Image -import PIL.ImageEnhance -from qtpy import QtWidgets -from qtpy.QtCore import Qt -from qtpy.QtGui import QImage - - -class BrightnessContrastDialog(QtWidgets.QDialog): - _base_value = 50 - - def __init__(self, img, callback, parent=None): - super(BrightnessContrastDialog, self).__init__(parent) - self.setModal(True) - self.setWindowTitle("Brightness/Contrast") - - sliders = {} - layouts = {} - for title in ["Brightness:", "Contrast:"]: - layout = QtWidgets.QHBoxLayout() - title_label = QtWidgets.QLabel(self.tr(title)) - title_label.setFixedWidth(75) - layout.addWidget(title_label) - # - slider = QtWidgets.QSlider(Qt.Horizontal) - slider.setRange(0, 3 * self._base_value) - slider.setValue(self._base_value) - layout.addWidget(slider) - # - value_label = QtWidgets.QLabel(f"{slider.value() / self._base_value:.2f}") - value_label.setAlignment(Qt.AlignRight) - layout.addWidget(value_label) - # - slider.valueChanged.connect(self.onNewValue) - slider.valueChanged.connect( - lambda: value_label.setText(f"{slider.value() / self._base_value:.2f}") - ) - layouts[title] = layout - sliders[title] = slider - - self.slider_brightness = sliders["Brightness:"] - self.slider_contrast = sliders["Contrast:"] - del sliders - - layout = QtWidgets.QVBoxLayout() - layout.addLayout(layouts["Brightness:"]) - layout.addLayout(layouts["Contrast:"]) - del layouts - self.setLayout(layout) - - assert isinstance(img, PIL.Image.Image) - self.img = img - self.callback = callback - - def onNewValue(self, _): - brightness = self.slider_brightness.value() / self._base_value - contrast = self.slider_contrast.value() / self._base_value - - img = self.img - if brightness != 1: - img = PIL.ImageEnhance.Brightness(img).enhance(brightness) - if contrast != 1: - img = PIL.ImageEnhance.Contrast(img).enhance(contrast) - - qimage = QImage( - img.tobytes(), img.width, img.height, img.width * 3, QImage.Format_RGB888 - ) - self.callback(qimage) From d3f5e3d1cc26d2fffbd88f91e99dc079b771faa1 Mon Sep 17 00:00:00 2001 From: Artemy Gladkov <52081678+Nasochec@users.noreply.github.com> Date: Tue, 15 Oct 2024 21:41:50 +0300 Subject: [PATCH 6/8] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=B2=20bbox=20=D0=B8=D0=B5=D1=80=D0=B0=D1=80=D1=85=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Добавил в bbox иерархию. Сделал их сохранение, загрузку, добавление. * add id to shape * Исправил ошибку с рисованием прямоугольников, Частично исправил отмену * finished * Поменял горячую клавишу. Удалил комментарии --- .gitignore | 2 + labelme/app.py | 92 +++++++++++--- labelme/config/default_config.yaml | 5 + labelme/label_file.py | 71 +++++++---- labelme/shape.py | 144 +++++++++++++++++++++- labelme/widgets/canvas.py | 185 +++++++++++++++++++++-------- 6 files changed, 407 insertions(+), 92 deletions(-) diff --git a/.gitignore b/.gitignore index 90a9899d8..1e99978d0 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ .idea/ /venv/* + +/.vscode/ diff --git a/labelme/app.py b/labelme/app.py index f86a1bd42..e67abb1b3 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -24,7 +24,7 @@ from labelme.label_file import LabelFile from labelme.label_file import LabelFileError from labelme.logger import logger -from labelme.shape import Shape +from labelme.shape import Shape, ShapeClass from labelme.widgets import AiPromptWidget from labelme.widgets import Canvas from labelme.widgets import FileDialogPreview @@ -83,6 +83,12 @@ def __init__( Shape.hvertex_fill_color = QtGui.QColor( *self._config["shape"]["hvertex_fill_color"] ) + Shape.text_color = QtGui.QColor( + *self._config["shape"]["text_color"] + ) + Shape.row_color = QtGui.QColor( + *self._config["shape"]["row_color"] + ) # Set point size from config file Shape.point_size = self._config["shape"]["point_size"] @@ -353,6 +359,26 @@ def __init__( tip=self.tr("Remove selected point from polygon"), enabled=False, ) + + # Действия для выбора и сброса выбора прямоугольника + # Отвечает за "переход" к элементу, чтобы создавались его потомки + # т.е. в тексте создавались строки, а в строках буквы + selectShape = action( + text=self.tr("Select rectangle"), + slot=self.selectShape, + shortcut=shortcuts["select"], + icon="edit", + tip=self.tr("Select rectangle and zoom in"), + enabled=True, + ) + deSelectShape = action( + text=self.tr("De select rectangle"), + slot=self.deSelectShape, + shortcut=shortcuts["deselect"], + icon="edit", + tip=self.tr("De select rectangle and zoom out"), + enabled=True, + ) undo = action( self.tr("Undo\n"), @@ -527,6 +553,8 @@ def __init__( undoLastPoint=undoLastPoint, undo=undo, removePoint=removePoint, + selectShape=selectShape, + deSelectShape=deSelectShape, editMode=editMode, createRectangleMode=createRectangleMode, createAiPolygonMode=createAiPolygonMode, @@ -547,6 +575,9 @@ def __init__( edit, delete, None, + selectShape, + deSelectShape, + None, undo, undoLastPoint, None, @@ -684,10 +715,14 @@ def __init__( save, deleteFile, None, + createRectangleMode, editMode, delete, undo, None, + selectShape, + deSelectShape, + None, fitWindow, zoom, None, @@ -1149,6 +1184,7 @@ def addLabel(self, shape): else: text = "{} ({})".format(shape.label, shape.group_id) label_list_item = LabelListWidgetItem(text, shape) + label_list_item.setCheckState(Qt.Checked if self.canvas.isVisible(shape) else Qt.Unchecked) self.labelList.addItem(label_list_item) if self.uniqLabelList.findItemByLabel(shape.label) is None: item = self.uniqLabelList.createItemFromLabel(shape.label) @@ -1209,16 +1245,15 @@ def loadShapes(self, shapes, replace=True): self._noSelectionSlot = False self.canvas.loadShapes(shapes, replace=replace) - def loadLabels(self, shapes): - s = [] - for shape in shapes: - label = shape["label"] - points = shape["points"] - shape_type = shape["shape_type"] - flags = shape["flags"] - description = shape.get("description", "") - group_id = shape["group_id"] - other_data = shape["other_data"] + def _loadLabelsRecursive(self,inputList, shapes, parent : Shape = None): + for shape_dict in inputList: + label = shape_dict["label"] + points = shape_dict["points"] + shape_type = shape_dict["shape_type"] + flags = shape_dict["flags"] + description = shape_dict.get("description", "") + group_id = shape_dict["group_id"] + other_data = shape_dict["other_data"] if not points: # skip point-empty shape @@ -1229,12 +1264,15 @@ def loadLabels(self, shapes): shape_type=shape_type, group_id=group_id, description=description, - mask=shape["mask"], + mask=shape_dict["mask"], + parent=parent, ) for x, y in points: shape.addPoint(QtCore.QPointF(x, y)) shape.close() + self._loadLabelsRecursive(shape_dict["shapes"], shapes, parent = shape) + default_flags = {} if self._config["label_flags"]: for pattern, keys in self._config["label_flags"].items(): @@ -1245,7 +1283,11 @@ def loadLabels(self, shapes): shape.flags.update(flags) shape.other_data = other_data - s.append(shape) + shapes.append(shape) + + def loadLabels(self, shapes): + s = [] + self._loadLabelsRecursive(shapes,s) self.loadShapes(s) def loadFlags(self, flags): @@ -1259,11 +1301,12 @@ def loadFlags(self, flags): def saveLabels(self, filename): lf = LabelFile() - def format_shape(s): + def format_shape(s:Shape): data = s.other_data.copy() data.update( dict( label=s.label.encode("utf-8") if PY2 else s.label, + shapes=[format_shape(a) for a in s.getChildren()], points=[(p.x(), p.y()) for p in s.points], group_id=s.group_id, description=s.description, @@ -1276,7 +1319,7 @@ def format_shape(s): ) return data - shapes = [format_shape(item.shape()) for item in self.labelList] + shapes = [format_shape(item.shape()) for item in self.labelList if item.shape().getClass() == ShapeClass.TEXT] flags = {} for i in range(self.flag_widget.count()): item = self.flag_widget.item(i) @@ -1439,7 +1482,11 @@ def enableKeepPrevScale(self, enabled): def togglePolygons(self, value): flag = value + shapes = self.canvas.selectedShape.getAllChildren() for item in self.labelList: + if not item.shape() in shapes: + continue + if value is None: flag = item.checkState() == Qt.Unchecked item.setCheckState(Qt.Checked if flag else Qt.Unchecked) @@ -1571,15 +1618,15 @@ def scaleFitWindow(self): h1 = self.centralWidget().height() - e a1 = w1 / h1 # Calculate a new scale value based on the pixmap's aspect ratio. - w2 = self.canvas.pixmap.width() - 0.0 - h2 = self.canvas.pixmap.height() - 0.0 + w2 = self.canvas.cropped_image.width() - 0.0 + h2 = self.canvas.cropped_image.height() - 0.0 a2 = w2 / h2 return w1 / w2 if a2 >= a1 else h1 / h2 def scaleFitWidth(self): # The epsilon does not seem to work too well here. w = self.centralWidget().width() - 2.0 - return w / self.canvas.pixmap.width() + return w / self.canvas.cropped_image.width() def closeEvent(self, event): if not self.mayContinue(): @@ -1862,6 +1909,15 @@ def removeSelectedPoint(self): action.setEnabled(False) self.setDirty() + + def selectShape(self): + self.canvas.zoomShape() + self.canvas.update() + + def deSelectShape(self): + self.canvas.unZoomShape() + self.canvas.update() + def deleteSelectedShape(self): yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No msg = self.tr( diff --git a/labelme/config/default_config.yaml b/labelme/config/default_config.yaml index 1ed66ce56..553c08f94 100644 --- a/labelme/config/default_config.yaml +++ b/labelme/config/default_config.yaml @@ -21,6 +21,8 @@ shape: line_color: [0, 255, 0, 128] fill_color: [0, 0, 0, 64] vertex_fill_color: [0, 255, 0, 255] + text_color: [0, 0, 0, 255] # color of text rectangles + row_color: [0, 0, 211, 255] # color of row rectangles # selecting / hovering select_line_color: [255, 255, 255, 255] select_fill_color: [0, 255, 0, 64] @@ -116,6 +118,9 @@ shortcuts: toggle_keep_prev_mode: Ctrl+P remove_selected_point: [Meta+H, Backspace] + select: Ctrl+A + deselect: Esc + show_all_polygons: null hide_all_polygons: null toggle_all_polygons: T diff --git a/labelme/label_file.py b/labelme/label_file.py index 262947550..3a3d62ae1 100644 --- a/labelme/label_file.py +++ b/labelme/label_file.py @@ -65,24 +65,61 @@ def load_image_file(filename): f.seek(0) return f.read() - def load(self, filename): - keys = [ - "version", - "imagePath", - "shapes", # polygonal annotations - "flags", # image level flags - "imageHeight", - "imageWidth", - ] + def _loadRecursice(self,data): + """ + Метод для рекурсивной подгрузки bbox-ов из словаря. + + Преобразует поля словаря как это делалось в коде ранее, + но с учётом иерархии в bbox. + + ------- + Параметры + + data + Список bbox-ов + + ------- + Возвращает + + Преобразованный список + """ shape_keys = [ "label", "points", "group_id", + "shapes", "shape_type", "flags", "description", "mask", ] + shapes = [ + dict( + label=s["label"], + shapes=self._loadRecursice(s["shapes"]), + points=s["points"], + shape_type=s.get("shape_type", "polygon"), + flags=s.get("flags", {}), + description=s.get("description"), + group_id=s.get("group_id"), + mask=utils.img_b64_to_arr(s["mask"]).astype(bool) + if s.get("mask") + else None, + other_data={k: v for k, v in s.items() if k not in shape_keys}, + ) + for s in data + ] + return shapes + + def load(self, filename): + keys = [ + "version", + "imagePath", + "shapes", # polygonal annotations + "flags", # image level flags + "imageHeight", + "imageWidth", + ] try: with open(filename, "r") as f: data = json.load(f) @@ -98,21 +135,7 @@ def load(self, filename): data.get("imageHeight"), data.get("imageWidth"), ) - shapes = [ - dict( - label=s["label"], - points=s["points"], - shape_type=s.get("shape_type", "polygon"), - flags=s.get("flags", {}), - description=s.get("description"), - group_id=s.get("group_id"), - mask=utils.img_b64_to_arr(s["mask"]).astype(bool) - if s.get("mask") - else None, - other_data={k: v for k, v in s.items() if k not in shape_keys}, - ) - for s in data["shapes"] - ] + shapes = self._loadRecursice(data["shapes"]) except Exception as e: raise LabelFileError(e) diff --git a/labelme/shape.py b/labelme/shape.py index 046b5fc53..72a88746a 100644 --- a/labelme/shape.py +++ b/labelme/shape.py @@ -1,4 +1,7 @@ +from typing import List + import copy +from enum import Enum import numpy as np import skimage.measure @@ -11,7 +14,26 @@ # TODO(unknown): # - [opt] Store paths instead of creating new ones at each paint. - +class ShapeClass(Enum): + TEXT = 0 + ROW = 1 + LETTER = 2 + +class IdController: + + _count : int = 0 + + @classmethod + def resetCount(cls): + cls._count = 0 + + @classmethod + def getId(cls): + tmp = cls._count + cls._count += 1 + return tmp + + class Shape(object): # Render handles as squares P_SQUARE = 0 @@ -27,6 +49,9 @@ class Shape(object): PEN_WIDTH = 2 + # цвета для блока текста и строки + text_color = None + row_color = None # The following class variables influence the drawing of all shape objects. line_color = None fill_color = None @@ -40,6 +65,7 @@ class Shape(object): def __init__( self, + id = None, label=None, line_color=None, shape_type=None, @@ -47,10 +73,15 @@ def __init__( group_id=None, description=None, mask=None, + parent : "Shape" = None, ): + if id is None: + self._id : int = IdController.getId() + else: + self._id = id self.label = label self.group_id = group_id - self.points = [] + self.points : List[QtCore.QPoint] = [] self.point_labels = [] self.shape_type = shape_type self._shape_raw = None @@ -62,6 +93,24 @@ def __init__( self.description = description self.other_data = {} self.mask = mask + + # self.parent - родительский элемент по отношению к текущему. + # В зависимости от класса родителя автоматически подбирается класс потомка + # self._shape_class - класс элемента (текст, строка, буква) + if parent is None: + self.parent = None + self._shape_class = ShapeClass.TEXT + else: + self.parent = parent + if parent.getClass() == ShapeClass.TEXT: + self._shape_class = ShapeClass.ROW + elif parent.getClass() == ShapeClass.ROW: + self._shape_class = ShapeClass.LETTER + else: + raise Exception(f"Shape wrong parent shape_class: {parent.getClass()}") + parent._addChild(self) + # self._children - список потомков элемента + self._children : List[Shape] = [] self._highlightIndex = None self._highlightMode = self.NEAR_VERTEX @@ -77,6 +126,51 @@ def __init__( # with an object attribute. Currently this # is used for drawing the pending line a different color. self.line_color = line_color + + def delete(self): + """ + Удаляет элемент и также стирает его из списка потомков родителя + """ + if self.parent is not None: + self.parent._deleteChild(self) + + def _addChild(self,shape:"Shape"): + if self._shape_class == ShapeClass.LETTER: + Exception("Letter can't be parent.") + self._children.append(shape) + + def _deleteChild(self,shape:"Shape"): + if shape in self._children: + self._children.remove(shape) + + def _childrenRecursive(self,list:List["Shape"]): + for a in self._children: + list.append(a) + for a in self._children: + a._childrenRecursive(list) + + def getAllChildren(self) -> List["Shape"] : + """ + Возвращает всех потомков + """ + list = [] + self._childrenRecursive(list) + return list + + def getChildren(self): + """ + Возвращает прямых потомков + """ + return self._children + + def getId(self): + return self._id + + def getClass(self): + """ + Возвращает класс элемента (Текст, Строка, Буква) + """ + return self._shape_class def _scale_point(self, point: QtCore.QPointF) -> QtCore.QPointF: return QtCore.QPointF(point.x() * self.scale, point.y() * self.scale) @@ -118,6 +212,28 @@ def addPoint(self, point, label=1): else: self.points.append(point) self.point_labels.append(label) + + def getCroppBox(self) -> QtCore.QRect: + """ + Находит обрамляющий прямоугольник для обрезки изоображения + + ------------- + Возвращает + + QTCore.QRect(x, y, width, height) + Координаты и размеры прямоугольника + """ + + x = [100000000, 0] + y = [100000000, 0] + for point in self.points: + x[0]= min(point.x(),x[0]) + x[1]= max(point.x(),x[1]) + + y[0]= min(point.y(),y[0]) + y[1]= max(point.y(),y[1]) + + return QtCore.QRect(int(x[0]),int(y[0]),int(x[1]-x[0]),int(y[1]-y[0])) def canAddPoint(self): return self.shape_type in ["polygon"] @@ -337,7 +453,29 @@ def highlightClear(self): self._highlightIndex = None def copy(self): - return copy.deepcopy(self) + shape = Shape(parent=self.parent, id = self._id) + shape.label = self.label + shape.points = copy.deepcopy(self.points) + shape.shape_type = self.shape_type + shape.flags = self.flags + shape.description = self.description + + def _copyWithChildren(self, list : List["Shape"], parent : "Shape" = None): + shape = Shape(parent=parent,id = self._id) + shape.label = self.label + shape.points = copy.deepcopy(self.points) + shape.shape_type = self.shape_type + shape.flags = self.flags + shape.description = self.description + list.append(shape) + for child in self._children: + child._copyWithChildren(list,shape) + + def copyWithChildren(self): + allShapes : List[Shape] = [] + self._copyWithChildren(allShapes, None) + return allShapes + def __len__(self): return len(self.points) diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py index f9239a2ce..ab234c606 100644 --- a/labelme/widgets/canvas.py +++ b/labelme/widgets/canvas.py @@ -1,3 +1,5 @@ +from typing import List, Dict + import imgviz from qtpy import QtCore from qtpy import QtGui @@ -7,7 +9,7 @@ import labelme.utils from labelme import QT5 from labelme.logger import logger -from labelme.shape import Shape +from labelme.shape import Shape,ShapeClass,IdController # TODO(unknown): # - [maybe] Find optimal epsilon value. @@ -57,10 +59,10 @@ def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT - self.shapes = [] - self.shapesBackups = [] + self.shapes : List[Shape] = [] + self.shapesBackups : List[Shape] = [] self.current = None - self.selectedShapes = [] # save the selected shapes here + self.selectedShapes : List[Shape] = [] # save the selected shapes here self.selectedShapesCopy = [] # self.line represents: # - createMode == 'polygon': edge from last point to current @@ -72,8 +74,13 @@ def __init__(self, *args, **kwargs): self.prevMovePoint = QtCore.QPoint() self.offsets = QtCore.QPoint(), QtCore.QPoint() self.scale = 1.0 - self.pixmap = QtGui.QPixmap() - self.visible = {} + # Полное изоображение + self.full_image = QtGui.QPixmap() + # Обрезанное изоображение + self.cropped_image = QtGui.QPixmap() + # Сдвиг обрезанного изообоажения относительно полного + self.image_offsets = (0 , 0) + self.visible : Dict[int,bool] = {} # TODO: change visible logic. Use shape id self._hideBackround = False self.hideBackround = False self.hShape = None @@ -85,6 +92,8 @@ def __init__(self, *args, **kwargs): self.movingShape = False self.snapping = True self.hShapeIsSelected = False + self.selectedShape : Shape = None + self._selectedShapeId : int = -1 self._painter = QtGui.QPainter() self._cursor = CURSOR_DEFAULT # Menus: @@ -127,18 +136,21 @@ def initializeAiModel(self, name): logger.debug("Initializing AI model: %r" % model.name) self._ai_model = model() - if self.pixmap is None: + if self.cropped_image is None: logger.warning("Pixmap is not set yet") return self._ai_model.set_image( - image=labelme.utils.img_qt_to_arr(self.pixmap.toImage()) + image=labelme.utils.img_qt_to_arr(self.cropped_image.toImage()) ) def storeShapes(self): shapesBackup = [] for shape in self.shapes: - shapesBackup.append(shape.copy()) + if shape.getClass() == ShapeClass.TEXT: + list = shape.copyWithChildren() + for shap in list: + shapesBackup.append(shap) if len(self.shapesBackups) > self.num_backups: self.shapesBackups = self.shapesBackups[-self.num_backups - 1 :] self.shapesBackups.append(shapesBackup) @@ -164,9 +176,11 @@ def restoreShape(self): # push this right back onto the stack. shapesBackup = self.shapesBackups.pop() self.shapes = shapesBackup - self.selectedShapes = [] + self.selectedShapes : List[Shape] = [] for shape in self.shapes: shape.selected = False + if shape.getId() == self._selectedShapeId: + self.selectedShape = shape self.update() def enterEvent(self, ev): @@ -179,8 +193,8 @@ def leaveEvent(self, ev): def focusOutEvent(self, ev): self.restoreCursor() - def isVisible(self, shape): - return self.visible.get(shape, True) + def isVisible(self, shape : Shape): + return self.visible.get(shape.getId(), True) def drawing(self): return self.mode == self.CREATE @@ -363,6 +377,37 @@ def removeSelectedPoint(self): self.prevhVertex = None self.movingShape = True # Save changes + def zoomShape(self): + """ + "Переходит" к элементу, ч тобы добавлять элементы + соответствующего типа. + """ + if len(self.selectedShapes) == 1: + if self.selectedShapes[0].getClass() != ShapeClass.LETTER: + self.selectedShape = self.selectedShapes[0] + self._selectedShapeId = self.selectedShape.getId() + self.visible.update((k, False) for k in self.visible) + self.visible.update((shape.getId(), True) for shape in self.selectedShape.getAllChildren()) + self.cropp() + else: + print("Необходимо выбрать 1 примоугольник.") + + def unZoomShape(self): + """ + "Переходит" к родителю текущего элемента. + """ + if self.selectedShape is not None: + self.selectedShape = self.selectedShape.parent + + if self.selectedShape is not None: + self._selectedShapeId = self.selectedShape.getId() + self.visible.update((k, False) for k in self.visible) + self.visible.update((shape.getId(), True) for shape in self.selectedShape.getAllChildren()) + else: + self._selectedShapeId = -1 + self.visible.update((k, True) for k in self.visible) + self.cropp() + def mousePressEvent(self, ev): if QT5: pos = self.transformPos(ev.localPos()) @@ -393,7 +438,8 @@ def mousePressEvent(self, ev): self.current = Shape( shape_type="points" if self.createMode in ["ai_polygon"] - else self.createMode + else self.createMode, + parent = self.selectedShape ) self.current.addPoint(pos, label=0 if is_shift_pressed else 1) if ( @@ -530,10 +576,12 @@ def selectShapePoint(self, point, multiple_selection_mode): self.deSelectShape() def calculateOffsets(self, point): - left = self.pixmap.width() - 1 - right = 0 - top = self.pixmap.height() - 1 - bottom = 0 + x0, y0 = self.image_offsets + + left = x0 + self.cropped_image.width() - 1 + right = x0 + top = y0 + self.cropped_image.height() - 1 + bottom = y0 for s in self.selectedShapes: rect = s.boundingRect() if rect.left() < left: @@ -558,20 +606,30 @@ def boundedMoveVertex(self, pos): pos = self.intersectionPoint(point, pos) shape.moveVertexBy(index, pos - point) - def boundedMoveShapes(self, shapes, pos): + def _outOfPixmapClear(self, p : QtCore.QPointF): + w, h = self.cropped_image.width(), self.cropped_image.height() + return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1) + + def boundedMoveShapes(self, shapes : List[Shape], pos): if self.outOfPixmap(pos): return False # No need to move - o1 = pos + self.offsets[0] - if self.outOfPixmap(o1): + x0, y0 = self.image_offsets + qp = QtCore.QPointF(x0, y0) + + pos -= qp + o1 = pos + self.offsets[0] + if self._outOfPixmapClear(o1): pos -= QtCore.QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] - if self.outOfPixmap(o2): + if self._outOfPixmapClear(o2): pos += QtCore.QPointF( - min(0, self.pixmap.width() - o2.x()), - min(0, self.pixmap.height() - o2.y()), + min(0, self.cropped_image.width() - o2.x()), + min(0, self.cropped_image.height() - o2.y()), ) + pos += qp + # XXX: The next line tracks the new position of the cursor - # relative to the shape, but also results in making it + # relative to the shape, but also resulSts in making it # a bit "shaky" when nearing the border and allows it to # go outside of the shape's area for some reason. # self.calculateOffsets(self.selectedShapes, pos) @@ -596,16 +654,18 @@ def deleteSelected(self): for shape in self.selectedShapes: self.shapes.remove(shape) deleted_shapes.append(shape) + shape.delete() self.storeShapes() self.selectedShapes = [] self.update() return deleted_shapes - def deleteShape(self, shape): + def deleteShape(self, shape:Shape): if shape in self.selectedShapes: self.selectedShapes.remove(shape) if shape in self.shapes: self.shapes.remove(shape) + shape.delete() self.storeShapes() self.update() @@ -620,7 +680,7 @@ def boundedShiftShapes(self, shapes): self.boundedMoveShapes(shapes, point + offset) def paintEvent(self, event): - if not self.pixmap: + if not self.cropped_image: return super(Canvas, self).paintEvent(event) p = self._painter @@ -631,8 +691,12 @@ def paintEvent(self, event): p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) - - p.drawPixmap(0, 0, self.pixmap) + + # Сдвиг относительно. + # неоходимость вознокает из-за обрезания картинки по "переходу" к элементу + p.translate(-self.image_offsets[0],-self.image_offsets[1]) + + p.drawPixmap(self.image_offsets[0],self.image_offsets[1], self.cropped_image) p.scale(1 / self.scale, 1 / self.scale) @@ -693,20 +757,22 @@ def paintEvent(self, event): def transformPos(self, point): """Convert from widget-logical coordinates to painter-logical ones.""" - return point / self.scale - self.offsetToCenter() + return (point / self.scale - self.offsetToCenter() + + QtCore.QPointF(self.image_offsets[0],self.image_offsets[1])) def offsetToCenter(self): s = self.scale area = super(Canvas, self).size() - w, h = self.pixmap.width() * s, self.pixmap.height() * s + w, h = self.cropped_image.width() * s, self.cropped_image.height() * s aw, ah = area.width(), area.height() x = (aw - w) / (2 * s) if aw > w else 0 y = (ah - h) / (2 * s) if ah > h else 0 return QtCore.QPointF(x, y) def outOfPixmap(self, p): - w, h = self.pixmap.width(), self.pixmap.height() - return not (0 <= p.x() <= w - 1 and 0 <= p.y() <= h - 1) + w, h = self.cropped_image.width(), self.cropped_image.height() + x,y = self.image_offsets + return not (x <= p.x() <= x + w - 1 and y <= p.y() <= y + h - 1) def finalise(self): assert self.current @@ -742,16 +808,18 @@ def intersectionPoint(self, p1, p2): # Cycle through each image edge in clockwise fashion, # and find the one intersecting the current line segment. # http://paulbourke.net/geometry/lineline2d/ - size = self.pixmap.size() + size = self.cropped_image.size() + x0, y0 = self.image_offsets + points = [ - (0, 0), - (size.width() - 1, 0), - (size.width() - 1, size.height() - 1), - (0, size.height() - 1), + (x0, y0), + (x0 + size.width() - 1, y0), + (x0 + size.width() - 1, y0 + size.height() - 1), + (x0, y0 + size.height() - 1), ] # x1, y1 should be in the pixmap, x2, y2 should be out of the pixmap - x1 = min(max(p1.x(), 0), size.width() - 1) - y1 = min(max(p1.y(), 0), size.height() - 1) + x1 = min(max(p1.x() + x0, x0), x0 + size.width() - 1) + y1 = min(max(p1.y() + y0, y0), y0 + size.height() - 1) x2, y2 = p2.x(), p2.y() d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) x3, y3 = points[i] @@ -759,9 +827,9 @@ def intersectionPoint(self, p1, p2): if (x, y) == (x1, y1): # Handle cases where previous point is on one of the edges. if x3 == x4: - return QtCore.QPointF(x3, min(max(0, y2), max(y3, y4))) + return QtCore.QPointF(x3, min(max(y0, y2), max(y3, y4))) else: # y3 == y4 - return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3) + return QtCore.QPointF(min(max(x0, x2), max(x3, x4)), y3) return QtCore.QPointF(x, y) def intersectingEdges(self, point1, point2, points): @@ -799,8 +867,8 @@ def sizeHint(self): return self.minimumSizeHint() def minimumSizeHint(self): - if self.pixmap: - return self.scale * self.pixmap.size() + if self.cropped_image: + return self.scale * self.cropped_image.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): @@ -902,11 +970,28 @@ def undoLastPoint(self): self.drawingPolygon.emit(False) self.update() + def cropp(self): + """ + Образает картинку соответственно текущему выбранному элементу + """ + if self.selectedShape is None: + self.cropped_image = self.full_image.copy() + self.image_offsets = (0,0) + else: + shape = self.selectedShape + rect = shape.getCroppBox() + self.image_offsets = (rect.x(),rect.y()) + self.cropped_image.convertFromImage(self.full_image.copy(rect).toImage()) + + self.update() + + def loadPixmap(self, pixmap, clear_shapes=True): - self.pixmap = pixmap + self.full_image = pixmap + self.cropped_image = pixmap.copy() if self._ai_model: self._ai_model.set_image( - image=labelme.utils.img_qt_to_arr(self.pixmap.toImage()) + image=labelme.utils.img_qt_to_arr(self.cropped_image.toImage()) ) if clear_shapes: self.shapes = [] @@ -925,7 +1010,7 @@ def loadShapes(self, shapes, replace=True): self.update() def setShapeVisible(self, shape, value): - self.visible[shape] = value + self.visible[shape.getId()] = value self.update() def overrideCursor(self, cursor): @@ -938,6 +1023,12 @@ def restoreCursor(self): def resetState(self): self.restoreCursor() - self.pixmap = None + self.cropped_image = None self.shapesBackups = [] + self.shapes = [] + self.selectedShape = None + self._selectedShapeId = -1 + self.image_offsets = (0, 0) + IdController.resetCount() + self.visible = {} self.update() From d38e6ca18831b5685e7ee3ad9fe312dc2467dcf7 Mon Sep 17 00:00:00 2001 From: Leonid Bystrov Date: Thu, 17 Oct 2024 22:39:22 +0300 Subject: [PATCH 7/8] =?UTF-8?q?MARK-6=20=D0=9C=D0=BE=D0=B6=D0=BD=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D0=BC=D0=B5=D1=89=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=D1=81=D1=8F=20=D0=BF=D0=BE=20=D0=B8=D0=B7=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8E,=20=D0=B7=D0=B0=D0=B6?= =?UTF-8?q?=D0=B8=D0=BC=D0=B0=D1=8F=20=D0=BA=D0=BE=D0=BB=D1=91=D1=81=D0=B8?= =?UTF-8?q?=D0=BA=D0=BE=20=D0=BC=D1=8B=D1=88=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- labelme/app.py | 10 ++++++++++ labelme/widgets/__init__.py | 2 -- labelme/widgets/canvas.py | 24 +++++++++++++++++++++--- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/labelme/app.py b/labelme/app.py index e67abb1b3..6c7fa9101 100644 --- a/labelme/app.py +++ b/labelme/app.py @@ -186,6 +186,7 @@ def __init__( Qt.Horizontal: scrollArea.horizontalScrollBar(), } self.canvas.scrollRequest.connect(self.scrollRequest) + self.canvas.scrollDragRequest.connect(self.scrollDragRequest) self.canvas.newShape.connect(self.newShape) self.canvas.shapeMoved.connect(self.setDirty) @@ -1422,6 +1423,15 @@ def scrollRequest(self, delta, orientation): value = bar.value() + bar.singleStep() * units self.setScroll(orientation, value) + # Обработка события панорамирования + def scrollDragRequest(self, delta, orientation): + bar = self.scrollBars[orientation] + # Новое значение слайдера получается как предыдущее + нормированное смещение по координатам в окне + if orientation == QtCore.Qt.Vertical: + self.setScroll(orientation, bar.value() + delta * bar.height()) + else: + self.setScroll(orientation, bar.value() + delta * bar.width()) + def setScroll(self, orientation, value): self.scrollBars[orientation].setValue(int(value)) self.scroll_values[orientation][self.filename] = value diff --git a/labelme/widgets/__init__.py b/labelme/widgets/__init__.py index 6283ef1e8..a570f4ec6 100644 --- a/labelme/widgets/__init__.py +++ b/labelme/widgets/__init__.py @@ -2,8 +2,6 @@ from .ai_prompt_widget import AiPromptWidget -from .brightness_contrast_dialog import BrightnessContrastDialog - from .canvas import Canvas from .color_dialog import ColorDialog diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py index ab234c606..9fd29b64a 100644 --- a/labelme/widgets/canvas.py +++ b/labelme/widgets/canvas.py @@ -33,6 +33,7 @@ class Canvas(QtWidgets.QWidget): drawingPolygon = QtCore.Signal(bool) vertexSelected = QtCore.Signal(bool) mouseMoved = QtCore.Signal(QtCore.QPointF) + scrollDragRequest = QtCore.Signal(float, int) # Сигнал для панорамирования CREATE, EDIT = 0, 1 @@ -228,6 +229,19 @@ def selectedEdge(self): return self.hEdge is not None def mouseMoveEvent(self, ev): + """ + Если зажато колёсико мыши, то запускаем панорамирование. + deltaX, deltaY -- нормированное смещение по соответствующей координате + """ + if ev.buttons() & QtCore.Qt.MiddleButton: + QtGui.QCursor.setPos(self.mapToGlobal(self._pan_start)) + deltaX = (ev.x() - self._pan_start.x()) / self.cropped_image.width() / self.scale + deltaY = (ev.y() - self._pan_start.y()) / self.cropped_image.height() / self.scale + self.scrollDragRequest.emit(deltaX, QtCore.Qt.Horizontal) + self.scrollDragRequest.emit(deltaY, QtCore.Qt.Vertical) + # self._pan_start = ev.pos() # Позволяет панорамировать относительно зажатого курсора + ev.accept() + return """Update line with last point and current coordinates.""" try: if QT5: @@ -379,7 +393,7 @@ def removeSelectedPoint(self): def zoomShape(self): """ - "Переходит" к элементу, ч тобы добавлять элементы + "Переходит" к элементу, чтобы добавлять элементы соответствующего типа. """ if len(self.selectedShapes) == 1: @@ -389,8 +403,6 @@ def zoomShape(self): self.visible.update((k, False) for k in self.visible) self.visible.update((shape.getId(), True) for shape in self.selectedShape.getAllChildren()) self.cropp() - else: - print("Необходимо выбрать 1 примоугольник.") def unZoomShape(self): """ @@ -481,6 +493,10 @@ def mousePressEvent(self, ev): self.selectShapePoint(pos, multiple_selection_mode=group_mode) self.repaint() self.prevPoint = pos + elif ev.button() == QtCore.Qt.MiddleButton: + self._pan_start = ev.pos() # Точка, относительно которой выполняется панорамирование + self.overrideCursor(CURSOR_MOVE) # Поменять курсор на сжатую ручку + ev.accept() def mouseReleaseEvent(self, ev): if ev.button() == QtCore.Qt.RightButton: @@ -500,6 +516,8 @@ def mouseReleaseEvent(self, ev): self.selectionChanged.emit( [x for x in self.selectedShapes if x != self.hShape] ) + elif ev.button() == QtCore.Qt.MiddleButton: + self.overrideCursor(CURSOR_GRAB) # Панорамирование окончено. Возвращение курсора к обычному виду. if self.movingShape and self.hShape: index = self.shapes.index(self.hShape) From 0c2fb057e52b1e5c177a0f7318afb846149eddf2 Mon Sep 17 00:00:00 2001 From: Artemy Gladkov Date: Fri, 18 Oct 2024 20:03:21 +0300 Subject: [PATCH 8/8] =?UTF-8?q?MARK-6=20=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D0=BD=D0=B0=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=B5=D1=80=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=BD=D0=B0?= =?UTF-8?q?=20=D0=BF=D1=80=D0=B8=D0=B2=D1=8B=D1=87=D0=BD=D0=BE=D0=B5=20(?= =?UTF-8?q?=D0=B0=20=D0=BD=D0=B5=20=D0=B8=D0=BD=D0=B2=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=BD=D0=BE=D0=B5)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- labelme/widgets/canvas.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/labelme/widgets/canvas.py b/labelme/widgets/canvas.py index 9fd29b64a..8f0f8d219 100644 --- a/labelme/widgets/canvas.py +++ b/labelme/widgets/canvas.py @@ -235,8 +235,8 @@ def mouseMoveEvent(self, ev): """ if ev.buttons() & QtCore.Qt.MiddleButton: QtGui.QCursor.setPos(self.mapToGlobal(self._pan_start)) - deltaX = (ev.x() - self._pan_start.x()) / self.cropped_image.width() / self.scale - deltaY = (ev.y() - self._pan_start.y()) / self.cropped_image.height() / self.scale + deltaX = - (ev.x() - self._pan_start.x()) / self.cropped_image.width() / self.scale + deltaY = - (ev.y() - self._pan_start.y()) / self.cropped_image.height() / self.scale self.scrollDragRequest.emit(deltaX, QtCore.Qt.Horizontal) self.scrollDragRequest.emit(deltaY, QtCore.Qt.Vertical) # self._pan_start = ev.pos() # Позволяет панорамировать относительно зажатого курсора