程序员最近都爱上了这个网站  程序员们快来瞅瞅吧!  it98k网:it98k.com

本站消息

站长简介/公众号

  出租广告位,需要合作请联系站长

+关注
已关注

分类  

暂无分类

标签  

暂无标签

日期归档  

暂无数据

如何根据场景形状裁剪像素图图像

发布于2024-11-26 22:18     阅读(733)     评论(0)     点赞(27)     收藏(0)


我正在尝试使用 QGraphicsScene 的给定边界来裁剪 QGraphicsPixmapItem。但是,我认为我并不真正理解裁剪的工作原理,据我所知,当您在 QGraphicsPixmapItem 的 paint 方法中将 setClipRect 应用于图像时,它应该会裁剪图像。

我有这个类,它充当查看器:

class ImageViewer(QGraphicsView):
    def __init__(self, parent, tab):
        super().__init__(parent)
        self.setScene(QGraphicsScene(self))

    def displayImage(self, image):
        rgb_image = image.convert('RGB')
        data = rgb_image.tobytes()
        qim = QImage(data, rgb_image.size[0], rgb_image.size[1], QImage.Format_RGB888)
        pixmap = QPixmap(qim)
        self.scene().clear()
        scene_rect = QRectF(0, 0, pixmap.width(), pixmap.height())
        self.setSceneRect(scene_rect)
        pixmapItem = ImagePixmapItem(pixmap, self.sceneRect())
        self.scene().addItem(pixmapItem)
        self.fitInView(self.sceneRect(), Qt.KeepAspectRatio)
        self.drawBorder()
        pixmapItem.rotate()

在这里,我有我的图像 QPixmap,并定义了 pixmapItem,它是从 QGraphicsPixmapItem 继承的自定义类。我还画了一个边框,只是为了看看我的场景的边界在哪里。

class ImagePixmapItem(QGraphicsPixmapItem):
    def __init__(self, pixmap, scene_rect):
        super().__init__(pixmap)
        self.scene_rect = scene_rect

    def paint(self, painter, option, widget):
        painter.setClipRect(self.scene_rect)
        super().paint(painter, option, widget)
        
    def rotate(self):
        self.setTransformOriginPoint(self.boundingRect().center())
        rotation_angle = 45
        self.setRotation(rotation_angle)

现在,我的图像尺寸为 1000x1000。启动程序时,场景的尺寸也是 1000x1000。当我使用 rotate 方法旋转图像时,方形图像理论上应该比 1000x1000 占用更多空间。整个程序启动后以及我在场景内滚动或移动时,都会调用 paint 方法。在那里,我期望 setClipRect 会剪辑我的图像。场景之外的部分不应显示。但是,它根本不起作用。

我被困在这里,不知道问题是什么。


解决方案


相对坐标和变换

除了明确基于“父”上下文的函数(例如pos(),位于父坐标中)外,所有内容始终位于项目坐标中,包括paint()

所有绘画都是在局部坐标中完成的。

调用时paint(),假定收到的 QPainter 已经处于对项目设置的所有变换的上下文中,包括其父级、视图和其自身的变换。这是因为paint()无论对项目应用了何种变换(即使是间接的),都应始终无缝实现。

这是出于显而易见且良好的实现原因:如果您有一个显示矩形的项目,则只需调用painter.drawRect(),而不必关心其可能的缩放或旋转。调用setRotation()将在显示时自动旋转该矩形,并在转换后的drawRect()绘制上下文中绘制一个矩形最终将显示为旋转后的矩形。

相对剪裁

QPainter 的所有setClip...()函数都指定以下内容:

请注意,剪辑矩形是在逻辑(画家)坐标中指定的。

这与上面的相同,意味着剪辑也将在当前变换上下文中设置:如果画家旋转,剪辑矩形也会旋转,类似于drawRect()上面的示例。

这就是简单地使用场景矩形进行调用毫无意义的原因setClipRect():“矩形”与已变换的画家矩阵正交,并且由于该矩形也与像素图矩形重合,因此结果不变。尝试将该调用更改为较小的矩形,您将看到剪辑仍然与项目正交。

可能的解决方案

有很多可能的方法可以正确实现请求,每种方法都有各自的优点和缺点。

剪切前重置变换

一种可能性是存储画家的当前变换,然后暂时重置变换,设置剪裁,最后在调用之前再次恢复变换super().paint()

class ImagePixmapItem(QGraphicsPixmapItem):
    ...
    def paint(self, painter, option, widget):
        realTransform = painter.transform()
        painter.resetTransform()
        view = self.scene().views()[0]
        mapRect = view.mapFromScene(self.scene_rect).boundingRect()
        painter.setClipRect(mapRect)
        painter.setTransform(realTransform)
        super().paint(painter, option, widget)

这种方法的主要缺点是它依赖于必须始终只有一个视图的事实。如果场景设置在多个视图上,它将对任何非“第一个”视图产生意外影响,甚至如果根本没有视图(例如,对于屏幕外渲染),它可能会引发异常。

我不建议采用这种方法,因为唯一的“好处”就是能够使用setClipRect(),但这样做的复杂性、设置/重置转换的开销以及上面提到的缺点使得它无效。

在项目坐标中设置剪辑

因为如上所述,setClipRect()在当前画家上下文中调用是无效的,并且上述解决方案不可靠,所以更准确的解决方案是将实际场景矩形映射到项目变换,并使用setClipPath()包含该映射矩形的 QPainterPath(实际上是 QPolygonF,因为变换也可以有透视)。

    def paint(self, painter, option, widget):
        map = self.mapFromScene(self.scene_rect)
        path = QPainterPath()
        path.addPolygon(map)
        painter.setClipPath(path)
        super().paint(painter, option, widget)

这比第一个解决方案好得多,但仍然需要变换映射并在每次paint()调用时构造一个 QPainterPath(即使使用缓存,这也可能发生很多次),即使场景和变换没有改变。

使用父项进行剪辑

正如ekhumoro 所建议的,可以添加一个充当剪辑蒙版的顶级 QGraphicsRectItem(通过设置ItemClipsChildrenToShape标志),然后将所有需要剪辑的元素添加为该项目的子项。

    def displayImage(self, image):
        ...
        clipItem = self.scene().addRect(scene_rect)
        clipItem.setFlag(QGraphicsItem.ItemClipsChildrenToShape)
        clipItem.setPen(QPen(Qt.NoPen))
        # see the changed argument signature
        pixmapItem = ImagePixmapItem(pixmap, clipItem)
        ...


class ImagePixmapItem(QGraphicsPixmapItem):
    # no __init__() nor paint() override required
    def rotate(self):
        ...

这是一个改进,因为paint()不需要覆盖(因此避免了 Python 瓶颈)。唯一的缺点是所有项目都必须设置为的子项clipItem,这种复杂程度可能会在检查对象结构时产生一些问题。

请注意,由于__init__paint现在都不再需要,仅为 进行子类化rotate变得毫无意义,因为您只需在 内调用setTransformOriginPoint()和即可setRotation(45)displayImage()

覆盖drawForeground()

Finally, as long as all items have to be clipped, you could override drawForeground() on the view or the scene, set a possibly "hollow region" as masking, and draw the given rectangle above all the contents.

The "hollow region" is a QRegion based on the rect argument of drawForeground() (the possibly full rectangle drawn on the view, or a portion of it), subtracted by a QRegion created with the scene rect.

In this way, the given rect will only paint in the parts that are outside of the scene, and you don't need to implement clipping at all.

class ImageViewer(QGraphicsView):
    ...
    def drawForeground(self, qp, rect):
        full = QRegion(rect.toRect())
        mask = QRegion(self.sceneRect().toRect())
        qp.setClipRegion(full - mask)
        qp.fillRect(rect, self.palette().base())

Alternatively, the same code can also be used in a subclass of QGraphicsScene, but in displayImage() you need to call self.scene().setSceneRect() instead, because calling the view's setSceneRect() does not affect the sceneRect of the scene.

This approach is actually a "hack", but has its benefits: you don't need to override paint() at all (or even require subclassing, as explained in the previous point), and the object structure is preserved.

Still, it's not without issues: if the scene is changed frequently (eg. due to fast scrolling/scaling or animations), it can affect performance even if no real clipping is in effect; it also completely clears out any background explicitly set using setBackgroundBrush() or overriding drawBackground() outside of the "clip" region.



所属网站分类: 技术文章 > 问答

作者:黑洞官方问答小能手

链接:https://www.pythonheidong.com/blog/article/2046118/c7cebed769d86c9e6751/

来源:python黑洞网

任何形式的转载都请注明出处,如有侵权 一经发现 必将追究其法律责任

27 0
收藏该文
已收藏

评论内容:(最多支持255个字符)