专栏首页字节流动OpenGL 使用 Shader 实现 RGBA 转 I420(附项目源码)

OpenGL 使用 Shader 实现 RGBA 转 I420(附项目源码)

前面连续写过两篇 shader 实现 RGBA 转 YUV 的文章:

关于 YUV 图像的相关知识这里也贴出来一些链接,供不熟悉的同学查阅。

Shader 实现 RGBA 转 I420

I420 格式的图像在视频解码中比较常见,像前面文章中提到的,在工程中一般会选择使用 Shader 将 RGBA 转 YUV,这样再使用 glReadPixels 读取图像时可以有效降低传输数据量,提升性能,并且兼容性好。

所以,在读取 OpenGL 渲染结果时,先利用 Shader 将 RGBA 转 YUV 然后再进行读取,这种方式非常高效便捷

例如 YUYV 格式相对 RGBA 数据量降为原来的 50% ,而采用 NV21 或者 I420 格式可以降低为原来的 37.5% 。

当然读取 OpenGL 渲染结果的方式还有很多种,要视具体的需求和使用场景而定,具体可以参考文章:OpenGL 渲染图像读取哪家强?

对 I420 格式比较熟悉的同学应该非常了解,I420 有 3 个平面(plane), 一个 plane 存储 Y 分量,另外 2 个 plane 分别存储 UV 分量。

I420 格式

其中 Y plane 的宽和高就是图像的宽高,U plane 和 V plane 的宽高分别是原图像宽高的一半,所以 I420 图像占用的内存大小是 width * height + width * height / 4 * 2 = width * height * 1.5

注意这个尺寸,后续申请用于颜色缓冲区的纹理也是这个尺寸,用于保存生成 I420 图像(简单这样理解)。

根据这个尺寸设置渲染缓冲区纹理的大小:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width / 4, m_RenderImage.height * 1.5, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);

用于保存生成 I420 图像的纹理可以简单抽象成如下结构(实际上纹理中的数据不是这样排列的):

I420 图像纹理结构

为什么宽度是 width/4 ? 因为我们用的是 RGBA 格式的纹理,一个像素占用 4 个字节,而我们每个 Y 只需要一个字节来存储。

从图上纹理坐标可以看出,在纹理坐标 y < (2/3) 范围,需要完成一次对整个纹理的采样,用于生成 Y plane 的图像;

当纹理坐标 y > (2/3) 且 y < (5/6) 范围,需要再进行一次对整个纹理的采样,用于生成 U plane 的图像;

同理,当纹理坐标 y > (5/6) 范围,再进行一次对整个纹理的采样生成 V plane 的图像。

最重要的一点是视口要设置正确:glViewport(0, 0, width / 4, height * 1.5); 。

Y plane 偏移采样

由于视口宽度设置为原来的 1/4 ,可以简单的认为(实际上比较复杂)相对于原来的图像每隔 4 个像素做一次采样,由于我们生成 Y plane 的图像需要对每一个像素都进行采样,所以还需要进行 3 次偏移采样。

U plane 偏移采样

V plane 偏移采样

同样,生成 U plane 和 V plane 的图像也需要进行 3 次额外的偏移采样,不同的是每次需要偏移 2 个像素。

offset 需要设置为一个像素归一化之后的值:1.0/width, 按照原理图,为了便于理解,这里将采样过程简化为以 4 个像素为单位进行。

在纹理坐标 y < (2/3) 范围,一次采样(加三次偏移采样)4 个 RGBA 像素(R,G,B,A)生成 1 个(Y0,Y1,Y2,Y3),整个范围采样结束时填充好 width*height 大小的缓冲区;

当纹理坐标 y > (2/3) 且 y < (5/6) 范围,一次采样(加三次偏移采样)8 个 RGBA 像素(R,G,B,A)生成(U0,U1,U2,U3),又因为 U plane 缓冲区的宽高均为原图的 1/2 ,U plane 在垂直方向和水平方向的采样都是隔行进行,整个范围采样结束时填充好 width*height/4 大小的缓冲区。

当纹理坐标 y > (5/6) 范围,一次采样(加三次偏移采样)8 个 RGBA 像素(R,G,B,A)生成(V0,V1,V2,V3),同理,因为 V plane 缓冲区的宽高均为原图的 1/2 ,垂直方向和水平方向都是隔行采样,整个范围采样结束时填充好 width*height/4 大小的缓冲区

最后我们使用 glReadPixels 读取生成的 I420 图像(注意宽和高):

glReadPixels(0, 0, width / 4, height * 1.5, GL_RGBA, GL_UNSIGNED_BYTE, pBuffer);

代码实现

上节我们详细讨论了 Shader 实现 RGBA 转 I420 原理,下面将直接贴出几处关键的实现代码。

创建 FBO 时,需要注意作为颜色缓冲区纹理的尺寸(width / 4, height * 1.5),上文已经详细解释过。

bool RGB2I420Sample::CreateFrameBufferObj()
{
    // 创建并初始化 FBO 纹理
    glGenTextures(1, &m_FboTextureId);
    glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, GL_NONE);

    // 创建并初始化 FBO
    glGenFramebuffers(1, &m_FboId);
    glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);
    glBindTexture(GL_TEXTURE_2D, m_FboTextureId);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, m_FboTextureId, 0);
    //创建 FBO 时,需要注意作为颜色缓冲区纹理的尺寸(width / 4, height * 1.5)
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width / 4, m_RenderImage.height * 1.5, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) {
        LOGCATE("RGB2I420Sample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
        return false;
    }
    glBindTexture(GL_TEXTURE_2D, GL_NONE);
    glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE);
    return true;

}

实现 RGBA 转 I420 完整的 shader 脚本:

#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;
uniform float u_Offset;//偏移量 1.0/width
uniform vec2 u_ImgSize;//图像尺寸
//Y =  0.299R + 0.587G + 0.114B
//U = -0.147R - 0.289G + 0.436B
//V =  0.615R - 0.515G - 0.100B
const vec3 COEF_Y = vec3( 0.299,  0.587,  0.114);
const vec3 COEF_U = vec3(-0.147, -0.289,  0.436);
const vec3 COEF_V = vec3( 0.615, -0.515, -0.100);
const float U_DIVIDE_LINE = 2.0 / 3.0;
const float V_DIVIDE_LINE = 5.0 / 6.0;
void main()
{
    vec2 texelOffset = vec2(u_Offset, 0.0);
    if(v_texCoord.y <= U_DIVIDE_LINE) {
        //在纹理坐标 y < (2/3) 范围,需要完成一次对整个纹理的采样,
        //一次采样(加三次偏移采样)4 个 RGBA 像素(R,G,B,A)生成 1 个(Y0,Y1,Y2,Y3),整个范围采样结束时填充好 width*height 大小的缓冲区;

        vec2 texCoord = vec2(v_texCoord.x, v_texCoord.y * 3.0 / 2.0);
        vec4 color0 = texture(s_TextureMap, texCoord);
        vec4 color1 = texture(s_TextureMap, texCoord + texelOffset);
        vec4 color2 = texture(s_TextureMap, texCoord + texelOffset * 2.0);
        vec4 color3 = texture(s_TextureMap, texCoord + texelOffset * 3.0);

        float y0 = dot(color0.rgb, COEF_Y);
        float y1 = dot(color1.rgb, COEF_Y);
        float y2 = dot(color2.rgb, COEF_Y);
        float y3 = dot(color3.rgb, COEF_Y);
        outColor = vec4(y0, y1, y2, y3);
    }
    else if(v_texCoord.y <= V_DIVIDE_LINE){

        //当纹理坐标 y > (2/3) 且 y < (5/6) 范围,一次采样(加三次偏移采样)8 个 RGBA 像素(R,G,B,A)生成(U0,U1,U2,U3),
        //又因为 U plane 缓冲区的宽高均为原图的 1/2 ,U plane 在垂直方向和水平方向的采样都是隔行进行,整个范围采样结束时填充好 width*height/4 大小的缓冲区。 

        float offsetY = 1.0 / 3.0 / u_ImgSize.y;
        vec2 texCoord;
        if(v_texCoord.x <= 0.5) {
            texCoord = vec2(v_texCoord.x * 2.0, (v_texCoord.y - U_DIVIDE_LINE) * 2.0 * 3.0);
        }
        else {
            texCoord = vec2((v_texCoord.x - 0.5) * 2.0, ((v_texCoord.y - U_DIVIDE_LINE) * 2.0 + offsetY) * 3.0);
        }

        vec4 color0 = texture(s_TextureMap, texCoord);
        vec4 color1 = texture(s_TextureMap, texCoord + texelOffset * 2.0);
        vec4 color2 = texture(s_TextureMap, texCoord + texelOffset * 4.0);
        vec4 color3 = texture(s_TextureMap, texCoord + texelOffset * 6.0);

        float u0 = dot(color0.rgb, COEF_U) + 0.5;
        float u1 = dot(color1.rgb, COEF_U) + 0.5;
        float u2 = dot(color2.rgb, COEF_U) + 0.5;
        float u3 = dot(color3.rgb, COEF_U) + 0.5;
        outColor = vec4(u0, u1, u2, u3);
    }
    else {
        //当纹理坐标 y > (5/6) 范围,一次采样(加三次偏移采样)8 个 RGBA 像素(R,G,B,A)生成(V0,V1,V2,V3),
        //同理,因为 V plane 缓冲区的宽高均为原图的 1/2 ,垂直方向和水平方向都是隔行采样,整个范围采样结束时填充好 width*height/4 大小的缓冲区。 

        float offsetY = 1.0 / 3.0 / u_ImgSize.y;
        vec2 texCoord;
        if(v_texCoord.x <= 0.5) {
            texCoord = vec2(v_texCoord.x * 2.0, (v_texCoord.y - V_DIVIDE_LINE) * 2.0 * 3.0);
        }
        else {
            texCoord = vec2((v_texCoord.x - 0.5) * 2.0, ((v_texCoord.y - V_DIVIDE_LINE) * 2.0 + offsetY) * 3.0);
        }

        vec4 color0 = texture(s_TextureMap, texCoord);
        vec4 color1 = texture(s_TextureMap, texCoord + texelOffset * 2.0);
        vec4 color2 = texture(s_TextureMap, texCoord + texelOffset * 4.0);
        vec4 color3 = texture(s_TextureMap, texCoord + texelOffset * 6.0);

        float v0 = dot(color0.rgb, COEF_V) + 0.5;
        float v1 = dot(color1.rgb, COEF_V) + 0.5;
        float v2 = dot(color2.rgb, COEF_V) + 0.5;
        float v3 = dot(color3.rgb, COEF_V) + 0.5;
        outColor = vec4(v0, v1, v2, v3);
    }
}

离屏渲染及 I420 图像的读取:

void RGB2I420Sample::Draw(int screenW, int screenH)
{
    // 离屏渲染
    glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);
    // 渲染成 I420 宽度像素变为 1/4 宽度,高度为 height * 1.5
    glViewport(0, 0, m_RenderImage.width / 4, m_RenderImage.height * 1.5);
    glUseProgram(m_FboProgramObj);
    glBindVertexArray(m_VaoIds[1]);
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, m_ImageTextureId);
    glUniform1i(m_FboSamplerLoc, 0);

    float texelOffset = (float) (1.f / (float) m_RenderImage.width);
    GLUtils::setFloat(m_FboProgramObj, "u_Offset", texelOffset);
    GLUtils::setVec2(m_FboProgramObj, "u_ImgSize", m_RenderImage.width, m_RenderImage.height);

    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);
    glBindVertexArray(0);
    glBindTexture(GL_TEXTURE_2D, 0);

    //I420 buffer = width * height * 1.5;
    uint8_t *pBuffer = new uint8_t[m_RenderImage.width * m_RenderImage.height * 3 / 2];

    NativeImage nativeImage = m_RenderImage;
    nativeImage.format = IMAGE_FORMAT_I420;
    nativeImage.ppPlane[0] = pBuffer;
    nativeImage.ppPlane[1] = pBuffer + m_RenderImage.width * m_RenderImage.height;
    nativeImage.ppPlane[2] = nativeImage.ppPlane[1] + m_RenderImage.width * m_RenderImage.height / 4;

    //使用 glReadPixels 读取生成的 I420 图像(注意宽和高)
    glReadPixels(0, 0, nativeImage.width / 4, nativeImage.height * 1.5, GL_RGBA, GL_UNSIGNED_BYTE, pBuffer);


    //保存 I420 格式的 YUV 图片
    std::string path(DEFAULT_OGL_ASSETS_DIR);
    NativeImageUtil::DumpNativeImage(&nativeImage, path.c_str(), "RGB2I420");
    delete []pBuffer;

    glBindFramebuffer(GL_FRAMEBUFFER, 0);

}

思考题:利用 shader 实现 RGBA 转 I420 的效率为什么没有转 NV21 的效率高?

码字不易,帮忙点个在看呗!完整实现代码扫描下方二维码获取

-- END --

文章分享自微信公众号:
字节流动

本文参与 腾讯云自媒体分享计划 ,欢迎热爱写作的你一起参与!

作者:字节流动
原始发表时间:2021-11-22
如有侵权,请联系 cloudcommunity@tencent.com 删除。
登录 后参与评论
0 条评论

相关文章

  • OpenGL 使用 Shader 实现 RGBA 转 I420(附项目源码)

    I420 格式的图像在视频解码中比较常见,像前面文章中提到的,在工程中一般会选择使用 Shader 将 RGBA 转 YUV,这样再使用 glReadPixel...

    字节流动
  • 面试官:请使用 OpenGL ES 将 RGB 图像转换为 YUV 格式。我 ……

    最近,有位读者大人在后台反馈:在参加一场面试的时候,面试官要求他用 shader 实现图像格式 RGB 转 YUV ,他听了之后一脸懵,然后悻悻地对面试官说,他...

    字节流动
  • 使用 OpenGL 实现 RGB 到 YUV 的图像格式转换

    最近,有位读者大人在后台反馈:在参加一场面试的时候,面试官要求他用 shader 实现图像格式 RGB 转 YUV ,他听了之后一脸懵,然后悻悻地对面试官说,他...

    字节流动
  • OpenGL: 如何利用 Shader 实现 RGBA 到 NV21 图像格式转换?(全网首次开源)

    之前写过一篇 OpenGL 使用 shader 实现 RGBA 转 YUYV 的文章,有几位读者大人在后台建议写一篇 shader 实现 RGBA 转 NV21...

    字节流动
  • FFmpeg 播放器视频渲染优化

    前文中,我们已经利用 FFmpeg + OpenGLES + OpenSLES 实现了一个多媒体播放器,本文将在视频渲染方面对播放器进行优化。

    字节流动
  • OpenGL ES实践教程(五)多重纹理实现图像混合

    教程 OpenGL ES实践教程1-Demo01-AVPlayer OpenGL ES实践教程2-Demo02-摄像头采集数据和渲染 OpenGL ES实践...

    落影
  • OpenGLES-07 纹理

    前面的文章都是绘制实实在在的图形的,在OpenGL中,我们还可以使用纹理图片来渲染图形,使用图片可以让描绘出来的物体更加真实也可以让我们的开发更加简单。 资料:...

    清墨
  • 在 iOS 上用 Shader 实现 图片 转 字符画 效果~~

    那天在朋友圈问了一下如何通过 OpenGL Shader 实现同样效果,没想到引来了大神的关注。

    音视频开发进阶
  • Android多媒体之GL-ES战记第一集--勇者集结

    张风捷特烈
  • 人像抠图 + OpenGL ES 还能这样玩?没想到吧

    现在人像分割技术就像当初的人脸检测算法一样,称为广泛使用的基础算法。今天本文介绍的人像留色其实就是三年前某 AI 巨头利用 video 分割技术展示的应用场景:...

    字节流动
  • iOS开发-OpenGLES进阶教程4

    教程 OpenGLES入门教程1-Tutorial01-GLKit OpenGLES入门教程2-Tutorial02-shader入门 OpenGLES入门...

    落影
  • iOS GPUImage源码解读(一)

    最近在不断学习、使用的过程中,有了更深刻的理解,特来写一篇源码解读的文章详细介绍下核心代码的具体实现。

    天天P图攻城狮
  • opengl入门-纹理

    sumsmile
  • 人像抠图 + OpenGL ES 还能这样玩?没想到吧

    今天本文介绍的人像留色其实就是三年前某 AI 巨头利用 video 分割技术展示的应用场景:人体区域保留彩色,人体区域之外灰度化。所以人像留色的关键技术在于高精...

    字节流动
  • 「Android音视频编码那点破事」第一章,使用SurfaceTexture作为Camera输出

      在Android系统中,使用GPU对摄像头画面进行高效可控的渲染,几乎是必须的。说到GPU就不得不提OpenGL,一组GPU暴露给应用层使用的接口。

    阿利民
  • 图片转场和轮播特效,你想要的都在这了

    熟悉的 OpenGL 开发的朋友已经非常了解 GLTransitions 项目,该项目主要用来收集各种 GL 转场特效及其 GLSL 实现代码,开发者可以很方便...

    字节流动
  • 12.QT-通过QOpenGLWidget显示YUV画面,通过QOpenGLTexture纹理渲染YUV

    在上章11.QT-ffmpeg+QAudioOutput实现音频播放器,我们学习了如何播放音频,接下来我们便来学习如何通过opengl来显示YUV画面

    张诺谦

扫码关注腾讯云开发者

领取腾讯云代金券