You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于ModernGL实现用OpenCV加载纹理并保存图像的问题

嘿,我来帮你搞定这两个OpenCV + ModernGL的问题!不管是用OpenCV加载图像作为纹理,还是用它保存处理后的结果,咱们都能彻底摆脱Pillow依赖,让代码跑起来顺顺当当的。


问题1:用cv2.imread加载图像为ModernGL纹理失败

你之前的代码直接把cv2加载的图像转成float32就想当纹理用,忽略了几个关键细节:

  • OpenCV默认加载的是BGR格式,而ModernGL的纹理默认期望RGBA格式
  • 图像的维度需要调整(从H×W×BGR转成H×W×RGBA)
  • ModernGL创建纹理时需要明确指定数据格式和类型

修改后的加载代码应该是这样的:

# 替换原来的纹理加载代码
img = cv2.imread("test6.png", cv2.IMREAD_UNCHANGED)  # 带alpha通道的图用这个,普通图用IMREAD_COLOR
# 处理通道:BGR转RGBA(如果是普通图,先加alpha通道)
if img.shape[2] == 3:
    img = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)
else:
    img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
# 转成float32,因为ModernGL常用f4格式,记得归一化到0-1范围!
img_float = img.astype('f4') / 255.0
# 创建ModernGL纹理
self.texture = self.ctx.texture(
    size=(img.shape[1], img.shape[0]),  # 注意是W×H,OpenCV是H×W
    components=4,
    data=img_float.tobytes(),
)
# 可选:设置纹理过滤参数,避免拉伸模糊
self.texture.filter = (moderngl.LINEAR, moderngl.LINEAR)

核心注意点:

  • cv2.IMREAD_UNCHANGED可以保留图像的alpha通道(如果有的话)
  • 必须把BGR转成RGBA,不然颜色会完全错乱
  • 图像数据要归一化到0-1的float范围,和ModernGL的纹理格式匹配
  • 创建纹理时要传入正确的宽高(OpenCV是H×W,ModernGL需要W×H)

问题2:用cv2.imwrite保存FBO结果无输出

FBO读取到的是RGBA格式的float32数据,而且OpenGL的纹理坐标y轴是翻转的(和OpenCV的图像坐标相反),直接存肯定不行。咱们需要做这几步转换:

  1. 把float32数据转成uint8(0-255范围)
  2. 把RGBA转成BGRA(或者BGR,如果不需要alpha的话)
  3. 翻转y轴,修正坐标方向

修改后的write方法代码:

def write(self, name):
    # 从FBO读取RGBA float32数据
    raw_data = self.fbo.read(components=4, dtype='f4')
    # 转成numpy数组,形状为 (H*W*4,)
    buf = np.frombuffer(raw_data, dtype='f4')
    # 重塑为H×W×4的RGBA图像(FBO的size是W,H,所以要对应H,W,4)
    img = buf.reshape((self.size[1], self.size[0], 4))
    # 1. 归一化到0-255,转成uint8
    img = (img * 255).astype(np.uint8)
    # 2. RGBA转BGRA(OpenCV的带alpha格式),如果不需要alpha就转成BGR
    img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
    # 3. 翻转y轴,修正OpenGL和OpenCV的坐标差异
    img = cv2.flip(img, 0)
    # 保存图像
    cv2.imwrite(name, img)

核心注意点:

  • FBO的size是(W, H),所以reshape的时候要对应成(H, W, 4)
  • 必须把float的0-1值转成uint8的0-255,不然cv2保存的会是全黑的图
  • 翻转y轴是因为OpenGL的原点在左下角,OpenCV在左上角
  • 如果你的处理不需要alpha通道,可以把COLOR_RGBA2BGRA改成COLOR_RGBA2BGR,保存普通RGB图

完整修改后的代码

把上面的修改整合到你的代码里,就是这样:

from pathlib import Path
from array import array
import cv2
import numpy as np
import moderngl
import moderngl_window

class ImageProcessing(moderngl_window.WindowConfig):
    window_size = 3840 // 2, 2160 // 2
    resource_dir = Path(__file__).parent.resolve()

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.image_processing = ImageTransformer(self.ctx, self.window_size)
        
        # 用OpenCV加载图像并创建ModernGL纹理
        img = cv2.imread("test6.png", cv2.IMREAD_UNCHANGED)
        # 处理通道格式
        if img.shape[2] == 3:
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)
        else:
            img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
        # 转成归一化的float32
        img_float = img.astype('f4') / 255.0
        # 创建纹理
        self.texture = self.ctx.texture(
            size=(img.shape[1], img.shape[0]),
            components=4,
            data=img_float.tobytes(),
        )
        self.texture.filter = (moderngl.LINEAR, moderngl.LINEAR)

    def render(self, time, frame_time):
        # 窗口显示
        self.image_processing.render(self.texture, target=self.ctx.screen)
        # 离屏渲染到FBO
        self.image_processing.render(self.texture)
        self.image_processing.write("output.png")

class ImageTransformer:
    def __init__(self, ctx, size, program=None):
        self.ctx = ctx
        self.size = size
        self.program = None
        self.fbo = self.ctx.framebuffer(
            color_attachments=[self.ctx.texture(self.size, 4)]
        )
        # 创建默认着色器程序
        if not program:
            self.program = self.ctx.program(
                vertex_shader="""
#version 330
in vec2 in_position;
in vec2 in_uv;
out vec2 uv;
void main() {
    gl_Position = vec4(in_position, 0.0, 1.0);
    uv = in_uv;
}
""",
                fragment_shader = """
#version 330
uniform sampler2D image;
in vec2 uv;
out vec4 out_color;
void main() {
    vec4 color = texture(image, uv);
    // 这里是你的图像处理逻辑(保留原红色通道)
    out_color = vec4(color.r, 0, 0, color.a);
}
""",
            )
        # 全屏四边形顶点数据
        self.vertices = self.ctx.buffer(
            array(
                'f',
                [
                    -1, 1, 0, 1,
                    -1, -1, 0, 0,
                    1, 1, 1, 1,
                    1, -1, 1, 0,
                ]
            )
        )
        self.quad = self.ctx.vertex_array(
            self.program,
            [
                (self.vertices, '2f 2f', 'in_position', 'in_uv'),
            ]
        )

    def render(self, texture, target=None):
        if target:
            target.use()
        else:
            self.fbo.use()
        texture.use(0)
        self.quad.render(mode=moderngl.TRIANGLE_STRIP)

    def write(self, name):
        # 读取FBO数据并转换为OpenCV格式
        raw_data = self.fbo.read(components=4, dtype='f4')
        buf = np.frombuffer(raw_data, dtype='f4')
        img = buf.reshape((self.size[1], self.size[0], 4))
        # 转换为uint8并调整颜色通道
        img = (img * 255).astype(np.uint8)
        img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
        # 翻转y轴修正坐标
        img = cv2.flip(img, 0)
        # 保存图像
        cv2.imwrite(name, img)

if __name__ == "__main__":
    ImageProcessing.run()

现在运行这个代码,应该就能正常用OpenCV加载纹理,并且保存出正确的图像了,完全不需要Pillow!

内容的提问来源于stack exchange,提问作者Noah

火山引擎 最新活动