1use std::fmt;
4use std::path::PathBuf;
5use std::str::FromStr;
6
7use crate::{ColorSpace, Error};
8use ffcine_core::color::named_enum;
9use ffcine_core::color::{HdrMetadata, Matrix, Primaries, Range, Transfer};
10use ffcine_core::geom::Mat3;
11use ffcine_core::lut::{Lut, LutType};
12pub use ffcine_filters::{Denoise, Grain, Spatial, Temporal};
13
14named_enum!(
15 ToneMapping {
17 Auto = "auto",
18 Bt2390 = "bt.2390",
20 Bt2446a = "bt.2446a",
21 Spline = "spline",
22 Reinhard = "reinhard",
23 Mobius = "mobius",
24 Hable = "hable",
25 Clip = "clip",
26 St2094_40 = "st2094-40",
28 St2094_10 = "st2094-10",
30 Gamma = "gamma",
31 Linear = "linear",
32 }
33);
34
35named_enum!(
36 GamutMapping {
38 Perceptual = "perceptual",
39 Relative = "relative",
40 Saturation = "saturation",
41 Absolute = "absolute",
42 Desaturate = "desaturate",
43 Darken = "darken",
44 Clip = "clip",
45 Softclip = "softclip",
47 Highlight = "highlight",
49 Linear = "linear",
51 }
52);
53
54#[derive(Debug, Clone, Copy, PartialEq)]
57pub struct ColorMapTuning {
58 pub contrast_recovery: f32,
60 pub perceptual_strength: f32,
62 pub perceptual_deadzone: f32,
64}
65
66impl Default for ColorMapTuning {
67 fn default() -> Self {
68 Self {
69 contrast_recovery: 0.30,
70 perceptual_strength: 0.80,
71 perceptual_deadzone: 0.30,
72 }
73 }
74}
75
76named_enum!(
77 Scaler {
79 Lanczos = "lanczos",
80 EwaLanczos = "ewa_lanczos",
81 EwaLanczosSharp = "ewa_lanczossharp",
83 Spline36 = "spline36",
84 Mitchell = "mitchell",
85 Bicubic = "bicubic",
86 Bilinear = "bilinear",
87 Nearest = "nearest",
88 }
89);
90
91#[derive(Debug, Clone, Copy, PartialEq)]
94pub struct Scaling {
95 pub upscaler: Scaler,
96 pub downscaler: Scaler,
97 pub linear: bool,
99 pub sigmoid: bool,
101}
102
103impl Default for Scaling {
104 fn default() -> Self {
105 Self {
106 upscaler: Scaler::Lanczos,
107 downscaler: Scaler::Lanczos,
108 linear: false,
109 sigmoid: false,
110 }
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116pub struct Size {
117 pub width: u32,
118 pub height: u32,
119}
120
121impl FromStr for Size {
122 type Err = Error;
123 fn from_str(s: &str) -> Result<Self, Error> {
125 let invalid = || Error::Invalid {
126 what: "size (WxH)",
127 value: s.to_owned(),
128 };
129 let (w, h) = s.split_once('x').ok_or_else(invalid)?;
130 Ok(Self {
131 width: w.parse().map_err(|_| invalid())?,
132 height: h.parse().map_err(|_| invalid())?,
133 })
134 }
135}
136
137impl fmt::Display for Size {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 write!(f, "{}x{}", self.width, self.height)
140 }
141}
142
143#[derive(Debug, Clone, Copy, PartialEq)]
148pub struct Crop {
149 pub x: f32,
150 pub y: f32,
151 pub width: f32,
152 pub height: f32,
153}
154
155impl FromStr for Crop {
156 type Err = Error;
157 fn from_str(s: &str) -> Result<Self, Error> {
159 let invalid = || Error::Invalid {
160 what: "crop (w:h:x:y)",
161 value: s.to_owned(),
162 };
163 let v: Vec<f32> = s
164 .split(':')
165 .map(str::parse)
166 .collect::<Result<_, _>>()
167 .map_err(|_| invalid())?;
168 match v[..] {
169 [width, height, x, y] => Ok(Self {
170 x,
171 y,
172 width,
173 height,
174 }),
175 _ => Err(invalid()),
176 }
177 }
178}
179
180named_enum!(
181 DitherMethod {
183 Blue = "blue",
185 Ordered = "ordered",
186 White = "white",
187 }
188);
189
190#[derive(Debug, Clone, Copy, PartialEq)]
192pub struct Dither {
193 pub method: DitherMethod,
194 pub temporal: bool,
196}
197
198impl Default for Dither {
199 fn default() -> Self {
200 Self {
201 method: DitherMethod::Blue,
202 temporal: false,
203 }
204 }
205}
206
207#[derive(Debug, Clone, Copy, PartialEq)]
209pub struct Deband {
210 pub iterations: u32,
211 pub threshold: f32,
212 pub radius: f32,
213 pub grain: f32,
214}
215
216impl Default for Deband {
217 fn default() -> Self {
219 Self {
220 iterations: 1,
221 threshold: 4.0,
222 radius: 16.0,
223 grain: 6.0,
224 }
225 }
226}
227
228#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
230pub struct ColorOverride {
231 pub primaries: Option<Primaries>,
232 pub transfer: Option<Transfer>,
233 pub matrix: Option<Matrix>,
234 pub range: Option<Range>,
235}
236
237impl FromStr for ColorOverride {
238 type Err = Error;
239 #[allow(clippy::many_single_char_names)]
242 fn from_str(s: &str) -> Result<Self, Error> {
243 fn opt<T: FromStr<Err = Error>>(v: &str) -> Result<Option<T>, Error> {
244 if v == "-" {
245 Ok(None)
246 } else {
247 v.parse().map(Some)
248 }
249 }
250 let v: Vec<&str> = s.split('/').collect();
251 match v[..] {
252 [m, p, t, r] => Ok(Self {
253 matrix: opt(m)?,
254 primaries: opt(p)?,
255 transfer: opt(t)?,
256 range: opt(r)?,
257 }),
258 _ => Err(Error::Invalid {
259 what: "colour override (matrix/primaries/transfer/range)",
260 value: s.to_owned(),
261 }),
262 }
263 }
264}
265
266#[derive(Debug, Clone, PartialEq)]
268pub struct LutStage {
269 pub lut: Lut,
270 pub kind: LutType,
271}
272
273#[derive(Debug, Clone, PartialEq, Default)]
278pub struct Warp {
279 pub transforms: Vec<Mat3>,
280}
281
282#[derive(Debug, Clone, PartialEq, Default)]
284pub struct Hook {
285 pub name: String,
287 pub source: String,
289 pub params: Vec<(String, f64)>,
291 pub tracks: Vec<ParamTrack>,
293}
294
295#[derive(Debug, Clone, PartialEq, Default)]
298pub struct ParamTrack {
299 pub name: String,
300 pub values: Vec<f32>,
301}
302
303impl ParamTrack {
304 pub fn at(&self, frame: usize) -> Option<f32> {
305 self.values.get(frame).or(self.values.last()).copied()
306 }
307}
308
309impl Grade {
310 pub fn merge_luts(&mut self) {
314 let (Some(cam), Some(lut)) = (&self.input_lut, &self.lut) else {
315 return;
316 };
317 if cam.kind != LutType::Normalized || lut.kind != LutType::Normalized {
318 return;
319 }
320 let size = cam.lut.size.max(lut.lut.size).max(33);
321 let baked = crate::bake::Bake {
322 camera: Some(&cam.lut),
323 lut: Some(&lut.lut),
324 look: crate::shaders::LookParams::default(),
325 }
326 .bake(size);
327 self.input_lut = Some(LutStage {
328 lut: baked,
329 kind: LutType::Normalized,
330 });
331 self.lut = None;
332 }
333
334 pub fn same_frame(&self, a: usize, b: usize) -> bool {
339 self.crop_at(a) == self.crop_at(b)
340 && self.warp.is_none()
341 && self.grain.is_none()
342 && self.shaders.is_empty()
343 && self.dither.is_none_or(|d| !d.temporal)
344 && self.hooks.iter().all(|h| {
345 !h.source
346 .contains(&format!("//!PARAM {}", ffcine_stabilize::hook::FRAME_PARAM))
347 && h.tracks.iter().all(|t| t.at(a) == t.at(b))
348 })
349 }
350
351 pub fn crop_at(&self, i: usize) -> Option<Crop> {
353 self.crops
354 .get(i)
355 .or(self.crops.last())
356 .copied()
357 .or(self.crop)
358 }
359}
360
361#[derive(Debug, Clone, Copy, PartialEq, Default)]
363pub enum Border {
364 #[default]
365 Black,
366 Blur(f32),
369}
370
371#[derive(Debug, Clone, PartialEq)]
374pub struct Grade {
375 pub input_color: ColorOverride,
377 pub input_lut: Option<LutStage>,
380 pub lut: Option<LutStage>,
381 pub rgb_gain: Option<[f32; 3]>,
385 pub tone_mapping: ToneMapping,
386 pub gamut_mapping: GamutMapping,
387 pub color_map: ColorMapTuning,
388 pub peak_detect: bool,
390 pub deband: Option<Deband>,
392 pub dither: Option<Dither>,
394 pub crop: Option<Crop>,
395 pub warp: Option<Warp>,
397 pub crops: Vec<Crop>,
400 pub place: Option<Crop>,
403 pub border: Border,
405 pub size: Option<Size>,
407 pub scaling: Scaling,
408 pub sharpen: f32,
410 pub hooks: Vec<Hook>,
414 pub shaders: Vec<PathBuf>,
416 pub denoise: Option<Denoise>,
418 pub grain: Option<Grain>,
420 pub output: ColorSpace,
421 pub output_hdr: Option<HdrMetadata>,
425}
426
427impl Default for Grade {
428 fn default() -> Self {
429 Self {
430 input_color: ColorOverride::default(),
431 input_lut: None,
432 lut: None,
433 rgb_gain: None,
434 tone_mapping: ToneMapping::Bt2390,
435 gamut_mapping: GamutMapping::Perceptual,
436 color_map: ColorMapTuning::default(),
437 peak_detect: true,
438 deband: None,
439 dither: Some(Dither::default()),
440 crop: None,
441 warp: None,
442 crops: Vec::new(),
443 place: None,
444 border: Border::Black,
445 size: None,
446 scaling: Scaling::default(),
447 sharpen: 0.0,
448 hooks: Vec::new(),
449 shaders: Vec::new(),
450 denoise: None,
451 grain: None,
452 output: ColorSpace::BT709_TV,
453 output_hdr: None,
454 }
455 }
456}
457
458#[cfg(test)]
459mod same_frame_tests {
460 use super::*;
461
462 #[test]
463 fn same_frame_follows_tracks_and_crops() {
464 let mut g = Grade {
465 dither: None,
466 hooks: vec![Hook {
467 name: "alpha".into(),
468 source: crate::shaders::layer_alpha().into(),
469 params: Vec::new(),
470 tracks: vec![ParamTrack {
471 name: "opacity".into(),
472 values: vec![0.0, 0.5, 1.0, 1.0],
473 }],
474 }],
475 ..Grade::default()
476 };
477 assert!(!g.same_frame(0, 1));
478 assert!(g.same_frame(2, 3) && g.same_frame(3, 9));
479 let c = |x| Crop {
480 x,
481 y: 0.0,
482 width: 10.0,
483 height: 10.0,
484 };
485 g.crops = vec![c(0.0), c(0.0), c(0.0), c(1.0)];
486 assert!(!g.same_frame(2, 3));
487 g.crops.clear();
488 g.dither = Some(Dither {
489 temporal: true,
490 ..Dither::default()
491 });
492 assert!(!g.same_frame(2, 3));
493 }
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn merges_camera_and_grade_luts() {
502 let mut half = Lut::identity(5);
503 for e in &mut half.data {
504 *e = e.map(|v| v * 0.5);
505 }
506 let stage = |lut| LutStage {
507 lut,
508 kind: LutType::Normalized,
509 };
510 let mut g = Grade {
511 input_lut: Some(stage(half.clone())),
512 lut: Some(stage(half)),
513 ..Grade::default()
514 };
515 g.merge_luts();
516 assert!(g.lut.is_none());
517 let m = g.input_lut.unwrap().lut;
518 assert_eq!(m.size, 33);
519 assert!((m.sample([1.0, 0.5, 0.0])[0] - 0.25).abs() < 1e-5);
520 }
521
522 #[test]
523 fn parse_size_and_crop() {
524 assert_eq!(
525 "3840x2160".parse::<Size>().unwrap(),
526 Size {
527 width: 3840,
528 height: 2160
529 }
530 );
531 assert!("3840".parse::<Size>().is_err());
532 let c: Crop = "1920:1080:10:20".parse().unwrap();
533 assert_eq!((c.width, c.x), (1920.0, 10.0));
534 assert!("1:2:3".parse::<Crop>().is_err());
535 let o: ColorOverride = "-/bt709/bt709/tv".parse().unwrap();
536 assert_eq!(o.matrix, None);
537 assert_eq!(o.range, Some(Range::Limited));
538 assert!("bt709/bt709".parse::<ColorOverride>().is_err());
539 assert_eq!(
540 "bt.2390".parse::<ToneMapping>().unwrap(),
541 ToneMapping::Bt2390
542 );
543 assert_eq!(
544 "st2094-40".parse::<ToneMapping>().unwrap(),
545 ToneMapping::St2094_40
546 );
547 assert_eq!(
548 "softclip".parse::<GamutMapping>().unwrap(),
549 GamutMapping::Softclip
550 );
551 }
552}