1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
//! Opto-electronic / electro-optical transfer functions.
//!
//! Transfers map between non-linear video signal values (the `[0, 1]`
//! domain encoded in a pixel buffer) and linear-light values (relative
//! scene-referred for an OETF, or display-referred luminance for an
//! EOTF). They are an orthogonal axis to the YUV↔RGB matrix selected by
//! [`crate::convert::ColorSpace`]; a complete HDR pipeline mixes a
//! BT.2020 / BT.2100 matrix with a PQ or HLG transfer.
//!
//! Equations and exact constants are reproduced from:
//!
//! - `docs/video/signal-metadata/R-REC-BT.2020-2-201510-I.pdf` Table 4
//! (BT.709 / BT.2020 OETF; the same `α = 1.099, β = 0.018` curve has
//! been the SDR transmission characteristic since BT.709).
//! - `docs/video/signal-metadata/R-REC-BT.1886-0-201103-I.pdf` Annex 1
//! (BT.1886 reference EOTF, `L = a · max(V+b, 0)^γ`, `γ = 2.40`).
//! - `docs/video/signal-metadata/R-REC-BT.2100-3-202502-I.pdf` Tables 4–5
//! (PQ EOTF / OETF, HLG OETF / EOTF / OOTF).
//!
//! All functions operate on `f32`. Their domains are explicitly
//! documented; callers are responsible for any preceding quantisation
//! step (10/12-bit narrow- or full-range integer formats per BT.2020
//! Table 5 / BT.2100 Table 9).
//!
//! The hot paths intentionally stay on f32 instead of fixed-point —
//! transfer-function callers tend to be downstream of float-RGB tone
//! mapping and are not in the per-pixel SDR YUV↔RGB hot loop.
/// Identifier for a transfer function.
///
/// `BT709Oetf` and `BT2020_10Oetf` share the same `α = 1.099, β = 0.018`
/// curve from BT.2020-2 Table 4 (the 10-bit values); they are separate
/// variants so that callers can document which signalling chain they
/// belong to. The 12-bit variant uses the more-precise `α = 1.0993,
/// β = 0.0181` constants.
// ------------------------------------------------------------------
// BT.709 / BT.2020 SDR OETF — BT.2020-2 Table 4 row "Non-linear
// transfer function": E' = 4.5·E for 0 ≤ E < β, otherwise
// E' = α·E^0.45 − (α − 1).
//
// The 10-bit constants are α = 1.099, β = 0.018; the 12-bit precision
// values are α = 1.0993, β = 0.0181 (same table, "for 12-bit systems"
// column).
const BT709_ALPHA: f32 = 1.099;
const BT709_BETA: f32 = 0.018;
const BT2020_12_ALPHA: f32 = 1.0993;
const BT2020_12_BETA: f32 = 0.0181;
/// BT.709 / BT.2020 OETF (10-bit precision constants).
///
/// Maps relative scene-light `E ∈ [0, 1]` to the non-linear signal
/// `E' ∈ [0, 1]`. Values below 0 are clamped to 0; values above 1 pass
/// through the non-linear branch (BT.2020-2 doesn't define behaviour
/// outside `[0, 1]` — callers wanting to clip should do so afterwards).
/// BT.2020 12-bit OETF (more-precise α/β constants).
/// Inverse of [`bt709_oetf`] (the EOTF the camera curve implies).
///
/// The crossover point in the inverse direction lies at
/// `E' = 4.5 · β`. We use the 10-bit constants by default.
// ------------------------------------------------------------------
// BT.1886 reference EOTF — BT.1886 Annex 1.
//
// L = a · max(V + b, 0)^γ
//
// with γ = 2.40, and a/b determined to satisfy L(0) = L_B,
// L(1) = L_W. The infinite-contrast case L_B = 0 collapses to the
// pure power law L = V^γ (a = 1, b = 0), which is the form most
// frequently used in practice.
/// BT.1886 reference EOTF (default infinite-contrast case `L_B = 0`,
/// `L_W = 1`).
///
/// `v ∈ [0, 1]` (normalised video signal) → relative luminance `L`
/// (also `[0, 1]`).
/// Inverse of [`bt1886_eotf`] (relative luminance → normalised signal).
/// Generalised BT.1886 EOTF that honours non-zero black / non-unity
/// peak white. `l_w` and `l_b` are both expressed in the same arbitrary
/// luminance unit; the returned `L` is in the same unit.
// ------------------------------------------------------------------
// PQ — BT.2100-3 Table 4.
//
// EOTF (signal → display light):
// F_D = 10000 · Y,
// Y = ( max(E'^(1/m2) − c1, 0) / (c2 − c3 · E'^(1/m2)) )^(1/m1)
//
// inverse EOTF (display light → signal):
// E' = ((c1 + c2 · Y^m1) / (1 + c3 · Y^m1))^m2
//
// where m1 = 2610/16384, m2 = 128 · 2523/4096, c1 = 3424/4096,
// c2 = 32 · 2413/4096, c3 = 32 · 2392/4096 and c1 = c3 − c2 + 1.
//
// E' is the non-linear PQ signal in [0, 1]; F_D is luminance in cd/m²
// with peak 10000 cd/m². Y is normalised in [0, 1].
const PQ_M1: f32 = 2610.0 / 16384.0;
const PQ_M2: f32 = 2523.0 / 4096.0 * 128.0;
const PQ_C1: f32 = 3424.0 / 4096.0;
const PQ_C2: f32 = 2413.0 / 4096.0 * 32.0;
const PQ_C3: f32 = 2392.0 / 4096.0 * 32.0;
const PQ_PEAK: f32 = 10000.0;
/// PQ EOTF normalised to `[0, 1]`. Maps a PQ-encoded signal `e' ∈ [0, 1]`
/// to relative luminance in `[0, 1]` (where 1.0 corresponds to the
/// 10 000 cd/m² peak). Multiply by [`PQ_PEAK`] to recover the absolute
/// cd/m² luminance.
/// PQ EOTF returning absolute display luminance in cd/m² (peak 10 000).
/// PQ inverse EOTF on the normalised range — luminance `y ∈ [0, 1]`
/// (where 1.0 is the 10 000 cd/m² peak) → non-linear signal `e' ∈ [0, 1]`.
/// PQ inverse EOTF accepting absolute cd/m² luminance.
// ------------------------------------------------------------------
// HLG — BT.2100-3 Table 5.
//
// OETF (scene light → signal):
// E' = sqrt(3 E) 0 ≤ E ≤ 1/12
// = a · ln(12 E − b) + c 1/12 < E ≤ 1
//
// with a = 0.17883277, b = 1 − 4·a = 0.28466892,
// c = 0.5 − a · ln(4·a) = 0.55991073 (Note 5c).
//
// OETF⁻¹ (signal → scene light):
// x² / 3 0 ≤ x ≤ 1/2
// (exp((x − c)/a) + b) / 12 1/2 < x ≤ 1
// Spec constants from BT.2100-3 Table 5 / Note 5c. The b and c values
// are derived from `a` as `b = 1 − 4·a` and `c = 0.5 − a·ln(4·a)`;
// the literal forms are quoted in the standard for convenience.
// Truncated to f32 precision (≈7 significant figures).
const HLG_A: f32 = 0.178_832_77;
const HLG_B: f32 = 0.284_668_92;
const HLG_C: f32 = 0.559_910_7;
/// HLG OETF (scene light → non-linear signal). Both `e` and the result
/// live in `[0, 1]`; values below 0 are clamped, values above 1 are
/// extrapolated using the upper-branch logarithm.
/// HLG OETF⁻¹ (non-linear signal → relative scene light). This is the
/// inner kernel of the HLG EOTF; the full display-light path further
/// applies the OOTF (see [`hlg_apply_ootf`]).
/// HLG luminance coefficients per BT.2100-3 Table 5 ("HLG Reference
/// OOTF" row): Y_S = 0.2627 R_S + 0.6780 G_S + 0.0593 B_S. These are
/// numerically identical to the BT.2020 NCL matrix coefficients in
/// [`crate::yuv::YuvMatrix::BT2020`].
pub const HLG_Y_R: f32 = 0.2627;
pub const HLG_Y_G: f32 = 0.6780;
pub const HLG_Y_B: f32 = 0.0593;
/// HLG OOTF — applies the system gamma to a single linear scene-light
/// component (`r_s`, `g_s` or `b_s`) given the normalised luminance
/// `y_s` of the pixel.
///
/// `alpha` is the user gain `L_W` in cd/m² (set to 1.0 for normalised
/// output); `gamma` is the system gamma (1.2 at the nominal 1000 cd/m²
/// peak, scaled per Note 5f for other peaks).
/// HLG inverse OOTF — recover scene-light from display-light per
/// Note 5i.
/// System gamma γ for a display with peak luminance `l_w` (cd/m²),
/// inside the "usual production monitoring range" of 400..2000 cd/m²
/// — γ = 1.2 + 0.42 · log10(L_W / 1000) per BT.2100-3 Note 5f.
// ------------------------------------------------------------------
// PQ peak constant exposed for callers.
/// PQ peak luminance in cd/m². Per BT.2100-3 Table 4, the PQ EOTF maps
/// a non-linear signal of 1.0 to a display luminance of 10 000 cd/m².
pub const PQ_PEAK_CDM2: f32 = PQ_PEAK;