def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.shapes = [] self.current = None self.polygons = [] self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None 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.moreselected = False #polygon drawing self.polshapes = [] self.polcurrent = None self.pollineColor = QtGui.QColor(0, 0, 255) self.polline = Polygon(line_color=self.pollineColor) self.createMode = 'polygon' self.polygondrawing = False self.shapesBackups = [] self.selectedpolygon = None self.selectedpolygon2 = None self.hpolygonVertex = None self.hpolygon = None self.hpolygonedge = None self.tempbox = [] self.rectangleVertrixcheck = False
def handleDrawingPolygon(self, pos, epsilon=1): self.polyPrev = pos if self.currentPolygon is None: self.currentPolygon = Polygon() self.currentPolygon.addPoint(pos) # self.currentPolygon.highlightVertex(0,self.currentPolygon.MOVE_VERTEX) else: self.currentPolygon.addPoint(pos) if self.currentPolygon.reachMaxPoints(epsilon): self.currentPolygon.popPoint() self.currentPolygon._closed = True self.polygons.append(self.currentPolygon) # for i in range(len(self.currentPolygon)): # self.currentPolygon.highlightVertex(i,self.currentPolygon.MOVE_VERTEX) self.currentPolygon = None self.mode = self.EDIT # self.polyPoints.append(pos) # self.update() pass
class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) # CREATE, EDIT = list(range(2)) #byMe CREATE, EDIT, POLYGON = list(range(3)) # epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.shapes = [] self.current = None self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None 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.hPolygon = None self.hVertexPolygon = None self.selectedPolygon = None self.selectedPolygonCopy = None self._painter = QPainter() self._endPaint = False 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 #===byMe========== self.text = [] self.locText = [] self.drawingTextColor = Qt.red self.fontText = QFont("Arial", 20) # self.fontText.setItalic(True) self.polyPrev = QPointF() self.polyNext = QPointF() self.polygons = [] self.currentPolygon = None #===byMe========== 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 drawPolygon(self): return self.mode == self.POLYGON def setDrawPolygon(self): self.mode = self.POLYGON 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())) #byMe if self.drawPolygon(): self.overrideCursor(CURSOR_DRAW_POLYGON) self.polyNext = pos if self.currentPolygon: 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.currentPolygon[-1], pos) pass elif len(self.currentPolygon) > 1 and self.closeEnough( pos, self.currentPolygon[0]): # self.currentPolygon.highlightVertex(0, Shape.NEAR_VERTEX) pass # self.currentPolygon.highlightClear() self.repaint() return # # rect drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.drawingLineColor 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.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: initPos = self.current[0] minX = initPos.x() minY = initPos.y() min_size = min(abs(pos.x() - minX), abs(pos.y() - minY)) directionX = -1 if pos.x() - minX < 0 else 1 directionY = -1 if pos.y() - minY < 0 else 1 self.line[1] = QPointF(minX + directionX * min_size, minY + directionY * min_size) else: self.line[1] = pos 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.selectedShapeCopy and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShapeCopy, pos) self.repaint() elif self.selectedShape: self.selectedShapeCopy = self.selectedShape.copy() self.repaint() #byMe ====== if self.selectedPolygonCopy and self.polyPrev: self.overrideCursor(CURSOR_MOVE) self.boundedMovePolygon(self.selectedPolygonCopy, pos) self.repaint() elif self.selectedPolygon: self.selectedPolygonCopy = self.selectedPolygon.copy() self.repaint() #========= return # Polygon/Vertex moving. if Qt.LeftButton & ev.buttons(): if self.selectedVertex(): self.boundedMoveVertex(pos) self.shapeMoved.emit() self.repaint() elif self.selectedShape and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMoveShape(self.selectedShape, pos) self.shapeMoved.emit() self.repaint() #byMe===== if self.selectedVertexPolygon(): self.boundedMoveVertexPolygon(pos) # self.shapeMoved.emit() self.repaint() elif self.selectedPolygon and self.polyPrev: self.overrideCursor(CURSOR_MOVE) self.boundedMovePolygon(self.selectedPolygon, pos) # self.shapeMoved.emit() self.repaint() #============= return # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. self.setToolTip("Image") bNearestPoit = False bContains = False for polygon in self.polygons: # Look for a nearby vertex to highlight. If that fails, # check if we happen to be inside a shape. index = polygon.nearestVertex(pos, self.epsilon) if index is not None: # if len bNearestPoit = True # if self.selectedVertex(): # self.hShape.highlightClear() self.hVertexPolygon, self.hPolygon = index, polygon polygon.highlightVertex(index, polygon.MOVE_VERTEX) self.overrideCursor(CURSOR_POINT) self.setToolTip("Click & drag to move point") self.setStatusTip(self.toolTip()) self.update() elif polygon.containsPoint(pos): bContains = True # polygon.highlightClear() # if self.selectedVertex(): # self.hShape.highlightClear() self.hVertexPolygon, self.hPolygon = None, polygon polygon.fill_color = QColor(50, 200, 0, 128) polygon.fill = True self.setToolTip("Click & drag to move polygon") self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() break else: # Nothing found, clear highlights, reset state. if not bNearestPoit and not bContains: if self.hPolygon: self.hPolygon.fill = False self.hPolygon.highlightClear() self.update() self.hVertexPolygon, self.hPolygon = None, None self.overrideCursor(CURSOR_DEFAULT) 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: bNearestPoit = True 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): bContains = True if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = None, shape 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 not bNearestPoit and not bContains: 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: # byme if self.drawPolygon(): self.handleDrawingPolygon(pos, self.epsilon / self.scale) else: self.selectPolygonPoint(pos) self.polyPrev = pos if self.drawing(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): if ev.button() == Qt.RightButton: menu = self.menus[bool(self.selectedShapeCopy)] self.restoreCursor() if not menu.exec_(self.mapToGlobal(ev.pos()))\ and self.selectedShapeCopy: # Cancel the move by deleting the shadow copy. self.selectedShapeCopy = None self.repaint() elif ev.button() == Qt.LeftButton and self.selectedShape: if self.selectedVertex(): self.overrideCursor(CURSOR_POINT) else: self.overrideCursor(CURSOR_GRAB) elif ev.button() == Qt.LeftButton: pos = self.transformPos(ev.pos()) if self.drawing(): self.handleDrawing(pos) def endMove(self, copy=False): assert self.selectedShape and self.selectedShapeCopy shape = self.selectedShapeCopy #del shape.fill_color #del shape.line_color if copy: self.shapes.append(shape) self.selectedShape.selected = False self.selectedShape = shape self.repaint() else: self.selectedShape.points = [p for p in shape.points] self.selectedShapeCopy = None def hideBackroundShapes(self, value): self.hideBackround = value if self.selectedShape: # 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() #====byme def handleDrawingPolygon(self, pos, epsilon=1): self.polyPrev = pos if self.currentPolygon is None: self.currentPolygon = Polygon() self.currentPolygon.addPoint(pos) # self.currentPolygon.highlightVertex(0,self.currentPolygon.MOVE_VERTEX) else: self.currentPolygon.addPoint(pos) if self.currentPolygon.reachMaxPoints(epsilon): self.currentPolygon.popPoint() self.currentPolygon._closed = True self.polygons.append(self.currentPolygon) # for i in range(len(self.currentPolygon)): # self.currentPolygon.highlightVertex(i,self.currentPolygon.MOVE_VERTEX) self.currentPolygon = None self.mode = self.EDIT # self.polyPoints.append(pos) # self.update() pass #=========== def handleDrawing(self, pos): if self.current and self.current.reachMaxPoints() is False: 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.parent().window().showRectOfShape(self.current) self.finalise() elif not self.outOfPixmap(pos): 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: self.current.popPoint() self.finalise() def selectShape(self, shape): self.deSelectShape() shape.selected = True self.selectedShape = shape self.setHiding() self.selectionChanged.emit(True) self.update() def selectShapePoint(self, point): """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) self.selectShape(shape) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): self.selectShape(shape) self.calculateOffsets(shape, point) return #byMe======== def selectedVertexPolygon(self): return self.hVertexPolygon is not None def deSelectPolygon(self): if self.selectedPolygon: self.selectedPolygon.selected = False self.selectedPolygon = None self.setHiding(False) # self.selectionChanged.emit(False) self.update() def selectPolygon(self, polygon): self.deSelectPolygon() polygon.selected = True self.selectedPolygon = polygon self.setHiding() # self.selectionChanged.emit(True) self.update() def selectPolygonPoint(self, point): """Select the first shape created which contains this point.""" self.deSelectPolygon() if self.selectedVertexPolygon(): # A vertex is marked for selection. index, polygon = self.hVertex, self.hPolygon polygon.highlightVertex(index, polygon.MOVE_VERTEX) self.selectPolygon(polygon) return for polygon in reversed(self.polygons): if polygon.containsPoint(point): self.selectPolygon(polygon) self.calculateOffsets(polygon, point) return #=============== 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): pos = self.intersectionPoint(point, pos) 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 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) def boundedMoveShape(self, shape, pos): 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: shape.moveBy(dp) self.prevPoint = pos return True return False #byMe Move Polygon def boundedMoveVertexPolygon(self, pos): index, shape = self.hVertexPolygon, self.hPolygon point = shape[index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) 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 shape.moveVertexBy(index, shiftPos) lindex = (index + 1) % len(shape) rindex = (index + 3) % len(shape) 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) def boundedMovePolygon(self, shape, pos): 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.polyPrev if dp: shape.moveBy(dp) self.polyPrev = pos return True return False #================== def deSelectShape(self): if self.selectedShape: self.selectedShape.selected = False self.selectedShape = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def deleteSelected(self): if self.selectedShape: shape = self.selectedShape self.shapes.remove(self.selectedShape) self.selectedShape = None self.update() return shape def copySelectedShape(self): if self.selectedShape: shape = self.selectedShape.copy() self.deSelectShape() self.shapes.append(shape) shape.selected = True self.selectedShape = shape self.boundedShiftShape(shape) return shape def boundedShiftShape(self, shape): # Try to move in one direction, and if it fails in another. # Give up if both fail. point = shape[0] offset = QPointF(2.0, 2.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) self._endPaint = True 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 #byMe Polygon.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.selectedShapeCopy: self.selectedShapeCopy.paint(p) #===byMe========== #paint text for txt, loc in zip(self.text, self.locText): if loc is not None and txt is not None: p.setPen(self.drawingTextColor) p.setFont(self.fontText) if isinstance(loc, QPoint): p.drawText(loc, txt) else: p.drawText(loc, Qt.AlignLeft, txt) #paint polygons for polygon in self.polygons: # if (polygon.selected or not self._hideBackround) and self.isVisible(polygon): # polygon.fill = True polygon.paint(p) if self.currentPolygon: self.currentPolygon.paint(p) # paint polygon if self.currentPolygon is not None: pen = QPen(self.drawingRectColor) pen.setWidth(max(1, int(round(2.0 / self.scale)))) p.setPen(pen) p.drawLine(self.polyPrev, self.polyNext) # p.drawEllipse(self.polyNext,self.currentPolygon.point_size/2,self.currentPolygon.point_size/2) # # p.setBrush(Qt.BDiagPattern) # # points = self.currentPolygon.points # # p.drawPolygon(QPolygonF(points)) # # p.setPen(Qt.black) #============= # Paint rect if self.current is not None and len(self.line) == 2: leftTop = self.line[0] rightBottom = self.line[1] rectWidth = rightBottom.x() - leftTop.x() rectHeight = rightBottom.y() - leftTop.y() # p.setPen(self.drawingRectColor) pen = QPen(self.drawingRectColor) pen.setWidth(max(1, int(round(2.0 / self.scale)))) p.setPen(pen) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) if self.drawing() and not self.prevPoint.isNull( ) and not self.outOfPixmap(self.prevPoint): pen = QPen(QColor(self.drawingRectColor)) pen.setWidth(max(1, int(round(2.0 / self.scale)))) p.setPen(pen) p.drawLine(self.prevPoint.x(), 0, self.prevPoint.x(), self.pixmap.height()) p.drawLine(0, self.prevPoint.y(), self.pixmap.width(), 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) p.end() self._endPaint = False 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]: self.current = None self.drawingPolygon.emit(False) self.update() return self.current.close() 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 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(), 0), (size.width(), size.height()), (0, size.height())] x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() intersectingEdges = self.intersectingEdges((x1, y1), (x2, y2), points) d, i, (x, y) = min(intersectingEdges) 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 QPointF(x3, min(max(0, y2), max(y3, y4))) else: # y3 == y4 return QPointF(min(max(0, x2), max(x3, x4)), y3) # Ensure the labels are within the bounds of the image. If not, fix them. x, y, _ = self.snapPointToCanvas(x, y) return QPointF(x, y) def intersectingEdges(self, x1y1, x2y2, points): """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 = x1y1 x2, y2 = x2y2 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 = QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = distance(m - QPointF(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): 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() 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.selectedShape: self.moveOnePixel('Left') elif key == Qt.Key_Right and self.selectedShape: self.moveOnePixel('Right') elif key == Qt.Key_Up and self.selectedShape: self.moveOnePixel('Up') elif key == Qt.Key_Down and self.selectedShape: self.moveOnePixel('Down') # elif key == Qt.Key_Delete: # self.parent().window().deleteSelectedShape() def moveOnePixel(self, direction): # print(self.selectedShape.points) 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) 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, params=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 params: self.shapes[-1].paramsVision = params 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 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): self.shapes = list(shapes) self.current = None 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() def setDrawingShapeToSquare(self, status): self.drawSquare = status
def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing() or self.polygonDrawing(): if self.polcurrent: # Add point to existing shape. if self.createMode == 'polygon': self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] if self.polcurrent.isClosed(): self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode in ['rectangle', 'circle', 'line']: assert len(self.polcurrent.points) == 1 if self.createMode == 'rectangle': self.polcurrent.points = self.getrectanglepoints( self.polline.points) else: self.polcurrent.points = self.polline.points self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode == 'linestrip': self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] if int(ev.modifiers()) == QtCore.Qt.ControlModifier: self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode == 'x-rectangle': if len(self.polcurrent.points) < 2: self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] elif len(self.polcurrent.points) >= 2: self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif not self.outOfPixmap(pos): # Create new shape. self.polcurrent = Polygon(shape_type=self.createMode, line_color=self.pollineColor) self.polcurrent.addPoint(pos) if self.createMode == 'point': self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() else: if self.createMode == 'circle': self.polcurrent.shape_type = 'circle' self.polline.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() else: if ev.button( ) == QtCore.Qt.LeftButton and self.hpolygon and self.hpolygonVertex: if self.hpolygon.shape_type == 'x-rectangle' and ( self.polygonEditing() or self.editing()): self.rectangleVertrixcheck = True self.selectpolygonPoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.drawing(): if self.polcurrent: if self.polcurrent.shape_type not in [ 'circle', 'rectangle', 'line' ]: self.polcurrent.points.pop(-1) self.repaint() elif ev.button() == Qt.RightButton: if self.polygonEditing() and self.selectedpolygon2: if self.selectedpolygon2.shape_type not in [ 'circle', 'rectangle', 'line' ]: index = self.selectedpolygon2.nearestVertex( pos, self.epsilon) if index is not None: del self.selectedpolygon2.points[index] if self.selectedpolygon2.points == []: self.selectedpolygon.innerpolygons.remove( self.selectedpolygon2) self.shapeMoved.emit() self.repaint() elif self.selectedpolygon: if self.selectedpolygon.shape_type not in [ 'circle', 'rectangle', 'line' ]: index = self.selectedpolygon.nearestVertex( pos, self.epsilon) if index is not None: del self.selectedpolygon.points[index] if self.selectedpolygon.points == []: self.polygons.remove(self.selectedpolygon) self.shapeMoved.emit() self.repaint() self.prevPoint = pos elif ev.button() == Qt.MidButton and self.drawing(): if self.createMode == 'linestrip': self.polfinalise() self.newShape.emit() elif ev.button() == Qt.MidButton: if self.polygonEditing() and self.selectedpolygon2: if self.selectedpolygon2.shape_type not in [ 'circle', 'rectangle', 'point', 'line' ]: index = self.selectedpolygon2.nearest1Edge(pos) self.selectedpolygon2.points.insert(index, pos) self.shapeMoved.emit() self.repaint() elif self.selectedpolygon: if self.selectedpolygon.shape_type not in [ 'circle', 'rectangle', 'point', 'line' ]: index = self.selectedpolygon.nearest1Edge(pos) self.selectedpolygon.points.insert(index, pos) self.shapeMoved.emit() self.repaint()
class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) selectionpolChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) deletePolygon = pyqtSignal() CREATE, EDIT, POLYGON, EDITPOLYGON = list(range(4)) epsilon = 6.0 polepsilon = 10.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.shapes = [] self.current = None self.polygons = [] self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None 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.moreselected = False #polygon drawing self.polshapes = [] self.polcurrent = None self.pollineColor = QtGui.QColor(0, 0, 255) self.polline = Polygon(line_color=self.pollineColor) self.createMode = 'polygon' self.polygondrawing = False self.shapesBackups = [] self.selectedpolygon = None self.selectedpolygon2 = None self.hpolygonVertex = None self.hpolygon = None self.hpolygonedge = None self.tempbox = [] self.rectangleVertrixcheck = 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 polygonDrawing(self): return self.mode == self.POLYGON def polygonEditing(self): return self.mode == self.EDITPOLYGON def editing(self): return self.mode == self.EDIT def setEditing(self, valueCreate=False, valuePolygon=False): if valueCreate: self.mode = self.CREATE elif valuePolygon: if valuePolygon == 3: self.mode = self.EDITPOLYGON else: self.mode = self.POLYGON else: self.mode = self.EDIT if valueCreate: # Create self.unHighlight() self.deSelectpolygon() 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()) selecting = False selecting2 = False innerpolygon = None if not self.selectedpolygonVertex(): self.tempbox = [] self.setToolTip('(' + str(int(pos.x())) + '[' + str(int(pos.x()) + 176) + '],' + str(int(pos.y())) + ')') # Polygon drawing. if self.drawing() or self.polygonDrawing(): self.polline.shape_type = self.createMode self.overrideCursor(CURSOR_DRAW) if not self.polcurrent: return color = self.pollineColor 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.polcurrent[-1], pos) elif len(self.polcurrent) > 1 and self.createMode == 'polygon' and\ self.polcloseEnough(pos, self.polcurrent[0]): # Attract line to starting point and # colorise to alert the user. pos = self.polcurrent[0] color = self.polcurrent.line_color self.overrideCursor(CURSOR_POINT) self.polcurrent.highlightVertex(0, Shape.NEAR_VERTEX) if self.createMode in ['polygon', 'linestrip']: self.polline[0] = self.polcurrent[-1] self.polline[1] = pos elif self.createMode == 'rectangle': self.polline.points = [self.polcurrent[0], pos] self.polline.close() elif self.createMode == 'circle': self.polline.points = [self.polcurrent[0], pos] self.polline.shape_type = "circle" elif self.createMode == 'line': self.polline.points = [self.polcurrent[0], pos] self.polline.close() elif self.createMode == 'point': self.polline.points = [self.polcurrent[0]] self.polline.close() elif self.createMode == 'x-rectangle': if len(self.polcurrent.points) >= 2: if len(self.polcurrent.points) == 2: point1, point2 = self.squarepoint( self.polcurrent.points, pos) self.polcurrent.points = [ self.polcurrent.points[0], point1, self.polcurrent.points[1], point2 ] elif len(self.polcurrent.points) == 4: point1, point2 = self.squarepoint([ self.polcurrent.points[0], self.polcurrent.points[2] ], pos) self.polcurrent.points[1] = point1 self.polcurrent.points[3] = point2 self.polcurrent.close() self.polline.points = [ self.polcurrent[0], self.polcurrent[2] ] else: self.polline.points = [self.polcurrent[0], pos] self.polline.close() self.prevPoint = pos self.polline.line_color = color self.repaint() self.polcurrent.highlightClear() return # Polygon copy moving. if Qt.RightButton & ev.buttons() and not self.hpolygonVertex: if self.selectedShapeCopy and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMovepolygonShape(self.selectedShapeCopy, pos) self.repaint() elif self.selectedpolygon: self.selectedShapeCopy = self.selectedpolygon.copy(1) self.repaint() return return # Polygon/Vertex moving. if Qt.LeftButton & ev.buttons(): self.setToolTip('(' + str(int(pos.x())) + '[' + str(int(pos.x()) + 176) + '],' + str(int(pos.y())) + ')') if self.selectedpolygonVertex(): if self.hpolygon.shape_type == 'rectangle': self.boundedMoverectangleVertex(pos) elif self.hpolygon.shape_type == 'x-rectangle': if self.tempbox == []: self.tempbox = [ self.hpolygon.points[0], self.hpolygon.points[1], self.hpolygon.points[2], self.hpolygon.points[3] ] self.boundedMoveXrectangleVertex(pos) else: self.boundedMovepolygonVertex(pos) self.movingShape = True self.shapeMoved.emit() self.repaint() elif self.selectedpolygon2 and self.prevPoint and self.polygonEditing( ): self.overrideCursor(CURSOR_MOVE) self.boundedMovepolygonShape(self.selectedpolygon2, pos) self.movingShape = True self.shapeMoved.emit() self.repaint() elif self.selectedpolygon and self.prevPoint: self.overrideCursor(CURSOR_MOVE) self.boundedMovepolygonShape(self.selectedpolygon, pos) self.movingShape = True self.shapeMoved.emit() self.repaint() # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. # self.("Image") for polygon in reversed( [s for s in self.polygons if self.isVisible(s)]): # Look for a nearby vertex to highlight. If that fails, # check if we happen to be inside a shape. if self.selectedpolygon: if self.polygonEditing( ) and self.selectedpolygon.innerpolygons != []: for inpolygon in reversed([ s for s in self.selectedpolygon.innerpolygons if self.isVisible(self.selectedpolygon) ]): if self.selectedpolygon2: index = self.selectedpolygon2.nearestVertex( pos, self.epsilon) index_edge = self.selectedpolygon2.nearestEdge( pos, self.epsilon) if index is not None: selecting2 = True else: index = inpolygon.nearestVertex( pos, self.epsilon) innerpolygon = inpolygon break else: index = inpolygon.nearestVertex(pos, self.epsilon) index_edge = inpolygon.nearestEdge( pos, self.epsilon) innerpolygon = inpolygon if index != None: break else: index = self.selectedpolygon.nearestVertex( pos, self.epsilon) index_edge = self.selectedpolygon.nearestEdge( pos, self.epsilon) if index is not None: selecting = True else: index = polygon.nearestVertex(pos, self.epsilon) else: index = polygon.nearestVertex(pos, self.epsilon) index_edge = polygon.nearestEdge(pos, self.epsilon) if index is not None: if not self.rectangleVertrixcheck: if self.selectedpolygonVertex(): if self.hpolygon: self.hpolygon.highlightClear() if self.polygonEditing(): if selecting2: self.hpolygonVertex, self.hpolygon, self.hpolygonedge = index, self.selectedpolygon2, index_edge self.selectedpolygon2.highlightVertex( index, self.selectedpolygon2.MOVE_VERTEX) else: self.hpolygonVertex, self.hpolygon, self.hpolygonedge = index, innerpolygon, index_edge if innerpolygon: innerpolygon.highlightVertex( index, innerpolygon.MOVE_VERTEX) elif selecting: self.hpolygonVertex, self.hpolygon, self.hpolygonedge = index, self.selectedpolygon, index_edge self.selectedpolygon.highlightVertex( index, polygon.MOVE_VERTEX) else: self.hpolygonVertex, self.hpolygon, self.hpolygonedge = index, polygon, index_edge polygon.highlightVertex(index, polygon.MOVE_VERTEX) self.overrideCursor(CURSOR_POINT) self.setToolTip("Click & drag to move point") self.setStatusTip(self.toolTip()) self.update() break elif self.polygonEditing() and self.selectedpolygon: if self.selectedpolygon.innerpolygons != []: for polygon in self.selectedpolygon.innerpolygons: if polygon.containsPoint(pos): if self.selectedpolygonVertex(): self.hpolygon.highlightClear() self.hpolygonVertex = None self.hpolygon = polygon self.hpolygonedge = index_edge self.setToolTip("Click & drag to move shape '%s'") self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() break else: # Nothing found, clear highlights, reset state. if self.hpolygon: self.hpolygon.highlightClear() self.update() if not self.rectangleVertrixcheck: if not self.selectedpolygon: self.hpolygon = None self.hpolygonVertex, self.hpolygonedge = None, None elif polygon.containsPoint(pos) and self.hpolygon: if self.selectedpolygonVertex(): self.hpolygon.highlightClear() if not self.rectangleVertrixcheck: self.hpolygonVertex = None self.hpolygon = polygon self.hpolygonedge = index_edge self.setToolTip("Click & drag to move shape '%s'") self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() break else: # Nothing found, clear highlights, reset state. if self.hpolygon: self.hpolygon.highlightClear() self.update() if not self.rectangleVertrixcheck: if not self.selectedpolygon: self.hpolygon = None self.hpolygonVertex, self.hpolygonedge = None, None self.setToolTip('(' + str(int(pos.x())) + '[' + str(int(pos.x()) + 176) + '],' + str(int(pos.y())) + ')') if not self.outOfPixmap(pos): self.setToolTip('(' + str(int(pos.x())) + '[' + str(int(pos.x()) + 176) + '],' + str(int(pos.y())) + ')') self.repaint() def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing() or self.polygonDrawing(): if self.polcurrent: # Add point to existing shape. if self.createMode == 'polygon': self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] if self.polcurrent.isClosed(): self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode in ['rectangle', 'circle', 'line']: assert len(self.polcurrent.points) == 1 if self.createMode == 'rectangle': self.polcurrent.points = self.getrectanglepoints( self.polline.points) else: self.polcurrent.points = self.polline.points self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode == 'linestrip': self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] if int(ev.modifiers()) == QtCore.Qt.ControlModifier: self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif self.createMode == 'x-rectangle': if len(self.polcurrent.points) < 2: self.polcurrent.addPoint(self.polline[1]) self.polline[0] = self.polcurrent[-1] elif len(self.polcurrent.points) >= 2: self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() elif not self.outOfPixmap(pos): # Create new shape. self.polcurrent = Polygon(shape_type=self.createMode, line_color=self.pollineColor) self.polcurrent.addPoint(pos) if self.createMode == 'point': self.polfinalise() if not self.polygonDrawing(): self.newShape.emit() else: if self.createMode == 'circle': self.polcurrent.shape_type = 'circle' self.polline.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() else: if ev.button( ) == QtCore.Qt.LeftButton and self.hpolygon and self.hpolygonVertex: if self.hpolygon.shape_type == 'x-rectangle' and ( self.polygonEditing() or self.editing()): self.rectangleVertrixcheck = True self.selectpolygonPoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.drawing(): if self.polcurrent: if self.polcurrent.shape_type not in [ 'circle', 'rectangle', 'line' ]: self.polcurrent.points.pop(-1) self.repaint() elif ev.button() == Qt.RightButton: if self.polygonEditing() and self.selectedpolygon2: if self.selectedpolygon2.shape_type not in [ 'circle', 'rectangle', 'line' ]: index = self.selectedpolygon2.nearestVertex( pos, self.epsilon) if index is not None: del self.selectedpolygon2.points[index] if self.selectedpolygon2.points == []: self.selectedpolygon.innerpolygons.remove( self.selectedpolygon2) self.shapeMoved.emit() self.repaint() elif self.selectedpolygon: if self.selectedpolygon.shape_type not in [ 'circle', 'rectangle', 'line' ]: index = self.selectedpolygon.nearestVertex( pos, self.epsilon) if index is not None: del self.selectedpolygon.points[index] if self.selectedpolygon.points == []: self.polygons.remove(self.selectedpolygon) self.shapeMoved.emit() self.repaint() self.prevPoint = pos elif ev.button() == Qt.MidButton and self.drawing(): if self.createMode == 'linestrip': self.polfinalise() self.newShape.emit() elif ev.button() == Qt.MidButton: if self.polygonEditing() and self.selectedpolygon2: if self.selectedpolygon2.shape_type not in [ 'circle', 'rectangle', 'point', 'line' ]: index = self.selectedpolygon2.nearest1Edge(pos) self.selectedpolygon2.points.insert(index, pos) self.shapeMoved.emit() self.repaint() elif self.selectedpolygon: if self.selectedpolygon.shape_type not in [ 'circle', 'rectangle', 'point', 'line' ]: index = self.selectedpolygon.nearest1Edge(pos) self.selectedpolygon.points.insert(index, pos) self.shapeMoved.emit() self.repaint() def mouseReleaseEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and not self.drawing( ) and not self.polygonEditing() and not (self.selectedpolygon and self.hpolygonVertex): menu = self.menus[bool(self.selectedShapeCopy)] self.restoreCursor() if not menu.exec_(self.mapToGlobal(ev.pos()))\ and self.selectedShapeCopy: # Cancel the move by deleting the shadow copy. self.selectedShapeCopy = None self.repaint() elif ev.button( ) == QtCore.Qt.LeftButton and self.hpolygon and self.hpolygonVertex: if self.hpolygon.shape_type == 'x-rectangle' and ( self.polygonEditing() or self.editing()): self.rectangleVertrixcheck = False elif ev.button() == QtCore.Qt.LeftButton and self.selectedShape: self.overrideCursor(CURSOR_GRAB) def endMove(self, copy=False): assert self.selectedpolygon and self.selectedShapeCopy shape = self.selectedShapeCopy #del shape.fill_color #del shape.line_color if copy: self.polygons.append(shape) self.selectedpolygon.selected = False self.selectedpolygon = shape self.repaint() else: self.selectedpolygon.points = [p for p in shape.points] if self.selectedpolygon.innerpolygons != []: for i in range(0, len(self.selectedpolygon.innerpolygons)): self.selectedpolygon.innerpolygons[i].points = [ p for p in shape.innerpolygons[i].points ] self.selectedShapeCopy = None def hideBackroundShapes(self, value): self.hideBackround = value if self.selectedpolygon: # 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 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 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 polygon in self.polygons: if self.isVisible(polygon): polygon.paint(p) if self.polcurrent: self.polcurrent.paint(p) if not self.polcurrent.shape_type == 'x-rectangle' or not len( self.polcurrent.points) == 4: self.polline.paint(p) else: pass if self.selectedShapeCopy: self.selectedShapeCopy.paint(p) if self.polcurrent: if self.createMode == 'line': try: x1, y1, x1_p, y1_p = self.getLine(self.polcurrent, self.prevPoint) p.drawLine( self.prevPoint.x(), self.prevPoint.y(), 2 * self.prevPoint.x() - self.polcurrent[-1].x(), 2 * self.prevPoint.y() - self.polcurrent[-1].y()) p.drawLine(self.prevPoint.x() + x1, self.prevPoint.y() + y1, self.prevPoint.x() - x1, self.prevPoint.y() - y1) # if x1_p != None: # p.drawLine(self.polcurrent[-1].x(), self.polcurrent[-1].y(), x1_p, y1_p) except Exception as e: print(e.args) elif self.drawing() and not self.prevPoint.isNull( ) and not self.outOfPixmap(self.prevPoint): if self.createMode == 'x-rectangle': pass else: p.setPen(QColor(0, 0, 0)) p.drawLine(self.prevPoint.x(), 0, self.prevPoint.x(), self.pixmap.height()) p.drawLine(0, self.prevPoint.y(), self.pixmap.width(), self.prevPoint.y()) self.polcurrent.paint(p) self.polline.paint(p) self.setAutoFillBackground(True) if self.verified == 'yes': pal = self.palette() pal.setColor(self.backgroundRole(), QColor(184, 239, 38, 128)) self.setPalette(pal) elif self.verified == 'yes1': pal = self.palette() pal.setColor(self.backgroundRole(), QColor(238, 44, 44, 128)) self.setPalette(pal) elif self.verified == 'yes2': pal = self.palette() pal.setColor(self.backgroundRole(), QColor(0, 0, 255, 128)) self.setPalette(pal) else: pal = self.palette() pal.setColor(self.backgroundRole(), QColor(232, 232, 232, 255)) self.setPalette(pal) p.end() 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 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 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(), 0), (size.width(), size.height()), (0, size.height())] 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 QPointF(x3, min(max(0, y2), max(y3, y4))) else: # y3 == y4 return QPointF(min(max(0, x2), max(x3, x4)), y3) return QPointF(x, y) def intersectingEdges(self, x1y1, x2y2, points): """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 = x1y1 x2, y2 = x2y2 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 = QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = distance(m - QPointF(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): 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() 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.selectedpolygon: self.moveOnePixel('Left') elif key == Qt.Key_Right and self.selectedpolygon: self.moveOnePixel('Right') elif key == Qt.Key_Up and self.selectedpolygon: self.moveOnePixel('Up') elif key == Qt.Key_Down and self.selectedpolygon: self.moveOnePixel('Down') if key == 16777248: self.moreselected = True def keyReleaseEvent(self, ev): key = ev.key() if key == 16777248: self.moreselected = False def moveOnePixel(self, direction): # print(self.selectedShape.points) if direction == 'Left' and not self.moveOutOfBound(QPointF(-1.0, 0)): # print("move Left one pixel") for point in self.selectedpolygon.points: point += QPointF(-1.0, 0) elif direction == 'Right' and not self.moveOutOfBound(QPointF(1.0, 0)): # print("move Right one pixel") for point in self.selectedpolygon.points: point += QPointF(1.0, 0) elif direction == 'Up' and not self.moveOutOfBound(QPointF(0, -1.0)): # print("move Up one pixel") for point in self.selectedpolygon.points: point += QPointF(0, -1.0) elif direction == 'Down' and not self.moveOutOfBound(QPointF(0, 1.0)): # print("move Down one pixel") for point in self.selectedpolygon.points: point += QPointF(0, 1.0) self.shapeMoved.emit() self.repaint() def copySelectedShape(self, type=0): if self.selectedpolygon: shape = self.selectedpolygon.copy(type=type) self.deSelectpolygon() self.polygons.append(shape) shape.selected = True self.selectedpolygon = shape self.boundedShiftShape(shape) return shape 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, text_pose, text_unique, line_color=None, fill_color=None): assert text self.polygons[-1].label = text self.polygons[-1].pose = text_pose self.polygons[-1].unique = text_unique if line_color: self.polygons[-1].line_color = line_color if fill_color: self.polygons[-1].fill_color = fill_color return self.polygons[-1] def setPolygonLabel(self, text): if text != None: self.polcurrent.label = text def boundedShiftShape(self, shape): # Try to move in one direction, and if it fails in another. # Give up if both fail. point = shape[0] offset = QPointF(2.0, 2.0) self.calculateOffsets(shape, point) self.prevPoint = point if not self.boundedMovepolygonShape(shape, point - offset): self.boundedMovepolygonShape(shape, point + offset) def undoLastLine(self): assert self.polygons self.current = self.polygons.pop() self.current.setOpen() self.line.points = [self.current[-1], self.current[0]] self.drawingPolygon.emit(True) def resetAllLines(self): assert self.polygons self.current = self.polygons.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.rectangleVertrixcheck = False self.polygons = [] self.repaint() def loadShapes(self, shapes): self.polygons = list(shapes) self.current = None 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() # polygon addtional def squarepoint(self, points, pos): dx = points[1].x() - pos.x() dy = points[1].y() - pos.y() u = (points[0].x() - points[1].x()) * dx + (points[0].y() - points[1].y()) * dy div = dx * dx + dy * dy if div == 0: div = 1 u = u / div x1 = points[1].x() + u * dx y1 = points[1].y() + u * dy point1 = QPointF(x1, y1) # x2 = points[0].x() + points[1].x() - x1 y2 = points[0].y() + points[1].y() - y1 point2 = QPointF(x2, y2) return point1, point2 def getLine(self, pointbox, point2): x1 = pointbox[-1].x() y1 = pointbox[-1].y() x2 = point2.x() y2 = point2.y() c = x2 - x1 d = y2 - y1 if len(pointbox) <= 1: if c <= 0: # return x2+d,y2 -c,None,None return d, -c, None, None else: # return x2-d,y2 +c,None,None return -d, c, None, None else: if pointbox[-1].x() <= pointbox[-2].x(): x1_p = pointbox[-1].x() + (pointbox[-1].y() - pointbox[-2].y()) y1_p = pointbox[-1].y() - (pointbox[-1].x() - pointbox[-2].x()) else: x1_p = pointbox[-1].x() - (pointbox[-1].y() - pointbox[-2].y()) y1_p = pointbox[-1].y() + (pointbox[-1].x() - pointbox[-2].x()) c_p = x1_p - pointbox[-1].x() d_p = y1_p - pointbox[-1].y() if abs(d / c) > abs(d_p / c_p): return d, -c, x1_p, y1_p else: return -d, c, x1_p, y1_p def rotatepoint(self, point1, point2, point3, targetpoint1, check): div1 = (point2.x() - point1.x()) div2 = (point3.x() - point1.x()) if div1 == 0: div1 = 0.001 if div2 == 0: div2 = 0.001 iniangle = math.atan((point2.y() - point1.y()) / div1) newangle = math.atan((point3.y() - point1.y()) / div2) d_angle = newangle - iniangle if point3.x() < point1.x() and check == 0: d_angle = d_angle - math.pi if point3.x() > point1.x() and check == 1: d_angle = d_angle - math.pi cosd = math.cos(d_angle) sind = math.sin(d_angle) pointx = (targetpoint1.x() - point1.x()) * cosd - ( targetpoint1.y() - point1.y()) * sind + point1.x() pointy = (targetpoint1.x() - point1.x()) * sind + ( targetpoint1.y() - point1.y()) * cosd + point1.y() targetpoint2 = QPointF(pointx, pointy) # print('------------') # print(targetpoint1) # print('#########') # print(targetpoint2) # print('------------') return targetpoint2 def deleteSelected(self): if self.selectedpolygon2: shape = self.selectedpolygon2 self.selectedpolygon.innerpolygons.remove(self.selectedpolygon2) self.selectedpolygon2 = None self.update() return shape, 0 elif self.selectedpolygon: shape = self.selectedpolygon self.polygons.remove(self.selectedpolygon) self.selectedpolygon = None self.update() return shape, 1 def getrectanglepoints(self, points): newpoints = [] newpoints.append(points[0]) newpoints.append(QPointF(points[1].x(), points[0].y())) newpoints.append(points[1]) newpoints.append(QPointF(points[0].x(), points[1].y())) return newpoints def polfinalise(self): assert self.polcurrent self.polcurrent.close() self.polshapes.append(self.polcurrent) self.storeShapes() if self.drawing(): self.polygons.append(self.polcurrent) elif self.polygonDrawing() and self.selectedpolygon: self.selectedpolygon.innerpolygons.append(self.polcurrent) self.polcurrent = None self.update() def storeShapes(self): shapesBackup = [] for shape in self.polshapes: shapesBackup.append(shape.copy()) if len(self.shapesBackups) >= 10: self.shapesBackups = self.shapesBackups[-9:] self.shapesBackups.append(shapesBackup) def polcloseEnough(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.polepsilon def selectShape(self, polygon): self.deSelectpolygon() polygon.selected = True if self.polygonEditing(): self.selectedpolygon2 = polygon else: self.selectedpolygon = polygon self.setHiding() if not self.polygonEditing(): self.selectionChanged.emit(True) self.update() def selectpolygonPoint(self, point): """Select the first polygon created which contains this point.""" if not self.selectedpolygon or not (self.hpolygonVertex != None): self.deSelectpolygon() if self.selectedpolygonVertex(): # A vertex is marked for selection. index, polygon = self.hVertex, self.hpolygon polygon.highlightVertex(index, polygon.MOVE_VERTEX) self.selectShape(polygon) return if self.polygonDrawing() or self.polygonEditing(): if self.polcurrent and self.polygonDrawing(): pass elif self.selectedpolygon and self.polygonEditing(): for polygon in reversed(self.selectedpolygon.innerpolygons): if polygon.containsPoint(point): polygon.selected = True self.selectedpolygon2 = polygon self.calculateOffsets(polygon, point) self.setHiding() self.update() return else: for polygon in reversed(self.polygons): if self.isVisible(polygon) and polygon.containsPoint(point): polygon.selected = True self.selectedpolygon = polygon self.calculateOffsets(polygon, point) self.setHiding() self.selectionChanged.emit(True) self.update() return def deSelectpolygon(self): if self.selectedpolygon2 and self.polygonEditing(): self.selectedpolygon2.selected = False self.selectedpolygon2 = None self.setHiding(False) self.update() elif self.selectedpolygon and not self.polygonEditing(): if self.selectedpolygon2: self.selectedpolygon2.selected = False self.selectedpolygon2.highlightClear() self.selectedpolygon2 = None self.selectedpolygon.selected = False self.selectedpolygon.highlightClear() self.selectedpolygon = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def selectedpolygonVertex(self): return self.hpolygonVertex is not None def boundedMovepolygonVertex(self, pos): index, shape = self.hpolygonVertex, self.hpolygon point = shape[index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) shape.moveVertexBy(index, pos - point) def boundedMoverectangleVertex(self, pos): index, shape = self.hpolygonVertex, self.hpolygon point = shape.points[index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) shiftPos = pos - point 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) # shape.points = self.rectanglepointsresize(shape) def boundedMoveXrectangleVertex(self, pos): index, shape = self.hpolygonVertex, self.hpolygon if index == 1 or index == 3: points = (shape.points[index - 3], shape.points[index - 1]) point1, point2 = self.squarepoint(points, pos) shape.points[index] = point1 shape.points[index - 2] = point2 elif index == 0 or index == 2: scale = utils.distance(pos - self.tempbox[index - 2] ) / utils.distance(self.tempbox[index] - self.tempbox[index - 2]) if index == 0: check = 1 else: check = 0 newpoint1x = self.tempbox[index - 2].x() - ( scale * (self.tempbox[index - 2].x() - self.tempbox[index - 1].x())) newpoint1y = self.tempbox[index - 2].y() + ( scale * (self.tempbox[index - 1].y() - self.tempbox[index - 2].y())) newpoint1 = QPointF(newpoint1x, newpoint1y) newpoint2x = self.tempbox[index - 2].x() - ( scale * (self.tempbox[index - 2].x() - self.tempbox[index - 3].x())) newpoint2y = self.tempbox[index - 2].y() + ( scale * (self.tempbox[index - 3].y() - self.tempbox[index - 2].y())) newpoint2 = QPointF(newpoint2x, newpoint2y) shape.points[index - 1] = self.rotatepoint(self.tempbox[index - 2], self.tempbox[index], pos, newpoint1, check) shape.points[index - 3] = self.rotatepoint(self.tempbox[index - 2], self.tempbox[index], pos, newpoint2, check) shape.points[index] = pos # if index == 0: # for inpolygon in shape.innerpolygons: # for point in inpolygon.points: # inpolygon.points[inpolygon.points.index(point)] = self.rotatepoint(self.tempbox[index-2],self.tempbox[index],pos,point,check) def rectanglepointsresize(self, polygon): newpointbox = [ QPointF(0, 0), QPointF(0, 0), QPointF(0, 0), QPointF(0, 0) ] xpoint = [] ypoint = [] for point in polygon.points: xpoint.append(point.x()) ypoint.append(point.y()) for point in polygon.points: if point.x() == min(xpoint) and point.y() == min(ypoint): newpointbox[0] = point elif point.x() == max(xpoint) and point.y() == min(ypoint): newpointbox[1] = point elif point.x() == max(xpoint) and point.y() == max(ypoint): newpointbox[2] = point elif point.x() == min(xpoint) and point.y() == max(ypoint): newpointbox[3] = point return newpointbox def boundedMovepolygonShape(self, shape, 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() - 2), min(0, self.pixmap.height() - o2.y()) - 2) # 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. # dp = pos - self.prevPoint if dp: shape.moveBy(dp) self.prevPoint = pos return True return False