use crate::nlmeans::{ChannelMode, HqParams, MotionCompensationMode, MotionEstimation, NlmParams};
pub const MAX_MISMATCH_SCALE: f32 = 16.0;
#[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 confidence_variance: bool,
}
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.3,
c_min: 0.05,
mismatch_scale: 1.0,
confidence_variance: true,
}
}
}
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 !(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
));
}
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_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");
}
}
}