1#![forbid(unsafe_code)]
42#![allow(clippy::cast_lossless)]
43#![allow(clippy::cast_precision_loss)]
44#![allow(clippy::cast_possible_truncation)]
45#![allow(clippy::cast_sign_loss)]
46#![allow(clippy::cast_possible_wrap)]
47#![allow(clippy::similar_names)]
48#![allow(clippy::many_single_char_names)]
49#![allow(clippy::missing_errors_doc)]
50#![allow(clippy::match_same_arms)]
51#![allow(clippy::doc_markdown)]
52#![allow(clippy::unused_self)]
53#![allow(clippy::unnecessary_cast)]
54#![allow(clippy::bool_to_int_with_if)]
55#![allow(clippy::needless_range_loop)]
56#![allow(clippy::too_many_lines)]
57#![allow(clippy::unnecessary_wraps)]
58#![allow(clippy::map_unwrap_or)]
59#![allow(clippy::no_effect_underscore_binding)]
60#![allow(clippy::unreadable_literal)]
61#![allow(clippy::too_many_arguments)]
62#![allow(dead_code)]
63
64use crate::error::{GraphError, GraphResult};
65use crate::frame::FilterFrame;
66use crate::node::{Node, NodeId, NodeState, NodeType};
67use crate::port::{InputPort, OutputPort, PortFormat, PortId, PortType, VideoPortFormat};
68use oximedia_codec::{ColorInfo, Plane, VideoFrame};
69use oximedia_core::PixelFormat;
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
73pub enum TransferFunction {
74 Linear,
76 #[default]
78 Pq,
79 Hlg,
81 Bt709,
83}
84
85impl TransferFunction {
86 #[must_use]
89 pub fn eotf(&self, signal: f64, peak_nits: f64) -> f64 {
90 match self {
91 Self::Linear => signal * peak_nits,
92 Self::Pq => pq_eotf(signal) * peak_nits,
93 Self::Hlg => hlg_eotf(signal) * peak_nits,
94 Self::Bt709 => bt709_eotf(signal) * peak_nits,
95 }
96 }
97
98 #[must_use]
101 pub fn oetf(&self, linear: f64, peak_nits: f64) -> f64 {
102 let normalized = (linear / peak_nits).clamp(0.0, 1.0);
103 match self {
104 Self::Linear => normalized,
105 Self::Pq => pq_oetf(normalized),
106 Self::Hlg => hlg_oetf(normalized),
107 Self::Bt709 => bt709_oetf(normalized),
108 }
109 }
110}
111
112#[must_use]
115fn pq_eotf(e: f64) -> f64 {
116 const M1: f64 = 2610.0 / 16384.0;
117 const M2: f64 = 2523.0 / 4096.0 * 128.0;
118 const C1: f64 = 3424.0 / 4096.0;
119 const C2: f64 = 2413.0 / 4096.0 * 32.0;
120 const C3: f64 = 2392.0 / 4096.0 * 32.0;
121
122 let e = e.clamp(0.0, 1.0);
123 let e_m2 = e.powf(1.0 / M2);
124 let num = (e_m2 - C1).max(0.0);
125 let den = C2 - C3 * e_m2;
126
127 if den.abs() < 1e-10 {
128 0.0
129 } else {
130 (num / den).powf(1.0 / M1)
131 }
132}
133
134#[must_use]
137fn pq_oetf(y: f64) -> f64 {
138 const M1: f64 = 2610.0 / 16384.0;
139 const M2: f64 = 2523.0 / 4096.0 * 128.0;
140 const C1: f64 = 3424.0 / 4096.0;
141 const C2: f64 = 2413.0 / 4096.0 * 32.0;
142 const C3: f64 = 2392.0 / 4096.0 * 32.0;
143
144 let y = y.clamp(0.0, 1.0);
145 let y_m1 = y.powf(M1);
146 let num = C1 + C2 * y_m1;
147 let den = 1.0 + C3 * y_m1;
148
149 (num / den).powf(M2)
150}
151
152#[must_use]
155fn hlg_eotf(e: f64) -> f64 {
156 const A: f64 = 0.17883277;
157 const B: f64 = 0.28466892;
158 const C: f64 = 0.55991073;
159
160 let e = e.clamp(0.0, 1.0);
161
162 if e <= 0.5 {
163 (e * e) / 3.0
164 } else {
165 (((e - C) / A).exp() + B) / 12.0
166 }
167}
168
169#[must_use]
172fn hlg_oetf(y: f64) -> f64 {
173 const A: f64 = 0.17883277;
174 const B: f64 = 0.28466892;
175 const C: f64 = 0.55991073;
176
177 let y = y.clamp(0.0, 1.0);
178
179 if y <= 1.0 / 12.0 {
180 (3.0 * y).sqrt()
181 } else {
182 A * (12.0 * y - B).ln() + C
183 }
184}
185
186#[must_use]
188fn bt709_eotf(e: f64) -> f64 {
189 const BETA: f64 = 0.018053968510807;
190 const ALPHA: f64 = 1.09929682680944;
191 const GAMMA: f64 = 1.0 / 0.45;
192
193 let e = e.clamp(0.0, 1.0);
194
195 if e < BETA * 4.5 {
196 e / 4.5
197 } else {
198 ((e + (ALPHA - 1.0)) / ALPHA).powf(GAMMA)
199 }
200}
201
202#[must_use]
204fn bt709_oetf(y: f64) -> f64 {
205 const BETA: f64 = 0.018053968510807;
206 const ALPHA: f64 = 1.09929682680944;
207 const GAMMA: f64 = 0.45;
208
209 let y = y.clamp(0.0, 1.0);
210
211 if y < BETA {
212 4.5 * y
213 } else {
214 ALPHA * y.powf(GAMMA) - (ALPHA - 1.0)
215 }
216}
217
218#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
220pub enum TonemapAlgorithm {
221 Reinhard,
223 ReinhardExtended,
225 #[default]
227 Aces,
228 Hable,
230 Clip,
232}
233
234impl TonemapAlgorithm {
235 #[must_use]
238 pub fn tonemap(&self, linear: f64, params: &TonemapParams) -> f64 {
239 match self {
240 Self::Reinhard => reinhard_tonemap(linear, params),
241 Self::ReinhardExtended => reinhard_extended_tonemap(linear, params),
242 Self::Aces => aces_tonemap(linear),
243 Self::Hable => hable_tonemap(linear),
244 Self::Clip => linear.clamp(0.0, 1.0),
245 }
246 }
247}
248
249#[derive(Clone, Copy, Debug)]
251pub struct TonemapParams {
252 pub peak_luminance: f64,
254 pub target_luminance: f64,
256 pub white_point: f64,
258 pub exposure: f64,
260 pub contrast: f64,
262 pub saturation: f64,
264}
265
266impl Default for TonemapParams {
267 fn default() -> Self {
268 Self {
269 peak_luminance: 1000.0,
270 target_luminance: 100.0,
271 white_point: 1000.0,
272 exposure: 0.0,
273 contrast: 1.0,
274 saturation: 1.0,
275 }
276 }
277}
278
279#[must_use]
282fn reinhard_tonemap(linear: f64, params: &TonemapParams) -> f64 {
283 let normalized = linear / params.peak_luminance;
285
286 let exposed = normalized * 2.0_f64.powf(params.exposure);
288
289 let mapped = exposed / (1.0 + exposed);
291
292 mapped * params.target_luminance / 100.0
294}
295
296#[must_use]
299fn reinhard_extended_tonemap(linear: f64, params: &TonemapParams) -> f64 {
300 let normalized = linear / params.peak_luminance;
301 let exposed = normalized * 2.0_f64.powf(params.exposure);
302
303 let white = params.white_point / params.peak_luminance;
304 let white_sq = white * white;
305
306 let mapped = (exposed * (1.0 + exposed / white_sq)) / (1.0 + exposed);
308
309 mapped * params.target_luminance / 100.0
310}
311
312#[must_use]
316fn aces_tonemap(linear: f64) -> f64 {
317 const A: f64 = 2.51;
319 const B: f64 = 0.03;
320 const C: f64 = 2.43;
321 const D: f64 = 0.59;
322 const E: f64 = 0.14;
323
324 let x = linear.max(0.0);
325 let num = x * (A * x + B);
326 let den = x * (C * x + D) + E;
327
328 if den.abs() < 1e-10 {
329 0.0
330 } else {
331 (num / den).clamp(0.0, 1.0)
332 }
333}
334
335#[must_use]
338fn hable_tonemap(linear: f64) -> f64 {
339 const EXPOSURE_BIAS: f64 = 2.0;
340
341 fn hable_partial(x: f64) -> f64 {
342 const A: f64 = 0.15;
343 const B: f64 = 0.50;
344 const C: f64 = 0.10;
345 const D: f64 = 0.20;
346 const E: f64 = 0.02;
347 const F: f64 = 0.30;
348
349 ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F
350 }
351
352 let curr = hable_partial(linear * EXPOSURE_BIAS);
353 let white = hable_partial(11.2);
354
355 if white.abs() < 1e-10 {
356 0.0
357 } else {
358 (curr / white).clamp(0.0, 1.0)
359 }
360}
361
362#[derive(Clone, Copy, Debug, PartialEq)]
364pub struct ColorPrimaries {
365 pub red: (f64, f64),
367 pub green: (f64, f64),
369 pub blue: (f64, f64),
371 pub white: (f64, f64),
373}
374
375impl ColorPrimaries {
376 pub const BT709: Self = Self {
378 red: (0.64, 0.33),
379 green: (0.30, 0.60),
380 blue: (0.15, 0.06),
381 white: (0.3127, 0.3290), };
383
384 pub const BT2020: Self = Self {
386 red: (0.708, 0.292),
387 green: (0.170, 0.797),
388 blue: (0.131, 0.046),
389 white: (0.3127, 0.3290), };
391
392 pub const DCI_P3: Self = Self {
394 red: (0.680, 0.320),
395 green: (0.265, 0.690),
396 blue: (0.150, 0.060),
397 white: (0.3127, 0.3290), };
399}
400
401#[derive(Clone, Copy, Debug)]
403pub struct ColorMatrix3x3 {
404 pub m: [[f64; 3]; 3],
406}
407
408impl ColorMatrix3x3 {
409 #[must_use]
411 pub fn identity() -> Self {
412 Self {
413 m: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
414 }
415 }
416
417 #[must_use]
419 pub fn apply(&self, rgb: [f64; 3]) -> [f64; 3] {
420 [
421 self.m[0][0] * rgb[0] + self.m[0][1] * rgb[1] + self.m[0][2] * rgb[2],
422 self.m[1][0] * rgb[0] + self.m[1][1] * rgb[1] + self.m[1][2] * rgb[2],
423 self.m[2][0] * rgb[0] + self.m[2][1] * rgb[1] + self.m[2][2] * rgb[2],
424 ]
425 }
426
427 #[must_use]
430 pub fn primaries_conversion(src: &ColorPrimaries, dst: &ColorPrimaries) -> Self {
431 let src_to_xyz = Self::rgb_to_xyz_matrix(src);
433 let xyz_to_dst = Self::xyz_to_rgb_matrix(dst);
434
435 xyz_to_dst.multiply(&src_to_xyz)
437 }
438
439 #[must_use]
441 fn multiply(&self, other: &Self) -> Self {
442 let mut result = Self::identity();
443
444 for i in 0..3 {
445 for j in 0..3 {
446 result.m[i][j] = 0.0;
447 for k in 0..3 {
448 result.m[i][j] += self.m[i][k] * other.m[k][j];
449 }
450 }
451 }
452
453 result
454 }
455
456 #[must_use]
458 fn rgb_to_xyz_matrix(primaries: &ColorPrimaries) -> Self {
459 let xr = primaries.red.0;
461 let yr = primaries.red.1;
462 let zr = 1.0 - xr - yr;
463
464 let xg = primaries.green.0;
465 let yg = primaries.green.1;
466 let zg = 1.0 - xg - yg;
467
468 let xb = primaries.blue.0;
469 let yb = primaries.blue.1;
470 let zb = 1.0 - xb - yb;
471
472 let xw = primaries.white.0;
474 let yw = primaries.white.1;
475 let yw_y = 1.0; let xw_xyz = (yw_y / yw) * xw;
477 let zw_xyz = (yw_y / yw) * (1.0 - xw - yw);
478
479 let det = xr * (yg * zb - yb * zg) - xg * (yr * zb - yb * zr) + xb * (yr * zg - yg * zr);
481
482 if det.abs() < 1e-10 {
483 return Self::identity();
484 }
485
486 let sr = (xw_xyz * (yg * zb - yb * zg) - xg * (yw_y * zb - zw_xyz * yb)
487 + xb * (yw_y * zg - zw_xyz * yg))
488 / det;
489 let sg = (xr * (yw_y * zb - zw_xyz * yb) - xw_xyz * (yr * zb - yb * zr)
490 + xb * (yr * zw_xyz - yw_y * zr))
491 / det;
492 let sb = (xr * (yg * zw_xyz - yw_y * zg) - xg * (yr * zw_xyz - yw_y * zr)
493 + xw_xyz * (yr * zg - yg * zr))
494 / det;
495
496 Self {
497 m: [
498 [sr * xr, sg * xg, sb * xb],
499 [sr * yr, sg * yg, sb * yb],
500 [sr * zr, sg * zg, sb * zb],
501 ],
502 }
503 }
504
505 #[must_use]
507 fn xyz_to_rgb_matrix(primaries: &ColorPrimaries) -> Self {
508 let m = Self::rgb_to_xyz_matrix(primaries);
509
510 let det = m.m[0][0] * (m.m[1][1] * m.m[2][2] - m.m[1][2] * m.m[2][1])
512 - m.m[0][1] * (m.m[1][0] * m.m[2][2] - m.m[1][2] * m.m[2][0])
513 + m.m[0][2] * (m.m[1][0] * m.m[2][1] - m.m[1][1] * m.m[2][0]);
514
515 if det.abs() < 1e-10 {
516 return Self::identity();
517 }
518
519 let inv_det = 1.0 / det;
520
521 Self {
522 m: [
523 [
524 inv_det * (m.m[1][1] * m.m[2][2] - m.m[1][2] * m.m[2][1]),
525 inv_det * (m.m[0][2] * m.m[2][1] - m.m[0][1] * m.m[2][2]),
526 inv_det * (m.m[0][1] * m.m[1][2] - m.m[0][2] * m.m[1][1]),
527 ],
528 [
529 inv_det * (m.m[1][2] * m.m[2][0] - m.m[1][0] * m.m[2][2]),
530 inv_det * (m.m[0][0] * m.m[2][2] - m.m[0][2] * m.m[2][0]),
531 inv_det * (m.m[0][2] * m.m[1][0] - m.m[0][0] * m.m[1][2]),
532 ],
533 [
534 inv_det * (m.m[1][0] * m.m[2][1] - m.m[1][1] * m.m[2][0]),
535 inv_det * (m.m[0][1] * m.m[2][0] - m.m[0][0] * m.m[2][1]),
536 inv_det * (m.m[0][0] * m.m[1][1] - m.m[0][1] * m.m[1][0]),
537 ],
538 ],
539 }
540 }
541}
542
543#[derive(Clone, Copy, Debug, Default)]
545pub struct HdrMetadata {
546 pub max_cll: Option<f64>,
548 pub max_fall: Option<f64>,
550 pub master_peak: Option<f64>,
552 pub master_min: Option<f64>,
554}
555
556impl HdrMetadata {
557 #[must_use]
559 pub fn estimate_peak_luminance(&self) -> f64 {
560 self.max_cll.or(self.master_peak).unwrap_or(1000.0) }
562}
563
564#[derive(Clone, Debug)]
566pub struct TonemapConfig {
567 pub algorithm: TonemapAlgorithm,
569 pub source_transfer: TransferFunction,
571 pub target_transfer: TransferFunction,
573 pub source_primaries: ColorPrimaries,
575 pub target_primaries: ColorPrimaries,
577 pub params: TonemapParams,
579 pub metadata: HdrMetadata,
581 pub target_format: PixelFormat,
583 pub convert_gamut: bool,
585}
586
587impl TonemapConfig {
588 #[must_use]
590 pub fn new() -> Self {
591 Self {
592 algorithm: TonemapAlgorithm::default(),
593 source_transfer: TransferFunction::Pq,
594 target_transfer: TransferFunction::Bt709,
595 source_primaries: ColorPrimaries::BT2020,
596 target_primaries: ColorPrimaries::BT709,
597 params: TonemapParams::default(),
598 metadata: HdrMetadata::default(),
599 target_format: PixelFormat::Yuv420p,
600 convert_gamut: true,
601 }
602 }
603
604 #[must_use]
606 pub fn with_algorithm(mut self, algorithm: TonemapAlgorithm) -> Self {
607 self.algorithm = algorithm;
608 self
609 }
610
611 #[must_use]
613 pub fn with_source_transfer(mut self, transfer: TransferFunction) -> Self {
614 self.source_transfer = transfer;
615 self
616 }
617
618 #[must_use]
620 pub fn with_target_transfer(mut self, transfer: TransferFunction) -> Self {
621 self.target_transfer = transfer;
622 self
623 }
624
625 #[must_use]
627 pub fn with_source_primaries(mut self, primaries: ColorPrimaries) -> Self {
628 self.source_primaries = primaries;
629 self
630 }
631
632 #[must_use]
634 pub fn with_target_primaries(mut self, primaries: ColorPrimaries) -> Self {
635 self.target_primaries = primaries;
636 self
637 }
638
639 #[must_use]
641 pub fn with_peak_luminance(mut self, nits: f64) -> Self {
642 self.params.peak_luminance = nits;
643 self
644 }
645
646 #[must_use]
648 pub fn with_target_luminance(mut self, nits: f64) -> Self {
649 self.params.target_luminance = nits;
650 self
651 }
652
653 #[must_use]
655 pub fn with_white_point(mut self, nits: f64) -> Self {
656 self.params.white_point = nits;
657 self
658 }
659
660 #[must_use]
662 pub fn with_exposure(mut self, stops: f64) -> Self {
663 self.params.exposure = stops;
664 self
665 }
666
667 #[must_use]
669 pub fn with_contrast(mut self, contrast: f64) -> Self {
670 self.params.contrast = contrast;
671 self
672 }
673
674 #[must_use]
676 pub fn with_saturation(mut self, saturation: f64) -> Self {
677 self.params.saturation = saturation;
678 self
679 }
680
681 #[must_use]
683 pub fn with_metadata(mut self, metadata: HdrMetadata) -> Self {
684 self.metadata = metadata;
685 if self.params.peak_luminance == 1000.0 {
687 self.params.peak_luminance = metadata.estimate_peak_luminance();
688 }
689 self
690 }
691
692 #[must_use]
694 pub fn with_target_format(mut self, format: PixelFormat) -> Self {
695 self.target_format = format;
696 self
697 }
698
699 #[must_use]
701 pub fn with_gamut_conversion(mut self, enable: bool) -> Self {
702 self.convert_gamut = enable;
703 self
704 }
705}
706
707impl Default for TonemapConfig {
708 fn default() -> Self {
709 Self::new()
710 }
711}
712
713pub struct TonemapFilter {
718 id: NodeId,
719 name: String,
720 state: NodeState,
721 inputs: Vec<InputPort>,
722 outputs: Vec<OutputPort>,
723 config: TonemapConfig,
724 gamut_matrix: ColorMatrix3x3,
725}
726
727impl TonemapFilter {
728 #[must_use]
730 pub fn new(id: NodeId, name: impl Into<String>, config: TonemapConfig) -> Self {
731 let gamut_matrix = if config.convert_gamut {
733 ColorMatrix3x3::primaries_conversion(&config.source_primaries, &config.target_primaries)
734 } else {
735 ColorMatrix3x3::identity()
736 };
737
738 let output_format = PortFormat::Video(VideoPortFormat::new(config.target_format));
739
740 Self {
741 id,
742 name: name.into(),
743 state: NodeState::Idle,
744 inputs: vec![InputPort::new(PortId(0), "input", PortType::Video)
745 .with_format(PortFormat::Video(VideoPortFormat::any()))],
746 outputs: vec![
747 OutputPort::new(PortId(0), "output", PortType::Video).with_format(output_format)
748 ],
749 config,
750 gamut_matrix,
751 }
752 }
753
754 #[must_use]
756 pub fn config(&self) -> &TonemapConfig {
757 &self.config
758 }
759
760 fn tonemap_pixel(&self, r: u8, g: u8, b: u8) -> (u8, u8, u8) {
764 let r_norm = r as f64 / 255.0;
766 let g_norm = g as f64 / 255.0;
767 let b_norm = b as f64 / 255.0;
768
769 let peak = self.config.params.peak_luminance;
771 let r_lin = self.config.source_transfer.eotf(r_norm, peak);
772 let g_lin = self.config.source_transfer.eotf(g_norm, peak);
773 let b_lin = self.config.source_transfer.eotf(b_norm, peak);
774
775 let rgb_lin = if self.config.convert_gamut {
777 let converted = self.gamut_matrix.apply([r_lin, g_lin, b_lin]);
778 [
779 converted[0].max(0.0),
780 converted[1].max(0.0),
781 converted[2].max(0.0),
782 ]
783 } else {
784 [r_lin, g_lin, b_lin]
785 };
786
787 let r_mapped = self
789 .config
790 .algorithm
791 .tonemap(rgb_lin[0], &self.config.params);
792 let g_mapped = self
793 .config
794 .algorithm
795 .tonemap(rgb_lin[1], &self.config.params);
796 let b_mapped = self
797 .config
798 .algorithm
799 .tonemap(rgb_lin[2], &self.config.params);
800
801 if (self.config.params.saturation - 1.0).abs() > 0.001 {
803 let luma = 0.2126 * r_mapped + 0.7152 * g_mapped + 0.0722 * b_mapped;
804 let sat = self.config.params.saturation;
805
806 let r_sat = luma + (r_mapped - luma) * sat;
807 let g_sat = luma + (g_mapped - luma) * sat;
808 let b_sat = luma + (b_mapped - luma) * sat;
809
810 let r_out = (self.config.target_transfer.oetf(r_sat.max(0.0), 100.0) * 255.0)
812 .clamp(0.0, 255.0) as u8;
813 let g_out = (self.config.target_transfer.oetf(g_sat.max(0.0), 100.0) * 255.0)
814 .clamp(0.0, 255.0) as u8;
815 let b_out = (self.config.target_transfer.oetf(b_sat.max(0.0), 100.0) * 255.0)
816 .clamp(0.0, 255.0) as u8;
817
818 (r_out, g_out, b_out)
819 } else {
820 let r_out =
822 (self.config.target_transfer.oetf(r_mapped, 100.0) * 255.0).clamp(0.0, 255.0) as u8;
823 let g_out =
824 (self.config.target_transfer.oetf(g_mapped, 100.0) * 255.0).clamp(0.0, 255.0) as u8;
825 let b_out =
826 (self.config.target_transfer.oetf(b_mapped, 100.0) * 255.0).clamp(0.0, 255.0) as u8;
827
828 (r_out, g_out, b_out)
829 }
830 }
831
832 fn yuv_to_rgb(&self, frame: &VideoFrame) -> Vec<u8> {
834 let width = frame.width as usize;
835 let height = frame.height as usize;
836
837 let y_plane = frame.planes.first();
838 let u_plane = frame.planes.get(1);
839 let v_plane = frame.planes.get(2);
840
841 let (h_sub, v_sub) = frame.format.chroma_subsampling();
842 let mut rgb_data = vec![0u8; width * height * 3];
843
844 const KR: f64 = 0.2627;
846 const KB: f64 = 0.0593;
847 const KG: f64 = 1.0 - KR - KB;
848
849 for y in 0..height {
850 for x in 0..width {
851 let y_val = y_plane
852 .map(|p| p.row(y).get(x).copied().unwrap_or(16))
853 .unwrap_or(16) as f64;
854
855 let chroma_x = x / h_sub as usize;
856 let chroma_y = y / v_sub as usize;
857
858 let u_val = u_plane
859 .map(|p| p.row(chroma_y).get(chroma_x).copied().unwrap_or(128))
860 .unwrap_or(128) as f64;
861 let v_val = v_plane
862 .map(|p| p.row(chroma_y).get(chroma_x).copied().unwrap_or(128))
863 .unwrap_or(128) as f64;
864
865 let y_norm = (y_val - 16.0) * 255.0 / 219.0;
867 let cb = (u_val - 128.0) * 255.0 / 224.0;
868 let cr = (v_val - 128.0) * 255.0 / 224.0;
869
870 let r = y_norm + cr / (1.0 - KR) * KR;
871 let g = y_norm - cb / ((1.0 - KB) * KG) * KB - cr / ((1.0 - KR) * KG) * KR;
872 let b = y_norm + cb / (1.0 - KB) * KB;
873
874 let offset = (y * width + x) * 3;
875 rgb_data[offset] = r.clamp(0.0, 255.0) as u8;
876 rgb_data[offset + 1] = g.clamp(0.0, 255.0) as u8;
877 rgb_data[offset + 2] = b.clamp(0.0, 255.0) as u8;
878 }
879 }
880
881 rgb_data
882 }
883
884 fn rgb_to_yuv(
886 &self,
887 rgb_data: &[u8],
888 width: usize,
889 height: usize,
890 ) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
891 const KR: f64 = 0.2126;
893 const KB: f64 = 0.0722;
894 const KG: f64 = 1.0 - KR - KB;
895
896 let mut y_data = vec![0u8; width * height];
897 let chroma_width = width / 2;
898 let chroma_height = height / 2;
899 let mut u_data = vec![128u8; chroma_width * chroma_height];
900 let mut v_data = vec![128u8; chroma_width * chroma_height];
901
902 for y in 0..height {
903 for x in 0..width {
904 let offset = (y * width + x) * 3;
905 let r = rgb_data[offset] as f64;
906 let g = rgb_data[offset + 1] as f64;
907 let b = rgb_data[offset + 2] as f64;
908
909 let y_val = KR * r + KG * g + KB * b;
911 let cb = (b - y_val) / (2.0 * (1.0 - KB));
912 let cr = (r - y_val) / (2.0 * (1.0 - KR));
913
914 let y_out = y_val * 219.0 / 255.0 + 16.0;
915 let cb_out = cb * 224.0 / 255.0 + 128.0;
916 let cr_out = cr * 224.0 / 255.0 + 128.0;
917
918 y_data[y * width + x] = y_out.clamp(16.0, 235.0) as u8;
919
920 if x % 2 == 0 && y % 2 == 0 {
922 let chroma_x = x / 2;
923 let chroma_y = y / 2;
924 u_data[chroma_y * chroma_width + chroma_x] = cb_out.clamp(16.0, 240.0) as u8;
925 v_data[chroma_y * chroma_width + chroma_x] = cr_out.clamp(16.0, 240.0) as u8;
926 }
927 }
928 }
929
930 (y_data, u_data, v_data)
931 }
932
933 fn process_rgb(&self, input: &VideoFrame) -> GraphResult<VideoFrame> {
935 let width = input.width as usize;
936 let height = input.height as usize;
937
938 let src_plane = input
939 .planes
940 .first()
941 .ok_or_else(|| GraphError::ProcessingError {
942 node: self.id,
943 message: "Missing RGB plane".to_string(),
944 })?;
945
946 let src_bpp = if input.format == PixelFormat::Rgba32 {
947 4
948 } else {
949 3
950 };
951 let mut output_rgb = vec![0u8; width * height * 3];
952
953 for y in 0..height {
955 for x in 0..width {
956 let row = src_plane.row(y);
957 let offset = x * src_bpp;
958
959 let r = row.get(offset).copied().unwrap_or(0);
960 let g = row.get(offset + 1).copied().unwrap_or(0);
961 let b = row.get(offset + 2).copied().unwrap_or(0);
962
963 let (r_out, g_out, b_out) = self.tonemap_pixel(r, g, b);
964
965 let out_offset = (y * width + x) * 3;
966 output_rgb[out_offset] = r_out;
967 output_rgb[out_offset + 1] = g_out;
968 output_rgb[out_offset + 2] = b_out;
969 }
970 }
971
972 if self.config.target_format.is_yuv() {
974 let (y_data, u_data, v_data) = self.rgb_to_yuv(&output_rgb, width, height);
975
976 let mut output = VideoFrame::new(self.config.target_format, input.width, input.height);
977 output.timestamp = input.timestamp;
978 output.frame_type = input.frame_type;
979 output.color_info = ColorInfo {
980 full_range: false,
981 ..input.color_info
982 };
983
984 let chroma_width = width / 2;
985 output.planes.push(Plane::new(y_data, width));
986 output.planes.push(Plane::new(u_data, chroma_width));
987 output.planes.push(Plane::new(v_data, chroma_width));
988
989 Ok(output)
990 } else {
991 let mut output = VideoFrame::new(self.config.target_format, input.width, input.height);
992 output.timestamp = input.timestamp;
993 output.frame_type = input.frame_type;
994 output.color_info = input.color_info;
995 output.planes.push(Plane::new(output_rgb, width * 3));
996
997 Ok(output)
998 }
999 }
1000
1001 fn process_yuv(&self, input: &VideoFrame) -> GraphResult<VideoFrame> {
1003 let width = input.width as usize;
1004 let height = input.height as usize;
1005
1006 let rgb_data = self.yuv_to_rgb(input);
1008
1009 let mut output_rgb = vec![0u8; width * height * 3];
1011
1012 for y in 0..height {
1013 for x in 0..width {
1014 let offset = (y * width + x) * 3;
1015 let r = rgb_data[offset];
1016 let g = rgb_data[offset + 1];
1017 let b = rgb_data[offset + 2];
1018
1019 let (r_out, g_out, b_out) = self.tonemap_pixel(r, g, b);
1020
1021 output_rgb[offset] = r_out;
1022 output_rgb[offset + 1] = g_out;
1023 output_rgb[offset + 2] = b_out;
1024 }
1025 }
1026
1027 if self.config.target_format.is_yuv() {
1029 let (y_data, u_data, v_data) = self.rgb_to_yuv(&output_rgb, width, height);
1030
1031 let mut output = VideoFrame::new(self.config.target_format, input.width, input.height);
1032 output.timestamp = input.timestamp;
1033 output.frame_type = input.frame_type;
1034 output.color_info = ColorInfo {
1035 full_range: false,
1036 ..input.color_info
1037 };
1038
1039 let chroma_width = width / 2;
1040 output.planes.push(Plane::new(y_data, width));
1041 output.planes.push(Plane::new(u_data, chroma_width));
1042 output.planes.push(Plane::new(v_data, chroma_width));
1043
1044 Ok(output)
1045 } else {
1046 let mut output = VideoFrame::new(self.config.target_format, input.width, input.height);
1047 output.timestamp = input.timestamp;
1048 output.frame_type = input.frame_type;
1049 output.color_info = input.color_info;
1050 output.planes.push(Plane::new(output_rgb, width * 3));
1051
1052 Ok(output)
1053 }
1054 }
1055
1056 fn tonemap_frame(&self, input: &VideoFrame) -> GraphResult<VideoFrame> {
1058 if input.format.is_yuv() {
1059 self.process_yuv(input)
1060 } else {
1061 self.process_rgb(input)
1062 }
1063 }
1064}
1065
1066impl Node for TonemapFilter {
1067 fn id(&self) -> NodeId {
1068 self.id
1069 }
1070
1071 fn name(&self) -> &str {
1072 &self.name
1073 }
1074
1075 fn node_type(&self) -> NodeType {
1076 NodeType::Filter
1077 }
1078
1079 fn state(&self) -> NodeState {
1080 self.state
1081 }
1082
1083 fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
1084 if !self.state.can_transition_to(state) {
1085 return Err(GraphError::InvalidStateTransition {
1086 node: self.id,
1087 from: self.state.to_string(),
1088 to: state.to_string(),
1089 });
1090 }
1091 self.state = state;
1092 Ok(())
1093 }
1094
1095 fn inputs(&self) -> &[InputPort] {
1096 &self.inputs
1097 }
1098
1099 fn outputs(&self) -> &[OutputPort] {
1100 &self.outputs
1101 }
1102
1103 fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
1104 match input {
1105 Some(FilterFrame::Video(frame)) => {
1106 let tonemapped = self.tonemap_frame(&frame)?;
1107 Ok(Some(FilterFrame::Video(tonemapped)))
1108 }
1109 Some(_) => Err(GraphError::PortTypeMismatch {
1110 expected: "Video".to_string(),
1111 actual: "Audio".to_string(),
1112 }),
1113 None => Ok(None),
1114 }
1115 }
1116}