I used QPainter’s drawPixmap()
to draw an image, and the result is shown as B in the following picture.
Then, I opened the same image in the image viewer program provided by Windows, zooming it even smaller, as shown in A in the following picture.
It’s obvious that B is larger than A, but it is not as clear as A. B has noticeable Aliasing.
I tried setRenderHint(QPainter::Antialiasing)
and setRenderHint(QPainter::SmoothPixmapTransform)
, it had some effect, but not enough to solve the problem.
I tried different ways to display the same image(OpenGL, webEngine, QPainter.drawPixmap, graphicsView), and the results varied, but all of them were of lower quality than the Windows built-in image viewer.
I don’t think it’s an issue with the image’s clarity. When I use drawPixmap() to draw the image and call QPainter’s scale() method to enlarge it, I can see the details of the image.
Does anyone know how to draw images in Qt with the same level of clarity as the built-in Windows software? Even when I scale down the image, I want to avoid Aliasing problem.
-- EDIT --
minimal reproducible demo:
// canvaswidget.h
#ifndef CANVASWIDGET_H
#define CANVASWIDGET_H
#include <QWidget>
class CanvasWidget : public QWidget
{
Q_OBJECT
public:
explicit CanvasWidget(QImage img, QWidget *parent = nullptr);
void zoomIn();
void zoomOut();
signals:
protected:
QSize sizeHint();
void paintEvent(QPaintEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
private:
qreal scale;
QPixmap pixmap;
};
#endif // CANVASWIDGET_H
// canvaswidget.cpp
#include "canvaswidget.h"
#include <QWheelEvent>
#include <QPainter>
#include <QPixmap>
CanvasWidget::CanvasWidget(QImage img, QWidget *parent)
: QWidget{parent}, scale(1.0)
{
// make sure high resolution source
pixmap = QPixmap::fromImage(img.scaled(img.size() * 10, Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
void CanvasWidget::zoomIn() {
scale = fmin(scale + 0.1, 10);
update();
}
void CanvasWidget::zoomOut() {
scale = fmax(scale - 0.1, 0.1);
update();
}
void CanvasWidget::paintEvent(QPaintEvent *event) {
if(!pixmap) {
return QWidget::paintEvent(event);
}
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing);
p.setRenderHint(QPainter::SmoothPixmapTransform);
p.drawPixmap(0,0,width() * scale,height() * scale, pixmap); // draw image
}
void CanvasWidget::wheelEvent(QWheelEvent *event)
{
if(event->modifiers() == Qt::ControlModifier) {
QPointF delta = event->angleDelta();
int v_delta = delta.y();
if(v_delta > 0) {
zoomIn();
} else {
zoomOut();
}
update();
adjustSize();
} else {
QWidget::wheelEvent(event);
}
}
QSize CanvasWidget::sizeHint()
{
return QSize(800,800);
}
The left side of the following picture shows the result of this demo, while the right side shows the Windows built-in image viewer.
--- EDIT 2 ---
Here is the image I used in the demo, it's format was QImage::Format_ARGB32
and it's size is QSize(1541, 1188)
25KB PNG
There are two causes for your problem:
Scaling an aliased image like that will often create issues even with scaling factors closer to 1 (above or below), specifically with factors that are not based on powers of 2.
The simple bilinear interpolation will make those artifacts much more visible, because some of those "lines" (made of just one pixel) will be completely ignored with a downsampling factor greater than two.
There is no warning about this aspect in the documentation, but I've been able to track down a reliable source, from the words of Allan Sandfeld Jensen (currently Senior Manager and Principal Engineer of Qt WebEngine):
Likes like you are downscaling more than 2x. QPainter is doing bilinear sampling when smooth scaling, and that produces bad results at 2x downscaling.
QImage::smoothScaled() uses a slower box scaling algorithm that works at even the most aggressive downscaling.
Except for platforms/configurations that implement QPixmap transformations on their own, QPixmap indeed relies internally on QImage::smoothScaled()
when using the Qt::SmoothTransformation
mode.
This is achieved by using the "box scaling" written above, which is similar in concept, but uses larger "boxes" of near pixels of the source in order to compute the value of each scaled down pixel of the destination.
The attempt of using OpenGL didn't work because in that case using the SmoothPixmapTransform
render hint on the painter is not sufficient (it is, indeed, a hint). The surface must also be properly configured so that it explicitly sets the number of samples per pixel used when multisampling (which is what is used to reduce aliasing); see all the answers to this related post.
The partial failing of QtWebEngine may depend on the default scaling algorithm set in Chromium (which also depends on the actual Chromium version in use in your Qt). Note that CSS provides the image-rendering
property, which may improve that (I've not tested it with QtWebEngine, though).
The QGraphicsView approach will be identical in results, because its paint()
function relies on the scaling done by drawPixmap()
.
As you already found out, the only appropriate solution for such images and scaling factors is to use QPixmap::scaled()
, but be aware about the performance issues of the QImage
conversion noted above: not only the QPixmap is converted to a QImage in order to be scaled, but it also has to be converted back to QPixmap in order to be rendered.
Those conversions are quite expansive, especially for large images.
Considering that, one may be tempted to just use QImage as a source, then only call drawImage()
with a scaled()
of the source QImage, but keep in mind that when QPainter is used on a QPixmap or QWidget drawImage()
will always convert the QImage to a QPixmap anyway. That option may be an improvement, but it still is not optimal.
The more appropriate solution would be to actually scale the image at the required size but only when a resize happens; since you already get a QImage in the constructor, that's even better:
scaled()
, converted to a QPixmap;resizeEvent()
and call the above function;paintEvent()
check whether the cached QPixmap is valid (and call that function in case it isn't), then draw it;Alternatively, invalidate the pixmap in the resizeEvent()
and create a function getter for the scaled pixmap that will regenerate it if it's not valid.
Finally, as already noted in the comments, creating an upscaled image of the source in the constructor is completely inappropriate:
paintEvent()
call, which is something that happens quite often;