什么是YUV
HDR视频是10位或12位YUV,OpenGL不支持10位YUV纹理需要特殊处理,本篇文章讲的就是HDR转SDR第一步解码10位YUV纹理的方法(12位同理)。如果你觉得有所收获,来给HDR转SDR开源代码点个赞吧,你的鼓励是我前进最大的动力。
YUV是为了解决彩色电视和黑白电视的兼容问题发明的。下图中右边的RGB图像和左边的YUV三通道图像等价,黑白电视机只需要Y亮度通道,UV是色度(U是蓝色与亮度的差量,V是红色与亮度的差量),这也是为什么YUV又被叫做YCbCr的原因,Cb是ColorBlue的缩写,Cr是ColorRed的缩写。
下图所示是电视机上YCbCr的接口,只接Y显示黑白。
UV是蓝红色度,那么绿色去哪里了?绿色在Y通道里,如下图所示Y通道就是一定比例的红绿蓝混合而成。为什么一定比例的红绿蓝混合变成了黑白色,因为红绿蓝光的比例虽然不同,但是三个感光细胞的电脉冲信号强度一样,电脉冲信号强度一样看起来就是黑白色。不是说RGB三光混合变成了白光而是眼睛看起来像白光,这也是为什么Y亮度更适合叫做Y明度的原因,明度比亮度更适合表示生理感受(为了方便,后续Y还是叫做亮度)。
MediaCodec解码10位YUV流程
MediaCodec解码10位YUV有3种方案,最终都是为了得到归一化的RGB纹理。
方案1: 解码到SurfaceTexture,SurfaceTexture与samplerExternalOES纹理采样器绑定得到归一化非线性的RGB数据
方案2: 解码到SurfaceTexture,SurfaceTexture与samplerExternal2DY2YEXT纹理采样器绑定得到归一化非线性的YUV数据,再用BT2020YUV转RGB矩阵转换成归一化非线性的RGB纹理,需GL_EXT_YUV_target扩展支持
方案3: 解码出16位YUV420Buffer(10位YUV是16位存储的),上传到纹理后经Shader处理得到归一化的RGB纹理(先把YUV420Buffer上传到16位纹理中,再用Shader从YUV420转换成YUV,然后右移6位得到10位YUV,再进行YUV转RGB转换得到10位RGB归一化纹理)。
方案 | 优点 | 缺点 |
---|---|---|
方案1:解码到samplerExternalOES绑定的SurfaceTexture | 代码简单 | 虽然samplerExternalOES只支持8位,但是高8位归一化后最多只差也可视为支持10位 |
方案2:解码到samplerExternal2DY2YEXT绑定的SurfaceTexture | 代码简单 | 需GL_EXT_YUV_target扩展支持 |
方案3:解码出16位YUV420Buffer再用Shader转换 | 自己处理流程可控 | 1. 不一定支持解码出16位Buffer(测试发现解出16位的手机大都是晓龙中高端机,华为的麒麟芯片不支持) 2. 代码略繁琐 |
3个方案各有利弊互补解决兼容性问题,从代码方便程度上方案1>方案2>方案3,从兼容性程度上方案3>方案1>方案2。
10位YUV存储
实现10位YUV纹理之前还要解决一个问题,10位YUV的Buffer数据是用16位存储的,还要实现16位YUV转10位YUV,因为字节对齐后处理方便还能加快读取速度,10位也就是1.25字节,对齐后就是2字节(16位)。10位变16位多出来的位数补0就可以了,大端情况下0补在前面数据不会发生变化,小端情况下0补在后面导致数据左移需要右移回来。
如上图所示YUV16位十进制479大端情况下还是479,小端右边补6位变成30656(479*2^6),16位YUV变成10位YUV需要右移6位去除0。上图中看到正常的小端数据和YUV的小端数据是不一样的,那么为什么YUV小端不用正常小端存储,这样不是更简单吗,我的猜测是为了保证16位数据被当成8位归一化和16位直接归一化的数据差距最小(YUV小端右边8位保留着原数据的高8位丢弃低位归一化后和16位归一化最多就差)。
10位YUV纹理方案
OpenGL本身不支持YUV纹理只支持RGB纹理需要特殊扩展或者自己处理,下面3个方案为了方便用伪代码讲解。
方案一解码到samplerExternalOES绑定的SurfaceTexture
第一步: Mediacodec用Configure方法配置Surfacetexture,解码的数据传递到Surfacetexture
第二步: SurfaceTexture在OpenGL环境中调用attachToGLContext绑定纹理,updateTexImage方法把SurfaceTexture的内容更新到纹理。
第三步: 纹理采样器samplerExternalOES和SurfaceTexture的纹理绑定,samplerExternalOES是OpenGL的扩展支持把YUV转成RGB
samplerExternalOES支持YUV转RGB是毫无疑问的,那么samplerExternalOES支持10位BT2020YUV转RGB吗?经过测试发现把RGB打印出来非常接近正常流程转出来的RGB,误差可以忽略不计,所以排除机型兼容和精度误差情况下,samplerExternalOES可以视为支持BT2020YUV转RGB。
方案二解码到samplerExternal2DY2YEXT绑定的SurfaceTexture
注意:
第一步: Mediacodec使用Configure方法配置Surfacetexture,解码的数据会传递到Surfacetexture
第二步: SurfaceTexture在OpenGL环境中调用attachToGLContext绑定纹理,updateTexImage方法把SurfaceTexture的内容更新到纹理。
第三步: 纹理采样器samplerExternal2DY2YEXT yuvtexture和SurfaceTexture的纹理绑定,samplerExternal2DY2YEXT是OpenGLGL_EXT_YUV_target扩展的YUV采样器(支持直接YUV插值),注意使用要判断手机是否支持GL_EXT_YUV_target扩展。
第四步: GL_EXT_YUV_target扩展只支持BT709YUV转RGB,需要通过BT2020YUV转RGB矩阵转换成归一化RGB。
方案三解码出16位YUV420Buffer再用Shader转换
这种方式是略繁琐的需要处理很多逻辑,不过也因为代码是自己写的,效果可控兼容性最高。
步骤1:解码16位YUV420ByteBuffer
备注:
- 解码到Buffer时configure第二个参数要传null,解码到Surface时传Surface
- 配置KEY_COLOR_FORMAT标识视频是什么颜色格式,只是一个标识不是说配置什么格式就一定输出什么格式,大部分情况视频是什么格式就会输出什么格式,不传也不行部分手机会崩溃,android13以后COLOR_FormatYUVP010代表10位YUV420,13以前用COLOR_FormatYUV420Flexible代表了4种8位YUV420格式。
- 解码出来的数据不一定是16位还有可能是8位,8位不一定表示硬件不支持HDR,只是手机内部使用特殊的ColorFormat标识成8位就输出8位Buffer,测试发现解出16位的手机大都是晓龙中高端机,华为的麒麟芯片不支持,暂时没有方法绕过,但是就像前面说的16位YUV小端取原数据高8位归一化和16位YUV归一化相差很少。YUV的位数可以通过来判断,8位YUV420是1.5倍,16位YUV420是3倍。算起来很简单,YUV每个通道表示1字节(8位)的话,UV通道两个方向都会减小了一半,最终8位YUV是=1.5,16位自然要乘以2等于3喽。
步骤2:生成16位YUV420纹理
无法使用OpenGL扩展的情况下,调用glTexImage2D上传YUV420Buffer到GL_R16UI格式纹理上,GL_R16UI格式纹理与YUV420中的位置一一对应。常规做法是把YUV420Buffer拆成y、u、v三个buffer分别上传到三个纹理,为了方便加速处理直接把buffer上传,常规做法之所以拆成多个是因为YUV420的YUV数据混在一起插值会导致数据错误。
- texImage2D方法表示把某格式的Buffer数据上传到某格式的纹理上
- GL_R16UI表示纹理格式,R表示RGB三位都是R值,16表示像素16位,U表示unsigned无符号,I表示Integer整形不归一化,也就是16位无符号量化纹理
- GL_RED_INTEGER、GL_UNSIGNED_SHORT表示Buffer的像素格式,GL_RED_INTEGER表示每个像素都是R值,GL_UNSIGNED_SHORT表示无符号Short也就是2字节一像素。
- 纹理的宽设为strideWidth/2,因为MediaCodec的strideWidth表示字节宽度而不是像素宽度,要从strideWidth得到纹理的宽就需要除以字节大小,16位YUV是2字节一像素就除以2
- 纹理的高设为info.size/stridewidth, 因为视频的高并不是YUV420的高,Buffer大小是纹理的字节宽度和纹理高的乘积,要从Buffer大小得到纹理高只要除以纹理的字节宽度就可以。譬如视频宽1280高720,字节大小是通道,strideWidth是字节,那么纹理的宽就是,纹理的高就是。
步骤3:16位YUV420纹理转16位YUV纹理
YUV420是为了传输YUV减小大小发明的,自然可以用YUV420转YUV公式转换。注意转换之前要根据Android视频的颜色格式判断是哪种YUV420,Android 颜色格式中整理出颜色格式和YUV420的对应关系如下所示。
Android颜色格式 | YUV420 |
---|---|
COLOR_FormatYUV420Planar | 8位i420 |
COLOR_FormatYUV420PackedPlanar | 8位 YV12 |
COLOR_FormatYUV420SemiPlanar | 8位 NV12 |
COLOR_FormatYUV420PackedSemiPlanar | 8位 NV21 |
HAL_PIXEL_FORMAT_YCbCr_420_P010 HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC HAL_PIXEL_FORMAT_YCbCr_420_P010_VENUS COLOR_FormatYUVP010 HAL_PIXEL_FORMAT_YCbCr_420_P010_UBWC | 10位 NV12 |
步骤4:16位YUV纹理转归一化RGB纹理
先右移6位把16位YUV转成10位YUV,然后用YUV转RGB矩阵把10位YUV转换成10位RGB,10位RGB再归一化就可以
注意:
- YUV转RGB矩阵要注意判断YUV的范围和色域不能直接从网络上复制,不同矩阵针对不同色域用错会出现色差
- 10位RGB归一化除以1023而不是1024,因为颜色是从0-1023而不是0-1024
问题思考
下面6个问题留给大家思考
-
怎么解决部分手机MediaCodec不支持解码16位YUV420Buffer?
-
如何打印OpenGL Shader内纹理数据验证samplerExternalOESYUV转RGB的色域支持情况?
-
YUV和YCbCr有什么区别?
-
如何实现加快4种YUV420格式转YUV速度?
-
不同范围和色域下YUV转RGB矩阵不一样,怎么推导或者找到正确的公式?
-
samplerExternalOES内部是如何实现YUV转RGB、位数和色域支持情况如何?