Skip to main content

histogram/
histogram.rs

1//! Extract per-channel color histograms from a video file.
2//!
3//! Uses [`HistogramExtractor`] to compute R, G, B, and luma histograms at
4//! configurable frame intervals.  Prints a summary of each sampled frame's
5//! dominant color bin and mean luma.
6//!
7//! # Usage
8//!
9//! ```bash
10//! cargo run --example histogram -- --input video.mp4
11//! cargo run --example histogram -- --input video.mp4 --interval 30
12//! ```
13
14use std::process;
15
16use ff_analysis::{FrameHistogram, HistogramExtractor};
17
18/// Returns the bin index with the highest count (dominant intensity level).
19fn dominant_bin(channel: &[u32; 256]) -> usize {
20    channel
21        .iter()
22        .enumerate()
23        .max_by_key(|&(_, count)| count)
24        .map_or(0, |(i, _)| i)
25}
26
27/// Computes mean value from a histogram (weighted average of bin indices).
28fn mean_from_histogram(channel: &[u32; 256]) -> f64 {
29    let total: u64 = channel.iter().map(|&c| u64::from(c)).sum();
30    if total == 0 {
31        return 0.0;
32    }
33    let weighted_sum: u64 = channel
34        .iter()
35        .enumerate()
36        .map(|(i, &c)| i as u64 * u64::from(c))
37        .sum();
38    #[allow(clippy::cast_precision_loss)]
39    let result = weighted_sum as f64 / total as f64;
40    result
41}
42
43fn print_histogram_summary(i: usize, h: &FrameHistogram) {
44    let secs = h.timestamp.as_secs_f64();
45    let mean_r = mean_from_histogram(&h.r);
46    let mean_g = mean_from_histogram(&h.g);
47    let mean_b = mean_from_histogram(&h.b);
48    let mean_luma = mean_from_histogram(&h.luma);
49    println!(
50        "  [{i:4}] t={secs:7.3}s  R={mean_r:5.1}  G={mean_g:5.1}  B={mean_b:5.1}  luma={mean_luma:5.1}  \
51         dominant_r={:3}  dominant_g={:3}  dominant_b={:3}",
52        dominant_bin(&h.r),
53        dominant_bin(&h.g),
54        dominant_bin(&h.b),
55    );
56}
57
58fn main() {
59    let mut args = std::env::args().skip(1);
60    let mut input = None::<String>;
61    let mut interval_frames = 30u32;
62
63    while let Some(flag) = args.next() {
64        match flag.as_str() {
65            "--input" | "-i" => input = Some(args.next().unwrap_or_default()),
66            "--interval" | "-n" => {
67                let raw = args.next().unwrap_or_default();
68                interval_frames = raw.parse().unwrap_or_else(|_| {
69                    eprintln!("Invalid interval: {raw}");
70                    process::exit(1);
71                });
72            }
73            other => {
74                eprintln!("Unknown flag: {other}");
75                process::exit(1);
76            }
77        }
78    }
79
80    let input = input.unwrap_or_else(|| {
81        eprintln!("Usage: histogram --input <video> [--interval <frames>]");
82        process::exit(1);
83    });
84
85    println!("Extracting color histograms from: {input}");
86    println!("Sampling every {interval_frames} frame(s)");
87    println!();
88
89    let histograms: Vec<FrameHistogram> = HistogramExtractor::new(&input)
90        .interval_frames(interval_frames)
91        .run()
92        .unwrap_or_else(|e| {
93            eprintln!("Error: {e}");
94            process::exit(1);
95        });
96
97    println!("Extracted {} histogram(s).", histograms.len());
98    println!();
99    println!(
100        "  {:^6}  {:^8}  {:^6}  {:^6}  {:^6}  {:^6}  {:^11}  {:^11}  {:^11}",
101        "Index", "Time (s)", "Mean R", "Mean G", "Mean B", "Luma", "Dom. R", "Dom. G", "Dom. B"
102    );
103    println!("{}", "-".repeat(95));
104
105    let display_count = histograms.len().min(30);
106    for (i, h) in histograms.iter().take(display_count).enumerate() {
107        print_histogram_summary(i, h);
108    }
109    if histograms.len() > display_count {
110        println!("  … ({} more histograms)", histograms.len() - display_count);
111    }
112}