Search code examples
pythonqtpyqt5qgraphicssceneqgraphicsitem

How to paint QGraphicsItems from scene onto QImage without changing them in the QGraphicsScene?


I'm converting QGraphicsItem's into rasterized masks at their location in a QGraphicsScene. I'm using those masks for further processing of a video (taking the average intensity inside the mask). To achieve this, I'm painting each item in the scene one by one on a QImage, which has a size just big enough to envelop the item. Everything works well enough, but the items in the scene disappear. That is because I'm removing the pen from the item when I paint it on the QImage. I set the original pen back when I'm done, but the items don't reappear on the scene. How can I "refresh" the scene to make the items reappear, or alternatively, prevent the items form disappearing altogether?

I couldn't really find anything of people running into this problem. So maybe I'm just doing something fundamentally wrong. Any suggestions are welcome.

Here's my code:

class MyThread(QtCore.QThread):
    def __init__(self, scene):
        super().__init__()
        self.scene = scene

    def run(self):
        for item in self.scene.items():
            # Render the ROI item to create a rasterized mask.
            qimage = self.qimage_from_shape_item(item)
            # do some stuff

    @staticmethod
    def qimage_from_shape_item(item: QtWidgets.QAbstractGraphicsShapeItem) -> QtGui.QImage:
        # Get items pen and brush to set back later.
        pen = item.pen()
        brush = item.brush()
        # Remove pen, set brush to white.
        item.setPen(QtGui.QPen(QtCore.Qt.NoPen))
        item.setBrush(QtCore.Qt.white)

        # Determine the bounding box in pixel coordinates.
        top = int(item.scenePos().y() + item.boundingRect().top())
        left = int(item.scenePos().x() + item.boundingRect().left())
        bottom = int(item.scenePos().y() + item.boundingRect().bottom()) + 1
        right = int(item.scenePos().x() + item.boundingRect().right()) + 1

        size = QtCore.QSize(right - left, bottom - top)

        # Initialize qimage, use 8-bit grayscale.
        qimage = QtGui.QImage(size, QtGui.QImage.Format_Grayscale8)
        qimage.fill(QtCore.Qt.transparent)
        painter = QtGui.QPainter(qimage)

        painter.setRenderHint(QtGui.QPainter.Antialiasing)

        # Offset the painter to paint item in its correct pixel location.
        painter.translate(item.scenePos().x() - left, item.scenePos().y() - top)

        # Paint the item.
        item.paint(painter, QtWidgets.QStyleOptionGraphicsItem())

        # Set the pen and brush back.
        item.setPen(pen)
        item.setBrush(brush)

        # Set the pixel coordinate offset of the item to the QImage.
        qimage.setOffset(QtCore.QPoint(left, top))

        return qimage


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)

    widget = QtWidgets.QWidget()
    layout = QtWidgets.QVBoxLayout(widget)

    view = QtWidgets.QGraphicsView()
    layout.addWidget(view)

    scene = QtWidgets.QGraphicsScene()
    view.setScene(scene)
    thread = MyThread(scene)

    view.setFixedSize(400, 300)
    scene.setSceneRect(0, 0, 400, 300)

    rect_item = QtWidgets.QGraphicsRectItem()

    p = QtCore.QPointF(123.4, 56.78)
    rect_item.setPos(p)

    r = QtCore.QRectF(0., 0., 161.8, 100.)
    rect_item.setRect(r)

    scene.addItem(rect_item)

    button = QtWidgets.QPushButton("Get masks")
    layout.addWidget(button)

    button.clicked.connect(thread.start)

    widget.show()

    sys.exit(app.exec_())

Solution

  • The problem is that you "can only create and use GUI widgets on main thread", see more information in this SO answer here.

    The way I solved it was to take the GUI interaction part, i.e. qimage_from_shape_item(), out of the thread and deal with it in the main loop. I suppose it's still not great that I'm using the items directly, although there is no visible flicker effect or anything from temporarily setting NoPen.

    An alternative might have been to use QGraphicsScene::render; however, I don't know how to render the items one by one without interacting with the other items on the scene.