1
+ #![ allow( clippy:: inline_always) ]
2
+
1
3
use std:: fs:: File ;
2
4
use std:: io:: { BufWriter , Write } ;
3
5
use std:: path:: Path ;
@@ -28,10 +30,51 @@ const PQ_C1: f32 = 3424. / 4096.;
28
30
const PQ_C2 : f32 = 32. * 2413. / 4096. ;
29
31
const PQ_C3 : f32 = 32. * 2392. / 4096. ;
30
32
33
+ const BT1886_WHITEPOINT : f32 = 203. ;
34
+ const BT1886_BLACKPOINT : f32 = 0.1 ;
35
+ const BT1886_GAMMA : f32 = 2.4 ;
36
+
37
+ // BT.1886 formula from https://en.wikipedia.org/wiki/ITU-R_BT.1886.
38
+ //
39
+ // TODO: the inverses, alpha, and beta should all be constants
40
+ // once floats in const fns are stabilized and `powf` is const.
41
+ // Until then, `inline(always)` gets us close enough.
42
+
43
+ #[ inline( always) ]
44
+ fn bt1886_inv_whitepoint ( ) -> f32 {
45
+ BT1886_WHITEPOINT . powf ( 1.0 / BT1886_GAMMA )
46
+ }
47
+
48
+ #[ inline( always) ]
49
+ fn bt1886_inv_blackpoint ( ) -> f32 {
50
+ BT1886_BLACKPOINT . powf ( 1.0 / BT1886_GAMMA )
51
+ }
52
+
53
+ /// The variable for user gain:
54
+ /// `α = (Lw^(1/λ) - Lb^(1/λ)) ^ λ`
55
+ #[ inline( always) ]
56
+ fn bt1886_alpha ( ) -> f32 {
57
+ ( bt1886_inv_whitepoint ( ) - bt1886_inv_blackpoint ( ) ) . powf ( BT1886_GAMMA )
58
+ }
59
+
60
+ /// The variable for user black level lift:
61
+ /// `β = Lb^(1/λ) / (Lw^(1/λ) - Lb^(1/λ))`
62
+ #[ inline( always) ]
63
+ fn bt1886_beta ( ) -> f32 {
64
+ bt1886_inv_blackpoint ( ) / ( bt1886_inv_whitepoint ( ) - bt1886_inv_blackpoint ( ) )
65
+ }
66
+
31
67
impl TransferFunction {
32
68
pub fn to_linear ( self , x : f32 ) -> f32 {
33
69
match self {
34
- TransferFunction :: BT1886 => x. powf ( 2.8 ) ,
70
+ TransferFunction :: BT1886 => {
71
+ // The screen luminance in cd/m^2:
72
+ // L = α * max((x + β, 0))^λ
73
+ let luma = bt1886_alpha ( ) * 0f32 . max ( x + bt1886_beta ( ) ) . powf ( BT1886_GAMMA ) ;
74
+
75
+ // Normalize to between 0.0 and 1.0
76
+ luma / BT1886_WHITEPOINT
77
+ }
35
78
TransferFunction :: SMPTE2084 => {
36
79
let pq_pow_inv_m2 = x. powf ( 1. / PQ_M2 ) ;
37
80
( 0_f32 . max ( pq_pow_inv_m2 - PQ_C1 ) / ( PQ_C2 - PQ_C3 * pq_pow_inv_m2) ) . powf ( 1. / PQ_M1 )
@@ -42,19 +85,27 @@ impl TransferFunction {
42
85
#[ allow( clippy:: wrong_self_convention) ]
43
86
pub fn from_linear ( self , x : f32 ) -> f32 {
44
87
match self {
45
- TransferFunction :: BT1886 => x. powf ( 1. / 2.8 ) ,
88
+ TransferFunction :: BT1886 => {
89
+ // Scale to a raw cd/m^2 value
90
+ let luma = x * BT1886_WHITEPOINT ;
91
+
92
+ // The inverse of the `to_linear` formula:
93
+ // `(L / α)^(1 / λ) - β = x`
94
+ ( luma / bt1886_alpha ( ) ) . powf ( 1.0 / BT1886_GAMMA ) - bt1886_beta ( )
95
+ }
46
96
TransferFunction :: SMPTE2084 => {
97
+ if x == 0.0 {
98
+ return 0.0 ;
99
+ }
47
100
let linear_pow_m1 = x. powf ( PQ_M1 ) ;
48
101
( PQ_C2 . mul_add ( linear_pow_m1, PQ_C1 ) / PQ_C3 . mul_add ( linear_pow_m1, 1. ) ) . powf ( PQ_M2 )
49
102
}
50
103
}
51
104
}
52
105
106
+ #[ inline( always) ]
53
107
pub fn mid_tone ( self ) -> f32 {
54
- match self {
55
- TransferFunction :: BT1886 => 0.18 ,
56
- TransferFunction :: SMPTE2084 => 26. / 10000. ,
57
- }
108
+ self . to_linear ( 0.5 )
58
109
}
59
110
}
60
111
@@ -157,3 +208,56 @@ fn write_film_grain_table(
157
208
file. flush ( ) ?;
158
209
Ok ( ( ) )
159
210
}
211
+
212
+ #[ cfg( test) ]
213
+ mod tests {
214
+ use super :: * ;
215
+ use quickcheck:: TestResult ;
216
+ use quickcheck_macros:: quickcheck;
217
+
218
+ #[ quickcheck]
219
+ fn bt1886_to_linear_within_range ( x : f32 ) -> TestResult {
220
+ if x < 0.0 || x > 1.0 || x. is_nan ( ) {
221
+ return TestResult :: discard ( ) ;
222
+ }
223
+
224
+ let tx = TransferFunction :: BT1886 ;
225
+ let res = tx. to_linear ( x) ;
226
+ TestResult :: from_bool ( res >= 0.0 && res <= 1.0 )
227
+ }
228
+
229
+ #[ quickcheck]
230
+ fn bt1886_to_linear_reverts_correctly ( x : f32 ) -> TestResult {
231
+ if x < 0.0 || x > 1.0 || x. is_nan ( ) {
232
+ return TestResult :: discard ( ) ;
233
+ }
234
+
235
+ let tx = TransferFunction :: BT1886 ;
236
+ let res = tx. to_linear ( x) ;
237
+ let res = tx. from_linear ( res) ;
238
+ TestResult :: from_bool ( ( x - res) . abs ( ) < f32:: EPSILON )
239
+ }
240
+
241
+ #[ quickcheck]
242
+ fn smpte2084_to_linear_within_range ( x : f32 ) -> TestResult {
243
+ if x < 0.0 || x > 1.0 || x. is_nan ( ) {
244
+ return TestResult :: discard ( ) ;
245
+ }
246
+
247
+ let tx = TransferFunction :: SMPTE2084 ;
248
+ let res = tx. to_linear ( x) ;
249
+ TestResult :: from_bool ( res >= 0.0 && res <= 1.0 )
250
+ }
251
+
252
+ #[ quickcheck]
253
+ fn smpte2084_to_linear_reverts_correctly ( x : f32 ) -> TestResult {
254
+ if x < 0.0 || x > 1.0 || x. is_nan ( ) {
255
+ return TestResult :: discard ( ) ;
256
+ }
257
+
258
+ let tx = TransferFunction :: SMPTE2084 ;
259
+ let res = tx. to_linear ( x) ;
260
+ let res = tx. from_linear ( res) ;
261
+ TestResult :: from_bool ( ( x - res) . abs ( ) < f32:: EPSILON )
262
+ }
263
+ }
0 commit comments