diff --git a/CHANGELOG.md b/CHANGELOG.md index 30826c7..4228147 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased (0.8.1) * Support negative `--preset` args. +* Add `--vmaf-fps`: Frame rate override used to analyse both reference & distorted videos. # v0.8.0 * crf-search: Tweak 2nd iteration logic that slices the crf range at the 25% or 75% crf point. diff --git a/src/command/args/vmaf.rs b/src/command/args/vmaf.rs index a4e3c08..1ea4e66 100644 --- a/src/command/args/vmaf.rs +++ b/src/command/args/vmaf.rs @@ -4,7 +4,7 @@ use clap::Parser; use std::{borrow::Cow, fmt::Display, sync::Arc, thread}; /// Common vmaf options. -#[derive(Parser, Clone, Hash)] +#[derive(Debug, Default, Parser, Clone)] pub struct Vmaf { /// Additional vmaf arg(s). E.g. --vmaf n_threads=8 --vmaf n_subsample=4 /// @@ -29,9 +29,18 @@ pub struct Vmaf { /// to post input/reference vfilter dimensions. /// /// Scaling happens after any input/reference vfilters. - #[arg(long, default_value_t = VmafScale::Auto, value_parser = parse_vmaf_scale)] + #[arg(long, default_value_t, value_parser = parse_vmaf_scale)] pub vmaf_scale: VmafScale, + /// Frame rate override used to analyse both reference & distorted videos. + /// Maps to ffmpeg `-r` input arg. + /// + /// Setting to a value, e.g. 25, can workaround issues with some videos. + /// + /// By default no override is set. + #[arg(long)] + pub vmaf_fps: Option, + /// Ffmpeg video filter applied to the VMAF reference before analysis. /// E.g. --reference-vfilter "scale=1280:-1,fps=24". /// @@ -40,6 +49,15 @@ pub struct Vmaf { pub reference_vfilter: Option, } +impl std::hash::Hash for Vmaf { + fn hash(&self, state: &mut H) { + self.vmaf_args.hash(state); + self.vmaf_scale.hash(state); + self.vmaf_fps.map(|f| f.to_ne_bytes()).hash(state); + self.reference_vfilter.hash(state); + } +} + fn parse_vmaf_arg(arg: &str) -> anyhow::Result> { Ok(arg.to_owned().into()) } @@ -49,6 +67,7 @@ impl Vmaf { let Self { vmaf_args, vmaf_scale, + vmaf_fps: _, reference_vfilter, } = self; vmaf_args.is_empty() && *vmaf_scale == VmafScale::Auto && reference_vfilter.is_none() @@ -147,11 +166,15 @@ fn minimally_scale((from_w, from_h): (u32, u32), (target_w, target_h): (u32, u32 } } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum VmafScale { None, + #[default] Auto, - Custom { width: u32, height: u32 }, + Custom { + width: u32, + height: u32, + }, } fn parse_vmaf_scale(vs: &str) -> anyhow::Result { @@ -208,8 +231,7 @@ impl VmafModel { fn vmaf_lavfi() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(None, PixelFormat::Yuv420p, Some("scale=1280:-1,fps=24")), @@ -224,6 +246,7 @@ fn vmaf_lavfi_override_reference_vfilter() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()], vmaf_scale: VmafScale::Auto, + vmaf_fps: None, reference_vfilter: Some("scale=2560:-1".into()), }; assert_eq!( @@ -240,10 +263,25 @@ fn vmaf_lavfi_override_reference_vfilter() { #[test] fn vmaf_lavfi_default() { + let vmaf = Vmaf::default(); + let expected = format!( + "[0:v]format=yuv420p10le,setpts=PTS-STARTPTS,settb=AVTB[dis];\ + [1:v]format=yuv420p10le,setpts=PTS-STARTPTS,settb=AVTB[ref];\ + [dis][ref]libvmaf=shortest=true:ts_sync_mode=nearest:n_threads={}", + thread::available_parallelism().map_or(1, |p| p.get()) + ); + assert_eq!( + vmaf.ffmpeg_lavfi(None, PixelFormat::Yuv420p10le, None), + expected + ); +} + +/// `vmaf_fps` shouldn't affect lavfi +#[test] +fn vmaf_fps_lavfi() { let vmaf = Vmaf { - vmaf_args: vec![], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + vmaf_fps: Some(25.0), + ..<_>::default() }; let expected = format!( "[0:v]format=yuv420p10le,setpts=PTS-STARTPTS,settb=AVTB[dis];\ @@ -261,8 +299,7 @@ fn vmaf_lavfi_default() { fn vmaf_lavfi_include_n_threads() { let vmaf = Vmaf { vmaf_args: vec!["log_path=output.xml".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; let expected = format!( "[0:v]format=yuv420p,setpts=PTS-STARTPTS,settb=AVTB[dis];\ @@ -281,8 +318,7 @@ fn vmaf_lavfi_include_n_threads() { fn vmaf_lavfi_small_width() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None), @@ -297,8 +333,7 @@ fn vmaf_lavfi_small_width() { fn vmaf_lavfi_4k() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((3840, 2160)), PixelFormat::Yuv420p, None), @@ -313,8 +348,7 @@ fn vmaf_lavfi_4k() { fn vmaf_lavfi_3k_upscale_to_4k() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((3008, 1692)), PixelFormat::Yuv420p, None), @@ -333,8 +367,7 @@ fn vmaf_lavfi_small_width_custom_model() { "n_threads=5".into(), "n_subsample=4".into(), ], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None), @@ -357,7 +390,7 @@ fn vmaf_lavfi_custom_model_and_width() { width: 123, height: 720, }, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((1280, 720)), PixelFormat::Yuv420p, None), @@ -371,8 +404,7 @@ fn vmaf_lavfi_custom_model_and_width() { fn vmaf_lavfi_1080p() { let vmaf = Vmaf { vmaf_args: vec!["n_threads=5".into(), "n_subsample=4".into()], - vmaf_scale: VmafScale::Auto, - reference_vfilter: None, + ..<_>::default() }; assert_eq!( vmaf.ffmpeg_lavfi(Some((1920, 1080)), PixelFormat::Yuv420p, None), diff --git a/src/command/sample_encode.rs b/src/command/sample_encode.rs index 508343a..34f7c90 100644 --- a/src/command/sample_encode.rs +++ b/src/command/sample_encode.rs @@ -294,6 +294,7 @@ pub fn run( .max(input_pixel_format.unwrap_or(PixelFormat::Yuv444p10le)), args.vfilter.as_deref(), ), + vmaf.vmaf_fps, )?; let mut vmaf = pin!(vmaf); let mut logger = ProgressLogger::new("ab_av1::vmaf", Instant::now()); diff --git a/src/command/vmaf.rs b/src/command/vmaf.rs index 124eb93..82e9acb 100644 --- a/src/command/vmaf.rs +++ b/src/command/vmaf.rs @@ -74,6 +74,7 @@ pub async fn vmaf( dpix_fmt.max(rpix_fmt), vmaf.reference_vfilter.as_deref(), ), + vmaf.vmaf_fps, )?); let mut logger = ProgressLogger::new(module_path!(), Instant::now()); let mut vmaf_score = None; diff --git a/src/vmaf.rs b/src/vmaf.rs index 92af767..1f7113f 100644 --- a/src/vmaf.rs +++ b/src/vmaf.rs @@ -13,6 +13,7 @@ pub fn run( reference: &Path, distorted: &Path, filter_complex: &str, + fps: Option, ) -> anyhow::Result> { info!( "vmaf {} vs reference {}", @@ -22,7 +23,9 @@ pub fn run( let mut cmd = Command::new("ffmpeg"); cmd.kill_on_drop(true) + .arg2_opt("-r", fps) .arg2("-i", distorted) + .arg2_opt("-r", fps) .arg2("-i", reference) .arg2("-filter_complex", filter_complex) .arg2("-f", "null")