scope_analyzer/
scope_analyzer.rs1use std::process;
15
16use ff_analysis::{RgbParade, ScopeAnalyzer};
17use ff_decode::VideoDecoder;
18
19fn mean_f32(vals: &[f32]) -> f32 {
20 if vals.is_empty() {
21 return 0.0;
22 }
23 #[allow(clippy::cast_precision_loss)]
24 let len = vals.len() as f32;
25 vals.iter().sum::<f32>() / len
26}
27
28fn print_vectorscope_summary(frame_idx: u64, scatter: &[(f32, f32)]) {
29 if scatter.is_empty() {
30 println!(" [{frame_idx:5}] vectorscope: (unsupported format)");
31 return;
32 }
33 let mean_cb = mean_f32(&scatter.iter().map(|(cb, _)| *cb).collect::<Vec<_>>());
34 let mean_cr = mean_f32(&scatter.iter().map(|(_, cr)| *cr).collect::<Vec<_>>());
35 let max_cb = scatter
36 .iter()
37 .map(|(cb, _)| cb.abs())
38 .fold(0.0_f32, f32::max);
39 let max_cr = scatter
40 .iter()
41 .map(|(_, cr)| cr.abs())
42 .fold(0.0_f32, f32::max);
43 println!(
44 " [{frame_idx:5}] vectorscope: samples={:5} \
45 mean_cb={mean_cb:+.3} mean_cr={mean_cr:+.3} \
46 max_cb={max_cb:.3} max_cr={max_cr:.3}",
47 scatter.len()
48 );
49}
50
51fn print_rgb_parade_summary(frame_idx: u64, parade: &RgbParade) {
52 if parade.r.is_empty() {
53 println!(" [{frame_idx:5}] rgb_parade: (unsupported format)");
54 return;
55 }
56 let all_r: Vec<f32> = parade.r.iter().flatten().copied().collect();
57 let all_g: Vec<f32> = parade.g.iter().flatten().copied().collect();
58 let all_b: Vec<f32> = parade.b.iter().flatten().copied().collect();
59 let avg_r = mean_f32(&all_r);
60 let avg_g = mean_f32(&all_g);
61 let avg_b = mean_f32(&all_b);
62 println!(
63 " [{frame_idx:5}] rgb_parade: cols={:4} \
64 avg_r={avg_r:.3} avg_g={avg_g:.3} avg_b={avg_b:.3}",
65 parade.r.len()
66 );
67}
68
69fn main() {
70 let mut args = std::env::args().skip(1);
71 let mut input = None::<String>;
72 let mut interval_frames: u64 = 30;
73
74 while let Some(flag) = args.next() {
75 match flag.as_str() {
76 "--input" | "-i" => input = Some(args.next().unwrap_or_default()),
77 "--interval" | "-n" => {
78 let raw = args.next().unwrap_or_default();
79 interval_frames = raw.parse().unwrap_or_else(|_| {
80 eprintln!("Invalid interval: {raw}");
81 process::exit(1);
82 });
83 }
84 other => {
85 eprintln!("Unknown flag: {other}");
86 process::exit(1);
87 }
88 }
89 }
90
91 let input = input.unwrap_or_else(|| {
92 eprintln!("Usage: scope_analyzer --input <video> [--interval <frames>]");
93 process::exit(1);
94 });
95
96 let mut decoder = match VideoDecoder::open(&input).build() {
97 Ok(d) => d,
98 Err(e) => {
99 eprintln!("Error opening video: {e}");
100 process::exit(1);
101 }
102 };
103
104 println!("Scope analysis: {input}");
105 println!("Sampling every {interval_frames} frame(s)");
106 println!();
107
108 let mut frame_count: u64 = 0;
109 let mut sample_count: u64 = 0;
110
111 loop {
112 let frame = match decoder.decode_one() {
113 Ok(Some(f)) => f,
114 Ok(None) => break,
115 Err(e) => {
116 eprintln!("Decode error: {e}");
117 process::exit(1);
118 }
119 };
120
121 if frame_count.is_multiple_of(interval_frames) {
122 let scatter = ScopeAnalyzer::vectorscope(&frame);
123 let parade = ScopeAnalyzer::rgb_parade(&frame);
124 print_vectorscope_summary(frame_count, &scatter);
125 print_rgb_parade_summary(frame_count, ¶de);
126 sample_count += 1;
127 }
128
129 frame_count += 1;
130 }
131
132 println!();
133 println!("Decoded {frame_count} frame(s), sampled {sample_count}.");
134}