OpenGL ES详解——YUV数据渲染

目录

一、YUV介绍

1. 为什么使用YUV

2. YUV 格式

二、YUV转RGB公式

三、YUV渲染

1. GL_LUMINANCE介绍

2. 获取 YUV 视频

3. 读取YUV文件数据

4. 着色器编写

5. 纹理加载和绘制

四、源码下载


一、YUV介绍

YUV是一种图像颜色编码方式。

相对于常见且直观的RGB颜色编码,YUV的产生自有其意义,它基于人眼对亮度比色彩的敏感度更高的特点,使用Y、U、V三个分量来表示颜色,并通过降低U、V分量的采样率,尽可能保证图像质量的情况下,做到如下3点:

  • 占用更低的存储空间
  • 数据传输效率更高
  • 兼容黑白与彩色显示

在说 yuv 之前,就不得不说 RGB 图像空间,顾名思义,RGB 是值图像的每一个像素都有 R、G,B 三个值,且三个值一次排列存储;但不一定说一定是按照 R,G,B 顺序排列,也可以是 B,G,R 这样的顺序。其中 R,G,B 的位深为 8 bit。
在这里插入图片描述

我们常见的图片处理,都是用 R,G,B 的图像格式,比如bitmap,比如图像的存储,基本使用 R,G,B

1. 为什么使用YUV

我们知道,视频是由一张张图片组成,假设有一个 1920 * 1080 分辨率、帧率为60帧的视频,如果不进行压缩处理,并且使用RGB进行存储的话,仅仅一分钟的视频就能达到 ( 1920 * 1080 * 8 * 60 * 60 )bit (约等于56G),这显然是很夸张的。
但R,G,B这三个颜色是彼此是由相关性的,不利于编码压缩,所以,我们需要另外一种图像格式,来解决图像压缩问题,这个时候,yuv 就被提升来了。

yuv 图像格式将亮度信息 Y 和 色彩信息 UV 分离开来,Y 表示亮度,是图像的总体轮廓,即我们常说的灰度值,UV 表示色度,主要描绘图像的色彩信息,即颜色饱和度。如下图(图片来源wiki百科):

yuv 最早用于电视系统和模拟视频领域,它兼容了黑白电视和彩色电视,如果你家有vcd,dvd 这种设备,就会发现有 YCbCr(YUV) 这种接口,如果是黑白电视,值需要接入Y分量即可。

从很早的时候,人们就发现,人类对亮度信息比较敏感,而对色彩信息不那么敏感,比如我们降低一些颜色值,并不影响人对这张图像感官。因此,yuv 的编码压缩,又可以分为 YUV 4:4:4、YUV 4:2:2、YUV 4:2:0 这几种常用的类型。

2. YUV 格式

YUV 4:4:4、YUV 4:2:2、YUV 4:2:0,指的是U,V 分量像素点的个数和采集方式,其中又以 YUV 4:2:0 最为常用。

可以这样简单理解:

  • YUV 4:4:4:每一个 Y 就对应一个 U 和一个 V分量
  • YUV 4:2:2:每两个 Y 共用一个 U、一个 V 分量
  • YUV 4:2:0:每四个 Y 共用一个 U、V分量

如下图:

其中,YUV 又有不同的存储方式:

  • packed :packed格式是先连续存储所有的Y分量,然后依次交叉储存U、V分量;
  • planar:planar格式也会先连续存储所有的Y分量,但planar会先连续存储U分量的数据,再连续存储V分量的数据,或者先连续存储V分量的数据,再连续存储U分量的数据:

二、YUV转RGB公式

首先我们回顾一下YUV转RGB的公式,之前说过,不同的转换标准,运用不同的公式:

三、YUV渲染

从之前OpenGL 的纹理教程中,我们是把一张图片,通过纹理的方式,传递给片段着色器,最终通过纹理采样,复制给片段颜色值,呈现出来的。
现在使用 YUV ,该如何处理呢?我们知道,视频最终的呈现还是RGB格式的数据,因此,我们需要把 YUV 的数据,所以需要在片段着色器赋值之前,把YUV转换成 RGB。

1. GL_LUMINANCE介绍

在OpenGL 的api 中,可以发现有个 GL_LUMINANCE 格式,它表示只取一个颜色通道,这样的话,就可以把 YUV 拆分成3个通道来读取,然后我们设置 3个纹理,把 YUV 数据传入其中,并最终把这三个通道合并在一起。

2. 获取 YUV 视频

为了方便演示,我们使用 YUV420P 的视频,即4个Y共用一个U,V 分量,且存储是先存储Y,然后是U,最后再存储V分量。
这里我们可以用 ffmepg 的命令,轻松把一个 MP4 的视频转换成 YUV,由于 YUV 比较大,记得修改分辨率,这样小一些:

ffmpeg -i input.mp4 -s 288x512 -r 30 -pix_fmt yuv420p out.yuv

3. 读取YUV文件数据

之后,就可以通过不断读取这个yuv文件,拿到y,u,v的数据,假设视频大小为 wxh ,则先读取 wh 个y,再读取 wh/4 个u,再读取 w*h/4 个 v;一帧读取完后,就进行渲染,然后再重复操作,直到文件被读取完毕。
我们把文件放在 assert 文件夹下:

 /**
  * i420为yuv数据,注意 w,h 为视频或图片宽高
  */
    fun setYuvData(i420: ByteArray, width: Int, height: Int) {
        yBuffer?.clear();
        uBuffer?.clear();
        vBuffer?.clear();
        // 该函数多次被调用的时,不要每次都new,可以设置为全局变量缓存起来
        var y = ByteArray(width * height)
        var u = ByteArray(width * height / 4)
        var v = ByteArray(width * height / 4)
        System.arraycopy(i420, 0, y, 0, y.size);
        System.arraycopy(i420, y.size, u, 0, u.size);
        System.arraycopy(i420, y.size + u.size, v, 0, v.size);
        yBuffer = ByteBuffer.wrap(y);
        uBuffer = ByteBuffer.wrap(u);
        vBuffer = ByteBuffer.wrap(v);
        yuvWidth = width;
        yuvHeight = height;
    }

4. 着色器编写

位置和纹理坐标如下:

    private val POINT_RECT_DATA = floatArrayOf(
        // 前三个数字为顶点坐标(x, y, z),后两个数字为纹理坐标(s, t)
        // 第一个三角形
        0.8f, 0.5f, 0f, 1f, 0f,
        0.8f, -0.5f, 0f, 1f, 1f,
        -0.8f, -0.5f, 0f, 0f, 1f,
        // 第二个三角形
        0.8f, 0.5f, 0f, 1f, 0f,
        -0.8f, -0.5f, 0f, 0f, 1f,
        -0.8f, 0.5f, 0f, 0f, 0f
    )

顶点着色器中,获取位置和纹理坐标数据:

#version 300 es

layout(location = 0) in vec4 a_Position;
layout(location = 1) in vec2 a_TextureCoordinates;

out vec2 texture_coord;

void main() {
    gl_Position = a_Position;
    texture_coord = a_TextureCoordinates;
}

片段着色器中,设置三个纹理,用来读取 yuv分量的数据:

#version 300 es

precision mediump float;

in vec2 texture_coord;
layout(location = 0) uniform sampler2D sampler_y;
layout(location = 1) uniform sampler2D sampler_u;
layout(location = 2) uniform sampler2D sampler_v;

out vec4 out_color;

void main() {
    float y = texture(sampler_y, texture_coord).r;
    float u = texture(sampler_u, texture_coord).g- 0.5;
    float v = texture(sampler_v, texture_coord).b- 0.5;

    vec3 rgb;
    rgb.r = y + 1.540*v;
    rgb.g = y - 0.183*u - 0.459*v;
    rgb.b = y + 1.818*u;
    out_color = vec4(rgb, 1);
}

可以看到,我们使用了三个纹理textureY,textureU,textureV,然后用了三个变量 y,u,v 用来接收纹理数据。
前面说到,OpenGL 的分量,除了包含位置信息{x,y,z,w},还有颜色(r,g,b,a)和纹理信息(s,t,r,q):

  • x,y,z,w: 与位置相关的分量
  • r,g,b,a: 与颜色相关的分量
  • s,t,p,q: 与纹理坐标相关的分量

当我们设置 sampler2D 的类型为 GL_LUMINANCE,所以 texture().r 拿到的是yuv 的第一个颜色向量的第一个分量信息,就是y;

那这个 0.5 是什么?为啥要减去它?
先看到YUV与RGB 的转换公式,这里用高清模式(BT709),颜色空间为 Limited Range 的转换公式:

可以看到,有个转换偏差值,而 U,V 默认是127 ,Y 的偏移量为0。8 个 bit 位的取值范围是 0 ~ 255,由于在 shader 中纹理采样值需要进行归一化(注意,纹理的范围是[0,1]),所以 UV 分量的采样值需要分别减去 0.5 ,确保 YUV 到 RGB 正确转换。

5. 纹理加载和绘制

编写完着色器,就可以编写纹理对象了。首先,设置纹理的下标:

private val textures = IntArray(3)
 //三个纹理,需要设置纹理的下标
GLES30.glUniform1i(mShaderProgram!!.getTextureYLocation(), 0)
GLES30.glUniform1i(mShaderProgram!!.getTextureULocation(), 1)
GLES30.glUniform1i(mShaderProgram!!.getTextureVLocation(), 2)

设置纹理的对象:


GLES30.glGenTextures(3, textures, 0)
for (i in 0..2) {
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[i])

    //纹理环绕
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT)
    GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT)

    //纹理过滤
    GLES30.glTexParameteri(
        GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MIN_FILTER,
        GLES30.GL_NEAREST
    )
    GLES30.glTexParameteri(
        GLES30.GL_TEXTURE_2D,
        GLES30.GL_TEXTURE_MAG_FILTER,
        GLES30.GL_LINEAR
    )

    //解绑纹理对象
    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0)
}

前面3.章节,已经拿到了 yuv 的数据,这里,我们使用 glTexImage2D 把数据设置给纹理

    override fun onDrawFrame(gl: GL10?) {
        if (yBuffer == null || uBuffer == null || vBuffer == null) {
            return;
        }
        GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT)
        //1.加载纹理y
        GLES30.glActiveTexture(GLES30.GL_TEXTURE0); //激活纹理0
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[0]); //绑定纹理
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth,
            yuvHeight, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, yBuffer
        )// 赋值
        // sampler_y的location=0, 把纹理0赋值给sampler_y
        GLES30.glUniform1i(mShaderProgram!!.getTextureYLocation(),0); 
        //2.加载纹理u
        GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[1]);
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth / 2,
            yuvHeight / 2, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, uBuffer
        );
        // sampler_u的location=1, 把纹理1赋值给sampler_u
        GLES30.glUniform1i(mShaderProgram!!.getTextureULocation(),1); 

        // 3.加载纹理v
        GLES30.glActiveTexture(GLES30.GL_TEXTURE2);
        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textures[2]);
        GLES30.glTexImage2D(
            GLES30.GL_TEXTURE_2D, 0, GLES30.GL_LUMINANCE, yuvWidth / 2,
            yuvHeight / 2, 0, GLES30.GL_LUMINANCE, GLES30.GL_UNSIGNED_BYTE, vBuffer
        );
        // sampler_v的location=2, 把纹理1赋值给sampler_v
        GLES30.glUniform1i(mShaderProgram!!.getTextureVLocation(),2); 
        // 4.绘制
        GLES30.glDrawArrays(
            GLES30.GL_TRIANGLES,
            0,
            POINT_RECT_DATA.size / (POSITION_COMPONENT_COUNT + TEXTURE_COORDINATES_COMPONENT_COUNT)
        );
    }
效果图如下:

四、源码下载

Android OpenGL ES创建绘制YUV格式图片和视频项目源码

下载地址:

https://download.youkuaiyun.com/download/github_27263697/90113852

参考文章

Android OpenGL ES 学习(十一) –渲染YUV视频以及视频抖音特效_android 视频渲染-优快云博客

Android使用OpenGL 3.0绘制yuv图片示例_android opengl绘制yuv-优快云博客

https://juejin.cn/post/7160304816877469733

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闲暇部落

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值