def forward_json(self): json_path = self.seleDir + '\\' + self.dir_list[ self.dir_show_num].split('.', 2)[0] + '.json' if os.path.exists(json_path): with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) # self.tempDict = data[0]['labels'] # print(self.tempDict) # print(data) for shape in data: points = shape['position'] label = shape['labels'] start_point = QtCore.QPointF(points[0]['x'], points[0]['y']) # 左上 end_point = QtCore.QPointF(points[1]['x'], points[1]['y']) # 右下 two_points = Shape() two_points.addPoint(start_point) two_points.addPoint(end_point) four_points = self.canvas.points_to_point_four( copy.deepcopy(two_points)) with_points_shape = Shape() with_points_shape.points = four_points with_points_shape.close() # 闭合最后一条线 self.canvas.shapes.append(with_points_shape) self.canvas.shapes[-1].label = label self.canvas.repaint() else: self.tempDict = copy.deepcopy(self.exampleDict)
def loadLabels(self, shapes): s = [] for label, points, line_color, fill_color, difficult in shapes: shape = Shape(label=label) for x, y in points: # Ensure the labels are within the bounds of the image. # If not, fix them. x, y, snapped = self.canvas.snapPointToCanvas(x, y) if snapped: self.setDirty() shape.addPoint(QPointF(x, y)) shape.difficult = difficult shape.close() s.append(shape) if line_color: shape.line_color = QColor(*line_color) else: shape.line_color = generateColorByText(label) if fill_color: shape.fill_color = QColor(*fill_color) else: shape.fill_color = generateColorByText(label) self.addLabel(shape) # self.updateComboBox() self.canvas.loadShapes(s)
def load_record(self): if not self.may_continue(): return self.set_clean() self.clear_labels() item = self.recordList.selectedItems()[0] if not item: return self.current_record = item.text() if self.stream_enabled: self._stop_video() component = self.componentList.selectedItems()[0].text() filename = item.text().replace('№', '') f = filename.split(' ') filename = '{}{:04d}'.format(f[0], int(f[1])) path = os.path.join(self.path, component, 'records', filename) img_path = path + '.jpg' txt_path = path + '.txt' self.frame = cv2.imread(img_path) if self.frame is None: return self.frame = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB) self.canvas.loadPixmap( QPixmap.fromImage(qimage2ndarray.array2qimage(self.frame))) self.canvas.adjustSize() with open(txt_path, 'r') as bndboxes_file: for box in bndboxes_file: index, xcen, ycen, w, h = box.strip().split(' ') label = self.database_handler.classes[int(index)] xmin, ymin, xmax, ymax = yolo2points(xcen, ycen, w, h, self.frame.shape[1], self.frame.shape[0]) points = [(xmin, ymin), (xmax, ymin), (xmax, ymax), (xmin, ymax)] shape = Shape(label=label) for x, y in points: x, y, snapped = self.canvas.snapPointToCanvas(x, y) if snapped: self.set_dirty() shape.addPoint(QPointF(x, y)) shape.difficult = False shape.fill_color = generate_color_by_text(label) shape.line_color = generate_color_by_text(label) shape.close() self.shapes.append(shape) self.add_label(shape) self.canvas.loadShapes(self.shapes)
def loadLabels(shapes): s = [] for label, points, line_color, fill_color, difficult in shapes: shape = Shape(label=label) for x, y in points: # Ensure the labels are within the bounds of the image. If not, fix them. # x, y, snapped = self.canvas.snapPointToCanvas(x, y) # if snapped: # self.setDirty() shape.addPoint(QPointF(x, y)) shape.difficult = difficult shape.close() s.append(shape) return s
def randomTable(num): # Create a black image img = np.zeros((512, 512, 3), np.uint8) img.fill(255) left = random.randint(20, 50) top = random.randint(30, 50) width = random.randint(100, 150) height = random.randint(30, 70) row = random.randint(2, 4) column = random.randint(2, 3) if row == 1 and column == 1: width, height = width * 2, height * 2 rectShapes = [] # print("image:{}, row num={}, column num ={} ".format(num, row, column)) for r in range(0, row): for c in range(0, column): start_point = (left + c * width, top + r * height) end_point = (left + (c + 1) * width, top + (r + 1) * height) # 画框 thickness = [1, 2] # 表格线粗细随机 cv.rectangle(img, start_point, end_point, BLACK, random.choice(thickness)) points = [start_point, end_point] shape = Shape(label='rect') for x, y in points: shape.addPoint(Point(x, y)) shape.close() rectShapes.append(shape) imagePath = 'VOC2007/JPEGImages/' xmlPath = 'VOC2007/Annotations/' fileName = imagePath + num + IMAGE_EXT xmlName = xmlPath + num + XML_EXT # print('fileName=', fileName) cv.imwrite(fileName, img) savePascalVocFormat(xmlName, rectShapes, imagePath, img)
def createBox(self, pos=None): if pos is None: pos = self.mousepos if self.quickbox: if not self.outOfPixmap(pos): shape = Shape(label="sugar-beet") dim = self.box_size / 2 x = pos.x() y = pos.y() points = ((x - dim, y - dim), (x + dim, y - dim), (x + dim, y + dim), (x - dim, y + dim)) for p in points: shape.addPoint(QPointF(*p)) shape.close() self.shapes.append(shape) self.boxCreated.emit() return True return False
def loadLabels(self, shapes): s = [] for label, points, line_color, fill_color, difficult in shapes: shape = Shape(label=label) for x, y in points: shape.addPoint(QPointF(x, y)) shape.difficult = difficult shape.close() s.append(shape) self.addLabel(shape) if line_color: shape.line_color = QColor(*line_color) if fill_color: shape.fill_color = QColor(*fill_color) self.canvas.loadShapes(s)
def add_shape(self, bbox, label, bbox_source, id_number): is_gate = label == "gate" shape = Shape(label, bbox_source=bbox_source, id_number=id_number, is_gate=is_gate) xmin, ymin, xmax, ymax = bbox if is_gate: shape.addPoint(QPointF(xmin, ymin)) shape.addPoint(QPointF(xmax, ymax)) else: shape.addPoint(QPointF(xmin, ymin)) shape.addPoint(QPointF(xmax, ymin)) shape.addPoint(QPointF(xmax, ymax)) shape.addPoint(QPointF(xmin, ymax)) shape.close() self.main_window.addLabel(shape) self.shapes.append(shape)
class Canvas(QWidget): """ :type main_window: MainWindow """ zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT = list(range(2)) epsilon = 11.0 def __init__(self, main_window, *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.lineColor = QColor(0, 0, 255) self.line = Shape() 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.main_window = main_window self.has_text = False self.is_gate = False self.last_click_t = 0 def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True, is_gate=False, id_number=None): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.is_gate = is_gate self.id_number = id_number 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()) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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'\n" "Double click to add mask" % 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) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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: # click_t = time.time() # if self.last_click_t: # click_interval = click_t - self.last_click_t # self.last_click_t = 0 # if click_interval < 1: # # print('Double click detected') # self.selectedShape.addMask(self.main_window.image, # self.main_window.args.mask_disp_size, # self.main_window.args.mask_del_thresh, # self.main_window.args.mask_magnified_window) # # self.selectedShape.addMask(self.main_window.image) # else: # self.last_click_t = click_t 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: if self.is_gate: targetPos = self.line[1] self.current.addPoint(targetPos) else: 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(is_gate=self.is_gate, id_number=self.id_number) 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() elif self.selectedShape is not None: self.selectedShape.addMask(self.main_window.image_np, self.main_window.params.mask, self.main_window.augment, self.main_window.hed_net ) 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): try: pos = self.intersectionPoint(point, pos) except ValueError: return shiftPos = pos - point shape.moveVertexBy(index, shiftPos) if shape.is_gate: _shift = QPointF(shiftPos.x(), shiftPos.y()) shape.moveVertexBy(index, _shift) else: 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 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 paintGates(self, painter, gates): for gate in gates: points = gate['points'] gate_id = gate['id'] color = self.select_line_color if self.selected else self.line_color pen = QPen(color) # Try using integer sizes for smoother drawing(?) pen.setWidth(max(1, int(round(2.0 / self.scale)))) painter.setPen(pen) line_path = QPainterPath() vrtx_path = QPainterPath() line_path.moveTo(points[0]) # Uncommenting the following line will draw 2 paths # for the 1st vertex, and make it non-filled, which # may be desirable. # self.drawVertex(vrtx_path, 0) for i, p in enumerate(points): line_path.lineTo(p) self.drawVertex(vrtx_path, i) if self.isClosed(): line_path.lineTo(points[0]) painter.drawPath(line_path) painter.drawPath(vrtx_path) painter.fillPath(vrtx_path, self.vertex_fill_color) if self.fill: color = self.select_fill_color if self.selected else self.fill_color painter.fillPath(line_path, color) if self.has_text: self.drawText(painter) def paintEvent(self, event): if not self.pixmap: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) # if self.main_window.gates is not None: # self.paintGates(p, self.main_window.gates) 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] color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) if self.is_gate: p.drawLine(leftTop.x(), leftTop.y(), rightBottom.x(), rightBottom.y()) else: rectWidth = rightBottom.x() - leftTop.x() rectHeight = rightBottom.y() - leftTop.y() 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) 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.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() 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.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) if not self.selectedShape.is_gate: 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) if not self.selectedShape.is_gate: 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) if not self.selectedShape.is_gate: 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) if not self.selectedShape.is_gate: 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): assert text self.shapes[-1].label = text 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.set_show_text() 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 add_shape(self, bbox, label, bbox_source, id_number): is_gate = label == "gate" shape = Shape(label, bbox_source=bbox_source, id_number=id_number, is_gate=is_gate) xmin, ymin, xmax, ymax = bbox if is_gate: shape.addPoint(QPointF(xmin, ymin)) shape.addPoint(QPointF(xmax, ymax)) else: shape.addPoint(QPointF(xmin, ymin)) shape.addPoint(QPointF(xmax, ymin)) shape.addPoint(QPointF(xmax, ymax)) shape.addPoint(QPointF(xmin, ymax)) shape.close() self.main_window.addLabel(shape) self.shapes.append(shape) def set_show_text(self, _status=None): if _status is None: _status = self.has_text self.has_text = _status for shape in self.shapes: shape.has_text = _status
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)) 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.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.line2 = [] 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.tetragonMode = False self.hVertex = None self.rotationPoint = None self._painter = QPainter() self._cursor = CURSOR_DEFAULT self.draw3DMode = False self.clicksCount = 0 self.sk = 0 # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) self.verified = False def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def deSelectShape(self): if self.selectedShape: self.lastSelected = self.selectedShape self.selectedShape.selected = False self.selectedShape = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def selectedVertex(self): return self.hVertex is not None def selectedRotationPoint(self): return self.rotationPoint is not None def pointsCountingFor3DShape(self, pos): pos = QPointF(round(pos.x()), round(pos.y())) self.clicksCount += 1 if self.clicksCount == 4: self.draw3DMode = False self.finalise() def addVirtualPoint(self, pointA, pointB, pos): deltaX = pointA.x() - pointB.x() deltaY = pointA.y() - pointB.y() delta = QPointF(deltaX, deltaY) self.current.addPoint(pos - delta) def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) self.restoreCursor() # 3D drawing while drawing if self.draw3DMode: self.overrideCursor(CURSOR_DRAW) pos = QPointF(round(pos.x()), round(pos.y())) if self.clicksCount == 0: if len(self.current.points) == 0: self.current.addPoint(pos) elif len(self.current.points) == 1: self.current.points[0] = pos if self.clicksCount == 1: if len(self.current.points) == 1: self.current.addPoint(pos) elif len(self.current.points) == 2: self.current.points[1] = pos if self.clicksCount == 2: if len(self.current.points) == 2: self.current.addPoint(pos) self.addVirtualPoint(self.current[1], self.current[0], pos) elif len(self.current.points) == 4: del self.current.points[3] del self.current.points[2] self.current.addPoint(pos) self.addVirtualPoint(self.current[1], self.current[0], pos) if self.clicksCount == 3: if len(self.current.points) == 4: self.addVirtualPoint(self.current[1], self.current[0], pos) self.current.addPoint(pos) self.addVirtualPoint(self.current[2], self.current[1], pos) self.addVirtualPoint(self.current[2], self.current[0], pos) elif len(self.current.points) == 8: del self.current.points[7] del self.current.points[6] del self.current.points[5] del self.current.points[4] self.addVirtualPoint(self.current[1], self.current[0], pos) self.current.addPoint(pos) self.addVirtualPoint(self.current[2], self.current[1], pos) self.addVirtualPoint(self.current[2], self.current[0], pos) self.repaint() return # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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.repaint() self.current.highlightClear() 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() elif self.selectedRotationPoint: self.overrideCursor(CURSOR_MOVE) try: self.rotateShape(self.hShape, pos) except: pass 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. # print(shape.points) index = shape.nearestVertex(pos, self.epsilon) # print(overTheDot) if index is not None: # print(index) 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.overRotationPoint(pos, self.epsilon): self.hShape = self.selectedShape self.overrideCursor(CURSOR_POINT) # self.setToolTip("Click & drag to rotate label") # self.setStatusTip(self.toolTip()) self.update() break elif shape.containsPoint(pos): # print(shape.points) 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 def getRotatedShape(self, shape, angle): return [ Shape.rotatePoint(self, shape.centerPoint, point, angle) for point in shape.points ] def rotateShape(self, shape, pos): zeroPoint = shape.centerPoint + QPointF(0, -10) deg = self.angleBetween3Points(zeroPoint, shape.centerPoint, pos) delta = -(shape.deg - deg) if delta != 0: rotatedShape = self.getRotatedShape(shape, delta) if self.shapeOutOfPixmap(rotatedShape) == False: shape.points = rotatedShape shape.deg += delta def angleBetween3Points(self, p0, c, p1): p0c = math.sqrt( math.pow(c.x() - p0.x(), 2) + math.pow(c.y() - p0.y(), 2)) # p0->c (b) p1c = math.sqrt( math.pow(c.x() - p1.x(), 2) + math.pow(c.y() - p1.y(), 2)) # p1->c (a) p0p1 = math.sqrt( math.pow(p1.x() - p0.x(), 2) + math.pow(p1.y() - p0.y(), 2)) # p0->p1 (c) ratio = math.acos( (p1c * p1c + p0c * p0c - p0p1 * p0p1) / (2 * p1c * p0c)) deg = round(((ratio * 180) / math.pi)) if c.x() < p1.x(): deg = 360 - deg if deg == 360: return 0 return deg def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing(): self.handleDrawing(pos) elif self.draw3DMode: self.pointsCountingFor3DShape(pos) self.repaint() else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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 self.draw3DMode: self.overrideCursor(CURSOR_DRAW) elif ev.button() == Qt.LeftButton and self.selectedShape: 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.current.addPoint(initPos) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif not self.outOfPixmap(pos): self.current = Shape() self.current.addPoint(pos) if self.tetragonMode == True: self.current.tetragon = True 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 transformTetragon(self, points): arrX = [] arrY = [] for x in points: arrX.append(x.x()) arrY.append(x.y()) return [min(arrX), min(arrY), max(arrX), max(arrY)] 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) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) 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 if self.shapeOutOfPixmap(shape.points): #this is pre fix. For bug when shape reach Picmap edge. return if shape.shape3D: # 3D editing OFF return rotatedAxis = False if shape.deg > 0 and not shape.tetragon: rotatedAxis = True shape.points = self.getRotatedShape(shape, -shape.deg) pos = Shape.rotatePoint(self, shape.centerPoint, pos, -shape.deg) point = shape[index] if self.outOfPixmap(pos): if self.shapeOutOfPixmap(shape.points) == False: 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: #lyginiai index if shape.tetragon == True: rshift = QPointF(0, 0) lshift = QPointF(0, 0) elif shape.tetragon == False: rshift = QPointF(shiftPos.x(), 0) lshift = QPointF(0, shiftPos.y()) else: #nelyginiai index if shape.tetragon == True: lshift = QPointF(0, 0) rshift = QPointF(0, 0) elif shape.tetragon == False: lshift = QPointF(shiftPos.x(), 0) rshift = QPointF(0, shiftPos.y()) shape.moveVertexBy(rindex, rshift) shape.moveVertexBy(lindex, lshift) if rotatedAxis: shape.points = self.getRotatedShape(shape, shape.deg) pos = Shape.rotatePoint(self, shape.centerPoint, pos, shape.deg) 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 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) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.selectedShapeCopy: self.selectedShapeCopy.paint(p) # Paint rect if self.current is not None and len( self.line) == 2 and self.draw3DMode == False: leftTop = self.line[0] rightBottom = self.line[1] rectWidth = rightBottom.x() - leftTop.x() rectHeight = rightBottom.y() - leftTop.y() color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) 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 shapeOutOfPixmap(self, points): for p in points: if self.outOfPixmap(p): return True return False def finalise(self): assert self.current 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() 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 print(0) else: h_delta = ev.delta() v_delta = 0 print(1) else: delta = ev.angleDelta() h_delta = delta.x() v_delta = delta.y() print(2) mods = ev.modifiers() if Qt.ControlModifier == int(mods) and v_delta: self.zoomRequest.emit(v_delta) print(3) # print(Qt.ControlModifier) else: print(4) v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical) h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal) # print(v_delta) # print(h_delta) 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') 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): assert text self.shapes[-1].label = text 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 overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QApplication.setOverrideCursor(cursor) def restoreCursor(self): QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = None self.update()
class Canvas(QWidget): # 自定义的信号 zoomRequest = pyqtSignal(int) # 缩放图像释放的信号 scrollRequest = pyqtSignal(int, int) # 在放大的图像上滚动滚轮以显示不同位置释放的信号 newShape = pyqtSignal() # 创建新bbox的信号 selectionChanged = pyqtSignal(bool) # 改变bbox的边界 shapeMoved = pyqtSignal() # 改变bbox的位置 drawingPolygon = pyqtSignal(bool) # 鼠标拖动绘制信号 CREATE, EDIT = list(range(2)) # 两种模式,CREATE模式为0,EDIT模式为1 epsilon = 11.0 # TODO 这是啥 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT # 默认为EDIT模式 self.shapes = [] # 用于存储当前图像上的bbox,内容为current(libs.shape.py中Shape()类型的数据结构) self.current = None # 用于存储当前绘制的bbox的四个角的坐标的数据 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 = {} # TODO 这是啥 self._hideBackround = False # TODO 这两个是啥? 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) # 鼠标追踪,要想实现mouseMoveEvent,则需要开启鼠标追踪 self.setFocusPolicy(Qt.WheelFocus) # 与焦点设置有关,tab+鼠标滚轮选择焦点 self.verified = False # TODO 这个是啥 self.drawSquare = False # TODO 盲猜点Create RectBox设为True,画完BBox后变为False #initialisation for panning self.pan_initial_pos = QPoint() # QPoint代表一个坐标点 def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): # 移动鼠标时即刻追踪 """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText( 'X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: # Display annotation width and height while drawing currentWidth = abs(self.current[0].x() - pos.x()) currentHeight = abs(self.current[0].y() - pos.y()) self.parent().window().labelCoordinates.setText( 'Width: %d, Height: %d / X: %d; Y: %d' % (currentWidth, currentHeight, pos.x(), pos.y())) color = self.drawingLineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Clip the coordinates to 0 or max, # if they are outside the range [0, max] size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) elif len(self.current) > 1 and self.closeEnough(pos, self.current[0]): # Attract line to starting point and colorise to alert the # user: pos = self.current[0] color = self.current.line_color self.overrideCursor(CURSOR_POINT) self.current.highlightVertex(0, Shape.NEAR_VERTEX) if self.drawSquare: 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() 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() else: #pan delta_x = pos.x() - self.pan_initial_pos.x() delta_y = pos.y() - self.pan_initial_pos.y() self.scrollRequest.emit(delta_x, Qt.Horizontal) self.scrollRequest.emit(delta_y, Qt.Vertical) self.update() return # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. self.setToolTip("Image") for shape in reversed([s for s in self.shapes if self.isVisible(s)]): # Look for a nearby vertex to highlight. If that fails, # check if we happen to be inside a shape. index = shape.nearestVertex(pos, self.epsilon) if index is not None: if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = index, shape shape.highlightVertex(index, shape.MOVE_VERTEX) self.overrideCursor(CURSOR_POINT) self.setToolTip("Click & drag to move point") self.setStatusTip(self.toolTip()) self.update() break elif shape.containsPoint(pos): if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = None, shape self.setToolTip( "Click & drag to move shape '%s'" % shape.label) 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): # 按下鼠标事件,开始拖动bbox pos = self.transformPos(ev.pos()) # TODO:【重要函数】用ev.pos()获得在缩放了的图像上的按键位置,在用transformPos变换到真实的坐标 if ev.button() == Qt.LeftButton: # 如果按的是左键 if self.drawing(): self.handleDrawing(pos) # TODO:【重点函数】获得按下鼠标左键时的坐标位置,传给handleDrawing函数,开始绘制BBox else: selection = self.selectShapePoint(pos) self.prevPoint = pos if selection is None: #pan QApplication.setOverrideCursor(QCursor(Qt.OpenHandCursor)) self.pan_initial_pos = pos elif ev.button() == Qt.RightButton and self.editing(): # 如果按的是右键,并且处于EDIT模式 self.selectShapePoint(pos) # 根据右键按在了图像上的位置,来判断是在哪个bbox上右击 self.prevPoint = pos self.update() 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: # 如果是左键点在了已经创建好的bbox上 if self.selectedVertex(): # TODO 这是啥 self.overrideCursor(CURSOR_POINT) else: self.overrideCursor(CURSOR_GRAB) elif ev.button() == Qt.LeftButton: # 其他情况,当前是在绘制新的bbox pos = self.transformPos(ev.pos()) # 获得鼠标位置在图像上的真实坐标 if self.drawing(): # 如果当前是画图模式 self.handleDrawing(pos) # 处理绘图功能 else: #pan QApplication.restoreOverrideCursor() 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() # 根据释放鼠标左键时光标所在图像上的坐标,生成bbox的另外三个角的坐标,并存入self.current中 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._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() # 没有选中bbox时的处理 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 self.hVertex for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): self.selectShape(shape) self.calculateOffsets(shape, point) return self.selectedShape return 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 snapPointToCanvas(self, x, y): """ Moves a point x,y to within the boundaries of the canvas. :return: (x,y,snapped) where snapped is True if x or y were changed, False if not. """ if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height(): x = max(x, 0) y = max(y, 0) x = min(x, self.pixmap.width()) y = min(y, self.pixmap.height()) return x, y, True return x, y, False def boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape point = shape[index] if self.outOfPixmap(pos): size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) if self.drawSquare: opposite_point_index = (index + 2) % 4 opposite_point = shape[opposite_point_index] min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y())) directionX = -1 if pos.x() - opposite_point.x() < 0 else 1 directionY = -1 if pos.y() - opposite_point.y() < 0 else 1 shiftPos = QPointF(opposite_point.x() + directionX * min_size - point.x(), opposite_point.y() + directionY * min_size - point.y()) else: shiftPos = pos - point 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 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) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.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) 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): # TODO:【重要函数】得到按下鼠标和释放鼠标的坐标时,开始绘制bbox assert self.current if self.current.points[0] == self.current.points[-1]: # 如果按下和释放鼠标为同一个位置,则不绘制bbox self.current = None self.drawingPolygon.emit(False) self.update() return self.current.close() self.shapes.append(self.current) self.current = None # 绘制完一个bbox后,重新初始化current self.setHiding(False) self.newShape.emit() # 弹出填写标注label的对话框 self.update() def closeEnough(self, p1, p2): #d = distance(p1 - p2) #m = (p1-p2).manhattanLength() # print "d %.2f, m %d, %.2f" % (d, m, d - m) return distance(p1 - p2) < self.epsilon # These two, along with a call to adjustSize are required for the # scroll area. def sizeHint(self): return self.minimumSizeHint() def minimumSizeHint(self): if self.pixmap: return self.scale * self.pixmap.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): qt_version = 4 if hasattr(ev, "delta") else 5 # ev应该是一种获取滚动的具体情况的数据类型 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() # 应该是用来表示滑动的距离的距离,单位为度的1/8,大多数鼠标以15度为步进工作 h_delta = delta.x() # 水平滚动距离(按住alt键后) v_delta = delta.y() # 垂直滚动距离(默认) mods = ev.modifiers() # 用来判断缩放时候是否按住了组合键 if Qt.ControlModifier == int(mods) and v_delta: # 如果按住了ctrl键并滚动,则代表是缩放功能 self.zoomRequest.emit(v_delta) else: v_delta and self.scrollRequest.emit(v_delta, Qt.Vertical) # 如果垂直滚动有数值,即没有按住alt键,则释放垂直滚动信号 h_delta and self.scrollRequest.emit(h_delta, Qt.Horizontal) # 如果水平滚动有数值,即按住了alt键,则释放水平滚动信号 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') 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): # TODO 重要函数,加载图像 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
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)) epsilon = 11.0 FIXED_WIDTH = 192 FIXED_HEIGHT = 192 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._painter = QPainter() self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) self.verified = False self.drawSquare = False self.drawRegion = True # Add by yuan : set drawRegion flag to true self.fileName = 'unknow' def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText('X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: 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) # add by yuan ==> draw region if self.drawRegion: # pos_tmp添加到self.pos_xy中 # pos_tmp = (pos.x(), pos.y()) if self.current: self.current.addPoint(pos) #self.line[1] = pos else: 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() 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 self.drawRegion: # pos_tmp添加到self.pos_xy中 if ev.button() == Qt.LeftButton: if self.drawing(): self.handleDrawing(pos) #self.pos_xy.append(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() else: if ev.button() == Qt.LeftButton: if self.drawing(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): if self.drawRegion: 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) else: 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.drawRegion: if self.current: #initPos = self.current[0] self.current.addPoint(self.line[1]) self.finalise_region() elif not self.outOfPixmap(pos): self.current = Shape(drawRegion=True) self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() else: if self.current and self.current.reachMaxPoints() is False: initPos = self.current[0] minX = initPos.x() minY = initPos.y() if Canvas.FIXED_HEIGHT == 0 and Canvas.FIXED_WIDTH == 0: targetPos = self.line[1] maxX = targetPos.x() maxY = targetPos.y() else: self.current.clearPoints() orig_minX = minX orig_minY = minY minX = minX - Canvas.FIXED_WIDTH / 2 minY = minY - Canvas.FIXED_HEIGHT / 2 maxX = minX + Canvas.FIXED_WIDTH maxY = minY + Canvas.FIXED_HEIGHT # lef scale if minX < 25: maxX = maxX + (25 - minX) minX = 25 if maxX > 740: minX = minX - (maxX - 740) maxX = 740 # top if minY < 155: maxY = maxY + (155 - minY) minY = 155 # if maxY > 540: minY = minY - (maxY - 540) maxY = 540 if orig_minX <= 378: # left image if minY <= 200 and maxX > 310: #avoid color bar if orig_minY < 200: minX = minX - (maxX - 310) maxX = 310 else: minX = minX - (maxX - 378) maxX = 378 maxY = maxY - (minY - 200) minY = 200 elif maxX > 378: minX = minX - (maxX - 378) maxX = 378 else: # right image if minY <= 200 and maxX > 690: # avoid color bar if orig_minY < 200: minX = minX - (maxX - 690) maxX = 690 else: minX = minX - (maxX - 740) maxX = 740 maxY = maxY - (minY - 192) minY = 200 else: if maxX > 740: minX = minX - (maxX - 740) maxX = 740 if minX < 378: maxX = maxX + (378 - minX) minX = 378 if minY < 170: maxY = maxY + (170 - minY) minY = 170 self.current.addPoint(QPointF(minX, minY)) targetPos = QPointF(maxX, maxY) self.line[1] = targetPos 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._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.drawRegion = shape.drawRegion # add by yuan 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 not shape.drawRegion: 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) 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 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) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) # Paint rect # add by yuan ==> draw region if self.drawRegion: if self.current: self.current.paint(p) else: #Shape.scale = self.scale #for shape in self.shapes: # if (shape.selected or not self._hideBackround) and self.isVisible(shape): # shape.fill = shape.selected or shape == self.hShape # shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.selectedShapeCopy: self.selectedShapeCopy.paint(p) 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) 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) # add by yuan def finalise_region(self): assert self.current # first equlas last is closed region if len(self.current.points) >= 10: if self.current.points[0] == self.current.points[-1]: self.current.close() self.shapes.append(self.current) self.current = None self.setHiding(False) self.drawingPolygon.emit(False) self.newShape.emit() # notify to create labedialog self.update() def finalise(self): assert self.current # first equlas last is closed region 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() # notify to create labedialog 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() 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) # add by yuan def covert2Rectangle(self, points): xmin = float('inf') ymin = float('inf') xmax = float('-inf') ymax = float('-inf') for p in points: x = p.x() y = p.y() xmin = min(x, xmin) ymin = min(y, ymin) xmax = max(x, xmax) ymax = max(y, ymax) # Martin Kersner, 2015/11/12 # 0-valued coordinates of BB caused an error while # training faster-rcnn object detector. if xmin < 1: xmin = 1 if ymin < 1: ymin = 1 width = int(xmax - xmin) if width < 1: width = 1 height = int(ymax - ymin) if height < 1: height = 1 return (int(xmin), int(ymin), width, height) def storePoint(self, points): xmin = float('inf') ymin = float('inf') xmax = float('-inf') ymax = float('-inf') for p in points: x = p.x() y = p.y() xmin = min(x, xmin) ymin = min(y, ymin) xmax = max(x, xmax) ymax = max(y, ymax) # Martin Kersner, 2015/11/12 # 0-valued coordinates of BB caused an error while # training faster-rcnn object detector. if xmin < 1: xmin = 1 if ymin < 1: ymin = 1 return (int(xmin), int(ymin), int(xmax), int(ymax)) # add by yuan def saveSplitImage(self, shape, split_img, ismask, image_side, serial_num): print("begin - saveSplitImage") label_name = 'unknow' if not shape.label is None: label_name = str(shape.label) #self.fileName if ismask: img_file = '{}_{}_{}_{}_mask.jpg'.format(label_name, self.fileName, image_side, serial_num) else: img_file = '{}_{}_{}_{}.jpg'.format(label_name, self.fileName, image_side, serial_num) img_path = path.abspath('breast dataset/train dataset') if not path.exists(img_path): mkdir(img_path) split_img.save(path.join(img_path, img_file), 'JPG', 100) print("end - saveSplitImage ") def saveSplitHSImage(self, shape, split_img, image_side, serial_num): print("begin - saveSplitHSImage") label_name = 'unknow' if not shape.label is None: label_name = str(shape.label) # self.fileName img_file = '{}_{}_{}_{}_HS.jpg'.format(label_name, self.fileName, image_side, serial_num) afimg_file = '{}_{}_{}_{}_after.jpg'.format(label_name, self.fileName, image_side, serial_num) beimg_file = '{}_{}_{}_{}_before.jpg'.format(label_name, self.fileName, image_side, serial_num) img_path = path.abspath('breast dataset/train HSdataset') if not path.exists(img_path): mkdir(img_path) splitimg = split_img.toImage() splitimg = splitimg.convertToFormat(QImage.Format_Grayscale8) splitimg = qp.qimageview(splitimg) splitimg = np.stack((splitimg, ) * 3, axis=-1) #print (splitimg) min_gray = np.min(splitimg) max_gray = np.max(splitimg) # print (img[0][:10]) print('min:', min_gray, 'max:', max_gray) x_axis = list(range(0, 255)) #y_axis = splitimg.flatten().tolist() plt.figure() plt.hist(splitimg.flatten().tolist(), bins=x_axis) plt.title('Original Histogram - Min:{} Max:{}'.format( min_gray, max_gray)) plt.savefig(path.join(img_path, beimg_file)) scale = 255 / (max_gray - min_gray) print(scale) trasfor_img = (splitimg - min_gray) splitimg = trasfor_img * scale splitimg = splitimg.astype(int) #splitimg.save(path.join(img_path, img_file), 'JPG', 100) cv2.imwrite(path.join(img_path, img_file), splitimg, [cv2.IMWRITE_JPEG_QUALITY, 100]) min_gray = np.min(splitimg) max_gray = np.max(splitimg) print('min:', min_gray, 'max:', max_gray) plt.figure() plt.hist(splitimg.flatten().tolist(), bins=x_axis) plt.title('Histogram Stretching - Min:{} Max:{}'.format( min_gray, max_gray)) plt.savefig(path.join(img_path, afimg_file)) print("end - saveHSSplitImage ") def saveSelectedShape(self): print('begin - saveSelectedShape') #select_shape = self.selectedShape # the tiles are known to divide evenly # width = self.pixmap.width() height = self.pixmap.height() str_time = strftime("%m%d%H%M%S", gmtime()) shape = self.selectedShape imgside = 'right' if shape.drawRegion: print("start - saveRegionByOutsideRectangle") rect = self.storePoint(shape.points) rect = self.findFitRectangleShape(rect) split_img = self.pixmap.copy(rect[0], rect[1], rect[2], rect[3]) if rect[0] < 380: imgside = 'left' self.saveSplitImage(shape, split_img, False, imgside, str_time) self.saveSplitHSImage(shape, split_img, imgside, str_time) self.saveRegionByOutsideRectangle(shape, rect, str_time) print("end - saveRegionByOutsideRectangle") else: print("start - findContainRegion") rect = self.covert2Rectangle(shape.points) split_img = self.pixmap.copy(rect[0], rect[1], rect[2], rect[3]) if rect[0] < 380: imgside = 'left' self.saveSplitImage(shape, split_img, False, imgside, str_time) self.saveSplitHSImage(shape, split_img, imgside, str_time) self.findContainRegion(shape, rect, str_time) print("end - findContainRegion") QMessageBox.information(self, "Save Image", "Save finished!") # add by yuan def findFitRectangleShape(self, rectangle): min_rectx = 0 min_recty = 0 max_rectx = float('inf') max_recty = float('inf') isfindrect = False for shape in self.shapes: if not shape.drawRegion: rect = self.storePoint(shape.points) if rect[0] <= rectangle[0] and rect[1] <= rectangle[1]: if rect[2] >= rectangle[2] and rect[3] >= rectangle[3]: if rect[0] >= min_rectx and rect[1] >= min_recty: if rect[2] <= max_rectx and rect[3] <= max_recty: min_rectx = rect[0] min_recty = rect[1] max_rectx = rect[2] max_recty = rect[3] isfindrect = True if isfindrect: rect_x = min_rectx rect_y = min_recty rect_width = int(max_rectx - min_rectx) rect_height = int(max_recty - min_recty) else: rect_x = rectangle[0] - 5 if rect_x <= 0: rect_x = 1 rect_y = rectangle[1] - 5 if rect_y <= 0: rect_y = 1 rect_width = int(rectangle[2] - rectangle[0]) + 10 rect_height = int(rectangle[3] - rectangle[1]) + 10 return (rect_x, rect_y, rect_width, rect_height) def saveRegionByOutsideRectangle(self, shape, rectangle, str_time): print('begin - ') pix = QPixmap(self.pixmap.width(), self.pixmap.height()) pp = QPainter() pp.begin(pix) pp.setPen(QPen(QColor(0, 0, 0))) pp.setBrush(QBrush(QColor(255, 255, 255))) polygon = QPolygonF() for point in shape: polygon.append(QPointF(point.x(), point.y())) print('draw polygon') pp.drawPolygon(polygon) pp.end() print('copy image') pix = pix.copy(rectangle[0], rectangle[1], rectangle[2], rectangle[3]) print('save image') imgside = 'right' if rectangle[0] < 380: imgside = 'left' self.saveSplitImage(shape, pix, True, imgside, str_time) print('end - ') # add by yuan => save mask by rectangle shape def findContainRegion(self, shape, rectangle, str_time): pix = QPixmap(self.pixmap.width(), self.pixmap.height()) #size = pix.size() pp = QPainter() pp.begin(pix) pp.setPen(QPen(QColor(0, 0, 0))) pp.setBrush(QBrush(QColor(255, 255, 255))) polygon = QPolygonF() for s in self.shapes: if s.drawRegion: polygon.clear() for point in s: polygon.append(QPointF(point.x(), point.y())) pp.drawPolygon(polygon) pp.end() pix = pix.copy(rectangle[0], rectangle[1], rectangle[2], rectangle[3]) imgside = 'right' if rectangle[0] < 380: imgside = 'left' self.saveSplitImage(shape, pix, True, imgside, str_time) # 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') 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): 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 setDrawRegion(self, status): self.drawRegion = status
class Canvas(QWidget): zoomRequest = pyqtSignal(int) #zoom需求的自定义信号 scrollRequest = pyqtSignal(int, int) #scroll的自定义信号 newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() Point_Change = pyqtSignal(int, bool, list) Point_Vis_Change = pyqtSignal(int) Point_Error = pyqtSignal(bool, int) Parse_Error = pyqtSignal(bool, int) drawingPolygon = pyqtSignal(bool) CREATE, EDIT = range(2) RECT_SHAPE, POLYGON_SHAPE = range(2) #矩形,多边形 epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) #*args将输入的参数存放为元组,**kwargs将输入的参数存放为字典 # Initialise local state. PP = Predefined_Points() self.shape_type = self.POLYGON_SHAPE self.image = QImage() self.brush_point = None self.task_mode = 3 self.erase_mode = False self.current_brush_path = None self.mask_Image = None self.brush_color = QColor(255, 0, 0, 255) self.brush_size = 10 self.brush = QPainter() self.mode = self.EDIT self.shapes = [] self.current = None self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None self.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.prevPoint = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.2 self.bg_image = QImage() self.visible = {} self._hideBackround = False self.hideBackround = False self.hShape = None self.hVertex = None self._painter = QPainter(self) self.font_size = 50 self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) ##point self.point_point = None self.point_point_list = [] self.point_point_list_tmp = [] self.point_dex = None self.point_color = [ QColor(r, g, b) for r in [0, 160, 120, 30, 40] for g in [0, 160, 120, 30, 90] for b in [0, 160, 120, 30] ] self.point_move = None self.point_path = None self.point_selecteditem = None self.point_delete = False self.point_modified = False self.point_shape = {} self.point_link = PP.define_points_links() self.point_num = max(np.array(self.point_link)[:, 1]) # self.point_visible = {i:True for i in range(len(self.point_link))} self.point_visible = {i: True for i in range(self.point_num + 1)} self.point_cover = {i: 2 for i in range(self.point_num)} self.point_deletedid = [] self.point_ids = [] self.point_all_deleted = False self.point_save = [] self.point_link_save = [] self.point_rect = [] self.point_rects = [] self.point_rects_index = 0 self.point_next_rect = False self.point_shape = {} self.point_changed = False self.point_cover_change = False self.point_cover_change_dex = 0 self.point_cover_dict = {} #parse self.parse_rects = {} self.parse_rects_index = 0 self.parse_rects_num = 0 self.parse_shapes = {} self.xuxian = None self.start = False self.started = False def set_shape_type(self, type): if type == 0: self.shape_type = self.RECT_SHAPE self.line.set_shape_type(type) return True elif type == 1: self.shape_type = self.POLYGON_SHAPE self.line.set_shape_type(type) return True else: print("not support the shape type: " + str(type)) return False def enterEvent(self, ev): self.overrideCursor(self._cursor) def get_mask_image(self): return self.mask_pixmap def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): #这个函数是最重要的函数 """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) #点一下return一个pos self.restoreCursor() #鼠标图标 if not self.outOfPixmap(pos) and self.start and not self.started and ( self.task_mode == 0 or self.task_mode == 5): self.xuxian = pos self.repaint() if self.task_mode == 3: self.brush_point = pos if Qt.LeftButton & ev.buttons(): #左鼠标点击 if self.outOfPixmap(pos): #超出图像范围 return if not self.current_brush_path: self.current_brush_path = QPainterPath() self.current_brush_path.moveTo(pos) else: self.current_brush_path.lineTo(pos) self.repaint() return if self.task_mode == 4: self.point_point = pos if len(self.point_rects) == 1: self.draw_point_single_img(self.point_point_list, 0) else: if self.point_rects_index > 0: self.draw_point_single_img(self.point_point_list, self.point_rects_index) # else: # self.draw_point_single_img(self.point_point_list, self.point_rects_index) for i, p in enumerate(self.point_point_list): if p and distance(p - pos) <= 5: self.point_dex = i + 1 #这里加一的目的是为了绘制的时候 不会出现id=0的情况,因此self.point_dex最小为1 而不是0 self.Point_Change.emit(i, True, []) # point change 发射信号 self.repaint() return # Polygon drawing. if self.drawing(): self.start = True self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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) #add xu xian # self.line[1] = pos self.line.line_color = color self.repaint() self.current.highlightClear() 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 def mousePressEvent(self, ev): mods = ev.modifiers() pos = self.transformPos(ev.pos()) task_mode = self.task_mode if self.start == True and task_mode == 0: self.started = True self.xuxian = None if ev.button() == Qt.LeftButton: if self.drawing(): # if self.shape_type == self.POLYGON_SHAPE and self.current: self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif self.shape_type == self.RECT_SHAPE and self.current and self.current.reachMaxPoints( ) is False: if self.task_mode == 5 and len(self.shapes) >= 3: self.current = None self.xuxian = None self.start = False self.started = False QMessageBox.about( self, "About", self.tr('<p><b>%s</b></p>%s <p>%s</p>' % ('注意标注已经为', str(3) + '个', '只可修改'))) return # self.repaint() initPos = self.current[0] minX = initPos.x() minY = initPos.y() targetPos = self.line[1] maxX = targetPos.x() maxY = targetPos.y() self.current.addPoint(QPointF(minX, maxY)) self.current.addPoint(targetPos) self.current.addPoint(QPointF(maxX, minY)) self.current.addPoint(initPos) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif not self.outOfPixmap(pos): self.current = Shape(shape_type=self.shape_type) self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() elif self.task_mode == 4: self.point_changed = True if self.point_modified: self.point_point_list[self.point_modified - 1] = pos self.point_modified = False self.repaint() else: distances = [] self.point_point_list.append(pos) # if not len(self.point_point_list)>self.point_num : if Qt.LeftButton & ev.buttons(): # 左鼠标点击 if self.outOfPixmap(pos): # 超出图像范围 return elif len(self.point_point_list) > 1: if self.point_point_list[ -1] and self.point_point_list[-2]: if distance(self.point_point_list[-1] - self.point_point_list[-2]) <= 5: self.point_move = True del self.point_point_list[-1] for i, p in enumerate(self.point_point_list[:-2]): if p: distances.append(distance(p - pos)) distances.sort() print('distances') if len(distances) >= 1: if distances[0] <= 5: #注意 一次只能删除一个点 if distances[0] <= 5: self.point_move = True #这里给出可移动的指令 del self.point_point_list[-1] if self.point_move: self.point_point = pos self.overrideCursor(Qt.CrossCursor) self.repaint() return else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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: self.overrideCursor(CURSOR_GRAB) elif ev.button( ) == Qt.LeftButton and self.task_mode == 3 and self.current_brush_path: self.current_brush_path = None elif ev.button( ) == Qt.LeftButton and self.task_mode == 4 and self.point_move: #删除点 # del self.point_point_list[self.point_dex] self.point_changed = True self.point_point_list[self.point_dex - 1] = self.point_point self.point_move = False self.repaint() def endMove(self, copy=False): assert self.selectedShape and self.selectedShapeCopy shape = self.selectedShapeCopy print("end move") if copy: self.shapes.append(shape) self.selectedShape.selected = False self.selectedShape = shape self.repaint() else: shape.label = self.selectedShape.label shape.ignore = self.selectedShape.ignore self.deleteSelected() self.shapes.append(shape) 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 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) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) 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 #point 是之前画好的点 pos为当前的点 shape.moveVertexBy(index, shiftPos) if self.shape_type == self.RECT_SHAPE: lindex = (index + 1) % 4 rindex = (index + 3) % 4 lshift = None rshift = None if index % 2 == 0: lshift = QPointF(shiftPos.x(), 0) rshift = QPointF(0, shiftPos.y()) else: rshift = QPointF(shiftPos.x(), 0) lshift = 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.bg_image.width() - o2.x()), min(0, self.bg_image.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 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): #所有的绘制操作都在paintEvent中完成 if not self.bg_image: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) #都在begin()和end()间完成 p.setFont(QFont('Times', self.font_size, QFont.Bold)) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawImage(0, 0, self.bg_image) if self.task_mode == 3: p.setOpacity(0.3) p.drawImage(0, 0, self.mask_pixmap) if self.brush_point: p.drawEllipse(self.brush_point, self.brush_size / 2, self.brush_size / 2) #椭圆 if self.current_brush_path: if self.mask_pixmap.isNull(): self.mask_pixmap = QImage(self.bg_image.size(), QImage.Format_ARGB32) self.mask_pixmap.fill(QColor(255, 255, 255, 0)) self.brush.begin(self.mask_pixmap) brush_pen = QPen() self.brush.setCompositionMode(QPainter.CompositionMode_Source) brush_pen.setColor(self.brush_color) brush_pen.setWidth(self.brush_size) brush_pen.setCapStyle(Qt.RoundCap) brush_pen.setJoinStyle(Qt.RoundJoin) self.brush.setPen(brush_pen) self.brush.drawPath(self.current_brush_path) self.brush.end() if self.task_mode == 4: self.point_save = self.point_point_list w, h = self.bg_image.width(), self.bg_image.height() size = 4 * min(w // 200, h // 200) if self.point_delete: print('delete point') self.point_point_list[self.point_delete - 1] = None self.point_delete = False if self.point_rects: if self.point_rects_index == len(self.point_rects): self.point_rects_index = 0 x, y, w, h = self.point_rects[self.point_rects_index] p.setPen(QColor(255, 0, 0)) p.drawRect(x, y, w, h) if len(self.point_point_list) > 0: for i, point in enumerate(self.point_point_list): if point and self.point_visible[i]: p.setBrush(self.point_color[i]) p.setPen(self.point_color[i]) p.drawEllipse(float(point.x() - size // 2), float(point.y() - size // 2), size, size) if i in self.point_cover: if self.point_cover[i] == 1: p.setPen(QColor(255, 0, 0)) p.setFont(QFont('SimSun', size)) p.drawText( QRect(point.x(), point.y(), size, size), Qt.AlignLeft | Qt.AlignVCenter, '1') if self.point_dex: #注意这里剔除了0 0是none 高亮标注的操作 包括move p.setPen(QColor(255, 0, 0)) p.setBrush(QColor(0, 0, 0, 0)) try: if self.point_point_list[self.point_dex - 1]: p.drawRect( float(self.point_point_list[self.point_dex - 1].x() - size // 2), float(self.point_point_list[self.point_dex - 1].y() - size // 2), size, size) if self.point_move: #移动 p.setPen(QColor(255, 0, 0)) p.drawLine(self.point_point_list[self.point_dex - 1], self.point_point) if len(self.point_point_list) > 1: #画线 for i in self.point_link: # 每次都要遍历所有的Link if i[0] - 1 in range( len(self.point_point_list )) and i[1] - 1 in range( len(self.point_point_list)): pen = QPen(Qt.red, 2) p.setPen(pen) if i not in self.point_link_save: self.point_link_save.append(i) if self.point_point_list[ i[0] - 1] and self.point_point_list[i[1] - 1]: p.drawLine(self.point_point_list[i[0] - 1], self.point_point_list[i[1] - 1]) except IndexError: pass if self.task_mode == 5: #这里融合了point模型 # 这个是用来画instances的框 if self.parse_rects: p.setPen(QColor(255, 0, 0)) x, y, w, h = self.parse_rects[self.parse_rects_index] p.setPen(QColor(255, 0, 0)) p.drawRect(x, y, w, h) if self.task_mode == 0 or self.task_mode == 5: #这个部分用来添加框的虚线 w, h = self.bg_image.width(), self.bg_image.height() size = min(w // 200, h // 200) pen = QPen(Qt.green, size, Qt.SolidLine) pen.setStyle(Qt.DashDotLine) p.setPen(pen) if self.xuxian: p.drawLine(self.xuxian.x(), self.xuxian.y(), self.xuxian.x(), self.xuxian.y() + h - 20) p.drawLine(self.xuxian.x(), self.xuxian.y(), self.xuxian.x() + w - 10, self.xuxian.y()) Shape.scale = self.scale for shape in self.shapes: if shape.fill_color: shape.fill = True shape.paint(p) ###这里是其他模式的调用shape中的Paint函数 elif (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) # 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() color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) if self.shape_type == self.RECT_SHAPE: p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) p.end() # # parse mode def parse_next_rect(self): print("canvas_parse_shapes", self.parse_shapes) #这个函数的主要作用是下一个框的保存值 #先保存每个object的相应任务的值 if self.parse_rects_index != 0: #这里的parse_rects_index都是表示的是下一个框的index self.parse_shapes[self.parse_rects_index - 1] = self.shapes else: self.parse_shapes[self.parse_rects_num - 1] = self.shapes #再看看能不能赋值,不能赋值就直接给空值就可以 if self.parse_rects_index in self.parse_shapes: self.shapes = self.parse_shapes[self.parse_rects_index] else: self.shapes = [] #最后重新绘制即可,这里是一定要加上的,这里加了上面的就不用加了。 self.repaint() def parse_new_bbox(self): #这个函数主要是对新的操作所产生的shape做存值的处理 #注意新的操作至少要写分为三个操作:1、新增2、删除3、移动等 if self.parse_rects_index != 0: # 这里的parse_rects_index都是表示的是下个框的index self.parse_shapes[self.parse_rects_index] = self.shapes elif len(self.parse_shapes) > 0 and not self.parse_rects_index == 0: self.parse_shapes[self.parse_rects_num - 1] = self.shapes # 0的特殊性 else: self.parse_shapes[0] = self.shapes def parse_clear(self): #这个函数的所有的定义的初始化变量都要重新初始化 #在主函数里写在nextimg preimg self.parse_shapes = {} self.shapes = [] self.repaint() self.parse_rects_index = 0 # point mode def deletepoint(self, i): self.point_delete = i def point_finish(self): del self.point_point_list[-1] self.point_dex = self.point_num - 1 QMessageBox.about( self, "About", self.tr('<p><b>%s</b></p>%s <p>%s</p>' % ('注意标注已经为', str(self.point_num + 1) + '个', '只可修改'))) self.repaint() def point_change(self, id, visible): # self.point_changed=True if id: if visible: self.point_visible[id] = True if id <= len(self.point_point_list): #当前的点在point list 并且可视化 self.point_dex = id self.repaint() else: #当前的点不在point list 但是想可视 self.point_dex = self.point_dex # self.Point_Error.emit(True,id) else: #不可见 那个这个点的值就是None if id <= len(self.point_point_list): #当前的点在point list 并且不可视 pass # # print('v1') # self.point_point_list[id] =self.point_point # self.point_visible[id]=True # self.repaint() else: try: self.point_point_list[id] = None self.point_visible[id] = False self.repaint() print('pointlink delete') except IndexError: pass else: #针对 全选 pass def point_modify(self, i): if i in range(len(self.point_point_list)): print('modified', i) self.point_modified = i + 1 def point_all_delete(self): self.point_changed = True self.point_all_deleted = True l = [i for i in range(len(self.point_point_list))] # self.point_cover = {i: 2 for i in range(len(self.point_link)-2)} self.point_point_list = [] self.point_dex = 0 self.Point_Change.emit(0, True, l) #调到第一个参数 # self.point_shape[self.point_rects_index]=[] self.repaint() def point_load_point_shape(self, point_list): point = [] print('canvas point_shape', point_list) for i, ll in enumerate(point_list): print("ll") if not ll: print("list", ll) self.point_shape[i] = [] self.point_dex = 1 self.repaint() else: for p in ll: point.append(QPointF(int(p[0]), int(p[1]))) self.point_shape[i] = point point = [] if len(self.point_shape[0]) > 0: self.point_point_list = self.point_shape[ self.point_rects_index] self.point_dex = len(self.point_point_list) self.repaint() def point_load(self, point_list): #transform list to qpointf list point = [] rect_dex = 0 if not point_list: self.point_point_list = [] self.point_dex = 1 self.repaint() else: for p in point_list: point.append(QPointF(int(p[0]), int(p[1]))) self.point_point_list = point self.point_shape[self.point_rects_index] = self.point_point_list self.point_dex = len(self.point_point_list) self.repaint() def point_rect_points(self, x, y, w, h): self.point_rects.append([x, y, w, h]) # next rect def draw_rects(self, rects): #主要对xml分析后进来的rects处理 self.parse_rects = rects self.parse_rects_num = len(self.parse_rects) self.repaint() def draw_point_single_img(self, point_point_list, i): self.point_shape[i] = self.point_point_list def draw_next_rect(self): point_point_list = [] if self.point_changed: if len(self.point_shape) <= len(self.point_rects): self.point_shape[ self.point_rects_index] = self.point_point_list self.point_rects_index += 1 if self.point_rects_index == len(self.point_rects): self.point_rects_index = 0 self.point_point_list = [] # .clear 清内存 最好不要这样 self.point_dex = 0 if self.point_rects_index in self.point_shape: point_point_list = self.point_shape[self.point_rects_index] else: pass if point_point_list and isinstance(point_point_list[0], QPointF): #绘制过程 self.point_point_list = point_point_list self.point_dex = len(self.point_point_list) self.repaint() else: #加载过程 print("point_point_list", point_point_list) self.point_load(point_point_list) ############## 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() if self.bg_image: w, h = self.bg_image.width() * s, self.bg_image.height() * s else: w, h = 100, 100 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): if self.bg_image: w, h = self.bg_image.width(), self.bg_image.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) def finalise(self): #完成 assert self.current self.current.close() self.shapes.append(self.current) self.current = None self.setHiding(False) self.newShape.emit() self.update() self.xuxian = None self.start = False self.started = False 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.bg_image.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() try: d, i, (x, y) = min(self.intersectingEdges((x1, y1), (x2, y2), points)) except: pass 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, xxx_todo_changeme, xxx_todo_changeme1, 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) = xxx_todo_changeme (x2, y2) = xxx_todo_changeme1 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.bg_image: return self.scale * self.bg_image.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): mods = ev.modifiers() if Qt.ControlModifier == int(mods): # ctrl键 self.zoomRequest.emit(ev.angleDelta().y()) else: ev.angleDelta().y() and self.scrollRequest.emit( ev.angleDelta().y(), Qt.Horizontal if (Qt.ShiftModifier == int(mods)) else Qt.Vertical) ev.angleDelta().x() and self.scrollRequest.emit( ev.angleDelta().x(), Qt.Horizontal) ev.accept() def keyPressEvent(self, ev): #键盘事件 key = ev.key() if self.task_mode == 4: if key == Qt.Key_0: self.point_point_list.append(None) self.point_visible[self.point_dex] = False self.Point_Change.emit( self.point_dex + 1, False, [] ) #当前点的下一个点,自动移动到当前点的下一个点 第一个参数为选择的item的id,第二个参数为非选择状态,第三个参数为全删除的当前的list self.point_dex += 1 #point_dex 从1开始表示当前点的dex (包括指向点和下一个即将绘制的点) elif key == Qt.Key_1: #在这里对全局变量进行键盘判断,不需要self.repaint() self.point_cover[self.point_dex - 1] = 1 self.point_cover_change = True elif key == Qt.Key_2: if self.point_cover[self.point_dex - 1] == 1: self.point_cover[self.point_dex - 1] = 2 self.point_cover_change = True if key == Qt.Key_Escape and self.current: print('ESC press') self.current = None #虚线的初始化 self.xuxian = None self.start = False self.started = False ########### self.drawingPolygon.emit(False) self.update() elif key == Qt.Key_Return and self.canCloseShape(): self.finalise() def setLastLabel(self, text): assert text self.shapes[-1].label = text return self.shapes[-1] def setSelectedShape(self, ignore): """ :param ignore: :return: you can set everything from the label main function """ if self.selectedShape: self.selectedShape.ignore = ignore self.repaint() 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 loadMaskmap(self, mask): self.mask_pixmap = mask self.repaint() def loadPixmap(self, pixmap): self.bg_image = pixmap self.shapes = [] self.mask_pixmap = QImage(self.bg_image.size(), QImage.Format_ARGB32) self.mask_pixmap.fill(QColor(255, 255, 255, 0)) self.repaint() def loadShapes(self, shapes): self.shapes = list(shapes) self.shape_type = shapes[0].get_shape_type() self.current = None self.repaint() def setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() def overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QApplication.setOverrideCursor(cursor) def restoreCursor(self): QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() # self.bg_image = None self.update() def update_image(self, img): self.bg_image = img self.repaint()
class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) saveFileSignal = pyqtSignal() CREATE, EDIT = list(range(2)) epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. self.mode = self.EDIT self.contourMode = False self.shapes = [] self.current = None self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None self.globalMousePos = None self.memIdx = list() self.currentIdx = 0 # only needed for toggling between multiple overlaying cells # self.drawingLineColor = QColor(0, 0, 255) # self.drawingRectColor = QColor(0, 0, 255) self.drawingLineColor = QColor(255, 255, 255, 255) self.drawingRectColor = QColor(255, 255, 255, 255) self.drawingContourColor = QColor(255, 0, 0) 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.showContourOverlay = False def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) self.globalMousePos = pos # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText( 'X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: 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 not self.contourMode: 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 not self.contourMode: 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.selectedShape.contour_points = list() self.repaint() return if self.contourMode and self.selectedShape: if Qt.LeftButton & ev.buttons(): self.overrideCursor(CURSOR_MOVE) if self.cntOldidx is not None: shapeOrigin = self.selectedShape.points[0] cntOld = self.selectedShape.contour_points[self.cntOldidx] cntNew = int(pos.y() - shapeOrigin.y()), int(pos.x() - shapeOrigin.x()) delta_y, delta_x = cntNew[0] - cntOld[0], cntNew[1] - cntOld[1] # print(delta_y, delta_x) mods = ev.modifiers() if Qt.ControlModifier == int(mods): for i, p in enumerate(self.selectedShape.contour_points): self.selectedShape.contour_points[i] = (p[0] + delta_y, p[1] + delta_x) else: for i in range(self.cntOldidx-3, self.cntOldidx+4, 1): i %= len(self.selectedShape.contour_points) p = self.selectedShape.contour_points[i] d = np.sqrt((p[0]-cntOld[0])**2 + (p[1]-cntOld[1])**2) f = 1/(1 + 1.8 * d) self.selectedShape.contour_points[i] = (p[0] + delta_y * f, p[1] + delta_x * f) # print(p, f, self.selectedShape.contour_points[i]) self.repaint() return # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. # self.setToolTip('') 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.contourMode and self.showContourOverlay: self.unHighlight() xmin, ymin = int(self.selectedShape.points[0].x()), int(self.selectedShape.points[0].y()) punktmenge = list() begin = pos.y() - ymin, pos.x() - xmin d_s = list() for p in self.selectedShape.contour_points: d = np.sqrt((p[0] - begin[0])**2 + (p[1] - begin[1])**2) # print(d) if d < 10: punktmenge.append(p) d_s.append(d) try: nearest = punktmenge[np.argmin(d_s)] self.cntOldidx = self.selectedShape.contour_points.index(nearest) # sprint(nearest, self.cntOldidx) except Exception as e: self.cntOldidx = None # print(e) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): pos = self.transformPos(ev.pos()) 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.contourMode and self.showContourOverlay: # print('MouseReleaseEvent from contourMode') # print(pos) if self.cntOldidx is not None: shapeOrigin = self.selectedShape.points[0] cntOld = self.selectedShape.contour_points[self.cntOldidx] cntNew = int(pos.y() - shapeOrigin.y()), int(pos.x() - shapeOrigin.x()) delta_y, delta_x = cntNew[0] - cntOld[0], cntNew[1] - cntOld[1] # print(delta_y, delta_x) for i in range(self.cntOldidx-3, self.cntOldidx+4, 1): i %= len(self.selectedShape.contour_points) p = self.selectedShape.contour_points[i] d = np.sqrt((p[0]-cntOld[0])**2 + (p[1]-cntOld[1])**2) f = 1/(1 + 1.4 * d) self.selectedShape.contour_points[i] = (p[0] + delta_y * f, p[1] + delta_x * f) # print(p, f, self.selectedShape.contour_points[i]) self.repaint() 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 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) # print(shape.points) # print(shape.contour_points) 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) 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 and not self.contourMode: self.selectedShape.selected = False self.selectedShape = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def deleteSelected(self): if self.selectedShape and not self.contourMode: shape = self.selectedShape if self.selectedShape in self.shapes: print('Box wurde gelöscht') self.shapes.remove(self.selectedShape) else: print('Zu löschen Box wurde nicht gefunden {0}{1}{2}{3}\n'.format(xmin, xmax, ymin, ymax)) 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) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.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) if self.showContourOverlay: # print('showing contour with Qt') p.setPen(self.drawingContourColor) brush = QBrush(QColor('transparent')) p.setBrush(brush) for shape in self.shapes: p.drawPolygon(self.createPoly(shape)) if self.contourMode and self.selectedShape: pen = QPen(QColor('black')) pen.setWidth(4) p.setPen(pen) xmin, ymin = int(self.selectedShape.points[0].x()), int(self.selectedShape.points[0].y()) for i in self.selectedShape.contour_points: p.drawPoint(QPointF(i[1]+xmin, i[0]+ymin)) p.end() def createPoly(self, shape): polygon = QPolygonF() xmin, ymin = int(shape.points[0].x()), int(shape.points[0].y()) for p in shape.contour_points: polygon.append(QPointF(p[1]+xmin, p[0]+ymin)) return polygon 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() 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.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_S and self.selectedShape: if(not self.selectedShape.contourEdited): self.selectedShape.contourEdited = True self.deSelectShape() self.update() elif key == Qt.Key_E and self.selectedShape and self.showContourOverlay: self.contourMode = True self.unHighlight() # print('Entering contour mode') elif key == Qt.Key_Q and self.selectedShape and self.contourMode: self.contourMode = False self.selectedShape.contourEdited = True self.deSelectShape() # print('Leaving contour mode') self.saveFileSignal.emit() elif key == Qt.Key_N and self.selectedShape and self.contourMode: if not self.selectedShape.contour_points: self.selectedShape.contour_points = self.genContourInShape(self.selectedShape) self.update() elif key == Qt.Key_R and self.selectedShape and self.contourMode: self.selectedShape.contour_points = list() self.update() elif key == Qt.Key_F and not self.contourMode: pos = self.globalMousePos shapesIdx = list() for s in self.shapes: if s.containsPoint(pos): shapesIdx.append(self.shapes.index(s)) if len(shapesIdx) >= 2: if sorted(self.memIdx) != sorted(shapesIdx): self.memIdx = shapesIdx self.currentIdx = 0 else: self.currentIdx += 1 i = self.memIdx[self.currentIdx%len(self.memIdx)] if self.selectedShape != self.shapes[i]: self.deSelectShape() self.selectShape(self.shapes[i]) self.update() return def genContourInShape(self, shape): genContour = list() xmin, xmax, ymin, ymax = shape.points[0].x(), shape.points[2].x(), shape.points[0].y(), shape.points[2].y() height = xmax - xmin width = ymax - ymin r = (xmax - xmin) // 2 w = 360/16 for i in range(16): t = w*i x = r*math.cos(math.radians(t)) y = r*math.sin(math.radians(t)) genContour.append((width/2 + x, height/2 + y)) return genContour 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): 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.memIdx = list() self.currentIdx = 0 self.update()
class Canvas(QtWidgets.QWidget): zoomRequest = QtCore.pyqtSignal(int) scrollRequest = QtCore.pyqtSignal(int, int) newShape = QtCore.pyqtSignal() selectionChanged = QtCore.pyqtSignal(bool) shapeMoved = QtCore.pyqtSignal() drawingPolygon = QtCore.pyqtSignal(bool) CREATE, EDIT = 0, 1 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.lineColor = QtGui.QColor(0, 0, 255) self.drawingLineColor = QtGui.QColor(0, 0, 255) self.drawingRectColor = QtGui.QColor(0, 0, 255) self.line = Shape(line_color=self.drawingLineColor) self.prevPoint = QtCore.QPointF() self.offsets = QtCore.QPointF(), QtCore.QPointF() self.scale = 1.0 self.pixmap = QtGui.QPixmap() self.visible = {} self._hideBackround = False self.hideBackround = False self.hShape = None self.hVertex = None self._painter = QtGui.QPainter() self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QtWidgets.QMenu(), QtWidgets.QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(QtCore.Qt.WheelFocus) self.verified = False def setDrawingColor(self, qColor): self.drawingLineColor = qColor # print(self.drawingLineColor.getRgb()) self.drawingRectColor = qColor for shape in self.shapes: shape.line_color = qColor self.repaint() # self.line.line_color = qColor # self.lineColor = qColor # self.current.line_color = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() 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.""" if PYQT5: pos = self.transformPos(ev.pos()) else: pos = self.transformPos(ev.posF()) self.restoreCursor() # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.drawingLineColor # print("a",self.drawingLineColor.getRgb()) 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 # print("*********",color.getRgb()) self.overrideCursor(CURSOR_POINT) self.current.highlightVertex(0, Shape.NEAR_VERTEX) self.line[1] = pos self.line.line_color = color # print("b",self.line.line_color.getRgb()) self.repaint() self.current.highlightClear() return # Polygon copy moving. if QtCore.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 QtCore.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 def mousePressEvent(self, ev): if PYQT5: pos = self.transformPos(ev.pos()) else: pos = self.transformPos(ev.posF()) if ev.button() == QtCore.Qt.LeftButton: if self.drawing(): if self.current: try: self.current.addPoint(self.line[1]) except Exception as e: print(e, file=sys.stderr) return self.line[0] = self.current[-1] if self.current.isClosed(): 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() else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == QtCore.Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): if ev.button() == QtCore.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() == QtCore.Qt.LeftButton and self.selectedShape: self.overrideCursor(CURSOR_GRAB) 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: shape.label = self.selectedShape.label self.deleteSelected() self.shapes.append(shape) 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 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) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) 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 = QtCore.QPointF(x1, y1), QtCore.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) shape.moveVertexBy(index, pos - point) 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 -= QtCore.QPointF(min(0, o1.x()), min(0, o1.y())) o2 = pos + self.offsets[1] if self.outOfPixmap(o2): pos += QtCore.QPointF(min(0, self.pixmap.width() - o2.x()), min(0, self.pixmap.height() - o2.y())) # XXX: The next line tracks the new position of the cursor # relative to the shape, but also results in making it # a bit "shaky" when nearing the border and allows it to # go outside of the shape's area for some reason. # self.calculateOffsets(self.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 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 = QtCore.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(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.HighQualityAntialiasing) p.setRenderHint(QtGui.QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale # print(len(self.shapes),86210) for shape in self.shapes: if not shape.hided: if (shape.selected or not self._hideBackround) and \ self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) # 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.line_color = self.drawingLineColor self.current.paint(p) self.line.paint(p) if self.selectedShapeCopy: self.selectedShapeCopy.paint(p) p.end() def transformPos(self, point): """Convert from widget-logical coordinates to painter-logical ones.""" return point / self.scale - self.offsetToCenter() def offsetToCenter(self): s = self.scale area = super(Canvas, self).size() w, h = self.pixmap.width() * s, self.pixmap.height() * s aw, ah = area.width(), area.height() x = (aw - w) / (2 * s) if aw > w else 0 y = (ah - h) / (2 * s) if ah > h else 0 return QtCore.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 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() # print(size.width(),size.height()) 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 QtCore.QPointF(x3, min(max(0, y2), max(y3, y4))) else: # y3 == y4 return QtCore.QPointF(min(max(0, x2), max(x3, x4)), y3) return QtCore.QPointF(x, y) def intersectingEdges(self, point1, point2, points): """Find intersecting edges. For each edge formed by `points', yield the intersection with the line segment `(x1,y1) - (x2,y2)`, if it exists. Also return the distance of `(x2,y2)' to the middle of the edge along with its index, so that the one closest can be chosen. """ (x1, y1) = point1 (x2, y2) = point2 for i in range(4): x3, y3 = points[i] x4, y4 = points[(i + 1) % 4] denom = (y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1) nua = (x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3) nub = (x2 - x1) * (y1 - y3) - (y2 - y1) * (x1 - x3) if denom == 0: # This covers two cases: # nua == nub == 0: Coincident # otherwise: Parallel continue ua, ub = nua / denom, nub / denom if 0 <= ua <= 1 and 0 <= ub <= 1: x = x1 + ua * (x2 - x1) y = y1 + ua * (y2 - y1) m = QtCore.QPointF((x3 + x4) / 2, (y3 + y4) / 2) d = distance(m - QtCore.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): if PYQT5: mods = ev.modifiers() delta = ev.pixelDelta() if QtCore.Qt.ControlModifier == int(mods): # with Ctrl/Command key # zoom self.zoomRequest.emit(delta.y()) else: # scroll self.scrollRequest.emit(delta.x(), QtCore.Qt.Horizontal) self.scrollRequest.emit(delta.y(), QtCore.Qt.Vertical) else: if ev.orientation() == QtCore.Qt.Vertical: mods = ev.modifiers() if QtCore.Qt.ControlModifier == int(mods): # with Ctrl/Command key self.zoomRequest.emit(ev.delta()) else: self.scrollRequest.emit( ev.delta(), QtCore.Qt.Horizontal if (QtCore.Qt.ShiftModifier == int(mods)) else QtCore.Qt.Vertical) else: self.scrollRequest.emit(ev.delta(), QtCore.Qt.Horizontal) ev.accept() def keyPressEvent(self, ev): key = ev.key() if key == QtCore.Qt.Key_Escape and self.current: self.current = None self.drawingPolygon.emit(False) self.update() elif key == QtCore.Qt.Key_Return and self.canCloseShape(): self.finalise() def setLastLabel(self, text): assert text self.shapes[-1].label = text 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 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 overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QtWidgets.QApplication.setOverrideCursor(cursor) def restoreCursor(self): QtWidgets.QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = None self.update()
class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() shapeMovesFinished = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT, CREATEELLIPSE = list(range(3)) epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) # Initialise local state. if 'parent' in kwargs.keys(): self.parent = kwargs['parent'] self.mode = self.EDIT self.shapes = [] self.current = None self.selectedShape = None # save the selected shape here # self.curr_dt = None self.selectedShapeCopy = None self.drawingLineColor = QColor(0, 0, 255) self.drawingRectColor = QColor(0, 0, 255) # self.line = Shape(line_color=self.drawingLineColor, parent_canvas=self) self.line = Shape(parent_canvas=self) 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 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 drawing_ellipse(self): return self.mode == self.CREATEELLIPSE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATEELLIPSE 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()) posLat, posLon = self.transformToLatLon(pos) self.mousePosLatLon = QPointF(posLon, posLat) # print(posLat, posLon) # 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())) 'lat: %f; lon: %f' % (posLat, posLon)) # Polygon drawing. if self.drawing_ellipse(): return elif 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) 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() self.recalculateMovedLatlonPointsSelectedShape() return # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. # self.setToolTip("Image") # self.setToolTip('X: %d; Y: %d' % (pos.x(), pos.y())) try: bmhelper = self.parent.window().basemaphelper valueStr = bmhelper.getValueStr_AtCoordinates( self.mousePosLatLon.x(), self.mousePosLatLon.y()) # valueStr = 'unknown' except: valueStr = 'unknown' self.setToolTip('lat: %f; lon: %f; value: %s' % (posLat, posLon, valueStr)) 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) self.recalculateMovedLatlonPointsSelectedShape() def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing() | self.drawing_ellipse(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() self.recalculateMovedLatlonPointsSelectedShape() 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) self.shapeMovesFinished.emit() elif ev.button() == Qt.LeftButton: pos = self.transformPos(ev.pos()) if self.drawing(): self.handleDrawing(pos) self.recalculateMovedLatlonPointsSelectedShape() # def endMove(self, copy=False): # assert self.selectedShape and self.selectedShapeCopy # shape = self.selectedShapeCopy # 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 # self.recalculateMovedLatlonPointsSelectedShape() def endMove(self): assert self.selectedShape and self.selectedShapeCopy shape = self.selectedShapeCopy self.selectedShape.points = [p for p in shape.points] self.selectedShapeCopy = None self.recalculateMovedLatlonPointsSelectedShape() 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: self.current.addPoint(pos, self.transformToLatLon(pos, True)) if len(self.current) == 3: self.current.close() self.finalise() elif not self.outOfPixmap(pos): self.current = Shape(parent_canvas=self) self.current.addPoint(pos, self.transformToLatLon(pos, True)) self.recalculateMovedLatlonPointsSelectedShape() 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) 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 # 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): shape.fill = shape.selected or shape == self.hShape # shape.paint(p) shape.paint() if self.current: # self.current.paint(p) # self.line.paint(p) self.current.paint() # self.line.paint(self.current._painter) if self.selectedShapeCopy: # self.selectedShapeCopy.paint(p) self.selectedShapeCopy.paint() # 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) p.end() def transformPos(self, point): """Convert from widget-logical coordinates to painter-logical coordinates.""" return point / self.scale - self.offsetToCenter() def transformToLatLon(self, PicturePoint, outputQPointF=False): lat, lon = 0., 0. try: bmhelper = self.parent.window().basemaphelper # bm = bmhelper.bm x_pic, y_pic = PicturePoint.x(), PicturePoint.y() # for cylindrical projections ONLY lon = (x_pic / self.pixmap.width()) * ( bmhelper.urcrnrlon - bmhelper.llcrnrlon) + bmhelper.llcrnrlon lat = ((self.pixmap.height() - y_pic) / self.pixmap.height()) * ( bmhelper.urcrnrlat - bmhelper.llcrnrlat) + bmhelper.llcrnrlat except: pass if outputQPointF: return QPointF(lon, lat) else: return lat, lon def transformLatLonToPixmapCoordinates(self, lon, lat): x_pic, y_pic = 0., 0. try: bmhelper = self.parent.window().basemaphelper # bm = bmhelper.bm pixmap_width = self.pixmap.width() pixmap_height = self.pixmap.height() # for cylindrical projections ONLY y_pic = pixmap_height * (1. - (lat - bmhelper.llcrnrlat) / (bmhelper.urcrnrlat - bmhelper.llcrnrlat)) x_pic = pixmap_width * (lon - bmhelper.llcrnrlon) / ( bmhelper.urcrnrlon - bmhelper.llcrnrlon) except: pass return x_pic, y_pic def recalculateMovedLatlonPointsSelectedShape(self): if self.selectedShape: self.selectedShape.latlonPoints = [] for pt in self.selectedShape.points: latlonPt = self.transformToLatLon(pt, outputQPointF=True) self.selectedShape.latlonPoints.append(latlonPt) 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): 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.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) 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) 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) 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.recalculateMovedLatlonPointsSelectedShape() self.shapeMoved.emit() self.repaint() def moveOutOfBound(self, step): points = [ p1 + p2 for p1, p2 in zip(self.selectedShape.points, [step] * 4) ] self.recalculateMovedLatlonPointsSelectedShape() return True in map(self.outOfPixmap, points) def setLastLabel(self, text, line_color=None, fill_color=None): assert text self.shapes[-1].label.name = 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, clearShapes=True): self.pixmap = pixmap if clearShapes: self.shapes = [] self.repaint() def loadShapes(self, shapes, extend=False): if extend: self.shapes.extend(list(shapes)) else: self.shapes = list(shapes) self.current = None self.repaint() def loadBasemapShapes(self, bmShapes): self.bmShapes = list(bmShapes) def setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() def switchBasemapAndMainShapes(self): self.temporaryShapes = self.shapes self.selectedTemporaryShape = self.selectedShape self.shapes = self.bmShapes self.selectedShape = self.selectedBmShape self.current = None self.repaint() def switchBackBasemapAndMainShapes(self): self.shapes = self.temporaryShapes self.selectedShape = self.selectedTemporaryShape self.current = None 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)
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 randomTable(num): # Create a black image img = np.zeros((512, 512, 3), np.uint8) img.fill(255) left = random.randint(20, 50) top = random.randint(50, 60) width = random.randint(40, 50) height = random.randint(30, 70) row = random.randint(2, 5) column = random.randint(4, 8) if row == 1 and column == 1: width, height = width * 2, height * 2 rectShapes = [] # print("image:{}, row num={}, column num ={} ".format(num, row, column)) for r in range(0, row): if r % 2 == 0: cell_width = column * width start_point = (left, top + r * height) text_start_point = (start_point[0] + int(width * 0.2), start_point[1] + int(height * 0.2)) end_point = (left + cell_width, top + (r + 1) * height) # PIL image转换成array img = Image.fromarray(np.uint8(img)) draw = ImageDraw.Draw(img) word_height = int(height * 0.6) FONT = ImageFont.truetype('fonts/simhei.ttf', word_height) # 字数量 word_sum = int(cell_width / word_height - 1) random_text = randomText(word_sum) text_end_point = (text_start_point[0] + word_height * word_sum, text_start_point[1] + word_height) # 填字 draw.text(text_start_point, random_text, BLACK, font=FONT) # 标注文字 text_points = [text_start_point, text_end_point] text_shape = Shape(label='text') for x, y in text_points: text_shape.addPoint(Point(x, y)) text_shape.close() rectShapes.append(text_shape) # array转换成image img = np.asarray(img) # 画框 thickness = [1, 2] # 表格线粗细随机 # cv.rectangle(img, start_point, end_point, BLACK, random.choice(thickness)) # 用线画矩形 top_right_point = (end_point[0], start_point[1]) bottom_left_point = (start_point[0], end_point[1]) cv.line(img, start_point, top_right_point, BLACK, 1) # draw top line cv.line(img, bottom_left_point, end_point, BLACK, 1) # draw bottom line cv.line(img, start_point, bottom_left_point, BLACK, 1) # draw left line cv.line(img, top_right_point, end_point, BLACK, 1) # draw right line points = [start_point, end_point] shape = Shape(label='rect') for x, y in points: shape.addPoint(Point(x, y)) shape.close() rectShapes.append(shape) continue for c in range(0, column): start_point = (left + c * width, top + r * height) text_start_point = (start_point[0] + int(width * 0.2), start_point[1] + int(height * 0.2)) end_point = (left + (c + 1) * width, top + (r + 1) * height) # PIL image转换成array img = Image.fromarray(np.uint8(img)) draw = ImageDraw.Draw(img) word_height = int(height * 0.6) FONT = ImageFont.truetype('fonts/simhei.ttf', word_height) text_end_point = (text_start_point[0] + word_height * 0.75, text_start_point[1] + word_height) # 填字 draw.text(text_start_point, random.choice(TEXT2), BLACK, font=FONT) # 标注文字 text_points = [text_start_point, text_end_point] text_shape = Shape(label='text') for x, y in text_points: text_shape.addPoint(Point(x, y)) text_shape.close() rectShapes.append(text_shape) # array转换成image img = np.asarray(img) # 画框 thickness = [1, 2] # 表格线粗细随机 # cv.rectangle(img, start_point, end_point, BLACK, random.choice(thickness)) # 用线画矩形 top_right_point = (end_point[0], start_point[1]) bottom_left_point = (start_point[0], end_point[1]) cv.line(img, start_point, top_right_point, BLACK, 1) # draw top line cv.line(img, bottom_left_point, end_point, BLACK, 1) # draw bottom line cv.line(img, start_point, bottom_left_point, BLACK, 1) # draw left line cv.line(img, top_right_point, end_point, BLACK, 1) # draw right line points = [start_point, end_point] shape = Shape(label='rect') for x, y in points: shape.addPoint(Point(x, y)) shape.close() rectShapes.append(shape) # cv.namedWindow(num, 0) # cv.imshow(num, img) # cv.waitKey(0) imagePath = 'VOC2007/JPEGImages/' xmlPath = 'VOC2007/Annotations/' fileName = imagePath + num + IMAGE_EXT xmlName = xmlPath + num + XML_EXT # print('fileName=', fileName) noise_percetage = random.uniform(0, .20) # print('noise_percetage=', noise_percetage) salt_noise_image = SaltAndPepper(img, noise_percetage) # 添加的椒盐噪声 gaussian_noise_image = addGaussianNoise(salt_noise_image) # 添加的高斯噪声 cv.imwrite(fileName, gaussian_noise_image) savePascalVocFormat(xmlName, rectShapes, imagePath, img)
def rotateShape(self, pos, shape, debug=True): """ Rotates a shape by dragging the shape-rotation-button to the position `pos`. Checks if the resulting shape is completely inside the image in the image. If not, rotate by an angle that is closest to the desired angle but still yielding a shape inside the image. """ # Case 1: Rotate the shape vertex_not_rotated = shape.getShapeRotationVertex(False) if vertex_not_rotated is not None: eucl_sq = lambda a: a.x()**2 + a.y()**2 # Fetch the original (=not rotated) vertex-position for movement # and the center of mass of the shape (once again according to # the coordinates that are not rotated) vertex_point = vertex_not_rotated[0] vertex_mirrored = vertex_not_rotated[2] shape_center = shape.getCenter(False) # Compute the vector and distance between both aforementioned points vec_old_center = vertex_point - shape_center # - vertex_point dist_old_center_square = eucl_sq(vec_old_center) # Compute the vector and distance between the new position and the center vec_new_center = pos - shape_center # - pos dist_new_center_square = eucl_sq(vec_new_center) # Now compute the angle between both the vector pointing to the new # position of the rotation vertex and the one pointing to its # original position. # Make completely sure that no rounding errors can cause # mathematical errors for the input value by checking bounds. val = QPointF.dotProduct(vec_new_center, vec_old_center) / \ (dist_new_center_square * dist_old_center_square) **.5 val = min(max(val, -1), 1) angle = acos(val) # The direction of movement has to be adapted depending on the # current state of the vertex. # First condition: vertex is 'mirrored': # the initially topmost line is dragged under line # at the bottom; In this case the sign must be # swapped. # Second condition: as the shape-move vertex is always # directly above or beneath the shape's center # it is succicent to check the x coordinate for # checking if the rotation is 'in the second # half'. In that case, rotate by 2pi -angle transform_angle = lambda a, posx : \ (-1 if vertex_mirrored else 1) \ * (a if (posx >= vertex_point.x()) else 2. * pi - a) angle = transform_angle(angle, pos.x()) # XXX: this checking mechanism does not work entirely (the # distinction of valid angles sometimes does not recognize the # fact that two edges are outside the valid area). # and contains debug code (that inserts vertices to some # positions for debugging) and thus should only be commented # in for finishing the implementation of that feature (in case # it is required)). # If it is not required it should be removed. performCheckOfIntervals = False if performCheckOfIntervals: # Not all angles are valid. Find out which angles are leading to # coordiantes outside the image: # Step 1) find (x,y) with \|(x,y) - c \| = \|x_1 - x_3\| # and (x,y) on image's borders # Step 2) find the associated rotation angles and store them # in a sorted way width, height = self.pixmap.width(), self.pixmap.height() # get the radius of the circle p_c = shape.pointsWithoutRotation[0] - shape_center len_p_c = eucl_sq(p_c) # list all the support vectors indicating image border alongside # with their directions support_direction = [ [QPointF(0, 0), QPointF(width - 1, 0)], [QPointF(0, 0), QPointF(0, height - 1)], [QPointF(width - 1, 0), QPointF(0, height - 1)], [QPointF(0, height - 1), QPointF(width - 1, 0)] ] forbiddenAngleIntervals = [] for s, d in support_direction: # find intersections between the circle (defined by the center # and its radius) and the currently considered image border. # # In case there is only one (or none) intersection, # no conditions are imposed in this step on the anlge as the # image borders are selected to be the last line of pixels # inside the image. # # If there are two intersections, the space in between them is # forbidden intersects = Canvas.intersectionLineCircle( s - shape_center, d, sqrt(len_p_c)) if intersects is not None: # In case debugging is enabled, add new shapes that show # the intersections with the borders in the image. # Attention: debugging cannot be used in a productive mode. # Results in a bunch of new vertices. if debug: deb = Shape() deb.addPoint(intersects[0] + shape_center) deb.addPoint(intersects[1] + shape_center) deb.close() self.shapes.append(deb) deb = Shape() deb.addPoint(p_c + shape_center) deb.close() self.shapes.append(deb) # the corresponding angle is the angle between the # intersection point and the vertex_point (shifted by # center) if len(intersects) == 2: angles = [[ transform_angle( acos( QPointF.dotProduct( spwr - shape_center, a) / (len_p_c * eucl_sq(a))**.5), spwr.x()) for a in intersects ] for spwr in shape.pointsWithoutRotation] for i, (a, b) in enumerate(angles): # find the min and max value and compute the # min and max value that are still allowed. # if the angle might be affected by them t = 0 if a < 0: a += 2 * pi if b < 0: b += 2 * pi mx, mi = max(a, b), min(a, b) if mx - mi > pi: forbiddenAngleIntervals.append( [mx, 2 * pi]) forbiddenAngleIntervals.append([0, mi]) else: forbiddenAngleIntervals.append([mi, mx]) #if a < b: # forbiddenAngleIntervals.append([a, b]) #elif b < a: # forbiddenAngleIntervals.append([a, 2*pi]) # forbiddenAngleIntervals.append([0, b]) # paint vector (forbidden area) based on the # computed angle if debug: p1 = Shape.rotatePoint( shape.pointsWithoutRotation[i], shape_center, a) p2 = Shape.rotatePoint( shape.pointsWithoutRotation[i], shape_center, b) deb = Shape() deb.addPoint(p1) deb.addPoint(p2) deb.close() self.shapes.append(deb) # XXX: There most likely is a better solution to this. # The code below is supposed to unite all forbidden intervals. # This is necessary for being able to pick the closest point # to the forbidden area. if len(forbiddenAngleIntervals): unionInterval = [forbiddenAngleIntervals[0]] uiid = 0 # starts before other.end and stops after other.start checkIntersect = lambda a, b: a[1] >= b[0] and a[0] <= b[1] checkIntersectMutual = lambda a, b: checkIntersect(a, b) \ or checkIntersect(b, a) # need to check multiple times as there might be an array that # unites two other arrays. for k in range(len(forbiddenAngleIntervals) - 1): for i in range(1, len(forbiddenAngleIntervals)): # check if there is already is an interval comprising me inters = False for ui in range(len(unionInterval)): # end union > start this if (checkIntersectMutual( unionInterval[ui], forbiddenAngleIntervals[i])): unionInterval[ui][0] = min( unionInterval[ui][0], forbiddenAngleIntervals[i][0]) unionInterval[ui][1] = max( unionInterval[ui][1], forbiddenAngleIntervals[i][1]) inters = True break if not inters: unionInterval.append( forbiddenAngleIntervals[i]) print(forbiddenAngleIntervals, unionInterval) # Check if there is some intersection and use the closest point # as corrected angle. if angle < 0: angle += 2 * pi for i in unionInterval: if i[0] < angle and angle < i[1]: angle = i[0] if angle - i[0] < i[1] - angle else i[ 1] break # Apply the rotation for the shape (computes new location of rotated # values and stores the current angle for future reference): shape.applyRotationAngle(angle, shape_center)
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)) 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.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 def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText('X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: 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 possibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. self.setToolTip("Image") # Declaration of variables storing selection criterion and information and the selected items themselves: vertex_selected = None vertex_min_dist = 0 shape_selected = None shape_min_len = 0 for shape in self.shapes: if self.isVisible(shape): # Iteratively find the vertex that is closest to the # specified point. Vertices are prioritised to shapes vertex_info = shape.getClosestVertex(pos, self.epsilon) if vertex_info: index, dist = vertex_info if vertex_selected is None or dist < vertex_min_dist: vertex_selected = index, shape vertex_min_dist = dist # In case no vertex has been found yet, check if the # current shape contains the point. Select the shape # with the smallest length (as length indicates whether # the shape is likely to to be completely covered by # a different shape. elif not vertex_selected: path = shape.makePath() contains = path.contains(pos) if contains: length = path.length() if not shape_selected or length < shape_min_len: shape_selected = shape shape_min_len = length # update the graphical user interface accordingly if vertex_selected is not None: if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = vertex_selected self.hShape.highlightVertex(self.hVertex, self.hShape.MOVE_VERTEX) self.setToolTip("Click & drag to move point") if self.hVertex == Shape.INDEX_ROTATION_ENTITY: self.overrideCursor(CURSOR_ROTATE) else: self.overrideCursor(CURSOR_POINT) self.setStatusTip(self.toolTip()) self.update() elif shape_selected is not None: if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = None, shape_selected self.setToolTip("Click & drag to move shape '%s'" % shape_selected.label) self.setStatusTip(self.toolTip()) self.overrideCursor(CURSOR_GRAB) self.update() else: # Nothing found, clear highlights, reset state. if self.selectedVertex(): self.hShape.highlightClear() self.hVertex, self.hShape = None, None self.overrideCursor(CURSOR_DEFAULT) self.update() def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() pass elif ev.button() == Qt.RightButton and self.editing(): 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 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): 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(): index, shape = self.hVertex, self.hShape shape.highlightVertex(index, shape.MOVE_VERTEX) self.selectShape(shape) elif self.hShape is not None: self.selectShape(self.hShape) def boundedMoveVertex(self, pos): """ Code executed when dragging a vertex. This can imply two different operations: 1. Rotation of the vertex 2. Stretch the vertex :param pos: the 'new' position of the vertex in question """ index, shape = self.hVertex, self.hShape if index == Shape.INDEX_ROTATION_ENTITY: self.rotateShape(pos, shape) else: self.resizeShape(pos, index, shape) def getClosestValid(self, point): """ Return the point that is closest to any point inside the rectangle. Especially, if the point itself is inside, return the point. """ return QPointF(max(min(point.x(), self.pixmap.width() - 1), 0), max(min(point.y(), self.pixmap.height() - 1), 0)) def rotateShape(self, pos, shape, debug=True): """ Rotates a shape by dragging the shape-rotation-button to the position `pos`. Checks if the resulting shape is completely inside the image in the image. If not, rotate by an angle that is closest to the desired angle but still yielding a shape inside the image. """ # Case 1: Rotate the shape vertex_not_rotated = shape.getShapeRotationVertex(False) if vertex_not_rotated is not None: eucl_sq = lambda a: a.x()**2 + a.y()**2 # Fetch the original (=not rotated) vertex-position for movement # and the center of mass of the shape (once again according to # the coordinates that are not rotated) vertex_point = vertex_not_rotated[0] vertex_mirrored = vertex_not_rotated[2] shape_center = shape.getCenter(False) # Compute the vector and distance between both aforementioned points vec_old_center = vertex_point - shape_center # - vertex_point dist_old_center_square = eucl_sq(vec_old_center) # Compute the vector and distance between the new position and the center vec_new_center = pos - shape_center # - pos dist_new_center_square = eucl_sq(vec_new_center) # Now compute the angle between both the vector pointing to the new # position of the rotation vertex and the one pointing to its # original position. # Make completely sure that no rounding errors can cause # mathematical errors for the input value by checking bounds. val = QPointF.dotProduct(vec_new_center, vec_old_center) / \ (dist_new_center_square * dist_old_center_square) **.5 val = min(max(val, -1), 1) angle = acos(val) # The direction of movement has to be adapted depending on the # current state of the vertex. # First condition: vertex is 'mirrored': # the initially topmost line is dragged under line # at the bottom; In this case the sign must be # swapped. # Second condition: as the shape-move vertex is always # directly above or beneath the shape's center # it is succicent to check the x coordinate for # checking if the rotation is 'in the second # half'. In that case, rotate by 2pi -angle transform_angle = lambda a, posx : \ (-1 if vertex_mirrored else 1) \ * (a if (posx >= vertex_point.x()) else 2. * pi - a) angle = transform_angle(angle, pos.x()) # XXX: this checking mechanism does not work entirely (the # distinction of valid angles sometimes does not recognize the # fact that two edges are outside the valid area). # and contains debug code (that inserts vertices to some # positions for debugging) and thus should only be commented # in for finishing the implementation of that feature (in case # it is required)). # If it is not required it should be removed. performCheckOfIntervals = False if performCheckOfIntervals: # Not all angles are valid. Find out which angles are leading to # coordiantes outside the image: # Step 1) find (x,y) with \|(x,y) - c \| = \|x_1 - x_3\| # and (x,y) on image's borders # Step 2) find the associated rotation angles and store them # in a sorted way width, height = self.pixmap.width(), self.pixmap.height() # get the radius of the circle p_c = shape.pointsWithoutRotation[0] - shape_center len_p_c = eucl_sq(p_c) # list all the support vectors indicating image border alongside # with their directions support_direction = [ [QPointF(0, 0), QPointF(width - 1, 0)], [QPointF(0, 0), QPointF(0, height - 1)], [QPointF(width - 1, 0), QPointF(0, height - 1)], [QPointF(0, height - 1), QPointF(width - 1, 0)] ] forbiddenAngleIntervals = [] for s, d in support_direction: # find intersections between the circle (defined by the center # and its radius) and the currently considered image border. # # In case there is only one (or none) intersection, # no conditions are imposed in this step on the anlge as the # image borders are selected to be the last line of pixels # inside the image. # # If there are two intersections, the space in between them is # forbidden intersects = Canvas.intersectionLineCircle( s - shape_center, d, sqrt(len_p_c)) if intersects is not None: # In case debugging is enabled, add new shapes that show # the intersections with the borders in the image. # Attention: debugging cannot be used in a productive mode. # Results in a bunch of new vertices. if debug: deb = Shape() deb.addPoint(intersects[0] + shape_center) deb.addPoint(intersects[1] + shape_center) deb.close() self.shapes.append(deb) deb = Shape() deb.addPoint(p_c + shape_center) deb.close() self.shapes.append(deb) # the corresponding angle is the angle between the # intersection point and the vertex_point (shifted by # center) if len(intersects) == 2: angles = [[ transform_angle( acos( QPointF.dotProduct( spwr - shape_center, a) / (len_p_c * eucl_sq(a))**.5), spwr.x()) for a in intersects ] for spwr in shape.pointsWithoutRotation] for i, (a, b) in enumerate(angles): # find the min and max value and compute the # min and max value that are still allowed. # if the angle might be affected by them t = 0 if a < 0: a += 2 * pi if b < 0: b += 2 * pi mx, mi = max(a, b), min(a, b) if mx - mi > pi: forbiddenAngleIntervals.append( [mx, 2 * pi]) forbiddenAngleIntervals.append([0, mi]) else: forbiddenAngleIntervals.append([mi, mx]) #if a < b: # forbiddenAngleIntervals.append([a, b]) #elif b < a: # forbiddenAngleIntervals.append([a, 2*pi]) # forbiddenAngleIntervals.append([0, b]) # paint vector (forbidden area) based on the # computed angle if debug: p1 = Shape.rotatePoint( shape.pointsWithoutRotation[i], shape_center, a) p2 = Shape.rotatePoint( shape.pointsWithoutRotation[i], shape_center, b) deb = Shape() deb.addPoint(p1) deb.addPoint(p2) deb.close() self.shapes.append(deb) # XXX: There most likely is a better solution to this. # The code below is supposed to unite all forbidden intervals. # This is necessary for being able to pick the closest point # to the forbidden area. if len(forbiddenAngleIntervals): unionInterval = [forbiddenAngleIntervals[0]] uiid = 0 # starts before other.end and stops after other.start checkIntersect = lambda a, b: a[1] >= b[0] and a[0] <= b[1] checkIntersectMutual = lambda a, b: checkIntersect(a, b) \ or checkIntersect(b, a) # need to check multiple times as there might be an array that # unites two other arrays. for k in range(len(forbiddenAngleIntervals) - 1): for i in range(1, len(forbiddenAngleIntervals)): # check if there is already is an interval comprising me inters = False for ui in range(len(unionInterval)): # end union > start this if (checkIntersectMutual( unionInterval[ui], forbiddenAngleIntervals[i])): unionInterval[ui][0] = min( unionInterval[ui][0], forbiddenAngleIntervals[i][0]) unionInterval[ui][1] = max( unionInterval[ui][1], forbiddenAngleIntervals[i][1]) inters = True break if not inters: unionInterval.append( forbiddenAngleIntervals[i]) print(forbiddenAngleIntervals, unionInterval) # Check if there is some intersection and use the closest point # as corrected angle. if angle < 0: angle += 2 * pi for i in unionInterval: if i[0] < angle and angle < i[1]: angle = i[0] if angle - i[0] < i[1] - angle else i[ 1] break # Apply the rotation for the shape (computes new location of rotated # values and stores the current angle for future reference): shape.applyRotationAngle(angle, shape_center) def resizeShape(self, pos, index, shape): """ Resize shape of rectangle, enforcing rectangular form in the original coordinates """ dot = lambda x, y: x.x() * y.x() + x.y() * y.y() eucl = lambda a: 1. * a.x()**2 + 1. * a.y()**2 rot = lambda p, a: Shape.rotatePoint(p, QPointF(0, 0), a) rotShapeLine = lambda i, ia, s: rot( s.pointsWithoutRotation[ia] - s.pointsWithoutRotation[i], s. currentAngle) if shape.points[index] != pos: # give reasonable names to the vectices that are affected ('left' # and 'right' and to the vertex that remains unaffected 'other') rindex, lindex, oindex = [(index + o) % 4 for o in [1, 3, 2]] # A) compute the offset defining the movement to be applied in this # step at the vertex in question. pos = self.getClosestValid(pos) offset_rotated = shape.points[index] - pos shape.points[index] = pos # B) Find new location of affected points (lindex and rindex) # 1) w,h = rotate back vector from dragged vertex (=: i) to other # vertex (=: o) # 2) cos(w or h), sin(w or h) -> vector from i to lindex (=:l) # or rindex (=: r) vec_involved = shape.points[oindex] - shape.points[index] size = -rot(vec_involved, -shape.currentAngle) w, h = size.x(), size.y() vec_il = QPointF( cos(shape.currentAngle) * w, sin(shape.currentAngle) * w) vec_ir = QPointF(-sin(shape.currentAngle) * h, cos(shape.currentAngle) * h) # C) Correct locations accordingly # 1) compute position 1, 3 # 2) compute intersection in rotated space, such that it is # ensured that the resulting values are rounded inside the # coordinates. # Subtract the resulting value directly from i and 1 or 3 # 3) Use the projection vector to move both the currently # dragged point and the point that is out of bounds to the # last valid location. if index % 2 == 0: rind = vec_ir vec_ir = vec_il vec_il = rind shape.points[rindex] = shape.points[index] - vec_ir shape.points[lindex] = shape.points[index] - vec_il # apply the rotation angle to the data that is uk shape_center = shape.getCenter(rotated=True) shifts = self.checkBorders(shape, index, lindex, vec_ir), \ self.checkBorders(shape, index, rindex, vec_il) for shift in shifts: if shift is not None: shape.points[index] += shift # apply the new coordinates to the latent (unrotated) array shape.applyRotationAngle(shape.currentAngle, shape_center, False) def checkBorders(self, shape, index, aindex, direction): width, height = self.pixmap.width(), self.pixmap.height() a1, a2 = shape[aindex].y() < 0, shape[aindex].y() >= height if shape[aindex].y() < 0 or shape[aindex].y() >= height: lam = self.intersectionParametrized( shape[aindex], direction, QPointF(0, (shape[aindex].y() >= height) * height), QPointF(width, 0)) if lam is not None: shift = lam * direction res = shape[aindex] + shift if round(res.x()) >= 0 and round(res.x()) < width: shape[aindex] = res return shift if shape[aindex].x() < 0 or shape[aindex].x() >= width: lam = self.intersectionParametrized( shape[aindex], direction, QPointF((shape[aindex].x() >= width) * width, 0), QPointF(0, height)) shift = lam * direction res = shape[aindex] + shift if round(res.y()) >= 0 and round(res.y()) < height: shape[aindex] = res return shift return None @staticmethod def intersectionParametrized(s_1, d_1, s_2, d_2): """ Return the multiplier \lambda of d_1 such that \exists \mu s.th. s_1 + \lambda d_1 = s_2 + \mu d_2. Otherwise return None. :param s_1: first support vector :param d_1: first direction vector that is multiplied by \lambda. \lambda is to be returned :param s_2: second support :param d_2: second direction. Multiplier is irrelevant and not required. :return: the multiplier \lambda of d_1 such that the two lines intersect """ denominator = d_2.y() * d_1.x() - d_2.x() * d_1.y() return None if denominator == 0 else 1. * ( d_2.x() * (s_1.y() - s_2.y()) - d_2.y() * (s_1.x() - s_2.x())) / denominator @staticmethod def intersectionLineCircle(s, d, radius): signStar = lambda x: -1 if x < 0 else 1 s2 = s + d dx = d.x() dy = d.y() dr = sqrt(dx**2. + dy**2.) D = s.x() * s2.y() - s2.x() * s.y() delta = radius**2. * dr**2. - D**2. if delta >= 0: p1 = QPointF( (1. * D * dy + signStar(dy) * dx * sqrt(delta)) / dr**2, (-1. * D * dx + abs(dy) * sqrt(delta)) / dr**2) p2 = QPointF( (1. * D * dy - signStar(dy) * dx * sqrt(delta)) / dr**2, (-1. * D * dx - abs(dy) * sqrt(delta)) / dr**2) if delta == 0: return p1 return p1, p2 return None def boundedMoveShape(self, shape, pos): dp = pos - self.prevPoint self.prevPoint = pos return self.boundedMoveShapeBy(shape, dp) def boundedMoveShapeBy(self, shape, dp): minx, miny = shape.points[0].x(), shape.points[0].y() maxx, maxy = minx, miny for p in shape.points[1:]: minx = min(p.x(), minx) miny = min(p.y(), miny) maxx = max(p.x(), maxx) maxy = max(p.y(), maxy) maxx = self.pixmap.width() - maxx maxy = self.pixmap.height() - maxy dp = QPointF(max(min(maxx, dp.x()), -minx), max(min(maxy, dp.y()), -miny)) if dp: shape.shift(dp) 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.prevPoint = point if not self.boundedMoveShape(shape, point - offset): self.boundedMoveShape(shape, point + offset) def paintEvent(self, event): if not self.pixmap: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.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) 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.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() 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(len(points)): 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(QPointF(-1, 0)) elif key == Qt.Key_Right and self.selectedShape: self.moveOnePixel(QPointF(1, 0)) elif key == Qt.Key_Up and self.selectedShape: self.moveOnePixel(QPointF(0, -1)) elif key == Qt.Key_Down and self.selectedShape: self.moveOnePixel(QPointF(0, 1)) def moveOnePixel(self, direction): self.boundedMoveShapeBy(self.selectedShape, direction) self.shapeMoved.emit() self.repaint() 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): 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()
class Canvas(QWidget): zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) 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.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) 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.actions = None 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 ploygon(self): return self.mode == self.POLYGON def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() if self.mode == self.CREATE: self.flag_enter_polygon = False def setPolygon(self, actions, filepath, value=True): self.mode = self.POLYGON if value else self.EDIT self.flag_enter_polygon = True if not value: # Create self.unHighlight() self.deSelectShape() self.actions = actions self.filepath = filepath self.img_for_grab = cv2.imread(self.filepath) self.img2_for_grab = self.img_for_grab.copy() self.mask_for_grab = np.zeros( self.img_for_grab.shape[:2], dtype=np.uint8) # mask initialized to PR_BG self.output = np.zeros(self.img_for_grab.shape, np.uint8) # output image to be shown self.BLUE = [255, 0, 0] # flag_rectangle color self.RED = [0, 0, 255] # PR BG self.GREEN = [0, 255, 0] # PR FG self.BLACK = [0, 0, 0] # sure BG self.WHITE = [255, 255, 255] # sure FG self.DRAW_BG = {'color': self.BLACK, 'val': 0} self.DRAW_FG = {'color': self.WHITE, 'val': 1} self.DRAW_PR_FG = {'color': self.GREEN, 'val': 3} self.DRAW_PR_BG = {'color': self.RED, 'val': 2} # setting up flags self.rect_for_grab = (0, 0, 1, 1) self.value_for_grab = self.DRAW_FG # flag_drawing initialized to FG self.thickness = 1 # brush thickness def Segment_Roi(self): # self.seg_count = self.seg_count +1 if self.flag_enter_polygon: bgdmodel = np.zeros((1, 65), np.float64) fgdmodel = np.zeros((1, 65), np.float64) cv2.grabCut(self.img2_for_grab, self.mask_for_grab, self.rect_for_grab, bgdmodel, fgdmodel, 1, cv2.GC_INIT_WITH_RECT) cv2.grabCut(self.img2_for_grab, self.mask_for_grab, self.rect_for_grab, bgdmodel, fgdmodel, 1, cv2.GC_INIT_WITH_MASK) mask2 = np.where( (self.mask_for_grab == 1) + (self.mask_for_grab == 3), 255, 0).astype('uint8') self.output = cv2.bitwise_and(self.img2_for_grab, self.img2_for_grab, mask=mask2) gray = cv2.cvtColor(self.output, cv2.COLOR_BGR2GRAY) ret, binary = cv2.threshold(gray, 1, 255, cv2.THRESH_BINARY) contours = cv2.findContours(binary, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) #,hierarchy contours_big_area = [] max_contours = 0 max_contours_i = 0 for i in range(len(contours[1])): if len(contours[1][i]) > max_contours: max_contours = len(contours[1][i]) max_contours_i = i # for i in range(len(contours[1][max_contours_i])): contours_tmp = np.array(contours[1][max_contours_i]) contours_big_area.append(contours_tmp) # if self.seg_count == 1: self.shapes.remove(self.shapes[len(self.shapes) - 1]) # self.update() self.contours_t = Shape() for i in range(len(contours_big_area[0])): self.contours_t.addPoint( QPointF(contours_big_area[0][i][0][0], contours_big_area[0][i][0][1])) self.contours_t.addPoint( QPointF(contours_big_area[0][0][0][0], contours_big_area[0][0][0][1])) self.shapes.append(self.contours_t) self.setHiding(False) self.newShape.emit() self.update() else: print('Please paint polygon first') 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()) self.restoreCursor() # print(self.mode) # Polygon drawing. if not self.editing(): self.overrideCursor(CURSOR_DRAW) if self.current: # print('enter mousemove') color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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.repaint() self.current.highlightClear() 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 def mousePressEvent(self, ev): pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if not self.editing(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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: 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: if self.ploygon(): self.current.addPoint(self.line[1]) else: 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.current.addPoint(initPos) self.line[0] = self.current[-1] if self.current.isClosed(): 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.ploygon() 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) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) 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 if len(shape) == 4 and (int(shape[0].x()) == int(shape[3].x()) and int(shape[0].y()) == int(shape[1].y())): 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.moveVertexBy(index, shiftPos) 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 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): 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.drawing(): 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() color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) 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 self.current.close() self.shapes.append(self.current) if self.actions is not None: self.actions.Segment_Roi.setEnabled(True) #get the point xmin = float('inf') ymin = float('inf') xmax = float('-inf') ymax = float('-inf') for i in range(len(self.shapes[len(self.shapes) - 1])): x = self.shapes[len(self.shapes) - 1][i].x() y = self.shapes[len(self.shapes) - 1][i].y() xmin = int(min(x, xmin)) ymin = int(min(y, ymin)) xmax = int(max(x, xmax)) ymax = int(max(y, ymax)) #rect self.grab_0_x = xmin self.grab_0_y = ymin self.grab_2_x = xmax self.grab_2_y = ymax self.rect_for_grab = (self.grab_0_x, self.grab_0_y, int(abs(self.grab_0_x - self.grab_2_x)), int(abs(self.grab_0_y - self.grab_2_y))) #bg self.value_for_grab = self.DRAW_BG for i in range(len(self.shapes[len(self.shapes) - 1])): self.bg_0_x = int(self.shapes[len(self.shapes) - 1][i].x()) self.bg_0_y = int(self.shapes[len(self.shapes) - 1][i].y()) cv2.circle(self.mask_for_grab, (self.bg_0_x, self.bg_0_y), self.thickness + 2, self.value_for_grab['val'], -1) # if self.flag_fg: # self.fg_0_x = int(self.shapes[len(self.shapes)-1][0].x()) # self.fg_0_y = int(self.shapes[len(self.shapes)-1][0].y()) # self.value_for_grab = self.DRAW_FG # cv2.circle(self.mask_for_grab,(self.fg_0_x,self.fg_0_y),self.thickness+2,self.value_for_grab['val'],-1) 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() 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.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): assert text self.shapes[-1].label = text 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 overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QApplication.setOverrideCursor(cursor) def restoreCursor(self): QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.pixmap = None self.update()
def loadLabels(self, shapes): s = [] self.initLabelList() idx = len(self.labelInfoDict) for label, lineWidth, points, ellipse_points, rotate, r1, r2, center in shapes: if label not in self.labelInfoDict.keys(): with open( os.path.join(os.path.dirname(__file__), "predefined_labels.json")) as f: predefined_json = json.load(f) new_append_predefined_dict = { "name": "", "isPathClosed": True, "type": 0, "enable": True } new_append_predefined_dict['name'] = label if r1: new_append_predefined_dict['type'] = 1 predefined_json['labels'].append( new_append_predefined_dict) with open( os.path.join(os.path.dirname(__file__), "predefined_labels.json"), "w") as f: json.dump(predefined_json, f, indent=4) self.canvas.LABEL_COLORS.extend(self.canvas.random_colors(N=1)) self.labelInfoDict[label] = ( True, new_append_predefined_dict['type'], self.canvas.LABEL_COLORS[idx]) item = HashableQListWidgetItem(label) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) item.setSelected(True) self.ui.labelList.addItem(item) self.labelToItem[label] = item idx = idx + 1 shape = Shape(label=label) shape.lineWidth = lineWidth shape.isPathClosed = self.labelInfoDict[label][0] shape.d_type = self.labelInfoDict[label][1] shape.lineColor = self.labelInfoDict[label][2] shape.rotate = rotate shape.r1 = r1 shape.r2 = r2 shape.center = center for x, y in points: shape.addPoint(QPointF(x, y)) for x, y in ellipse_points: shape.ellipse_points.append(QPointF(x, y)) if shape.d_type == self.canvas.Ellipse: shape.sampleEllipsePoints() shape.close() s.append(shape) self.addShapeList(shape) self.canvas.loadShapes(s)
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)) 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.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) 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 def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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 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) 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 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): 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() color = QColor(0, 220, 0) p.setPen(color) 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) 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.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() 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.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): assert text self.shapes[-1].label = text 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()
class Canvas(QWidget): """ Canvas inherits QWidget and is the area that displays the image and handles the drawing, resizing, and moving the shapes. """ zoomRequest = pyqtSignal(int) scrollRequest = pyqtSignal(int, int) newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT = list(range(2)) epsilon = 11.0 def __init__(self, *args, **kwargs): """ Create a canvas object and setup the instance variables. """ 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.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.prevPoint = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.0 self.pixmap = QPixmap() self.visible = {} self._hideBackground = False self.hideBackground = 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) def enterEvent(self, ev): """ This event is triggered whenever the mouse enters the canvas area. :param ev: The event. """ self.overrideCursor(self._cursor) def leaveEvent(self, ev): """ This event is triggered whenever the mouse leaves the canvas area. :param ev: The event. """ self.restoreCursor() def focusOutEvent(self, ev): """ This event is triggered whenever the focus leaves the canvas area. :param ev: The event. """ self.restoreCursor() def isVisible(self, shape): """ Returns True if the shape is visible. :param shape: The shape object :return: True if the shape object is visible. False otherwise. """ return self.visible.get(shape, True) def drawing(self): """ :return: True if the user is currently drawing a shape. False otherwise. """ return self.mode == self.CREATE def editing(self): """ :return: True if the user is currently editing a shape. False otherwise. """ return self.mode == self.EDIT def setEditing(self, value=True): """ Set the canvas to editing mode when the user is resizing or moving a shape. :param value: bool that tells if the user is editing or not. """ self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() def unHighlight(self): """ Clear the highlighting color from the inside of a shape when the user mouses out of the shape's location. """ if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): """ :return: The vertex that the user has currently selected. """ return self.hVertex is not None def mouseMoveEvent(self, ev): """ This event is triggered whenever the user moves the mouse while drawing, resizing, or moving a box. :param ev: The event. """ pos = self.transformPos(ev.pos()) self.restoreCursor() # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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.repaint() self.current.highlightClear() 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("") 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 def mousePressEvent(self, ev): """ This event is triggered whenever the user clicks to draw, move, or resize a shape. :param ev: The event. """ pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing(): self.handleDrawing(pos) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() def mouseReleaseEvent(self, ev): """ This event is triggered whenever the user releases the mouse after drawing, moving, or resizing a shape. :param ev: The event. """ 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: 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): """ Called whenever a user has finished moving a box. :param copy: bool value that tells whether the user has just duplicated a box or not. """ 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 hideBackgroundShapes(self, value): """ Only hide other shapes if there is a current selection. """ self.hideBackground = value if self.selectedShape: self.setHiding(True) self.repaint() def handleDrawing(self, pos): """ Handles the drawing of a box. :param pos: The position of the box. """ 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.current.addPoint(initPos) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalize() 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): """ Hide the background shapes. :param enable: bool value that tells whether the shapes can be hidden. """ self._hideBackground = self.hideBackground if enable else False def canCloseShape(self): """ :return: True if the shape can be closed (has 4 points). """ return self.drawing() and self.current and len(self.current) > 2 def mouseDoubleClickEvent(self, ev): """ This event is called whenever the user double clicks on the canvas. :param ev: The event. """ if self.canCloseShape() and len(self.current) > 3: self.current.popPoint() self.finalize() def selectShape(self, shape): """ This is called whenever the user selects a shape. :param shape: The shape that the user selects. """ self.deSelectShape() shape.selected = True self.selectedShape = shape self.setHiding() self.selectionChanged.emit(True) self.update() def selectShapePoint(self, point): """ Select the shape which contains the given point. :param point: The point the user clicks on. """ self.deSelectShape() if self.selectedVertex(): # A vertex is marked for selection. index, shape = self.hVertex, self.hShape shape.highlightVertex(index, shape.MOVE_VERTEX) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) return def calculateOffsets(self, shape, point): """ Calculate the offsets between the shape and the point. :param shape: The shape. :param point: The 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): """ Move the vertex of a shape. :param pos: The position that the user moves the vertex to. """ 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) 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): """ Move a shape to a new position. :param shape: The shape that the user moves. :param pos: The position that the user moves the given shape to. """ 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): """ Called when the user clicks off of a selected shape. """ if self.selectedShape: self.selectedShape.selected = False self.selectedShape = None self.setHiding(False) self.selectionChanged.emit(False) self.update() def deleteSelected(self): """ Called whenever the user deletes the selected shape. :return: The shape that the user deleted. """ if self.selectedShape: shape = self.selectedShape self.shapes.remove(self.selectedShape) self.selectedShape = None self.update() return shape def copySelectedShape(self): """ Called whenever the user duplicates the selected shape. :return: The copied shape. """ 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): """ This is called after the user duplicates a shape. This method automatically translate the new box so that they are not entirely overlapping. :param shape: The duplicated 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): """ Updates the entire canvas, including the current bounding boxes, the vertices, and the currently selected box. :param event: The 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._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() color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) self.setAutoFillBackground(True) 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. :param point: The point to convert. """ return point / self.scale - self.offsetToCenter() def offsetToCenter(self): """ This method helps the image remain in the center during zooming. :return the point that is the center of the image. """ 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): """ :param p: The point. :return: True if the point p is outside of the image's pixmap. False otherwise. """ w, h = self.pixmap.width(), self.pixmap.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) def finalize(self): """ Helper method that's called when a shape is finished being drawn. """ assert self.current self.current.close() self.shapes.append(self.current) self.current = None self.setHiding(False) self.newShape.emit() self.update() def closeEnough(self, p1, p2): """ Returns the distance between a point and epsilon. This helps the user select a point of shape even if they're a few pixels off, which improves responsiveness. :param p1: Point 1. :param p2: Point 2. :return: True if the distance between p1 and p2 is less that epsilon. False otherwise. """ 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/ :param p1: Point 1. :param p2: Point 2. :return: The point that intersects with the current line segment. """ 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. :param x1y1: Point 1. :param x2y2: Point 2. """ 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): """ Required for the scroll area. """ if self.pixmap: return self.scale * self.pixmap.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): """ Event is triggered whenever the mouse wheel is scrolled. :param ev: The event. """ 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): """ This event is triggered whenever a key is pressed. :param ev: The event. """ 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.finalize() 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): """ This method moves the selected shape one pixel in the given direction. :param direction: The direction as a string [Up, Down, Left, Right]. :return: """ # 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): """ Move the shape of out bounds. :param step: The amount of pixels to move it out of bounds. :return: True if any of the points are out of bounds. """ points = [ p1 + p2 for p1, p2 in zip(self.selectedShape.points, [step] * 4) ] return True in map(self.outOfPixmap, points) def setLastLabel(self, text): """ Set the last label that was given for a shape. :param text: The label. """ assert text self.shapes[-1].label = text 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): """ Reset all of the lines in the last shape. """ 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): """ Load the pixmap of an image. Repaint the canvas after. :param pixmap: The pixmap of an image. """ self.pixmap = pixmap self.shapes = [] self.repaint() def loadShapes(self, shapes): """ Load the shapes onto the canvas. Repaint the canvas after loaded. :param shapes: The list of shape objects. """ [self.shapes.append(shape) for shape in list(shapes)] self.current = None self.repaint() def setShapeVisible(self, shape, value): """ Set a shape to be visible or hidden. :param shape: The shape to hide or shoe. :param value: Bool. If true, shape is shown. If false, shape is hidden. :return: """ self.visible[shape] = value self.repaint() def overrideCursor(self, cursor): """ Override the cursor method of QApplication :param cursor: """ self.restoreCursor() self._cursor = cursor QApplication.setOverrideCursor(cursor) def restoreCursor(self): """ Restore the override cursor of QApplication. """ QApplication.restoreOverrideCursor() def resetState(self): """ Reset the canvas. """ self.restoreCursor() self.pixmap = None self.update()
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)) 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._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.radius = None def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText('X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: 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(): radius = (((self.hShape[0].x() - self.hShape[-1].x())**2) + ((self.hShape[0].y() - self.hShape[-1].y())**2))**0.5 # tmp = self.shapes[0] - self.shapes[-1] # dist = (tmp.x() ** 2 + tmp.y() ** 2) ** 0.5 # print("--------------------press vertex of rectangle---------------", pos) if self.hVertex != 4: self.boundedMoveVertex(pos) else: # print("self.shapes[-1])kkkkkkkkkkkkkkkkkkkk", self.shapes[-1]) # rotate from center self.rotationVertext(0, pos, radius) # self.rotationVertext(1, pos, radius) # self.rotationVertext(2, pos, radius) # self.rotationVertext(3, pos, radius) print( "--------------------rotation from center---------------", 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() print( "--------------------press and move rectangle---------------" ) return self.setToolTip("Image") # Just hovering over the canvas, 2 posibilities: # - Highlight shapes # - Highlight vertex # Update shape/vertex fill and tooltip value accordingly. i = 0 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) # print(len(s)) i += 1 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) if self.hVertex != 4: self.setToolTip("Click & drag to move point") else: self.setToolTip("Click & drag to rotated") self.setStatusTip(self.toolTip()) self.update() print("move a vertext of shape************** self.hVertex: ", self.hVertex) 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) print("-----only click left in rectangle-----") else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() # print("-----click left mouse to change place-----") elif ev.button() == Qt.RightButton and self.editing(): self.selectShapePoint(pos) self.prevPoint = pos self.repaint() # print("--------------click right--------------") 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.current.addPoint(QPointF((maxX + minX) / 2, (maxY + minY) / 2)) # self.current_.addPoint(QPointF((maxX + minX) / 2 , (maxY + minY) / 2)) # print(self.current_.points) 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 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 # print("point :",point,"shiftPos: ",shiftPos,"pos: ",pos,"index = ",index, "shape = ", len(shape)) shape.moveVertexBy(index, shiftPos) # shape.moveVertexBy(-1, ) 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 rotationVertext(self, pos, radius, i): # tmp = self.points[0:4] # vector = [] # dist = [] # for index, vertext in enumerate(self.points[0:4]): # # self.points[index] = pos-vertext # vector.append(pos - vertext) # dist.append((vector[index].x() ** 2 + vector[index].y() ** 2) ** 0.5) shape = self.hShape center = shape[-1] vector = pos - center dist = (vector.x()**2 + vector.y()**2)**0.5 # follows mouse around i = 0 if dist > 0: scalar = radius / dist shape[i].setX(int(round(center.x() + vector.x() * scalar))) shape[i].setY(int(round(center.y() + vector.y() * scalar))) center = shape[-1] arage = (shape[0] + shape[1]) / 2 vector1 = center - arage vector2 = center - pos tu = vector1.x() * vector2.x() + vector1.y() * vector2.y() mau = ((vector1.x()**2 + vector1.y()**2) * (vector2.x()**2 + vector2.y()**2))**0.5 # return np.degrees(np.arccos(tu/mau)) 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 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) # print(event,"-------------------") p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.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) 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.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() 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.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 setLastLabel_2(self, text, line_color=None, fill_color=None, kind="Horizontal"): # kind = vertical or horizontal assert text self.shapes[-1].label = text self.shapes[-1].kind = kind if line_color: self.shapes[-1].line_color = line_color if fill_color: self.shapes[-1].fill_color = fill_color # if kind: # self.shapes[-1].kind = kind 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 randomTable(num): # Create a black image img = np.zeros((512, 512, 3), np.uint8) img.fill(255) left = random.randint(20, 50) top = random.randint(50, 50) width = random.randint(100, 150) height = random.randint(30, 70) row = 4 #random.randint(1, 4) column = 3 # random.randint(1, 3) if row == 1 and column == 1: width, height = width * 2, height * 2 rectShapes = [] # print("image:{}, row num={}, column num ={} ".format(num, row, column)) for r in range(0, row): for c in range(0, column): start_point = (left + c * width, top + r * height) text_start_point = (start_point[0] + int(width * 0.1), start_point[1] + int(height * 0.1)) end_point = (left + (c + 1) * width, top + (r + 1) * height) # PIL image转换成array img = Image.fromarray(np.uint8(img)) draw = ImageDraw.Draw(img) word_height = int(height * 0.6) FONT = ImageFont.truetype('fonts/simhei.ttf', word_height) # 填字 draw.text(text_start_point, randomText(int(width / word_height - 1)), BLACK, font=FONT) # array转换成image img = np.asarray(img) # 画框 thickness = [1, 2] # 表格线粗细随机 cv.rectangle(img, start_point, end_point, BLACK, random.choice(thickness)) points = [start_point, end_point] shape = Shape(label='rect') for x, y in points: shape.addPoint(Point(x, y)) shape.close() rectShapes.append(shape) imagePath = 'VOC2007/JPEGImages/' xmlPath = 'VOC2007/Annotations/' fileName = imagePath + num + IMAGE_EXT xmlName = xmlPath + num + XML_EXT # print('fileName=', fileName) noise_percetage = random.uniform(0, .25) # print('noise_percetage=', noise_percetage) salt_noise_image = SaltAndPepper(img, noise_percetage) # 添加的椒盐噪声 gaussian_noise_image = addGaussianNoise(salt_noise_image) # 添加的高斯噪声 cv.imwrite(fileName, gaussian_noise_image) savePascalVocFormat(xmlName, rectShapes, imagePath, img)
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)) 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._painter = QPainter() self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) self.verified = False self.drawSquare = False def setDrawingColor(self, qColor): self.drawingLineColor = qColor self.drawingRectColor = qColor def enterEvent(self, ev): self.overrideCursor(self._cursor) def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() self.prevPoint = QPointF() self.repaint() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) # Update coordinates in status bar if image is opened window = self.parent().window() if window.filePath is not None: self.parent().window().labelCoordinates.setText('X: %d; Y: %d' % (pos.x(), pos.y())) # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: # Display annotation width and height while drawing currentWidth = abs(self.current[0].x() - pos.x()) currentHeight = abs(self.current[0].y() - pos.y()) self.parent().window().labelCoordinates.setText( 'Width: %d, Height: %d / X: %d; Y: %d' % (currentWidth, currentHeight, pos.x(), pos.y())) color = self.drawingLineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Clip the coordinates to 0 or max, # if they are outside the range [0, max] size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) elif len(self.current) > 1 and self.closeEnough( pos, self.current[0]): # Attract line to starting point and colorise to alert the # user: pos = self.current[0] color = self.current.line_color self.overrideCursor(CURSOR_POINT) self.current.highlightVertex(0, Shape.NEAR_VERTEX) if self.drawSquare: 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() 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) else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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 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 snapPointToCanvas(self, x, y): """ Moves a point x,y to within the boundaries of the canvas. :return: (x,y,snapped) where snapped is True if x or y were changed, False if not. """ if x < 0 or x > self.pixmap.width() or y < 0 or y > self.pixmap.height( ): x = max(x, 0) y = max(y, 0) x = min(x, self.pixmap.width()) y = min(y, self.pixmap.height()) return x, y, True return x, y, False def boundedMoveVertex(self, pos): index, shape = self.hVertex, self.hShape point = shape[index] if self.outOfPixmap(pos): size = self.pixmap.size() clipped_x = min(max(0, pos.x()), size.width()) clipped_y = min(max(0, pos.y()), size.height()) pos = QPointF(clipped_x, clipped_y) if self.drawSquare: opposite_point_index = (index + 2) % 4 opposite_point = shape[opposite_point_index] min_size = min(abs(pos.x() - opposite_point.x()), abs(pos.y() - opposite_point.y())) directionX = -1 if pos.x() - opposite_point.x() < 0 else 1 directionY = -1 if pos.y() - opposite_point.y() < 0 else 1 shiftPos = QPointF( opposite_point.x() + directionX * min_size - point.x(), opposite_point.y() + directionY * min_size - point.y()) else: shiftPos = pos - point 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 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) p = self._painter p.begin(self) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawPixmap(0, 0, self.pixmap) Shape.scale = self.scale for shape in self.shapes: if (shape.selected or not self._hideBackround) and self.isVisible(shape): shape.fill = shape.selected or shape == self.hShape shape.paint(p) if self.current: self.current.paint(p) self.line.paint(p) if self.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) 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.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 # 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') 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): 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
class Canvas(QWidget): zoomRequest = pyqtSignal(int) #zoom需求的自定义信号 scrollRequest = pyqtSignal(int, int) #scroll的自定义信号 newShape = pyqtSignal() selectionChanged = pyqtSignal(bool) shapeMoved = pyqtSignal() drawingPolygon = pyqtSignal(bool) CREATE, EDIT = range(2) # RECT_SHAPE, POLYGON_SHAPE = range(2) #矩形,多边形 epsilon = 11.0 def __init__(self, *args, **kwargs): super(Canvas, self).__init__(*args, **kwargs) #*args将输入的参数存放为元组,**kwargs将输入的参数存放为字典 # Initialise local state. self.shape_type = self.POLYGON_SHAPE self.brush_point = None self.task_mode = 3 self.erase_mode = False self.current_brush_path = None self.mask_Image = None self.brush_color = QColor(255, 0, 0, 255) self.brush_size = 10 self.brush = QPainter() self.mode = self.EDIT self.shapes = [] self.current = None self.selectedShape = None # save the selected shape here self.selectedShapeCopy = None self.lineColor = QColor(0, 0, 255) self.line = Shape(line_color=self.lineColor) self.prevPoint = QPointF() self.offsets = QPointF(), QPointF() self.scale = 1.0 self.bg_image = QImage() self.visible = {} self._hideBackround = False self.hideBackround = False self.hShape = None self.hVertex = None self._painter = QPainter(self) self.font_size = 50 self._cursor = CURSOR_DEFAULT # Menus: self.menus = (QMenu(), QMenu()) # Set widget options. self.setMouseTracking(True) self.setFocusPolicy(Qt.WheelFocus) ##point self.point_point = None self.point_point_list = [] self.point_dex = None self.point_color = [ QColor(r, g, b) for r in [0, 255, 120, 30] for g in [0, 255, 120, 30] for b in [0, 255, 120, 30] ] self.point_move = None self.point_path = None self.point_selecteditem = None self.point_delete = False def set_shape_type(self, type): if type == 0: self.shape_type = self.RECT_SHAPE self.line.set_shape_type(type) return True elif type == 1: self.shape_type = self.POLYGON_SHAPE self.line.set_shape_type(type) return True else: print("not support the shape type: " + str(type)) return False def enterEvent(self, ev): self.overrideCursor(self._cursor) def get_mask_image(self): return self.mask_pixmap def leaveEvent(self, ev): self.restoreCursor() def focusOutEvent(self, ev): self.restoreCursor() def isVisible(self, shape): return self.visible.get(shape, True) def drawing(self): return self.mode == self.CREATE def editing(self): return self.mode == self.EDIT def setEditing(self, value=True): self.mode = self.EDIT if value else self.CREATE if not value: # Create self.unHighlight() self.deSelectShape() def unHighlight(self): if self.hShape: self.hShape.highlightClear() self.hVertex = self.hShape = None def selectedVertex(self): return self.hVertex is not None def mouseMoveEvent(self, ev): #这个函数是最重要的函数 """Update line with last point and current coordinates.""" pos = self.transformPos(ev.pos()) #点一下return一个pos self.point_point = pos self.restoreCursor() #鼠标图标 if self.task_mode == 3: self.brush_point = pos if Qt.LeftButton & ev.buttons(): #左鼠标点击 if self.outOfPixmap(pos): #超出图像范围 return if not self.current_brush_path: self.current_brush_path = QPainterPath() self.current_brush_path.moveTo(pos) else: self.current_brush_path.lineTo(pos) self.repaint() return if self.task_mode == 4: for i, p in enumerate(self.point_point_list): if distance(p - pos) <= 5: self.point_dex = i + 1 if self.point_delete: print('deletepoint') self.point_color[self.point_dex - 1] = QColor(0, 0, 0, 0) self.point_delete = False self.repaint() return # Polygon drawing. if self.drawing(): self.overrideCursor(CURSOR_DRAW) if self.current: color = self.lineColor if self.outOfPixmap(pos): # Don't allow the user to draw outside the pixmap. # Project the point to the pixmap's edges. pos = self.intersectionPoint(self.current[-1], pos) elif len(self.current) > 1 and self.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.repaint() self.current.highlightClear() 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 def mousePressEvent(self, ev): mods = ev.modifiers() pos = self.transformPos(ev.pos()) if ev.button() == Qt.LeftButton: if self.drawing(): # if self.shape_type == self.POLYGON_SHAPE and self.current: self.current.addPoint(self.line[1]) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif self.shape_type == self.RECT_SHAPE and 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(minX, maxY)) self.current.addPoint(targetPos) self.current.addPoint(QPointF(maxX, minY)) self.current.addPoint(initPos) self.line[0] = self.current[-1] if self.current.isClosed(): self.finalise() elif not self.outOfPixmap(pos): self.current = Shape(shape_type=self.shape_type) self.current.addPoint(pos) self.line.points = [pos, pos] self.setHiding() self.drawingPolygon.emit(True) self.update() elif self.task_mode == 4: distances = [] self.point_point_list.append(pos) if Qt.LeftButton & ev.buttons(): # 左鼠标点击 if self.outOfPixmap(pos): # 超出图像范围 return elif len(self.point_point_list) > 1: if distance(self.point_point_list[-1] - self.point_point_list[-2]) <= 5: self.point_move = True del self.point_point_list[-1] for i, p in enumerate(self.point_point_list[:-2]): distances.append(distance(p - pos)) distances.sort() print('distances', distances) if len(distances) >= 1: if distances[0] <= 5: #注意 一次只能删除一个点 print(self.point_point_list[-1]) if distances[0] <= 2: self.point_move = True #这里给出可移动的指令 del self.point_point_list[-1] if self.point_move: self.point_point = pos # elif Qt.RightButton & ev.buttons(): # print('dede') # for i, p in enumerate(self.point_point_list[:-2]): # if distance(p-pos)<=5: # print('delete point') self.overrideCursor(Qt.CrossCursor) self.repaint() return else: self.selectShapePoint(pos) self.prevPoint = pos self.repaint() elif ev.button() == Qt.RightButton and self.editing(): 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: self.overrideCursor(CURSOR_GRAB) elif ev.button( ) == Qt.LeftButton and self.task_mode == 3 and self.current_brush_path: self.current_brush_path = None elif ev.button( ) == Qt.LeftButton and self.task_mode == 4 and self.point_move: # del self.point_point_list[self.point_dex] self.point_point_list[self.point_dex - 1] = self.point_point self.point_move = False self.repaint() def deletepoint(self): self.point_delete = True 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: shape.label = self.selectedShape.label self.deleteSelected() self.shapes.append(shape) 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 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) return for shape in reversed(self.shapes): if self.isVisible(shape) and shape.containsPoint(point): shape.selected = True self.selectedShape = shape self.calculateOffsets(shape, point) self.setHiding() self.selectionChanged.emit(True) 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 #point 是之前画好的点 pos为当前的点 shape.moveVertexBy(index, shiftPos) if self.shape_type == self.RECT_SHAPE: lindex = (index + 1) % 4 rindex = (index + 3) % 4 lshift = None rshift = None if index % 2 == 0: lshift = QPointF(shiftPos.x(), 0) rshift = QPointF(0, shiftPos.y()) else: rshift = QPointF(shiftPos.x(), 0) lshift = 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.bg_image.width() - o2.x()), min(0, self.bg_image.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 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): #所有的绘制操作都在paintEvent中完成 if not self.bg_image: return super(Canvas, self).paintEvent(event) p = self._painter p.begin(self) #都在begin()和end()间完成 p.setFont(QFont('Times', self.font_size, QFont.Bold)) p.setRenderHint(QPainter.Antialiasing) p.setRenderHint(QPainter.HighQualityAntialiasing) p.setRenderHint(QPainter.SmoothPixmapTransform) p.scale(self.scale, self.scale) p.translate(self.offsetToCenter()) p.drawImage(0, 0, self.bg_image) #print self.brush_point.x(),self.brush_point.y() if self.task_mode == 3: p.setOpacity(0.3) p.drawImage(0, 0, self.mask_pixmap) if self.brush_point: p.drawEllipse(self.brush_point, self.brush_size / 2, self.brush_size / 2) #椭圆 if self.current_brush_path: if self.mask_pixmap.isNull(): self.mask_pixmap = QImage(self.bg_image.size(), QImage.Format_ARGB32) self.mask_pixmap.fill(QColor(255, 255, 255, 0)) self.brush.begin(self.mask_pixmap) brush_pen = QPen() self.brush.setCompositionMode(QPainter.CompositionMode_Source) brush_pen.setColor(self.brush_color) brush_pen.setWidth(self.brush_size) brush_pen.setCapStyle(Qt.RoundCap) brush_pen.setJoinStyle(Qt.RoundJoin) self.brush.setPen(brush_pen) self.brush.drawPath(self.current_brush_path) self.brush.end() if self.task_mode == 4: for i, point in enumerate(self.point_point_list): if point: p.setBrush(self.point_color[i]) p.setPen(self.point_color[i]) p.drawEllipse(float(point.x() - 2), float(point.y() - 2), 4, 4) if self.point_dex: #注意这里剔除了0 0是none print(self.point_dex) p.setPen(QColor(255, 255, 255)) p.setBrush(QColor(0, 0, 0, 0)) p.drawRect( float(self.point_point_list[self.point_dex - 1].x() - 2), float(self.point_point_list[self.point_dex - 1].y() - 2), 4, 4) if self.point_move: p.setPen(QColor(255, 255, 255)) p.drawLine(self.point_point_list[self.point_dex - 1], self.point_point) Shape.scale = self.scale for shape in self.shapes: if shape.fill_color: shape.fill = True shape.paint(p) ###这里是其他模式的调用shape中的Paint函数 elif (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) # 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() color = QColor(0, 220, 0) p.setPen(color) brush = QBrush(Qt.BDiagPattern) p.setBrush(brush) if self.shape_type == self.RECT_SHAPE: p.drawRect(leftTop.x(), leftTop.y(), rectWidth, rectHeight) 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() if self.bg_image: w, h = self.bg_image.width() * s, self.bg_image.height() * s else: w, h = 100, 100 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.bg_image.width(), self.bg_image.height() return not (0 <= p.x() <= w and 0 <= p.y() <= h) def finalise(self): #完成 assert self.current 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.bg_image.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, xxx_todo_changeme, xxx_todo_changeme1, 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) = xxx_todo_changeme (x2, y2) = xxx_todo_changeme1 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.bg_image: return self.scale * self.bg_image.size() return super(Canvas, self).minimumSizeHint() def wheelEvent(self, ev): mods = ev.modifiers() if Qt.ControlModifier == int(mods): # ctrl键 self.zoomRequest.emit(ev.angleDelta().y()) else: ev.angleDelta().y() and self.scrollRequest.emit( ev.angleDelta().y(), Qt.Horizontal if (Qt.ShiftModifier == int(mods)) else Qt.Vertical) ev.angleDelta().x() and self.scrollRequest.emit( ev.angleDelta().x(), 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() def setLastLabel(self, text): assert text self.shapes[-1].label = text 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 loadMaskmap(self, mask): self.mask_pixmap = mask self.repaint() def loadPixmap(self, pixmap): self.bg_image = pixmap self.shapes = [] self.mask_pixmap = QImage(self.bg_image.size(), QImage.Format_ARGB32) self.mask_pixmap.fill(QColor(255, 255, 255, 0)) self.repaint() def loadShapes(self, shapes): self.shapes = list(shapes) self.shape_type = shapes[0].get_shape_type() print("shape_type:", self.shape_type) self.current = None self.repaint() def setShapeVisible(self, shape, value): self.visible[shape] = value self.repaint() def overrideCursor(self, cursor): self.restoreCursor() self._cursor = cursor QApplication.setOverrideCursor(cursor) def restoreCursor(self): QApplication.restoreOverrideCursor() def resetState(self): self.restoreCursor() self.bg_image = None self.update()