zoomvtools 2.0.0

Video motion vector analysis utilities in pure Rust
Documentation
#[cfg(test)]
mod tests;

use anyhow::{Result, anyhow, ensure};
use safefma::Fma;
use semisafe::slice::get as semisafe_get;
use semisafe::slice::get_mut as semisafe_get_mut;

use crate::{
    util::{Pixel, reduce_fraction},
    video::Framerate,
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
/// Source-frame window and interpolation offset for one synthesized output frame.
pub struct FrameWindow {
    /// Index of the earlier source frame.
    pub nleft: usize,
    /// Index of the later source frame.
    pub nright: usize,
    /// Blend or interpolation position in 1/256ths.
    pub time256: i32,
}

#[must_use]
#[inline]
/// Returns the output frame count for block-based FPS conversion.
#[cfg(test)]
pub fn output_num_frames(input_frames: usize, fa: u64, fb: u64) -> usize {
    1 + (input_frames - 1) * usize::try_from(fb).expect("fps ratio fits usize")
        / usize::try_from(fa).expect("fps ratio fits usize")
}

#[inline]
/// Resolves the requested output frame rate, defaulting to 2x the source rate
/// when `num` or `den` is zero.
pub fn resolve_target_fps(source: Framerate, num: i64, den: i64) -> Result<(u64, u64)> {
    if num == 0 || den == 0 {
        return Ok((source.numerator * 2, source.denominator));
    }

    let num = u64::try_from(num).map_err(|_| anyhow!("num must be greater than 0."))?;
    let den = u64::try_from(den).map_err(|_| anyhow!("den must be greater than 0."))?;

    ensure!(num > 0, "num must be greater than 0.");
    ensure!(den > 0, "den must be greater than 0.");

    Ok(reduce_fraction(num, den))
}

#[must_use]
#[inline]
/// Computes the source-frame window used to synthesize output frame `n`.
pub fn frame_window(n: usize, fa: u64, fb: u64, off: usize) -> FrameWindow {
    let nleft = (u128::from(n as u64) * u128::from(fa) / u128::from(fb)) as usize;
    let scaled = (n as f64) * fa as f64 / fb as f64;
    let mut time256 = (scaled - nleft as f64).fma(256.0, 0.5) as i32;
    if off > 1 {
        time256 /= off as i32;
    }

    FrameWindow {
        nleft,
        nright: nleft + off,
        time256,
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "shared FPS blend helper mirrors C usage"
)]
#[inline]
/// Blends `src` and `reference` into `dst` using a 1/256th fractional time.
pub fn blend_plane<T: Pixel>(
    dst: &mut [T],
    src: &[T],
    reference: &[T],
    dst_stride: usize,
    src_stride: usize,
    ref_stride: usize,
    width: usize,
    height: usize,
    time256: i32,
) {
    for y in 0..height {
        for x in 0..width {
            let value =
                (<T as num_traits::AsPrimitive<u32>>::as_(*semisafe_get(src, y * src_stride + x))
                    * (256 - time256) as u32
                    + <T as num_traits::AsPrimitive<u32>>::as_(*semisafe_get(
                        reference,
                        y * ref_stride + x,
                    )) * time256 as u32)
                    >> 8;
            *semisafe_get_mut(dst, y * dst_stride + x) = T::from_u32_or_max_value(value);
        }
    }
}