如何将 PYopenGL 写入 JPG 图像

Posted

技术标签:

【中文标题】如何将 PYopenGL 写入 JPG 图像【英文标题】:How to write PYopenGL in to JPG image 【发布时间】:2016-12-13 16:35:02 【问题描述】:

我是 PYopenGL 的新手, 实际上,我也不确定 PYopenGL 是否适合我的任务。

我有一个 Wavefront obj 文件格式的 3D 模型。我需要从给定的视图中获取模型的“打印屏幕”。换句话说,我需要渲染模型而不是显示模型,而是将其保存为图像 (jpg)

我的想法是使用 PYopenGL 来完成这项任务。但是,谷歌搜索我找不到如何做到这一点的建议或示例。因此,我开始怀疑 PYopenGL 是否适合我的任务。

你们中是否有人已经有了类似的东西或知道我可以用来学习的示例?

提前致谢。

【问题讨论】:

欢迎来到 SO!您可能想查看***.com/help/how-to-ask 以避免未回答的问题或否决票。您的问题非常广泛,我们在这里重视的一件事是您为解决您的问题做出并表现出事先的努力。因此,我建议您添加代码以显示您到目前为止所尝试的内容。如果您还没有尝试过什么,那么我们不是来为您完成工作的,您可能会被标记为“过于宽泛”,但只有足够信息的人可能会看到这一点并指导您前进。跨度> 【参考方案1】:

GLUT 隐藏窗口方法要简单得多,并且与平台无关,但会导致窗口闪烁。

要为 Django 设置它,即您可以将渲染器实现为单独的 Web 服务器,它只会在启动时按窗口闪烁一次,然后通过 http 响应返回渲染图像。

from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

from PIL import Image
from PIL import ImageOps

import sys

width, height = 300, 300

def init():
    glClearColor(0.5, 0.5, 0.5, 1.0)
    glColor(0.0, 1.0, 0.0)
    gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
    glViewport(0, 0, width, height)

def render():

    glClear(GL_COLOR_BUFFER_BIT)

    # draw xy axis with arrows
    glBegin(GL_LINES)

    # x
    glVertex2d(-1, 0)
    glVertex2d(1, 0)
    glVertex2d(1, 0)
    glVertex2d(0.95, 0.05)
    glVertex2d(1, 0)
    glVertex2d(0.95, -0.05)

    # y
    glVertex2d(0, -1)
    glVertex2d(0, 1)
    glVertex2d(0, 1)
    glVertex2d(0.05, 0.95)
    glVertex2d(0, 1)
    glVertex2d(-0.05, 0.95)

    glEnd()

    glFlush()


def draw():
    render()
    glutSwapBuffers()

def main():
    glutInit(sys.argv)

    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB)
    glutInitWindowSize(300, 300)
    glutCreateWindow(b"OpenGL Offscreen")
    glutHideWindow()

    init()
    render()

    glPixelStorei(GL_PACK_ALIGNMENT, 1)
    data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
    image = Image.frombytes("RGBA", (width, height), data)
    image = ImageOps.flip(image) # in my case image is flipped top-bottom for some reason
    image.save('glutout.png', 'PNG')

    #glutDisplayFunc(draw)
    #glutMainLoop()

main()

【讨论】:

因为它在 2020 年仍然是实际的,所以我想更新我的答案并强调,离屏渲染的正确方法是使用(惊喜!)离屏 OpenGL 渲染器,称为 OS Mesa(屏幕外的操作系统)。有自文档环境,展示了如何安装它(参见 Dockerfile,可能有点过时,请尝试版本)并与 PyOpenGL 一起使用。 github.com/AntonOvsyannikov/DockerGL【参考方案2】:

我的回答(部分基于CodeSurgeon's answer)是针对问题的第二部分。

离屏渲染(意味着将某些内容渲染到内部缓冲区而不是可见窗口,并将渲染的图像保存到文件或传输为http响应以显示在网页上)在PyOpenGL中(如在OpenGL本身中) ) 有点棘手,因为到目前为止由 GLUT 完成的所有事情(创建窗口、初始化 opengl 上下文等)现在都需要手动完成,因为您不需要标准的 GLUT 窗口弹出甚至闪烁。

所以在OpenGL中有3种离屏渲染的方法:

1) 使用 GLUT 进行初始化,但隐藏 glut 窗口并对其进行渲染。此方法完全独立于平台,但在初始化过程中会出现短暂的 GLUT 窗口,因此不太适合 Web 服务器,即,您仍然可以将其设置为单独的 Web 服务器,仅在启动时进行初始化并使用某些接口与之通信它。

2) 手动创建所有内容:隐藏窗口、OpenGL 上下文和要渲染到的帧缓冲区对象。这个方法很好,因为你控制了一切,没有窗口出现,但是上下文的创建是平台特定的(下面是 Win64 的例子)

3) 第 3 种方法与方法 2 类似,但使用 WGL 创建的默认 Framebuffer,而不是手动创建 FBO。效果与方法 2 相同,但更简单。如果您出于其他原因不需要 FBO,则可能是首选。

现在我将描述方法 2,核心方法一。更多示例在我的GitHub repository。

所以离屏渲染算法包括以下步骤:

    创建隐藏窗口,因为你需要这个窗口,即使隐藏到 创建 OpenGL 上下文 创建 OpenGL 上下文 创建帧缓冲区对象 (FBO) 创建渲染缓冲区(颜色和深度)并将它们附加到 FBO(有关详细信息,请参阅 FBO manual) 将 FBO 绑定到 OpenGL 上下文以进行渲染 渲染一些东西。在此示例中,我仅使用 2D 图元进行简化,但缓冲区已准备好使用深度测试进行 3D 渲染 设置读取缓冲区,在我们的例子中只有一个 FBO,所以不需要选择一个来读取 使用 glReadPixels() 从颜色渲染缓冲区读取渲染数据 对接收到的数据做任何你想做的事,即从中创建 PIL 图像并将其保存到文件中。您还可以使用双分辨率渲染并调整 PIL 图像的大小以获得抗锯齿效果。

所以下面有完整的例子。

重要! 3.1.1 PyOpenGL 实现有个BUG!当您刚刚导入 WGL 时,glReadPixels() 开始崩溃

ctypes.ArgumentError: 参数 7: : 错误类型

为避免这种情况,请转到您的包 dir\OpenGL\raw\WGL_types.py ,找到以下几行

HANDLE = POINTER(None)  # /home/mcfletch/pylive/OpenGL-ctypes/src/wgl.h:60
# TODO: figure out how to make the handle not appear as a void_p within the code...
HANDLE.final = True

并将其替换为(当然对于 x64,对于 x86 UINT32 假设)

HANDLE = UINT64
HANDLE.final = True

所以有例子

from win32api import *
from win32con import *
from win32gui import *

from OpenGL.WGL import *
from OpenGL.GL import *
from OpenGL.GLU import *
from OpenGL.GLUT import *

from PIL import Image
from PIL import ImageOps

import uuid

# =========================================
# I left here only necessary constants, it's easy to search for the rest

PFD_TYPE_RGBA =         0
PFD_MAIN_PLANE =        0
PFD_DOUBLEBUFFER =      0x00000001
PFD_DRAW_TO_WINDOW =    0x00000004
PFD_SUPPORT_OPENGL =    0x00000020

# =========================================
# OpenGL context creation helpers

def mywglCreateContext(hWnd):
    pfd = PIXELFORMATDESCRIPTOR()

    pfd.dwFlags = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL
    pfd.iPixelType = PFD_TYPE_RGBA
    pfd.cColorBits = 32
    pfd.cDepthBits = 24
    pfd.iLayerType = PFD_MAIN_PLANE

    hdc = GetDC(hWnd)

    pixelformat = ChoosePixelFormat(hdc, pfd)
    SetPixelFormat(hdc, pixelformat, pfd)

    oglrc = wglCreateContext(hdc)
    wglMakeCurrent(hdc, oglrc)

    # check is context created succesfully
    # print "OpenGL version:", glGetString(GL_VERSION)


def mywglDeleteContext():
    hrc = wglGetCurrentContext()
    wglMakeCurrent(0, 0)
    if hrc: wglDeleteContext(hrc)


# =========================================
# OpenGL Framebuffer Objects helpers

def myglCreateBuffers(width, height):

    fbo = glGenFramebuffers(1)
    color_buf = glGenRenderbuffers(1)
    depth_buf = glGenRenderbuffers(1)

    # binds created FBO to context both for read and draw
    glBindFramebuffer(GL_FRAMEBUFFER, fbo)

    # bind color render buffer
    glBindRenderbuffer(GL_RENDERBUFFER, color_buf)
    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, color_buf)

    # bind depth render buffer - no need for 2D, but necessary for real 3D rendering
    glBindRenderbuffer(GL_RENDERBUFFER, depth_buf)
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, width, height)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depth_buf)

    return fbo, color_buf, depth_buf, width, height

def myglDeleteBuffers(buffers):
    fbo, color_buf, depth_buf, width, height = buffers
    glBindFramebuffer(GL_FRAMEBUFFER, 0)
    glDeleteRenderbuffers(1, color_buf)
    glDeleteRenderbuffers(1, depth_buf)
    glDeleteFramebuffers(1, fbo)

def myglReadColorBuffer(buffers):
    fbo, color_buf, depth_buf, width, height = buffers
    glPixelStorei(GL_PACK_ALIGNMENT, 1)
    glReadBuffer(GL_COLOR_ATTACHMENT0)
    data = glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE)
    return data, width, height

# =========================================
# Scene rendering

def renderInit(width, height):

    glClearColor(0.5, 0.5, 0.5, 1.0)
    glColor(0.0, 1.0, 0.0)
    gluOrtho2D(-1.0, 1.0, -1.0, 1.0)
    glViewport(0, 0, width, height)


def render():

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)

    # draw xy axis with arrows
    glBegin(GL_LINES)

    # x
    glVertex2d(-1, 0)
    glVertex2d(1, 0)
    glVertex2d(1, 0)
    glVertex2d(0.95, 0.05)
    glVertex2d(1, 0)
    glVertex2d(0.95, -0.05)

    # y
    glVertex2d(0, -1)
    glVertex2d(0, 1)
    glVertex2d(0, 1)
    glVertex2d(0.05, 0.95)
    glVertex2d(0, 1)
    glVertex2d(-0.05, 0.95)

    glEnd()

    glFlush()

# =========================================
# Windows stuff and main steps

def main():

    # Create window first with Win32 API

    hInstance = GetModuleHandle(None)

    wndClass = WNDCLASS()

    wndClass.lpfnWndProc = DefWindowProc
    wndClass.hInstance = hInstance
    wndClass.hbrBackground = GetStockObject(WHITE_BRUSH)
    wndClass.hCursor = LoadCursor(0, IDC_ARROW)
    wndClass.lpszClassName = str(uuid.uuid4())
    wndClass.style = CS_OWNDC

    wndClassAtom = RegisterClass(wndClass)

    # don't care about window size, couse we will create independent buffers
    hWnd = CreateWindow(wndClassAtom, '', WS_POPUP, 0, 0, 1, 1, 0, 0, hInstance, None)

    # Ok, window created, now we can create OpenGL context

    mywglCreateContext(hWnd)

    # In OpenGL context create Framebuffer Object (FBO) and attach Color and Depth render buffers to it

    width, height = 300, 300
    buffers = myglCreateBuffers(width, height)

    # Init our renderer
    renderInit(width, height)

    # Now everything is ready for job to be done!
    # Render something and save it to file

    render()

    data, width, height = myglReadColorBuffer(buffers)
    image = Image.frombytes("RGBA", (width, height), data)
    image = ImageOps.flip(image) # in my case image is flipped top-bottom for some reason

    # it's easy to achive antialiasing effect by resizing rendered image
    # don't forget to increase initial rendered image resolution and line thikness for 2D
    #image = image.resize((width/2, height/2), Image.ANTIALIAS)

    image.save("fbo.png", "PNG")

    # Shutdown everything
    myglDeleteBuffers(buffers)
    mywglDeleteContext()

main()

【讨论】:

【参考方案3】:

除了 glutHideWindow(),如果你需要在服务器上做,你可以使用虚拟显示。 requirements.txt:pyopengl、枕头、pyvirtualdisplay。 包:freeglut3-dev、xvfb。

from pyvirtualdisplay import Display
# before glutInit create virtual display
display = Display(visible=0, size=(HEIGHT, WIDTH))
display.start()

【讨论】:

【参考方案4】:

如果您感兴趣的只是将 3d 模型/场景从特定角度渲染到图像,只要您不介意打开并运行 OpenGL 窗口,PyOpenGL 就可以用于您的目的。

网上有很多资源讨论如何为 .obj 格式编写解析器。除了查看here 格式的***文章,相信您可以在pygame website 上找到一个固定函数obj loader 的实现。如果您自己制作 .obj 模型,那会更容易,因为规范非常松散,并且很难编写一个健壮的解析器。或者,您可以使用 Assimp 之类的库来加载您的模型并提取它们的数据,它具有 python pyAssimp 绑定。

至于保存屏幕截图,您需要将 3D 场景渲染为纹理。我强烈建议您查看this answer。如果要进行离屏渲染,则需要了解如何使用 glReadPixels 以及如何使用 FBO(帧缓冲区对象)。

【讨论】:

以上是关于如何将 PYopenGL 写入 JPG 图像的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Rails 或 iOS 上将图像组合成 JPG 图像/精灵并写入元数据

如何使用 imwrite OpenCV 3.1 将图像写入目录?

对 JPG 图像进行操作时出现“无法将模式 P 写入 JPEG”

蟒蛇 2.7.3 。 . .写入 .jpg/.png 图像文件?

PyOpenGL - 如何加载在 mtl 文件中定义了颜色的 obj 文件

python:将base64编码的png图像转换为jpg