1use rayon::prelude::*;
11use std::f32::consts::PI;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum Gamut {
15 Ap0, Ap1, P3D65, P3D60, Rec2020, Rec709,
16 Awg3, Awg4, Rwg, SGamut3, SGamut3Cine,
17 BlackmagicWg, CanonCinema, DaVinciWg, EGamut,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum Transfer {
22 Linear,
23 AcesCct,
24 ArriLogC3,
25 ArriLogC4,
26 RedLog3G10,
27 SonySLog3,
28 SonySLog2,
29 BmFilmGen5,
30 CanonLog3,
31 CanonLog2,
32 DaVinciIntermediate,
33 FilmlightTLog,
34 AgxLogKraken,
35 VLog,
36 FLog2,
37 FLog2C,
38 AppleLog2,
39 HLG,
40 PQ,
41 DNG,
42 DI,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum OutputTransfer {
47 Linear,
48 SrgbInverseEotf,
49 Bt1886InverseEotf,
50}
51
52#[derive(Debug, Clone)]
53pub struct AgxConfig {
54 pub inset_red: f32,
55 pub inset_green: f32,
56 pub inset_blue: f32,
57 pub rotate_red: f32,
58 pub rotate_green: f32,
59 pub rotate_blue: f32,
60 pub outset_red: f32,
61 pub outset_green: f32,
62 pub outset_blue: f32,
63 pub toe_power: f32,
64 pub shoulder_power: f32,
65 pub slope: f32,
66 pub in_gamut: Gamut,
67 pub in_transfer: Transfer,
68 pub working_curve: Transfer,
69 pub working_mid_grey: f32,
70 pub out_gamut: Gamut,
71 pub out_transfer: OutputTransfer,
72 pub log_output: bool,
73}
74
75impl Default for AgxConfig {
76 fn default() -> Self {
77 Self {
78 inset_red: 0.2,
79 inset_green: 0.2,
80 inset_blue: 0.2,
81 rotate_red: 0.0,
82 rotate_green: 0.0,
83 rotate_blue: 0.0,
84 outset_red: 0.0,
85 outset_green: 0.0,
86 outset_blue: 0.0,
87 toe_power: 3.0,
88 shoulder_power: 3.25,
89 slope: 2.0,
90 in_gamut: Gamut::Rec709,
91 in_transfer: Transfer::Linear,
92 working_curve: Transfer::AgxLogKraken,
93 working_mid_grey: 0.606060,
94 out_gamut: Gamut::Rec709,
95 out_transfer: OutputTransfer::SrgbInverseEotf,
96 log_output: false,
97 }
98 }
99}
100
101pub struct AgxPipeline {
102 config: AgxConfig,
103 inset_mat: [f32; 9],
104 outset_mat: [f32; 9],
105 gamut_mat: [f32; 9],
106 out_gamma: f32,
107 log_floor: [f32; 3],
108 mid_grey_lin: f32,
109}
110
111impl AgxPipeline {
112 pub fn new(config: AgxConfig) -> Self {
113 let in_chrom = gamut_chromaticities(config.in_gamut);
114 let out_chrom = gamut_chromaticities(config.out_gamut);
115
116 let inset_chrom = inset_primaries(
117 in_chrom,
118 config.inset_red,
119 config.inset_green,
120 config.inset_blue,
121 config.rotate_red,
122 config.rotate_green,
123 config.rotate_blue,
124 );
125 let outset_chrom = inset_primaries(
126 in_chrom,
127 config.outset_red,
128 config.outset_green,
129 config.outset_blue,
130 config.rotate_red,
131 config.rotate_green,
132 config.rotate_blue,
133 );
134
135 let inset_mat = rgb_to_rgb(inset_chrom, in_chrom);
136 let outset_mat = rgb_to_rgb(in_chrom, outset_chrom);
137 let gamut_mat = rgb_to_rgb(in_chrom, out_chrom);
138
139 let out_gamma = match config.out_transfer {
140 OutputTransfer::Linear => 1.0,
141 OutputTransfer::SrgbInverseEotf => 2.2,
142 OutputTransfer::Bt1886InverseEotf => 2.4,
143 };
144
145 let log_floor = log_to_lin([0.0, 0.0, 0.0], config.in_transfer);
146
147 let mid_grey_lin = log_to_lin(
148 [config.working_mid_grey, config.working_mid_grey, config.working_mid_grey],
149 config.working_curve,
150 )[0];
151
152 Self {
153 config,
154 inset_mat,
155 outset_mat,
156 gamut_mat,
157 out_gamma,
158 log_floor,
159 mid_grey_lin,
160 }
161 }
162
163 #[inline]
164 pub fn process_pixel(&self, r: f32, g: f32, b: f32) -> [f32; 3] {
165 let mut rgb = [r, g, b];
166
167 rgb = log_to_lin(rgb, self.config.in_transfer);
168
169 rgb[0] = rgb[0].max(self.log_floor[0]);
170 rgb[1] = rgb[1].max(self.log_floor[1]);
171 rgb[2] = rgb[2].max(self.log_floor[2]);
172
173 rgb = mat_mul_vec3(&self.inset_mat, rgb);
174
175 rgb = lin_to_log(rgb, self.config.working_curve);
176 let log_rgb = rgb;
177
178 let mg = 0.5;
179 let lmg = self.config.working_mid_grey;
180 rgb[0] = tone_scale(rgb[0], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);
181 rgb[1] = tone_scale(rgb[1], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);
182 rgb[2] = tone_scale(rgb[2], self.config.shoulder_power, self.config.toe_power, self.config.slope, lmg, mg, 1.0, 0.0);
183
184 if self.config.log_output {
185 return log_rgb;
186 }
187
188 rgb[0] = rgb[0].powf(2.2);
189 rgb[1] = rgb[1].powf(2.2);
190 rgb[2] = rgb[2].powf(2.2);
191
192 let lum = (rgb[0] + rgb[1] + rgb[2]) * (1.0 / 3.0);
193 let max_ch = rgb[0].max(rgb[1]).max(rgb[2]);
194 if max_ch > 0.85 {
195 let t = ((max_ch - 0.85) / 0.15).min(1.0);
196 rgb[0] = rgb[0] + (lum - rgb[0]) * t;
197 rgb[1] = rgb[1] + (lum - rgb[1]) * t;
198 rgb[2] = rgb[2] + (lum - rgb[2]) * t;
199 }
200
201 rgb = mat_mul_vec3(&self.outset_mat, rgb);
202
203 rgb = mat_mul_vec3(&self.gamut_mat, rgb);
204
205 rgb = inverse_eotf(rgb, self.out_gamma);
206
207 rgb[0] = rgb[0].max(0.0);
208 rgb[1] = rgb[1].max(0.0);
209 rgb[2] = rgb[2].max(0.0);
210
211 rgb
212 }
213
214 pub fn process_frame(&self, pixels: &mut [f32]) {
215 pixels.par_chunks_exact_mut(3).for_each(|chunk| {
216 let out = self.process_pixel(chunk[0], chunk[1], chunk[2]);
217 chunk[0] = out[0];
218 chunk[1] = out[1];
219 chunk[2] = out[2];
220 });
221 }
222
223 pub fn config(&self) -> &AgxConfig {
224 &self.config
225 }
226}
227
228#[inline]
229fn mat_mul_vec3(m: &[f32; 9], v: [f32; 3]) -> [f32; 3] {
230 [
231 v[0] * m[0] + v[1] * m[3] + v[2] * m[6],
232 v[0] * m[1] + v[1] * m[4] + v[2] * m[7],
233 v[0] * m[2] + v[1] * m[5] + v[2] * m[8],
234 ]
235}
236
237#[derive(Clone, Copy)]
238struct Chromaticities {
239 r: [f32; 2],
240 g: [f32; 2],
241 b: [f32; 2],
242 w: [f32; 2],
243}
244
245fn gamut_chromaticities(g: Gamut) -> Chromaticities {
246 const D65: [f32; 2] = [0.3127, 0.3290];
247 match g {
248 Gamut::Ap0 => Chromaticities { r: [0.7347, 0.2653], g: [0.0000, 1.0000], b: [0.0001, -0.0770], w: D65 },
249 Gamut::Ap1 => Chromaticities { r: [0.7130, 0.2930], g: [0.1650, 0.8300], b: [0.1280, 0.0440], w: D65 },
250 Gamut::Rec709 => Chromaticities { r: [0.6400, 0.3300], g: [0.3000, 0.6000], b: [0.1500, 0.0600], w: D65 },
251 Gamut::Rec2020 => Chromaticities { r: [0.7080, 0.2920], g: [0.1700, 0.7970], b: [0.1310, 0.0460], w: D65 },
252 Gamut::P3D65 => Chromaticities { r: [0.6800, 0.3200], g: [0.2650, 0.6900], b: [0.1500, 0.0600], w: D65 },
253 Gamut::P3D60 => Chromaticities { r: [0.6800, 0.3200], g: [0.2650, 0.6900], b: [0.1500, 0.0600], w: D65 },
254 Gamut::SGamut3Cine => Chromaticities { r: [0.7660, 0.2750], g: [0.2250, 0.8000], b: [0.0890, -0.0870], w: D65 },
255 Gamut::Awg3 => Chromaticities { r: [0.6840, 0.3130], g: [0.2210, 0.8480], b: [0.0861, -0.1020], w: D65 },
256 Gamut::Awg4 => Chromaticities { r: [0.6800, 0.3150], g: [0.2200, 0.8500], b: [0.0860, -0.1000], w: D65 },
257 Gamut::Rwg => Chromaticities { r: [0.7300, 0.2800], g: [0.1400, 0.8550], b: [0.1000, -0.0900], w: D65 },
258 Gamut::SGamut3 => Chromaticities { r: [0.7500, 0.2700], g: [0.2100, 0.8000], b: [0.1000, -0.0500], w: D65 },
259 Gamut::BlackmagicWg => Chromaticities { r: [0.7500, 0.2700], g: [0.2100, 0.8000], b: [0.1000, -0.0500], w: D65 },
260 Gamut::CanonCinema => Chromaticities { r: [0.7400, 0.2700], g: [0.1700, 0.7900], b: [0.0800, -0.1000], w: D65 },
261 Gamut::DaVinciWg => Chromaticities { r: [0.7350, 0.2650], g: [0.2150, 0.8100], b: [0.1200, -0.0500], w: D65 },
262 Gamut::EGamut => Chromaticities { r: [0.7300, 0.2800], g: [0.1700, 0.8000], b: [0.1000, -0.0600], w: D65 },
263 }
264}
265
266fn rgb_to_rgb(src: Chromaticities, dst: Chromaticities) -> [f32; 9] {
267 let src_to_xyz = rgb_to_xyz(src);
268 let xyz_to_dst = xyz_to_rgb(dst);
269 mat_mul_3x3(&xyz_to_dst, &src_to_xyz)
270}
271
272fn rgb_to_xyz(c: Chromaticities) -> [f32; 9] {
273 let [rx, ry] = c.r;
274 let [gx, gy] = c.g;
275 let [bx, by] = c.b;
276 let [wx, wy] = c.w;
277
278 let rz = 1.0 - rx - ry;
279 let gz = 1.0 - gx - gy;
280 let bz = 1.0 - bx - by;
281 let wz = 1.0 - wx - wy;
282
283 let m = [
284 rx, gx, bx,
285 ry, gy, by,
286 rz, gz, bz,
287 ];
288 let inv = invert_3x3(&m);
289 let s = mat_mul_vec3(&inv, [wx / wy, 1.0, wz / wy]);
290
291 [
292 inv[0] * s[0], inv[1] * s[1], inv[2] * s[2],
293 inv[3] * s[0], inv[4] * s[1], inv[5] * s[2],
294 inv[6] * s[0], inv[7] * s[1], inv[8] * s[2],
295 ]
296}
297
298fn xyz_to_rgb(c: Chromaticities) -> [f32; 9] {
299 invert_3x3(&rgb_to_xyz(c))
300}
301
302fn mat_mul_3x3(a: &[f32; 9], b: &[f32; 9]) -> [f32; 9] {
303 let mut out = [0.0; 9];
304 for i in 0..3 {
305 for j in 0..3 {
306 out[i * 3 + j] = a[i * 3] * b[j] + a[i * 3 + 1] * b[3 + j] + a[i * 3 + 2] * b[6 + j];
307 }
308 }
309 out
310}
311
312fn invert_3x3(m: &[f32; 9]) -> [f32; 9] {
313 let det = m[0] * (m[4] * m[8] - m[5] * m[7])
314 - m[1] * (m[3] * m[8] - m[5] * m[6])
315 + m[2] * (m[3] * m[7] - m[4] * m[6]);
316 let inv_det = 1.0 / det;
317 [
318 (m[4] * m[8] - m[5] * m[7]) * inv_det,
319 (m[2] * m[7] - m[1] * m[8]) * inv_det,
320 (m[1] * m[5] - m[2] * m[4]) * inv_det,
321 (m[5] * m[6] - m[3] * m[8]) * inv_det,
322 (m[0] * m[8] - m[2] * m[6]) * inv_det,
323 (m[2] * m[3] - m[0] * m[5]) * inv_det,
324 (m[3] * m[7] - m[4] * m[6]) * inv_det,
325 (m[1] * m[6] - m[0] * m[7]) * inv_det,
326 (m[0] * m[4] - m[1] * m[3]) * inv_det,
327 ]
328}
329
330fn inset_primaries(
331 c: Chromaticities,
332 att_r: f32,
333 att_g: f32,
334 att_b: f32,
335 rot_r: f32,
336 rot_g: f32,
337 rot_b: f32,
338) -> Chromaticities {
339 fn shift(xy: [f32; 2], w: [f32; 2], attenuation: f32, rotation_deg: f32) -> [f32; 2] {
340 let dx = xy[0] - w[0];
341 let dy = xy[1] - w[1];
342 let dist = (dx * dx + dy * dy).sqrt();
343 if dist < 1e-6 {
344 return xy;
345 }
346 let scale = 1.0 - attenuation;
347 let angle = rotation_deg * PI / 180.0;
348 let cos_a = angle.cos();
349 let sin_a = angle.sin();
350 let rx = (dx * cos_a - dy * sin_a) * scale;
351 let ry = (dx * sin_a + dy * cos_a) * scale;
352 [w[0] + rx, w[1] + ry]
353 }
354 Chromaticities {
355 r: shift(c.r, c.w, att_r, rot_r),
356 g: shift(c.g, c.w, att_g, rot_g),
357 b: shift(c.b, c.w, att_b, rot_b),
358 w: c.w,
359 }
360}
361
362#[inline]
363fn log_to_lin(v: [f32; 3], tf: Transfer) -> [f32; 3] {
364 let f = |x: f32| -> f32 {
365 match tf {
366 Transfer::Linear => x,
367 Transfer::AcesCct => {
368 if x <= 0.155251141552511 {
369 (x - 0.077415122655251) / 10.5402377416545
370 } else {
371 2.0f32.powf((x - 0.413588402493492) * 17.52)
372 }
373 }
374 Transfer::SonySLog3 => {
375 if x < 0.01125 {
376 (x - 0.030001222851889) / 0.01125
377 } else {
378 10.0f32.powf((x - 0.42) / 0.24) * 0.18
379 }
380 }
381 Transfer::SonySLog2 => {
382 if x < 0.01018 {
383 (x - 0.0245786) / 0.0183089
384 } else {
385 10.0f32.powf((x - 0.384410) / 0.235443) * 0.18
386 }
387 }
388 Transfer::ArriLogC3 => {
389 if x <= 0.010591 {
390 (x - 0.005228) / 0.047491
391 } else {
392 0.18 * 2.0f32.powf((x - 0.385537) / 0.247190)
393 }
394 }
395 Transfer::ArriLogC4 => {
400 let a: f32 = ((1u32 << 18) as f32 - 16.0) / 117.45;
401 let b: f32 = (1023.0 - 95.0) / 1023.0;
402 let c: f32 = 95.0 / 1023.0;
403 let s: f32 = (7.0 * std::f32::consts::LN_2 * (7.0 - 14.0 * c / b).exp2()) / (a * b);
404 let t: f32 = ((14.0 * (-c / b) + 6.0).exp2() - 64.0) / a;
405 if x >= 0.0 {
406 ((14.0 * ((x - c) / b) + 6.0).exp2() - 64.0) / a
407 } else {
408 x * s + t
409 }
410 }
411 Transfer::RedLog3G10 => {
412 if x < 0.0 {
413 0.0
414 } else {
415 (10.0f32.powf(x * 17.52 - 14.0) - 0.001) / 9.999
416 }
417 }
418 Transfer::VLog => {
419 if x <= 0.010592 {
420 (x - 0.005255) / 0.047120
421 } else {
422 0.18 * 2.0f32.powf((x - 0.389583) / 0.244949)
423 }
424 }
425 Transfer::FLog2 => {
426 if x < 0.0 {
427 0.0
428 } else {
429 let a = 0.86445045;
430 let b = 0.5779536;
431 let c = 0.13967949;
432 let d = 0.4174028;
433 ((x - b) / a).powf(1.0 / c) - d
434 }
435 }
436 Transfer::FLog2C => {
437 if x < 0.0 {
438 0.0
439 } else {
440 let a = 0.86445045;
441 let b = 0.5779536;
442 let c = 0.13967949;
443 let d = 0.4174028;
444 ((x - b) / a).powf(1.0 / c) - d
445 }
446 }
447 Transfer::AppleLog2 => {
448 if x < 0.0 {
449 0.0
450 } else {
451 1.0 / (1.0 + (-4.0 * x).exp())
452 }
453 }
454 Transfer::BmFilmGen5 => {
455 if x < 0.125 {
456 x * 4.0
457 } else {
458 1.0 + (x - 0.5).ln() / 0.693147
459 }
460 }
461 Transfer::CanonLog3 => {
462 if x < 0.0149 {
463 (x - 0.0721) / 3.5766
464 } else {
465 0.18 * 10.0f32.powf((x - 0.406539) / 0.301940)
466 }
467 }
468 Transfer::CanonLog2 => {
469 if x < 0.0205 {
470 (x - 0.078516) / 2.4397
471 } else {
472 0.18 * 10.0f32.powf((x - 0.419398) / 0.283691)
473 }
474 }
475 Transfer::DaVinciIntermediate => {
476 if x < 0.0 {
477 0.0
478 } else if x < 0.5 {
479 x * x * 4.0
480 } else {
481 1.0 + (x - 0.5).ln() / 0.693147
482 }
483 }
484 Transfer::FilmlightTLog => {
485 if x <= 0.0 {
486 0.0
487 } else {
488 let a = 1.0 / (10.0_f32.powf(0.002) - 1.0);
489 (a + 1.0).powf(x - 1.0) - a
490 }
491 }
492 Transfer::AgxLogKraken => {
493 0.18 * 2.0f32.powf(x * 16.5 - 10.0)
494 }
495 Transfer::HLG => {
496 if x <= 0.5 {
497 x * x * 4.0
498 } else {
499 let beta = 1.09929682680944 - 1.0;
500 let gamma = 0.5;
501 ((x + beta - 1.0) / beta).powf(1.0 / gamma)
502 }
503 }
504 Transfer::PQ => {
505 if x < 0.0 {
506 0.0
507 } else {
508 let n = x.cbrt();
509 let m = (10000.0_f32.powf(1.0 / 2.4) - 1.0) / 10000.0_f32.powf(1.0 / 2.4);
510 ((n - m) / (1.0 - m)).powf(2.4)
511 }
512 }
513 Transfer::DNG => x,
514 Transfer::DI => x,
515 }
516 };
517 [f(v[0]), f(v[1]), f(v[2])]
518}
519
520#[inline]
521fn lin_to_log(v: [f32; 3], tf: Transfer) -> [f32; 3] {
522 let f = |x: f32| -> f32 {
523 match tf {
524 Transfer::Linear => x,
525 Transfer::AcesCct => {
526 if x <= 0.0078125 {
527 x * 10.5402377416545 + 0.077415122655251
528 } else {
529 (x / 2.0).log2() / 17.52 + 0.413588402493492
530 }
531 }
532 Transfer::SonySLog3 => {
533 if x < 0.0 {
534 0.030001222851889
535 } else {
536 0.24 * (x / 0.18).log10() + 0.42
537 }
538 }
539 Transfer::SonySLog2 => {
540 if x < 0.0 {
541 0.0245786
542 } else {
543 0.235443 * (x / 0.18).log10() + 0.384410
544 }
545 }
546 Transfer::ArriLogC3 => {
547 if x <= 0.005228 {
548 x * 0.047491 + 0.005228
549 } else {
550 0.247190 * (x / 0.18).log2() + 0.385537
551 }
552 }
553 Transfer::ArriLogC4 => {
557 let a: f32 = ((1u32 << 18) as f32 - 16.0) / 117.45;
558 let b: f32 = (1023.0 - 95.0) / 1023.0;
559 let c: f32 = 95.0 / 1023.0;
560 let s: f32 = (7.0 * std::f32::consts::LN_2 * (7.0 - 14.0 * c / b).exp2()) / (a * b);
561 let t: f32 = ((14.0 * (-c / b) + 6.0).exp2() - 64.0) / a;
562 if x >= t {
563 ((a * x + 64.0_f32).log2() - 6.0) / 14.0 * b + c
564 } else {
565 (x - t) / s
566 }
567 }
568 Transfer::RedLog3G10 => {
569 if x < 0.0 {
570 0.0
571 } else {
572 ((x * 9.999 + 0.001).log10() / 17.52) + 14.0
573 }
574 }
575 Transfer::VLog => {
576 if x <= 0.005255 {
577 x * 0.047120 + 0.005255
578 } else {
579 0.244949 * (x / 0.18).log2() + 0.389583
580 }
581 }
582 Transfer::FLog2 => {
583 if x < 0.0 {
584 0.0
585 } else {
586 let a = 0.86445045;
587 let b = 0.5779536;
588 let c = 0.13967949;
589 let d = 0.4174028;
590 a * (x + d).powf(c) + b
591 }
592 }
593 Transfer::FLog2C => {
594 if x < 0.0 {
595 0.0
596 } else {
597 let a = 0.86445045;
598 let b = 0.5779536;
599 let c = 0.13967949;
600 let d = 0.4174028;
601 a * (x + d).powf(c) + b
602 }
603 }
604 Transfer::AppleLog2 => {
605 (-(1.0 / x - 1.0).ln()) / 4.0
606 }
607 Transfer::BmFilmGen5 => {
608 if x < 0.5 {
609 x / 4.0
610 } else {
611 0.5 + 0.693147 * (x - 1.0).exp()
612 }
613 }
614 Transfer::CanonLog3 => {
615 if x < 0.00390625 {
616 x * 3.5766 + 0.0721
617 } else {
618 0.301940 * (x / 0.18).log10() + 0.406539
619 }
620 }
621 Transfer::CanonLog2 => {
622 if x < 0.00390625 {
623 x * 2.4397 + 0.078516
624 } else {
625 0.283691 * (x / 0.18).log10() + 0.419398
626 }
627 }
628 Transfer::DaVinciIntermediate => {
629 if x < 0.0 {
630 0.0
631 } else if x < 0.25 {
632 x.sqrt() / 2.0
633 } else {
634 0.5 + 0.693147 * (x - 1.0).exp()
635 }
636 }
637 Transfer::FilmlightTLog => {
638 if x <= 0.0 {
639 0.0
640 } else {
641 let a = 1.0 / (10.0_f32.powf(0.002) - 1.0);
642 1.0 + (x / a + 1.0).ln() / (a + 1.0).ln()
643 }
644 }
645 Transfer::AgxLogKraken => {
646 if x <= 0.0 {
647 0.0
648 } else {
649 let ev = (x / 0.18).log2().clamp(-10.0, 6.5);
650 (ev + 10.0) / 16.5
651 }
652 }
653 Transfer::HLG => {
654 if x < 0.0 {
655 0.0
656 } else if x <= 0.5 {
657 x.sqrt() / 2.0
658 } else {
659 let beta = 1.09929682680944 - 1.0;
660 let gamma = 0.5;
661 beta * (x).powf(gamma) + 1.0 - beta
662 }
663 }
664 Transfer::PQ => {
665 if x < 0.0 {
666 0.0
667 } else {
668 let m = (10000.0_f32.powf(1.0 / 2.4) - 1.0) / 10000.0_f32.powf(1.0 / 2.4);
669 1.0 - (x.max(0.0).cbrt() * (1.0 - m) - m).abs()
670 }
671 }
672 Transfer::DNG => x,
673 Transfer::DI => x,
674 }
675 };
676 [f(v[0]), f(v[1]), f(v[2])]
677}
678
679#[inline]
680fn inverse_eotf(v: [f32; 3], gamma: f32) -> [f32; 3] {
681 if gamma == 1.0 {
682 return v;
683 }
684 let g = 1.0 / gamma;
685 [v[0].powf(g), v[1].powf(g), v[2].powf(g)]
686}
687
688#[inline]
689fn spowf(a: f32, b: f32) -> f32 {
690 let s = if a > 0.0 { 1.0 } else if a < 0.0 { -1.0 } else { 0.0 };
691 s * a.abs().powf(b)
692}
693
694#[inline]
695fn tone_scale(x: f32, shoulder: f32, toe: f32, slope: f32, lmg: f32, mg: f32, s0: f32, t0: f32) -> f32 {
696 let ss = spowf(
697 (spowf(slope * (s0 - lmg) / (1.0 - mg), shoulder) - 1.0) * spowf(slope * (s0 - lmg), -shoulder),
698 -1.0 / shoulder,
699 );
700 let ms = slope * (x - lmg) / ss;
701 let fs = ms / spowf(1.0 + spowf(ms, shoulder), 1.0 / shoulder);
702
703 let ts = spowf(
704 (spowf(slope * (lmg - t0) / mg, toe) - 1.0) * spowf(slope * (lmg - t0), -toe),
705 -1.0 / toe,
706 );
707 let mr = slope * (x - lmg) / (-ts);
708 let ft = mr / spowf(1.0 + spowf(mr, toe), 1.0 / toe);
709
710 if x >= lmg { ss * fs + mg } else { -ts * ft + mg }
711}
712
713impl From<crate::color::TransferFunction> for Transfer {
714 fn from(tf: crate::color::TransferFunction) -> Self {
715 match tf {
716 crate::color::TransferFunction::Linear => Transfer::Linear,
717 crate::color::TransferFunction::Rec709 => Transfer::Linear,
718 crate::color::TransferFunction::SLog3 => Transfer::SonySLog3,
719 crate::color::TransferFunction::VLog => Transfer::VLog,
720 crate::color::TransferFunction::ARRIlog3 => Transfer::ArriLogC3,
721 crate::color::TransferFunction::ARRIlog4 => Transfer::ArriLogC4,
722 crate::color::TransferFunction::CLog3 => Transfer::CanonLog3,
723 crate::color::TransferFunction::FLog2 => Transfer::FLog2,
724 crate::color::TransferFunction::AppleLog | crate::color::TransferFunction::AppleLog2 => Transfer::Linear,
725 crate::color::TransferFunction::ACESCCT => Transfer::AcesCct,
726 crate::color::TransferFunction::HLG => Transfer::HLG,
727 crate::color::TransferFunction::PQ => Transfer::PQ,
728 crate::color::TransferFunction::DaVinciIntermediate => Transfer::DaVinciIntermediate,
729 crate::color::TransferFunction::Gamma24 => Transfer::Linear,
730 }
731 }
732}
733
734impl From<crate::color::ColorSpace> for Gamut {
735 fn from(cs: crate::color::ColorSpace) -> Self {
736 match cs {
737 crate::color::ColorSpace::Rec709 => Gamut::Rec709,
738 crate::color::ColorSpace::Rec2020 => Gamut::Rec2020,
739 crate::color::ColorSpace::DciP3 => Gamut::P3D65,
740 crate::color::ColorSpace::Srgb => Gamut::Rec709,
741 crate::color::ColorSpace::SGamut3Cine => Gamut::SGamut3Cine,
742 crate::color::ColorSpace::SGamut3 => Gamut::SGamut3,
743 crate::color::ColorSpace::ARRIWideGamut3 => Gamut::Awg3,
744 crate::color::ColorSpace::ARRIWideGamut4 => Gamut::Awg4,
745 crate::color::ColorSpace::CanonCinemaGamut => Gamut::CanonCinema,
746 crate::color::ColorSpace::PanasonicVGamut => Gamut::Rwg,
747 crate::color::ColorSpace::ACESAP1 => Gamut::Ap1,
748 crate::color::ColorSpace::FGamut => Gamut::Rwg,
749 crate::color::ColorSpace::FGamutC => Gamut::Ap0,
750 crate::color::ColorSpace::DaVinciWideGamut => Gamut::DaVinciWg,
751 crate::color::ColorSpace::DisplayP3 => Gamut::P3D65,
752 }
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn test_agx_pipeline_creation() {
762 let cfg = AgxConfig::default();
763 let pipe = AgxPipeline::new(cfg);
764 assert!(pipe.inset_mat.iter().any(|&v| v != 0.0));
765 }
766
767 #[test]
768 fn test_mid_grey_pivot() {
769 let cfg = AgxConfig::default();
770 let pipe = AgxPipeline::new(cfg);
771 let out = pipe.process_pixel(0.18, 0.18, 0.18);
772 assert!((out[0] - 0.5).abs() < 0.01, "mid grey: expected ~0.5, got {}", out[0]);
773 }
774
775 #[test]
776 fn test_black_output() {
777 let cfg = AgxConfig::default();
778 let pipe = AgxPipeline::new(cfg);
779 let out = pipe.process_pixel(0.0, 0.0, 0.0);
780 assert!(out[0] < 0.001, "black: expected near 0, got {}", out[0]);
781 }
782
783 #[test]
784 fn test_white_clip() {
785 let cfg = AgxConfig::default();
786 let pipe = AgxPipeline::new(cfg);
787 let out = pipe.process_pixel(10.0, 10.0, 10.0);
788 assert!(out[0] < 1.0 && out[0] > 0.9, "white: expected near 1.0, got {}", out[0]);
789 }
790
791 #[test]
792 fn test_gamut_conversion_rec2020_to_rec709() {
793 let mut cfg = AgxConfig::default();
794 cfg.in_gamut = Gamut::Rec2020;
795 cfg.out_gamut = Gamut::Rec709;
796
797 let pipe = AgxPipeline::new(cfg);
798 let out = pipe.process_pixel(0.5, 0.5, 0.5);
799
800 assert!(out[0] >= 0.0 && out[0] <= 1.0);
801 assert!(out[1] >= 0.0 && out[1] <= 1.0);
802 assert!(out[2] >= 0.0 && out[2] <= 1.0);
803 }
804}