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 分量。

其中 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 图像的纹理可以简单抽象成如下结构(实际上纹理中的数据不是这样排列的):

为什么宽度是 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); 。

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

同样,生成 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);// 创建并初始化 FBOglGenFramebuffers(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.5glViewport(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);}

OpenGL 实现 RGBA 转 I420相关推荐

  1. RGBA、YUV色彩格式及libyuv的使用

    最近一段时间因为工作的需要,要使用到libyuv.因为之前写录制视频的时候,也要用到rgb转yuv,自己结合网上的资料做了个实现,记录了点笔记,现在索性一起整理下. 常用的色彩格式 常见的色彩格式主要 ...

  2. Android端WebRTC本地音视频采集流程源码分析

    WebRTC源码版本为:org.webrtc:google-webrtc:1.0.32006 本文仅分析Java层源码,在分析之前,先说明一下一些重要类的基本概念. MediaSource:WebRT ...

  3. Unity接入海康网络摄像头(测试无延迟)

    海康威视官网下载最新版本设备网络SDK:http://www.hikvision.com/Cn/download_more_401.html 下载好SDK后,将需要的DLL导入到Unity的Plugi ...

  4. Deepson在Jetson Nano上进行视频分析的入门

    系列文章目录 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 例如:第一章 Python 机器学习入门之pandas的使用 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮 ...

  5. android 屏幕录制方案,ShareREC for Android全系统录屏原理解析

    本文是Mob开发者平台技术副总监余勋杰基于MediaProjection实现Android全系统录屏功能的原理解析,包括了结合MediaRecorder和MediaCodec两套方案. 文 / 余勋杰 ...

  6. 老吕RawViewer下载地址

    一,软件介绍及帮助 老吕RawViewer介绍及使用帮助_老吕丶的博客-CSDN博客 二,下载地址: 1.最新版本V1.1.2下载: (1)V1.1.2主要更新内容: a,处理保存为argb1555图 ...

  7. 自己写的古剑奇谭3D宣传画(哈哈)

    先上图: 也是简单程序,就是加了个背景,然后六个主角的图片作为纹理贴图,覆盖正方体的表面. 貌似旋转起来的时候有部分图案不是很清楚,不过无伤大雅,无伤大雅--(嘿嘿) 用MFC写的,所以贴上view类 ...

  8. OpenGL学习笔记:颜色(RGBA颜色,颜色索引模式)

    OpenGL支持两种颜色模式:一种是RGBA,一种是颜色索引模式. 无论哪种颜色模式,计算机都必须为每一个像素保存一些数据. 不同的是,RGBA模式中,数据直接就代表了颜色:而颜色索引模式中,数据代表 ...

  9. opengl纹理颠倒,rgb通道错位等。详解rgba,bgra,argb等内存序

    一.opengl纹理颠倒的原因 opengl的顶点坐标为范围 [-1,1],纹理坐标范围 [0,1],但是经常遇到这样一个问题,顶点绑定窗口左下角(-1,-1),纹理绑定为左下角(0,0),结果纹理是 ...

最新文章

  1. SpringBoot 用RestTemplate 优雅的发送HTTP请求,注意需要@Autowired注入后才能用
  2. 上班请病假还得看AI脸色,10秒钟判别真假,打工人太难了
  3. 开源 java CMS - FreeCMS2.8 自定义标签 siteOne
  4. VS中 无法创建虚拟目录 本地IIS IIS Express 外部主机
  5. IAR中断定义#pragma vector = P0INT_VECTOR __interrupt void P0_ISR(void)啥意思?
  6. exit()、_Exit() 和 _exit() 函数的区别和联系
  7. 个人对持续集成的理解和实践
  8. 安卓应用安全指南 5.7 使用指纹认证功能
  9. Linux内核设计第四周——扒开系统调用三层皮
  10. 5G大数据技术防控新型肺炎疫情
  11. Linux内核部件分析 设备驱动模型之bus
  12. 谭浩强C语言(第三版)习题9.10
  13. var模型可以用spss做吗_求:如何用SPSS做VAR模型?
  14. 质谱借力ICL平台,静待LDT制度打开更大空间
  15. 晶振与匹配电容的总结
  16. 趣味解析,斗鱼直播大数据的玩法儿
  17. 微信自定义菜单和个性化菜单添加emoji表情(兼容ios和安卓,防止小方框)
  18. KVM远程迁移启动报错
  19. LIteOS学习笔记-7LiteOS启动流程与编译流程
  20. 华为任正非会见马云_什么让华为任正非怒骂“还过个屁年”!连马云、李彦宏都沉思!...

热门文章

  1. 蓝桥杯 算法训练 ALGO-128 Cowboys 递推、动态规划
  2. iOS应用安全读书笔记之Safari书签
  3. 30万条弹幕大军都推荐你去看的《山海情》,是怎样一部最搞笑最土味的扶贫剧
  4. [日常] NOIWC2019 冬眠记
  5. android studio AVD模拟器安装某些app出现 “app not installed(未安装应用程序)”的问题
  6. 云服务AppId或AppKey和AppSecret生成策略(对外接口使用)
  7. 华为获印尼NTS WCDMA商用合同
  8. 【读书笔记】《奇特的一生》
  9. 在计算机的游戏怎样打开,电脑上那个吃鸡游戏怎么打开 | 手游网游页游攻略大全...
  10. 深度学习工作站由于显卡驱动问题导致不能使用GPU