use crate::nlmeans::{ChannelMode, HqParams, MotionCompensationMode, MotionEstimation, NlmParams};
pub const MAX_MISMATCH_SCALE: f32 = 16.0;
pub const MAX_KAISER_BETA: f32 = 8.0;
pub const MAX_COVERING_BLOCKS: u32 = 4;
#[derive(Debug, Clone)]
pub struct Nl4dParams {
pub nlm: NlmParams,
pub temporal_radius: u32,
pub refine: u32,
pub spatial_radius: u32,
pub lambda_ht: f32,
pub c_min: f32,
pub mismatch_scale: f32,
pub kaiser_beta: f32,
pub confidence_variance: bool,
pub field_lambda: f32,
}
impl Default for Nl4dParams {
fn default() -> Self {
Self {
nlm: NlmParams {
temporal_radius: 2,
channels: ChannelMode::Yuv,
motion_compensation: MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 8,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
},
hq: Some(HqParams::default()),
..NlmParams::default()
},
temporal_radius: 2,
refine: 2,
spatial_radius: 9,
lambda_ht: 5.2,
c_min: 0.05,
mismatch_scale: 1.0,
kaiser_beta: 2.0,
confidence_variance: true,
field_lambda: 1.0,
}
}
}
impl Nl4dParams {
pub fn validate(&self) -> Result<(), String> {
let Some(hq) = self.nlm.hq else {
return Err(
"nlm.hq must be Some, the front end's noise estimate and confidence weighting \
are what submit_machinery builds the ring view from"
.to_string(),
);
};
if !self.nlm.motion_compensation.is_active() {
return Err(
"nlm.motion_compensation must be active, the temporal grouping kernel reads \
the motion field submit_machinery builds from it"
.to_string(),
);
}
if !hq.temporal_confidence {
return Err(
"nlm.hq.temporal_confidence must be true, submit_machinery returns an error \
unless both motion compensation and the confidence buffer are active"
.to_string(),
);
}
if let MotionCompensationMode::Mvtools { blksize, overlap, .. } = self.nlm.motion_compensation
&& overlap < blksize
{
let step = blksize - overlap;
let covers = blksize.div_ceil(step);
if covers > MAX_COVERING_BLOCKS {
return Err(format!(
"nlm.motion_compensation blksize={blksize} at overlap={overlap} gives a step \
of {step}, so {covers} blocks cover a patch on each axis, past the \
{MAX_COVERING_BLOCKS} the temporal grouping kernel unrolls its search over. \
Raise the step by lowering the overlap."
));
}
}
if !(1..=crate::collab::MAX_TEMPORAL_RADIUS).contains(&self.temporal_radius) {
return Err(format!(
"temporal_radius={} must be in 1..={}",
self.temporal_radius,
crate::collab::MAX_TEMPORAL_RADIUS,
));
}
if !(1..=4).contains(&self.refine) {
return Err(format!("refine={} must be in 1..=4", self.refine));
}
if !(1..=16).contains(&self.spatial_radius) {
return Err(format!(
"spatial_radius={} must be in 1..=16",
self.spatial_radius
));
}
if !(self.lambda_ht.is_finite() && self.lambda_ht > 0.0) {
return Err(format!(
"lambda_ht must be finite and greater than 0, got {}",
self.lambda_ht
));
}
if !(self.c_min.is_finite() && self.c_min >= 0.0 && self.c_min < 1.0) {
return Err(format!("c_min must be finite and in [0, 1), got {}", self.c_min));
}
if !(self.mismatch_scale.is_finite() && (0.0..=MAX_MISMATCH_SCALE).contains(&self.mismatch_scale)) {
return Err(format!(
"mismatch_scale must be finite and in [0, {MAX_MISMATCH_SCALE}], got {}",
self.mismatch_scale
));
}
if !(self.kaiser_beta.is_finite() && (0.0..=MAX_KAISER_BETA).contains(&self.kaiser_beta)) {
return Err(format!(
"kaiser_beta must be finite and in 0..={MAX_KAISER_BETA}, got {}",
self.kaiser_beta
));
}
if !(self.field_lambda.is_finite() && self.field_lambda >= 0.0) {
return Err(format!(
"field_lambda must be finite and at least 0, got {}",
self.field_lambda
));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn validate_accepts_default() {
assert!(Nl4dParams::default().validate().is_ok());
}
#[test]
fn validate_accepts_the_whole_mismatch_scale_range() {
for scale in [0.0, 1.0, 8.0, MAX_MISMATCH_SCALE] {
let params = Nl4dParams {
mismatch_scale: scale,
..Nl4dParams::default()
};
assert!(
params.validate().is_ok(),
"mismatch_scale={scale} should be accepted"
);
}
}
#[test]
fn validate_rejects_a_mismatch_scale_past_saturation_or_below_zero() {
for scale in [-1.0, MAX_MISMATCH_SCALE + 0.1, f32::NAN, f32::INFINITY] {
let params = Nl4dParams {
mismatch_scale: scale,
..Nl4dParams::default()
};
let err = params
.validate()
.expect_err("mismatch_scale={scale} should be rejected");
assert!(
err.contains("mismatch_scale"),
"error should name mismatch_scale, got {err}"
);
}
}
#[test]
fn validate_accepts_block_geometries_up_to_the_covering_bound() {
for (blksize, overlap, covers) in [(16u32, 8u32, 2u32), (16, 12, 4), (32, 24, 4), (8, 4, 2)] {
let params = Nl4dParams {
nlm: NlmParams {
motion_compensation: MotionCompensationMode::Mvtools {
blksize,
overlap,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
},
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
assert!(
params.validate().is_ok(),
"blksize={blksize} overlap={overlap} covers {covers} blocks and should be accepted"
);
}
}
#[test]
fn validate_rejects_a_block_geometry_past_the_covering_bound() {
for (blksize, overlap) in [(16u32, 13u32), (16, 14), (32, 31), (32, 25)] {
let params = Nl4dParams {
nlm: NlmParams {
motion_compensation: MotionCompensationMode::Mvtools {
blksize,
overlap,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
},
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
let err = params
.validate()
.expect_err("a step this small should be rejected");
assert!(
err.contains(&format!("blksize={blksize}")) && err.contains(&format!("overlap={overlap}")),
"error should name the offending blksize and overlap, got {err}"
);
}
}
#[test]
fn overlap_equal_to_blksize_reports_the_overlap_constraint_not_covering_blocks() {
let params = Nl4dParams {
nlm: NlmParams {
motion_compensation: MotionCompensationMode::Mvtools {
blksize: 16,
overlap: 16,
search_radius: 4,
pyramid_levels: 2,
estimation: MotionEstimation::Auto,
},
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
assert!(
params.validate().is_ok(),
"the covering-block check must not fire on a geometry nlm.validate() rejects on its \
own terms"
);
let err = params
.nlm
.validate()
.expect_err("overlap == blksize must be rejected")
.to_string();
assert!(
err.contains("overlap") && err.contains("blksize"),
"error should name the overlap constraint, got {err}"
);
assert!(
!err.contains("cover a patch"),
"error should not be the covering-block message, got {err}"
);
}
#[test]
fn validate_rejects_missing_hq() {
let params = Nl4dParams {
nlm: NlmParams {
hq: None,
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
let err = params.validate().expect_err("expected rejection");
assert!(err.contains("nlm.hq"), "error should name nlm.hq, got {err}");
}
#[test]
fn validate_rejects_inactive_motion_compensation() {
let params = Nl4dParams {
nlm: NlmParams {
motion_compensation: MotionCompensationMode::None,
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
let err = params.validate().expect_err("expected rejection");
assert!(
err.contains("motion_compensation"),
"error should name nlm.motion_compensation, got {err}"
);
}
#[test]
fn validate_rejects_missing_temporal_confidence() {
let params = Nl4dParams {
nlm: NlmParams {
hq: Some(HqParams {
temporal_confidence: false,
..HqParams::default()
}),
..Nl4dParams::default().nlm
},
..Nl4dParams::default()
};
let err = params.validate().expect_err("expected rejection");
assert!(
err.contains("temporal_confidence"),
"error should name nlm.hq.temporal_confidence, got {err}"
);
}
#[test]
fn validate_rejects_temporal_radius_out_of_range() {
for bad in [0u32, 9] {
let params = Nl4dParams {
temporal_radius: bad,
..Nl4dParams::default()
};
assert!(
params.validate().is_err(),
"temporal_radius={bad} should be rejected"
);
}
}
#[test]
fn validate_rejects_refine_out_of_range() {
for bad in [0u32, 5] {
let params = Nl4dParams {
refine: bad,
..Nl4dParams::default()
};
assert!(params.validate().is_err(), "refine={bad} should be rejected");
}
}
#[test]
fn validate_rejects_spatial_radius_out_of_range() {
for bad in [0u32, 17] {
let params = Nl4dParams {
spatial_radius: bad,
..Nl4dParams::default()
};
assert!(
params.validate().is_err(),
"spatial_radius={bad} should be rejected"
);
}
}
#[test]
fn validate_rejects_non_positive_lambda_ht() {
for bad in [0.0f32, -1.0, f32::NAN, f32::INFINITY] {
let params = Nl4dParams {
lambda_ht: bad,
..Nl4dParams::default()
};
assert!(params.validate().is_err(), "lambda_ht={bad} should be rejected");
}
}
#[test]
fn validate_rejects_c_min_out_of_range() {
for bad in [-0.1f32, 1.0, f32::NAN] {
let params = Nl4dParams {
c_min: bad,
..Nl4dParams::default()
};
assert!(params.validate().is_err(), "c_min={bad} should be rejected");
}
}
#[test]
fn validate_accepts_zero_and_positive_field_lambda() {
for lambda in [0.0, 0.5, 4.0] {
let params = Nl4dParams {
field_lambda: lambda,
..Nl4dParams::default()
};
assert!(
params.validate().is_ok(),
"field_lambda={lambda} should be accepted"
);
}
}
#[test]
fn validate_rejects_negative_or_non_finite_field_lambda() {
for lambda in [-0.1, f32::NAN, f32::INFINITY] {
let params = Nl4dParams {
field_lambda: lambda,
..Nl4dParams::default()
};
let err = params
.validate()
.expect_err("field_lambda={lambda} should be rejected");
assert!(
err.contains("field_lambda"),
"error should name field_lambda, got {err}"
);
}
}
}