2

Introduction

I have a game project coded with C++ and Lua (I know in the subject is written 'Python', don't worry about it), and, in there, I apply a fragment (not vertex!) shader into the game screen. Since part of my project was not made by me (because its Open-Source), the system to apply fragment shaders using OpenGL was made already by others, but not the 2xSal fragment shader itself. I consider myself as a beginner in this subject. The only piece of code I "made", was the fragment shader (I don't know if it's necessary, but I based my code on the code of the following link: https://github.com/libretro/glsl-shaders/blob/master/xsal/shaders/2xsal-level2-pass2.glsl).

This is my changes for make the 2xSal fragment shader to work (on my game):

#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);
}

The issue

This said, what I need now has nothing to do with my game, but with the code itself that I showed you. I need to apply this fragment shader to an input image for a given path and save the output to another (and new) image file, using Python, but I DON'T WANT TO DISPLAY THEM inside a window (like using GLFW or GLUT).

The idea is too simple, but I can't figure out what and how to do it. I just need to save the output result of what the fragment changed in the input image.

I don't need a vertex shader. I don't need a window to display the result, I don't need a window at all. I just want to save the result to a image file.

What I've tried

I found a code in Python in which displays an image input into a GLFW window. I thought that, inch by inch, I would get to where I want, but I can't find how to:

  1. Apply the 2xSal fragment shader that I showed to you (would be awesome to separate the fragment shader to a .glsl file);
  2. Remove the GLFW window;
  3. Remove the vertex shader (if it is even possible), since I don't need to make any changes to the vertices;
  4. Save the result to an output file.

Also, I noticed that the Python code has an issue in which stretches or shrinks the image, because the vertices makes a square and draws the input image texture within it. So with the actual code, I think it would work only for images that are squares.

The Python actual code

# # 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()

> Solved by Rabbid76

Thanks to @Rabbid76, I was able to make this code.

Since I love when people posts the result, this is my final code:

# # 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()
River
  • 33
  • 7

1 Answers1

1

You have to render to a Framebuffer Object.

Create a frame buffer with a render target, that has the same size as the image:

# 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. [...]

You'll need a vertex shader anyway, because you have to draw a quad on the entire viewport. The shader program needs a vertex shader and a fragment shader. Since the geometry has to cover the entire viewport, the vertex coordinates have to be in range [-1, 1]. Further more you've to change the texture coordinates, else the window would be flipped:

#           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 [...]

You need the window to create the OpenGL context. See:
Minimal Windowless OpenGL Context Initialization
Windowless OpenGL
How to render offscreen on OpenGL?
How to use GLUT/OpenGL to render to a file?

Bind the framebuffer, set the viewport rectangle to the size of the framebuffer, install the shader program, bind the texture and render the quad:

# 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)

Finally use glReadPixels to read the data from the 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)

The code above generated a png file. If you want to generate a jpg, then it change to:

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") 

You do not need a render loop at all, but after that it would be possible to render to the window.

Rabbid76
  • 202,892
  • 27
  • 131
  • 174