noest 0.1.0

Tool to estimate noise in a video and output the results, optionally as ISO for use with photon noise in AV1 encoding.
#[cfg(not(target_env = "msvc"))]
use tikv_jemallocator::Jemalloc;

#[cfg(not(target_env = "msvc"))]
#[global_allocator]
static GLOBAL: Jemalloc = Jemalloc;

use std::{path::{PathBuf, Path}, time::Duration, io::{stderr, IsTerminal}};

use clap::{Parser, ValueEnum};
use indicatif::{ProgressBar, ProgressDrawTarget};

mod noise_estimate;
mod progress_stuff;

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Format {
    /// Output the median for each plane
    Medians,
    /// Output just the median
    Median,

    /// Output the iso value (LUMA:ISO, CHROMA:ISO)
    ISO,

    /// Returns Av1an arguments to use
    Av1an
}

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    /// The video to run noise estimation on
    input: PathBuf,

    /// Formatting to use when outputting
    #[arg(long, short, value_enum, default_value_t = Format::Medians)]
    format: Format,

    /// Whether to output ISO as strength or not
    /// A Strength value is ISO/100 (4 = 400 ISO)
    /// 
    /// Only applies when using --format iso
    #[arg(long, short, default_value_t = false, verbatim_doc_comment)]
    strength: bool,

    /// Amount of threads to use for decoding and noise estimation
    #[arg(long, short, default_value_t = 1)]
    threads: usize,
}

fn main() {
    let cli = Cli::parse();
    let (y, u, v) = run(&cli.input, cli.threads);

    let avg_y: f64 = y.iter().sum::<f64>() / y.len() as f64;
    let avg_u: f64 = u.iter().sum::<f64>() / u.len() as f64;
    let avg_v: f64 = v.iter().sum::<f64>() / v.len() as f64;
    //println!("Y: {avg_y}\nU: {avg_u}\nV: {avg_v}");

    match cli.format {
        Format::Medians => {
            println!("Y:{avg_y}");
            println!("U:{avg_u}");
            println!("V:{avg_v}");
        },
        Format::ISO => {
            if avg_y > avg_u+avg_v {
                // It's likely luma noise
                let iso = iso_from_noise(avg_y, cli.strength);
                println!("LUMA:{iso}");
            } else {
                // It's likely chroma noise

                let noise_floor = (avg_y+avg_u+avg_v)/3.0;
                let iso = iso_from_noise(noise_floor, cli.strength);
                println!("CHROMA:{iso}");
            }
        },
        Format::Av1an => {
            if avg_y > avg_u+avg_v {
                // It's likely luma noise
                let iso = iso_from_noise(avg_y, true);
                println!("--photon noise {iso}");
            } else {
                // It's likely chroma noise

                let noise_floor = (avg_y+avg_u+avg_v)/3.0;
                let iso = iso_from_noise(noise_floor, true);
                println!("--photon-noise {iso} --chroma-noise");
            }
        },
        Format::Median => {
            if avg_y > avg_u+avg_v {
                // It's likely luma noise
                println!("{avg_y}");
            } else {
                let noise = (avg_y+avg_u+avg_v)/3.0;
                println!("{noise}");
            }
        },
    }


}

// Converts the estimated noise value to a photon-noise iso
// Outputs in iso*k
// 
// Todo: More precise coefficients
fn iso_from_noise(noise: f64, stength: bool) -> usize {

    let (a, b, c) = (0.745974214, 1570.33842, -5.16323056);

    let res = libm::exp((noise-c)/a)-b;
    let iso = libm::round(res/100.0).clamp(0.0, 64.0);

    if stength { iso as usize } else { iso as usize * 100 }
}

fn run(path: &Path, threads: usize) -> (Vec<f64>, Vec<f64>, Vec<f64>) {
    let (frame_count_opt, receiver) = noise_estimate::run(path, threads);

    let progress = if stderr().is_terminal() {
        let pb = if let Some(frame_count) = frame_count_opt {
            ProgressBar::new(frame_count as u64)
                .with_style(progress_stuff::pretty_progress_style())
        } else {
            ProgressBar::new_spinner().with_style(progress_stuff::pretty_spinner_style())
        };
        pb.set_draw_target(ProgressDrawTarget::stderr());
        pb.enable_steady_tick(Duration::from_millis(100));
        pb.reset();
        pb.reset_eta();
        pb.reset_elapsed();
        pb.set_position(0);
        pb
    } else {
        ProgressBar::hidden()
    };

    let mut results = Vec::new();
    for score in receiver {
        results.push(score);
        progress.inc(1);
    }

    progress.finish();

    let results_y: Vec<f64> = results.iter().map(|r| { r[0] }).collect();
    let results_u: Vec<f64> = results.iter().map(|r| { r[1] }).collect();
    let results_v: Vec<f64> = results.iter().map(|r| { r[2] }).collect();

    (results_y, results_u, results_v)
}