仿照FFmpeg在GLSL中处理HDR.ToneMapping(上)

本文介绍了不同色域标准(BT.601、709、2020)下的YUV到RGB转换公式,并展示了GLSL fragment shader的实现。还探讨了10位数据在GPU中的处理,强调了位掩码、精度问题以及色调映射(Tonemapping)的重要性,包括Gamma校正和PQ曲线在HDR到SDR转换中的应用。最后,通过ffmpeg命令行展示了HDR视频转SDR的处理流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

承接上文内容,纯干货,别看漏一个字。

BT.601、709、2020标准下的YUV转RGB

上回我们已经把解码数据以short类型传递到gpu显存环境当中,下一步就是要正确的转换城rgb形式,翻查libyuv的最新源码就能找到BT.601(full / limited) BT.709(full / limited)BT.2020(full / limited)的转换公式系数。(关于色域的三个标准有不明白的,可以翻查HDR专栏的文章,具体标准可以到这下载 https://www.itu.int/rec/R-REC-BT/en

// BT.601 limited range YUV to RGB reference
static void YUVToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte((y - 16) * 1.164 - (v - 128) * -1.596);
  *g = RoundToByte((y - 16) * 1.164 - (u - 128) * 0.391 - (v - 128) * 0.813);
  *b = RoundToByte((y - 16) * 1.164 - (u - 128) * -2.018);
}

// BT.601 full range YUV to RGB reference (aka JPEG)
static void YUVJToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte(y - (v - 128) * -1.40200);
  *g = RoundToByte(y - (u - 128) * 0.34414 - (v - 128) * 0.71414);
  *b = RoundToByte(y - (u - 128) * -1.77200);
}

// BT.709 limited range YUV to RGB reference
static void YUVHToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte((y - 16) * 1.164 - (v - 128) * -1.793);
  *g = RoundToByte((y - 16) * 1.164 - (u - 128) * 0.213 - (v - 128) * 0.533);
  *b = RoundToByte((y - 16) * 1.164 - (u - 128) * -2.112);
}

// BT.709 full range YUV to RGB reference
static void YUVFToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte(y - (v - 128) * -1.5748);
  *g = RoundToByte(y - (u - 128) * 0.18732 - (v - 128) * 0.46812);
  *b = RoundToByte(y - (u - 128) * -1.8556);
}

// BT.2020 limited range YUV to RGB reference
static void YUVUToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte((y - 16) * 1.164384 - (v - 128) * -1.67867);
  *g = RoundToByte((y - 16) * 1.164384 - (u - 128) * 0.187326 -
                   (v - 128) * 0.65042);
  *b = RoundToByte((y - 16) * 1.164384 - (u - 128) * -2.14177);
}

// BT.2020 full range YUV to RGB reference
static void YUVVToRGBReference(int y, int u, int v, int* r, int* g, int* b) {
  *r = RoundToByte(y + (v - 128) * 1.474600);
  *g = RoundToByte(y - (u - 128) * 0.164553 - (v - 128) * 0.571353);
  *b = RoundToByte(y + (u - 128) * 1.881400);
}

libyuv里面的转换公式

根据转换公式,此时的GLSL. fragment shader应该是这样的:

#version 320 es
precision highp float;
uniform int bitMark; // 双字节(16bit)存储10位数据的 位掩码个数
uniform lowp float imgWidth;
uniform lowp float imgHeight;
uniform highp usampler2D tex_unsigned_y; // GL_R16UI、GL_RED_INTEGER、GL_UNSIGNED_SHORT
uniform highp usampler2D tex_unsigned_uv;// GL_RG16UI、GL_RG_INTEGER、GL_UNSIGNED_SHORT
in  vec2 vTextureCoord;
out vec4 _FragColor;

highp vec3 YuvConvertRGB_BT2020(highp uvec3 yuv, int normalize) {
    highp vec3 rgb;
    highp int y = highp int(yuv.x);
    highp int u = highp int(yuv.y);
    highp int v = highp int(yuv.z);
	// [64, 960]
    float r = float(y - 64) * 1.164384                             - float(v - 512) * -1.67867;
    float g = float(y - 64) * 1.164384 - float(u - 512) * 0.187326 - float(v - 512) * 0.65042;
    float b = float(y - 64) * 1.164384 - float(u - 512) * -2.14177;
    rgb.r = r;
    rgb.g = g;
    rgb.b = b;
    if (normalize == 1) { 
        rgb.r = r / 1024.0; 
        rgb.g = g / 1024.0; 
        rgb.b = b / 1024.0; 
    }// 归一化处理,10位数据共有1024个色阶
    return rgb;
}
			
void main() {
	float samplerPosX = vTextureCoord.x * imgWidth;
	float samplerPosY = vTextureCoord.y * imgHeight;
	highp uint unsigned_y = texelFetch(tex_unsigned_y, ivec2(int(samplerPosX), int(samplerPosY)), 0).x;
	highp uint unsigned_u = texelFetch(tex_unsigned_uv, ivec2(int(samplerPosX / 2.0), int(samplerPosY / 2.0)), 0).r;
	highp uint unsigned_v = texelFetch(tex_unsigned_uv, ivec2(int(samplerPosX / 2.0), int(samplerPosY / 2.0)), 0).g;
	highp uvec3 yuv10bit = uvec3(unsigned_y >> bitMark, unsigned_u >> bitMark, unsigned_v >> bitMark);
	//highp vec3 rgb10bit = YuvConvertRGB_BT601(yuv10bit, 1);
	//highp vec3 rgb10bit = YuvConvertRGB_BT709(yuv10bit, 1);
	highp vec3 rgb10bit = YuvConvertRGB_BT2020(yuv10bit, 1);
	 _FragColor = vec4(fragColor, 1.0);
}

这里有几处地方要提醒:

1、bitMark,双字节(16bit)存储10位数据的位掩码数。HEVCProfileMain10HDR10 和 非8bit数据位深的一些讨论。_Mr_Zzr的博客-优快云博客

在 ​​​​​​HEVCProfileMain10HDR10 和 非8bit数据位深的讨论文章当中,已经分析过双字节(16bit)存储10位数据的存储格式,其中需要右移正确的位掩码数才能得到需要的色值。

2、usampler2D tex_unsigned_uv;的格式组合是用GL_RG16UI、GL_RG_INTEGER、GL_UNSIGNED_SHORT,在texelFetch提取纹素的时候,记得uv的宽高要跟原大小保持一直,即Y分量是1920*1088,那UV分量就是960*544了。

3、YuvConvertRGB_BT2020会根据输入参数,进行归一化处理,10bit数据单个色光有1024个值,即取值范围是[0,1024],1 / 1024 = 0.0009765625,数数多少个小数位。所以还是要时刻留意着运算过程中的精度问题。

4、顺带说一句:10bit imited的Y取值范围[64,960]

Gamma矫正、Tone Mapping处理

按照以上直接输出光栅化渲染,你会发现这显示的内容和原8bit的数据比较起来,看着更暗淡,好像似蒙上了一层灰一样的糟糕。

在这里插入图片描述在这里插入图片描述

 左侧直接光栅化,右侧8bit效果

为啥会这样?主要是是10bit数据直接归一化后,按照8bit原数据渲染,精度都丢失了。换一种说法就是:原本[0,1024]的数据,只使用了[0~255]的部分,[256~1024]全丢失了。

那可咋整?此时我们需要色调映射 Tone mapping

色调映射(Tone Mapping)是一个将HDR的图像或者视频转换成SDR的图像,并在SDR显示设备上正确显示的技术。目前HDR的内容还不是很多,HDR和SDR混合的内容会长期存在,而且能够支持HDR显示的屏幕还没全民推广,要让HDR的内容能够有一个正确的显示,色调映射是一个经常用到的技术。

在介绍Tone Mapping之前,还需要铺垫一个知识:Gamma校正

当我们计算出场景中所有像素的最终颜色以后,我们就必须把它们显示在监视器上。过去,大多数监视器是阴极射线管显示器(CRT)。这些监视器有一个物理特性就是两倍的输入电压产生的不是两倍的亮度。输入电压产生约为输入电压的2.2次幂的亮度,这叫做Gamma效应。

人类所感知的亮度恰好和CRT所显示出来相似的指数关系非常匹配。为了更好的理解所有含义,请看下面的图片:

因为人眼看到颜色的亮度更倾向于顶部的灰阶,监视器使用的也是一种指数关系(电压的2.2次幂),所以物理亮度通过监视器能够被映射到顶部的非线性亮度;因此看起来效果不错。监视器的这个非线性映射的确可以让亮度在我们眼中看起来更好,但当渲染图像时,会产生一个问题:我们在应用中配置的亮度和颜色是基于监视器所看到的,这样所有的配置实际上是非线性的亮度/颜色配置。请看下图: 

有关Gamma矫正的详细内容,请参阅 LearnOpenGL的这篇文章

正因为人眼对于物理世界的感知是非线性的,即对于中等亮度和暗部区间的敏感程度远高于高亮度区间,于是就有了SDR领域的Gamma曲线。我们倾向于用更多的比特数来表示中等亮度或者暗部的区域,以提高压缩率,取得更贴近人眼的效果。

在HDR时代,Gamma曲线已经不能满足最大亮度的需求。基于人眼的对比敏感度函数(Contrast Sensitivity Function,CSF),在SMPTE ST 2084标准中规定了EOTF曲线,就是感知量化(PQ)曲线。亮度范围可从最暗0.00005 nits到最亮10000 nits。PQ曲线最早是由Dolby公司开发的,并且在ST 2084中进行了标准化。

混合对数伽马(HLG)是另外一个重要的HDR传输曲线,由BBC和NHK公司开发。这个曲线与PQ曲线不同,HLG规定的是OETF曲线,因为在低亮度区域基本与Gamma曲线重合,所以提供了与SDR显示设备很好的兼容性,在广播电视系统里有着广泛的应用。HLG曲线最早在ARIB STD-B67中进行了标准化,后面也进入了ITU-R BT.2100。

有关PQ曲线和HLG曲线的详细内容,请参阅 之前我搬运的 HDR in Mind。本文使用的感知量化(PQ)曲线的方法。


好了,看到这可能很多同学又吐槽,我这说的是啥玩意呢?这是越说越懵逼了。我这就用人说的话总结一下:

颜色值(数字信号 / 电信号 / Electrical signal)转到硬件设备显示(模拟信号 / 光信号 / Optical signal)这一过程的转换,并不是线性关系;所以如果直接基于数字信号的图像处理(即我们写的运算代码)并不能正确的反应的硬件设备上。基于此,才有了EOTF(电光转换)和 OETF(光电转换)在图像处理前,先利用eotf转换成线性比例,ToneMapping图像处理完之后,再逆一次eotf,最终输出到硬件设备

此时你再去看 LearnOpenGL的这篇文章 和 HDR in Mind。我相信你会有不一样的收获。

FFmpeg 命令行 HDR 转 SDR

理论说了一大堆,那到底该如何做呢?还是先看看万能的ffmpeg是如何处理这一块的吧。

ffmpeg -i planet_hdr.MP4 -vf zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p planet_ff_hdr2sdr.MP4

一行行来拆解命令行内容,最主要的是 -vf  video filter的一大串命令。 

zscale=t=linear:npl=100 => 利用zscale模块的转换函数linear,输入参数npl=100

format=gbrpf32le            => 转换格式gbr浮点32 little end 

zscale=p=bt709              => 利用zscale模块的设置色域bt709

tonemap=tonemap=hable:desat=0 => 指定tonemapping转换算法hable,输入参数desat=0

zscale=t=bt709:m=bt709:r=tv         => 利用zscale模块的转换函数bt709,range=tv. limited

format=yuv420p                              => 转换格式yuv420p

zscale转换模块是ffmpeg内部引用第三方库zimg,官方介绍地址:FFmpeg Filters Documentation想利用zscale,必须确认ffmpeg编译是--enable-libzimg,zimg库源码:GitHub - sekrit-twc/zimg: Scaling, colorspace conversion, and dithering library

从命令行的流程可以印证刚刚的总结。

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Mr_Zzr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值