发现好久没有写博客了,其实有很多草稿,但都处于烂尾的状态。

搬运一个 Github 上的 Issue,有助于正确使用和理解各种 HDR 传递函数。

https://github.com/colour-science/colour/issues/1348

Issue 的大致意思是,按照 BT.2408 的表1,把漫射白放在 203.0 nits 的显示亮度,计算出18% 灰卡的显示亮度不是 26 nits。

BT.2408 和问题

BT.2408 是一份关于 HDR 操作实践的标准,表1 推荐了一些物体的显示亮度和不同传递函数下的码值。

Reflectance Object or Reference (Luminance Factor, %) Nominal Luminance, cd/m² (PQ & 1000 cd/m² HLG) Nominal Signal Level Nominal Signal Level
Grey Card (18%) 26 38 38
Greyscale Chart Max (83%) 162 56 71
Greyscale Chart Max (90%) 179 57 73
Reference Level: HDR Reference White (100%) also diffuse white and Graphics White 203 58 75

问题是如何使用 HDR 传递函数,完成这些数字间的转换,比如使用 Python 的 colour 库。

import colour

理论

表格分三部分,第一列是反射率,对应场景参考的线性光(Scene Referred Linear Light),第二列是显示亮度,也就是显示参考的线性光(Display Referred Linear Light),后两列是两个系统下的编码值(Code Value)。

他们的数值范围是:

  • 场景光:0.0 - 1.0,是相对的,比如在一个摄影系统里由相机的曝光控制。一个灰卡和漫射白的场景光可能是 0.18 和 1.0,也可能是 0.018 和 0.1。
  • 显示光:0.0 - 10000 nits,在 HDR 系统中是绝对的,对 HLG 系统来说,要规定一个峰值亮度,比如 1000 nits。
  • 码值:0 - 255 或者 0 - 1023,取决于系统的位深,也可以用 0.0 - 1.0 的浮点数表示,个人觉得浮点数会好用一点。

一个传递函数系统定义了如何完成这些亮度和码值的转换。

  • 场景线性光到显示线性光的转换称为 OOTF(Opto-Optical Transfer Function)
  • 从场景线性光到码值的转换称为 OETF(Optical-Electrical Transfer Function)
  • 从码值到显示线性光的转换称为 EOTF(Electrical-Optical Transfer Function)

特别注意的是,OETF 不是 EOTF 的逆函数。从显示线性光到码值的转换应当使用 EOTF 的逆函数,而不是 OETF,从码值到场景线性光的转换同理,应该使用 OETF 的逆函数,而不是 EOTF。

场景光、显示光和码值之间的转换关系实际上只需要两个传递函数,PQ 用到的是 EOTF 和 OOTF,HLG 用到的是 OETF 和 OOTF。在 BT.2100 中,PQ 的 OETF 等价于 OOTF 接 EOTF 的逆函数,HLG 的 EOTF 等价于 OETF 的逆函数接 OOTF。

colour 里有提供这些传递函数的实现,包括 PQ 的 OETF 和 HLG 的 EOTF。

PQ

PQ 是显示参考的,完整的转换关系是:使用 OOTF 将场景线性光转换为显示线性光,然后用 EOTF 的逆函数将显示线性光转换为码值。显示的时候使用 EOTF 将码值转换为显示线性光。

我们先把漫射白放在 203 nits 的显示亮度,此时的场景线性光由 OOTF 的逆函数得到,场景线性光中,灰卡和漫射白的比例是 0.18,计算得到灰卡的场景光,应用 OOTF 得到灰卡的显示线性光(26 nits)。对漫射白和灰卡的显示线性光应用 EOTF 的逆函数,得到他们的对应码值。

disp_lin_white = 203
scene_lin_white = colour.models.ootf_inverse_BT2100_PQ(disp_lin_white)
print("Scene Linear:", scene_lin_white)  # 0.0307311888745
scene_lin_grey = scene_lin_white * 0.18
print("Scene Linear Grey:", scene_lin_grey)  # 0.00553161399741
disp_lin_grey = colour.models.ootf_BT2100_PQ(scene_lin_grey)
print("Display Linear Grey:", disp_lin_grey)  # 25.6890888306
signal_white = colour.models.eotf_inverse_BT2100_PQ(disp_lin_white)
print("Signal White:", signal_white)  # 0.580688881042
signal_grey = colour.models.eotf_inverse_BT2100_PQ(disp_lin_grey)
print("Signal Grey:", signal_grey)  # 0.378961634455

HLG

HLG 是场景参考的,完整的转换关系是:使用 OETF 将场景线性光转换为码值,解码时使用 OETF 的逆函数将码值转换为场景线性光,然后使用 OOTF 将场景线性光转换为显示线性光。

同样,把漫射白放在 203 nits 的显示亮度,此时的场景线性光由 OOTF 的逆函数得到,场景线性光中,灰卡和漫射白的比例是 0.18,计算得到灰卡的场景光,对场景光应用 OETF 得到码值,对场景光应用 OOTF 得到显示线性光。

disp_lin_white = 203
scene_lin_white = colour.models.ootf_inverse_BT2100_HLG(disp_lin_white)
print("Scene Linear:", scene_lin_white)  # 0.264797185624
scene_lin_grey = scene_lin_white * 0.18
print("Scene Linear Grey:", scene_lin_grey)  # 0.0476634934123
disp_lin_grey = colour.models.ootf_BT2100_HLG(scene_lin_grey)
print("Display Linear Grey:", disp_lin_grey)  # 25.9312256307
signal_white = colour.models.oetf_BT2100_HLG(scene_lin_white)
print("Signal White:", signal_white)  # 0.749877364632
signal_grey = colour.models.oetf_BT2100_HLG(scene_lin_grey)
print("Signal Grey:", signal_grey)  # 0.378140820644

需要注意的是,HLG 的 OOTF 会随着显示峰值亮度变化而变化,不传任何参数的情况下,colour 默认使用 1000 nits 的峰值亮度,对应的 OOTF 是一个 1.2 的 gamma。另外,HLG 的 OOTF 应当作用在 RGB 分量上,只传一个亮度会提出 Warning,但思路和结果是正确的。

一点后记

关于为什么不能把 OETF 和 EOTF 当作逆函数使用,正是因为 OOTF 在其中作怪,最理想的情况下,显示器完全再现场景光,OOTF 应该是一个线性的函数,此时 OETF 和 EOTF 是逆函数(用一个亮度做系数)。

PQ 用到的 OOTF 是 BT.709 OETF 和 BT.1886 EOTF 的组合,弯曲程度介于线性和 gamma 1.2 之间,HLG 用到的是一个与亮度相关的伽马函数,1000 nits 时为 gamma 1.2。

个人觉得,理解这些传递函数是必要的,尤其是 HDR 系统中,很多时候需要在不同的传递函数间转换。BT.2408 的表格提供了一个很好的参考,但实际应用中,很少会真的参考这些传递函数,尤其是 OOTF,制作 PQ 内容的时候,通常是面向显示光调整后,用 EOTF 的逆函数转换为码值,并不用到 OOTF。HLG 要用到 OOTF 的设计更可能是为了兼容 SDR 系统和广播制作。

这个小实验也很好的证明了:只要运算正确,用什么传递函数得到的结果都是一样的(除了量化的区别),不存在用了 HLG 效果会和 PQ 不一样。

英语小课堂

Issue 里有一句:Now, the penny finally dropped.

这个表达来源于老式自动售货机,需要一枚硬币投入后才会工作,有时硬币卡住了,过一会儿才掉下去,机器才开始工作。所以这个短语比喻“突然明白过来”或“领悟到之前没搞懂的事情”。