【Qt】图片绘制不清晰的问题

背景

实现一个图片浏览器,可以支持放大/缩小查看图片。主要组件如下:

// 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)
{
    pixmap = QPixmap::fromImage(img);
}

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.scale(scale, scale);
    p.drawPixmap(0,0,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);
}

问题

在这种实现方式下,缩小图片时,图片会变得非常模糊,有非常明显的锯齿问题。
如下图所示,A是Windows自带图片查看器的效果,B是上述实现的效果。可以看出虽然B比A更大,但却更不清晰,有明显的锯齿。

在这里插入图片描述

尝试解决

为了解决这个不清晰的问题,尝试了很多种方案,方案及其实现方法如下:

不scale QPainter,而是在指定区域绘制Pixmap

p.drawPixmap(0,0,pixmap.size().width() * scale, pixmap.size().height * scale, pixmap);

使用QGraphicsView绘制图片

    QPixmap pixmap("/path/to/image.png");
    
    QGraphicsScene scene;
    QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap);
    scene.addItem(item);
    
    QGraphicsView view;
    view.resize(800,600);
    view.setScene(&scene);

    // Optionally set view properties
    view.setRenderHint(QPainter::Antialiasing);   // Improve rendering quality
    view.setDragMode(QGraphicsView::ScrollHandDrag); // Enable dragging
    view.setAlignment(Qt::AlignCenter);           // Center the image
    view.fitInView(item, Qt::KeepAspectRatio);    // Scale to fit the view

    // Show the view
    view.show();

使用QWebEngineView绘制图片

    QWebEngineView web_view;

    QString htmlContent = R"(
        <!DOCTYPE html>
        <html>
        <head>
            <style>
                body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; }
                img { max-width: 100%; max-height: 100%; }
            </style>
        </head>
        <body>
            <img src="/path/to/image.png" alt="Image Not Found">
        </body>
        </html>
    )";

    web_view.setHtml(htmlContent, QUrl::fromLocalFile(QCoreApplication::applicationDirPath() + "/"));
    web_view.resize(800, 600);
    web_view.show();

将图片作为texture在QOpenGLWidget中绘制图片

#ifndef OPENGLIMAGE_H
#define OPENGLIMAGE_H

#include <QOpenGLTexture>
#include <QOpenGLShaderProgram>
#include <QOpenGLWidget>
#include <QOpenGLFunctions>
#include <QOpenGLBuffer>
#include <QOpenGLVertexArrayObject>
#include <memory>

class OpenGLImage : public QOpenGLWidget, protected QOpenGLFunctions
{
    Q_OBJECT
public:
    explicit OpenGLImage(QWidget *parent = nullptr);
    ~OpenGLImage();
    QSize minimumSizeHint() const override;
    QSize sizeHint() const override;
    void loadImage(QString& path);
    QMatrix4x4 getViewMatrix() const;
    QMatrix4x4 getModelMatrix() const;

protected:
    void initializeGL() override;
    void paintGL() override;
    void resizeGL(int width, int height) override;
    void wheelEvent(QWheelEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mousePressEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
    void keyPressEvent(QKeyEvent *event) override;
    void keyReleaseEvent(QKeyEvent* event) override;

private:
    void setupDefaultShaderProgram();
    void setupDefaultTransform();
    void drawImage();
    void moveImage(const QPointF& cursorPos);
    void rotateImage(const QPointF& cursorPos);

    std::unique_ptr<QOpenGLShaderProgram> shaderProgram;
    std::unique_ptr<QOpenGLTexture> texture;
    std::unique_ptr<QImage> image;
    QOpenGLBuffer vbo;
    QOpenGLVertexArrayObject vao;
    QOpenGLBuffer ebo;
    bool isTextureSync;
    QColor clearColor;
    float norm_h;
    QSize viewSize;

    QVector3D cameraPos;
    QVector3D imagePos;
    QVector3D imageAngle;
    float viewAngle;
    float focalLength;

    QPointF lastClickPos;
    bool isRotMode;
};

#endif // OPENGLIMAGE_H
#include "glimageview.h"
#include <vector>
#include <QtMath>
#include <iostream>
#include <QResizeEvent>

#define PROGRAM_VERTEX_ATTRIBUTE 0
#define PROGRAM_TEXCOORD_ATTRIBUTE 1

#define DEFAULT_CAMERA_POS_X (0.0f)
#define DEFAULT_CAMERA_POS_Y (0.0f)
#define DEFAULT_CAMERA_POS_Z (-2.0f)

#define CLIP_NEAR (0.01f)
#define CLIP_FAR (100.0f)

#define MIN_FOCAL 1.0f
#define MAX_FOCAL 150.0f

OpenGLImage::OpenGLImage(QWidget *parent)
    : QOpenGLWidget(parent),
      shaderProgram(nullptr),
      texture(nullptr),
      image(nullptr),
      isTextureSync(false),
      clearColor(Qt::gray),
      norm_h(-1.0f),
      viewSize(640,640),
      ebo(QOpenGLBuffer::Type::IndexBuffer),
      viewAngle(45.0f),
      isRotMode(false)
{
    focalLength = 1/qTan(qDegreesToRadians(viewAngle/2.0f));
}

OpenGLImage::~OpenGLImage()
{
}

void OpenGLImage::initializeGL()
{
    initializeOpenGLFunctions();
    setupDefaultShaderProgram();
}

void OpenGLImage::paintGL()
{
    glClearColor(clearColor.redF(), clearColor.greenF(), clearColor.blueF(), clearColor.alphaF());
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    drawImage();
}

void OpenGLImage::resizeGL(int width, int height)
{
    viewSize = QSize(width, height);
}

QSize OpenGLImage::minimumSizeHint() const
{
    int min_h = (int)(320.0f * norm_h);
    return QSize(320, min_h);
}

QSize OpenGLImage::sizeHint() const
{
    return viewSize;
}

void OpenGLImage::wheelEvent(QWheelEvent *event)
{
    QPoint numDegrees = event->angleDelta() / 8;
    float degree = (float)numDegrees.y() * -1.0f;
    degree /= 2.0f;
    if (viewAngle+degree > MIN_FOCAL && viewAngle+degree < MAX_FOCAL) {
        viewAngle += degree;
        focalLength = 1/qTan(qDegreesToRadians(viewAngle/2.0f));
    }
    event->accept();
    update();
}

void OpenGLImage::drawImage() {
    if (image.get() == nullptr) return;
    glViewport(0, 0, viewSize.width(), viewSize.height());
    // qDebug() << viewSize.width() << ", " << viewSize.height() << "\n";
    // setup vertex array object
    if (!vao.isCreated())
    {
        vao.create();
    }
    vao.bind();

    // setup vertex buffer object
    std::vector<GLfloat> coords;
    // bottom left;
    coords.push_back(-1.0f);
    coords.push_back(-1.0f * norm_h);
    coords.push_back(0.0f);
    // tex coordinate
    coords.push_back(0.0f);
    coords.push_back(0.0f);

    // bottom right
    coords.push_back(1.0f);
    coords.push_back(-1.0f * norm_h);
    coords.push_back(0.0f);
    // tex coordinate
    coords.push_back(1.0f);
    coords.push_back(0.0f);

    // top right
    coords.push_back(1.0f);
    coords.push_back(1.0f * norm_h);
    coords.push_back(0.0f);
    // tex coordinate
    coords.push_back(1.0f);
    coords.push_back(1.0f);

    // top left
    coords.push_back(-1.0f);
    coords.push_back(1.0f * norm_h);
    coords.push_back(0.0f);
    // tex coordinate
    coords.push_back(0.0f);
    coords.push_back(1.0f);

    if (!vbo.isCreated())
    {
        vbo.create();
    }
    vbo.bind();
    vbo.allocate(coords.data(), coords.size()*sizeof(GLfloat));

    // setup vertex element object
    // [bl, br, tr, tl]
    static const std::vector<GLuint> indices {
        0, 1, 2,
        2, 3, 0
    };
    if (!ebo.isCreated())
    {
        ebo.create();
    }
    ebo.bind();
    ebo.allocate(indices.data(), indices.size()*sizeof(GLuint));

    // associate vertex and buffer
    shaderProgram->enableAttributeArray(PROGRAM_VERTEX_ATTRIBUTE);
    shaderProgram->enableAttributeArray(PROGRAM_TEXCOORD_ATTRIBUTE);
    shaderProgram->setAttributeBuffer(PROGRAM_VERTEX_ATTRIBUTE, GL_FLOAT, 0, 3, 5 * sizeof(GLfloat));
    shaderProgram->setAttributeBuffer(PROGRAM_TEXCOORD_ATTRIBUTE, GL_FLOAT, 3 * sizeof(GLfloat), 2, 5 * sizeof(GLfloat));

    // assign transform matrices
    QMatrix4x4 projection; // projection matrxi must update everytime!
    float ratio = ((float)viewSize.width())/((float)viewSize.height());
    projection.perspective(viewAngle, ratio, CLIP_NEAR, CLIP_FAR);
    QMatrix4x4 model = getModelMatrix();
    model.rotate(imageAngle.x(), 0.0f, 1.0f, 0.0f);
    model.rotate(imageAngle.y()*-1.0f, 1.0f, 0.0f, 0.0f);
    shaderProgram->setUniformValue("model", model);
    QMatrix4x4 viewMat = getViewMatrix();
    shaderProgram->setUniformValue("view", viewMat);
    shaderProgram->setUniformValue("projection", projection);

    // setup texture
    if (texture.get() == nullptr || !isTextureSync) {
        QImage& img = *image.get();
        texture = std::unique_ptr<QOpenGLTexture>(new QOpenGLTexture(img));
        isTextureSync = true;
    }
    texture->bind();
    glDrawElements( GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0 );
}

void OpenGLImage::setupDefaultShaderProgram()
{
    QOpenGLShader *vshader = new QOpenGLShader(QOpenGLShader::Vertex, this);
    const char *vsrc =
        "attribute highp vec3 vertex;\n"
        "uniform mediump mat4 model;\n"
        "uniform mediump mat4 view;\n"
        "uniform mediump mat4 projection;\n"
        "\n"
        "attribute mediump vec2 texCoord;\n"
        "varying mediump vec2 texc;\n"
        "void main(void)\n"
        "{\n"
        "    gl_Position = projection * view * model * vec4(vertex, 1.0f);\n"
        "    texc = texCoord;\n"
        "}\n";
    vshader->compileSourceCode(vsrc);

    QOpenGLShader *fshader = new QOpenGLShader(QOpenGLShader::Fragment, this);
    const char *fsrc =
        "uniform sampler2D texture;\n"
        "varying mediump vec2 texc;\n"
        "void main(void)\n"
        "{\n"
        "    gl_FragColor = texture2D(texture, texc);\n"
        "}\n";
    fshader->compileSourceCode(fsrc);

    shaderProgram = std::unique_ptr<QOpenGLShaderProgram>(new QOpenGLShaderProgram(this));
    shaderProgram->addShader(vshader);
    shaderProgram->addShader(fshader);
    // assign locations of vertex and texture coordinates
    shaderProgram->bindAttributeLocation("vertex", PROGRAM_VERTEX_ATTRIBUTE);
    shaderProgram->bindAttributeLocation("texCoord", PROGRAM_TEXCOORD_ATTRIBUTE);
    shaderProgram->link();
    shaderProgram->bind();
    shaderProgram->setUniformValue("texture", 0);
}

void OpenGLImage::setupDefaultTransform() {
    cameraPos = QVector3D(DEFAULT_CAMERA_POS_X, DEFAULT_CAMERA_POS_Y, DEFAULT_CAMERA_POS_Z);
    imagePos = QVector3D();
    imageAngle = QVector3D();
}

void OpenGLImage::loadImage(QString& path) {
    QImage* p = new QImage(QImage(path).mirrored());
    image = std::unique_ptr<QImage>(p);
    isTextureSync = false;
    norm_h = (float)((float)image->height()/(float)image->width());
    int h = (int)((float)viewSize.width()*norm_h);
    viewSize = QSize(viewSize.width(), h);
    resize(viewSize);
    setupDefaultTransform();
}

QMatrix4x4 OpenGLImage::getViewMatrix() const {
    QVector3D up(0.0f, 1.0f, 0.0f);
    QMatrix4x4 ret;
    ret.translate(cameraPos);
    QVector3D center(cameraPos.x(), cameraPos.y(), imagePos.z());
    ret.lookAt(QVector3D(), center, up);
    return ret;
}

QMatrix4x4 OpenGLImage::getModelMatrix() const {
    QMatrix4x4 ret;
    ret.translate(imagePos);
    return ret;
}

void OpenGLImage::mousePressEvent(QMouseEvent *event) {
    lastClickPos = event->localPos();
    qDebug() << lastClickPos;
}

// movement is weird somehow...
void OpenGLImage::mouseMoveEvent(QMouseEvent *event) {
    if (isRotMode) {
        rotateImage(event->localPos());
    } else {
        moveImage(event->localPos());
    }
    lastClickPos = event->pos();
    event->accept();
    update();
}

void OpenGLImage::moveImage(const QPointF &cursorPos) {
    QPointF delta = cursorPos-lastClickPos;
    float factor = qAbs(imagePos.z()-cameraPos.z()) / focalLength;
    factor /= (qMax(viewSize.width(), viewSize.height()));
    factor *= 3.5f;
    qDebug() << "dx=" << delta.x();
    qDebug() << "dy=" << delta.y();
    qDebug() << "L=" << (imagePos.z()-cameraPos.z());
    qDebug() << "focalLength=" << focalLength;
    qDebug() << "factor" << factor;
    delta *= factor;
    imagePos += QVector3D(delta.x(), -1.0f*delta.y(), 0.0f);
}

void OpenGLImage::rotateImage(const QPointF &cursorPos) {
    QPointF delta = cursorPos-lastClickPos;
    delta.setX(delta.x() / (qreal)viewSize.width());
    delta.setX(delta.x() * 180.0f);
    delta.setY(delta.y() / (qreal)viewSize.height());
    delta.setY(delta.y() * -180.0f);
    qDebug() << delta;
    imageAngle += QVector3D(delta.x(), delta.y(), 0.0f);
}

void OpenGLImage::mouseReleaseEvent(QMouseEvent *event) {
}

void OpenGLImage::keyPressEvent(QKeyEvent *event) {
    if (event->key() == Qt::Key_Control) {
        qDebug() << "ctrl is pressed";
        isRotMode = true;
    } else {
        // call base class method as event is not handled.
        QOpenGLWidget::keyPressEvent(event);
    }
}

void OpenGLImage::keyReleaseEvent(QKeyEvent *event) {
    if (event->key() == Qt::Key_Control) {
        qDebug() << "ctrl is released";
        isRotMode = false;
    } else {
        // call base class method as event is not handled.
        QOpenGLWidget::keyReleaseEvent(event);
    }
}

如下图所示,不同方案的效果略有不同,但所有方案都会出现缩小后图片变模糊的问题:
在这里插入图片描述

问题所在

最终在网友们的帮助下,发现了问题所在:这些实现方法在修改图片大小时都会对图片进行压缩。

比如void QPainter::drawPixmap(const QRectF &target, const QPixmap &pixmap, const QRectF &source),在指定矩形区域内绘制图片,如果指定的矩形区域比图片本身尺寸小,绘制过程中就会对图片进行压缩,导致图片变得模糊。

如果想要将图片变小的同时,保持图片的清晰度,应该直接使用QPixmapscaled函数:

p.drawPixmap(0,0,pixmap.scaled(pixmap.size() * scale, Qt::KeepAspectRatio, Qt::SmoothTransformation));

效果如下,左边是新的实现方法的效果,右边是Windows自带的图片查看软件的效果:
在这里插入图片描述

其实我一开始的实现方法不算错,甚至是官方建议的,在QPixmap的文档中提到:

In some cases it can be more beneficial to draw the pixmap to a painter with a scale set rather than scaling the pixmap. This is the case when the painter is for instance based on OpenGL or when the scale factor changes rapidly.

图片查看器其实就会频繁改变scale按照建议就是应该采用修改QPainter的scale的方法,但这种方法确实会导致图片清晰度变低,出现模糊的问题。

深究

本以为问题已经被完全解决了,直到换了一个高DPI的屏幕,又出现了新的问题。
这次没有出现锯齿问题,而是出现了模糊的问题,和Windows自带图片浏览器中的图片相比不够清晰。

关于在High DPI屏幕上的绘制问题,Qt官方文档其实提到过:

  • High DPI

    This is for example the case when drawing a QPixmap of 64x64 pixels size with a device pixel ratio of 2 onto a high DPI screen which also has a device pixel ratio of 2. Note that the pixmap is then effectively 32x32 pixels in user space.

  • Drawing High Resolution Versions of Pixmaps and Images

    High resolution versions of pixmaps have a device pixel ratio value larger than 1 (see QImageReader, QPixmap::devicePixelRatio()). Should it match the value of the underlying QPaintDevice, it is drawn directly onto the device with no additional transformation applied.

    This is for example the case when drawing a QPixmap of 64x64 pixels size with a device pixel ratio of 2 onto a high DPI screen which also has a device pixel ratio of 2. Note that the pixmap is then effectively 32x32 pixels in user space.

个人理解官方的意思是Qt会自适应高DPI的屏幕,不应该出现图片模糊的情况,没能找出具体问题出在哪里。

看到开源项目chatterino2也遇到过类似的[问题](https://github.com/Chatterino/chatterino2/issues/4552),他们的临时解决方案是在main.cpp中添加下面代码:

#if defined(Q_OS_WINDOWS) && QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
    qputenv("QT_ENABLE_HIGHDPI_SCALING", "0");
#endif

但这个方案官方文档中提到:

This variable is intended for testing purposes only, and we do not recommend setting it on a permanent basis.

所以这种方案有可能有副作用chatterino2后续的解决方案是调用Windows的一些原生接口,作为测试应用暂时就没必要考虑这么深了,暂时就用这个方案解决了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值