Browse Source

Create canvas.py

LaraStuStu 5 years ago
parent
commit
dd53e325a8
1 changed files with 713 additions and 0 deletions
  1. 713 0
      DataAnnotation/labelme/labelme/widgets/canvas.py

+ 713 - 0
DataAnnotation/labelme/labelme/widgets/canvas.py

@@ -0,0 +1,713 @@
+from qtpy import QtCore
+from qtpy import QtGui
+from qtpy import QtWidgets
+
+from labelme import QT5
+from labelme.shape import Shape
+import labelme.utils
+
+
+# TODO(unknown):
+# - [maybe] Find optimal epsilon value.
+
+
+CURSOR_DEFAULT = QtCore.Qt.ArrowCursor
+CURSOR_POINT = QtCore.Qt.PointingHandCursor
+CURSOR_DRAW = QtCore.Qt.CrossCursor
+CURSOR_MOVE = QtCore.Qt.ClosedHandCursor
+CURSOR_GRAB = QtCore.Qt.OpenHandCursor
+
+
+class Canvas(QtWidgets.QWidget):
+
+    zoomRequest = QtCore.Signal(int, QtCore.QPoint)
+    scrollRequest = QtCore.Signal(int, int)
+    newShape = QtCore.Signal()
+    selectionChanged = QtCore.Signal(list)
+    shapeMoved = QtCore.Signal()
+    drawingPolygon = QtCore.Signal(bool)
+    edgeSelected = QtCore.Signal(bool)
+
+    CREATE, EDIT = 0, 1
+
+    # polygon, rectangle, line, or point
+    _createMode = 'polygon'
+
+    _fill_drawing = False
+
+    def __init__(self, *args, **kwargs):
+        self.epsilon = kwargs.pop('epsilon', 10.0)
+        super(Canvas, self).__init__(*args, **kwargs)
+        # Initialise local state.
+        self.mode = self.EDIT
+        self.shapes = []
+        self.shapesBackups = []
+        self.current = 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
+        #   - createMode == 'rectangle': diagonal line of the rectangle
+        #   - createMode == 'line': the line
+        #   - createMode == 'point': the point
+        self.line = Shape(line_color=self.lineColor)
+        self.prevPoint = QtCore.QPoint()
+        self.prevMovePoint = QtCore.QPoint()
+        self.offsets = QtCore.QPoint(), QtCore.QPoint()
+        self.scale = 1.0
+        self.pixmap = QtGui.QPixmap()
+        self.visible = {}
+        self._hideBackround = False
+        self.hideBackround = False
+        self.hShape = None
+        self.hVertex = None
+        self.hEdge = None
+        self.movingShape = False
+        self._painter = QtGui.QPainter()
+        self._cursor = CURSOR_DEFAULT
+        # Menus:
+        # 0: right-click without selection and dragging of shapes
+        # 1: right-click with selection and dragging of shapes
+        self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu())
+        # Set widget options.
+        self.setMouseTracking(True)
+        self.setFocusPolicy(QtCore.Qt.WheelFocus)
+
+    def fillDrawing(self):
+        return self._fill_drawing
+
+    def setFillDrawing(self, value):
+        self._fill_drawing = value
+
+    @property
+    def createMode(self):
+        return self._createMode
+
+    @createMode.setter
+    def createMode(self, value):
+        if value not in ['polygon', 'rectangle', 'circle',
+           'line', 'point', 'linestrip']:
+            raise ValueError('Unsupported createMode: %s' % value)
+        self._createMode = value
+
+    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.selectedShapes = []
+        for shape in self.shapes:
+            shape.selected = False
+        self.repaint()
+
+    def enterEvent(self, ev):
+        self.overrideCursor(self._cursor)
+
+    def leaveEvent(self, ev):
+        self.restoreCursor()
+
+    def focusOutEvent(self, ev):
+        self.restoreCursor()
+
+    def isVisible(self, shape):
+        return self.visible.get(shape, True)
+
+    def drawing(self):
+        return self.mode == self.CREATE
+
+    def editing(self):
+        return self.mode == self.EDIT
+
+    def setEditing(self, value=True):
+        self.mode = self.EDIT if value else self.CREATE
+        if not value:  # Create
+            self.unHighlight()
+            self.deSelectShape()
+
+    def unHighlight(self):
+        if self.hShape:
+            self.hShape.highlightClear()
+        self.hVertex = self.hShape = None
+
+    def selectedVertex(self):
+        return self.hVertex is not None
+
+    def mouseMoveEvent(self, ev):
+        """Update line with last point and current coordinates."""
+        try:
+            if QT5:
+                pos = self.transformPos(ev.localPos())
+            else:
+                pos = self.transformPos(ev.posF())
+        except AttributeError:
+            return
+
+        self.prevMovePoint = pos
+        self.restoreCursor()
+
+        # Polygon drawing.
+        if self.drawing():
+            self.line.shape_type = self.createMode
+
+            self.overrideCursor(CURSOR_DRAW)
+            if not self.current:
+                return
+
+            color = self.lineColor
+            if self.outOfPixmap(pos):
+                # 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 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]
+                color = self.current.line_color
+                self.overrideCursor(CURSOR_POINT)
+                self.current.highlightVertex(0, Shape.NEAR_VERTEX)
+            if self.createMode in ['polygon', 'linestrip']:
+                self.line[0] = self.current[-1]
+                self.line[1] = pos
+            elif self.createMode == 'rectangle':
+                self.line.points = [self.current[0], pos]
+                self.line.close()
+            elif self.createMode == 'circle':
+                self.line.points = [self.current[0], pos]
+                self.line.shape_type = "circle"
+            elif self.createMode == 'line':
+                self.line.points = [self.current[0], pos]
+                self.line.close()
+            elif self.createMode == 'point':
+                self.line.points = [self.current[0]]
+                self.line.close()
+            self.line.line_color = color
+            self.repaint()
+            self.current.highlightClear()
+            return
+
+        # Polygon copy moving.
+        if QtCore.Qt.RightButton & ev.buttons():
+            if self.selectedShapesCopy and self.prevPoint:
+                self.overrideCursor(CURSOR_MOVE)
+                self.boundedMoveShapes(self.selectedShapesCopy, pos)
+                self.repaint()
+            elif self.selectedShapes:
+                self.selectedShapesCopy = \
+                    [s.copy() for s in self.selectedShapes]
+                self.repaint()
+            return
+
+        # Polygon/Vertex moving.
+        self.movingShape = False
+        if QtCore.Qt.LeftButton & ev.buttons():
+            if self.selectedVertex():
+                self.boundedMoveVertex(pos)
+                self.repaint()
+                self.movingShape = True
+            elif self.selectedShapes and self.prevPoint:
+                self.overrideCursor(CURSOR_MOVE)
+                self.boundedMoveShapes(self.selectedShapes, pos)
+                self.repaint()
+                self.movingShape = True
+            return
+
+        # Just hovering over the canvas, 2 posibilities:
+        # - Highlight shapes
+        # - Highlight vertex
+        # Update shape/vertex fill and tooltip value accordingly.
+        self.setToolTip("Image")
+        for shape in reversed([s for s in self.shapes if self.isVisible(s)]):
+            # Look for a nearby vertex to highlight. If that fails,
+            # check if we happen to be inside a shape.
+            index = shape.nearestVertex(pos, self.epsilon / self.scale)
+            index_edge = shape.nearestEdge(pos, self.epsilon / self.scale)
+            if index is not None:
+                if self.selectedVertex():
+                    self.hShape.highlightClear()
+                self.hVertex = index
+                self.hShape = shape
+                self.hEdge = index_edge
+                shape.highlightVertex(index, shape.MOVE_VERTEX)
+                self.overrideCursor(CURSOR_POINT)
+                self.setToolTip("Click & drag to move point")
+                self.setStatusTip(self.toolTip())
+                self.update()
+                break
+            elif shape.containsPoint(pos):
+                if self.selectedVertex():
+                    self.hShape.highlightClear()
+                self.hVertex = None
+                self.hShape = shape
+                self.hEdge = index_edge
+                self.setToolTip(
+                    "Click & drag to move shape '%s'" % shape.label)
+                self.setStatusTip(self.toolTip())
+                self.overrideCursor(CURSOR_GRAB)
+                self.update()
+                break
+        else:  # Nothing found, clear highlights, reset state.
+            if self.hShape:
+                self.hShape.highlightClear()
+                self.update()
+            self.hVertex, self.hShape, self.hEdge = None, None, None
+        self.edgeSelected.emit(self.hEdge is not None)
+
+    def addPointToEdge(self):
+        if (self.hShape is None and
+                self.hEdge is None and
+                self.prevMovePoint is None):
+            return
+        shape = self.hShape
+        index = self.hEdge
+        point = self.prevMovePoint
+        shape.insertPoint(index, point)
+        shape.highlightVertex(index, shape.MOVE_VERTEX)
+        self.hShape = shape
+        self.hVertex = index
+        self.hEdge = None
+
+    def mousePressEvent(self, ev):
+        if QT5:
+            pos = self.transformPos(ev.localPos())
+        else:
+            pos = self.transformPos(ev.posF())
+        if ev.button() == QtCore.Qt.LeftButton:
+            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']:
+                        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 not self.outOfPixmap(pos):
+                    # Create new shape.
+                    self.current = Shape(shape_type=self.createMode)
+                    self.current.addPoint(pos)
+                    if self.createMode == 'point':
+                        self.finalise()
+                    else:
+                        if self.createMode == 'circle':
+                            self.current.shape_type = 'circle'
+                        self.line.points = [pos, pos]
+                        self.setHiding()
+                        self.drawingPolygon.emit(True)
+                        self.update()
+            else:
+                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():
+            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[len(self.selectedShapesCopy) > 0]
+            self.restoreCursor()
+            if not menu.exec_(self.mapToGlobal(ev.pos())) \
+                    and self.selectedShapesCopy:
+                # Cancel the move by deleting the shadow copy.
+                self.selectedShapesCopy = []
+                self.repaint()
+        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):
+        assert self.selectedShapes and self.selectedShapesCopy
+        assert len(self.selectedShapesCopy) == len(self.selectedShapes)
+        # del shape.fill_color
+        # del shape.line_color
+        if copy:
+            for i, shape in enumerate(self.selectedShapesCopy):
+                self.shapes.append(shape)
+                self.selectedShapes[i].selected = False
+                self.selectedShapes[i] = shape
+        else:
+            for i, shape in enumerate(self.selectedShapesCopy):
+                self.selectedShapes[i].points = shape.points
+        self.selectedShapesCopy = []
+        self.repaint()
+        self.storeShapes()
+        return True
+
+    def hideBackroundShapes(self, value):
+        self.hideBackround = value
+        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)
+            self.repaint()
+
+    def setHiding(self, enable=True):
+        self._hideBackround = self.hideBackround if enable else False
+
+    def canCloseShape(self):
+        return self.drawing() and self.current and len(self.current) > 2
+
+    def mouseDoubleClickEvent(self, ev):
+        # We need at least 4 points here, since the mousePress handler
+        # adds an extra one before this handler is called.
+        if self.canCloseShape() and len(self.current) > 3:
+            self.current.popPoint()
+            self.finalise()
+
+    def selectShapes(self, shapes):
+        self.setHiding()
+        self.selectionChanged.emit(shapes)
+        self.update()
+
+    def selectShapePoint(self, point, multiple_selection_mode):
+        """Select the first shape created which contains this point."""
+        if self.selectedVertex():  # A vertex is marked for selection.
+            index, shape = self.hVertex, self.hShape
+            shape.highlightVertex(index, shape.MOVE_VERTEX)
+        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()
+        x1 = rect.x() - point.x()
+        y1 = rect.y() - point.y()
+        x2 = (rect.x() + rect.width() - 1) - point.x()
+        y2 = (rect.y() + rect.height() - 1) - point.y()
+        self.offsets = QtCore.QPoint(x1, y1), QtCore.QPoint(x2, y2)
+
+    def boundedMoveVertex(self, pos):
+        index, shape = self.hVertex, self.hShape
+        point = shape[index]
+        if self.outOfPixmap(pos):
+            pos = self.intersectionPoint(point, pos)
+        shape.moveVertexBy(index, pos - point)
+
+    def boundedMoveShapes(self, shapes, pos):
+        if self.outOfPixmap(pos):
+            return False  # No need to move
+        o1 = pos + self.offsets[0]
+        if self.outOfPixmap(o1):
+            pos -= QtCore.QPoint(min(0, o1.x()), min(0, o1.y()))
+        o2 = pos + self.offsets[1]
+        if self.outOfPixmap(o2):
+            pos += QtCore.QPoint(min(0, self.pixmap.width() - o2.x()),
+                                 min(0, self.pixmap.height() - o2.y()))
+        # XXX: The next line tracks the new position of the cursor
+        # 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.selectedShapes, pos)
+        dp = pos - self.prevPoint
+        if dp:
+            for shape in shapes:
+                shape.moveBy(dp)
+            self.prevPoint = pos
+            return True
+        return False
+
+    def deSelectShape(self):
+        if self.selectedShapes:
+            self.setHiding(False)
+            self.selectionChanged.emit([])
+            self.update()
+
+    def deleteSelected(self):
+        deleted_shapes = []
+        if self.selectedShapes:
+            for shape in self.selectedShapes:
+                self.shapes.remove(shape)
+                deleted_shapes.append(shape)
+            self.storeShapes()
+            self.selectedShapes = []
+            self.update()
+        return deleted_shapes
+
+    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 boundedShiftShapes(self, shapes):
+        # Try to move in one direction, and if it fails in another.
+        # Give up if both fail.
+        point = shapes[0][0]
+        offset = QtCore.QPoint(2.0, 2.0)
+        self.offsets = QtCore.QPoint(), QtCore.QPoint()
+        self.prevPoint = point
+        if not self.boundedMoveShapes(shapes, point - offset):
+            self.boundedMoveShapes(shapes, point + offset)
+
+    def paintEvent(self, event):
+        if not self.pixmap:
+            return super(Canvas, self).paintEvent(event)
+
+        p = self._painter
+        p.begin(self)
+        p.setRenderHint(QtGui.QPainter.Antialiasing)
+        p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing)
+        p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform)
+
+        p.scale(self.scale, self.scale)
+        p.translate(self.offsetToCenter())
+
+        p.drawPixmap(0, 0, self.pixmap)
+        Shape.scale = self.scale
+        for shape in self.shapes:
+            if (shape.selected or not self._hideBackround) and \
+                    self.isVisible(shape):
+                shape.fill = shape.selected or shape == self.hShape
+                shape.paint(p)
+        if self.current:
+            self.current.paint(p)
+            self.line.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):
+            drawing_shape = self.current.copy()
+            drawing_shape.addPoint(self.line[1])
+            drawing_shape.fill = True
+            drawing_shape.fill_color.setAlpha(64)
+            drawing_shape.paint(p)
+
+        p.end()
+
+    def transformPos(self, point):
+        """Convert from widget-logical coordinates to painter-logical ones."""
+        return point / self.scale - self.offsetToCenter()
+
+    def offsetToCenter(self):
+        s = self.scale
+        area = super(Canvas, self).size()
+        w, h = self.pixmap.width() * s, self.pixmap.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.QPoint(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)
+
+    def finalise(self):
+        assert self.current
+        self.current.close()
+        self.shapes.append(self.current)
+        self.storeShapes()
+        self.current = None
+        self.setHiding(False)
+        self.newShape.emit()
+        self.update()
+
+    def closeEnough(self, p1, p2):
+        # d = distance(p1 - p2)
+        # m = (p1-p2).manhattanLength()
+        # print "d %.2f, m %d, %.2f" % (d, m, d - m)
+        # divide by scale to allow more precision when zoomed in
+        return labelme.utils.distance(p1 - p2) < (self.epsilon / self.scale)
+
+    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()
+        points = [(0, 0),
+                  (size.width() - 1, 0),
+                  (size.width() - 1, size.height() - 1),
+                  (0, size.height() - 1)]
+        x1, y1 = p1.x(), p1.y()
+        x2, y2 = p2.x(), p2.y()
+        d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points))
+        x3, y3 = points[i]
+        x4, y4 = points[(i + 1) % 4]
+        if (x, y) == (x1, y1):
+            # Handle cases where previous point is on one of the edges.
+            if x3 == x4:
+                return QtCore.QPoint(x3, min(max(0, y2), max(y3, y4)))
+            else:  # y3 == y4
+                return QtCore.QPoint(min(max(0, x2), max(x3, x4)), y3)
+        return QtCore.QPoint(x, y)
+
+    def intersectingEdges(self, point1, point2, points):
+        """Find intersecting edges.
+
+        For each edge formed by `points', yield the intersection
+        with the line segment `(x1,y1) - (x2,y2)`, if it exists.
+        Also return the distance of `(x2,y2)' to the middle of the
+        edge along with its index, so that the one closest can be chosen.
+        """
+        (x1, y1) = point1
+        (x2, y2) = point2
+        for i in range(4):
+            x3, y3 = points[i]
+            x4, y4 = points[(i + 1) % 4]
+            denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1)
+            nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)
+            nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3)
+            if denom == 0:
+                # This covers two cases:
+                #   nua == nub == 0: Coincident
+                #   otherwise: Parallel
+                continue
+            ua, ub = nua / denom, nub / denom
+            if 0 <= ua <= 1 and 0 <= ub <= 1:
+                x = x1 + ua * (x2 - x1)
+                y = y1 + ua * (y2 - y1)
+                m = QtCore.QPoint((x3 + x4) / 2, (y3 + y4) / 2)
+                d = labelme.utils.distance(m - QtCore.QPoint(x2, y2))
+                yield d, i, (x, y)
+
+    # These two, along with a call to adjustSize are required for the
+    # scroll area.
+    def sizeHint(self):
+        return self.minimumSizeHint()
+
+    def minimumSizeHint(self):
+        if self.pixmap:
+            return self.scale * self.pixmap.size()
+        return super(Canvas, self).minimumSizeHint()
+
+    def wheelEvent(self, ev):
+        if QT5:
+            mods = ev.modifiers()
+            delta = ev.angleDelta()
+            if QtCore.Qt.ControlModifier == int(mods):
+                # with Ctrl/Command key
+                # zoom
+                self.zoomRequest.emit(delta.y(), ev.pos())
+            else:
+                # scroll
+                self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal)
+                self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical)
+        else:
+            if ev.orientation() == QtCore.Qt.Vertical:
+                mods = ev.modifiers()
+                if QtCore.Qt.ControlModifier == int(mods):
+                    # with Ctrl/Command key
+                    self.zoomRequest.emit(ev.delta(), ev.pos())
+                else:
+                    self.scrollRequest.emit(
+                        ev.delta(),
+                        QtCore.Qt.Horizontal
+                        if (QtCore.Qt.ShiftModifier == int(mods))
+                        else QtCore.Qt.Vertical)
+            else:
+                self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal)
+        ev.accept()
+
+    def keyPressEvent(self, ev):
+        key = ev.key()
+        if key == QtCore.Qt.Key_Escape and self.current:
+            self.current = None
+            self.drawingPolygon.emit(False)
+            self.update()
+        elif key == QtCore.Qt.Key_Return and self.canCloseShape():
+            self.finalise()
+
+    def setLastLabel(self, text, flags):
+        assert text
+        self.shapes[-1].label = text
+        self.shapes[-1].flags = flags
+        self.shapesBackups.pop()
+        self.storeShapes()
+        return self.shapes[-1]
+
+    def undoLastLine(self):
+        assert self.shapes
+        self.current = self.shapes.pop()
+        self.current.setOpen()
+        if self.createMode in ['polygon', 'linestrip']:
+            self.line.points = [self.current[-1], self.current[0]]
+        elif self.createMode in ['rectangle', 'line', 'circle']:
+            self.current.points = self.current.points[0:1]
+        elif self.createMode == 'point':
+            self.current = None
+        self.drawingPolygon.emit(True)
+
+    def undoLastPoint(self):
+        if not self.current or self.current.isClosed():
+            return
+        self.current.popPoint()
+        if len(self.current) > 0:
+            self.line[0] = self.current[-1]
+        else:
+            self.current = None
+            self.drawingPolygon.emit(False)
+        self.repaint()
+
+    def loadPixmap(self, pixmap):
+        self.pixmap = pixmap
+        self.shapes = []
+        self.repaint()
+
+    def loadShapes(self, shapes, replace=True):
+        if replace:
+            self.shapes = list(shapes)
+        else:
+            self.shapes.extend(shapes)
+        self.storeShapes()
+        self.current = None
+        self.repaint()
+
+    def setShapeVisible(self, shape, value):
+        self.visible[shape] = value
+        self.repaint()
+
+    def overrideCursor(self, cursor):
+        self.restoreCursor()
+        self._cursor = cursor
+        QtWidgets.QApplication.setOverrideCursor(cursor)
+
+    def restoreCursor(self):
+        QtWidgets.QApplication.restoreOverrideCursor()
+
+    def resetState(self):
+        self.restoreCursor()
+        self.pixmap = None
+        self.shapesBackups = []
+        self.update()