use std::{
cmp::min,
num::{NonZeroU8, NonZeroUsize},
};
use anyhow::{Result, anyhow, bail};
use crate::{
analysis::MVAnalysisData,
frame::FrameView,
group_of_planes::GroupOfPlanes,
mv_gof::MVGroupOfFrames,
params::{DctMode, DivideMode, MVPlaneSet, MotionFlags, PenaltyScaling, SearchType, Subpel},
plane_of_blocks::MvsOutput,
util::Pixel,
video::{ColorFamily, SampleType, VideoInfo},
};
#[cfg(test)]
mod tests;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SuperClipInfo {
pub height: NonZeroUsize,
pub hpad: usize,
pub vpad: usize,
pub pel: Subpel,
pub mode_yuv: MVPlaneSet,
pub levels: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct AnalyseOptions {
pub blk_size_x: Option<usize>,
pub blk_size_y: Option<usize>,
pub levels: Option<usize>,
pub search_type: Option<SearchType>,
pub search_param: Option<i32>,
pub pel_search: Option<usize>,
pub is_backward: bool,
pub lambda: Option<u32>,
pub chroma: Option<bool>,
pub delta_frame: isize,
pub truemotion: bool,
pub lambda_sad: Option<u32>,
pub penalty_level: Option<PenaltyScaling>,
pub global: Option<bool>,
pub penalty_new: Option<u16>,
pub penalty_zero: Option<u16>,
pub penalty_global: Option<u16>,
pub overlap_x: Option<usize>,
pub overlap_y: Option<usize>,
pub divide_extra: Option<DivideMode>,
pub bad_sad: Option<u64>,
pub bad_range: Option<i32>,
pub meander: bool,
pub try_many: bool,
pub fields: bool,
pub tff: Option<bool>,
pub search_type_coarse: Option<SearchType>,
pub dct_mode: Option<DctMode>,
}
impl Default for AnalyseOptions {
#[inline]
fn default() -> Self {
Self {
blk_size_x: None,
blk_size_y: None,
levels: None,
search_type: None,
search_param: None,
pel_search: None,
is_backward: false,
lambda: None,
chroma: None,
delta_frame: 1,
truemotion: true,
lambda_sad: None,
penalty_level: None,
global: None,
penalty_new: None,
penalty_zero: None,
penalty_global: None,
overlap_x: None,
overlap_y: None,
divide_extra: None,
bad_sad: None,
bad_range: None,
meander: true,
try_many: false,
fields: false,
tff: None,
search_type_coarse: None,
dct_mode: None,
}
}
}
#[derive(Debug, Clone)]
pub struct Analyse {
search_type: SearchType,
search_type_coarse: SearchType,
search_param: i32,
pel_search: i32,
lambda: u32,
lambda_sad: u32,
penalty_level: PenaltyScaling,
global: bool,
penalty_new: u16,
penalty_zero: u16,
penalty_global: u16,
dct_mode: DctMode,
divide_extra: DivideMode,
bad_sad: u64,
bad_range: i32,
meander: bool,
try_many: bool,
fields: bool,
tff: Option<bool>,
analysis_data: MVAnalysisData,
analysis_data_divided: Option<MVAnalysisData>,
super_info: SuperClipInfo,
plane_count: usize,
frame_count: usize,
}
#[derive(Debug, Clone)]
pub struct AnalyseResult {
pub analysis_data: MVAnalysisData,
pub vectors: MvsOutput,
}
impl Analyse {
#[inline]
pub fn new(
info: VideoInfo,
super_info: SuperClipInfo,
options: AnalyseOptions,
) -> Result<Self> {
let blk_size_x = options.blk_size_x.unwrap_or(8);
let blk_size_y = options.blk_size_y.unwrap_or(blk_size_x);
let overlap_x = options.overlap_x.unwrap_or(0);
let overlap_y = options.overlap_y.unwrap_or(overlap_x);
let truemotion = options.truemotion;
let penalty_new = options
.penalty_new
.unwrap_or(if truemotion { 50 } else { 0 });
let penalty_zero = options.penalty_zero.unwrap_or(penalty_new);
let penalty_global = options.penalty_global.unwrap_or(0);
let dct_mode = options.dct_mode.unwrap_or(DctMode::Spatial);
let search_type = options.search_type.unwrap_or(SearchType::Hex2);
let mut search_param = options.search_param.unwrap_or(2);
let divide_extra = options.divide_extra.unwrap_or(DivideMode::None);
let mut chroma = options.chroma.unwrap_or(true);
let mut lambda = options.lambda.unwrap_or(if truemotion {
(1000 * blk_size_x * blk_size_y / 64) as u32
} else {
0
});
let mut lambda_sad = options
.lambda_sad
.unwrap_or(if truemotion { 1200 } else { 400 });
let mut bad_sad = options.bad_sad.unwrap_or(10_000);
let is_backward = options.is_backward;
let delta_frame = options.delta_frame;
let mut pel_search = options.pel_search.unwrap_or(0);
if dct_mode.uses_satd() && blk_size_x == 16 && blk_size_y == 2 {
bail!("Analyse: dct 5-10 cannot work with 16x2 blocks");
}
match (blk_size_x, blk_size_y) {
(4, 4)
| (8, 4)
| (8, 8)
| (16, 2)
| (16, 8)
| (16, 16)
| (32, 16)
| (32, 32)
| (64, 32)
| (64, 64)
| (128, 64)
| (128, 128) => (),
_ => {
bail!(
"Analyse: the block size must be 4x4, 8x4, 8x8, 16x2, 16x8, 16x16, 32x16, \
32x32, 64x32, 64x64, 128x64, or 128x128."
);
}
}
if penalty_new > 256 {
bail!("Analyse: pnew must be between 0 and 256 (inclusive).");
}
if penalty_zero > 256 {
bail!("Analyse: pzero must be between 0 and 256 (inclusive).");
}
if penalty_global > 256 {
bail!("Analyse: pglobal must be between 0 and 256 (inclusive).");
}
if overlap_x > blk_size_x / 2 || overlap_y > blk_size_y / 2 {
bail!(
"Analyse: overlap must be at most half of blksize, and overlapv must be at most \
half of blksizev"
);
}
if divide_extra != DivideMode::None && (blk_size_x < 8 || blk_size_y < 8) {
bail!("Analyse: blksize and blksizev must be at least 8 when divide=True.");
}
if search_type == SearchType::Nstep {
if search_param < 0 {
search_param = 0;
}
} else if search_param < 1 {
search_param = 1;
}
let format = info.format;
if format.bits_per_sample.get() > 16 {
bail!("Analyse: input clip must be 8-16 bits");
}
if format.sample_type != SampleType::Integer {
bail!("Analyse: input clip must be integer super_format");
}
if ![ColorFamily::Yuv, ColorFamily::Gray].contains(&format.color_family)
|| format.sub_sampling_w > 1
|| format.sub_sampling_h > 1
{
bail!("Analyse: input clip must be GRAY, 420, 422, 440, or 444");
}
if format.color_family == ColorFamily::Gray {
chroma = false;
}
let bits_per_sample = format.bits_per_sample;
let pixel_max = (1u32 << bits_per_sample.get()) - 1;
lambda_sad = (lambda_sad as f32 * pixel_max as f32 / 255.0 + 0.5) as u32;
bad_sad = (bad_sad as f32 * pixel_max as f32 / 255.0 + 0.5) as u64;
lambda = (lambda as f32 * pixel_max as f32 / 255.0 + 0.5) as u32;
lambda_sad = (lambda_sad as usize * (blk_size_x * blk_size_y) / 64) as u32;
bad_sad = bad_sad * (blk_size_x * blk_size_y) as u64 / 64;
let mut motion_flags = MotionFlags::empty();
if is_backward {
motion_flags |= MotionFlags::IS_BACKWARD;
}
if chroma {
motion_flags |= MotionFlags::USE_CHROMA_MOTION;
}
let mode_yuv = if chroma {
MVPlaneSet::YUVPLANES
} else {
MVPlaneSet::YPLANE
};
if !overlap_x.is_multiple_of(1 << format.sub_sampling_w)
|| !overlap_y.is_multiple_of(1 << format.sub_sampling_h)
{
bail!(
"Analyse: the requested overlap is incompatible with the super clip's subsampling."
);
}
if divide_extra != DivideMode::None
&& (!overlap_x.is_multiple_of(2 << format.sub_sampling_w)
|| !overlap_y.is_multiple_of(2 << format.sub_sampling_h))
{
bail!(
"Analyse: overlap and overlapv must be multiples of 2 or 4 when divide=True, \
depending on the super clip's subsampling."
);
}
if delta_frame <= 0 && (-delta_frame) >= info.num_frames as isize {
bail!("Analyse: delta points to frame past the input clip's end.");
}
let width = NonZeroUsize::new(info.resolution.width)
.ok_or_else(|| anyhow!("Analyse: variable resolution input clips are not supported"))?;
let x_ratio_uv = NonZeroU8::new(1 << format.sub_sampling_w)
.ok_or_else(|| anyhow!("Analyse: invalid horizontal subsampling"))?;
let y_ratio_uv = NonZeroU8::new(1 << format.sub_sampling_h)
.ok_or_else(|| anyhow!("Analyse: invalid vertical subsampling"))?;
let super_height = super_info.height;
let super_hpad = super_info.hpad;
let super_vpad = super_info.vpad;
let super_pel = super_info.pel;
let super_mode_yuv = super_info.mode_yuv;
let super_levels = super_info.levels;
if super_hpad >= super_height.get() / 2 {
bail!("Analyse: parameters from super clip appear to be wrong.");
}
if mode_yuv & super_mode_yuv != mode_yuv {
bail!("Analyse: super clip does not contain needed colour data.");
}
let super_width =
NonZeroUsize::new(width.get().checked_sub(super_hpad * 2).ok_or_else(|| {
anyhow!("Analyse: parameters from super clip appear to be wrong.")
})?)
.ok_or_else(|| anyhow!("Analyse: parameters from super clip appear to be wrong."))?;
let blk_x = (super_width.get() - overlap_x) / (blk_size_x - overlap_x);
let blk_y = (super_height.get() - overlap_y) / (blk_size_y - overlap_y);
let width_b = (blk_size_x - overlap_x) * blk_x + overlap_x;
let height_b = (blk_size_y - overlap_y) * blk_y + overlap_y;
let mut levels_max = 0;
while ((width_b >> levels_max) - overlap_x) / (blk_size_x - overlap_x) > 0
&& ((height_b >> levels_max) - overlap_y) / (blk_size_y - overlap_y) > 0
{
levels_max += 1;
}
let level_count = options
.levels
.filter(|levels| *levels > 0)
.map_or(levels_max, |levels| min(levels_max, levels));
debug_assert!(level_count > 0);
if level_count > super_levels {
bail!(
"Analyse: super clip has {} levels. Analyse needs {} levels.",
super_levels,
level_count
);
}
if pel_search == 0 {
pel_search = usize::from(u8::from(super_pel));
}
let analysis_data = MVAnalysisData {
blk_size_x: NonZeroUsize::new(blk_size_x)
.ok_or_else(|| anyhow!("Analyse: blksize must be greater than 0"))?,
blk_size_y: NonZeroUsize::new(blk_size_y)
.ok_or_else(|| anyhow!("Analyse: blksizev must be greater than 0"))?,
pel: super_pel,
level_count,
delta_frame,
is_backward,
motion_flags,
width: super_width,
height: super_height,
overlap_x,
overlap_y,
blk_x: NonZeroUsize::new(blk_x).expect("block count should not be zero"),
blk_y: NonZeroUsize::new(blk_y).expect("block count should not be zero"),
bits_per_sample,
y_ratio_uv,
x_ratio_uv,
h_padding: super_hpad,
v_padding: super_vpad,
};
let analysis_data_divided = (divide_extra != DivideMode::None).then(|| {
let mut div_data = analysis_data;
div_data.blk_x = div_data
.blk_x
.saturating_mul(NonZeroUsize::new(2).expect("constant is non-zero"));
div_data.blk_y = div_data
.blk_y
.saturating_mul(NonZeroUsize::new(2).expect("constant is non-zero"));
div_data.blk_size_x = NonZeroUsize::new(div_data.blk_size_x.get() / 2)
.expect("block width stays non-zero");
div_data.blk_size_y = NonZeroUsize::new(div_data.blk_size_y.get() / 2)
.expect("block height stays non-zero");
div_data.overlap_x /= 2;
div_data.overlap_y /= 2;
div_data.level_count += 1;
div_data
});
Ok(Self {
search_type,
search_type_coarse: options.search_type_coarse.unwrap_or(SearchType::Exhaustive),
search_param,
pel_search: pel_search as i32,
lambda,
lambda_sad,
penalty_level: options.penalty_level.unwrap_or(if truemotion {
PenaltyScaling::Linear
} else {
PenaltyScaling::None
}),
global: options.global.unwrap_or(truemotion),
penalty_new,
penalty_zero,
penalty_global,
dct_mode,
divide_extra,
bad_sad,
bad_range: options.bad_range.unwrap_or(24),
meander: options.meander,
try_many: options.try_many,
fields: options.fields,
tff: options.tff,
analysis_data,
analysis_data_divided,
super_info,
plane_count: format.plane_count(),
frame_count: info.num_frames,
})
}
#[must_use]
#[inline]
pub const fn analysis_data(&self) -> MVAnalysisData {
self.analysis_data
}
#[must_use]
#[inline]
pub const fn analysis_data_divided(&self) -> Option<MVAnalysisData> {
self.analysis_data_divided
}
#[must_use]
#[inline]
pub const fn super_info(&self) -> SuperClipInfo {
self.super_info
}
#[must_use]
#[inline]
pub const fn plane_count(&self) -> usize {
self.plane_count
}
#[must_use]
#[inline]
pub const fn frame_count(&self) -> usize {
self.frame_count
}
#[must_use]
#[inline]
pub const fn search_type(&self) -> SearchType {
self.search_type
}
#[must_use]
#[inline]
pub const fn search_type_coarse(&self) -> SearchType {
self.search_type_coarse
}
#[must_use]
#[inline]
pub const fn search_param(&self) -> i32 {
self.search_param
}
#[must_use]
#[inline]
pub const fn pel_search(&self) -> i32 {
self.pel_search
}
#[must_use]
#[inline]
pub const fn lambda(&self) -> u32 {
self.lambda
}
#[must_use]
#[inline]
pub const fn lambda_sad(&self) -> u32 {
self.lambda_sad
}
#[must_use]
#[inline]
pub const fn penalty_level(&self) -> PenaltyScaling {
self.penalty_level
}
#[must_use]
#[inline]
pub const fn global(&self) -> bool {
self.global
}
#[must_use]
#[inline]
pub const fn penalty_new(&self) -> u16 {
self.penalty_new
}
#[must_use]
#[inline]
pub const fn penalty_zero(&self) -> u16 {
self.penalty_zero
}
#[must_use]
#[inline]
pub const fn penalty_global(&self) -> u16 {
self.penalty_global
}
#[must_use]
#[inline]
pub const fn dct_mode(&self) -> DctMode {
self.dct_mode
}
#[must_use]
#[inline]
pub const fn divide_extra(&self) -> DivideMode {
self.divide_extra
}
#[must_use]
#[inline]
pub const fn bad_sad(&self) -> u64 {
self.bad_sad
}
#[must_use]
#[inline]
pub const fn bad_range(&self) -> i32 {
self.bad_range
}
#[must_use]
#[inline]
pub const fn meander(&self) -> bool {
self.meander
}
#[must_use]
#[inline]
pub const fn try_many(&self) -> bool {
self.try_many
}
#[must_use]
#[inline]
pub const fn fields(&self) -> bool {
self.fields
}
#[must_use]
#[inline]
pub const fn tff(&self) -> Option<bool> {
self.tff
}
#[must_use]
#[inline]
pub fn reference_frame_number(&self, n: usize) -> Option<usize> {
let nref = if self.analysis_data.delta_frame > 0 {
let offset = if self.analysis_data.is_backward {
self.analysis_data.delta_frame
} else {
-self.analysis_data.delta_frame
};
n as isize + offset
} else {
-self.analysis_data.delta_frame
};
(nref >= 0 && (nref as usize) < self.frame_count).then_some(nref as usize)
}
#[inline]
pub fn analyse_frame<T: Pixel>(
&self,
n: usize,
src: &FrameView<'_, T>,
reference: Option<&FrameView<'_, T>>,
src_top_field: Option<bool>,
ref_top_field: Option<bool>,
) -> Result<AnalyseResult> {
let mut vector_fields = GroupOfPlanes::<T>::new(
self.analysis_data.blk_size_x,
self.analysis_data.blk_size_y,
self.analysis_data.level_count,
self.analysis_data.pel,
self.analysis_data.motion_flags,
self.analysis_data.overlap_x,
self.analysis_data.overlap_y,
self.analysis_data.blk_x,
self.analysis_data.blk_y,
self.analysis_data.x_ratio_uv,
self.analysis_data.y_ratio_uv,
self.divide_extra,
self.analysis_data.bits_per_sample,
)?;
let vectors = if let Some(reference) = reference {
let src_top_field = self.resolve_top_field(n, src_top_field, "source")?;
let ref_top_field = self.resolve_top_field(n, ref_top_field, "reference")?;
let field_shift = self.field_shift(src_top_field, ref_top_field);
let src_gof = MVGroupOfFrames::new(
self.super_info.levels,
self.analysis_data.width,
self.analysis_data.height,
self.super_info.pel,
self.super_info.hpad,
self.super_info.vpad,
self.super_info.mode_yuv,
self.analysis_data.x_ratio_uv,
self.analysis_data.y_ratio_uv,
self.analysis_data.bits_per_sample,
src.pitch(),
self.plane_count,
)?;
let ref_gof = MVGroupOfFrames::new(
self.super_info.levels,
self.analysis_data.width,
self.analysis_data.height,
self.super_info.pel,
self.super_info.hpad,
self.super_info.vpad,
self.super_info.mode_yuv,
self.analysis_data.x_ratio_uv,
self.analysis_data.y_ratio_uv,
self.analysis_data.bits_per_sample,
reference.pitch(),
self.plane_count,
)?;
let mut vectors = vector_fields.search_mvs(
&src_gof,
src.planes(),
&ref_gof,
reference.planes(),
self.search_type,
self.search_type_coarse,
self.search_param,
self.pel_search,
self.lambda,
self.lambda_sad,
self.penalty_new,
self.penalty_level,
self.global,
field_shift,
self.dct_mode,
self.penalty_zero,
self.penalty_global,
self.bad_sad,
self.bad_range,
self.meander,
self.try_many,
)?;
if self.divide_extra != DivideMode::None {
vector_fields.extra_divide(&mut vectors);
}
vectors
} else {
vector_fields.write_default_to_array()
};
Ok(AnalyseResult {
analysis_data: self.analysis_data_for_output(),
vectors,
})
}
#[must_use]
#[inline]
fn analysis_data_for_output(&self) -> MVAnalysisData {
self.analysis_data_divided.unwrap_or(self.analysis_data)
}
#[inline]
fn resolve_top_field(&self, n: usize, field: Option<bool>, frame_name: &str) -> Result<bool> {
if let Some(tff) = self.tff {
return Ok((tff as u8 ^ (n % 2) as u8) > 0);
}
if self.fields {
field.ok_or_else(|| {
anyhow!(
"Analyse: {frame_name} field order is required when fields=true and tff is not set."
)
})
} else {
Ok(field.unwrap_or(false))
}
}
#[must_use]
#[inline]
fn field_shift(&self, src_top_field: bool, ref_top_field: bool) -> i32 {
if self.fields
&& self.analysis_data.pel > Subpel::Full
&& (self.analysis_data.delta_frame % 2) > 0
{
if src_top_field && !ref_top_field {
(u8::from(self.analysis_data.pel) as u32 / 2) as i32
} else if ref_top_field && !src_top_field {
-((u8::from(self.analysis_data.pel) as u32 / 2) as i32)
} else {
0
}
} else {
0
}
}
}