如何将片段着色器图像更改保存到图像输出文件?

How to save fragment shader image changes to image output file?

简介

我有一个用 C++ 和 Lua 编码的游戏项目(我知道主题中写着 'Python',不用担心),并且,我在其中应用了一个片段(不是顶点!)着色器进入游戏屏幕。由于我的项目的一部分不是我制作的(因为它是开源的),使用 OpenGL 应用片段着色器的系统已经由其他人制作,但不是 2xSal 片段着色器本身。我认为自己是这门学科的初学者。我"made"唯一的一段代码是片段着色器(我不知道是否有必要,但我的代码是基于以下link:https://github.com/libretro/glsl-shaders/blob/master/xsal/shaders/2xsal-level2-pass2.glsl的代码)。

这是我为使 2xSal 片段着色器工作所做的更改(在我的游戏中):

#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D

#ifdef GL_ES
    #ifdef GL_FRAGMENT_PRECISION_HIGH
        precision highp float;
    #else
        precision mediump float;
    #endif
    #define COMPAT_PRECISION mediump
#else
    #define COMPAT_PRECISION
#endif

// uniform COMPAT_PRECISION int FrameDirection; // Not in use
// uniform COMPAT_PRECISION int FrameCount; // Not in use
// uniform COMPAT_PRECISION vec2 OutputSize; // Not in use
// uniform COMPAT_PRECISION vec2 TextureSize;
// uniform COMPAT_PRECISION vec2 InputSize;
uniform sampler2D Texture;
//COMPAT_VARYING vec4 TEX0;
// in variables go here as COMPAT_VARYING whatever

// fragment compatibility #defines
#define Source Texture
//#define vTexCoord TEX0.xy

COMPAT_VARYING vec2 v_TexCoord;

#define InputSize vec2(800.0, 608.0) // Width and height in pixels of game screen
#define SourceSize vec4(InputSize, 1.0 / InputSize) //either TextureSize or InputSize
// #define outsize vec4(OutputSize, 1.0 / OutputSize) // Not in use

void main()
{
    vec2 tex = v_TexCoord;
    //vec2 texsize = IN.texture_size;
    float dx = 0.25*SourceSize.z;
    float dy = 0.25*SourceSize.w;
    vec3  dt = vec3(1.0, 1.0, 1.0);

    vec4 yx = vec4(dx, dy, -dx, -dy);
    vec4 xh = yx*vec4(3.0, 1.0, 3.0, 1.0);
    vec4 yv = yx*vec4(1.0, 3.0, 1.0, 3.0);

    vec3 c11 = COMPAT_TEXTURE(Source, tex        ).xyz;
    vec3 s00 = COMPAT_TEXTURE(Source, tex + yx.zw).xyz;
    vec3 s20 = COMPAT_TEXTURE(Source, tex + yx.xw).xyz;
    vec3 s22 = COMPAT_TEXTURE(Source, tex + yx.xy).xyz;
    vec3 s02 = COMPAT_TEXTURE(Source, tex + yx.zy).xyz;
    vec3 h00 = COMPAT_TEXTURE(Source, tex + xh.zw).xyz;
    vec3 h20 = COMPAT_TEXTURE(Source, tex + xh.xw).xyz;
    vec3 h22 = COMPAT_TEXTURE(Source, tex + xh.xy).xyz;
    vec3 h02 = COMPAT_TEXTURE(Source, tex + xh.zy).xyz;
    vec3 v00 = COMPAT_TEXTURE(Source, tex + yv.zw).xyz;
    vec3 v20 = COMPAT_TEXTURE(Source, tex + yv.xw).xyz;
    vec3 v22 = COMPAT_TEXTURE(Source, tex + yv.xy).xyz;
    vec3 v02 = COMPAT_TEXTURE(Source, tex + yv.zy).xyz;

    float m1 = 1.0/(dot(abs(s00 - s22), dt) + 0.00001);
    float m2 = 1.0/(dot(abs(s02 - s20), dt) + 0.00001);
    float h1 = 1.0/(dot(abs(s00 - h22), dt) + 0.00001);
    float h2 = 1.0/(dot(abs(s02 - h20), dt) + 0.00001);
    float h3 = 1.0/(dot(abs(h00 - s22), dt) + 0.00001);
    float h4 = 1.0/(dot(abs(h02 - s20), dt) + 0.00001);
    float v1 = 1.0/(dot(abs(s00 - v22), dt) + 0.00001);
    float v2 = 1.0/(dot(abs(s02 - v20), dt) + 0.00001);
    float v3 = 1.0/(dot(abs(v00 - s22), dt) + 0.00001);
    float v4 = 1.0/(dot(abs(v02 - s20), dt) + 0.00001);

    vec3 t1 = 0.5*(m1*(s00 + s22) + m2*(s02 + s20))/(m1 + m2);
    vec3 t2 = 0.5*(h1*(s00 + h22) + h2*(s02 + h20) + h3*(h00 + s22) + h4*(h02 + s20))/(h1 + h2 + h3 + h4);
    vec3 t3 = 0.5*(v1*(s00 + v22) + v2*(s02 + v20) + v3*(v00 + s22) + v4*(v02 + s20))/(v1 + v2 + v3 + v4);

    float k1 = 1.0/(dot(abs(t1 - c11), dt) + 0.00001);
    float k2 = 1.0/(dot(abs(t2 - c11), dt) + 0.00001);
    float k3 = 1.0/(dot(abs(t3 - c11), dt) + 0.00001);

    FragColor = vec4((k1*t1 + k2*t2 + k3*t3)/(k1 + k2 + k3), 1.0);
}

问题

也就是说,我现在需要的与我的游戏无关,而是我给你看的代码本身。 我需要将这个片段着色器应用于给定路径的输入图像,并将输出保存到另一个(和新的)图像文件,使用 Python,但我不想在里面显示它们a window(比如使用 GLFW 或 GLUT).

想法太简单了,想不出来是什么,怎么做。 我只需要保存输入图像中片段变化的输出结果

我不需要顶点着色器。我不需要 window 来显示结果,我根本不需要 window。我只想将结果保存到图像文件中。

我试过的

我在 Python 中找到了一个代码,其中显示输入到 GLFW window 的图像。我以为,一点一点,我会到达我想要的地方,但我找不到如何:

  1. 应用我向您展示的 2xSal 片段着色器(将片段着色器分离到 .glsl 文件会很棒);
  2. 删除 GLFW window;
  3. 删除顶点着色器(如果可能的话),因为我不需要对顶点进行任何更改;
  4. 将结果保存到输出文件。

此外,我注意到 Python 代码存在拉伸或收缩图像的问题,因为顶点构成正方形并绘制输入图像纹理在里面。因此,对于实际代码,我认为它只适用于正方形图像。

Python实际代码

# # Requirements # #
# Execute these commands on terminal:
# pip install glfw
# pip install pyopengl
# pip install pyrr
# pip install pillow

import glfw
from OpenGL.GL import *
import OpenGL.GL.shaders
import numpy
from PIL import Image


def main():

    # initialize glfw
    if not glfw.init():
        return

    window = glfw.create_window(800, 600, "My OpenGL window", None, None)

    if not window:
        glfw.terminate()
        return

    glfw.make_context_current(window)
    #           positions        colors          texture coords
    quad = [   -0.5, -0.5, 0.0,  1.0, 0.0, 0.0,  0.0, 0.0,
                0.5, -0.5, 0.0,  0.0, 1.0, 0.0,  1.0, 0.0,
                0.5,  0.5, 0.0,  0.0, 0.0, 1.0,  1.0, 1.0,
               -0.5,  0.5, 0.0,  1.0, 1.0, 1.0,  0.0, 1.0]

    quad = numpy.array(quad, dtype = numpy.float32)

    indices = [0, 1, 2,
               2, 3, 0]

    indices = numpy.array(indices, dtype= numpy.uint32)

    print(quad.itemsize * len(quad))
    print(indices.itemsize * len(indices))
    print(quad.itemsize * 8)

    vertex_shader = """
    #version 330
    in layout(location = 0) vec3 position;
    in layout(location = 1) vec3 color;
    in layout(location = 2) vec2 inTexCoords;
    out vec3 newColor;
    out vec2 outTexCoords;
    void main()
    {
        gl_Position = vec4(position, 1.0f);
        newColor = color;
        outTexCoords = inTexCoords;
    }
    """

    fragment_shader = """
    #version 330
    in vec3 newColor;
    in vec2 outTexCoords;
    out vec4 outColor;
    uniform sampler2D samplerTex;
    void main()
    {
        outColor = texture(samplerTex, outTexCoords) * vec4(newColor, 1.0f);
    }
    """
    shader = OpenGL.GL.shaders.compileProgram(OpenGL.GL.shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
                                              OpenGL.GL.shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER))

    VBO = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, VBO)
    glBufferData(GL_ARRAY_BUFFER, quad.itemsize * len(quad), quad, GL_STATIC_DRAW)

    EBO = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.itemsize * len(indices), indices, GL_STATIC_DRAW)

    # position = glGetAttribLocation(shader, "position")
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)

    # color = glGetAttribLocation(shader, "color")
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(12))
    glEnableVertexAttribArray(1)

    # texture_coords = glGetAttribLocation(shader, "inTexCoords")
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(24))
    glEnableVertexAttribArray(2)

    texture = glGenTextures(1)
    glBindTexture(GL_TEXTURE_2D, texture)
    # texture wrapping params
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    # texture filtering params
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    image = Image.open("res/test.jpg")
    # img_data = numpy.array(list(image.getdata()), numpy.uint8)
    flipped_image = image.transpose(Image.FLIP_TOP_BOTTOM)
    img_data = flipped_image.convert("RGBA").tobytes()
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)
    # print(image.width, image.height)

    glUseProgram(shader)

    glClearColor(0.2, 0.3, 0.2, 1.0)

    while not glfw.window_should_close(window):
        glfw.poll_events()

        glClear(GL_COLOR_BUFFER_BIT)

        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)

        glfw.swap_buffers(window)

    glfw.terminate()


if __name__ == "__main__":
    main()

> 由 Rabbid76 解决

感谢@Rabbid76,我能够编写这段代码。

因为我喜欢人们发布结果,所以这是我的最终代码:

# # Requirements # #
# Execute these commands on PyCharm terminal:
# pip install glfw
# pip install pyopengl
# pip install pyrr
# pip install pillow


"""
    The OpenGL specification doesn't allow you to create a context without a window,
    since it needs the pixel format that you set into the device context.
    Actually, it is necessary to have a window handler to create a "traditional" rendering context.
    It is used to fetch OpenGL information and extensions availability.
    Once you got that information, you can destroy the render context and release the "dummy" window.
    So, in this code, the window is created, the context is set to this window,
    the image result is saved to an output image file and, then, this window is released.
"""


import glfw
from OpenGL.GL import *
import OpenGL.GL.shaders
import numpy
from PIL import Image


def main():
    # Initialize glfw
    if not glfw.init():
        return

    # Create window
    window = glfw.create_window(1, 1, "My OpenGL window", None, None)  # Size (1, 1) for show nothing in window
    # window = glfw.create_window(800, 600, "My OpenGL window", None, None)

    # Terminate if any issue
    if not window:
        glfw.terminate()
        return

    # Set context to window
    glfw.make_context_current(window)

    #

    # Initial data
    # Positions, colors, texture coordinates
    '''
    #           positions        colors          texture coords
    quad = [   -0.5, -0.5, 0.0,  1.0, 0.0, 0.0,  0.0, 0.0,
                0.5, -0.5, 0.0,  0.0, 1.0, 0.0,  1.0, 0.0,
                0.5,  0.5, 0.0,  0.0, 0.0, 1.0,  1.0, 1.0,
               -0.5,  0.5, 0.0,  1.0, 1.0, 1.0,  0.0, 1.0]
    '''
    #       positions      colors       texture coords
    quad = [-1., -1., 0.,  1., 0., 0.,  0., 0.,
             1., -1., 0.,  0., 1., 0.,  1., 0.,
             1.,  1., 0.,  0., 0., 1.,  1., 1.,
            -1.,  1., 0.,  1., 1., 1.,  0., 1.]
    quad = numpy.array(quad, dtype=numpy.float32)
    # Vertices indices order
    indices = [0, 1, 2,
               2, 3, 0]
    indices = numpy.array(indices, dtype=numpy.uint32)

    # print(quad.itemsize * len(quad))
    # print(indices.itemsize * len(indices))
    # print(quad.itemsize * 8)

    #

    # Vertex shader
    vertex_shader = """
    #version 330

    in layout(location = 0) vec3 position;

    //in layout(location = 1) vec3 inColor;
    //out vec3 outColor;

    in layout(location = 2) vec2 inTexCoords;
    out vec2 outTexCoords;

    void main()
    {
        gl_Position = vec4(position, 1.0f);

        //outColor = inColor;

        outTexCoords = inTexCoords;
    }
    """

    # Fragment shader
    fragment_shader = """
    #version 330

    //in vec3 outColor;

    out vec4 gl_FragColor;
    uniform sampler2D source;
    in vec2 outTexCoords;

    float intensityFactor = 2.;

    void main()
    {
        ivec2 textureSize2d = textureSize(source, 0); // Width and height of texture image

        vec2 inputSize = vec2(float(textureSize2d.x) / intensityFactor, float(textureSize2d.y) / intensityFactor);
        vec2 sourceSize = 1. / inputSize; // Either TextureSize or InputSize

        float dx = 0.25*sourceSize.x;
        float dy = 0.25*sourceSize.y;
        vec3  dt = vec3(1.0, 1.0, 1.0);

        vec4 yx = vec4(dx, dy, -dx, -dy);
        vec4 xh = yx*vec4(3.0, 1.0, 3.0, 1.0);
        vec4 yv = yx*vec4(1.0, 3.0, 1.0, 3.0);

        vec3 c11 = texture(source, outTexCoords        ).xyz;
        vec3 s00 = texture(source, outTexCoords + yx.zw).xyz;
        vec3 s20 = texture(source, outTexCoords + yx.xw).xyz;
        vec3 s22 = texture(source, outTexCoords + yx.xy).xyz;
        vec3 s02 = texture(source, outTexCoords + yx.zy).xyz;
        vec3 h00 = texture(source, outTexCoords + xh.zw).xyz;
        vec3 h20 = texture(source, outTexCoords + xh.xw).xyz;
        vec3 h22 = texture(source, outTexCoords + xh.xy).xyz;
        vec3 h02 = texture(source, outTexCoords + xh.zy).xyz;
        vec3 v00 = texture(source, outTexCoords + yv.zw).xyz;
        vec3 v20 = texture(source, outTexCoords + yv.xw).xyz;
        vec3 v22 = texture(source, outTexCoords + yv.xy).xyz;
        vec3 v02 = texture(source, outTexCoords + yv.zy).xyz;

        float m1 = 1.0/(dot(abs(s00 - s22), dt) + 0.00001);
        float m2 = 1.0/(dot(abs(s02 - s20), dt) + 0.00001);
        float h1 = 1.0/(dot(abs(s00 - h22), dt) + 0.00001);
        float h2 = 1.0/(dot(abs(s02 - h20), dt) + 0.00001);
        float h3 = 1.0/(dot(abs(h00 - s22), dt) + 0.00001);
        float h4 = 1.0/(dot(abs(h02 - s20), dt) + 0.00001);
        float v1 = 1.0/(dot(abs(s00 - v22), dt) + 0.00001);
        float v2 = 1.0/(dot(abs(s02 - v20), dt) + 0.00001);
        float v3 = 1.0/(dot(abs(v00 - s22), dt) + 0.00001);
        float v4 = 1.0/(dot(abs(v02 - s20), dt) + 0.00001);

        vec3 t1 = 0.5*(m1*(s00 + s22) + m2*(s02 + s20))/(m1 + m2);
        vec3 t2 = 0.5*(h1*(s00 + h22) + h2*(s02 + h20) + h3*(h00 + s22) + h4*(h02 + s20))/(h1 + h2 + h3 + h4);
        vec3 t3 = 0.5*(v1*(s00 + v22) + v2*(s02 + v20) + v3*(v00 + s22) + v4*(v02 + s20))/(v1 + v2 + v3 + v4);

        float k1 = 1.0/(dot(abs(t1 - c11), dt) + 0.00001);
        float k2 = 1.0/(dot(abs(t2 - c11), dt) + 0.00001);
        float k3 = 1.0/(dot(abs(t3 - c11), dt) + 0.00001);

        // gl_FragColor = texture(source, outTexCoords) * vec4(outColor, 1.0f);
        gl_FragColor = vec4((k1*t1 + k2*t2 + k3*t3)/(k1 + k2 + k3), 1.0f);
        //gl_FragColor = vec4(s00, 1.0f);
    }
    """

    #

    # Compile shaders
    shader = OpenGL.GL.shaders.compileProgram(OpenGL.GL.shaders.compileShader(vertex_shader, GL_VERTEX_SHADER),
                                              OpenGL.GL.shaders.compileShader(fragment_shader, GL_FRAGMENT_SHADER))

    # VBO
    v_b_o = glGenBuffers(1)
    glBindBuffer(GL_ARRAY_BUFFER, v_b_o)
    glBufferData(GL_ARRAY_BUFFER, quad.itemsize * len(quad), quad, GL_STATIC_DRAW)

    # EBO
    e_b_o = glGenBuffers(1)
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, e_b_o)
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.itemsize * len(indices), indices, GL_STATIC_DRAW)

    # Configure positions of initial data
    # position = glGetAttribLocation(shader, "position")
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(0))
    glEnableVertexAttribArray(0)

    # Configure colors of initial data
    # color = glGetAttribLocation(shader, "color")
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(12))
    glEnableVertexAttribArray(1)

    # Configure texture coordinates of initial data
    # texture_coords = glGetAttribLocation(shader, "inTexCoords")
    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, quad.itemsize * 8, ctypes.c_void_p(24))
    glEnableVertexAttribArray(2)

    # Texture
    texture = glGenTextures(1)
    # Bind texture
    glBindTexture(GL_TEXTURE_2D, texture)
    # Texture wrapping params
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
    # Texture filtering params
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    #

    # Open image
    image = Image.open("res/piece.png")
    #
    # img_data = numpy.array(list(image.getdata()), numpy.uint8)
    #
    # flipped_image = image.transpose(Image.FLIP_TOP_BOTTOM)
    # img_data = flipped_image.convert("RGBA").tobytes()
    #
    img_data = image.convert("RGBA").tobytes()
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, img_data)
    # print(image.width, image.height)

    #

    # Create render buffer with size (image.width x image.height)
    rb_obj = glGenRenderbuffers(1)
    glBindRenderbuffer(GL_RENDERBUFFER, rb_obj)
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, image.width, image.height)

    # Create frame buffer
    fb_obj = glGenFramebuffers(1)
    glBindFramebuffer(GL_FRAMEBUFFER, fb_obj)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb_obj)

    # Check frame buffer (that simple buffer should not be an issue)
    status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
    if status != GL_FRAMEBUFFER_COMPLETE:
        print("incomplete framebuffer object")

    #

    # Install program
    glUseProgram(shader)

    # Bind framebuffer and set viewport size
    glBindFramebuffer(GL_FRAMEBUFFER, fb_obj)
    glViewport(0, 0, image.width, image.height)

    # Draw the quad which covers the entire viewport
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)

    #

    # PNG
    # Read the data and create the image
    image_buffer = glReadPixels(0, 0, image.width, image.height, GL_RGBA, GL_UNSIGNED_BYTE)
    image_out = numpy.frombuffer(image_buffer, dtype=numpy.uint8)
    image_out = image_out.reshape(image.height, image.width, 4)
    img = Image.fromarray(image_out, 'RGBA')
    img.save(r"res/image_out.png")

    # JPG
    '''
    # Read the data and create the image
    image_buffer = glReadPixels(0, 0, image.width, image.height, GL_RGB, GL_UNSIGNED_BYTE)
    image_out = numpy.frombuffer(image_buffer, dtype=numpy.uint8)
    image_out = image_out.reshape(image.height, image.width, 3)
    img = Image.fromarray(image_out, 'RGB')
    img.save(r"res/image_out.jpg")
    '''

    #

    # Bind default frame buffer (0)
    glBindFramebuffer(GL_FRAMEBUFFER, 0)

    # Set viewport rectangle to window size
    glViewport(0, 0, 0, 0)  # Size (0, 0) for show nothing in window
    # glViewport(0, 0, 800, 600)

    # Set clear color
    glClearColor(0., 0., 0., 1.)

    #

    # Program loop
    while not glfw.window_should_close(window):
        # Call events
        glfw.poll_events()

        # Clear window
        glClear(GL_COLOR_BUFFER_BIT)

        # Draw
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)

        # Send to window
        glfw.swap_buffers(window)

        # Force terminate program, since it will work like clicked in 'Close' button
        break

    #

    # Terminate program
    glfw.terminate()


if __name__ == "__main__":
    main()

您必须渲染到 Framebuffer Object

创建一个带有渲染目标的帧缓冲区,其大小与图像相同:

# create render buffer with size (image.width x image.height)
rb_obj = glGenRenderbuffers(1)
glBindRenderbuffer(GL_RENDERBUFFER, rb_obj )
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA, image.width, image.height)

# create framebuffer
fb_obj = glGenFramebuffers(1)
glBindFramebuffer(GL_FRAMEBUFFER, fb_obj)
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb_obj )

# check framebuffer (that simple buffer should not be an issue)
status = glCheckFramebufferStatus(GL_FRAMEBUFFER)
if status != GL_FRAMEBUFFER_COMPLETE:
    print("incomplete framebuffer object") 

[...] I don't need a vertex shader. [...]

无论如何你都需要一个顶点着色器,因为你必须在整个视口上绘制一个四边形。着色器程序需要一个顶点着色器和一个片段着色器。 由于几何体必须覆盖整个视口,因此顶点坐标必须在 [-1, 1] 范围内。此外,您还必须更改纹理坐标,否则 window 将被翻转:

#           positions        colors          texture coords
quad = [   -1.0, -1.0, 0.0,  1.0, 0.0, 0.0,  0.0, 1.0,
            1.0, -1.0, 0.0,  0.0, 1.0, 0.0,  1.0, 1.0,
            1.0,  1.0, 0.0,  0.0, 0.0, 1.0,  1.0, 0.0,
           -1.0,  1.0, 0.0,  1.0, 1.0, 1.0,  0.0, 0.0]

[...] I don't need a window [...]

您需要 window 来创建 OpenGL 上下文。参见:
Minimal Windowless OpenGL Context Initialization
Windowless OpenGL
How to render offscreen on OpenGL?
How to use GLUT/OpenGL to render to a file?

绑定帧缓冲区,将视口矩形设置为帧缓冲区的大小,安装着色器程序,绑定纹理并渲染四边形:

# bind texture
glBindTexture(GL_TEXTURE_2D, texture)

# install program
glUseProgram(shader)

# bind framebuffer and set viewport size
glBindFramebuffer(GL_FRAMEBUFFER, fb_obj)
glViewport(0, 0, image.width, image.height)

# draw the quad which covers the entire viewport
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, None)

最后使用glReadPixels从framebuffer中读取数据:

# read the data and create the image
image_buffer = glReadPixels(0, 0, image.width, image.height, GL_RGBA, GL_UNSIGNED_BYTE)
imageout = numpy.frombuffer(image_buffer, dtype=numpy.uint8)
imageout = imageout.reshape(image.height, image.width, 4)
img = Image.fromarray(imageout, 'RGBA')
img.save(r"image_out.png")

# bind default framebuffer (0) and set viewport rectangle to window size
glBindFramebuffer(GL_FRAMEBUFFER, 0)
glViewport(0, 0, 800, 600)

上面的代码生成了一个 png 文件。如果要生成jpg,则改为:

image_buffer = glReadPixels(0, 0, image.width, image.height, GL_RGB, GL_UNSIGNED_BYTE)
imageout = numpy.frombuffer(image_buffer, dtype=numpy.uint8)
imageout = imageout.reshape(image.height, image.width, 3)
img = Image.fromarray(imageout, 'RGB')
img.save(r"image_out.jpg") 

您根本不需要渲染循环,但之后就可以渲染到 window。