Skip to main content

scope_analyzer/
scope_analyzer.rs

1//! Analyse colour scope data for every sampled frame in a video file.
2//!
3//! Uses [`ScopeAnalyzer::vectorscope`] and [`ScopeAnalyzer::rgb_parade`] to
4//! compute chroma scatter (Cb/Cr) and per-channel RGB waveform data.  Frames
5//! are sampled at a configurable interval and a summary is printed.
6//!
7//! # Usage
8//!
9//! ```bash
10//! cargo run --example scope_analyzer -- --input video.mp4
11//! cargo run --example scope_analyzer -- --input video.mp4 --interval 60
12//! ```
13
14use 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, &parade);
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}