示例#1
0
文件: _anchors.py 项目: pikers/piker
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)
示例#2
0
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)
示例#3
0
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()
示例#4
0
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()
示例#5
0
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)