1、什么是 SDR 和 HDR?
SDR(Standard Dynamic Range)即规范动态规模,是一种依据亮度、比照度、颜色特性,以及 CRT 显现器的局限性来展现视频的技能。这儿说的动态规模
一般是指亮度规模,更大的亮度规模能够支撑更高的比照度。SDR 的支撑的亮度规模在 0.1nit 到 100nit 之间,运用 Rec.709/sRGB 色域,并运用 Gamma 曲线来作为它的电光转化函数(Electro-Optical Transfer Function,EOTF)。
HDR(High Dynamic Range)即高动态规模,是对 SDR 的升级,是一种提高视频显现质量的技能。HDR 改变了视频和图画的亮度和颜色信息在信号中的表明方法,然后支撑更大的亮度规模(0.0005-10000nit) 、更宽广的色域(BT.2020) 、更高精度的量化(10bit 或 12bit) 。因此 HDR 视频画面能够展现出更多的亮部和暗部细节,画面具有丰厚的颜色和生动自然的细节表现,因此画面更接近人眼所见;SDR 视频的颜色饱满度以及画面比照度则不如 HDR 视频,比较 HDR 视频,SDR 视频的画面,给人一种暗淡不自然的观感,一起在亮部以及暗部细节上都有很明显的缺失。
HDR 和 SDR 的视觉差异
HDR 和 SDR 信息处理差异
HDR 技能有着不同的规范,其间常见的有四个:HDR10、HDR10+、Dolby Vision、HLG。
-
HDR10
是比较基础的一个版别,也是一个敞开的规范,于 2014 年被采用。HDR10 由于其易用性和免答应费而取得广泛的接受。该规范描述了契合 UHDTV Rec.ITU-R BT.2020 规范主张的视频内容。HDR10 采用的是 PQ EOTF 转化曲线,与 SDR 显现器不兼容。HDR10 采用了静态元数据,不能满意不同场景或许不同帧调色的需求,所以 HDR10 的作用展现才干比较有限。 -
HDR10+
是三星提出的用于对抗 Dolby Vision 的技能,在 HDR10 的基础上,添加了动态元数据的支撑。能够针对每一个视频场景或许视频帧进行亮度和颜色的调理,支撑动态颜色映射,向后也可兼容 HDR10 的格局。 -
Dolby Vision
是杜比公司具有知识产权的技能,需求授权费用才干运用。也由于需求授权费用,所以内容还不是特别丰厚。Dolby Vision 实际是国际上首个推出的商业化版别的 HDR 规范,具有非常强的竞争力。它采用动态元数据,能够最高支撑 10000nit 的峰值亮度。为了增强码流播映和显现兼容性,规划了众多的 Profile 支撑不同的运用。 -
HLG
规范出现于 2015 年,是由英国 BBC 公司和日本的 NHK 电视台共同开发,也被广泛采用。该规范描述了契合 BT.2020 要求的内容。如前所述,HLG 广泛地运用到广电系统中,有很好的兼容性。
下面不同格局 HDR 的参数比照:
不同格局 HDR 的参数比照
上面的介绍中,说到了
元数据
,这儿简略介绍一下:HDR 的元数据是用来描述视频或图画处理进程中的关键信息或许特征,首要有两种:
静态元数据
和动态元数据
。静态元数据规则了整个片子像素级别最大亮度上限,在 ST 2086 中有规范化的界说。静态元数据的缺陷是必须做大局的颜色映射,没有满意的调理空间,兼容性不好。
动态元数据能够很好地解决这个问题。动态元数据首要有两个方面的作用:与静态元数据比较,它能够在每一个场景或许每一帧画面,给调色师一个发挥的空间,以展现更丰厚的细节;另一个方面,通过动态元数据,在方针显现亮度上做颜色映射,能够最大程度在方针显现器上出现作者的创造目的。
SMPTE ST 2094 规范中界说了一系列的动态元数据。ST 2094-10、ST 2094-20、ST 2094-30、ST 2094-40 分别给出了杜比、飞利浦、特艺和三星四家公司的动态元数据和色域转化的计划。
参阅:
- Standard-dynamic-range[1]
- High-dynamic-range[2]
- HDR 技能趋势浅析[3]
2、HDR 在运用中可能遇到什么问题?
由于 HDR 是一套涉及到颜色空间和设备显现特性的技能计划,所以要完结对 HDR 的支撑,需求满意:
- 视频资源满意 HDR 规范
- 显现设备支撑 HDR 显现
由于 HDR 技能计划涉及到颜色空间,使得在相机收集、编码、解码、渲染到屏幕上这一整个流程里边,但凡涉及到要对颜色信息进行了解和处理的节点,都需求完结对 HDR 的支撑才干确保终究正确地出现出它的特性。这就很容易出现由于某一个环节缺失对 HDR 的支撑而形成终究的出现问题。
所以,作为一种新的技能规范,HDR 在运用中最大的问题是兼容性问题,这儿最大的兼容性问题是 HDR 与 SDR 新旧技能之间的兼容,此外还有不同 HDR 规范之间的兼容。
HDR 在运用中最常见的问题有:
- 视频播映黑屏。
- 视频播映颜色反常。
- 视频画面较暗。
- 视频画面发灰。
这种兼容性问题是怎么形成的呢?
1)颜色位深
中心原因之一是颜色位深的不同。SDR 运用的颜色空间是运用 8bit 的位深,而 HDR 则运用的颜色空间是运用 10bit 或 12bit 的位深。这样一来在表明信息时就有容量差异了。在视频处理的流程中,如果从 HDR 向 SDR 转化时,如果处理不合理就会出现破坏性的信息丢掉,导致终究视频展现作用的反常。
8bit vs. 10bit
2)色域
中心原因之二是色域的不同。色域是指一个颜色空间所能表明的一切颜色的集合,色域越广,所能表明的颜色越丰厚。
SDR 运用 BT.709 颜色空间标的色域,而 HDR 则运用 BT.2020 颜色空间的更广的色域。宽色域向窄色域兼容时,同样也有信息丢掉的问题,不合理的处理也会导致终究视频展现作用的反常。
BT.709 vs. BT.2020
3)转化函数(Transfer Function)
别的一个带来兼容性问题的原因是转化函数的不同。
这儿的转化函数是指光电转化函数(Optical-Electro Transfer Function)或电光转化函数(Electro-Optical Transfer Function)。
咱们为什么要运用转化函数呢?人眼关于物理世界的感知是非线性的,关于中等亮度和暗部区间的灵敏程度远高于高亮度区间。为了讨好人眼这种对不同亮度的非线性的灵敏度,咱们在设备的收集电路中收集到光信号向电信号转化时,一般会将其转化为非线性信号,这儿用到了 OETF,这样对非线性信号进行编码时,能够用更多的码率来编码人眼灵敏的中等亮度或暗部细节,然后使得编码在讨好人眼上有更好的 ROI。而在显现时,咱们要再将非线性信号还原为线性光展现给人眼,这时候则要用到 EOTF。
SDR 和 HDR 的非线性编码
传统的 SDR 运用 BT.709 Gamma 曲线作为转化函数,对高亮部分进行了切断,能够表达的亮度动态规模有限,最大亮度只有 100nit。而在 HDR 技能中,增加了高亮部分细节的表达,大大扩展了亮度的动态规模,Gamma 曲线已经不能满意最大亮度的需求,HDR 则运用 PQ(Perceptual Quantizer,感知量化)或 HLG(Hybrid Log Gamma,混合对数伽马)曲线作为转化函数。
SDR 和 HDR 的转化函数
不同 HDR 转化函数的规划初衷不同,下面是 PQ 和 HLG 的差异:
-
PQ(Perceptual Quantizer,感知量化)曲线
的规划更接近人眼的特点,亮度表达更准确。依据人眼的比照灵敏度函数(Contrast Sensitivity Function,CSF),在 SMPTE ST 2084 规范中规则了 EOTF 曲线。亮度规模可从最暗 0.00005nit 到最亮 10000nit。PQ 曲线最早是由 Dolby 公司开发的,并且在 ST 2084 中进行了规范化。 -
HLG(Hybrid Log Gamma,混合对数伽马)曲线
是别的一个重要的 HDR 转化函数曲线,由 BBC 和 NHK 公司开发。这个曲线与 PQ 曲线不同,HLG 规则的是 OETF 曲线,由于在低亮度区域基本与 Gamma 曲线重合,所以提供了与 SDR 显现设备很好的兼容性,在广播电视系统里有着广泛的运用。HLG 曲线最早在 ARIB STD-B67 中进行了规范化,后边也进入了 ITU-R BT.2100。
OETF 和 EOTF
如果运用的转化函数不匹配,就会出现信息过错而影响终究的视频展现作用。
4)设备显现亮度
其他的原因还有设备显现亮度支撑的问题。HDR 的运用也需求硬件设备的支撑,电脑或手机的屏幕是否能支撑更高的亮度和比照度也会影响终究出现视频的作用。
下图是人眼能感触的亮度规模,以及 SDR 和 HDR 所能支撑的亮度规模的比照:
人眼、SDR、HDR 的亮度规模
一般台式电脑显现器的继续亮度在 350 尼特左右,有些专业显现器的会高一点,但大部分继续不了较长时刻。做的比较好的,比如苹果的 Pro Display XDR 显现器则能够到达 1000 尼特全屏继续亮度,峰值亮度到达 1600 尼特。
此外,由于 HDR 有多种格局,不同格局之间的参数差异也可能是影响视频终究出现作用的原因。
咱们再回头看前面说到的 HDR 在运用中常见的问题,能够知道原因大致如下:
- 视频播映黑屏。 可能是在播映 HDR 视频时,解码器不支撑 BT.2020 颜色空间(界说了 10bit 颜色位深),出现解码过错形成视频黑屏。
- 视频播映颜色反常。 可能是播映器渲染模块不支撑 BT.2020 颜色空间导致渲染颜色反常的问题。
- 视频画面较暗。 可能是显现器不支撑 HDR 的亮度规模,无法识别视频数据中的亮度信息导致。
- 视频画面发灰。 可能是显现器不支撑 HDR 颜色空间 BT.2020 的宽色域,导致显现器上的颜色饱满度不足。
参阅:
- HDR 技能趋势浅析
- 微博 HDR 视频的落地实践
- 高动态规模电视的理论原理
3、如何正确将 HDR 视频转化成 SDR 视频?
要解决 HDR 在运用中的问题,最好的体会当然是全链路支撑 HDR 技能规范来出现最佳的视频图画视觉。但现实是,咱们仍是有许多现存的显现设备是不支撑 HDR 的,关于这种情况,咱们则需求在这些设备大将 HDR 视频转化成 SDR 视频,并确保转化进程对信息的合理处理然后尽量降低视频视觉体会的丢失。这便是咱们接下来要讲的:如何正确将 HDR 视频转化成 SDR 视频?
简略来讲,HDR 视频转 SDR 视频需求下面几步:
- 1、HDR 非线性电信号转为 HDR 线性光信号(EOTF)
- 2、HDR 线性光信号做颜色空间转化(Color Space Converting),一般是从 BT.2020 转化到 BT.709
- 3、HDR 线性光信号颜色映射为 SDR 线性光信号(Tone Mapping)
- 4、SDR 线性光信号转 SDR 非线性电信号(OETF)
HDR 到 SDR 视频的转化,阅历了亮度动态规模和颜色空间的紧缩,亮度规模从[0.0005,10000]nit
紧缩到[0.1,100]nit
,颜色空间从 BT.2020 转化到 BT.709;一起颜色位深也由 10bit 降低到 8bit;视频信号可用的色阶数量从 1024 降低到 256 个,减少了 75%;一起光电转化函数 EOTF 也会变化,从 PQ 或 HLG 变为 BT.709 Gamma。
完结 HDR 转 SDR 视频的计划有下面几种可供参阅:
3.1、运用 FFmpeg filter 完结转化
运用 FFmpeg 指令行完结 HDR 转 SDR,首要是运用了 FFmpeg 中zscale
(依靠 zimg)以及tonemap
这两个 filter,要运用 zscale,必须承认 FFmpeg 编译时有敞开--enable-libzimg
。其间zimg
是一个完结颜色空间转化的三方库:github.com/sekrit-twc/…
FFmpeg 完结 HDR 转 SDR 的指令如下:
ffmpeg-i<input>-vf\#-vf后边表明是videofilter的一系列指令
zscale=t=linear:npl=100,\#1)非线性转线性。指定zscale模块的转化函数为linear,输入参数为npl=100,npl表明标称峰值亮度(nominalpeakluminance)
format=gbrpf32le,\#转化格局为gbr,用littleend32位浮点类型存储10bit颜色通道
zscale=p=bt709,\#2)颜色空间转化。指定zscale模块的色域为bt709
tonemap=tonemap=hable:desat=0,\#3)颜色映射。指定tonemapping转化算法为hable,输入参数desat=0
zscale=t=bt709:m=bt709:r=tv,\#4)线性转非线性。指定zscale模块的转化函数为bt709,转化矩阵为bt709,range为tv.limited
format=yuv420p\#转化格局为yuv420p
<output>
上面的指令是一个串联执行流程,顺序也对应上面咱们说到的 HDR 和 SDR 转化流程。下面咱们以一个运用了 PQ 规范的 HDR 视频为例来介绍一下几个关键进程的相关代码:
1)非线性颜色数字信号经过 EOTF 转化为线性的模仿光信号。
第一个进程对应上的指令参数:zscale=t=linear:npl=100
,表明方针是要转化为 linear 线性模仿光信号,标称峰值亮度(nominal peak luminance)为 100。
咱们能够从zimg[5]源代码中找到关键函数:
//src/zimg/colorspace/graph.cpp
std::vector<ColorspaceNode>get_neighboring_colorspaces(constColorspaceDefinition&csp)
{
zassert_d(is_valid_csp(csp),"invalidcolorspace");
std::vector<ColorspaceNode>edges;
autoadd_edge=[&](constColorspaceDefinition&out_csp,autofunc)
{
edges.emplace_back(out_csp,std::bind(func,csp,out_csp,std::placeholders::_1,std::placeholders::_2));
};
if(csp.matrix==MatrixCoefficients::RGB){
constexprMatrixCoefficientsspecial_matrices[]={
MatrixCoefficients::UNSPECIFIED,
MatrixCoefficients::RGB,
MatrixCoefficients::REC_2020_CL,
MatrixCoefficients::CHROMATICITY_DERIVED_NCL,
MatrixCoefficients::CHROMATICITY_DERIVED_CL,
MatrixCoefficients::REC_2100_LMS,
MatrixCoefficients::REC_2100_ICTCP,
};
//RGBcanbeconvertedtoconventionalYUV.
for(automatrix:all_matrix()){
if(std::find(std::begin(special_matrices),std::end(special_matrices),matrix)==std::end(special_matrices))
add_edge(csp.to(matrix),create_ncl_rgb_to_yuv_operation);
}
if(csp.primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(MatrixCoefficients::CHROMATICITY_DERIVED_NCL),create_ncl_rgb_to_yuv_operation);
//LinearRGBcanbeconvertedtoothertransferfunctionsandprimaries;alsotocombinedmatrix-transfersystems.
if(csp.transfer==TransferCharacteristics::LINEAR){
for(autotransfer:all_transfer()){
if(transfer!=csp.transfer&&transfer!=TransferCharacteristics::UNSPECIFIED){
add_edge(csp.to(transfer),create_linear_to_gamma_operation);
if(csp.primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(transfer).to(MatrixCoefficients::CHROMATICITY_DERIVED_CL),create_cl_rgb_to_yuv_operation);
}
}
if(csp.primaries!=ColorPrimaries::UNSPECIFIED){
for(autoprimaries:all_primaries()){
if(primaries!=csp.primaries&&primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(primaries),create_gamut_operation);
}
}
add_edge(csp.to(MatrixCoefficients::REC_2020_CL).to(TransferCharacteristics::REC_709),create_cl_rgb_to_yuv_operation);
if(csp.primaries==ColorPrimaries::REC_2020)
add_edge(csp.to(MatrixCoefficients::REC_2100_LMS),create_ncl_rgb_to_yuv_operation);
}elseif(csp.transfer!=TransferCharacteristics::UNSPECIFIED){
//GammaRGBcanbeconvertedtolinearRGB.
add_edge(csp.to_linear(),create_gamma_to_linear_operation);
}
}elseif(csp.matrix==MatrixCoefficients::REC_2020_CL||csp.matrix==MatrixCoefficients::CHROMATICITY_DERIVED_CL){
add_edge(csp.to_rgb().to_linear(),create_cl_yuv_to_rgb_operation);
}elseif(csp.matrix==MatrixCoefficients::REC_2100_LMS){
//LMSwithST_2084orARIB_B67transferfunctionscanbeconvertedtoICtCpandalsotolineartransferfunction.
if(csp.transfer==TransferCharacteristics::ST_2084||csp.transfer==TransferCharacteristics::ARIB_B67){
add_edge(csp.to(MatrixCoefficients::REC_2100_ICTCP),create_lms_to_ictcp_operation);
add_edge(csp.to(TransferCharacteristics::LINEAR),create_gamma_to_linear_operation);
}
//LMSwithlineartransferfunctioncanbeconvertedtoRGBmatrixandtoARIB_B67andST_2084transferfunctions.
if(csp.transfer==TransferCharacteristics::LINEAR){
add_edge(csp.to_rgb(),create_ncl_yuv_to_rgb_operation);
add_edge(csp.to(TransferCharacteristics::ST_2084),create_linear_to_gamma_operation);
add_edge(csp.to(TransferCharacteristics::ARIB_B67),create_linear_to_gamma_operation);
}
}elseif(csp.matrix==MatrixCoefficients::REC_2100_ICTCP){
//ICtCpwithST_2084orARIB_B67transferfunctionscanbeconvertedtoLMS.
if(csp.transfer==TransferCharacteristics::ST_2084||csp.transfer==TransferCharacteristics::ARIB_B67)
add_edge(csp.to(MatrixCoefficients::REC_2100_LMS),create_ictcp_to_lms_operation);
}elseif(csp.matrix!=MatrixCoefficients::UNSPECIFIED){
//YUVcanbeconvertedtoRGB.
add_edge(csp.to_rgb(),create_ncl_yuv_to_rgb_operation);
}
returnedges;
}
EOTF 转化会先调用get_neighboring_colorspaces
函数,并创立 Gamma RGB → Linear RGB 的转化操作,即调用create_gamma_to_linear_operation
函数。
//src/zimg/colorspace/operation.cpp
std::unique_ptr<Operation>create_gamma_to_linear_operation(constColorspaceDefinition&in,constColorspaceDefinition&out,constOperationParams¶ms,CPUClasscpu)
{
zassert_d(in.primaries==out.primaries,"primariesmismatch");
zassert_d((in.matrix==MatrixCoefficients::RGB||in.matrix==MatrixCoefficients::REC_2100_LMS)&&
(out.matrix==MatrixCoefficients::RGB||out.matrix==MatrixCoefficients::REC_2100_LMS),"mustbeRGBorLMS");
zassert_d(in.transfer!=TransferCharacteristics::LINEAR&&out.transfer==TransferCharacteristics::LINEAR,"wrongtransfercharacteristics");
if(in.transfer==TransferCharacteristics::ARIB_B67&&use_display_referred_b67(in.primaries,params))
returncreate_inverse_arib_b67_operation(ncl_rgb_to_yuv_matrix_from_primaries(in.primaries),params);
else
returncreate_inverse_gamma_operation(select_transfer_function(in.transfer,params.peak_luminance,params.scene_referred),params,cpu);
}
由于现在是依照 PQ 规范做转化,所以in.transfer
不等于ARIB_B67
(这是 HLG 规范),会接着调用create_inverse_gamma_operation
函数。
//src/zimg/colorspace/operation_impl.cpp
//GammaOperationC类:
classGammaOperationCfinal:publicOperation{
gamma_funcm_func;
floatm_prescale;
floatm_postscale;
public:
GammaOperationC(gamma_funcfunc,floatprescale,floatpostscale):
m_func{func},
m_prescale{prescale},
m_postscale{postscale}
{}
voidprocess(constfloat*const*src,float*const*dst,unsignedleft,unsignedright)constoverride
{
EnsureSinglePrecisionx87;
for(unsignedp=0;p<3;++p){
constfloat*src_p=src[p];
float*dst_p=dst[p];
for(unsignedi=left;i<right;++i){
dst_p[i]=m_postscale*m_func(src_p[i]*m_prescale);
}
}
}
};
//...
//create_inverse_gamma_operation函数:
std::unique_ptr<Operation>create_inverse_gamma_operation(constTransferFunction&transfer,constOperationParams¶ms,CPUClasscpu)
{
std::unique_ptr<Operation>ret;
#ifdefined(ZIMG_X86)
ret=create_inverse_gamma_operation_x86(transfer,params,cpu);
#elifdefined(ZIMG_ARM)
ret=create_inverse_gamma_operation_arm(transfer,params,cpu);
#endif
if(!ret)
ret=std::make_unique<GammaOperationC>(transfer.to_linear,1.0f,transfer.to_linear_scale);
returnret;
}
暂时不考虑平台加速的代码,这儿则是要构建了一个GammaOperationC
。构建GammaOperationC
最重要的参数是转化函数目标:TransferFunction
。由于这儿是要转为线性光信号,所以是取的是TransferFunction
目标的to_linear
和to_linear_scale
特点。这个目标是之前调用select_transfer_function
函数来取得的,代码如下:
//src/zimg/colorspace/gamma.cpp
TransferFunctionselect_transfer_function(TransferCharacteristicstransfer,doublepeak_luminance,boolscene_referred)
{
zassert_d(!std::isnan(peak_luminance),"nandetected");
TransferFunctionfunc{};
func.to_linear_scale=1.0f;
func.to_gamma_scale=1.0f;
switch(transfer){
//...
caseTransferCharacteristics::REC_709:
func.to_linear=scene_referred?rec_709_inverse_oetf:rec_1886_eotf;
func.to_gamma=scene_referred?rec_709_oetf:rec_1886_inverse_eotf;
break;
caseTransferCharacteristics::ST_2084:
func.to_linear=scene_referred?st_2084_inverse_oetf:st_2084_eotf;
func.to_gamma=scene_referred?st_2084_oetf:st_2084_inverse_eotf;
func.to_linear_scale=static_cast<float>(ST2084_PEAK_LUMINANCE/peak_luminance);
func.to_gamma_scale=static_cast<float>(peak_luminance/ST2084_PEAK_LUMINANCE);
break;
caseTransferCharacteristics::ARIB_B67:
func.to_linear=scene_referred?arib_b67_inverse_oetf:arib_b67_eotf;
func.to_gamma=scene_referred?arib_b67_oetf:arib_b67_inverse_eotf;
func.to_linear_scale=scene_referred?12.0f:static_cast<float>(1000.0/peak_luminance);
func.to_gamma_scale=scene_referred?1.0f/12.0f:static_cast<float>(peak_luminance/1000.0);
break;
default:
error::throw_<error::InternalError>("invalidtransfercharacteristics");
break;
}
returnfunc;
}
从上面的代码中咱们能够知道,由于咱们这儿 PQ 曲线对应的是 SMPTE ST 2084 规范,转化函数to_linear
即st_2084_eotf
,to_linear_scale
则为ST2084_PEAK_LUMINANCE / peak_luminance
。
st_2084_eotf
函数的完结如下:
//src/zimg/colorspace/gamma.cpp
constexprfloatST2084_M1=0.1593017578125f;
constexprfloatST2084_M2=78.84375f;
constexprfloatST2084_C1=0.8359375f;
constexprfloatST2084_C2=18.8515625f;
constexprfloatST2084_C3=18.6875f;
constexprfloatFLT_MIN1.17549435082228750797e-38F
floatst_2084_eotf(floatx)noexcept
{
//FilternegativevaluestoavoidNAN.
if(x>0.0f){
floatxpow=zimg_x_powf(x,1.0f/ST2084_M2);
floatnum=std::max(xpow-ST2084_C1,0.0f);
floatden=std::max(ST2084_C2-ST2084_C3*xpow,FLT_MIN);
x=zimg_x_powf(num/den,1.0f/ST2084_M1);
}else{
x=0.0f;
}
returnx;
}
到这儿,处理一个运用了 PQ 规范的 HDR 视频,去获取对应 EOTF 转化函数及参数的中心进程就介绍完了。总结起来首要进程如下:
get_neighboring_colorspaces
->create_gamma_to_linear_operation
->create_inverse_gamma_operation
->select_transfer_function
caseTransferCharacteristics::ST_2084:
func.to_linear=scene_referred?st_2084_inverse_oetf:st_2084_eotf;
func.to_linear_scale=static_cast<float>(ST2084_PEAK_LUMINANCE/peak_luminance);
break;
2)HDR 线性光信号做颜色空间转化。
由于 HDR 和 SDR 运用的颜色空间是不同的,HDR 一般运用 BT.2020,SDR 一般用 BT.709,所以要做一下颜色空间转化。
这个进程对应的指令参数:zscale=p=bt709
,表明转化的方针颜色空间是 bt709。
这儿首要是依据颜色空间转化矩阵来做一下转化即可。咱们仍是能够从zimg[6]源代码中找到关键函数:
延时空间的转化也会先调用get_neighboring_colorspaces
函数。
//src/zimg/colorspace/graph.cpp
//创立颜色空间转化函数
std::vector<ColorspaceNode>get_neighboring_colorspaces(constColorspaceDefinition&csp)
{
zassert_d(is_valid_csp(csp),"invalidcolorspace");
std::vector<ColorspaceNode>edges;
autoadd_edge=[&](constColorspaceDefinition&out_csp,autofunc)
{
edges.emplace_back(out_csp,std::bind(func,csp,out_csp,std::placeholders::_1,std::placeholders::_2));
};
if(csp.matrix==MatrixCoefficients::RGB){
constexprMatrixCoefficientsspecial_matrices[]={
MatrixCoefficients::UNSPECIFIED,
MatrixCoefficients::RGB,
MatrixCoefficients::REC_2020_CL,
MatrixCoefficients::CHROMATICITY_DERIVED_NCL,
MatrixCoefficients::CHROMATICITY_DERIVED_CL,
MatrixCoefficients::REC_2100_LMS,
MatrixCoefficients::REC_2100_ICTCP,
};
//RGBcanbeconvertedtoconventionalYUV.
for(automatrix:all_matrix()){
if(std::find(std::begin(special_matrices),std::end(special_matrices),matrix)==std::end(special_matrices))
add_edge(csp.to(matrix),create_ncl_rgb_to_yuv_operation);
}
if(csp.primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(MatrixCoefficients::CHROMATICITY_DERIVED_NCL),create_ncl_rgb_to_yuv_operation);
//LinearRGBcanbeconvertedtoothertransferfunctionsandprimaries;alsotocombinedmatrix-transfersystems.
if(csp.transfer==TransferCharacteristics::LINEAR){
for(autotransfer:all_transfer()){
if(transfer!=csp.transfer&&transfer!=TransferCharacteristics::UNSPECIFIED){
add_edge(csp.to(transfer),create_linear_to_gamma_operation);
if(csp.primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(transfer).to(MatrixCoefficients::CHROMATICITY_DERIVED_CL),create_cl_rgb_to_yuv_operation);
}
}
if(csp.primaries!=ColorPrimaries::UNSPECIFIED){
for(autoprimaries:all_primaries()){
if(primaries!=csp.primaries&&primaries!=ColorPrimaries::UNSPECIFIED)
add_edge(csp.to(primaries),create_gamut_operation);
}
}
add_edge(csp.to(MatrixCoefficients::REC_2020_CL).to(TransferCharacteristics::REC_709),create_cl_rgb_to_yuv_operation);
if(csp.primaries==ColorPrimaries::REC_2020)
add_edge(csp.to(MatrixCoefficients::REC_2100_LMS),create_ncl_rgb_to_yuv_operation);
}elseif(csp.transfer!=TransferCharacteristics::UNSPECIFIED){
//GammaRGBcanbeconvertedtolinearRGB.
add_edge(csp.to_linear(),create_gamma_to_linear_operation);
}
}elseif(csp.matrix==MatrixCoefficients::REC_2020_CL||csp.matrix==MatrixCoefficients::CHROMATICITY_DERIVED_CL){
add_edge(csp.to_rgb().to_linear(),create_cl_yuv_to_rgb_operation);
}elseif(csp.matrix==MatrixCoefficients::REC_2100_LMS){
//LMSwithST_2084orARIB_B67transferfunctionscanbeconvertedtoICtCpandalsotolineartransferfunction.
if(csp.transfer==TransferCharacteristics::ST_2084||csp.transfer==TransferCharacteristics::ARIB_B67){
add_edge(csp.to(MatrixCoefficients::REC_2100_ICTCP),create_lms_to_ictcp_operation);
add_edge(csp.to(TransferCharacteristics::LINEAR),create_gamma_to_linear_operation);
}
//LMSwithlineartransferfunctioncanbeconvertedtoRGBmatrixandtoARIB_B67andST_2084transferfunctions.
if(csp.transfer==TransferCharacteristics::LINEAR){
add_edge(csp.to_rgb(),create_ncl_yuv_to_rgb_operation);
add_edge(csp.to(TransferCharacteristics::ST_2084),create_linear_to_gamma_operation);
add_edge(csp.to(TransferCharacteristics::ARIB_B67),create_linear_to_gamma_operation);
}
}elseif(csp.matrix==MatrixCoefficients::REC_2100_ICTCP){
//ICtCpwithST_2084orARIB_B67transferfunctionscanbeconvertedtoLMS.
if(csp.transfer==TransferCharacteristics::ST_2084||csp.transfer==TransferCharacteristics::ARIB_B67)
add_edge(csp.to(MatrixCoefficients::REC_2100_LMS),create_ictcp_to_lms_operation);
}elseif(csp.matrix!=MatrixCoefficients::UNSPECIFIED){
//YUVcanbeconvertedtoRGB.
add_edge(csp.to_rgb(),create_ncl_yuv_to_rgb_operation);
}
returnedges;
}
在get_neighboring_colorspaces
函数中会依据 ColorPrimaries 的差异来将ZIMG_PRIMARIES_BT2020
转化为ZIMG_PRIMARIES_BT709
,这个进程会调用create_gamut_operation
进行 rgb 与 xyz 对应颜色空间的转化。
//src/zimg/colorspace/operation.cpp
//创立rgb与xyz颜色空间转化矩阵
std::unique_ptr<Operation>create_gamut_operation(constColorspaceDefinition&in,constColorspaceDefinition&out,constOperationParams¶ms,CPUClasscpu)
{
zassert_d(in.matrix==MatrixCoefficients::RGB&&in.transfer==TransferCharacteristics::LINEAR,"mustbelinearRGB");
zassert_d(out.matrix==MatrixCoefficients::RGB&&out.transfer==TransferCharacteristics::LINEAR,"mustbelinearRGB");
Matrix3x3m=gamut_xyz_to_rgb_matrix(out.primaries)*white_point_adaptation_matrix(in.primaries,out.primaries)*gamut_rgb_to_xyz_matrix(in.primaries);
returncreate_matrix_operation(m,cpu);
}
这儿的完结是用 CIE XYZ 颜色空间作为中转,先把原颜色空间转为 XYZ 颜色空间,再把 XYZ 颜色空间转为方针颜色空间,整个进程能够了解为像素直接和两个转化矩阵相乘。其间对应的 BT.2020 RGB 转 XYZ 颜色空间、XYZ 转 BT.709 颜色空间的矩阵如下:
//BT2020
RGB2XYZMatrix:
0.6370,0.1446,0.1689
0.2627,0.6780,0.0593
0.0000,0.0281,1.0610
//BT709
XYZ2RGBMatrix:
3.2410,-1.5374,-0.4986
-0.9692,1.8760,0.0416
0.0556,-0.2040,1.0570
总结一下上述进程大致如下:
get_neighboring_colorspaces
->create_gamut_operation
->gamut_xyz_to_rgb_matrix&&gamut_rgb_to_xyz_matrix
3)HDR 的线性模仿光信号 ToneMapping 转化到 SDR 的线性模仿光信号。
通过上一步取得的 EOTF 转化函数,完结颜色数字信号转化为线性的模仿光信号后,接下来咱们要做的是将 HDR 的线性模仿光信号 ToneMapping 转化到 SDR 的线性模仿光信号。
这个进程对应的指令参数:tonemap=tonemap=hable:desat=0
,表明 tonemap 的算法用 hable,减饱满强度(desaturation strength)为 0。
这儿用到的中心代码是FFmpeg[7]的视频滤镜模块中的ffmpeg/libavfilter/vf_tonemap.c[8]。具体代码如下:
//ffmpeg/libavfilter/vf_tonemap.c
#defineMIX(x,y,a)(x)*(1-(a))+(y)*(a)
staticvoidtonemap(TonemapContext*s,AVFrame*out,constAVFrame*in,
constAVPixFmtDescriptor*desc,intx,inty,doublepeak)
{
intmap[3]={desc->comp[0].plane,desc->comp[1].plane,desc->comp[2].plane};
constfloat*r_in=(constfloat*)(in->data[map[0]]+x*desc->comp[map[0]].step+y*in->linesize[map[0]]);
constfloat*g_in=(constfloat*)(in->data[map[1]]+x*desc->comp[map[1]].step+y*in->linesize[map[1]]);
constfloat*b_in=(constfloat*)(in->data[map[2]]+x*desc->comp[map[2]].step+y*in->linesize[map[2]]);
float*r_out=(float*)(out->data[map[0]]+x*desc->comp[map[0]].step+y*out->linesize[map[0]]);
float*g_out=(float*)(out->data[map[1]]+x*desc->comp[map[1]].step+y*out->linesize[map[1]]);
float*b_out=(float*)(out->data[map[2]]+x*desc->comp[map[2]].step+y*out->linesize[map[2]]);
floatsig,sig_orig;
/*loadvalues*/
*r_out=*r_in;
*g_out=*g_in;
*b_out=*b_in;
/*desaturatetopreventunnaturalcolors*/
if(s->desat>0){
floatluma=s->coeffs->cr**r_in+s->coeffs->cg**g_in+s->coeffs->cb**b_in;
floatoverbright=FFMAX(luma-s->desat,1e-6)/FFMAX(luma,1e-6);
*r_out=MIX(*r_in,luma,overbright);
*g_out=MIX(*g_in,luma,overbright);
*b_out=MIX(*b_in,luma,overbright);
}
/*pickthebrightestcomponent,reducingthevaluerangeasnecessary
*tokeeptheentiresignalinrangeandpreventingdiscolorationdueto
*out-of-boundsclipping*/
sig=FFMAX(FFMAX3(*r_out,*g_out,*b_out),1e-6);
sig_orig=sig;
switch(s->tonemap){
default:
caseTONEMAP_NONE:
//donothing
break;
caseTONEMAP_LINEAR:
sig=sig*s->param/peak;
break;
caseTONEMAP_GAMMA:
sig=sig>0.05f?pow(sig/peak,1.0f/s->param)
:sig*pow(0.05f/peak,1.0f/s->param)/0.05f;
break;
caseTONEMAP_CLIP:
sig=av_clipf(sig*s->param,0,1.0f);
break;
caseTONEMAP_HABLE:
sig=hable(sig)/hable(peak);
break;
caseTONEMAP_REINHARD:
sig=sig/(sig+s->param)*(peak+s->param)/peak;
break;
caseTONEMAP_MOBIUS:
sig=mobius(sig,s->param,peak);
break;
}
/*applythecomputedscalefactortothecolor,
*linearlytopreventdiscoloration*/
*r_out*=sig/sig_orig;
*g_out*=sig/sig_orig;
*b_out*=sig/sig_orig;
}
这儿运用的 tonemap 算法是 hable,代码如下:
//ffmpeg/libavfilter/vf_tonemap.c
staticfloathable(floatin)
{
floata=0.15f,b=0.50f,c=0.10f,d=0.20f,e=0.02f,f=0.30f;
return(in*(in*a+b*c)+d*e)/(in*(in*a+b)+d*f)-e/f;
}
staticfloatmobius(floatin,floatj,doublepeak)
{
floata,b;
if(in<=j)
returnin;
a=-j*j*(peak-1.0f)/(j*j-2.0f*j+peak);
b=(j*j-2.0f*j*peak+peak)/FFMAX(peak-1.0f,1e-6);
return(b*b+2.0f*b*j+j*j)/(b-a)*(in+a)/(in+b);
}
tonemap 本质上是一个编码紧缩曲线,能够简略的了解其目的是为了把[0, 1024]
的空间规模如何较好的紧缩映射到[0, 255]
的空间规模。hable 是 tonemap 的一种算法,其他算法还有上面贴出来的 reinhard、mobius。更多的算法能够参阅:Tone mapping 进化论[9]。
4)线性的模仿光信号经过 OETF 转化为非线性颜色数字信号。
做完 ToneMapping 后,咱们就得到了契合 SDR 数据规模的线性模仿光信号。接下来咱们再将其转化为颜色数字信号。不过,由于是 SDR,这儿要运用的 OETF 是 BT.709。
这个进程对应的指令参数:zscale=t=bt709:m=bt709:r=tv
,表明运用的 OETF 转化函数为 bt709,转化矩阵也是 bt709,YUV 的 range 为 tv.limited。
代码调用流程和第一步相似,这儿只贴一下流程:
get_neighboring_colorspaces
->reate_linear_to_gamma_operation
->create_gamma_operation
->select_transfer_function
caseTransferCharacteristics::REC_709:
func.to_gamma=scene_referred?rec_709_oetf:rec_1886_inverse_eotf;
break;
最终对应的转化函数为rec_709_oetf
,代码如下:
//src/zimg/colorspace/gamma.cpp
constexprfloatREC709_ALPHA=1.09929682680944f;
constexprfloatREC709_BETA=0.018053968510807f;
floatrec_709_oetf(floatx)noexcept
{
x=std::max(x,0.0f);
if(x<REC709_BETA)
x=x*4.5f;
else
x=REC709_ALPHA*zimg_x_powf(x,0.45f)-(REC709_ALPHA-1.0f);
returnx;
}
参阅:
- 仿照 FFmpeg 在 GLSL 中处理 HDR.ToneMapping[10]
- HDR in Android[11]
- HDR 片源压制成 BT.709 颜色空间的 SDR 视频[12]
3.2、运用 FFmpeg 软解 + OpenGL 完结转化
上面讲了运用 FFmpeg filter 完结转化的计划,这儿有两个问题:一个是 FFmpeg 软解功能的问题,别的一个是运用 CPU 做 filter 功能的问题。咱们这儿先解决一下用 CPU 做 filter 的功能问题:将 EOTF、颜色空间转化、ToneMapping、OETF 运用 OpenGL 完结,然后将这些操作用 GPU 来完结。一起这儿咱们也能够一起支撑 PQ 和 HLG 规范。
这儿需求留意的是,FFmpeg 软解完结中解码出来的数据格局一般为AV_PIX_FMT_YUV420P10LE
,小端序,低 10 位有用,高 6 位均为 0,所以能够直接被 OpenGL 读取,不需求做移位操作。可是,要把这样的 YUV 10bit 的数据转为 Texture 纹路则需求做一下兼容处理,运用 16bit 的数据结构来存储 YUV 10bit。
YUV 10bit
下面是将 EOTF、颜色空间转化、ToneMapping、OETF 流程用 OpenGL ES Fragment Shader 完结的代码:
precisionhighpfloat;
uniformsampler2DinputImageTexture;
uniformmediumpmat3colorConversionMatrix;
uniformmediumpintisSt2084;
uniformmediumpintisAribB67;
varyinghighpvec2textureCoordinate;
#defineFFMAX(a,b)((a)>(b)?(a):(b))
#defineFFMAX3(a,b,c)FFMAX(FFMAX(a,b),c)
highpvec3YuvConvertRGB_BT2020(highpvec3yuv,intnormalize){
highpvec3rgb;
//[64,960]
floatr=float(yuv.x-64.)*1.164384-float(yuv.z-512.)*-1.67867;
floatg=float(yuv.x-64.)*1.164384-float(yuv.y-512.)*0.187326-float(yuv.z-512.)*0.65042;
floatb=float(yuv.x-64.)*1.164384-float(yuv.y-512.)*-2.14177;
rgb.r=r;
rgb.g=g;
rgb.b=b;
if(normalize==1){
rgb/=1024.0;
}
returnrgb;
}
//[aribb67eotf
consthighpfloatARIB_B67_A=0.17883277;
consthighpfloatARIB_B67_B=0.28466892;
consthighpfloatARIB_B67_C=0.55991073;
highpfloatarib_b67_inverse_oetf(highpfloatx)
{
//Preventnegativepixelsexpandingintopositivevalues.
x=max(x,0.0);
if(x<=0.5)
x=(x*x)*(1.0/3.0);
else
x=(exp((x-ARIB_B67_C)/ARIB_B67_A)+ARIB_B67_B)/12.0;
returnx;
}
highpfloatootf_1_2(highpfloatx)
{
returnx<0.0?x:pow(x,1.2);
}
highpfloatarib_b67_eotf(highpfloatx)
{
returnootf_1_2(arib_b67_inverse_oetf(x));
}
//aribb67eotf]
//[st2084eotf
highpfloatST2084_M1=0.1593017578125;
constfloatST2084_M2=78.84375;
constfloatST2084_C1=0.8359375;
constfloatST2084_C2=18.8515625;
constfloatST2084_C3=18.6875;
highpfloatFLT_MIN=1.17549435082228750797e-38;
highpfloatst_2084_eotf(highpfloatx)
{
highpfloatxpow=pow(x,float(1.0/ST2084_M2));
highpfloatnum=max(xpow-ST2084_C1,0.0);
highpfloatden=max(ST2084_C2-ST2084_C3*xpow,FLT_MIN);
returnpow(num/den,1.0/ST2084_M1);
}
//st2084eotf]
//[tonemaphable
highpfloathableF(highpfloatinVal)
{
highpfloata=0.15,b=0.50,c=0.10,d=0.20,e=0.02,f=0.30;
return(inVal*(inVal*a+b*c)+d*e)/(inVal*(inVal*a+b)+d*f)-e/f;
}
//tonemaphable]
//[bt709
highpfloatrec_1886_inverse_eotf(highpfloatx)
{
returnx<0.0?0.0:pow(x,1.0/2.4);
}
highpfloatrec_1886_eotf(floatx)
{
returnx<0.0?0.0:pow(x,2.4);
}
//bt709]
voidmain(){
highpvec3rgb10bit=texture2D(inputImageTexture,textureCoordinate).rgb;
//1、HDR非线性电信号转为HDR线性光信号(EOTF)
floatpeak_luminance=100.0;
floatST2084_PEAK_LUMINANCE=10000.0;
floatto_linear_scale;
highpvec3fragColor;
if(isSt2084==1){
to_linear_scale=10000.0/peak_luminance;
fragColor=to_linear_scale*vec3(st_2084_eotf(rgb10bit.r),st_2084_eotf(rgb10bit.g),st_2084_eotf(rgb10bit.b));
}elseif(isAribB67==1){
to_linear_scale=1000.0/peak_luminance;
fragColor=to_linear_scale*vec3(arib_b67_eotf(rgb10bit.r),arib_b67_eotf(rgb10bit.g),arib_b67_eotf(rgb10bit.b));
}else{
fragColor=vec3(rec_1886_eotf(rgb10bit.r),rec_1886_eotf(rgb10bit.g),rec_1886_eotf(rgb10bit.b));
}
//2、HDR线性光信号做颜色空间转化(ColorSpaceConverting)
//color-primariesREC_2020toREC_709
mat3rgb2xyz2020=mat3(0.6370,0.1446,0.1689,
0.2627,0.6780,0.0593,
0.0000,0.0281,1.0610);
mat3xyz2rgb709=mat3(3.2410,-1.5374,-0.4986,
-0.9692,1.8760,0.0416,
0.0556,-0.2040,1.0570);
fragColor*=rgb2xyz2020*xyz2rgb709;
//3、HDR线性光信号颜色映射为SDR线性光信号(ToneMapping)
highpfloatsig=FFMAX(FFMAX3(fragColor.r,fragColor.g,fragColor.b),1e-6);
highpfloatsig_orig=sig;
floatpeak=10.0;
sig=hableF(sig)/hableF(peak);
fragColor*=sig/sig_orig;
//4、SDR线性光信号转SDR非线性电信号(OETF)
fragColor=vec3(rec_1886_inverse_eotf(fragColor.r),rec_1886_inverse_eotf(fragColor.g),rec_1886_inverse_eotf(fragColor.b));
gl_FragColor=vec4(fragColor,1.0);
}
到这儿,咱们就把原本运用 FFmpegzscale
和tonemap
两个 Filter 的逻辑就搬迁为 OpenGL 完结了。
3.3、运用 Android 硬解 + OpenGL 完结转化
接下来,咱们接着解决 FFmpeg 软解的功能问题,在硬解支撑较好的机型上运用硬解来完结。Android 硬解能够将视频解码为 Texture 纹路,所以相关于软解只要完结从纹路中读出 YUV10bit 数据,然后完结后续的:EOTF、颜色空间转化、ToneMapping、OETF进程,就能够完结 HDR 转 SDR 了。
完结这个进程的 OpenGL ES Fragment Shader 代码和上一节中一样即可,这儿就不再重复了。
参阅资料
[1]
Standard-dynamic-range video:en.wikipedia.org/wiki/Standa…
[2]
High-dynamic-range:en.wikipedia.org/wiki/High-d…
[3]
HDR 技能趋势浅析:mp.weixin.qq.com/s?__biz=MzU…
[4]
zimg:github.com/sekrit-twc/…
[5]
zimg:github.com/sekrit-twc/…
[6]
zimg:github.com/sekrit-twc/…
[7]
FFmpeg:github.com/FFmpeg/FFmp…
[8]
vf_tonemap.c:github.com/FFmpeg/FFmp…
[9]
Tone mapping 进化论:zhuanlan.zhihu.com/p/21983679
[10]
仿照 FFmpeg 在 GLSL 中处理 HDR.ToneMapping:blog.csdn.net/a360940265a…
[11]
HDR in Android:blog.csdn.net/a360940265a…
[12]
HDR 片源压制成 BT.709 颜色空间的 SDR 视频:www.bilibili.com/read/cv3936…