前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >使用 OpenGL 实现 RGB 到 YUV 的图像格式转换

使用 OpenGL 实现 RGB 到 YUV 的图像格式转换

原创
作者头像
字节流动
修改于 2021-05-27 02:41:36
修改于 2021-05-27 02:41:36
7.7K0
举报
文章被收录于专栏:字节流动字节流动
面试官:请使用 OpenGL 实现 RGB 到 YUV 的图像格式转换。我 ……
面试官:请使用 OpenGL 实现 RGB 到 YUV 的图像格式转换。我 ……

最近,有位读者大人在后台反馈:在参加一场面试的时候,面试官要求他用 shader 实现图像格式 RGB 转 YUV ,他听了之后一脸懵,然后悻悻地对面试官说,他只用 shader 做过 YUV 转 RGB,不知道 RGB 转 YUV 是个什么思路。

针对他的这个疑惑,今天专门写文章介绍一下如何使用 OpenGL 实现 RGB 到 YUV 的图像格式转换,帮助读者大人化解此类问题。

YUV 看图工具推荐

有读者大人让推荐一个 YUV 看图软件,由于手头的工具没法分享出来,又在 Github 上找了一圈发现这一类开源软件用起来都不少 BUG 。

YUV 看图工具
YUV 看图工具

最后发现一款免费的商业软件 YUV Viewer ,用起来还行。

代码语言:txt
AI代码解释
复制
https://www.elecard.com/products/video-analysis/yuv-viewer

就是下载起来比较慢,我这里给读者大人已经下载好了,【字节流动】 后台回复关键字 yuvViewer 即可获取。

好处

使用 shader 实现 RGB 到 YUV 的图像格式转换有什么使用场景呢?在生产环境中使用极为普遍。

前文曾经介绍过 Android OpenGL 渲染图像的读取方式,分别是 glReadPixels、 PBO、 ImageReader 以及 HardwareBuffer 。

glReadPixels 大家经常用来读取 RGBA 格式的图像,那么我用它来读取 YUV 格式的图像行不行呢?答案是肯定的,这就要用到 shader 来实现 RGB 到 YUV 的图像格式转换。

glReadPixels 性能瓶颈一般出现在大分辨率图像的读取,在生产环境中通用的优化方法是在 shader 中将处理完成的 RGBA 转成 YUV (一般是 YUYV),然后基于 RGBA 的格式读出 YUV 图像,这样传输数据量会降低一半,性能提升明显,不用考虑兼容性问题。

YUV 转 RGB

这一节先做个铺垫简单介绍下 YUV 转 RGB 实现,在前面的文章中曾经介绍过 OpenGL 实现 YUV 的渲染,实际上就是利用 shader 实现了 YUV(NV21) 到 RGBA 的转换,然后渲染到屏幕上。

以渲染 NV21 格式的图像为例,下面是 (4x4) NV21 图像的 YUV 排布:

代码语言:txt
AI代码解释
复制
(0  ~  3) Y00  Y01  Y02  Y03  
(4  ~  7) Y10  Y11  Y12  Y13  
(8  ~ 11) Y20  Y21  Y22  Y23  
(12 ~ 15) Y30  Y31  Y32  Y33  

(16 ~ 19) V00  U00  V01  U01 
(20 ~ 23) V10  U10  V11  U11

YUV 渲染步骤:

  • 生成 2 个纹理,编译链接着色器程序;
  • 确定纹理坐标及对应的顶点坐标;
  • 分别加载 NV21 的两个 Plane 数据到 2 个纹理,加载纹理坐标和顶点坐标数据到着色器程序;
  • 绘制。

片段着色器脚本:

代码语言:txt
AI代码解释
复制
#version 300 es                                     
precision mediump float;                            
in vec2 v_texCoord;                                 
layout(location = 0) out vec4 outColor;             
uniform sampler2D y_texture;                        
uniform sampler2D uv_texture;                        
void main()                                         
{                                                   
vec3 yuv;										
yuv.x = texture(y_texture, v_texCoord).r;  	
yuv.y = texture(uv_texture, v_texCoord).a-0.5;	
yuv.z = texture(uv_texture, v_texCoord).r-0.5;	
vec3 rgb =mat3( 1.0,       1.0,       	1.0,					
                0.0, 		-0.344, 	1.770,					
                1.403,  -0.714,       0.0) * yuv; 			
outColor = vec4(rgb, 1);						
}

y_texture 和 uv_texture 分别是 NV21 Y Plane 和 UV Plane 纹理的采样器,对两个纹理采样之后组成一个(y,u,v)三维向量,之后左乘变换矩阵转换为(r,g,b)三维向量。

上面 YUV 转 RGB shader 中,面试官喜欢问的问题(一脸坏笑):为什么 UV 分量要减去 0.5 啊?

(迷之自信)答曰:因为归一化。YUV 格式图像 UV 分量的默认值分别是 127 ,Y 分量默认值是 0 ,8 个 bit 位的取值范围是 0 ~ 255,由于在 shader 中纹理采样值需要进行归一化,所以 UV 分量的采样值需要分别减去 0.5 ,确保 YUV 到 RGB 正确转换。

需要注意的是 OpenGL ES 实现 YUV 渲染需要用到 GL_LUMINANCE 和 GL_LUMINANCE_ALPHA 格式的纹理,其中 GL_LUMINANCE 纹理用来加载 NV21 Y Plane 的数据,GL_LUMINANCE_ALPHA 纹理用来加载 UV Plane 的数据,这一点很重要,初学的读者大人请好好捋一捋。

关于 shader 实现 YUV 转 RGB (NV21、NV12、I410 格式图像渲染)可以参考文章: OpenGL ES 3.0 开发(三):YUV 渲染FFmpeg 播放器视频渲染优化,本文主要重点讲 shader 如何实现 RGB 转 YUV 。

RGB 转 YUV

来到本文的重点,那么如何利用 shader 实现 RGB 转 YUV 呢?

前面小节已经提到,先说下一个简单的思路:先将 RGBA 按照公式转换为 YUV 如(YUYV),然后将 YUYV 按照 RGBA 进行排布,最后使用 glReadPixels 读取 YUYV 数据,由于 YUYV 数据量为 RGBA 的一半,需要注意输出 buffer 的大小,以及 viewport 的宽度(宽度为原来的一半)。

RGB to YUV 的转换公式:

RGB to YUV 的转换公式
RGB to YUV 的转换公式

开门见山,先贴实现 RGBA 转 YUV 的 shader 脚本:

代码语言:txt
AI代码解释
复制
#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D s_TextureMap;//RGBA 纹理
uniform float u_Offset;//采样偏移

//RGB to YUV
//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);

void main()
{
    vec2 texelOffset = vec2(u_Offset, 0.0);
    vec4 color0 = texture(s_TextureMap, v_texCoord);
    //偏移 offset 采样
    vec4 color1 = texture(s_TextureMap, v_texCoord + texelOffset);

    float y0 = dot(color0.rgb, COEF_Y);
    float u0 = dot(color0.rgb, COEF_U) + 0.5;
    float v0 = dot(color0.rgb, COEF_V) + 0.5;
    float y1 = dot(color1.rgb, COEF_Y);

    outColor = vec4(y0, u0, y1, v0);
}

shader 实现 RGB 转 YUV 原理图:

shader 实现 RGB 转 YUV 原理图
shader 实现 RGB 转 YUV 原理图

我们要将 RGBA 转成 YUYV,数据量相比于 RGBA 少了一半,这就相当于将两个像素点合并成一个像素点。

如图所示,我们在 shader 中执行两次采样,RGBA 像素(R0,G0,B0,A0)转换为(Y0,U0,V0),像素(R1,G1,B1,A1)转换为(Y1),然后组合成(Y0,U0,Y1,V0),这样 8 个字节表示的 2 个 RGBA 像素就转换为 4 个字节表示的 2 个 YUYV 像素。

转换成 YUYV 时数据量减半,那么 glViewPort 时 width 变为原来的一半,同样 glReadPixels 时 width 也变为原来的一半。

实现 RGBA 转成 YUYV 要保证原图分辨率不变,建议使用 FBO 离屏渲染 ,这里注意绑定给 FBO 的纹理是用来容纳 YUYV 数据,其宽度应该设置为原图的一半。

代码语言:txt
AI代码解释
复制
bool RGB2YUVSample::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 纹理是用来容纳 YUYV 数据,其宽度应该设置为原图的一半
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, m_RenderImage.width / 2, m_RenderImage.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
	if (glCheckFramebufferStatus(GL_FRAMEBUFFER)!= GL_FRAMEBUFFER_COMPLETE) {
		LOGCATE("RGB2YUVSample::CreateFrameBufferObj glCheckFramebufferStatus status != GL_FRAMEBUFFER_COMPLETE");
		return false;
	}
	glBindTexture(GL_TEXTURE_2D, GL_NONE);
	glBindFramebuffer(GL_FRAMEBUFFER, GL_NONE);
	return true;

}

离屏渲染和读取 YUYV 数据:

代码语言:txt
AI代码解释
复制
glBindFramebuffer(GL_FRAMEBUFFER, m_FboId);

// 渲染成 yuyv 宽度像素减半,glviewport 宽度减半
glViewport(0, 0, m_RenderImage.width / 2, m_RenderImage.height);

glUseProgram(m_FboProgramObj);
glBindVertexArray(m_VaoIds[1]);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_ImageTextureId);
glUniform1i(m_FboSamplerLoc, 0);

//参考原理图,偏移量应该设置为 1/(width / 2) * 1/2 = 1 / width; 理论上纹素的一半
float texelOffset = (float) (1.f / (float) m_RenderImage.width);
GLUtils::setFloat(m_FboProgramObj, "u_Offset", texelOffset);

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

//YUYV buffer = width * height * 2; 转换成 YUYV 时数据量减半,注意 buffer
uint8_t *pBuffer = new uint8_t[m_RenderImage.width * m_RenderImage.height * 2];

NativeImage nativeImage = m_RenderImage;
nativeImage.format = IMAGE_FORMAT_YUYV;
nativeImage.ppPlane[0] = pBuffer;

//glReadPixels 时 width 变为原来的一半
glReadPixels(0, 0, m_RenderImage.width / 2, nativeImage.height, GL_RGBA, GL_UNSIGNED_BYTE, pBuffer);

DumpNativeImage(&nativeImage, "/sdcard/DCIM");
delete []pBuffer;

glBindFramebuffer(GL_FRAMEBUFFER, 0);

完整代码参考下面项目,选择 OpenGL RGB to YUV demo:

代码语言:txt
AI代码解释
复制
https://github.com/githubhaohao/NDK_OpenGLES_3_0

那么面试官的问题又来了(一脸坏笑): RGBA 转 YUV 的 shader 中 uv 分量为什么要加 0.5 ? 请读者大人结合上面文章给予有力回击。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
⭐️ 关键字深度剖析 ⭐️第二章(多文件\数据类型&关键字extern/static)
第三章传送门:⭐️ 关键字深度剖析 ⭐️第三章(关键字sizeof\signed\unsigned&原反补\数据范围)
用户9645905
2022/11/30
2640
⭐️ 关键字深度剖析 ⭐️第二章(多文件\数据类型&关键字extern/static)
extern、定义和声明
    整体是函数的定义  ,函数的定义没有分号 而且要加上一对花括号 ,里边是函数的实现。
Twcat_tree
2022/11/30
1.3K0
extern、定义和声明
申明与定义的区别
C++编码过程中,我们经常谈及“定义”和“声明”,二者是编程过程中的基本概念。我们需要使用一个变量、类型(类、结构体、枚举、共用体)或者函数时,我们需要提前定义和申明。定义和申明的过程,就像我们向图书馆借阅书籍一般,需要先完成书籍的印刷,即创造出书籍,这是一个定义的过程,有了书籍,我们需要到图书馆完成借阅的登记手续,这是申明的过程。完成了申明,我们有了使用书籍的权限,就可以尽情的畅游在知识的海洋。如果说书籍是自己委托印刷厂印刷的,那么你无需向他人借阅,即无需声明,可以直接使用书籍。一本书籍只需要印刷一次,但是可以被多人多次借阅,也就是说定义只需要一次,但是申明可以有多次。这里的书籍指代的是“定义”和“声明”作用的对象,即变量、类型和函数。C/C++中,使用一个变量、类型或者函数必须先在使用前完成定义和申明。
恋喵大鲤鱼
2018/08/03
1.6K0
extern关键字
extern是c++引入的一个关键字,它可以应用于一个全局变量,函数或模板声明,说明该符号具有外部链接(external linkage)属性。也就是说,这个符号在别处定义。一般而言,C++全局变量的作用范围仅限于当前的文件,但同时C++也支持分离式编译,允许将程序分割为若干个文件被独立编译。于是就需要在文件间共享数据,这里extern就发挥了作用。
司空
2022/09/08
2.7K0
【C语言】C语言的变量和声明系统性讲解
在C语言中,**声明(Declaration)和定义(Definition)**是两个重要的基础概念,它们都涉及到变量、函数、结构体等的使用,但功能和作用存在明显区别:
LuckiBit
2024/12/11
2280
【C语言】C语言的变量和声明系统性讲解
值得一看:老外的嵌入式编程规范
作为维护人员,如果收到的补丁明显与周围代码的编码风格不同,这是令人沮丧的。这是不尊重人的,就像某人穿着泥泞的鞋子走进一间一尘不染的房子。
AI 电堂
2021/07/02
1.3K0
【深入浅出C#】章节 2:数据类型和变量:变量和常量的声明和初始化
变量在程序中扮演着重要的角色。它们用于存储和操作数据,为程序提供了灵活性和可扩展性。通过变量,我们可以方便地存储和访问不同类型的数据,如整数、浮点数、字符串等。变量还允许数据在程序的不同部分之间进行传递和共享,实现数据的交流和共享。同时,变量也用于对数据进行各种操作和计算,如算术运算、逻辑判断等,实现对数据的处理和转换。此外,变量还可以用于跟踪程序的状态和条件,根据不同的条件执行不同的操作或决策,实现程序的流程控制和逻辑控制。变量的可变性和灵活性使得程序的行为可以随着变量的值的改变而调整,满足不同的需求和条件。合理地管理变量可以提高程序的内存利用率,避免内存泄漏和资源浪费。因此,了解变量在程序中的作用和重要性对于设计和编写高质量、高效率的程序至关重要。
喵叔
2023/07/09
7420
C++面试高频考点:inline的深层理解与实战应用
您好,我是昊天,国内某头部音频公司的C++主程,多年的音视频开发经验,熟悉Qt、FFmpeg、OpenGL。
程序员的园
2025/04/10
1280
C++面试高频考点:inline的深层理解与实战应用
C语言深入理解extern用法 | 变量声明 | static
我们一般把所有的全局变量和全局函数的实现都放在一个*.cpp文件里面,然后用一个同名的*.h文件包含所有的函数和变量的声明。如:
CtrlX
2022/11/22
1.9K0
万字长文带你掌握C++11中auto和decltype的用法和区别
在 C++11 之前的版本(C++98 和 C++ 03)中,定义变量或者声明变量之前都必须指明它的类型,比如 int、char 等;但是在一些比较灵活的语言中,比如 C#、JavaScript、PHP、Python 等,程序员在定义变量时可以不指明具体的类型,而是让编译器(或者解释器)自己去推导,这就让代码的编写更加方便。
C语言与CPP编程
2024/04/26
7230
万字长文带你掌握C++11中auto和decltype的用法和区别
命名空间重定义问题
命名空间只不过是为了不让名字重复,而在两个cpp里面都调用了头文件,相当于这些变量被定义了两次,就类似于你是这样写的
无敌清风蓝
2024/11/12
1240
C++学习总结1——几个基本概念
最近我在做毕设。写程序的时候,总是被C++里面的指针搞得头昏脑胀。刚开始的时候还有些浮躁,不想静下心来仔细看看指针使用的细节。过了几天发现只在Visual Studio里面调试怎么也搞不定,只好硬着头皮,重新学习指针的用法。在看书和看别人写的博客后,感觉学到了许多新的东西,不光是关于指针,还有其他一些以前我不太清楚的内容。这些知识如果不常用或不记录下来的话,肯定会忘掉的,所以我就把它们都写下来,避免以后犯同样的错误。
王云峰
2019/12/25
5600
C++学习总结1——几个基本概念
[Modern CPP]内联变量——保证变量唯一性的利器
在 C++ 中,一个定义了全局变量的头文件被多个源文件包含时,每个源文件都会创建该全局变量的一个实例,这可能导致链接时出现重定义错误,代码示例如下。
程序员的园
2024/07/18
1350
[Modern CPP]内联变量——保证变量唯一性的利器
C++ 变量类型
变量其实只不过是程序可操作的存储区的名称。C++ 中每个变量都有指定的类型,类型决定了变量存储的大小和布局,该范围内的值都可以存储在内存中,运算符可应用于变量上。
iOS程序应用
2023/05/19
3500
【编程基础】extern "C"的用法解析
1.引言   C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。 2.从标准头文件说起 某企业曾经给出如下的一道面试题: 为什么标准头文件都有类似以下的结构? #if
程序员互动联盟
2018/03/13
1K0
声明和定义的区别(深入理解)
问题 声明和定义区别 definition declared 微信排版支持makdown语法不友好 可以查看原文链接 先看一下 例子1 编译有没有问题? class B{private: A* m_pData;}; 编译很明显出现错误::没有 声明 error: ‘A’ has not been declared error: ‘A’ does not name a type 例子2 编译有没有问题 class A;class B{private: A* m_pData;}; 编译没
早起的鸟儿有虫吃
2018/04/13
1.4K0
深入理解extern使用方法
我们一般把全部的全局变量和全局函数的实现都放在一个*.cpp文件中面,然后用一个同名的*.h文件包括全部的函数和变量的声明。如:
全栈程序员站长
2022/07/07
4970
[C语言] 常量与变量
说明:系统会在字符串尾部添加一个字符串作为结束字符"\0",那么如图所示的长度实际上是6的原因! 
謓泽
2022/12/12
7690
[C语言] 常量与变量
lnk2001 lnk1120_lnk1120
学习VC++时经常会遇到链接错误LNK2001,该错误非常讨厌,因为对于编程者来说,最好改的错误莫过于编译错误,而一般说来发生连接错误时,编译都已通过。产生连接错误的原因非常多,尤其LNK2001错误,常常使人不明其所以然。如果不深入地学习和理解VC++,要想改正连接错误LNK2001非常困难。
全栈程序员站长
2022/11/08
1.1K0
C++编译与链接(0)-.h与.cpp中的定义与声明
C++中有的东西需要放在可以在.h文件中定义,有的东西则必须放在.cpp文件中定义,有的东西在不同的cpp文件中的名字可以一样,而有的则不能一样 那么究竟哪些东西可在头文件中定义,声明,哪些东西又必须
magicsoar
2018/02/06
3.7K0
C++编译与链接(0)-.h与.cpp中的定义与声明
推荐阅读
相关推荐
⭐️ 关键字深度剖析 ⭐️第二章(多文件\数据类型&关键字extern/static)
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档