前言
在上一篇理论文章中我们介绍了YUV到RGB之间转换的几种公式与一些优化算法,今天我们再来介绍一下RGB到YUV的转换,顺便使用Opengl ES做个实践,将一张RGB的图片通过Shader
的方式转换YUV格式图,然后保存到本地。
可能有的童鞋会问,YUV转RGB是为了渲染显示,那么RGB转YUV的应用场景是什么?在做视频编码的时候我们可以使用MediaCodec搭配Surface就可以完成,貌似也没有用到RGB转YUV的功能啊,
硬编码没有用到,那么软编码呢?一般我们做视频编码的时候都是硬编码优先,软编码兜底的原则,在遇到一些硬编码不可用的情况下可能就需要用到x264库进行软编码了,而此时RGB转YUV可能就派上用场啦。
RGB到YUV的转换公式
在前面 Opengl ES之YUV数据渲染 一文中我们介绍过YUV的几种兼容标准,下面我们看看RGB到YUV的转换公式:
RGB 转 BT.601 YUV
Y = 0.257R + 0.504G + 0.098B + 16
Cb = -0.148R - 0.291G + 0.439B + 128
Cr = 0.439R - 0.368G - 0.071B + 128
RGB 转 BT.709 YUV
Y = 0.183R + 0.614G + 0.062B + 16
Cb = -0.101R - 0.339G + 0.439B + 128
Cr = 0.439R - 0.399G - 0.040B + 128
或者也可以使用矩阵运算的方式进行转换,更加的便捷:
RGB转YUV
先说一下RGB转YUV的过程,先将RGB数据按照公式转换为YUV数据,然后将YUV数据按照RGBA进行排布,这一步的目的是为了后续数据读取,最后使用glReadPixels
读取YUV数据。
而对于OpenGL ES来说,目前它输入只认RGBA、lumiance、luminace alpha这几个格式,输出大多数实现只认RGBA格式,因此输出的数据格式虽然是YUV格式,但是在存储时我们仍然要按照RGBA方式去访问texture数据。
以NV21的YUV数据为例,它的内存大小为width x height * 3 / 2
。如果是RGBA的格式存储的话,占用的内存空间大小是width x height x 4
(因为 RGBA 一共4个通道)。很显然它们的内存大小是对不上的,
那么该如何调整Opengl buffer的大小让RGBA的输出能对应上YUV的输出呢?我们可以设计输出的宽为width / 4
,高为height * 3 / 2
即可。
为什么是这样的呢?虽然我们的目的是将RGB转换成YUV,但是我们的输入和输出时读取的类型GLenum是依然是RGBA,也就是说:width x height x 4 = (width / 4) x (height * 3 / 2) * 4
而YUV数据在内存中的分布以下这样子的:
width / 4
|--------------|
| |
| | h
| Y |
|--------------|
| U | V |
| | | h / 2
|--------------|
那么上面的排序如果进行了归一化之后呢,就变成了下面这样子了:
(0,0) width / 4 (1,0)
|--------------|
| |
| | h
| Y |
|--------------| (1,2/3)
| U | V |
| | | h / 2
|--------------|
(0,1) (1,1)
从上面的排布可以看出看出,在纹理坐标y < (2/3)
时,需要完成一次对整个纹理的采样,用于生成Y数据,当纹理坐标 y > (2/3)
时,同样需要再进行一次对整个纹理的采样,用于生成UV的数据。
同时还需要将我们的视窗设置为glViewport(0, 0, width / 4, height * 1.5);
由于视口宽度设置为原来的 1/4 ,可以简单的认为相对于原来的图像每隔4个像素做一次采样,由于我们生成Y数据是要对每一个像素都进行采样,所以还需要进行3次偏移采样。
同理,生成对于UV数据也需要进行3次额外的偏移采样。
在着色器中offset变量需要设置为一个归一化之后的值:1.0/width
, 按照原理图,在纹理坐标 y < (2/3) 范围,一次采样(加三次偏移采样)4 个 RGBA 像素(R,G,B,A)生成 1 个(Y0,Y1,Y2,Y3),整个范围采样结束时填充好 width*height
大小的缓冲区;
当纹理坐标 y > (2/3) 范围,一次采样(加三次偏移采样)4 个 RGBA 像素(R,G,B,A)生成 1 个(V0,U0,V0,U1),又因为 UV 缓冲区的高度为 height/2 ,VU plane 在垂直方向的采样是隔行进行,整个范围采样结束时填充好 width*height/2
大小的缓冲区。
主要代码
RGBtoYUVOpengl.cpp
#include "../utils/Log.h"
#include "RGBtoYUVOpengl.h"
// 顶点着色器
static const char *ver = "#version 300 es\n"
"in vec4 aPosition;\n"
"in vec2 aTexCoord;\n"
"out vec2 v_texCoord;\n"
"void main() {\n"
" v_texCoord = aTexCoord;\n"
" gl_Position = aPosition;\n"
"}";
// 片元着色器
static const char *fragment = "#version 300 es\n"
"precision mediump float;\n"
"in vec2 v_texCoord;\n"
"layout(location = 0) out vec4 outColor;\n"
"uniform sampler2D s_TextureMap;\n"
"uniform float u_Offset;\n"
"const vec3 COEF_Y = vec3(0.299, 0.587, 0.114);\n"
"const vec3 COEF_U = vec3(-0.147, -0.289, 0.436);\n"
"const vec3 COEF_V = vec3(0.615, -0.515, -0.100);\n"
"const float UV_DIVIDE_LINE = 2.0 / 3.0;\n"
"void main(){\n"
" vec2 texelOffset = vec2(u_Offset, 0.0);\n"
" if (v_texCoord. y <= UV_DIVIDE_LINE) {\n"
" vec2 texCoord = vec2(v_texCoord. x, v_texCoord. y * 3.0 / 2.0);\n"
" vec4 color0 = texture(s_TextureMap, texCoord);\n&