Prechádzať zdrojové kódy

Group operations on shapes (#405)

* Group operations on shapes

* flake8 fixes
IlyaOvodov 6 rokov pred
rodič
commit
da3fa111b0
3 zmenil súbory, kde vykonal 138 pridanie a 104 odobranie
  1. 58 30
      labelme/app.py
  2. 79 74
      labelme/widgets/canvas.py
  3. 1 0
      labelme/widgets/label_qlist_widget.py

+ 58 - 30
labelme/app.py

@@ -304,10 +304,10 @@ class MainWindow(QtWidgets.QMainWindow):
                           shortcuts['edit_polygon'], 'edit',
                           'Move and edit polygons', enabled=False)
 
-        delete = action('Delete Polygon', self.deleteSelectedShape,
+        delete = action('Delete Polygon(s)', self.deleteSelectedShape,
                         shortcuts['delete_polygon'], 'cancel',
                         'Delete', enabled=False)
-        copy = action('Duplicate Polygon', self.copySelectedShape,
+        copy = action('Duplicate Polygon(s)', self.copySelectedShape,
                       shortcuts['duplicate_polygon'], 'copy',
                       'Create a duplicate of the selected polygon',
                       enabled=False)
@@ -526,6 +526,13 @@ class MainWindow(QtWidgets.QMainWindow):
                 action('&Move here', self.moveShape),
             ),
         )
+        utils.addActions(
+            self.canvas.menus[2],
+            (
+                action('&Copy here', self.copyShape),
+                action('&Move here', self.moveShape),
+            ),
+        )
 
         self.tools = self.toolbar('Tools')
         # Menu buttons on Left
@@ -899,19 +906,21 @@ class MainWindow(QtWidgets.QMainWindow):
                 self.loadFile(filename)
 
     # React to canvas signals.
-    def shapeSelectionChanged(self, selected=False):
-        if self._noSelectionSlot:
-            self._noSelectionSlot = False
-        else:
-            shape = self.canvas.selectedShape
-            if shape:
-                item = self.labelList.get_item_from_shape(shape)
-                item.setSelected(True)
-            else:
-                self.labelList.clearSelection()
+    def shapeSelectionChanged(self, selected_shapes):
+        self._noSelectionSlot = True
+        for shape in self.canvas.selectedShapes:
+            shape.selected = False
+        self.labelList.clearSelection()
+        self.canvas.selectedShapes = selected_shapes
+        for shape in self.canvas.selectedShapes:
+            shape.selected = True
+            item = self.labelList.get_item_from_shape(shape)
+            item.setSelected(True)
+        self._noSelectionSlot = False
+        selected = len(selected_shapes)
         self.actions.delete.setEnabled(selected)
         self.actions.copy.setEnabled(selected)
-        self.actions.edit.setEnabled(selected)
+        self.actions.edit.setEnabled(selected == 1)
         self.actions.shapeLineColor.setEnabled(selected)
         self.actions.shapeFillColor.setEnabled(selected)
 
@@ -928,13 +937,17 @@ class MainWindow(QtWidgets.QMainWindow):
         for action in self.actions.onShapesPresent:
             action.setEnabled(True)
 
-    def remLabel(self, shape):
-        item = self.labelList.get_item_from_shape(shape)
-        self.labelList.takeItem(self.labelList.row(item))
+    def remLabels(self, removed_shapes):
+        for shape in removed_shapes:
+            item = self.labelList.get_item_from_shape(shape)
+            self.labelList.takeItem(self.labelList.row(item))
 
     def loadShapes(self, shapes, replace=True):
+        self._noSelectionSlot = True
         for shape in shapes:
             self.addLabel(shape)
+        self.labelList.clearSelection()
+        self._noSelectionSlot = False
         self.canvas.loadShapes(shapes, replace=replace)
 
     def loadLabels(self, shapes):
@@ -1027,16 +1040,22 @@ class MainWindow(QtWidgets.QMainWindow):
             return False
 
     def copySelectedShape(self):
-        self.addLabel(self.canvas.copySelectedShape())
-        # fix copy and delete
-        self.shapeSelectionChanged(True)
+        added_shapes = self.canvas.copySelectedShapes()
+        self.labelList.clearSelection()
+        for shape in added_shapes:
+            self.addLabel(shape)
+        self.setDirty()
 
     def labelSelectionChanged(self):
-        item = self.currentItem()
-        if item and self.canvas.editing():
-            self._noSelectionSlot = True
-            shape = self.labelList.get_shape_from_item(item)
-            self.canvas.selectShape(shape)
+        if self._noSelectionSlot:
+            return
+        if self.canvas.editing():
+            selected_shapes = []
+            for item in self.labelList.selectedItems():
+                shape = self.labelList.get_shape_from_item(item)
+                selected_shapes.append(shape)
+            if selected_shapes:
+                self.canvas.selectShapes(selected_shapes)
 
     def labelItemChanged(self, item):
         shape = self.labelList.get_shape_from_item(item)
@@ -1075,6 +1094,7 @@ class MainWindow(QtWidgets.QMainWindow):
                               .format(text, self._config['validate_label']))
             text = ''
         if text:
+            self.labelList.clearSelection()
             self.addLabel(self.canvas.setLastLabel(text, flags))
             self.actions.editMode.setEnabled(True)
             self.actions.undoLastPoint.setEnabled(False)
@@ -1528,11 +1548,15 @@ class MainWindow(QtWidgets.QMainWindow):
 
     def deleteSelectedShape(self):
         yes, no = QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No
-        msg = 'You are about to permanently delete this polygon, ' \
-              'proceed anyway?'
+        if len(self.canvas.selectedShapes) > 1:
+            msg = 'You are about to permanently delete {} polygons, ' \
+                  'proceed anyway?'.format(len(self.canvas.selectedShapes))
+        else:
+            msg = 'You are about to permanently delete this polygon, ' \
+                  'proceed anyway?'
         if yes == QtWidgets.QMessageBox.warning(self, 'Attention', msg,
                                                 yes | no):
-            self.remLabel(self.canvas.deleteSelected())
+            self.remLabels(self.canvas.deleteSelected())
             self.setDirty()
             if self.noShapes():
                 for action in self.actions.onShapesPresent:
@@ -1542,7 +1566,8 @@ class MainWindow(QtWidgets.QMainWindow):
         color = self.colorDialog.getColor(
             self.lineColor, 'Choose line color', default=DEFAULT_LINE_COLOR)
         if color:
-            self.canvas.selectedShape.line_color = color
+            for shape in self.canvas.selectedShapes:
+                shape.line_color = color
             self.canvas.update()
             self.setDirty()
 
@@ -1550,13 +1575,16 @@ class MainWindow(QtWidgets.QMainWindow):
         color = self.colorDialog.getColor(
             self.fillColor, 'Choose fill color', default=DEFAULT_FILL_COLOR)
         if color:
-            self.canvas.selectedShape.fill_color = color
+            for shape in self.canvas.selectedShapes:
+                shape.fill_color = color
             self.canvas.update()
             self.setDirty()
 
     def copyShape(self):
         self.canvas.endMove(copy=True)
-        self.addLabel(self.canvas.selectedShape)
+        self.labelList.clearSelection()
+        for shape in self.canvas.selectedShapes:
+            self.addLabel(shape)
         self.setDirty()
 
     def moveShape(self):

+ 79 - 74
labelme/widgets/canvas.py

@@ -23,7 +23,7 @@ class Canvas(QtWidgets.QWidget):
     zoomRequest = QtCore.Signal(int, QtCore.QPoint)
     scrollRequest = QtCore.Signal(int, int)
     newShape = QtCore.Signal()
-    selectionChanged = QtCore.Signal(bool)
+    selectionChanged = QtCore.Signal(list)
     shapeMoved = QtCore.Signal()
     drawingPolygon = QtCore.Signal(bool)
     edgeSelected = QtCore.Signal(bool)
@@ -43,8 +43,8 @@ class Canvas(QtWidgets.QWidget):
         self.shapes = []
         self.shapesBackups = []
         self.current = None
-        self.selectedShape = None  # save the selected shape here
-        self.selectedShapeCopy = None
+        self.selectedShapes = []  # save the selected shapes here
+        self.selectedShapesCopy = []
         self.lineColor = QtGui.QColor(0, 0, 255)
         # self.line represents:
         #   - createMode == 'polygon': edge from last point to current
@@ -67,7 +67,7 @@ class Canvas(QtWidgets.QWidget):
         self._painter = QtGui.QPainter()
         self._cursor = CURSOR_DEFAULT
         # Menus:
-        self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
+        self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu(), QtWidgets.QMenu())
         # Set widget options.
         self.setMouseTracking(True)
         self.setFocusPolicy(QtCore.Qt.WheelFocus)
@@ -109,6 +109,9 @@ class Canvas(QtWidgets.QWidget):
         self.shapesBackups.pop()  # latest
         shapesBackup = self.shapesBackups.pop()
         self.shapes = shapesBackup
+        self.selectedShapes = []
+        for shape in self.shapes:
+            shape.selected = False
         self.repaint()
 
     def enterEvent(self, ev):
@@ -199,12 +202,13 @@ class Canvas(QtWidgets.QWidget):
 
         # Polygon copy moving.
         if QtCore.Qt.RightButton & ev.buttons():
-            if self.selectedShapeCopy and self.prevPoint:
+            if self.selectedShapesCopy and self.prevPoint:
                 self.overrideCursor(CURSOR_MOVE)
-                self.boundedMoveShape(self.selectedShapeCopy, pos)
+                self.boundedMoveShapes(self.selectedShapesCopy, pos)
                 self.repaint()
-            elif self.selectedShape:
-                self.selectedShapeCopy = self.selectedShape.copy()
+            elif self.selectedShapes:
+                self.selectedShapesCopy = \
+                    [s.copy() for s in self.selectedShapes]
                 self.repaint()
             return
 
@@ -215,9 +219,9 @@ class Canvas(QtWidgets.QWidget):
                 self.boundedMoveVertex(pos)
                 self.repaint()
                 self.movingShape = True
-            elif self.selectedShape and self.prevPoint:
+            elif self.selectedShapes and self.prevPoint:
                 self.overrideCursor(CURSOR_MOVE)
-                self.boundedMoveShape(self.selectedShape, pos)
+                self.boundedMoveShapes(self.selectedShapes, pos)
                 self.repaint()
                 self.movingShape = True
             return
@@ -314,49 +318,52 @@ class Canvas(QtWidgets.QWidget):
                         self.drawingPolygon.emit(True)
                         self.update()
             else:
-                self.selectShapePoint(pos)
+                group_mode = (int(ev.modifiers()) == QtCore.Qt.ControlModifier)
+                self.selectShapePoint(pos, multiple_selection_mode=group_mode)
                 self.prevPoint = pos
                 self.repaint()
         elif ev.button() == QtCore.Qt.RightButton and self.editing():
-            self.selectShapePoint(pos)
+            group_mode = (int(ev.modifiers()) == QtCore.Qt.ControlModifier)
+            self.selectShapePoint(pos, multiple_selection_mode=group_mode)
             self.prevPoint = pos
             self.repaint()
 
     def mouseReleaseEvent(self, ev):
         if ev.button() == QtCore.Qt.RightButton:
-            menu = self.menus[bool(self.selectedShapeCopy)]
+            menu = self.menus[min(len(self.selectedShapesCopy), 2)]
             self.restoreCursor()
-            if not menu.exec_(self.mapToGlobal(ev.pos()))\
-               and self.selectedShapeCopy:
+            if not menu.exec_(self.mapToGlobal(ev.pos())) \
+                    and self.selectedShapesCopy:
                 # Cancel the move by deleting the shadow copy.
-                self.selectedShapeCopy = None
+                self.selectedShapesCopy = []
                 self.repaint()
-        elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape:
+        elif ev.button() == QtCore.Qt.LeftButton and self.selectedShapes:
             self.overrideCursor(CURSOR_GRAB)
         if self.movingShape:
             self.storeShapes()
             self.shapeMoved.emit()
 
-    def endMove(self, copy=False):
-        assert self.selectedShape and self.selectedShapeCopy
-        shape = self.selectedShapeCopy
+    def endMove(self, copy):
+        assert self.selectedShapes and self.selectedShapesCopy
+        assert len(self.selectedShapesCopy) == len(self.selectedShapes)
         # del shape.fill_color
         # del shape.line_color
         if copy:
-            self.shapes.append(shape)
-            self.selectedShape.selected = False
-            self.selectedShape = shape
-            self.repaint()
+            for i, shape in enumerate(self.selectedShapesCopy):
+                self.shapes.append(shape)
+                self.selectedShapes[i].selected = False
+                self.selectedShapes[i] = shape
         else:
-            shape.label = self.selectedShape.label
-            self.deleteSelected()
-            self.shapes.append(shape)
+            for i, shape in enumerate(self.selectedShapesCopy):
+                self.selectedShapes[i].points = shape.points
+        self.selectedShapesCopy = []
+        self.repaint()
         self.storeShapes()
-        self.selectedShapeCopy = None
+        return True
 
     def hideBackroundShapes(self, value):
         self.hideBackround = value
-        if self.selectedShape:
+        if self.selectedShapes:
             # Only hide other shapes if there is a current selection.
             # Otherwise the user will not be able to select a shape.
             self.setHiding(True)
@@ -375,29 +382,29 @@ class Canvas(QtWidgets.QWidget):
             self.current.popPoint()
             self.finalise()
 
-    def selectShape(self, shape):
-        self.deSelectShape()
-        shape.selected = True
-        self.selectedShape = shape
+    def selectShapes(self, shapes):
         self.setHiding()
-        self.selectionChanged.emit(True)
+        self.selectionChanged.emit(shapes)
         self.update()
 
-    def selectShapePoint(self, point):
+    def selectShapePoint(self, point, multiple_selection_mode):
         """Select the first shape created which contains this point."""
-        self.deSelectShape()
         if self.selectedVertex():  # A vertex is marked for selection.
             index, shape = self.hVertex, self.hShape
             shape.highlightVertex(index, shape.MOVE_VERTEX)
-            return
-        for shape in reversed(self.shapes):
-            if self.isVisible(shape) and shape.containsPoint(point):
-                shape.selected = True
-                self.selectedShape = shape
-                self.calculateOffsets(shape, point)
-                self.setHiding()
-                self.selectionChanged.emit(True)
-                return
+        else:
+            for shape in reversed(self.shapes):
+                if self.isVisible(shape) and shape.containsPoint(point):
+                    self.calculateOffsets(shape, point)
+                    self.setHiding()
+                    if multiple_selection_mode:
+                        if shape not in self.selectedShapes:
+                            self.selectionChanged.emit(
+                                self.selectedShapes + [shape])
+                    else:
+                        self.selectionChanged.emit([shape])
+                    return
+        self.deSelectShape()
 
     def calculateOffsets(self, shape, point):
         rect = shape.boundingRect()
@@ -414,7 +421,7 @@ class Canvas(QtWidgets.QWidget):
             pos = self.intersectionPoint(point, pos)
         shape.moveVertexBy(index, pos - point)
 
-    def boundedMoveShape(self, shape, pos):
+    def boundedMoveShapes(self, shapes, pos):
         if self.outOfPixmap(pos):
             return False  # No need to move
         o1 = pos + self.offsets[0]
@@ -428,51 +435,48 @@ class Canvas(QtWidgets.QWidget):
         # relative to the shape, but also results 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.selectedShape, pos)
+        # self.calculateOffsets(self.selectedShapes, pos)
         dp = pos - self.prevPoint
         if dp:
-            shape.moveBy(dp)
+            for shape in shapes:
+                shape.moveBy(dp)
             self.prevPoint = pos
             return True
         return False
 
     def deSelectShape(self):
-        if self.selectedShape:
-            self.selectedShape.selected = False
-            self.selectedShape = None
+        if self.selectedShapes:
             self.setHiding(False)
-            self.selectionChanged.emit(False)
+            self.selectionChanged.emit([])
             self.update()
 
     def deleteSelected(self):
-        if self.selectedShape:
-            shape = self.selectedShape
-            self.shapes.remove(self.selectedShape)
+        deleted_shapes = []
+        if self.selectedShapes:
+            for shape in self.selectedShapes:
+                self.shapes.remove(shape)
+                deleted_shapes.append(shape)
             self.storeShapes()
-            self.selectedShape = None
+            self.selectedShapes = []
             self.update()
-            return shape
+        return deleted_shapes
 
-    def copySelectedShape(self):
-        if self.selectedShape:
-            shape = self.selectedShape.copy()
-            self.deSelectShape()
-            self.shapes.append(shape)
-            self.storeShapes()
-            shape.selected = True
-            self.selectedShape = shape
-            self.boundedShiftShape(shape)
-            return shape
+    def copySelectedShapes(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 boundedShiftShape(self, shape):
+    def boundedShiftShapes(self, shapes):
         # Try to move in one direction, and if it fails in another.
         # Give up if both fail.
-        point = shape[0]
+        point = shapes[0][0]
         offset = QtCore.QPoint(2.0, 2.0)
-        self.calculateOffsets(shape, point)
+        self.offsets = QtCore.QPoint(), QtCore.QPoint()
         self.prevPoint = point
-        if not self.boundedMoveShape(shape, point - offset):
-            self.boundedMoveShape(shape, point + offset)
+        if not self.boundedMoveShapes(shapes, point - offset):
+            self.boundedMoveShapes(shapes, point + offset)
 
     def paintEvent(self, event):
         if not self.pixmap:
@@ -497,8 +501,9 @@ class Canvas(QtWidgets.QWidget):
         if self.current:
             self.current.paint(p)
             self.line.paint(p)
-        if self.selectedShapeCopy:
-            self.selectedShapeCopy.paint(p)
+        if self.selectedShapesCopy:
+            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):

+ 1 - 0
labelme/widgets/label_qlist_widget.py

@@ -7,6 +7,7 @@ class LabelQListWidget(QtWidgets.QListWidget):
         super(LabelQListWidget, self).__init__(*args, **kwargs)
         self.canvas = None
         self.itemsToShapes = []
+        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
 
     def get_shape_from_item(self, item):
         for index, (item_, shape) in enumerate(self.itemsToShapes):