I have the below image.
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.
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.
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.
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");__init__
(converting QPixmap to QImage is a bit costly);QPainter.fillRect()
instead;QRectF(0, 0, self.image.width(), self.image.height())
is also pointless, as you can just use self.image.rect()
;super().paintEvent(event)
); returning it is also pointless, since it's implicitly None
;end()
call, which is implicit as soon as the function returns (since the painter will be garbage collected);self.show()
within the constructor of a widget, even if it's intended as a window;