Search code examples
c++qtimage-processingqpixmapqgraphicspixmapitem

How can I achieve semi-transparent overlapping regions in QGraphicsItems?


I'm currently working on image manipulation software in Qt, and I've encountered a specific requirement from the client. In my application, I have a customized QGraphicsScene that holds several images, each represented as a custom QGraphicsPixmapItem. The client's request is that when two of these images overlap, the overlapping region should be semi-transparent, with a 50% transparency level.

I've explored a couple of approaches but ran into a few roadblocks. Initially, I tried to make use of Qt's composition modes, but none of them provided the exact effect I'm aiming for – that is, achieving that 50% transparency in the overlapping region.

First attempt:

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
    QGraphicsPixmapItem::paint(painter, option, widget);

    painter->setRenderHint(QPainter::Antialiasing);

    QList<QGraphicsItem *> collidingItems = this->collidingItems();

    for (auto item : qAsConst(collidingItems)) {
        if (item == this)
            continue;

        if (QGraphicsPixmapItem *otherItem = qgraphicsitem_cast<QGraphicsPixmapItem *>(item)) {
            // Evaluate intersection between two items in local coordinates
            QPainterPath path = this->shape().intersected(this->mapFromItem(otherItem, otherItem->shape()));
            QPainterPath otherPath = otherItem->shape().intersected(otherItem->mapFromItem(this, this->shape()));

            if (!path.isEmpty() && !otherPath.isEmpty()) {
                QRectF thisBoundingRect = path.boundingRect();
                QRectF otherBoundingRect = otherPath.boundingRect();

                // Create two pixmap of the overlapping section
                QPixmap thisPixmap = this->pixmap().copy(thisBoundingRect.toRect());
                QPixmap otherPixmap = otherItem->pixmap().copy(otherBoundingRect.toRect());

                // Clear overlapping section
                painter->save();
                painter->fillPath(path, Qt::black);

                painter->setClipPath(path);

                // Redraw both the pixmaps with opacity at 0.5
                painter->setOpacity(0.65);
                painter->drawPixmap(path.boundingRect().topLeft(), thisPixmap);
                painter->drawPixmap(path.boundingRect().topLeft(), otherPixmap);
                painter->restore();
            }
        }
    }
}

Result when not rotated (which is exactly what I want):

Result when not rotated

Result when rotations are involved:

Result when rotations are involved

The above code works as expected when the images are not rotated, but things get tricky when rotations come into play.

To avoid issues related to transformations, especially when images are rotated, I decided to revise my approach:

void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) override
{
    QGraphicsPixmapItem::paint(painter, option, widget);

    painter->setRenderHint(QPainter::Antialiasing);

    QList<QGraphicsItem *> collidingItems = this->collidingItems();

    for (auto item : qAsConst(collidingItems)) {
        if (item == this)
            continue;

        if (CustomGraphicsPixmapItem *otherItem = qgraphicsitem_cast<CustomGraphicsPixmapItem *>(item)) {
            // Evaluate intersection between two items in local coordinates
            QPainterPath path = this->shape().intersected(this->mapFromItem(otherItem, otherItem->shape()));

            if (!path.isEmpty()) {
                QRectF thisBoundingRect = path.boundingRect();

                // Create two pixmap of the overlapping section
                QPixmap thisPixmap = this->pixmap().copy(thisBoundingRect.toRect());

                // Clear overlapping section
                painter->save();

                // Set the composition mode to clear and then draw with SourceOver
                painter->setCompositionMode(QPainter::CompositionMode_Clear);
                painter->fillPath(path, Qt::transparent);
                painter->setCompositionMode(QPainter::CompositionMode_SourceOver);

                painter->setOpacity(0.5);
                painter->drawPixmap(thisBoundingRect.topLeft(), thisPixmap);

                painter->restore();
            }
        }
    }
} 

However, with this revised approach, the problem I'm facing is that when the two images overlap, the CompositionMode_Clear of the second image not only clears the region of the second image but also clears the region of the underlying image, resulting in a black background for the second image, like so:

Second approach result

How do I efficiently achieve the desired effect? Especially when images are rotated.


Solution

  • Only using the mapFromItem() may create some issues for accumulating transforms, my suggestion is to always map everything to the scene and then remap back the result to the item.

    The concept is to create a QPainterPath that is the result of all scene polygons of all colliding items, using the full path of the bounding rect of the current item (also mapped to the scene), then draw the pixmap in two passes:

    1. set the clip path with the full path subtracted by the colliding polygons and draw at full opacity;
    2. set the clip path with the full intersected with the colliding polygons, then draw at half opacity;

    This is a basic example in PyQt, but I'm sure you can easily convert it to C++.

    class OverlapPixmapItem(QGraphicsPixmapItem):
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.setFlag(self.ItemIsMovable)
            self.setTransformationMode(Qt.SmoothTransformation)
            self.setTransformOriginPoint(self.boundingRect().center())
    
        def wheelEvent(self, event):
            if event.orientation() == Qt.Vertical:
                rot = self.rotation()
                if event.delta() > 0:
                    rot += 5
                else:
                    rot -= 5
                self.setRotation(rot)
            else:
                super().wheelEvent(event)
    
        def paint(self, qp, opt, widget=None):
            colliding = []
            for other in self.collidingItems():
                if isinstance(other, OverlapPixmapItem):
                    colliding.append(other)
    
            if not colliding:
                super().paint(qp, opt, widget)
                return
    
            qp.save()
    
            collisions = QPolygonF()
            for other in colliding:
                collisions = collisions.united(
                    other.mapToScene(other.boundingRect()))
            collisionPath = QPainterPath()
            collisionPath.addPolygon(collisions)
    
            fullPath = QPainterPath()
            fullPath.addPolygon(self.mapToScene(self.boundingRect()))
    
            # draw the pixmap only where it has no colliding items
            qp.setClipPath(self.mapFromScene(fullPath.subtracted(collisionPath)))
            super().paint(qp, opt, widget)
    
            # draw the collision parts with half opacity
            qp.setClipPath(self.mapFromScene(fullPath.intersected(collisionPath)))
            qp.setOpacity(.5)
            super().paint(qp, opt, widget)
    
            qp.restore()
    

    Here is a result with two images, both of which are also rotated:

    Screenshot of an example using the code above

    There is a relatively small issue with the boundaries of the clipping (magnify the image and check the aliased edges between the images), but I believe that it shouldn't be an important issue for your case.

    I also noted some strange (but not persisting) flickering while moving the items whenever one of them is rotated. Unfortunately, I'm afraid that there's little to do with that, unless you're ready to completely rewrite the whole implementation of QPixmap displaying within the graphics scene.