背景
实现一个图片浏览器,可以支持放大/缩小查看图片。主要组件如下:
// 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)
,在指定矩形区域内绘制图片,如果指定的矩形区域比图片本身尺寸小,绘制过程中就会对图片进行压缩,导致图片变得模糊。
如果想要将图片变小的同时,保持图片的清晰度,应该直接使用QPixmap
的scaled函数:
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
的方法,但这种方法确实会导致图片清晰度变低,出现模糊的问题。