# Copyright (c) <2015-Present> Tzutalin # Copyright (C) 2013 MIT, Computer Science and Artificial Intelligence Laboratory. Bryan Russell, Antonio Torralba, # William T. Freeman. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and # associated documentation files (the "Software"), to deal in the Software without restriction, including without # limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the # Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: # The above copyright notice and this permission notice shall be included in all copies or substantial portions of # the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT # NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. import copy from PyQt5.QtCore import Qt, pyqtSignal, QPointF, QPoint from PyQt5.QtGui import QPainter, QBrush, QColor, QPixmap from PyQt5.QtWidgets import QWidget, QMenu, QApplication from libs.shape import Shape from libs.utils import distance CURSOR_DEFAULT = Qt.ArrowCursor CURSOR_POINT = Qt.PointingHandCursor CURSOR_DRAW = Qt.CrossCursor CURSOR_MOVE = Qt.ClosedHandCursor CURSOR_GRAB = Qt.OpenHandCursor class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() # selectionChanged = pyqtSignal(bool) selectionChanged = pyqtSignal(list) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT = list(range(2)) _fill_drawing = False # draw shadows epsilon = 5.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.shapes = [] self.shapesBackups = [] self.current = None self.selectedShapes = [] self.selectedShape = None # save the selected shape here self.selectedShapesCopy = [] self.drawingLineColor = QColor(0, 0, 255) self.drawingRectColor = QColor(0, 0, 255) self.line = Shape(line_color=self.drawingLineColor) self.prevPoint = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.0 self.pixmap = QPixmap() self.visible = {} self._hideBackround = False self.hideBackround = False self.hShape = None self.hVertex = None self._painter = QPainter() self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) self.verified = False self.drawSquare = False self.fourpoint = True # ADD self.pointnum = 0 self.movingShape = False self.selectCountShape = False #initialisation for panning self.pan_initial_pos = QPoint() #lockedshapes related self.lockedShapes = [] self.isInTheSameImage = False def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor 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() self.prevPoint = QPointF() self.repaint() 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.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText( 'X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) # ? if self.current: # Display annotation width and height while drawing currentWidth = abs(self.current[0].x() - pos.x()) currentHeight = abs(self.current[0].y() - pos.y()) self.parent().window().labelCoordinates.setText( 'Width: %d, Height: %d / X: %d; Y: %d' % (currentWidth, currentHeight, pos.x(), pos.y())) color = self.drawingLineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Clip the coordinates to 0 or max, # if they are outside the range [0, max] size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) elif len(self.current) > 1 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.drawSquare: self.line.points = [self.current[0], pos] self.line.close() elif self.fourpoint: self.line[0] = self.current[-1] self.line[1] = pos else: self.line[1] = pos # pos is the mouse's current position self.line.line_color = color self.prevPoint = QPointF() # ? self.current.highlightClear() else: self.prevPoint = pos self.repaint() return # Polygon copy moving. if Qt.RightButton & ev.buttons(): if self.selectedShapesCopy and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShapesCopy, pos) self.repaint() elif self.selectedShapes: self.selectedShapesCopy = [ s.copy() for s in self.selectedShapes ] self.repaint() return # Polygon/Vertex moving. if Qt.LeftButton & ev.buttons(): if self.selectedVertex(): self.boundedMoveVertex(pos) self.shapeMoved.emit() self.repaint() self.movingShape = True elif self.selectedShapes and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShapes, pos) self.shapeMoved.emit() self.repaint() self.movingShape = True else: #pan delta_x = pos.x() - self.pan_initial_pos.x() delta_y = pos.y() - self.pan_initial_pos.y() self.scrollRequest.emit(delta_x, Qt.Horizontal) self.scrollRequest.emit(delta_y, Qt.Vertical) self.update() 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) if index is not None: if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = index, shape 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, self.hShape = None, shape self.setToolTip( "Click & drag to move shape '%s'" % shape.label[0]) 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 = None, None self.overrideCursor(CURSOR_DEFAULT) def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing(): # self.handleDrawing(pos) # OLD if self.current: if self.fourpoint: # ADD IF # Add point to existing shape. # print('Adding points in mousePressEvent is ', self.line[1]) self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if self.current.isClosed(): # print('1111') self.finalise() elif self.drawSquare: assert len(self.current.points) == 1 self.current.points = self.line.points self.finalise() elif not self.outOfPixmap(pos): # Create new shape. self.current = Shape() self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() else: group_mode = int(ev.modifiers()) == Qt.ControlModifier self.selectShapePoint(pos, multiple_selection_mode=group_mode) self.prevPoint = pos self.pan_initial_pos = pos elif ev.button() == Qt.RightButton and self.editing(): group_mode = int(ev.modifiers()) == Qt.ControlModifier self.selectShapePoint(pos, multiple_selection_mode=group_mode) self.prevPoint = pos self.update() def mouseReleaseEvent(self, ev): if ev.button() == Qt.RightButton: menu = self.menus[bool(self.selectedShapesCopy)] self.restoreCursor() 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() == Qt.LeftButton and self.selectedShapes: if self.selectedVertex(): self.overrideCursor(CURSOR_POINT) else: self.overrideCursor(CURSOR_GRAB) elif ev.button() == Qt.LeftButton and not self.fourpoint: pos = self.transformPos(ev.pos()) if self.drawing(): self.handleDrawing(pos) else: #pan QApplication.restoreOverrideCursor() # ? if self.movingShape and self.hShape: if self.hShape in self.shapes: index = self.shapes.index(self.hShape) if ( self.shapesBackups[-1][index].points != self.shapes[index].points ): self.storeShapes() self.shapeMoved.emit() # connect to updateBoxlist in PPOCRLabel.py self.movingShape = False def endMove(self, copy=False): assert self.selectedShapes and self.selectedShapesCopy assert len(self.selectedShapesCopy) == len(self.selectedShapes) if copy: for i, shape in enumerate(self.selectedShapesCopy): shape.idx = len(self.shapes) # add current box index 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 handleDrawing(self, pos): if self.current and self.current.reachMaxPoints() is False: if self.fourpoint: targetPos = self.line[self.pointnum] self.current.addPoint(targetPos) print('current points in handleDrawing is ', self.line[self.pointnum]) self.update() if self.pointnum == 3: self.finalise() else: initPos = self.current[0] print('initPos', self.current[0]) minX = initPos.x() minY = initPos.y() targetPos = self.line[1] maxX = targetPos.x() maxY = targetPos.y() self.current.addPoint(QPointF(maxX, minY)) self.current.addPoint(targetPos) self.current.addPoint(QPointF(minX, maxY)) self.finalise() elif not self.outOfPixmap(pos): print('release') self.current = Shape() self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() 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: if not self.fourpoint: self.current.popPoint() self.finalise() def selectShapes(self, shapes): for s in shapes: s.seleted = True 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) return self.hVertex 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: # list 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()) - point.x() y2 = (rect.y() + rect.height()) - point.y() self.offsets = QPointF(x1, y1), QPointF(x2, y2) def snapPointToCanvas(self, x, y): """ Moves a point x,y to within the boundaries of the canvas. :return: (x,y,snapped) where snapped is True if x or y were changed, False if not. """ if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height(): x = max(x, 0) y = max(y, 0) x = min(x, self.pixmap.width()) y = min(y, self.pixmap.height()) return x, y, True return x, y, False def boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape point = shape[index] if self.outOfPixmap(pos): size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) if self.drawSquare: opposite_point_index = (index + 2) % 4 opposite_point = shape[opposite_point_index] min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y())) directionX = -1 if pos.x() - opposite_point.x() < 0 else 1 directionY = -1 if pos.y() - opposite_point.y() < 0 else 1 shiftPos = QPointF(opposite_point.x() + directionX * min_size - point.x(), opposite_point.y() + directionY * min_size - point.y()) else: shiftPos = pos - point if [shape[0].x(), shape[0].y(), shape[2].x(), shape[2].y()] \ == [shape[3].x(),shape[1].y(),shape[1].x(),shape[3].y()]: shape.moveVertexBy(index, shiftPos) lindex = (index + 1) % 4 rindex = (index + 3) % 4 lshift = None rshift = None if index % 2 == 0: rshift = QPointF(shiftPos.x(), 0) lshift = QPointF(0, shiftPos.y()) else: lshift = QPointF(shiftPos.x(), 0) rshift = QPointF(0, shiftPos.y()) shape.moveVertexBy(rindex, rshift) shape.moveVertexBy(lindex, lshift) else: shape.moveVertexBy(index, shiftPos) def boundedMoveShape(self, shapes, pos): if type(shapes).__name__ != 'list': shapes = [shapes] if self.outOfPixmap(pos): return False # No need to move o1 = pos + self.offsets[0] if self.outOfPixmap(o1): pos -= QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] if self.outOfPixmap(o2): pos += QPointF(min(0, self.pixmap.width() - o2.x()), min(0, self.pixmap.height() - o2.y())) # 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. XXX #self.calculateOffsets(self.selectedShape, pos) dp = pos - self.prevPoint if dp: for shape in shapes: shape.moveBy(dp) shape.close() self.prevPoint = pos return True return False def deSelectShape(self): if self.selectedShapes: for shape in self.selectedShapes: shape.selected=False 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() self.updateShapeIndex() return deleted_shapes 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) def copySelectedShape(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. for shape in shapes: point = shape[0] offset = QPointF(5.0, 5.0) self.calculateOffsets(shape, point) self.prevPoint = point if not self.boundedMoveShape(shape, point - offset): self.boundedMoveShape(shape, point + offset) def paintEvent(self, event): if not self.pixmap: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(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) # Paint rect if self.current is not None and len(self.line) == 2 and not self.fourpoint: # print('Drawing rect') leftTop = self.line[0] rightBottom = self.line[1] rectWidth = rightBottom.x() - leftTop.x() rectHeight = rightBottom.y() - leftTop.y() p.setPen(self.drawingRectColor) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) # ADD: if ( self.fillDrawing() and self.fourpoint and self.current is not None and len(self.current.points) >= 2 ): print('paint event') drawing_shape = self.current.copy() drawing_shape.addPoint(self.line[1]) drawing_shape.fill = True drawing_shape.paint(p) if self.drawing() and not self.prevPoint.isNull() and not self.outOfPixmap(self.prevPoint): p.setPen(QColor(0, 0, 0)) p.drawLine(int(self.prevPoint.x()), 0, int(self.prevPoint.x()), self.pixmap.height()) p.drawLine(0, int(self.prevPoint.y()), self.pixmap.width(), int(self.prevPoint.y())) self.setAutoFillBackground(True) if self.verified: pal = self.palette() pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128)) self.setPalette(pal) else: pal = self.palette() pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255)) self.setPalette(pal) # adaptive BBOX label & index font size if self.pixmap: h, w = self.pixmap.size().height(), self.pixmap.size().width() fontszie = int(max(h, w) / 48) for s in self.shapes: s.fontsize = fontszie p.end() def fillDrawing(self): return self._fill_drawing def transformPos(self, point): """Convert from widget-logical coordinates to painter-logical coordinates.""" 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 QPointF(x, y) def outOfPixmap(self, p): w, h = self.pixmap.width(), self.pixmap.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) def finalise(self): assert self.current if self.current.points[0] == self.current.points[-1]: # print('finalse') self.current = None self.drawingPolygon.emit(False) self.update() return self.current.close() self.current.idx = len(self.shapes) # add current box index self.shapes.append(self.current) 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) return distance(p1 - p2) < self.epsilon # 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): qt_version = 4 if hasattr(ev, "delta") else 5 if qt_version == 4: if ev.orientation() == Qt.Vertical: v_delta = ev.delta() h_delta = 0 else: h_delta = ev.delta() v_delta = 0 else: delta = ev.angleDelta() h_delta = delta.x() v_delta = delta.y() mods = ev.modifiers() if Qt.ControlModifier == int(mods) and v_delta: self.zoomRequest.emit(v_delta) else: v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical) h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal) ev.accept() def keyPressEvent(self, ev): key = ev.key() shapesBackup = copy.deepcopy(self.shapes) if len(shapesBackup) == 0: return self.shapesBackups.pop() self.shapesBackups.append(shapesBackup) if key == Qt.Key_Escape and self.current: print('ESC press') self.current = None self.drawingPolygon.emit(False) self.update() elif key == Qt.Key_Return and self.canCloseShape(): self.finalise() elif key == Qt.Key_Left and self.selectedShapes: self.moveOnePixel('Left') elif key == Qt.Key_Right and self.selectedShapes: self.moveOnePixel('Right') elif key == Qt.Key_Up and self.selectedShapes: self.moveOnePixel('Up') elif key == Qt.Key_Down and self.selectedShapes: self.moveOnePixel('Down') elif key == Qt.Key_X and self.selectedShapes: for i in range(len(self.selectedShapes)): self.selectedShape = self.selectedShapes[i] if self.rotateOutOfBound(0.01): continue self.selectedShape.rotate(0.01) self.shapeMoved.emit() self.update() elif key == Qt.Key_C and self.selectedShapes: for i in range(len(self.selectedShapes)): self.selectedShape = self.selectedShapes[i] if self.rotateOutOfBound(-0.01): continue self.selectedShape.rotate(-0.01) self.shapeMoved.emit() self.update() def rotateOutOfBound(self, angle): for shape in range(len(self.selectedShapes)): self.selectedShape = self.selectedShapes[shape] for i, p in enumerate(self.selectedShape.points): if self.outOfPixmap(self.selectedShape.rotatePoint(p, angle)): return True return False def moveOnePixel(self, direction): # print(self.selectedShape.points) self.selectCount = len(self.selectedShapes) self.selectCountShape = True for i in range(len(self.selectedShapes)): self.selectedShape = self.selectedShapes[i] if direction == 'Left' and not self.moveOutOfBound(QPointF(-1.0, 0)): # print("move Left one pixel") self.selectedShape.points[0] += QPointF(-1.0, 0) self.selectedShape.points[1] += QPointF(-1.0, 0) self.selectedShape.points[2] += QPointF(-1.0, 0) self.selectedShape.points[3] += QPointF(-1.0, 0) elif direction == 'Right' and not self.moveOutOfBound(QPointF(1.0, 0)): # print("move Right one pixel") self.selectedShape.points[0] += QPointF(1.0, 0) self.selectedShape.points[1] += QPointF(1.0, 0) self.selectedShape.points[2] += QPointF(1.0, 0) self.selectedShape.points[3] += QPointF(1.0, 0) elif direction == 'Up' and not self.moveOutOfBound(QPointF(0, -1.0)): # print("move Up one pixel") self.selectedShape.points[0] += QPointF(0, -1.0) self.selectedShape.points[1] += QPointF(0, -1.0) self.selectedShape.points[2] += QPointF(0, -1.0) self.selectedShape.points[3] += QPointF(0, -1.0) elif direction == 'Down' and not self.moveOutOfBound(QPointF(0, 1.0)): # print("move Down one pixel") self.selectedShape.points[0] += QPointF(0, 1.0) self.selectedShape.points[1] += QPointF(0, 1.0) self.selectedShape.points[2] += QPointF(0, 1.0) self.selectedShape.points[3] += QPointF(0, 1.0) shapesBackup = [] shapesBackup = copy.deepcopy(self.shapes) self.shapesBackups.append(shapesBackup) self.shapeMoved.emit() self.repaint() def moveOutOfBound(self, step): points = [p1+p2 for p1, p2 in zip(self.selectedShape.points, [step]*4)] return True in map(self.outOfPixmap, points) def setLastLabel(self, text, line_color=None, fill_color=None, key_cls=None): assert text self.shapes[-1].label = text if line_color: self.shapes[-1].line_color = line_color if fill_color: self.shapes[-1].fill_color = fill_color if key_cls: self.shapes[-1].key_cls = key_cls self.storeShapes() return self.shapes[-1] def undoLastLine(self): assert self.shapes self.current = self.shapes.pop() self.current.setOpen() self.line.points = [self.current[-1], self.current[0]] 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 resetAllLines(self): assert self.shapes self.current = self.shapes.pop() self.current.setOpen() self.line.points = [self.current[-1], self.current[0]] self.drawingPolygon.emit(True) self.current = None self.drawingPolygon.emit(False) self.update() 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.current = None self.hShape = None self.hVertex = None # self.hEdge = None self.storeShapes() self.updateShapeIndex() self.repaint() def setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() def currentCursor(self): cursor = QApplication.overrideCursor() if cursor is not None: cursor = cursor.shape() return cursor def overrideCursor(self, cursor): self._cursor = cursor if self.currentCursor() is None: QApplication.setOverrideCursor(cursor) else: QApplication.changeOverrideCursor(cursor) def restoreCursor(self): QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = None self.update() self.shapesBackups = [] def setDrawingShapeToSquare(self, status): self.drawSquare = status 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.updateShapeIndex() self.repaint() @property def isShapeRestorable(self): if len(self.shapesBackups) < 2: return False return True def updateShapeIndex(self): for i in range(len(self.shapes)): self.shapes[i].idx = i self.update()