class ImageViewer(QGraphicsView, QObject): def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) #self.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setDragMode(QGraphicsView.ScrollHandDrag) #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._create_grid() self._create_grid_lines() self._pixmap = None self._selection_mode = SELECTION_MODE.NONE # polygon selection _polygon_guide_line_pen = QPen(QtGui.QColor(235, 72, 40)) _polygon_guide_line_pen.setWidth(2) _polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(_polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # rectangle selection self._box_origin = QPoint() self._box_picker = QRubberBand(QRubberBand.Rectangle, self) # free selection self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._current_label = None @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value @property def pixmap(self) -> ImagePixmap: return self._pixmap @pixmap.setter def pixmap(self, value: QPixmap): self.selection_mode = SELECTION_MODE.NONE self.resetTransform() if self.pixmap: self._scene.removeItem(self._pixmap) self.remove_annotations() self._pixmap = ImagePixmap() self._pixmap.setPixmap(value) self._pixmap.setOffset(-value.width() / 2, -value.height() / 2) self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._scene.addItem(self._pixmap) # rect=self._scene.addRect(QtCore.QRectF(0,0,100,100), QtGui.QPen(QtGui.QColor("red"))) # rect.setZValue(1.0) self.fit_to_window() @property def selection_mode(self): return self._selection_mode @selection_mode.setter def selection_mode(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._is_drawing = value == SELECTION_MODE.FREE if value == SELECTION_MODE.NONE: self.enable_items(True) else: self.enable_items(False) self._selection_mode = value def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableBox): self._scene.removeItem(item) elif isinstance(item, EditablePolygon): item.delete_polygon() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableBox): if item.label and item.label.name == label_name: self._scene.removeItem(item) elif isinstance(item, EditablePolygon): if item.label and item.label.name == label_name: item.delete_polygon() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditablePolygon) or isinstance( item, EditableBox): item.setEnabled(value) def _create_grid(self): gridSize = 15 backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) #backgroundPixmap.fill(QtGui.QColor("white")) backgroundPixmap.fill(QtGui.QColor(20, 20, 20)) #backgroundPixmap.fill(QtGui.QColor("powderblue")) painter = QtGui.QPainter(backgroundPixmap) #backgroundColor=QtGui.QColor("palegoldenrod") #backgroundColor=QtGui.QColor(237,237,237) backgroundColor = QtGui.QColor(0, 0, 0) painter.fillRect(0, 0, gridSize, gridSize, backgroundColor) painter.fillRect(gridSize, gridSize, gridSize, gridSize, backgroundColor) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def _create_grid_lines(self): pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) def fit_to_window(self): """Fit image within view.""" if not self.pixmap or not self._pixmap.pixmap(): return #self._pixmap.setTransformationMode(QtCore.Qt.SmoothTransformation) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def pixmap_hoverEnterEvent_slot(self): self.show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self.hide_guide_lines() def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: if self.selection_mode == SELECTION_MODE.BOX: if not self._box_origin.isNull(): self._box_picker.setGeometry( QRect(self._box_origin, evt.pos()).normalized()) elif self.selection_mode == SELECTION_MODE.POLYGON: if self._current_polygon: if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.selection_mode == SELECTION_MODE.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path: painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.buttons() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: self.setDragMode(QGraphicsView.NoDrag) self._box_origin = evt.pos() self._box_picker.setGeometry(QRect(self._box_origin, QSize())) self._box_picker.show() elif self._selection_mode == SELECTION_MODE.POLYGON: pixmap_rect: QRectF = self._pixmap.boundingRect() new_point = self.mapToScene(evt.pos()) # consider only the points intothe image if pixmap_rect.contains(new_point): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(new_point) else: self._current_polygon.addPoint(new_point) elif self._selection_mode == SELECTION_MODE.FREE: # start drawing new_point = self.mapToScene(evt.pos()) pixmap_rect: QRectF = self._pixmap.boundingRect() # consider only the points intothe image if pixmap_rect.contains(new_point): self.setDragMode(QGraphicsView.NoDrag) pen = QPen(QtGui.QColor(235, 72, 40)) pen.setWidth(10) self._last_point_drawn = new_point self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: if evt.button() == QtCore.Qt.LeftButton: if self.selection_mode == SELECTION_MODE.BOX: roi: QRect = self._box_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() pixmap_rect = self._pixmap.boundingRect() self._box_picker.hide() if pixmap_rect == roi.united(pixmap_rect): rect = EditableBox(roi) rect.label = self.current_label self._scene.addItem(rect) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.selection_mode == SELECTION_MODE.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() pixmap_rect = self._pixmap.boundingRect() if pixmap_rect == path_rect.united(pixmap_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.selection_mode = SELECTION_MODE.NONE self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def keyPressEvent(self, event: QtGui.QKeyEvent) -> None: if self._current_polygon and event.key() == QtCore.Qt.Key_Space: points = self._current_polygon.points self._current_polygon.label = self.current_label self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).keyPressEvent(event) def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.selection_mode = SELECTION_MODE.NONE self._polygon_guide_line.hide()
class ImageViewer(QGraphicsView, QObject): points_selection_sgn = pyqtSignal(list) key_press_sgn = pyqtSignal(QtGui.QKeyEvent) def __init__(self, parent=None): super(ImageViewer, self).__init__(parent) self.setDragMode(QGraphicsView.ScrollHandDrag) self.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) self.setTransformationAnchor(QGraphicsView.AnchorUnderMouse) self.setResizeAnchor(QGraphicsView.AnchorUnderMouse) self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) self._scene = ImageViewerScene(self) self.setScene(self._scene) self._image = None self._image_original = None self._pixmap = None self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._create_grid() self._channels = [] self._current_tool = SELECTION_TOOL.POINTER self._dataset = None # create grid lines pen_color = QColor(255, 255, 255, 255) pen = QPen(pen_color) pen.setWidth(2) pen.setStyle(QtCore.Qt.DotLine) self.vline = QGraphicsLineItem() self.vline.setVisible(False) self.vline.setPen(pen) self.hline = QGraphicsLineItem() self.hline.setVisible(False) self.hline.setPen(pen) self._scene.addItem(self.vline) self._scene.addItem(self.hline) self._current_label = None # rectangle selection tool self._rectangle_tool_origin = QPoint() self._rectangle_tool_picker = QRubberBand(QRubberBand.Rectangle, self) # polygon selection tool app = QApplication.instance() color = app.palette().color(QPalette.Highlight) self._polygon_guide_line_pen = QPen(color) self._polygon_guide_line_pen.setWidth(3) self._polygon_guide_line_pen.setStyle(QtCore.Qt.DotLine) self._polygon_guide_line = QGraphicsLineItem() self._polygon_guide_line.setVisible(False) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) self._scene.addItem(self._polygon_guide_line) self._current_polygon = None # circle self._current_ellipse = None # free selection tool self._current_free_path = None self._is_drawing = False self._last_point_drawn = QPoint() self._last_click_point = None self._free_Path_pen = QPen(color) self._free_Path_pen.setWidth(10) self._extreme_points = Queue(maxsize=4) @property def current_label(self): return self._current_label @current_label.setter def current_label(self, value): self._current_label = value if self._current_label: color = QColor(self._current_label.color) self._free_Path_pen.setColor(color) self._polygon_guide_line_pen.setColor(color) self._polygon_guide_line.setPen(self._polygon_guide_line_pen) @property def dataset(self): return self._dataset @dataset.setter def dataset(self, value): self._dataset = value @property def img_contrast(self): return self._img_contrast @img_contrast.setter def img_contrast(self, value): self._img_contrast = value @property def img_gamma(self): return self._img_gamma @img_gamma.setter def img_gamma(self, value): self._img_gamma = value @property def img_brightness(self): return self._img_brightness @img_brightness.setter def img_brightness(self, value): self._img_brightness = value @property def image(self): return self._image @image.setter def image(self, value): self._image = value self._image_original = value.copy() self.update_viewer() @property def pixmap(self) -> ImagePixmap: return self._pixmap @gui_exception def update_viewer(self, fit_image=True): rgb = cv2.cvtColor(self._image, cv2.COLOR_BGR2RGB) rgb = ImageUtilities.adjust_image(rgb, self._img_contrast, self._img_brightness) rgb = ImageUtilities.adjust_gamma(rgb, self._img_gamma) pil_image = Image.fromarray(rgb) qppixmap_image = pil_image.toqpixmap() x, y = -qppixmap_image.width() / 2, -qppixmap_image.height() / 2 if self._pixmap: self._pixmap.resetTransform() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) else: self._pixmap = ImagePixmap() self._pixmap.setPixmap(qppixmap_image) self._pixmap.setOffset(x, y) self._scene.addItem(self._pixmap) self._pixmap.signals.hoverEnterEventSgn.connect( self.pixmap_hoverEnterEvent_slot) self._pixmap.signals.hoverLeaveEventSgn.connect( self.pixmap_hoverLeaveEvent_slot) self._pixmap.signals.hoverMoveEventSgn.connect( self.pixmap_hoverMoveEvent_slot) self._hide_guide_lines() if fit_image: self.fit_to_window() @gui_exception def reset_viewer(self): self._img_contrast = 1.0 self._img_brightness = 50.0 self._img_gamma = 1.0 self._image = self._image_original.copy() @gui_exception def equalize_histogram(self): self._image = ImageUtilities.histogram_equalization(self._image) @gui_exception def correct_lightness(self): self._image = ImageUtilities.correct_lightness(self._image) def clusterize(self, k): self._image = ImageUtilities.kmeans(self._image.copy(), k) @property def current_tool(self): return self._current_tool @current_tool.setter def current_tool(self, value): self._polygon_guide_line.hide() self._current_polygon = None self._current_free_path = None self._current_ellipse = None self._is_drawing = value == SELECTION_TOOL.FREE self._current_tool = value self.clear_extreme_points() if value == SELECTION_TOOL.POINTER: self.enable_items(True) else: self.enable_items(False) def fit_to_window(self): if not self._pixmap or not self._pixmap.pixmap(): return self.resetTransform() self.setTransform(QtGui.QTransform()) self.fitInView(self._pixmap, QtCore.Qt.KeepAspectRatio) def _create_grid(self, gridSize=15): app: QApplication = QApplication.instance() curr_theme = "dark" if app: curr_theme = app.property("theme") if curr_theme == "light": color1 = QtGui.QColor("white") color2 = QtGui.QColor(237, 237, 237) else: color1 = QtGui.QColor(20, 20, 20) color2 = QtGui.QColor(0, 0, 0) backgroundPixmap = QtGui.QPixmap(gridSize * 2, gridSize * 2) backgroundPixmap.fill(color1) painter = QtGui.QPainter(backgroundPixmap) painter.fillRect(0, 0, gridSize, gridSize, color2) painter.fillRect(gridSize, gridSize, gridSize, gridSize, color2) painter.end() self._scene.setBackgroundBrush(QtGui.QBrush(backgroundPixmap)) def wheelEvent(self, event: QWheelEvent): adj = (event.angleDelta().y() / 120) * 0.1 self.scale(1 + adj, 1 + adj) @gui_exception def keyPressEvent(self, event: QKeyEvent): if event.key() == QtCore.Qt.Key_Space: image_rect: QRectF = self._pixmap.sceneBoundingRect() if self.current_tool == SELECTION_TOOL.POLYGON and self._current_polygon: points = self._current_polygon.points self._polygon_guide_line.hide() self.setDragMode(QGraphicsView.ScrollHandDrag) if len(points) <= 2: self._current_polygon.delete_item() self.current_tool = SELECTION_TOOL.POINTER elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS and \ self._extreme_points.full(): points = [] image_offset = QPointF(image_rect.width() / 2, image_rect.height() / 2) for pt in self._extreme_points.queue: pt: EditablePolygonPoint center = pt.sceneBoundingRect().center() x = math.floor(center.x() + image_offset.x()) y = math.floor(center.y() + image_offset.y()) points.append([x, y]) self.points_selection_sgn.emit(points) self.current_tool = SELECTION_TOOL.POINTER else: event.ignore() # guide lines events def _show_guide_lines(self): if self.hline and self.vline: self.hline.show() self.vline.show() def _hide_guide_lines(self): if self.hline and self.vline: self.hline.hide() self.vline.hide() def _update_guide_lines(self, x, y): bbox: QRect = self._pixmap.boundingRect() offset = QPointF(bbox.width() / 2, bbox.height() / 2) self.vline.setLine(x, -offset.y(), x, bbox.height() - offset.y()) self.vline.setZValue(1) self.hline.setLine(-offset.x(), y, bbox.width() - offset.x(), y) self.hline.setZValue(1) def pixmap_hoverMoveEvent_slot(self, evt: QGraphicsSceneHoverEvent, x, y): self._update_guide_lines(x, y) def pixmap_hoverEnterEvent_slot(self): self._show_guide_lines() def pixmap_hoverLeaveEvent_slot(self): self._hide_guide_lines() def delete_polygon_slot(self, polygon: EditablePolygon): self._current_polygon = None self.current_tool = SELECTION_TOOL.POINTER self._polygon_guide_line.hide() @gui_exception def mousePressEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() mouse_pos = self.mapToScene(evt.pos()) if evt.buttons() == QtCore.Qt.LeftButton: if self.current_tool == SELECTION_TOOL.BOX: # create rectangle self.setDragMode(QGraphicsView.NoDrag) self._rectangle_tool_origin = evt.pos() geometry = QRect(self._rectangle_tool_origin, QSize()) self._rectangle_tool_picker.setGeometry(geometry) self._rectangle_tool_picker.show() elif self.current_tool == SELECTION_TOOL.POLYGON: if image_rect.contains(mouse_pos): if self._current_polygon is None: self._current_polygon = EditablePolygon() self._current_polygon.label = self._current_label self._current_polygon.tag = self._dataset self._current_polygon.signals.deleted.connect( self.delete_polygon_slot) self._scene.addItem(self._current_polygon) self._current_polygon.addPoint(mouse_pos) else: self._current_polygon.addPoint(mouse_pos) elif self.current_tool == SELECTION_TOOL.ELLIPSE: if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) ellipse_rec = QtCore.QRectF(mouse_pos.x(), mouse_pos.y(), 0, 0) self._current_ellipse = EditableEllipse() self._current_ellipse.tag = self.dataset self._current_ellipse.label = self._current_label self._current_ellipse.setRect(ellipse_rec) self._scene.addItem(self._current_ellipse) elif self.current_tool == SELECTION_TOOL.FREE: # consider only the points into the image if image_rect.contains(mouse_pos): self.setDragMode(QGraphicsView.NoDrag) self._last_point_drawn = mouse_pos self._current_free_path = QGraphicsPathItem() self._current_free_path.setOpacity(0.6) self._current_free_path.setPen(self._free_Path_pen) painter = QPainterPath() painter.moveTo(self._last_point_drawn) self._current_free_path.setPath(painter) self._scene.addItem(self._current_free_path) elif self.current_tool == SELECTION_TOOL.EXTREME_POINTS: if image_rect.contains(mouse_pos): if not self._extreme_points.full(): def delete_point(idx): del self._extreme_points.queue[idx] idx = self._extreme_points.qsize() editable_pt = EditablePolygonPoint(idx) editable_pt.signals.deleted.connect(delete_point) editable_pt.setPos(mouse_pos) self._scene.addItem(editable_pt) self._extreme_points.put(editable_pt) else: self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mousePressEvent(evt) @gui_exception def mouseMoveEvent(self, evt: QtGui.QMouseEvent) -> None: mouse_pos = self.mapToScene(evt.pos()) image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: if not self._rectangle_tool_origin.isNull(): geometry = QRect(self._rectangle_tool_origin, evt.pos()).normalized() self._rectangle_tool_picker.setGeometry(geometry) elif self.current_tool == SELECTION_TOOL.POLYGON: if self._current_polygon and image_rect.contains(mouse_pos): if self._current_polygon.count > 0: last_point: QPointF = self._current_polygon.last_point self._polygon_guide_line.setZValue(1) self._polygon_guide_line.show() mouse_pos = self.mapToScene(evt.pos()) self._polygon_guide_line.setLine(last_point.x(), last_point.y(), mouse_pos.x(), mouse_pos.y()) else: self._polygon_guide_line.hide() elif self.current_tool == SELECTION_TOOL.ELLIPSE: if self._current_ellipse and image_rect.contains(mouse_pos): ellipse_rect = self._current_ellipse.rect() ellipse_pos = QPointF(ellipse_rect.x(), ellipse_rect.y()) distance = math.hypot(mouse_pos.x() - ellipse_pos.x(), mouse_pos.y() - ellipse_pos.y()) ellipse_rect.setWidth(distance) ellipse_rect.setHeight(distance) self._current_ellipse.setRect(ellipse_rect) elif self.current_tool == SELECTION_TOOL.FREE and evt.buttons( ) and QtCore.Qt.LeftButton: if self._current_free_path and image_rect.contains(mouse_pos): painter: QPainterPath = self._current_free_path.path() self._last_point_drawn = self.mapToScene(evt.pos()) painter.lineTo(self._last_point_drawn) self._current_free_path.setPath(painter) super(ImageViewer, self).mouseMoveEvent(evt) @gui_exception def mouseReleaseEvent(self, evt: QtGui.QMouseEvent) -> None: image_rect: QRectF = self._pixmap.boundingRect() if self.current_tool == SELECTION_TOOL.BOX: roi: QRect = self._rectangle_tool_picker.geometry() roi: QRectF = self.mapToScene(roi).boundingRect() self._rectangle_tool_picker.hide() if image_rect == roi.united(image_rect): rect = EditableBox(roi) rect.label = self.current_label rect.tag = self._dataset self._scene.addItem(rect) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) elif self.current_tool == SELECTION_TOOL.ELLIPSE and self._current_ellipse: roi: QRect = self._current_ellipse.boundingRect() if image_rect == roi.united(image_rect): self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) else: self._current_ellipse.delete_item() elif self.current_tool == SELECTION_TOOL.FREE and self._current_free_path: # create polygon self._current_free_path: QGraphicsPathItem path_rect = self._current_free_path.boundingRect() if image_rect == path_rect.united(image_rect): path = self._current_free_path.path() path_polygon = EditablePolygon() path_polygon.tag = self.dataset path_polygon.label = self.current_label self._scene.addItem(path_polygon) for i in range(0, path.elementCount(), 10): x, y = path.elementAt(i).x, path.elementAt(i).y path_polygon.addPoint(QPointF(x, y)) self._scene.removeItem(self._current_free_path) self.current_tool = SELECTION_TOOL.POINTER self.setDragMode(QGraphicsView.ScrollHandDrag) super(ImageViewer, self).mouseReleaseEvent(evt) def remove_annotations(self): for item in self._scene.items(): if isinstance(item, EditableItem): item.delete_item() def remove_annotations_by_label(self, label_name): for item in self._scene.items(): if isinstance(item, EditableItem): if item.label and item.label.name == label_name: item.delete_item() def enable_items(self, value): for item in self._scene.items(): if isinstance(item, EditableItem): item.setEnabled(value) def clear_extreme_points(self): if self._extreme_points.qsize() > 0: for pt in self._extreme_points.queue: self._scene.removeItem(pt) self._extreme_points.queue.clear()
class NodeItem(QGraphicsObject): """ An widget node item in the canvas. """ #: Signal emitted when the scene position of the node has changed. positionChanged = Signal() #: Signal emitted when the geometry of the channel anchors changes. anchorGeometryChanged = Signal() #: Signal emitted when the item has been activated (by a mouse double #: click or a keyboard) activated = Signal() #: The item is under the mouse. hovered = Signal() #: Span of the anchor in degrees ANCHOR_SPAN_ANGLE = 90 #: Z value of the item Z_VALUE = 100 def __init__(self, widget_description=None, parent=None, **kwargs): self.__boundingRect = None QGraphicsObject.__init__(self, parent, **kwargs) self.setFlag(QGraphicsItem.ItemSendsGeometryChanges, True) self.setFlag(QGraphicsItem.ItemHasNoContents, True) self.setFlag(QGraphicsItem.ItemIsSelectable, True) self.setFlag(QGraphicsItem.ItemIsMovable, True) self.setFlag(QGraphicsItem.ItemIsFocusable, True) # central body shape item self.shapeItem = None # in/output anchor items self.inputAnchorItem = None self.outputAnchorItem = None # title text item self.captionTextItem = None # error, warning, info items self.errorItem = None self.warningItem = None self.infoItem = None # background when selected self.backgroundItem = None self.__title = "" self.__processingState = 0 self.__progress = -1 self.__statusMessage = "" self.__error = None self.__warning = None self.__info = None self.__anchorLayout = None self.__animationEnabled = False self.setZValue(self.Z_VALUE) self.setupGraphics() self.setWidgetDescription(widget_description) @classmethod def from_node(cls, node): """ Create an :class:`NodeItem` instance and initialize it from a :class:`SchemeNode` instance. """ self = cls() self.setWidgetDescription(node.description) # self.setCategoryDescription(node.category) return self @classmethod def from_node_meta(cls, meta_description): """ Create an `NodeItem` instance from a node meta description. """ self = cls() self.setWidgetDescription(meta_description) return self def setupGraphics(self): """ Set up the graphics. """ shape_rect = QRectF(-24, -24, 48, 48) self.shapeItem = NodeBodyItem(self) self.shapeItem.setShapeRect(shape_rect) self.shapeItem.setAnimationEnabled(self.__animationEnabled) # Rect for widget's 'ears'. anchor_rect = QRectF(-31, -31, 62, 62) self.inputAnchorItem = SinkAnchorItem(self) input_path = QPainterPath() start_angle = 180 - self.ANCHOR_SPAN_ANGLE / 2 input_path.arcMoveTo(anchor_rect, start_angle) input_path.arcTo(anchor_rect, start_angle, self.ANCHOR_SPAN_ANGLE) self.inputAnchorItem.setAnchorPath(input_path) self.outputAnchorItem = SourceAnchorItem(self) output_path = QPainterPath() start_angle = self.ANCHOR_SPAN_ANGLE / 2 output_path.arcMoveTo(anchor_rect, start_angle) output_path.arcTo(anchor_rect, start_angle, - self.ANCHOR_SPAN_ANGLE) self.outputAnchorItem.setAnchorPath(output_path) self.inputAnchorItem.hide() self.outputAnchorItem.hide() # Title caption item self.captionTextItem = NameTextItem(self) self.captionTextItem.setPlainText("") self.captionTextItem.setPos(0, 33) def iconItem(standard_pixmap): item = GraphicsIconItem(self, icon=standard_icon(standard_pixmap), iconSize=QSize(16, 16)) item.hide() return item self.errorItem = iconItem(QStyle.SP_MessageBoxCritical) self.warningItem = iconItem(QStyle.SP_MessageBoxWarning) self.infoItem = iconItem(QStyle.SP_MessageBoxInformation) ################################ # PyQt 5.10 crashes because of this call (2) #self.backgroundItem = QGraphicsPathItem(self) self.backgroundItem = QGraphicsPathItem(None) ################################ backgroundrect = QPainterPath() backgroundrect.addRoundedRect(anchor_rect.adjusted(-4, -2, 4, 2), 5, 5, mode=Qt.AbsoluteSize) self.backgroundItem.setPen(QPen(Qt.NoPen)) self.backgroundItem.setBrush(QPalette().brush(QPalette.Highlight)) self.backgroundItem.setOpacity(0.5) self.backgroundItem.setPath(backgroundrect) self.backgroundItem.setZValue(-10) self.backgroundItem.setVisible(self.isSelected()) self.prepareGeometryChange() self.__boundingRect = None # TODO: Remove the set[Widget|Category]Description. The user should # handle setting of icons, title, ... def setWidgetDescription(self, desc): """ Set widget description. """ self.widget_description = desc if desc is None: return icon = icon_loader.from_description(desc).get(desc.icon) if icon: self.setIcon(icon) if not self.title(): self.setTitle(desc.name) if desc.inputs: self.inputAnchorItem.show() if desc.outputs: self.outputAnchorItem.show() tooltip = NodeItem_toolTipHelper(self) self.setToolTip(tooltip) def setWidgetCategory(self, desc): """ Set the widget category. """ self.category_description = desc if desc and desc.background: background = NAMED_COLORS.get(desc.background, desc.background) color = QColor(background) if color.isValid(): self.setColor(color) def setIcon(self, icon): """ Set the node item's icon (:class:`QIcon`). """ if isinstance(icon, QIcon): self.icon_item = GraphicsIconItem(self.shapeItem, icon=icon, iconSize=QSize(36, 36)) self.icon_item.setPos(-18, -18) else: raise TypeError def setColor(self, color, selectedColor=None): """ Set the widget color. """ if selectedColor is None: selectedColor = saturated(color, 150) palette = create_palette(color, selectedColor) self.shapeItem.setPalette(palette) def setPalette(self, palette): # TODO: The palette should override the `setColor` raise NotImplementedError def setTitle(self, title): """ Set the node title. The title text is displayed at the bottom of the node. """ self.__title = title self.__updateTitleText() def title(self): """ Return the node title. """ return self.__title title_ = Property(six.text_type, fget=title, fset=setTitle, doc="Node title text.") def setFont(self, font): """ Set the title text font (:class:`QFont`). """ if font != self.font(): self.prepareGeometryChange() self.captionTextItem.setFont(font) self.__updateTitleText() def font(self): """ Return the title text font. """ return self.captionTextItem.font() def setAnimationEnabled(self, enabled): """ Set the node animation enabled state. """ if self.__animationEnabled != enabled: self.__animationEnabled = enabled self.shapeItem.setAnimationEnabled(enabled) def animationEnabled(self): """ Are node animations enabled. """ return self.__animationEnabled def setProcessingState(self, state): """ Set the node processing state i.e. the node is processing (is busy) or is idle. """ if self.__processingState != state: self.__processingState = state self.shapeItem.setProcessingState(state) if not state: # Clear the progress meter. self.setProgress(-1) if self.__animationEnabled: self.shapeItem.ping() def processingState(self): """ The node processing state. """ return self.__processingState processingState_ = Property(int, fget=processingState, fset=setProcessingState) def setProgress(self, progress): """ Set the node work progress state (number between 0 and 100). """ if progress is None or progress < 0 or not self.__processingState: progress = -1 progress = max(min(progress, 100), -1) if self.__progress != progress: self.__progress = progress self.shapeItem.setProgress(progress) self.__updateTitleText() def progress(self): """ Return the node work progress state. """ return self.__progress progress_ = Property(float, fget=progress, fset=setProgress, doc="Node progress state.") def setStatusMessage(self, message): """ Set the node status message text. This text is displayed below the node's title. """ if self.__statusMessage != message: self.__statusMessage = six.text_type(message) self.__updateTitleText() def statusMessage(self): return self.__statusMessage def setStateMessage(self, message): """ Set a state message to display over the item. Parameters ---------- message : UserMessage Message to display. `message.severity` is used to determine the icon and `message.contents` is used as a tool tip. """ # TODO: Group messages by message_id not by severity # and deprecate set[Error|Warning|Error]Message if message.severity == UserMessage.Info: self.setInfoMessage(message.contents) elif message.severity == UserMessage.Warning: self.setWarningMessage(message.contents) elif message.severity == UserMessage.Error: self.setErrorMessage(message.contents) def setErrorMessage(self, message): if self.__error != message: self.__error = message self.__updateMessages() def setWarningMessage(self, message): if self.__warning != message: self.__warning = message self.__updateMessages() def setInfoMessage(self, message): if self.__info != message: self.__info = message self.__updateMessages() def newInputAnchor(self): """ Create and return a new input :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.inputs): raise ValueError("Widget has no inputs.") anchor = AnchorPoint() self.inputAnchorItem.addAnchor(anchor, position=1.0) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) return anchor def removeInputAnchor(self, anchor): """ Remove input anchor. """ self.inputAnchorItem.removeAnchor(anchor) positions = self.inputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.inputAnchorItem.setAnchorPositions(positions) def newOutputAnchor(self): """ Create and return a new output :class:`AnchorPoint`. """ if not (self.widget_description and self.widget_description.outputs): raise ValueError("Widget has no outputs.") anchor = AnchorPoint(self) self.outputAnchorItem.addAnchor(anchor, position=1.0) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) return anchor def removeOutputAnchor(self, anchor): """ Remove output anchor. """ self.outputAnchorItem.removeAnchor(anchor) positions = self.outputAnchorItem.anchorPositions() positions = uniform_linear_layout(positions) self.outputAnchorItem.setAnchorPositions(positions) def inputAnchors(self): """ Return a list of all input anchor points. """ return self.inputAnchorItem.anchorPoints() def outputAnchors(self): """ Return a list of all output anchor points. """ return self.outputAnchorItem.anchorPoints() def setAnchorRotation(self, angle): """ Set the anchor rotation. """ self.inputAnchorItem.setRotation(angle) self.outputAnchorItem.setRotation(angle) self.anchorGeometryChanged.emit() def anchorRotation(self): """ Return the anchor rotation. """ return self.inputAnchorItem.rotation() def boundingRect(self): # TODO: Important because of this any time the child # items change geometry the self.prepareGeometryChange() # needs to be called. if self.__boundingRect is None: self.__boundingRect = self.childrenBoundingRect() return self.__boundingRect def shape(self): # Shape for mouse hit detection. # TODO: Should this return the union of all child items? return self.shapeItem.shape() def __updateTitleText(self): """ Update the title text item. """ text = ['<div align="center">%s' % escape(self.title())] status_text = [] progress_included = False if self.__statusMessage: msg = escape(self.__statusMessage) format_fields = dict(parse_format_fields(msg)) if "progress" in format_fields and len(format_fields) == 1: # Insert progress into the status text format string. spec, _ = format_fields["progress"] if spec != None: progress_included = True progress_str = "{0:.0f}%".format(self.progress()) status_text.append(msg.format(progress=progress_str)) else: status_text.append(msg) if self.progress() >= 0 and not progress_included: status_text.append("%i%%" % int(self.progress())) if status_text: text += ["<br/>", '<span style="font-style: italic">', "<br/>".join(status_text), "</span>"] text += ["</div>"] text = "".join(text) # The NodeItems boundingRect could change. self.prepareGeometryChange() self.__boundingRect = None self.captionTextItem.setHtml(text) self.captionTextItem.document().adjustSize() width = self.captionTextItem.textWidth() self.captionTextItem.setPos(-width / 2.0, 33) def __updateMessages(self): """ Update message items (position, visibility and tool tips). """ items = [self.errorItem, self.warningItem, self.infoItem] messages = [self.__error, self.__warning, self.__info] for message, item in zip(messages, items): item.setVisible(bool(message)) item.setToolTip(message or "") shown = [item for item in items if item.isVisible()] count = len(shown) if count: spacing = 3 rects = [item.boundingRect() for item in shown] width = sum(rect.width() for rect in rects) width += spacing * max(0, count - 1) height = max(rect.height() for rect in rects) origin = self.shapeItem.boundingRect().top() - spacing - height origin = QPointF(-width / 2, origin) for item, rect in zip(shown, rects): item.setPos(origin) origin = origin + QPointF(rect.width() + spacing, 0) def mousePressEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.mousePressEvent(self, event) else: event.ignore() def mouseDoubleClickEvent(self, event): if self.shapeItem.path().contains(event.pos()): QGraphicsObject.mouseDoubleClickEvent(self, event) QTimer.singleShot(0, self.activated.emit) else: event.ignore() def contextMenuEvent(self, event): if self.shapeItem.path().contains(event.pos()): return QGraphicsObject.contextMenuEvent(self, event) else: event.ignore() def focusInEvent(self, event): self.shapeItem.setHasFocus(True) return QGraphicsObject.focusInEvent(self, event) def focusOutEvent(self, event): self.shapeItem.setHasFocus(False) return QGraphicsObject.focusOutEvent(self, event) def itemChange(self, change, value): if change == QGraphicsItem.ItemSelectedChange: selected = bool(qtcompat.qunwrap(value)) self.shapeItem.setSelected(selected) self.captionTextItem.setSelectionState(selected) self.backgroundItem.setVisible(selected) elif change == QGraphicsItem.ItemPositionHasChanged: self.positionChanged.emit() return QGraphicsObject.itemChange(self, change, value)