How to properly use QOpenGLBuffer.PixelPackBuffer with PyQt5

82 views Asked by At

I am trying to read the color buffer content of the default framebuffer in PyQt5 using pixel buffer object given by the Qt OpenGL framework.

It looks like the reading is unsuccessful because the end image always contains all zeros. There's very little examples with pixel buffers and PyQt5, so I was mostly relying on this c++ tutorial explaining pixel buffers, specifically section Example: Asynchronous Read-back.

My code goes something like this:

class GLCanvas(QtWidgets.QOpenGLWidget):
      # ...


      def screenDump(self):
            """
            Takes a screenshot and returns a pixmap.

            :returns:   A pixmap with the rendered content.
            :rtype:     QPixmap
            """
            self.makeCurrent()

            w = self.size().width()
            h = self.size().height()

            ppo = QtGui.QOpenGLBuffer(QtGui.QOpenGLBuffer.PixelPackBuffer)
            ppo.setUsagePattern(QOpenGLBuffer.StaticRead)
            ppo.create()
            success = ppo.bind()
            if success:
                  ppo.allocate(w * h * 4)

                  # Render the stuff
                  # ...

                  # Read the color buffer.
                  glReadBuffer(GL_FRONT)
                  glReadPixels(0, 0, w, h, GL_RGBA, GL_UNSIGNED_BYTE, 0)

                  # TRY1: Create an image with pixel buffer data - Doesn't work, image contains all zeros.
                  pixel_buffer_mapped = ppo.map(QOpenGLBuffer.ReadOnly)
                  image = QtGui.QImage(sip.voidptr(pixel_buffer_mapped), w, h, QtGui.QImage.Format_ARGB32)
                  ppo.unmap()

                  # TRY2: Create an image with pixel buffer data - Doesn't work, image contains all zeros.
                  # image = QtGui.QImage(w, h, QtGui.QImage.Format_ARGB32)
                  # bits = image.constBits()
                  # ppo.read(0, bits, w * h * 4)

                  ppo.release()


            pixmap = QtGui.QPixmap.fromImage(image)

            return pixmap

Any help would be greatly appreciated.

1

There are 1 answers

0
MarsaPalas On

I didn't have any success after a couple of days, so I decided to implement color buffer fetching with pixel buffer object in C++, and then use SWIG to pass the data to Python.

I'm posting relevant code, maybe it will help somebody.

CPP side

// renderer.cpp
class Renderer{
    // ...

    void resize(int width, int height) {
        // Set the viewport
        glViewport(0, 0, width, height);


        // Store width and height
        width_ = width;
        height_ = height;

        // ...
    }

    // -------------------------------------------------------------------------- //
    // Returns the color buffer data in GL_RGBA format.
    GLubyte* screenDumpCpp(){

        // Check if pixel buffer objects are available.
        if (!GLInfo::pixelBufferSupported()){
            return 0;
        }

        // Get the color buffer size in bytes.
        int channels = 4;
        int data_size = width_ * height_ * channels;
        GLuint pbo_id;

        // Generate pixel buffer for reading.
        glGenBuffers(1, &pbo_id);
        glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo_id);
        glBufferData(GL_PIXEL_PACK_BUFFER, data_size, 0, GL_STREAM_READ);

        // Set the framebuffer to read from.
        glReadBuffer(GL_FRONT);

        // Read the framebuffer and store data in the pixel buffer.
        glReadPixels(0, 0, width_, height_, GL_RGBA, GL_UNSIGNED_BYTE, 0);

        // Map the pixel buffer.
        GLubyte* pixel_buffer = (GLubyte*)glMapBuffer(GL_PIXEL_PACK_BUFFER, GL_READ_ONLY);

        // Cleanup.
        glUnmapBuffer(GL_PIXEL_PACK_BUFFER);  
        glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        glDeleteBuffers(1, &pbo_id);

        return pixel_buffer; 
    }

    // Returns the color buffer data in RGBA format as a numpy array.
    PyObject* screenDump(){

        // Get screen dump.
        GLubyte* cpp_image = screenDumpCpp();
        int channels = 4;
        int image_size = width_* height_ * channels;

        // Setup dimensions for numpy vector.
        PyObject * python_image = NULL;
        int ndim = 1;
        npy_intp dims[1] = {image_size};

        // Set up numpy vector.
        python_image = PyArray_SimpleNew(ndim, dims, NPY_UINT8);
        GLubyte * data = static_cast<GLubyte *>(PyArray_DATA(toPyArrayObject(python_image)));

        // Copy screen dump to python space.
        memcpy(data, cpp_image, image_size);

        // return screen dump to python.
        return python_image;
    }
};


// glinfo.cpp
const GLInt GLInfo::glVersionInt(){ ... }
GLV GLInt::GLV(int major, int minor){ ... }
bool GLInfo::pixelBufferSupported(){
    const GLint version = GLInfo::glVersionInt();
    bool supported = false;

    if (version >= GLInfo::GLV(1, 5) && version < GLInfo::GLV(3, 0)){
        supported = true;
    }
    else if (version >= GLInfo::GLV(3, 0)){
        GLint extensions_number;
        glGetIntegerv(GL_NUM_EXTENSIONS, &extensions_number);
        std::string pixel_buffer_extension("GL_ARB_pixel_buffer_object");
        while (extensions_number--) {
            const auto extension_name = reinterpret_cast<const char *>(glGetStringi(GL_EXTENSIONS, extensions_number));
            std::string extension_name_str(extension_name);
            if (pixel_buffer_extension == extension_name) {
                supported = true;
                break;
            }
        }
    }
    return supported;
}

Python side

# ...

class MyCanvas(QOpenGLWidget):
    
    def __init__(self):
        # Get renderer from c++
        self._renderer = Renderer()

    def resizeGL(self, width, height):
        self._renderer.resize(width, height)

    # ...

if __name__ == '__main__':
    # ...
    canvas = MyCanvas()
    canvas.show()

    width = canvas.width()
    height = canvas.height()
    data = canvas._renderer().screenDump()

    image = QtGui.QImage(data.data, width, height, QtGui.QImage.Format_RGBA8888)
    new_image = image.mirrored()

    pixmap = QtGui.QPixmap.fromImage(new_image)
    pixmap.save(path) 

    sys.exit(app.exec_())