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) 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.