Explorar o código

Undo shape edit by Ctrl+Z

Kentaro Wada %!s(int64=7) %!d(string=hai) anos
pai
achega
3f95cd7739
Modificáronse 3 ficheiros con 78 adicións e 16 borrados
  1. 39 14
      labelme/app.py
  2. 38 2
      labelme/canvas.py
  3. 1 0
      labelme/config/default_config.yaml

+ 39 - 14
labelme/app.py

@@ -116,7 +116,7 @@ class LabelQListWidget(QtWidgets.QListWidget):
         if self.canvas is None:
             raise RuntimeError('self.canvas must be set beforehand.')
         self.parent.setDirty()
-        self.canvas.shapes = self.shapes
+        self.canvas.loadShapes(shapes)
 
     @property
     def shapes(self):
@@ -270,9 +270,12 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
                       'Create a duplicate of the selected polygon',
                       enabled=False)
         undoLastPoint = action('Undo last point', self.canvas.undoLastPoint,
-                               shortcuts['undo_last_point'], 'undoLastPoint',
+                               shortcuts['undo_last_point'], 'undo',
                                'Undo last drawn point', enabled=False)
 
+        undo = action('Undo', self.undoShapeEdit, shortcuts['undo'], 'undo',
+                      'Undo last add and edit of shape', enabled=False)
+
         hideAll = action('&Hide\nPolygons',
                          functools.partial(self.togglePolygons, False),
                          icon='eye', tip='Hide all polygons', enabled=False)
@@ -349,7 +352,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             save=save, saveAs=saveAs, open=open_, close=close,
             lineColor=color1, fillColor=color2,
             delete=delete, edit=edit, copy=copy,
-            undoLastPoint=undoLastPoint,
+            undoLastPoint=undoLastPoint, undo=undo,
             createMode=createMode, editMode=editMode,
             shapeLineColor=shapeLineColor, shapeFillColor=shapeFillColor,
             zoom=zoom, zoomIn=zoomIn, zoomOut=zoomOut, zoomOrg=zoomOrg,
@@ -357,12 +360,12 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             zoomActions=zoomActions,
             fileMenuActions=(open_, opendir, save, saveAs, close, quit),
             tool=(),
-            editMenu=(edit, copy, delete, None, undoLastPoint,
+            editMenu=(edit, copy, delete, None, undo, undoLastPoint,
                       None, color1, color2),
             menu=(
                 createMode, editMode, edit, copy,
                 delete, shapeLineColor, shapeFillColor,
-                undoLastPoint,
+                undo, undoLastPoint,
             ),
             onLoadActive=(close, createMode, editMode),
             onShapesPresent=(saveAs, hideAll, showAll),
@@ -397,7 +400,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
         self.tools = self.toolbar('Tools')
         self.actions.tool = (
             open_, opendir, openNextImg, openPrevImg, save,
-            None, createMode, copy, delete, editMode, None,
+            None, createMode, copy, delete, editMode, undo, None,
             zoomIn, zoom, zoomOut, fitWindow, fitWidth)
 
         self.statusBar().showMessage('%s started.' % __appname__)
@@ -522,6 +525,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             return
         self.dirty = True
         self.actions.save.setEnabled(True)
+        self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
         title = __appname__
         if self.filename is not None:
             title = '{} - {}*'.format(title, self.filename)
@@ -573,6 +577,13 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
 
     # Callbacks
 
+    def undoShapeEdit(self):
+        self.canvas.restoreShape()
+        self.labelList.clear()
+        self.uniqLabelList.clear()
+        self.loadShapes(self.canvas.shapes)
+        self.actions.undo.setEnabled(self.canvas.isShapeRestorable)
+
     def tutorial(self):
         url = 'https://github.com/wkentaro/labelme/tree/master/examples/tutorial'  # NOQA
         webbrowser.open(url)
@@ -584,6 +595,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
         """
         self.actions.editMode.setEnabled(not drawing)
         self.actions.undoLastPoint.setEnabled(drawing)
+        self.actions.undo.setEnabled(not drawing)
 
     def toggleDrawMode(self, edit=True):
         self.canvas.setEditing(edit)
@@ -698,6 +710,11 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
         item = self.labelList.get_item_from_shape(shape)
         self.labelList.takeItem(self.labelList.row(item))
 
+    def loadShapes(self, shapes):
+        for shape in shapes:
+            self.addLabel(shape)
+        self.canvas.loadShapes(shapes)
+
     def loadLabels(self, shapes):
         s = []
         for label, points, line_color, fill_color in shapes:
@@ -706,12 +723,11 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
                 shape.addPoint(QtCore.QPointF(x, y))
             shape.close()
             s.append(shape)
-            self.addLabel(shape)
             if line_color:
                 shape.line_color = QtGui.QColor(*line_color)
             if fill_color:
                 shape.fill_color = QtGui.QColor(*fill_color)
-        self.canvas.loadShapes(s)
+        self.loadShapes(s)
 
     def saveLabels(self, filename):
         lf = LabelFile()
@@ -780,9 +796,12 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             text = None
         if text is None:
             self.canvas.undoLastLine()
+            self.canvas.shapesBackups.pop()
         else:
             self.addLabel(self.canvas.setLastLabel(text))
             self.actions.editMode.setEnabled(True)
+            self.actions.undoLastPoint.setEnabled(False)
+            self.actions.undo.setEnabled(True)
             self.setDirty()
 
     def scrollRequest(self, delta, orientation):
@@ -835,6 +854,13 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
 
     def loadFile(self, filename=None):
         """Load the specified file, or the last opened file if None."""
+        # changing fileListWidget loads file
+        if (filename in self.imageList and
+                self.fileListWidget.currentRow() !=
+                self.imageList.index(filename)):
+            self.fileListWidget.setCurrentRow(self.imageList.index(filename))
+            return
+
         self.resetState()
         self.canvas.setEnabled(False)
         if filename is None:
@@ -904,8 +930,6 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
         self.addRecentFile(self.filename)
         self.toggleActions(True)
         self.status("Loaded %s" % os.path.basename(str(filename)))
-        if filename in self.imageList:
-            self.fileListWidget.setCurrentRow(self.imageList.index(filename))
         return True
 
     def resizeEvent(self, event):
@@ -977,7 +1001,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             if filename:
                 self.loadFile(filename)
 
-    def openNextImg(self, _value=False):
+    def openNextImg(self, _value=False, load=True):
         if not self.mayContinue():
             return
 
@@ -991,9 +1015,10 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
             currIndex = self.imageList.index(self.filename)
             if currIndex + 1 < len(self.imageList):
                 filename = self.imageList[currIndex + 1]
+        self.filename = filename
 
-        if filename:
-            self.loadFile(filename)
+        if self.filename and load:
+            self.loadFile(self.filename)
 
     def openFile(self, _value=False):
         if not self.mayContinue():
@@ -1188,7 +1213,7 @@ class MainWindow(QtWidgets.QMainWindow, WindowMixin):
         for imgPath in self.scanAllImages(dirpath):
             item = QtWidgets.QListWidgetItem(imgPath)
             self.fileListWidget.addItem(item)
-        self.openNextImg()
+        self.openNextImg(load=False)
 
     def scanAllImages(self, folderPath):
         extensions = ['.%s' % fmt.data().decode("ascii").lower()

+ 38 - 2
labelme/canvas.py

@@ -41,6 +41,7 @@ class Canvas(QtWidgets.QWidget):
         # Initialise local state.
         self.mode = self.EDIT
         self.shapes = []
+        self.shapesBackups = []
         self.current = None
         self.selectedShape = None  # save the selected shape here
         self.selectedShapeCopy = None
@@ -63,6 +64,29 @@ class Canvas(QtWidgets.QWidget):
         self.setMouseTracking(True)
         self.setFocusPolicy(QtCore.Qt.WheelFocus)
 
+    def storeShapes(self):
+        shapesBackup = []
+        for shape in self.shapes:
+            shapesBackup.append(shape.copy())
+        if len(self.shapesBackups) >= 10:
+            self.shapesBackups = self.shapesBackups[-9:]
+        self.shapesBackups.append(shapesBackup)
+
+    @property
+    def isShapeRestorable(self):
+        if len(self.shapesBackups) < 2:
+            return False
+        return True
+
+    def restoreShape(self):
+        if not self.isShapeRestorable:
+            return
+        self.shapesBackups.pop()  # latest
+        shapesBackup = self.shapesBackups.pop()
+        self.shapes = shapesBackup
+        self.storeShapes()
+        self.repaint()
+
     def enterEvent(self, ev):
         self.overrideCursor(self._cursor)
 
@@ -140,16 +164,17 @@ class Canvas(QtWidgets.QWidget):
             return
 
         # Polygon/Vertex moving.
+        self.movingShape = False
         if QtCore.Qt.LeftButton & ev.buttons():
             if self.selectedVertex():
                 self.boundedMoveVertex(pos)
-                self.shapeMoved.emit()
                 self.repaint()
+                self.movingShape = True
             elif self.selectedShape and self.prevPoint:
                 self.overrideCursor(CURSOR_MOVE)
                 self.boundedMoveShape(self.selectedShape, pos)
-                self.shapeMoved.emit()
                 self.repaint()
+                self.movingShape = True
             return
 
         # Just hovering over the canvas, 2 posibilities:
@@ -230,6 +255,9 @@ class Canvas(QtWidgets.QWidget):
                 self.repaint()
         elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape:
             self.overrideCursor(CURSOR_GRAB)
+        if self.movingShape:
+            self.storeShapes()
+            self.shapeMoved.emit()
 
     def endMove(self, copy=False):
         assert self.selectedShape and self.selectedShapeCopy
@@ -245,6 +273,7 @@ class Canvas(QtWidgets.QWidget):
             shape.label = self.selectedShape.label
             self.deleteSelected()
             self.shapes.append(shape)
+        self.storeShapes()
         self.selectedShapeCopy = None
 
     def hideBackroundShapes(self, value):
@@ -341,6 +370,7 @@ class Canvas(QtWidgets.QWidget):
         if self.selectedShape:
             shape = self.selectedShape
             self.shapes.remove(self.selectedShape)
+            self.storeShapes()
             self.selectedShape = None
             self.update()
             return shape
@@ -350,6 +380,7 @@ class Canvas(QtWidgets.QWidget):
             shape = self.selectedShape.copy()
             self.deSelectShape()
             self.shapes.append(shape)
+            self.storeShapes()
             shape.selected = True
             self.selectedShape = shape
             self.boundedShiftShape(shape)
@@ -417,6 +448,7 @@ class Canvas(QtWidgets.QWidget):
         assert self.current
         self.current.close()
         self.shapes.append(self.current)
+        self.storeShapes()
         self.current = None
         self.setHiding(False)
         self.newShape.emit()
@@ -529,6 +561,8 @@ class Canvas(QtWidgets.QWidget):
     def setLastLabel(self, text):
         assert text
         self.shapes[-1].label = text
+        self.shapesBackups.pop()
+        self.storeShapes()
         return self.shapes[-1]
 
     def undoLastLine(self):
@@ -556,6 +590,7 @@ class Canvas(QtWidgets.QWidget):
 
     def loadShapes(self, shapes):
         self.shapes = list(shapes)
+        self.storeShapes()
         self.current = None
         self.repaint()
 
@@ -574,4 +609,5 @@ class Canvas(QtWidgets.QWidget):
     def resetState(self):
         self.restoreCursor()
         self.pixmap = None
+        self.shapesBackups = []
         self.update()

+ 1 - 0
labelme/config/default_config.yaml

@@ -19,6 +19,7 @@ shortcuts:
   edit_polygon: Ctrl+J
   delete_polygon: Delete
   duplicate_polygon: Ctrl+D
+  undo: Ctrl+Z
   undo_last_point: [Ctrl+Z, Backspace]
   edit_label: Ctrl+E
   edit_line_color: Ctrl+L