HDR转SDR实践之旅(二)解码10位YUV纹理

3,875 阅读10分钟

什么是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的缩写。

image.png

下图所示是电视机上YCbCr的接口,只接Y显示黑白。

image.png

UV是蓝红色度,那么绿色去哪里了?绿色在Y通道里,如下图所示Y通道就是一定比例的红绿蓝混合而成。为什么一定比例的红绿蓝混合变成了黑白色,因为红绿蓝光的比例虽然不同,但是三个感光细胞的电脉冲信号强度一样,电脉冲信号强度一样看起来就是黑白色。不是说RGB三光混合变成了白光而是眼睛看起来像白光,这也是为什么Y亮度更适合叫做Y明度的原因,明度比亮度更适合表示生理感受(为了方便,后续Y还是叫做亮度)。

image.png

MediaCodec解码10位YUV流程

image.png

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位归一化后最多只差31024\frac{3}{1024}也可视为支持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补在后面导致数据左移需要右移回来。

image.png 如上图所示YUV16位十进制479大端情况下还是479,小端右边补6位变成30656(479*2^6),16位YUV变成10位YUV需要右移6位去除0。上图中看到正常的小端数据和YUV的小端数据是不一样的,那么为什么YUV小端不用正常小端存储,这样不是更简单吗,我的猜测是为了保证16位数据被当成8位归一化和16位直接归一化的数据差距最小(YUV小端右边8位保留着原数据的高8位丢弃低位归一化后和16位归一化最多就差31024\frac{3}{1024})。

10位YUV纹理方案

OpenGL本身不支持YUV纹理只支持RGB纹理需要特殊扩展或者自己处理,下面3个方案为了方便用伪代码讲解。

方案一解码到samplerExternalOES绑定的SurfaceTexture

image.png

第一步: 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

image.png 注意:

第一步: 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转换

这种方式是略繁琐的需要处理很多逻辑,不过也因为代码是自己写的,效果可控兼容性最高。

image.png

步骤1:解码16位YUV420ByteBuffer

image.png

备注:

  1. 解码到Buffer时configure第二个参数要传null,解码到Surface时传Surface
  2. 配置KEY_COLOR_FORMAT标识视频是什么颜色格式,只是一个标识不是说配置什么格式就一定输出什么格式,大部分情况视频是什么格式就会输出什么格式,不传也不行部分手机会崩溃,android13以后COLOR_FormatYUVP010代表10位YUV420,13以前用COLOR_FormatYUV420Flexible代表了4种8位YUV420格式。
  3. 解码出来的数据不一定是16位还有可能是8位,8位不一定表示硬件不支持HDR,只是手机内部使用特殊的ColorFormat标识成8位就输出8位Buffer,测试发现解出16位的手机大都是晓龙中高端机,华为的麒麟芯片不支持,暂时没有方法绕过,但是就像前面说的16位YUV小端取原数据高8位归一化和16位YUV归一化相差很少。YUV的位数可以通过bufferSizewidthheight\frac{bufferSize}{width \cdot height}来判断,8位YUV420是1.5倍,16位YUV420是3倍。算起来很简单,YUV每个通道表示1字节(8位)的话,UV通道两个方向都会减小了一半,最终8位YUV是1+1421+\frac{1}{4}\cdot 2=1.5,16位自然要乘以2等于3喽。

步骤2:生成16位YUV420纹理

image.png

无法使用OpenGL扩展的情况下,调用glTexImage2D上传YUV420Buffer到GL_R16UI格式纹理上,GL_R16UI格式纹理与YUV420中的位置一一对应。常规做法是把YUV420Buffer拆成y、u、v三个buffer分别上传到三个纹理,为了方便加速处理直接把buffer上传,常规做法之所以拆成多个是因为YUV420的YUV数据混在一起插值会导致数据错误。

  1. texImage2D方法表示把某格式的Buffer数据上传到某格式的纹理上
  2. GL_R16UI表示纹理格式,R表示RGB三位都是R值,16表示像素16位,U表示unsigned无符号,I表示Integer整形不归一化,也就是16位无符号量化纹理
  3. GL_RED_INTEGERGL_UNSIGNED_SHORT表示Buffer的像素格式,GL_RED_INTEGER表示每个像素都是R值,GL_UNSIGNED_SHORT表示无符号Short也就是2字节一像素。
  4. 纹理的宽设为strideWidth/2,因为MediaCodec的strideWidth表示字节宽度而不是像素宽度,要从strideWidth得到纹理的宽就需要除以字节大小,16位YUV是2字节一像素就除以2
  5. 纹理的高设为info.size/stridewidth, 因为视频的高并不是YUV420的高,Buffer大小是纹理的字节宽度和纹理高的乘积,要从Buffer大小得到纹理高只要除以纹理的字节宽度就可以。譬如视频宽1280高720,字节大小是128072031280*720*3通道,strideWidth是128021280*2字节,那么纹理的宽就是strideWidth/2=12802/2=1280strideWidth/2=1280*2/2=1280,纹理的高就是size/strideWidth=12807203/(12802)=7201.5=1080size/strideWidth = 1280*720*3/(1280*2)= 720*1.5 =1080

步骤3:16位YUV420纹理转16位YUV纹理

YUV420是为了传输YUV减小大小发明的,自然可以用YUV420转YUV公式转换。注意转换之前要根据Android视频的颜色格式判断是哪种YUV420,Android 颜色格式中整理出颜色格式和YUV420的对应关系如下所示。

Android颜色格式YUV420
COLOR_FormatYUV420Planar8位i420
COLOR_FormatYUV420PackedPlanar8位 YV12
COLOR_FormatYUV420SemiPlanar8位 NV12
COLOR_FormatYUV420PackedSemiPlanar8位 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

image.png

步骤4:16位YUV纹理转归一化RGB纹理

image.png 先右移6位把16位YUV转成10位YUV,然后用YUV转RGB矩阵把10位YUV转换成10位RGB,10位RGB再归一化就可以

注意:

  1. YUV转RGB矩阵要注意判断YUV的范围和色域不能直接从网络上复制,不同矩阵针对不同色域用错会出现色差
  2. 10位RGB归一化除以1023而不是1024,因为颜色是从0-1023而不是0-1024

问题思考

下面6个问题留给大家思考

  1. 怎么解决部分手机MediaCodec不支持解码16位YUV420Buffer?

  2. 如何打印OpenGL Shader内纹理数据验证samplerExternalOESYUV转RGB的色域支持情况?

  3. YUV和YCbCr有什么区别?

  4. 如何实现加快4种YUV420格式转YUV速度?

  5. 不同范围和色域下YUV转RGB矩阵不一样,怎么推导或者找到正确的公式?

  6. samplerExternalOES内部是如何实现YUV转RGB、位数和色域支持情况如何?

系列文章