use super::helpers::*;
use crate::nlmeans::*;
fn lag1_horizontal(field: &[f32], w: u32, h: u32) -> f64 {
let mut sum_a = 0.0f64;
let mut sum_b = 0.0f64;
let mut sum_ab = 0.0f64;
let mut sum_aa = 0.0f64;
let mut sum_bb = 0.0f64;
let mut n = 0.0f64;
for y in 0..h {
for x in 0..(w - 1) {
let a = field[(y * w + x) as usize] as f64;
let b = field[(y * w + x + 1) as usize] as f64;
sum_a += a;
sum_b += b;
sum_ab += a * b;
sum_aa += a * a;
sum_bb += b * b;
n += 1.0;
}
}
let mean_a = sum_a / n;
let mean_b = sum_b / n;
let cov = sum_ab / n - mean_a * mean_b;
let var_a = sum_aa / n - mean_a * mean_a;
let var_b = sum_bb / n - mean_b * mean_b;
cov / (var_a.sqrt() * var_b.sqrt())
}
fn lag1_vertical(field: &[f32], w: u32, h: u32) -> f64 {
let mut sum_a = 0.0f64;
let mut sum_b = 0.0f64;
let mut sum_ab = 0.0f64;
let mut sum_aa = 0.0f64;
let mut sum_bb = 0.0f64;
let mut n = 0.0f64;
for y in 0..(h - 1) {
for x in 0..w {
let a = field[(y * w + x) as usize] as f64;
let b = field[((y + 1) * w + x) as usize] as f64;
sum_a += a;
sum_b += b;
sum_ab += a * b;
sum_aa += a * a;
sum_bb += b * b;
n += 1.0;
}
}
let mean_a = sum_a / n;
let mean_b = sum_b / n;
let cov = sum_ab / n - mean_a * mean_b;
let var_a = sum_aa / n - mean_a * mean_a;
let var_b = sum_bb / n - mean_b * mean_b;
cov / (var_a.sqrt() * var_b.sqrt())
}
fn tap_sigma(sigma_pre: f32, a: f32) -> f32 {
let b = 1.0 - 2.0 * a;
sigma_pre * (2.0 * a * a + b * b).sqrt()
}
fn std_dev(field: &[f32]) -> f64 {
let n = field.len() as f64;
let mean: f64 = field.iter().map(|&v| v as f64).sum::<f64>() / n;
let var: f64 = field.iter().map(|&v| (v as f64 - mean).powi(2)).sum::<f64>() / n;
var.sqrt()
}
struct Measurement {
rho_out_h: f64,
rho_out_v: f64,
sigma_ratio: f64,
}
#[expect(
clippy::too_many_arguments,
reason = "the test helper takes the full set of parameters its cases vary"
)]
fn measure(
client: &cubecl::prelude::ComputeClient<R>,
w: u32,
h: u32,
base: f32,
sigma: f32,
make_noise: impl Fn(u32) -> Vec<f32>,
search_radius: u32,
temporal_radius: u32,
) -> Measurement {
let params = NlmParams {
temporal_radius,
search_radius,
patch_radius: 2,
strength: 1.2,
self_weight: 1.0,
channels: ChannelMode::Luma,
prefilter: PrefilterMode::None,
motion_compensation: MotionCompensationMode::None,
hq: Some(HqParams::with_sigma(sigma)),
};
let mut denoiser = NlmDenoiser::<R>::new(client, params, w, h);
let n_push = 2 * temporal_radius + 1;
let mut center_noisy: Option<Vec<f32>> = None;
let mut output: Option<Vec<f32>> = None;
for i in 0..n_push {
let frame = make_noise(100 + i);
if i == temporal_radius {
center_noisy = Some(frame.clone());
}
denoiser.push_frame(&frame);
let result = denoiser.denoise().unwrap();
if i == n_push - 1 {
output = result.map(|o| o.as_f32().expect("f32 denoiser").to_vec());
}
}
let output = output.expect("a fully real window must emit on its final push");
let center_noisy = center_noisy.expect("center frame must have been pushed");
let clean = vec![base; (w * h) as usize];
let residual: Vec<f32> = output.iter().zip(clean.iter()).map(|(&o, &c)| o - c).collect();
let input_noise: Vec<f32> = center_noisy
.iter()
.zip(clean.iter())
.map(|(&o, &c)| o - c)
.collect();
Measurement {
rho_out_h: lag1_horizontal(&residual, w, h),
rho_out_v: lag1_vertical(&residual, w, h),
sigma_ratio: std_dev(&residual) / std_dev(&input_noise),
}
}
#[test]
fn nlm_residual_correlation_exceeds_input_correlation_and_tracks_window_overlap() {
let client = make_client();
let w = 160;
let h = 160;
let base = 0.5f32;
let sigma_pre = 0.06f32;
struct Case {
label: &'static str,
rho_in_h: f64,
rho_in_v: f64,
}
let cases = [
Case {
label: "rho_in=0.00",
rho_in_h: 0.0,
rho_in_v: 0.0,
},
Case {
label: "rho_in=0.32",
rho_in_h: 0.316,
rho_in_v: 0.0,
},
Case {
label: "rho_in=0.67",
rho_in_h: 2.0 / 3.0,
rho_in_v: 0.0,
},
];
let configs = [(2u32, 0u32), (2u32, 2u32), (0u32, 0u32), (0u32, 2u32)];
eprintln!(
"residual correlation sweep (w={w} h={h} sigma_pre={sigma_pre}):\n\
{:<12} {:>6} {:>6} | {:>6} {:>6} | {:>8} {:>8} | {:>8}",
"input", "R_s", "R_t", "rho_in_h", "rho_in_v", "rho_out_h", "rho_out_v", "sig_ratio"
);
struct Row {
label: &'static str,
search_radius: u32,
temporal_radius: u32,
rho_in_h: f64,
m: Measurement,
}
let mut rows = Vec::new();
for case in &cases {
for &(search_radius, temporal_radius) in &configs {
let m = match case.label {
"rho_in=0.00" => {
let clean = vec![base; (w * h) as usize];
measure(
&client,
w,
h,
base,
tap_sigma(sigma_pre, 0.0),
|seed| noisy_field_over(&clean, w, h, sigma_pre, seed),
search_radius,
temporal_radius,
)
},
"rho_in=0.32" => measure(
&client,
w,
h,
base,
tap_sigma(sigma_pre, 0.125),
|seed| correlated_noisy_frame_with_tap(w, h, base, sigma_pre, seed, 0.125),
search_radius,
temporal_radius,
),
_ => measure(
&client,
w,
h,
base,
tap_sigma(sigma_pre, 0.25),
|seed| correlated_noisy_frame(w, h, base, sigma_pre, seed),
search_radius,
temporal_radius,
),
};
eprintln!(
"{:<12} {:>6} {:>6} | {:>8.4} {:>8.4} | {:>8.4} {:>8.4} | {:>8.4}",
case.label,
search_radius,
temporal_radius,
case.rho_in_h,
case.rho_in_v,
m.rho_out_h,
m.rho_out_v,
m.sigma_ratio
);
rows.push(Row {
label: case.label,
search_radius,
temporal_radius,
rho_in_h: case.rho_in_h,
m,
});
}
}
for row in &rows {
if row.search_radius == 2 {
assert!(
row.m.rho_out_h > row.rho_in_h,
"{} at search_radius=2 temporal_radius={}: residual rho_h={:.4} did not exceed \
input rho_h={:.4}, expected window overlap to raise it",
row.label,
row.temporal_radius,
row.m.rho_out_h,
row.rho_in_h
);
}
}
for row in &rows {
if row.search_radius == 0 {
assert!(
(row.m.rho_out_h - row.rho_in_h).abs() < 0.08,
"{} at search_radius=0 temporal_radius={}: residual rho_h={:.4} should track the \
input's rho_h={:.4} within 0.08 once spatial window overlap is removed",
row.label,
row.temporal_radius,
row.m.rho_out_h,
row.rho_in_h
);
}
}
for row in &rows {
if row.search_radius == 0 {
assert!(
row.m.rho_out_v.abs() < 0.05,
"{} at search_radius=0 temporal_radius={}: residual rho_v={:.4} should stay near \
zero, the input never carried vertical correlation and there is no spatial \
window to manufacture it",
row.label,
row.temporal_radius,
row.m.rho_out_v
);
}
}
for row in &rows {
if row.search_radius == 2 {
assert!(
row.m.rho_out_v > 0.3,
"{} at search_radius=2 temporal_radius={}: residual rho_v={:.4} did not rise \
well above the input's zero vertical correlation, expected the isotropic \
window to induce substantial vertical correlation regardless",
row.label,
row.temporal_radius,
row.m.rho_out_v
);
}
}
}
fn lag1_horizontal_rect(field: &[f32], w: u32, x0: u32, y0: u32, x1: u32, y1: u32) -> f64 {
let mut sum_a = 0.0f64;
let mut sum_b = 0.0f64;
let mut sum_ab = 0.0f64;
let mut sum_aa = 0.0f64;
let mut sum_bb = 0.0f64;
let mut n = 0.0f64;
for y in y0..y1 {
for x in x0..(x1 - 1) {
let a = field[(y * w + x) as usize] as f64;
let b = field[(y * w + x + 1) as usize] as f64;
sum_a += a;
sum_b += b;
sum_ab += a * b;
sum_aa += a * a;
sum_bb += b * b;
n += 1.0;
}
}
let mean_a = sum_a / n;
let mean_b = sum_b / n;
let cov = sum_ab / n - mean_a * mean_b;
let var_a = sum_aa / n - mean_a * mean_a;
let var_b = sum_bb / n - mean_b * mean_b;
cov / (var_a.sqrt() * var_b.sqrt())
}
fn lag1_vertical_rect(field: &[f32], w: u32, x0: u32, y0: u32, x1: u32, y1: u32) -> f64 {
let mut sum_a = 0.0f64;
let mut sum_b = 0.0f64;
let mut sum_ab = 0.0f64;
let mut sum_aa = 0.0f64;
let mut sum_bb = 0.0f64;
let mut n = 0.0f64;
for y in y0..(y1 - 1) {
for x in x0..x1 {
let a = field[(y * w + x) as usize] as f64;
let b = field[((y + 1) * w + x) as usize] as f64;
sum_a += a;
sum_b += b;
sum_ab += a * b;
sum_aa += a * a;
sum_bb += b * b;
n += 1.0;
}
}
let mean_a = sum_a / n;
let mean_b = sum_b / n;
let cov = sum_ab / n - mean_a * mean_b;
let var_a = sum_aa / n - mean_a * mean_a;
let var_b = sum_bb / n - mean_b * mean_b;
cov / (var_a.sqrt() * var_b.sqrt())
}
fn std_dev_rect(field: &[f32], w: u32, x0: u32, y0: u32, x1: u32, y1: u32) -> f64 {
let n = ((x1 - x0) * (y1 - y0)) as f64;
let mut sum = 0.0f64;
for y in y0..y1 {
for x in x0..x1 {
sum += field[(y * w + x) as usize] as f64;
}
}
let mean = sum / n;
let mut var = 0.0f64;
for y in y0..y1 {
for x in x0..x1 {
let v = field[(y * w + x) as usize] as f64;
var += (v - mean).powi(2);
}
}
(var / n).sqrt()
}
#[expect(
clippy::too_many_arguments,
reason = "the test helper takes the full set of parameters its cases vary"
)]
fn run_front_end(
client: &cubecl::prelude::ComputeClient<R>,
w: u32,
h: u32,
clean: &[f32],
sigma: f32,
make_noise: impl Fn(&[f32], u32) -> Vec<f32>,
search_radius: u32,
temporal_radius: u32,
patch_radius: u32,
seed_base: u32,
) -> (Vec<f32>, Vec<f32>) {
let params = NlmParams {
temporal_radius,
search_radius,
patch_radius,
strength: 1.2,
self_weight: 1.0,
channels: ChannelMode::Luma,
prefilter: PrefilterMode::None,
motion_compensation: MotionCompensationMode::None,
hq: Some(HqParams::with_sigma(sigma)),
};
let mut denoiser = NlmDenoiser::<R>::new(client, params, w, h);
let n_push = 2 * temporal_radius + 1;
let mut center_noisy: Option<Vec<f32>> = None;
let mut output: Option<Vec<f32>> = None;
for i in 0..n_push {
let frame = make_noise(clean, seed_base + i);
if i == temporal_radius {
center_noisy = Some(frame.clone());
}
denoiser.push_frame(&frame);
let result = denoiser.denoise().unwrap();
if i == n_push - 1 {
output = result.map(|o| o.as_f32().expect("f32 denoiser").to_vec());
}
}
(
output.expect("a fully real window must emit on its final push"),
center_noisy.expect("center frame must have been pushed"),
)
}
struct Sample {
rho_out_h: f64,
rho_out_v: f64,
sigma_ratio: f64,
}
#[expect(
clippy::too_many_arguments,
reason = "the test helper takes the full set of parameters its cases vary"
)]
fn measure_flat(
client: &cubecl::prelude::ComputeClient<R>,
w: u32,
h: u32,
clean: &[f32],
sigma: f32,
make_noise: impl Fn(&[f32], u32) -> Vec<f32>,
search_radius: u32,
temporal_radius: u32,
patch_radius: u32,
) -> Sample {
let (output, center_noisy) = run_front_end(
client,
w,
h,
clean,
sigma,
make_noise,
search_radius,
temporal_radius,
patch_radius,
100,
);
let residual: Vec<f32> = output.iter().zip(clean.iter()).map(|(&o, &c)| o - c).collect();
let input_noise: Vec<f32> = center_noisy
.iter()
.zip(clean.iter())
.map(|(&o, &c)| o - c)
.collect();
Sample {
rho_out_h: lag1_horizontal(&residual, w, h),
rho_out_v: lag1_vertical(&residual, w, h),
sigma_ratio: std_dev(&residual) / std_dev(&input_noise),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the test helper takes the full set of parameters its cases vary"
)]
fn measure_diff(
client: &cubecl::prelude::ComputeClient<R>,
w: u32,
h: u32,
clean: &[f32],
sigma: f32,
make_noise: impl Fn(&[f32], u32) -> Vec<f32>,
search_radius: u32,
temporal_radius: u32,
patch_radius: u32,
) -> Sample {
let (output_a, noisy_a) = run_front_end(
client,
w,
h,
clean,
sigma,
&make_noise,
search_radius,
temporal_radius,
patch_radius,
100,
);
let (output_b, _noisy_b) = run_front_end(
client,
w,
h,
clean,
sigma,
&make_noise,
search_radius,
temporal_radius,
patch_radius,
500,
);
let diff: Vec<f32> = output_a
.iter()
.zip(output_b.iter())
.map(|(&a, &b)| a - b)
.collect();
let input_noise: Vec<f32> = noisy_a.iter().zip(clean.iter()).map(|(&o, &c)| o - c).collect();
let sqrt2 = 2.0f64.sqrt();
Sample {
rho_out_h: lag1_horizontal(&diff, w, h),
rho_out_v: lag1_vertical(&diff, w, h),
sigma_ratio: (std_dev(&diff) / sqrt2) / std_dev(&input_noise),
}
}
#[expect(
clippy::too_many_arguments,
reason = "the test helper takes the full set of parameters its cases vary"
)]
fn measure_diff_two_regions(
client: &cubecl::prelude::ComputeClient<R>,
w: u32,
h: u32,
clean: &[f32],
sigma: f32,
make_noise: impl Fn(&[f32], u32) -> Vec<f32>,
search_radius: u32,
temporal_radius: u32,
patch_radius: u32,
region_a: (u32, u32, u32, u32),
region_b: (u32, u32, u32, u32),
) -> (Sample, Sample) {
let (output_a, noisy_a) = run_front_end(
client,
w,
h,
clean,
sigma,
&make_noise,
search_radius,
temporal_radius,
patch_radius,
100,
);
let (output_b, _noisy_b) = run_front_end(
client,
w,
h,
clean,
sigma,
&make_noise,
search_radius,
temporal_radius,
patch_radius,
500,
);
let diff: Vec<f32> = output_a
.iter()
.zip(output_b.iter())
.map(|(&a, &b)| a - b)
.collect();
let input_noise: Vec<f32> = noisy_a.iter().zip(clean.iter()).map(|(&o, &c)| o - c).collect();
let sqrt2 = 2.0f64.sqrt();
let sample_for = |(x0, y0, x1, y1): (u32, u32, u32, u32)| Sample {
rho_out_h: lag1_horizontal_rect(&diff, w, x0, y0, x1, y1),
rho_out_v: lag1_vertical_rect(&diff, w, x0, y0, x1, y1),
sigma_ratio: (std_dev_rect(&diff, w, x0, y0, x1, y1) / sqrt2)
/ std_dev_rect(&input_noise, w, x0, y0, x1, y1),
};
(sample_for(region_a), sample_for(region_b))
}
#[test]
fn nlm_residual_correlation_search_radius_sweep_flat_vs_textured() {
let client = make_client();
let w = 160;
let h = 160;
let base = 0.5f32;
let sigma_pre = 0.06f32;
let patch_radius = 2;
let flat_clean = vec![base; (w * h) as usize];
let textured_clean = make_textured_frame(w, h);
struct Row {
search_radius: u32,
flat: Sample,
textured: Sample,
}
let mut rows = Vec::new();
eprintln!(
"search radius sweep, flat vs textured (w={w} h={h} sigma_pre={sigma_pre} \
patch_radius={patch_radius}, uncorrelated input):\n\
{:>3} | {:>8} {:>8} {:>8} | {:>8} {:>8} {:>8}",
"R_s", "flat_h", "flat_v", "flat_sig", "tex_h", "tex_v", "tex_sig"
);
for &search_radius in &[1u32, 2, 3, 4] {
let flat = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
patch_radius,
);
let textured = measure_diff(
&client,
w,
h,
&textured_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
patch_radius,
);
eprintln!(
"{:>3} | {:>8.4} {:>8.4} {:>8.4} | {:>8.4} {:>8.4} {:>8.4}",
search_radius,
flat.rho_out_h,
flat.rho_out_v,
flat.sigma_ratio,
textured.rho_out_h,
textured.rho_out_v,
textured.sigma_ratio
);
rows.push(Row {
search_radius,
flat,
textured,
});
}
for row in &rows {
assert!(
row.flat.sigma_ratio < 0.9,
"search_radius={}: flat sigma_ratio={:.4} too close to 1.0, no real smoothing \
happened, this configuration's correlation number is not valid data",
row.search_radius,
row.flat.sigma_ratio
);
assert!(
row.textured.sigma_ratio < 0.9,
"search_radius={}: textured sigma_ratio={:.4} too close to 1.0, no real smoothing \
happened, this configuration's correlation number is not valid data",
row.search_radius,
row.textured.sigma_ratio
);
}
for row in &rows {
assert!(
row.flat.rho_out_h > 0.3,
"search_radius={}: flat rho_out_h={:.4} did not show substantial window-induced \
correlation",
row.search_radius,
row.flat.rho_out_h
);
}
for row in &rows {
assert!(
(row.flat.rho_out_h - row.textured.rho_out_h).abs() < 0.08,
"search_radius={}: flat rho_out_h={:.4} and textured rho_out_h={:.4} diverge by more \
than the measured tolerance, flat and textured no longer agree",
row.search_radius,
row.flat.rho_out_h,
row.textured.rho_out_h
);
}
for pair in rows.windows(2) {
assert!(
pair[1].flat.rho_out_h >= pair[0].flat.rho_out_h - 1e-6,
"flat rho_out_h dropped from search_radius={} ({:.4}) to search_radius={} ({:.4})",
pair[0].search_radius,
pair[0].flat.rho_out_h,
pair[1].search_radius,
pair[1].flat.rho_out_h
);
}
}
#[test]
fn nlm_residual_correlation_search_radius_sweep_at_shipped_patch_radius() {
let client = make_client();
let w = 160;
let h = 160;
let base = 0.5f32;
let sigma_pre = 0.06f32;
let patch_radius = 4;
let flat_clean = vec![base; (w * h) as usize];
let textured_clean = make_textured_frame(w, h);
struct Row {
search_radius: u32,
flat: Sample,
textured: Sample,
}
let mut rows = Vec::new();
eprintln!(
"search radius sweep at shipped patch_radius={patch_radius}, flat vs textured \
(w={w} h={h} sigma_pre={sigma_pre}, uncorrelated input):\n\
{:>3} | {:>8} {:>8} {:>8} | {:>8} {:>8} {:>8}",
"R_s", "flat_h", "flat_v", "flat_sig", "tex_h", "tex_v", "tex_sig"
);
for &search_radius in &[0u32, 1, 2, 3, 4] {
let flat = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
patch_radius,
);
let textured = measure_diff(
&client,
w,
h,
&textured_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
patch_radius,
);
eprintln!(
"{:>3} | {:>8.4} {:>8.4} {:>8.4} | {:>8.4} {:>8.4} {:>8.4}",
search_radius,
flat.rho_out_h,
flat.rho_out_v,
flat.sigma_ratio,
textured.rho_out_h,
textured.rho_out_v,
textured.sigma_ratio
);
rows.push(Row {
search_radius,
flat,
textured,
});
}
for row in &rows {
if row.search_radius == 0 {
continue;
}
assert!(
row.flat.sigma_ratio < 0.9,
"search_radius={}: flat sigma_ratio={:.4} too close to 1.0, no real smoothing \
happened, this configuration's correlation number is not valid data",
row.search_radius,
row.flat.sigma_ratio
);
assert!(
row.textured.sigma_ratio < 0.9,
"search_radius={}: textured sigma_ratio={:.4} too close to 1.0, no real smoothing \
happened, this configuration's correlation number is not valid data",
row.search_radius,
row.textured.sigma_ratio
);
}
for row in &rows {
if row.search_radius == 0 {
assert!(
row.flat.rho_out_h.abs() < 0.1,
"search_radius=0: flat rho_out_h={:.4} should stay near zero, there is no \
spatial window to manufacture correlation",
row.flat.rho_out_h
);
}
}
for row in &rows {
if row.search_radius >= 1 {
assert!(
row.flat.rho_out_h > 0.3,
"search_radius={}: flat rho_out_h={:.4} did not show substantial window-induced \
correlation",
row.search_radius,
row.flat.rho_out_h
);
}
}
for row in &rows {
if row.search_radius == 0 {
continue;
}
assert!(
row.flat.rho_out_h > 0.6,
"search_radius={}: patch_radius=4 flat rho_out_h={:.4} unexpectedly low, expected it \
to sit above the patch_radius=2 sweep's own values at this radius",
row.search_radius,
row.flat.rho_out_h
);
}
for pair in rows.windows(2) {
assert!(
pair[1].flat.rho_out_h >= pair[0].flat.rho_out_h - 1e-6,
"flat rho_out_h dropped from search_radius={} ({:.4}) to search_radius={} ({:.4})",
pair[0].search_radius,
pair[0].flat.rho_out_h,
pair[1].search_radius,
pair[1].flat.rho_out_h
);
}
}
#[test]
fn nlm_residual_correlation_patch_radius_temporal_radius_and_input_correlation() {
let client = make_client();
let w = 160;
let h = 160;
let base = 0.5f32;
let sigma_pre = 0.06f32;
let flat_clean = vec![base; (w * h) as usize];
let search_radius = 2;
let baseline = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
2,
);
let patch4 = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
4,
);
let temporal2 = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
2,
2,
);
let rho_in_h = 2.0 / 3.0;
let corr_input = measure_flat(
&client,
w,
h,
&flat_clean,
sigma_pre,
|_clean, seed| correlated_noisy_frame(w, h, base, sigma_pre, seed),
search_radius,
0,
2,
);
eprintln!(
"secondary checks at search_radius=2, flat content (w={w} h={h} sigma_pre={sigma_pre}):\n\
{:<32} {:>8} {:>8} {:>8}",
"config", "rho_h", "rho_v", "sig_ratio"
);
for (label, s) in [
("baseline patch_radius=2 R_t=0 rho_in=0", &baseline),
("patch_radius=4", &patch4),
("temporal_radius=2", &temporal2),
("input rho_h=0.67", &corr_input),
] {
eprintln!(
"{:<32} {:>8.4} {:>8.4} {:>8.4}",
label, s.rho_out_h, s.rho_out_v, s.sigma_ratio
);
}
for (label, s) in [
("baseline", &baseline),
("patch_radius=4", &patch4),
("temporal_radius=2", &temporal2),
("input rho_h=0.67", &corr_input),
] {
assert!(
s.sigma_ratio < 0.9,
"{label}: sigma_ratio={:.4} too close to 1.0, no real smoothing happened, this \
configuration's correlation number is not valid data",
s.sigma_ratio
);
}
assert!(
temporal2.rho_out_h < baseline.rho_out_h,
"temporal_radius=2 rho_out_h={:.4} should be below temporal_radius=0's {:.4}, temporal \
averaging is expected to dilute the spatial window's contribution",
temporal2.rho_out_h,
baseline.rho_out_h
);
assert!(
corr_input.rho_out_h > baseline.rho_out_h,
"correlated input (rho_in_h={rho_in_h:.4}) rho_out_h={:.4} should exceed the uncorrelated \
baseline's {:.4}",
corr_input.rho_out_h,
baseline.rho_out_h
);
assert!(
corr_input.rho_out_h < 1.0,
"correlated input rho_out_h={:.4} must stay below 1.0",
corr_input.rho_out_h
);
}
fn make_flat_and_textured_frame(w: u32, h: u32, split_x: u32) -> Vec<f32> {
let mut frame = vec![0.0f32; (w * h) as usize];
for y in 0..h {
for x in 0..w {
let v = if x < split_x {
0.5
} else {
let fx = x as f32 / w as f32;
let fy = y as f32 / h as f32;
let raw = 0.5
+ 0.2 * (fx * 8.0 * std::f32::consts::PI).sin() * (fy * 6.0 * std::f32::consts::PI).cos()
+ 0.1 * (fx * 20.0 * std::f32::consts::PI).sin();
raw.clamp(0.05, 0.95)
};
frame[(y * w + x) as usize] = v;
}
}
frame
}
#[test]
fn nlm_residual_correlation_within_a_single_frame_flat_vs_textured_regions() {
let client = make_client();
let w = 200;
let h = 120;
let sigma_pre = 0.06f32;
let search_radius = 2;
let patch_radius = 2;
let split_x = 100;
let clean = make_flat_and_textured_frame(w, h, split_x);
let flat_region = (15u32, 15u32, 85u32, 105u32);
let textured_region = (115u32, 15u32, 185u32, 105u32);
let (flat, textured) = measure_diff_two_regions(
&client,
w,
h,
&clean,
sigma_pre,
|clean, seed| noisy_field_over(clean, w, h, sigma_pre, seed),
search_radius,
0,
patch_radius,
flat_region,
textured_region,
);
eprintln!(
"within-frame flat vs textured region (w={w} h={h} sigma_pre={sigma_pre} \
search_radius={search_radius} patch_radius={patch_radius}):\n\
{:<10} {:>8} {:>8} {:>8}",
"region", "rho_h", "rho_v", "sig_ratio"
);
for (label, s) in [("flat", &flat), ("textured", &textured)] {
eprintln!(
"{:<10} {:>8.4} {:>8.4} {:>8.4}",
label, s.rho_out_h, s.rho_out_v, s.sigma_ratio
);
}
eprintln!(
"within-frame gap: rho_h {:.4}, rho_v {:.4}",
(flat.rho_out_h - textured.rho_out_h).abs(),
(flat.rho_out_v - textured.rho_out_v).abs()
);
assert!(
flat.sigma_ratio < 0.9,
"flat region sigma_ratio={:.4} too close to 1.0, no real smoothing happened",
flat.sigma_ratio
);
assert!(
textured.sigma_ratio < 0.9,
"textured region sigma_ratio={:.4} too close to 1.0, no real smoothing happened",
textured.sigma_ratio
);
assert!(
(flat.rho_out_h - textured.rho_out_h).abs() < 0.1,
"within one frame, flat region rho_out_h={:.4} and textured region rho_out_h={:.4} \
diverge by more than the tolerance; a single per-frame correlation profile would not \
be structurally sound if this fails",
flat.rho_out_h,
textured.rho_out_h
);
}