I realised I haven’t written a blog post in a while. I actually have many drafts, but they are all in a state of being unfinished.
I’m porting over an issue from GitHub that helps with the correct use and understanding of various HDR transfer functions.
https://github.com/colour-science/colour/issues/1348
The gist of the issue is that, following Table 1 of BT.2408, if you place diffuse white at a display luminance of 203.0 nits, the calculated display luminance for an 18% grey card is not 26 nits.
BT.2408 and the Problem
BT.2408 is a standard concerning operational practices for HDR. Table 1 recommends the display luminance and corresponding code values under different transfer functions for several objects.
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 |
The question is how to use HDR transfer functions to convert between these numbers, for instance, using Python’s colour
library.
import colour
Theory
The table is divided into three parts. The first column is reflectance, corresponding to scene-referred linear light. The second column is display luminance, which is display-referred linear light. The last two columns are the code values for the two systems.
Their numerical ranges are:
- Scene Light: 0.0 - 1.0. This is relative and, for example, in a photography system, is controlled by the camera’s exposure. The scene light for a grey card and diffuse white could be 0.18 and 1.0, or it could be 0.018 and 0.1.
- Display Light: 0.0 - 10000 nits. This is absolute in an HDR system. For the HLG system, a peak luminance must be specified, for example, 1000 nits.
- Code Value: 0 - 255 or 0 - 1023, depending on the system’s bit depth. It can also be represented by floating-point numbers from 0.0 - 1.0. Personally, I find floating-point numbers easier to work with.
A transfer function system defines how to perform these conversions between luminance and code values.
- The conversion from scene linear light to display linear light is called the opto-opto transfer function (OOTF).
- The conversion from scene linear light to code value is called the OETF (Optical-Electrical Transfer Function).
- The conversion from code value to display linear light is called the EOTF (Electrical-Optical Transfer Function).
It is particularly important to note that the OETF is not the inverse of the EOTF. The conversion from display linear light to code value should use the inverse of the EOTF, not the OETF. Similarly, the conversion from code value to scene linear light should use the inverse of the OETF, not the EOTF.
The conversion relationships between scene light, display light, and code values actually only require two transfer functions. PQ uses the EOTF and OOTF, while HLG uses the OETF and OOTF. In BT.2100, the PQ OETF is equivalent to the OOTF followed by the inverse EOTF, and the HLG EOTF is equivalent to the inverse OETF followed by the OOTF.
The colour
library provides implementations of these transfer functions, including the PQ OETF and the HLG EOTF.
PQ
PQ is display-referred. The complete conversion process is: use the OOTF to convert scene linear light to display linear light, then use the inverse EOTF to convert display linear light to a code value. During display, the EOTF is used to convert the code value back to display linear light.
Let’s start by setting the display luminance of diffuse white to 203 nits. The corresponding scene linear light is obtained using the inverse OOTF. In scene linear light, the ratio of the grey card to diffuse white is 0.18. We can calculate the scene light for the grey card and then apply the OOTF to get its display linear light (26 nits). By applying the inverse EOTF to the display linear light of both diffuse white and the grey card, we get their corresponding code values.
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 is scene-referred. The complete conversion process is: use the OETF to convert scene linear light to a code value. During decoding, use the inverse OETF to convert the code value back to scene linear light, and then use the OOTF to convert the scene linear light to display linear light.
Similarly, we set the display luminance of diffuse white to 203 nits. The corresponding scene linear light is obtained using the inverse OOTF. In scene linear light, the ratio of the grey card to diffuse white is 0.18. We calculate the scene light for the grey card, apply the OETF to the scene light to get the code value, and apply the OOTF to the scene light to get the display linear light.
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
It is worth noting that the HLG OOTF changes with the peak luminance of the display. If no parameters are passed, colour
defaults to a peak luminance of 1000 nits, for which the corresponding OOTF is a gamma of 1.2. Additionally, the HLG OOTF should be applied to the RGB components; passing only a luminance value will raise a warning, but the approach and the result are correct.
A Few Afterthoughts
As for why the OETF and EOTF cannot be used as inverse functions of each other, it is precisely because the OOTF is at play. In the most ideal scenario, where the display perfectly reproduces the scene light, the OOTF would be a linear function. In that case, the OETF and EOTF would be inverse functions (with a luminance coefficient).
The OOTF used by PQ is a combination of the BT.709 OETF and the BT.1886 EOTF, with a curvature between linear and a gamma of 1.2. The OOTF used by HLG is a luminance-dependent gamma function, which is a gamma of 1.2 at 1000 nits.
Personally, I feel that understanding these transfer functions is necessary, especially in HDR systems where conversions between different transfer functions are often required. The table in BT.2408 provides a good reference, but in practical applications, these transfer functions, especially the OOTF, are rarely referenced directly. When creating PQ content, adjustments are typically made with respect to display light, and then the inverse EOTF is used to convert to code values, without involving the OOTF. The design of HLG to use an OOTF is more likely for compatibility with SDR systems and broadcast production.
This small experiment also serves as good proof that as long as the calculations are correct, the results obtained using either transfer function will be the same (aside from quantisation differences). It is not the case that using HLG will produce a different effect from PQ.
English Lesson
In the issue, there is a sentence: Now, the penny finally dropped.
This expression originates from old-fashioned vending machines, which required a coin to be inserted before they would operate. Sometimes the coin would get stuck and only fall into place after a moment, at which point the machine would start working. Therefore, this phrase is a metaphor for ‘suddenly understanding’ or ‘grasping something one didn’t understand before’.