class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT, CMASK, EMASK = list(range(4)) #BBOX->bounding box, LINE_MASK -> for lines, box-mask-> closed shape BBOX, LINE_MASK, BOX_MASK = 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.pVertex = None #polyline vertex self.maskType = 0 self.prevPoint = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.0 self.rotate = 0 self.penWidth = 13 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.hsv = (0, 0, 0) 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 editMask(self): return self.mode == self.EMASK def masking(self): return self.mode == self.CMASK 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 setEditMask(self, value=True): self.mode = self.EMASK if value else self.CMASK #TODO: create sliders for CV 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())) if self.masking(): self.overrideCursor(CURSOR_DRAW) # Polygon drawing. if self.drawing() or self.masking(): 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) 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() 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() 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) 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) elif self.masking(): self.handleMasking(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() #elif ev.button() == Qt.RightButton and self.editing(): 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() 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.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 handleMasking(self, pos): if not self.outOfPixmap(pos): if self.pVertex is None: self.pVertex = Shape(line_color=self.drawingLineColor) if self.maskType == self.BOX_MASK: self.drawingPolygon.emit(True) self.pVertex.penWidth = 2 self.pVertex.close() else: self.pVertex.penWidth = self.penWidth self.pVertex.addPoint(pos) self.pVertex.maskType = self.maskType self.pVertex.highlightVertex( len(self.pVertex) - 1, self.pVertex.MOVE_VERTEX) 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 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 boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape point = shape[index] if self.outOfPixmap(pos): pos = self.intersectionPoint(point, pos) shiftPos = pos - point shape.moveVertexBy(index, shiftPos) #only move one vertex if shape.maskType in [self.LINE_MASK, self.BOX_MASK]: return 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 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 shape.maskType = self.maskType 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) 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): if not shape.maskType == self.LINE_MASK: 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) # paint polyline if self.masking() and self.pVertex: self.pVertex.paint(p) # 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) 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): 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()) if self.masking(): 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.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() 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.maskType = self.maskType self.current.close() self.shapes.append(self.current) self.current = None self.setHiding(False) self.newShape.emit() self.update() def finaliseMask(self): assert self.pVertex if self.pVertex.points[0] == self.pVertex.points[-1]: return #self.pVertex.addPath() self.pVertex.maskType = self.maskType self.pVertex.penWidth = self.penWidth self.shapes.append(self.pVertex) self.pVertex = None self.setHiding(False) self.newShape.emit() self.update() #call it while saving def getMask(self, points, maskType): #Todo: calculate path and then pixmask while saving Green = QColor(0, 255, 0) White = QColor(255, 255, 255) maskPixmap = QPixmap(self.pixmap.size()) maskPixmap.fill(White) t = QPainter(maskPixmap) if maskType == self.LINE_MASK: # path mask line_path = QPainterPath() line_path.moveTo(QPointF(points[0][0], points[0][1])) for i, p in enumerate(points): line_path.lineTo(QPointF(p[0], p[1])) pen = QPen(Green) pen.setWidth(self.penWidth) t.setPen(pen) t.drawPath(line_path) elif maskType == self.BOX_MASK: # box mask poly = [] for point in points: poly.append(QPointF(point[0], point[1])) brush = QBrush(Green, Qt.SolidPattern) t.setBrush(brush) t.drawPolygon(QPolygonF(poly)) # t.save() mask = maskPixmap.createMaskFromColor(White, Qt.MaskInColor) #make copy of self.pixmap before applying mask #xPixmap = QPixmap(self.pixmap) #xPixmap.setMask(mask) #tPixmap = xPixmap.transformed(QMatrix().rotate(self.rotate), Qt.SmoothTransformation) #tmask = mask.transformed(QMatrix().rotate(self.rotate)) points are already transformed #file = QFile("/home/neeraj/mask.png") #file.open(QIODevice.WriteOnly) #tPixmap.save(file,"PNG") t.end() #return tPixmap, tmask return mask 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() # Take care of line mask if key == Qt.Key_Escape and self.pVertex: self.pVertex = None self.update() elif key == Qt.Key_F10 and self.masking(): self.finaliseMask() 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') 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): 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 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, retain=False): self.pixmap = pixmap if not retain: 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()
class Canvas(QWidget): drawingPolygon = pyqtSignal(bool) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() finishedDrawing = pyqtSignal() epsilon = 11.0 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__pixmap = QPixmap() self.__mode = PMode.IDLE self.__prevmode = PMode.IDLE self.__current = None self.__hShape = None self.__hVertex = None self.__shapes = [] self.__drawingLineColor = QColor(45, 168, 179) self.__drawingRectColor = QColor(45, 168, 179) self.__line = Shape(line_color=self.drawingLineColor) self.__prevPoint = QPointF() self.__selectedShape = None self.__verified = False self.__hideBackground = False self.__toggleBackground = False self.__visible = {} self.scale = 1.0 self._painter = QPainter() self._cursor = CURSOR_DEFAULT # Load string bundle for i18n self.__stringBundle = StringBundle.getBundle() self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) ########################################################################### # S T A T E # ########################################################################### def resetState(self): self.restoreCursor() self.pixmap = None self.update() def updateInfoR(self, pos): currentMode = self.mode.name window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText( 'MODE: %s X: %d; Y: %d' % (currentMode, pos.x(), pos.y())) ########################################################################### # E D I T # ########################################################################### def setEditing(self, value=True): self.mode = PMode.EDIT if value else PMode.CREATE def drawing(self): return self.mode == PMode.CREATE def editing(self): return self.mode == PMode.EDIT ########################################################################### # P O S E S # ########################################################################### def transformPos(self, point): # Convert from widget-logical coord to painter-logical coord. 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) ########################################################################### # C U R S O R # ########################################################################### 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 outOfPixmap(self, p): w, h = self.pixmap.width(), self.pixmap.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) ########################################################################### # S H A P E S # ########################################################################### 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 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) 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 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 selectedVertex(self): return self.hVertex is not None 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 setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() ########################################################################### # D R A W I N G # ########################################################################### 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 hideBackroundShapes(self, value): self.toggleBackground = 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() 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.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.hideBackground = self.toggleBackground if enable else False 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 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 closeEnough(self, p1, p2): return distance(p1 - p2) < self.epsilon def isVisible(self, shape): return self.visible.get(shape, True) def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor 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 ########################################################################### # E V E N T S # ########################################################################### def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def keyPressEvent(self, ev: QKeyEvent) -> None: if ev.key() == Qt.Key_Control: if self.mode == PMode.CREATE: self.setEditing(True) def keyReleaseEvent(self, ev: QKeyEvent) -> None: if ev.key() == Qt.Key_Control: if self.mode == PMode.EDIT: self.setEditing(False) if self.hShape: self.hShape.highlightClear() self.update() self.hVertex, self.hShape = None, 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 self.updateInfoR(pos) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.hShape: self.hShape.highlightClear() 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: if False: 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 if self.editing(): # Polygon copy moving. if Qt.RightButton & ev.buttons() and False: 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() 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() return 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.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) 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 Qt.LeftButton == ev.button(): if self.drawing(): self.handleDrawing(pos) self.deSelectShape() if self.selectedVertex(): self.hShape.highlightClear() self.update() else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() else: pass def mouseReleaseEvent(self, ev): # if Qt.RightButton == ev.button(): # 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() # pass if Qt.LeftButton == ev.button() and self.selectedShape: if self.selectedVertex(): self.overrideCursor(CURSOR_POINT) else: self.overrideCursor(CURSOR_GRAB) elif Qt.LeftButton == ev.button(): pos = self.transformPos(ev.pos()) if self.drawing(): self.finishedDrawing.emit() self.handleDrawing(pos) def paintEvent(self, event): if not self.pixmap: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) # #^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^# # # --------------------------- begin paint --------------------------- # # #^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^# # 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.hideBackground) \ 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) # 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) 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): 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.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) # #^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^# # # ---------------------------- end paint ---------------------------- # # #^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^^vv^# # p.end() ########################################################################### # G E T T E R # ########################################################################### def __getStr(self, strId): return self.__stringBundle.getString(strId) def __getVerified(self): return self.__verified def __getPixmap(self): return self.__pixmap def __getShapes(self): return self.__shapes def __getMode(self): return self.__mode def __getCurrent(self): return self.__current def __getLine(self): return self.__line def __getPrevPoint(self): return self.__prevPoint def __getDrawingLineColor(self): return self.__drawingLineColor def __getDrawingRectColor(self): return self.__drawingRectColor def __getSelectedShape(self): return self.__selectedShape def __getHideBackground(self): return self.__hideBackground def __getToggleBackground(self): return self.__toggleBackground def __getVisible(self): return self.__visible def __getHShape(self): return self.__hShape def __getHVertex(self): return self.__hVertex ########################################################################### # S E T T E R # ########################################################################### def setLastLabel(self, text, line_color=None, fill_color=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 return self.shapes[-1] def __setVerified(self, x): if isinstance(x, bool): self.__verified = x else: raise ValueError(x, self.__getStr('boolE')) def __setPixmap(self, x): if isinstance(x, QPixmap) or x is None: self.__pixmap = x else: raise ValueError(x, self.__getStr('pixmapE')) def __setShapes(self, x): self.__shapes = x def __setMode(self, x): if isinstance(x, PMode): self.__mode = x else: raise ValueError(x, self.__getStr('modeE')) def __setCurrent(self, x): if isinstance(x, Shape) or x is None: self.__current = x else: raise ValueError(x, self.__getStr('shapeE')) def __setLine(self, x): if isinstance(x, Shape) or x is None: self.__line = x else: raise ValueError(x, self.__getStr('shapeE')) def __setPrevPoint(self, x): if isinstance(x, QPointF): self.__prevPoint = x else: raise ValueError(x, self.__getStr('qpointfE')) def __setdrawingLineColor(self, x): if isinstance(x, QColor): self.__drawingLineColor = x else: raise ValueError(x, self.__getStr('colorE')) def __setdrawingRectColor(self, x): if isinstance(x, QColor): self.__drawingRectColor = x else: raise ValueError(x, self.__getStr('colorE')) def __setSelectedShape(self, x): if isinstance(x, Shape) or x is None: self.__selectedShape = x else: raise ValueError(x, self.__getStr('shapeE')) def __setHideBackground(self, x): if isinstance(x, bool): self.__hideBackground = x else: raise ValueError(x, self.__getStr('boolE')) def __setToggleBackground(self, x): if isinstance(x, bool): self.__toggleBackground = x else: raise ValueError(x, self.__getStr('boolE')) def __setVisible(self, x): if isinstance(x, bool): self.__visible = x else: raise ValueError(x, self.__getStr('dictE')) def __setHShape(self, x): if isinstance(x, Shape) or x is None: self.__hShape = x else: raise ValueError(x, self.__getStr('shapeE')) def __setHVertex(self, x): if isinstance(x, int) or x is None: self.__hVertex = x else: raise ValueError(x, self.__getStr('intE')) ########################################################################### # P R O P E R T I E S # ########################################################################### verified = property(__getVerified, __setVerified) pixmap = property(__getPixmap, __setPixmap) shapes = property(__getShapes, __setShapes) mode = property(__getMode, __setMode) current = property(__getCurrent, __setCurrent) line = property(__getLine, __setLine) prevPoint = property(__getPrevPoint, __setPrevPoint) drawingLineColor = property(__getDrawingLineColor, __setdrawingLineColor) drawingRectColor = property(__getDrawingRectColor, __setdrawingRectColor) selectedShape = property(__getSelectedShape, __setSelectedShape) hideBackground = property(__getHideBackground, __setHideBackground) toggleBackground = property(__getToggleBackground, __setToggleBackground) visible = property(__getVisible, __setVisible) hShape = property(__getHShape, __setHShape) hVertex = property(__getHVertex, __setHVertex)