How to mask an image with a smooth circle in PyQt5
Posted on
I am currently working on a PyQt5 based user manager and want to display user images as circle.
User image can have any size and aspect ratio, so I also need to crop them before applying the mask:
You can easily set a QPixmap
to a QLabel
and show it in your widget.
However, it took me hours to find out how to mask an image with a smooth,
anti-aliased circle.
If you search for this problem, you’ll find some (relatively old) StackOverflow questions and topics in Qt5 forums, but none of the proposed solutions worked for me.
The following snipped does:
- crop the original image to a square
- mask it with a smooth, anti-aliased circle
- rescale it to the required size
- take care of Retina / high DPI displays
from PyQt5.QtCore import Qt, QRect
from PyQt5.QtGui import QBrush, QImage, QPainter, QPixmap, QWindow
from PyQt5.QtWidgets import QLabel, QVBoxLayout, QWidget
def mask_image(imgdata, imgtype='jpg', size=64):
"""Return a ``QPixmap`` from *imgdata* masked with a smooth circle.
*imgdata* are the raw image bytes, *imgtype* denotes the image type.
The returned image will have a size of *size* × *size* pixels.
"""
# Load image and convert to 32-bit ARGB (adds an alpha channel):
image = QImage.fromData(imgdata, imgtype)
image.convertToFormat(QImage.Format_ARGB32)
# Crop image to a square:
imgsize = min(image.width(), image.height())
rect = QRect(
(image.width() - imgsize) / 2,
(image.height() - imgsize) / 2,
imgsize,
imgsize,
)
image = image.copy(rect)
# Create the output image with the same dimensions and an alpha channel
# and make it completely transparent:
out_img = QImage(imgsize, imgsize, QImage.Format_ARGB32)
out_img.fill(Qt.transparent)
# Create a texture brush and paint a circle with the original image onto
# the output image:
brush = QBrush(image) # Create texture brush
painter = QPainter(out_img) # Paint the output image
painter.setBrush(brush) # Use the image texture brush
painter.setPen(Qt.NoPen) # Don't draw an outline
painter.setRenderHint(QPainter.Antialiasing, True) # Use AA
painter.drawEllipse(0, 0, imgsize, imgsize) # Actually draw the circle
painter.end() # We are done (segfault if you forget this)
# Convert the image to a pixmap and rescale it. Take pixel ratio into
# account to get a sharp image on retina displays:
pr = QWindow().devicePixelRatio()
pm = QPixmap.fromImage(out_img)
pm.setDevicePixelRatio(pr)
size *= pr
pm = pm.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
return pm
class Window(QWidget):
"""Simple window that shows our masked image and text label."""
def __init__(self, imgpath):
super().__init__()
imgdata = open(imgpath, 'rb').read()
pixmap = mask_image(imgdata)
ilabel = QLabel()
ilabel.setPixmap(pixmap)
tlabel = QLabel('Hello, world!')
layout = QVBoxLayout()
layout.addWidget(ilabel, 0, Qt.AlignCenter)
layout.addWidget(tlabel, 0, Qt.AlignCenter)
self.setLayout(layout)
if __name__ == '__main__':
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
w = Window(imgpath=sys.argv[1])
w.show()
sys.exit(app.exec_())
And this is how the result looks like:
I also toyed around with QPainter.setCompositionMode()
and
QPixmap.setMask()
but both approaches did not lead to the desired results.
If you have found a better solution for this problem, please let me know via Mastodon or Twitter.