skip to Main Content

I have the below image.

image.png

I make it a mask in javascript and this lets me turn the image to every color that I want. here is the code.

index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body style="background-color: darkgray;">
    <canvas id="theCanvas" style="background-color: lightgray;"></canvas>
    <script src="main.js"></script>
</body>

</html>

main.js

const theCanvas = document.getElementById("theCanvas");
const ctx = theCanvas.getContext("2d");
theCanvas.width = 150;
theCanvas.height = 150;
const image = new Image();
image.onload = drawImageActualSize;
image.src = "image.png";
color = "red";

function drawImageActualSize() {
    ctx.fillStyle = color;
    ctx.rect(0, 0, theCanvas.width, theCanvas.height);
    ctx.fill();
    ctx.globalCompositeOperation = "destination-atop";
    ctx.drawImage(this, 0, 0, theCanvas.width, theCanvas.height);
    ctx.globalCompositeOperation = "multiply";
    ctx.drawImage(image, 0, 0, theCanvas.width, theCanvas.height);
}

and this gives me the below image.

image

now I want to do the same job in Python using PyQt6 but how should I make an image mask in pyqt6?

so far I did this below code.

main.py

from sys import argv
from sys import exit as ex
from pathlib2 import Path
from PyQt6.QtWidgets import QApplication, QWidget, QSizePolicy, QVBoxLayout
from PyQt6.QtCore import Qt, QRectF, QTimer
from PyQt6.QtGui import QPaintEvent, QPainter, QImage, QPen, QColor, QBrush


class PaintWidget(QWidget):
    def __init__(self, parent=None) -> None:
        super().__init__()
        self.setSizePolicy(
            QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
        )
        self.image = QImage(str(Path(Path(__file__).parent, "image.png")))
        self.image.scaledToWidth(150)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update)
        self.timer.start(17)

    def paintEvent(self, event: QPaintEvent | None) -> None:
        painter = QPainter()
        painter.begin(self)
        painter.setPen(QPen(QColor(169, 169, 169), 0, Qt.PenStyle.SolidLine))
        painter.setBrush(QBrush(QColor(169, 169, 169), Qt.BrushStyle.SolidPattern))
        painter.drawRect(0, 0, 1920, 1080)
        rect = QRectF(0, 0, self.image.width(), self.image.height())
        painter.drawImage(rect, self.image)
        painter.end()
        return super().paintEvent(event)


class MainWindow(QWidget):
    def __init__(self) -> None:
        super().__init__()
        self.setup_ui()
        self.show()

    def setup_ui(self) -> None:
        self.showFullScreen()
        self.main_window_layout = QVBoxLayout()
        self.painter_widget = PaintWidget()
        self.main_window_layout.addWidget(self.painter_widget)
        self.setLayout(self.main_window_layout)


if __name__ == "__main__":
    app = QApplication(argv)
    main_window = MainWindow()
    ex(app.exec())

that gives me this.

image

I try this code

self.masking = self.image.createMaskFromColor(0, Qt.MaskMode.MaskInColor)

painter.drawImage(rect, self.masking)

but it turns everything to black and white and gives me this.

image

2

Answers


  1. Chosen as BEST ANSWER

    StackOverflow doesn't let me post a new question so I am asking here again.

    Thanks to @musicamante I changed my code and now I want to change the position and angle of the image over time but the mask of the image does not fit on the image anymore. here is the new code:

    from sys import argv
    from sys import exit as ex
    from pathlib2 import Path
    from PyQt6.QtWidgets import QApplication, QWidget, QSizePolicy, QVBoxLayout
    from PyQt6.QtCore import Qt, QTimer, QPoint, QRect
    from PyQt6.QtGui import QPaintEvent, QPainter, QPixmap, QRegion, QColor, QBitmap
    
    
    class PaintWidget(QWidget):
        def __init__(self, parent=None) -> None:
            super().__init__(parent)
            self.setSizePolicy(
                QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
            )
            self.angle = 0
            self.position = QPoint(400, 400)
            self.image_width = 150
            self.image_height = 150
            image = QPixmap(str(Path(Path(__file__).parent, "image.png")))
            self.image = image.scaledToWidth(
                self.image_width, Qt.TransformationMode.SmoothTransformation
            )
            self.masking = self.image.toImage().createAlphaMask()
            self.update_timer = QTimer(self)
            self.update_timer.timeout.connect(self.update)
            self.update_timer.start(17)
            self.run_timer = QTimer(self)
            self.run_timer.timeout.connect(self.run)
            self.run_timer.start(10)
    
        def run(self):
            self.position.setX(self.position.x() + 1)
            self.angle += 0.5
    
        def paintEvent(self, event: QPaintEvent | None) -> None:
            painter = QPainter(self)
            painter.fillRect(self.rect(), QColor(34, 170, 85))
            rect = QRect(
                -self.image_width // 2,
                -self.image_height // 2,
                self.image_width,
                self.image_height,
            )
            painter.translate(
                self.position.x() - rect.width() / 2,
                self.position.y() - rect.height() / 2,
            )
            painter.rotate(self.angle)
            painter.drawPixmap(rect, self.image)
            painter.setClipRegion(QRegion(QBitmap.fromImage(self.masking)))
            painter.setCompositionMode(painter.CompositionMode.CompositionMode_Multiply)
            painter.fillRect(rect, Qt.GlobalColor.red)
    
    
    class MainWindow(QWidget):
        def __init__(self, parent=None) -> None:
            super().__init__(parent)
            self.setup_ui()
    
        def setup_ui(self) -> None:
            self.showFullScreen()
            self.main_window_layout = QVBoxLayout()
            self.painter_widget = PaintWidget()
            self.main_window_layout.addWidget(self.painter_widget)
            self.setLayout(self.main_window_layout)
    
    
    if __name__ == "__main__":
        app = QApplication(argv)
        main_window = MainWindow()
        ex(app.exec())
    

    and this is the result:

    image

    I also noticed a bug and that is some white points on the image and mask(not on background). Where did they come from?


  2. There are different ways to achieve such effects, and it depends on multiple aspects, including how the source image is actually made of.

    In your specific case, you have an alpha channel which is fully transparent, so you can use QPainter’s composition modes, which are conceptually similar to those of the javascript canvas, but work in slightly different ways.

    Your attempt doesn’t work for a simple reason: the createMaskFromColor() only creates a basic monochrome bitmap (pixels are either 0 or 1), and such image should normally be used as a mask, not directly drawn.

    One possibility is to draw the image, then set the clip region based on the mask of the image alpha, and set the CompositionMode_Multiply.

    Note that this will only work when the "border" is fully black, and it’s not 100% accurate, as you’ll clearly see a "red-ish" margin around the shape; coincidentally, this is also what you got from the javascript code.

    class PaintWidget(QWidget):
        def __init__(self, parent=None) -> None:
            super().__init__(parent)
            self.setSizePolicy(
                QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
            )
            image = QPixmap(str(Path(Path(__file__).parent, "image.png")))
            self.image = image.scaledToWidth(
                150, Qt.TransformationMode.SmoothTransformation
            )
            self.setMinimumSize(self.image.size())
    
        def paintEvent(self, event: QPaintEvent) -> None:
            painter = QPainter(self)
            painter.fillRect(self.rect(), QColor(169, 169, 169))
    
            rect = self.image.rect()
            painter.drawPixmap(rect, self.image)
    
            mask = self.image.toImage().createAlphaMask()
            painter.setClipRegion(QRegion(QBitmap.fromImage(mask)))
            painter.setCompositionMode(
                painter.CompositionMode.CompositionMode_Multiply)
            painter.fillRect(rect, Qt.GlobalColor.red)
    

    Note that I made some changes to your code; most importantly:

    • scaledToWidth() returns an image, does not resize itself (note that it’s named "scaled");
    • it’s normally better to use QPixmap instead of QImage when painting on screen; to slightly improve performance, you may create the mask in the __init__ (converting QPixmap to QImage is a bit costly);
    • using a 0-width pen with the same color as the brush to draw a colored rectangle is completely pointless: use QPainter.fillRect() instead;
    • QRectF(0, 0, self.image.width(), self.image.height()) is also pointless, as you can just use self.image.rect();
    • if you’re overriding painting, you shall not call the base implementation (super().paintEvent(event)); returning it is also pointless, since it’s implicitly None;
    • considering the above, you can also simplify the code by creating the painter with the widget in its constructor, and avoid the end() call, which is implicit as soon as the function returns (since the painter will be garbage collected);
    • there’s no need to use a timer to update the widget, unless the contents actually change;
    • it’s also good practice to not call self.show() within the constructor of a widget, even if it’s intended as a window;
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search