3

I have to render a huge image (e.g. 30.000 x 30.000 pixels) in a Qt based app.

I can do this via OpenGL, but I load it as a single texture. Therefore, I am limited by my graphics card max texture size (in this case, 16.368 pixels). I need to implement something like tiling or similar, while maintaining a good rendering performance.

Is there any example to accomplish this, preferably with good Qt integration? (not necessarily OpenGL). Or is there any other starting point?

Thank you

manatttta
  • 3,054
  • 4
  • 34
  • 72
  • You could always stitch it by hand ;) – vitaly-t Apr 01 '16 at 14:34
  • @vitaly-t I would also like out of the box tiled rendering, if there is such a library – manatttta Apr 01 '16 at 14:41
  • please note that questions asking for libraries are [off-topic](http://stackoverflow.com/help/on-topic) – BeyelerStudios Apr 01 '16 at 14:48
  • **What is the source of your image?** How do you know that your rendering is suffering? Do you have an example? If you need to scale it down, do it in a worker thread via QtConcurrent. Other than that, the blit of a small section of a huge image is very fast, e.g. showing a fragment of that large image in a widget, at 1:1 scale, will be very fast. Have you actually tried this and determined that you have a problem, or are you assuming problems without knowing that they'll be there? – Kuba hasn't forgotten Monica Apr 01 '16 at 15:08
  • @KubaOber I tried this using OpenGL and it simply crashes the app – manatttta Apr 01 '16 at 15:12
  • Don't try it using OpenGL, then. Again, **what is the source of your image**? How do you know that rendering it will be slow? Have you tried simply painting a slice of the huge `QImage` on a widget? It's fast! – Kuba hasn't forgotten Monica Apr 01 '16 at 15:13
  • Is the texture rendered 3D or 2D? – Andreas Apr 02 '16 at 09:49

1 Answers1

5

You can do it with the QOpenGLxxx classes. Here a complete functionnal example, with very good perfs and using modern OpenGL techniques. Each tile is an independant texture. In this example I use the same image for all the textures, but in your case you can create a slice of your original image and use it as the tile texture.

QOpenGLWidget is used as a base class for displaying the tiles. QOpenGLBuffer to manage the openGL vertex buffer. QOpenGLShaderProgram to manage the shaders.

tilewidget.cpp:

#include "tiledwidget.h"

#include <QOpenGLFunctions>
#include <QPainter>
#include <QOpenGLTexture>
#include <QTransform>
#include <QOpenGLBuffer>
#include <QVector2D>

// a small class to manage vertices in the vertex buffer
class Vertex2D
{
public:
    Vertex2D(){}
    Vertex2D(const QPointF &p, const QPointF &c) :
        position(p)
      , coords(c)
    {
    }
    QVector2D position; // position of the vertex
    QVector2D coords; // texture coordinates of the vertex
};


TiledWidget::TiledWidget(QWidget *parent) :
    QOpenGLWidget(parent)
  , m_rows(5)
  , m_cols(5)
  , m_vertexBuffer(new QOpenGLBuffer)
  , m_program(new QOpenGLShaderProgram(this))
{
}
TiledWidget::~TiledWidget()
{
    qDeleteAll(m_tiles);
    delete m_vertexBuffer;
    delete m_program;
}
void TiledWidget::initializeGL()
{
    // tiles creation based on a 256x256 image
    QImage image(":/lenna.png");
    if (image.format() != QImage::Format_ARGB32_Premultiplied)
        image = image.convertToFormat(QImage::Format_ARGB32_Premultiplied);

    for (int row = 0; row < m_rows; row++)
    {
        for (int col = 0; col < m_cols; col++)
        {
            QOpenGLTexture* tile = new QOpenGLTexture(QOpenGLTexture::Target2D);
            if (!tile)
            {
                qDebug() << "Ooops!";
                break;
            }
            if (!tile->create())
            {
                qDebug() << "Oooops again!";
                break;
            }

            tile->setSize(256, 256);
            tile->setFormat(QOpenGLTexture::RGBA8_UNorm);
            // you can manage the number of mimap you desire...
            // by default 256x256 => 9 mipmap levels will be allocated:
            // 256, 128, 64, 32, 16, 8, 4, 2 and 1px
            // to modify this use tile->setMipLevels(n);
            tile->setMinificationFilter(QOpenGLTexture::Nearest);
            tile->setMagnificationFilter(QOpenGLTexture::Nearest);
            tile->setData(image, QOpenGLTexture::GenerateMipMaps);
            m_tiles << tile;
        }
    }
    // vertex buffer initialisation
    if (!m_vertexBuffer->create())
    {
        qDebug() << "Ooops!";
        return;
    }
    m_vertexBuffer->setUsagePattern(QOpenGLBuffer::DynamicDraw);
    m_vertexBuffer->bind();
    // room for 2 triangles of 3 vertices
    m_vertexBuffer->allocate(2 * 3 * sizeof(Vertex2D));
    m_vertexBuffer->release();

    // shader program initialisation
    if (!m_program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/basic_vert.glsl"))
    {
        qDebug() << "Ooops!";
        return;
    }
    if (!m_program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/basic_frag.glsl"))
    {
        qDebug() << "Ooops!";
        return;
    }
    if (!m_program->link())
    {
        qDebug() << "Ooops!";
        return;
    }

    // ok, we are still alive at this point...
}
// this slot is called at windows close before the widget is destroyed
// use this to cleanup opengl
void TiledWidget::shutDown()
{
    // don't forget makeCurrent, OpenGL is a state machine!
    makeCurrent();
    foreach(QOpenGLTexture* tile, m_tiles)
    {
        if (tile->isCreated())
            tile->destroy();
    }
    if (m_vertexBuffer)
        m_vertexBuffer->destroy();
}
void TiledWidget::resizeGL(int width, int height)
{
    Q_UNUSED(width);
    Q_UNUSED(height);
    // ...
}
// you can alternatively override QOpenGLWidget::paintGL if you don't need
// to draw things with classic QPainter
void TiledWidget::paintEvent(QPaintEvent *event)
{
    Q_UNUSED(event)

    QPainter painter(this);
    // native draw
    painter.beginNativePainting();
    drawGL();
    painter.endNativePainting();

    // draw overlays if needed
    // ...draw something with painter...
}
void TiledWidget::drawGL()
{
    // always a good thing to make current
    makeCurrent();
    // enable texturing
    context()->functions()->glEnable(GL_TEXTURE_2D);
    // enable blending
    context()->functions()->glEnable(GL_BLEND);
    // blending equation (remember OpenGL textures are premultiplied)
    context()->functions()->glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
    // clear
    context()->functions()->glClearColor(0.8, 0.8, 0.8, 1);
    context()->functions()->glClear(GL_COLOR_BUFFER_BIT);
    context()->functions()->glClear(GL_DEPTH_BUFFER_BIT);

    // viewport and matrices setup for a 2D tile system
    context()->functions()->glViewport(0, 0, width(), height());
    QMatrix4x4 projectionMatrix;
    projectionMatrix.setToIdentity();
    projectionMatrix.ortho(0, width(), height(), 0, -1, 1);
    QMatrix4x4 viewProjectionMatrix;
    // use a QTransform to scale, translate, rotate your view
    viewProjectionMatrix = projectionMatrix * QMatrix4x4(m_transform);

    // program setup
    m_program->bind();
    // a good practice if you have to manage multiple shared context
    // with shared resources: the link is context dependant.
    if (!m_program->isLinked())
        m_program->link();

    // binding the buffer
    m_vertexBuffer->bind();

    // setup of the program attributes
    int pos = 0, count;
    // positions : 2 floats
    count = 2;
    m_program->enableAttributeArray("vertexPosition");
    m_program->setAttributeBuffer("vertexPosition", GL_FLOAT, pos, count, sizeof(Vertex2D));
    pos += count * sizeof(float);

    // texture coordinates : 2 floats
    count = 2;
    m_program->enableAttributeArray("textureCoordinates");
    m_program->setAttributeBuffer("textureCoordinates", GL_FLOAT, pos, count, sizeof(Vertex2D));
    pos += count * sizeof(float);

    m_program->setUniformValue("viewProjectionMatrix", viewProjectionMatrix);
    m_program->setUniformValue("f_opacity", (float) 0.5);


    // draw each tile
    for (int row = 0; row < m_rows; row++)
    {
        for (int col = 0; col < m_cols; col++)
        {
            QRect rect = tileRect(row, col);
            // write vertices in the buffer
            // note : better perf if you precreate this buffer

            Vertex2D v0;
            v0.position = QVector2D(rect.bottomLeft());
            v0.coords = QVector2D(0, 1);

            Vertex2D v1;
            v1.position = QVector2D(rect.topLeft());
            v1.coords = QVector2D(0, 0);

            Vertex2D v2;
            v2.position = QVector2D(rect.bottomRight());
            v2.coords = QVector2D(1, 1);

            Vertex2D v3;
            v3.position = QVector2D(rect.topRight());
            v3.coords = QVector2D(1, 0);

            int vCount = 0;
            // first triangle v0, v1, v2
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v0, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v1, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v2, sizeof(Vertex2D)); vCount++;

            // second triangle v1, v3, v2
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v1, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v3, sizeof(Vertex2D)); vCount++;
            m_vertexBuffer->write(vCount * sizeof(Vertex2D), &v2, sizeof(Vertex2D)); vCount++;

            // bind the tile texture on texture unit 0
            // you can add other textures binding them in texture units 1, 2...
            QOpenGLTexture* tile = m_tiles.at(tileIndex(row, col));
            // activate texture unit 0
            context()->functions()->glActiveTexture(GL_TEXTURE0);
            // setup texture options here if needed...
            // set sampler2D on texture unit 0
            m_program->setUniformValue("f_tileTexture", 0);
            // bind texture
            tile->bind();
            // draw 2 triangles = 6 vertices starting at offset 0 in the buffer
            context()->functions()->glDrawArrays(GL_TRIANGLES, 0, 6);
            // release texture
            tile->release();
        }
    }
    m_vertexBuffer->release();
    m_program->release();
}
// compute the tile index
int TiledWidget::tileIndex(int row, int col)
{
    return row * m_cols + col;
}

// compute the tile rectangle given a row and a col.
// Note : You will have to manage the opengl texture border effect
// to get correct results. To do this you must overlap textures when you draw them.
QRect TiledWidget::tileRect(int row, int col)
{
    int x = row * 256;
    int y = col * 256;
    return QRect(x, y, 256, 256);
}

tilewidget.h:

#ifndef TILEDWIDGET_H
#define TILEDWIDGET_H

#include <QOpenGLWidget>

#include <QOpenGLFramebufferObjectFormat>
#include <QOpenGLShaderProgram>

#include <QTransform>

class QOpenGLTexture;
class QOpenGLBuffer;
class QOpenGLShaderProgram;

class TiledWidget : public QOpenGLWidget
{
public:
    TiledWidget(QWidget *parent = 0);
    ~TiledWidget();
public slots:
    void shutDown();
private:
    QTransform m_transform;
    int m_rows;
    int m_cols;
    QVector<QOpenGLTexture*> m_tiles;
    QOpenGLBuffer *m_vertexBuffer;
    QOpenGLShaderProgram* m_program;

    void resizeGL(int width, int height);
    void initializeGL();
    void paintEvent(QPaintEvent *event) Q_DECL_OVERRIDE;
    void drawGL();
    int tileIndex(int row, int col);
    QRect tileRect(int row, int col);
};

#endif // TILEDWIDGET_H

mainwindow.cpp:

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "tiledwidget.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    m_tiledWidget = new TiledWidget(this);
    setCentralWidget(m_tiledWidget);
}
MainWindow::~MainWindow()
{
    delete ui;
}
void MainWindow::closeEvent(QCloseEvent *event)
{
    Q_UNUSED(event);
    // destroy textures before widget desctruction
    m_tiledWidget->shutDown();
}

and the shaders:

// vertex shader    
#version 330 core

in vec2 vertexPosition;
in vec2 textureCoordinates;

uniform mat4 viewProjectionMatrix;

out vec2 v_textureCoordinates;

void main()
{
    v_textureCoordinates = vec2(textureCoordinates);
    gl_Position = viewProjectionMatrix * vec4(vertexPosition, 0.0, 1.0);
}

// fragment shader
#version 330 core

// vertices datas
in vec2 v_textureCoordinates;

// uniforms
uniform sampler2D f_tileTexture; // tile texture
uniform float f_opacity = 1; // tile opacity

out vec4 f_fragColor; // shader output color

void main()
{
    // get the fragment color from the tile texture
    vec4 color = texture(f_tileTexture, v_textureCoordinates.st);
    // premultiplied output color
    f_fragColor = vec4(color * f_opacity);
}

You get this result:

enter image description here

Bertrand
  • 279
  • 2
  • 6
  • What's the point at using OpenGL if you still need to split the gigantic image in tiles? – peppe Apr 02 '16 at 14:18
  • @pepe: to get good perfs at display. Do you have a running example of displaying this kind of image with QPainter and QImage with raisonnable perfs ? I would be very interested by it. I don't think it is a hard task to slice a gigantic image into smaller ones. You split once at initialisation and get the perfs benefit of using accelerated techniques at display. Am I wrong or not ? I have been confronted to the exact same problem and I turned to OpenGL to get a good result. – Bertrand Apr 02 '16 at 14:34
  • The whole problem is getting the tiles in memory (and managing them efficiently). How do you solve that? OP has not clarified this point. Once you've done so, QPainter is not that slow. – peppe Apr 02 '16 at 19:28
  • @peppe If you want to zoom out the view the number of tiles to draw increase drasticly and QImage/QPainter quickly show their limits. You have to deal with mipmaps (zoom out) and manage the tiles visibily (zoom in). In the both cases you have to deal with linear interpolation to get a correct visual result. OpenGL is a lot more efficent than QImage: these functionalities are native. Doing a mipmap system with QImage is like reinventing the wheel. For the memory bandwidth of graphic cards you have to use a tile cache on disk and updload/offload the cache in textures on demand. Not easy to do! – Bertrand Apr 03 '16 at 07:45
  • I know that, but OP so far did not talk about zooming, didn't talk about the source of the image, didn't talk about what's the problem in the first place (QImage unable to load it because it's too big? Out of memory? Strange format?)... – peppe Apr 03 '16 at 09:34
  • @pepe As a test is better than a talk, I tried to display an ARGB32 30000x30000 pixels QImage on a QOpenGLWidget in paintEvent with a QPainter::drawImage(0, 0, image), no transformation, no antialias: it failed. On my GeForce GTX 460 card textures have the same limitation of 16000x16000 pixels as OP said. I also tried to display it with a simple QWidget: failed. So, a tile system seems mandatory. Any idea? – Bertrand Apr 03 '16 at 16:02
  • Load it in tiles (and I'm not sure if QImage can help you *loading* sub-tiles from a big image on disk; surely it can hold one tile once loaded -- but you don't need then QImage, you want QPixmap) and paint the tiles using `QPainter::drawPixmap`. What's wrong with that? – peppe Apr 03 '16 at 17:21
  • @pepe As you suggested, I tried with a QPixmap. I get the same result. Looking closer it's the image or pixmap allocation who seems to be problematic. It returns a null image/pixmap when image size is over 16000 pixels. I don't know if it is a bug or a physical limitation. QPixmap is accelerated so I think that the graphic card limitation should be the problem. But in the case of a QImage I don't understand: QImage is a memory raster device. To be sure that there is no stack effect in memory allocation, I used a pointer. Is it a question for Qt support ? – Bertrand Apr 04 '16 at 07:24
  • It is a limitation of QImage / QPixmap. You can't load images that big in *one* tile, no matter what. Now: can you load it in multiple tiles? Each small tile can then be managed by QImage / QPixmap, and you can draw them using QPainter. – peppe Apr 04 '16 at 07:30
  • there is this thread on the QPixmap and QImage size limitations: [link](http://stackoverflow.com/questions/7080052/qimage-qpixmap-size-limitations) – Bertrand Apr 04 '16 at 07:34
  • @peppe To tile a big image you can use ImageMagick for example. In my personnal case I do it myself: I use a raw format (directly in ARGB 32 premultiplied to have no convertion to do and directly load datas in textures) and read/write chunks of image to updload and download if your app update the image. It's rather complex to do but work well. Doing like that you can parametrize the size of tiles and adjust it depending on the capacity of the graphic card. As I said OpenGL provide the advantage to have mipmaps and thus to display a large amount of tiles with very good perfs. – Bertrand Apr 04 '16 at 07:49