从图像构造 QPainterPath [重复]

Posted

技术标签:

【中文标题】从图像构造 QPainterPath [重复]【英文标题】:Construct a QPainterPath from an image [duplicate] 【发布时间】:2020-12-01 06:48:07 【问题描述】:

以这张图片为例:

是否可以从图像构造一个QPainterPath(路径为黑色形状/轮廓)?

我知道您可以使用 QPainterPath 方法手动定义这个形状(quadTocubicTolineTo 等——尽管很难准确地重新创建)并设置一个具有适当宽度的 QPen。但我想知道是否有任何方法可以通过读取像素数据或其他方式来定义基于黑色像素的 QPainterPath。

我的目标是填充形状的内部(显示在 GUI 中),我非常喜欢使用 QPainter 完成此操作,而不是填充图像,因为:

它比洪水填充快 100 倍。 线条经过抗锯齿处理(因此泛光填充会在轮廓周围留下白色/灰色像素)。 Flood 填充仅限于纯色,而 QPainter 可以填充纹理、图案和渐变。

【问题讨论】:

“它比洪水填充快 100 倍”虽然这可能是真的,但是是什么让您认为矢量化光栅图像更快?矢量化实际上非常复杂,它基于必须对图像进行多次的启发式和优化。可能有一种方法可以将createMaskFromColor()createHeuristicMask() 与QRegion 混合使用,但问题是:值得吗?我只是花一些时间在手动构建的 QPainterPath 上。 不管怎样,这个问题已经在这里问过了:How to convert QPixMap to QPainterPath and vice-versa? 【参考方案1】:

如 cmets 和(可能)duplicate question 中所述,Qt 没有光栅图像矢量化的方法。

除了手动重新创建路径之外,可以对示例中具有大边框的简单图像实现更好的“泛滥”结果。

问题显然在于图像内部(以及外部,如果您想使用不同的颜色)的抗锯齿。 诀窍是执行光栅图像处理程序通常所做的事情,即根据颜色创建一个蒙版,并扩展该蒙版“羽化”它。

在 Qt 术语中,这可以通过位于作为掩码一部分的像素中间的小椭圆(2 像素宽)来实现。由于无法知道遮罩的边界,因此我们需要循环遍历区域的所有像素,并在点位于区域内时绘制平滑像素。

如您所见,效果还不错,但仍远非完美(简单的遮罩是中间图像,而伪抗锯齿在右侧):

这是一个展示整个过程的示例,包括简单的遮罩和“平滑”像素技巧:

from PyQt5 import QtGui, QtCore, QtWidgets

class Test(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()
        layout = QtWidgets.QHBoxLayout(self)

        self.sourceLabel = QtWidgets.QLabel()
        layout.addWidget(self.sourceLabel)
        self.maskedLabel = QtWidgets.QLabel()
        layout.addWidget(self.maskedLabel)
        self.targetLabel = QtWidgets.QLabel()
        layout.addWidget(self.targetLabel)

        self.border = QtGui.QColor(QtCore.Qt.red)
        self.color = QtGui.QColor(QtCore.Qt.green)

        self.sourceLabel.installEventFilter(self)
        self.maskedLabel.installEventFilter(self)
        self.targetLabel.installEventFilter(self)

        self.processPixmap('cloud.png')

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.MouseButtonPress:
            self.setColor(event.pos() in self.innerMask)
        return super().eventFilter(source, event)

    def setColor(self, inside):
        dialog = QtWidgets.QColorDialog(self.color, self)
        if dialog.exec_():
            if inside:
                self.color = dialog.currentColor()
            else:
                self.border = dialog.currentColor()
            self.processPixmap()

    def processPixmap(self, path=None):
        if path is None:
            path = self.path
        self.path = path
        self.sourceLabel.setPixmap(QtGui.QPixmap(path))

        source = QtGui.QImage(path)
        fullMask = source.createHeuristicMask()
        fullMaskRegion = QtGui.QRegion(QtGui.QBitmap(fullMask))
        outColor = source.pixel(0, 0)
        borderMask = source.createMaskFromColor(outColor)
        borderMaskRegion = QtGui.QRegion(QtGui.QBitmap(borderMask))
        self.innerMask = fullMaskRegion - borderMaskRegion
        outerMask = fullMaskRegion + borderMaskRegion

        masked = QtGui.QPixmap(source.size())
        masked.fill(self.border)
        qp = QtGui.QPainter(masked)
        qp.setClipRegion(fullMaskRegion)
        qp.drawImage(0, 0, source)
        qp.setClipRegion(self.innerMask)
        qp.fillRect(source.rect(), self.color)
        qp.end()
        self.maskedLabel.setPixmap(masked)

        t = QtCore.QElapsedTimer()
        t.start()
        output = QtGui.QPixmap(source.size())
        output.fill(QtCore.Qt.transparent)
        qp = QtGui.QPainter(output)
        qp.setRenderHints(qp.Antialiasing)
        qp.save()
        qp.setClipRegion(fullMaskRegion)
        qp.fillRect(source.rect(), self.border)
        qp.drawImage(0, 0, source)
        qp.restore()

        qp.setPen(QtCore.Qt.NoPen)
        pixel = QtCore.QRectF(-.5, -.5, 2, 2)
        for rowPixel in range(output.height()):
            for colPixel in range(output.width()):
                p = QtCore.QPoint(colPixel, rowPixel)
                if p in self.innerMask:
                    qp.setBrush(self.color)
                    qp.drawEllipse(pixel.translated(p))
                if not p in outerMask:
                    qp.setBrush(self.border)
                    qp.drawEllipse(pixel.translated(p))
        qp.end()
        self.targetLabel.setPixmap(output)

        print('Antialiasing finished in ms'.format(t.elapsed()))

import sys
app = QtWidgets.QApplication(sys.argv)
w = Test()
w.show()
sys.exit(app.exec_())

您可以点击任意图片的外部选择外部颜色,点击内部选择背景颜色。

【讨论】:

谢谢,这至少可以填充任何 QBrush 并很好地处理抗锯齿 @alec 请注意,这远非完美:它适用于不太薄的“柔和”颜色和轮廓,当然不适用于直线和明确定义的水平或垂直线,因为即使它们的来源不是,它们也会被“软化”。但是,是的,这是一个可以接受的解决方法。还要考虑处理所有像素需要一些时间:对于您的源图像,在我 8 岁的 4 核 i5 上需要大约 100 毫秒,这是很多时间。如果您要处理更大的图像,您应该考虑查看图像专用库,例如​​ OpenCV。

以上是关于从图像构造 QPainterPath [重复]的主要内容,如果未能解决你的问题,请参考以下文章

使用Python对图像进行不同级别量化QP,使用RLE计算压缩比,并计算对应的PSNR

四种码率控制相关概念

查找直线和 QPainterPath 之间的交点

在 QGraphicsItem 中使用 QPainterPath 检测鼠标命中

QPainterPath 不规则提示框

派生QPainterPath,QPainter性能下降很快