def gpath_pin( gpath: QGraphicsPathItem, label: Label, # noqa location_description: str = 'right-of-path-centered', use_right_of_pp_label: bool = False, ) -> QPointF: # get actual arrow graphics path path_br = gpath.mapToScene(gpath.path()).boundingRect() # label.vb.locate(label.txt) #, children=True) if location_description == 'right-of-path-centered': return path_br.topRight() - QPointF(label.h / 16, label.h / 3) if location_description == 'left-of-path-centered': return path_br.topLeft() - QPointF(label.w, label.h / 6) elif location_description == 'below-path-left-aligned': return path_br.bottomLeft() - QPointF(0, label.h / 6) elif location_description == 'below-path-right-aligned': return path_br.bottomRight() - QPointF(label.w, label.h / 6)
class CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int = -1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush( QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen( QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen( QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical( None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect( ).height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left() ) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top() ) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen( QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)
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 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 CharItem(QGraphicsRectItem): """ This item represents character item The purpose of the class is to draw a character, create a matrix of rectangles and resolve in which rectangles the character passes The class allow the following operations -# Drawing a character using the mouse events: -# Start by the mouse press event -# Continues by the mouse move event -# The character is stored in QGraphicsPathItem -# Transform the character to occupy the whole item's space -# Set operation : resolving the Occupied matrix which tell on which rectangle the character passes -# Reset operation : reverse the character transform so it is possible to continue drawing the character -# Save operation : To a QDataStream -# Load operation : From a QDataStream The graphical view of the class is composed from: -# This class which inherits from QGraphicsRectItem and holds : -# A QGraphicsPathItem : representing the character -# A QGraphicsPathItem : representing the occupied rectangles -# A QGraphicsPathItem : representing the unoccupied rectangles """ def __init__(self, rect: QRectF, pos: QPointF, viewIndex: int=-1): """ CharItem constructor Args: rect (QRectF) : The rectangle that the character should fill pos (QPointF) : The position of the item within the parent viewIndex (int) : The index of the item in case it is presented in multi character presentation """ super(CharItem, self).__init__(rect) self.setAcceptedMouseButtons(Qt.LeftButton) self.setPresentationPrms() self.occupied = [[False for idx in range(self.netCols)] for idx in range(self.netRows)] self.charPath = None self.wasSetted = False self.occupiedPathItem = None self.unoccupiedPathItem = None self.dirty = False self.viewIndex = viewIndex self.filename = "" self.boundaries = rect self.dx = 1 self.dy = 1 self.posInParent = pos self.setPos(self.posInParent) def setPresentationPrms(self): """ Setting the presentation prms The reason the there is a duplicate set of presentation parameters is that it allows changing the presentation parameters for one character (like in the select option """ self.netColor = netColor self.netThickness = netThickness self.occupyColor = occupyColor self.unOccupyColor = unOccupyColor self.shapeColor = shapeColor self.shapeLineThickness = shapeLineThickness self.selectedOccupiedColor = selectedOccupiedColor self.selectedShapeColor = selectedShapeColor self.netRows = netRows self.netCols = netCols def setNetBoxDimensions(self, rect: QRectF): """ Set net box dimensions The net box is the rectangle that compose the network drawn to show the occupy matrix """ self.netRectHeight = rect.height() / self.netRows self.netRectWidth = rect.width() / self.netCols self.left = rect.left() self.top = rect.top() def netRect(self, row_idx: int, col_idx: int) -> QRectF: """ Set net rect The net box is the rectangle that compose the network drawn to show the occupy matrix Args: row_idx (int) : The row of the network rectangle col_idx (int) : The col of the network rectangle Returns: QRectF : The rectangle """ return QRectF(self.left + col_idx * self.netRectWidth, self.top + row_idx * self.netRectHeight, self.netRectWidth, self.netRectHeight) def mouseMoveEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse move event : continue draw a line This event is activated when the mouse is pressed and moves The methods draws the line in 2 conditions: -# The item is not part of multi character presentation -# A character path was initiated (using the mouse press event) Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: if self.charPath is not None: point = event.scenePos() path = self.charPath.path() path.lineTo(point) self.charPath.setPath(path) self.update() def mousePressEvent(self, event: QGraphicsSceneMouseEvent): """ Mouse Press Event : Start a new line / Select the character If the character is part of multi character presentation activate the character selection If the character is in single character presentation Start a new line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ if self.viewIndex == -1: self.startLine(event) else: self.setSelected() def startLine(self, event: QGraphicsSceneMouseEvent): """ Start drawing a line When the mouse button is pressed and we are in single character dialog this method is activated to start drowning a line in the character Args: event (QGraphicsSceneMouseEvent) : the event description """ # There are 2 modes for the presentation: # Original mode where the character is it's original size # After setting mode when the set was done and the character fullfill all # the item's space # Drawing can be done only in original mode if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # If this is the first start of a line - generate the QPainterPath and QGraphicsPathItem if self.charPath is None: self.initCharPath() # Move to the mouse position point = event.scenePos() path = self.charPath.path() path.moveTo(point) self.charPath.setPath(path) self.dirty = True def initCharPath(self): """ Init the item that holds the character There is one path item that holds the character This method is activated by start line if the char item was not created to create the new and only one """ self.dirty = True self.charPath = QGraphicsPathItem(self) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.charPath.setZValue(1) self.charPath.originalPos = self.charPath.pos() self.charPath.setPath(QPainterPath()) def setSelected(self): """ Set the item a selected item This method is activated when the mouse button is presses and the item is part of multi character presentation """ # Set the colors of the item self.occupiedPathItem.setBrush(QBrush(QColor(self.selectedOccupiedColor))) self.charPath.setPen(QPen(QColor(self.selectedShapeColor), self.shapeLineThickness)) self.update() # Report to the parent item about the selection self.parentItem().setSelected(self.viewIndex) def resetSelected(self): """ Set the colors of the item to not selected """ self.occupiedPathItem.setBrush(QBrush(QColor(self.occupyColor))) self.charPath.setPen(QPen(QColor(self.shapeColor), self.shapeLineThickness)) self.update() def set(self): """ Calculate the occupied matrix and present the results This method does the following: -# Fill the occupied matrix -# Generate the occupied and unoccupied pathes items -# Transform the char path to fit to the item's boundaries """ # If there is no shape drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is in setted mode - return if self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was already setted use revert setting") return # fill the occupied matrix with the data before the scaling self.fillOccupied() self.setNetBoxDimensions(self.boundingRect()) self.createNetPaths() # update the transform - change the dimensions and location # only on the first time self.transformCharPath() self.wasSetted = True # update the presentation self.update() def revertTransform(self): """ Change from Setted mode to drawing mode The drawing mode is the mode where the character can be drawn -# Restore the original size of the character (Reset the transform of the char item) -# Restor the char path item's position to the original one (saved when created) -# Empty the occupiedPath and the unoccupiedPath """ # If there is no character drawn - return if self.charPath is None: QMessageBox.critical(None, "Char identifier window", "There is no shape drawn") return # If the item is already in drawing mode - return if not self.wasSetted: QMessageBox.critical(None, "Char identifier window", "The shape was not setted use set button") return # The char path item transform = self.charPath.transform() transform.reset() # The self.dx and self.dy are the scale parameters created when the item # begins and they are the scale parameters that transform it to the boundaries # given by the parent item transform.scale(self.dx, self.dy) self.charPath.setTransform(transform) self.charPath.setPos(self.charPath.originalPos) # Empty the network pathes self.occupiedPathItem.setPath(QPainterPath()) self.unoccupiedPathItem.setPath(QPainterPath()) self.wasSetted = False def transformCharPath(self): """ Transform char path when the item is setted This method does the following -# scale the char path to the size of the item -# calculate the new position of the char path so that it will be placed at the top left corner of the item """ dx = self.boundingRect().width() / self.charPath.boundingRect().width() dy = self.boundingRect().height() / self.charPath.boundingRect().height() transform = self.charPath.transform() transform.reset() transform.scale(dx, dy) self.charPath.setTransform(transform) # Move the shape to the origin moveX = -(self.charPath.boundingRect().left() - self.boundingRect().left()) * dx moveY = -(self.charPath.boundingRect().top() - self.boundingRect().top()) * dy self.charPath.setX(self.charPath.x() + moveX) self.charPath.setY(self.charPath.y() + moveY) def fillOccupied(self): """ Fill the occupied matrix The algorithm of filling the occupied matrix is -# Scanning the char path -# For each point decide on where row and column of the net -# Set the occupies matrix for this column and row to True """ for idx in range(100): point = self.charPath.path().pointAtPercent(idx / 100.) row_idx, col_idx = self.calcRowCol(point) self.occupied[row_idx][col_idx] = True def calcRowCol(self, point: QPointF): """ Calculate the network row and column that a point is int calc the row and column indexes of a point The following is the algorithm: 1. Find the distance between the point and the left (or top) 2. Divide the distance with the width of path to find the relative position 3. Multipile this relative position with the number of rows/cols 4. Convert the result to int to find the indexes 5. If the index is the number of row/col reduce the index (This is for the case the the point is on the boundary and in this case the relative position is 1 which will cause the indexes to be the number of rows/cols - out of the matrix indexes) Args: point (QPointF) : The point to resolve Returns: int : The network row that the point is in int : The network column that the point is in """ partialX = (point.x() - self.charPath.boundingRect().left()) / self.charPath.boundingRect().width() partialY = (point.y() - self.charPath.boundingRect().top()) / self.charPath.boundingRect().height() col_idx = int(partialX * self.netCols) row_idx = int(partialY * self.netRows) if row_idx == self.netRows: row_idx -= 1 if col_idx == self.netCols: col_idx -= 1 return row_idx, col_idx def createNetPaths(self): """ Create the network pathes This method creates 2 network pathes items one for holding the occupied rectangles and one to hold the unoccupied rectangles """ # Generate 2 QPainterPath occupiedPath = QPainterPath() unoccupiedPath = QPainterPath() # For each entry in occupied matrix : # Add a rectangle to the appropriate path according the entry value for row_idx in range(self.netRows): for col_idx in range(self.netCols): if self.occupied[row_idx][col_idx]: occupiedPath.addRect(self.netRect(row_idx, col_idx)) else: unoccupiedPath.addRect(self.netRect(row_idx, col_idx)) # Create the QGraphicsPathItems that will hold the path self.createNetPath(self.occupyColor, occupiedPath, True) self.createNetPath(self.unOccupyColor, unoccupiedPath, False) def createNetPath(self, brushColor: str, painterPath: QPainterPath, isOccupyPathItem: bool): """ Create a QGraphicsPathItem for a network path Args: brushColor (str) : The color for filling the rectangles painterPath (QPainterPath) : The path to be inserted to the item isOccupyPathItem (bool) : Whether the path is occupied or unoccupied path """ # Generate the path item if not created if isOccupyPathItem: if self.occupiedPathItem is None: self.occupiedPathItem = QGraphicsPathItem(self) pathItem = self.occupiedPathItem else: if self.unoccupiedPathItem is None: self.unoccupiedPathItem = QGraphicsPathItem(self) pathItem = self.unoccupiedPathItem if pathItem is None: pathItem = QGraphicsPathItem(self) # Set the item parameters pathItem.setPath(painterPath) pathItem.setPen(QPen(QColor(self.netColor), self.netThickness, style=Qt.SolidLine)) pathItem.setBrush(QBrush(QColor(brushColor))) pathItem.setZValue(0) def save(self, stream: QDataStream, filename: str): """ Save the item to QDataStream Args: stream (QDataStream) : The data stream to write the item to filename (str) : The filename (for documenting purposes) """ # The item position stream << self.pos() # The dimensions stream << self.rect() # The presentation parameters stream.writeQString(self.netColor) stream.writeQString(self.occupyColor) stream.writeQString(self.unOccupyColor) stream.writeQString(self.shapeColor) stream.writeInt16(self.shapeLineThickness) stream.writeInt16(self.netRows) stream.writeInt16(self.netRows) # The items paths stream << self.charPath.path() self.dirty = False self.filename = filename def load(self, stream, filename): """ Loads the item from QDataStream Args: stream (QDataStream) : The data stream to read the item from filename (str) : The filename (for documenting purposes) """ # read the pos pos = QPointF() stream >> pos self.setPos(pos) # read the dimensions rect = QRectF() stream >> rect self.setRect(rect) # The presentation parameters self.netColor = stream.readQString() self.occupyColor = stream.readQString() self.unOccupyColor = stream.readQString() self.shapeColor = stream.readQString() self.shapeLineThickness = stream.readInt16() self.netRows = stream.readInt16() self.netRows = stream.readInt16() # read the paths self.initCharPath() path = self.charPath.path() stream >> path self.charPath.setPath(path) # Fit the item to the boundaries and position given by the item's parent self.fitToBoundaries() # The presentation of the item is in setted mode so we activate the set method self.wasSetted = False self.set() self.dirty = False self.filename = filename def fitToBoundaries(self): """ Fit the item to the boundaries and position given by it's parent This method was made to support the change of the character boundaries and that the char can be presented in different boundaries and position """ self.setPos(self.posInParent) self.dx = self.boundaries.width() / self.rect().width() self.dy = self.boundaries.height() / self.rect().height() transform = self.transform() transform.scale(self.dx, self.dy) self.setTransform(transform)