use ndarray::{Array2, ArrayView2, Axis};
use crate::convolve::{
Boundary, GaussianSampling, Normalization, conv_axis, conv_axis_renorm, conv2d_renorm,
gaussian1d,
};
use crate::float::Float;
const TRUNCATE: f64 = 4.0;
pub fn convolve_psf<T: Float>(image: ArrayView2<T>, psf: ArrayView2<T>) -> Array2<T> {
assert!(
psf.nrows() % 2 == 1 && psf.ncols() % 2 == 1,
"convolve_psf: psf must have odd dimensions (a centered PSF); got {}x{}",
psf.nrows(),
psf.ncols()
);
let flipped = psf.slice(ndarray::s![..;-1, ..;-1]);
let (value, _weight) = conv2d_renorm(image, flipped);
value
}
pub fn convolve_gaussian_axis<T: Float>(
image: ArrayView2<T>,
sigma: f64,
axis: Axis,
normalization: Normalization,
boundary: Boundary,
renormalize: bool,
) -> Array2<T> {
let kernel = gaussian1d::<T>(
sigma,
TRUNCATE,
GaussianSampling::ErfIntegrated,
normalization,
);
if renormalize {
let (value, _weight) = conv_axis_renorm(image, kernel.view(), axis);
value
} else {
conv_axis(image, kernel.view(), axis, boundary)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::convolve::conv2d;
use ndarray::{Array2, array};
const TOL_F64: f64 = 1e-12;
fn approx_eq_f64(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol * a.abs().max(b.abs()).max(1.0)
}
#[test]
fn true_convolution_keeps_psf_orientation_f64() {
let mut image = Array2::<f64>::zeros((5, 5));
image[(2, 2)] = 1.0;
let mut psf = Array2::<f64>::zeros((3, 3));
psf[(0, 1)] = 1.0; let out = convolve_psf(image.view(), psf.view());
assert!(approx_eq_f64(out[(1, 2)], 1.0, TOL_F64));
assert!(approx_eq_f64(out[(3, 2)], 0.0, TOL_F64));
}
#[test]
fn sum_normalized_psf_interior_equals_true_convolution_f64() {
let mut image = Array2::<f64>::zeros((7, 7));
for ((i, j), v) in image.indexed_iter_mut() {
*v = (i * 5 + j * 3) as f64 * 0.25 - 2.0;
}
let mut psf = array![
[0.05_f64, 0.10, 0.02],
[0.15, 0.30, 0.08],
[0.04, 0.18, 0.08]
];
let sum: f64 = psf.iter().sum();
psf.mapv_inplace(|v| v / sum);
let out = convolve_psf(image.view(), psf.view());
let flipped = psf.slice(ndarray::s![..;-1, ..;-1]);
let reference = conv2d(image.view(), flipped, Boundary::Zero);
for i in 1..6 {
for j in 1..6 {
assert!(
approx_eq_f64(out[(i, j)], reference[(i, j)], 1e-9),
"interior mismatch at ({i},{j})"
);
}
}
}
#[test]
fn renorm_does_not_darken_at_nan_or_edges_f64() {
let constant = 4.2_f64;
let mut image = Array2::<f64>::from_elem((9, 9), constant);
image[(4, 4)] = f64::NAN;
let mut psf = Array2::<f64>::from_elem((3, 3), 1.0);
let sum: f64 = psf.iter().sum();
psf.mapv_inplace(|v| v / sum); let out = convolve_psf(image.view(), psf.view());
for v in out.iter() {
assert!(v.is_finite());
assert!(approx_eq_f64(*v, constant, 1e-9));
}
}
#[test]
#[should_panic(expected = "odd dimensions")]
fn even_dim_psf_panics() {
let image = Array2::<f64>::zeros((4, 4));
let psf = Array2::<f64>::from_elem((2, 2), 0.25);
let _ = convolve_psf(image.view(), psf.view());
}
#[test]
fn matched_filter_peak_maximized_at_injected_sigma_f64() {
let rows = 121usize;
let cols = 4usize;
let line_row = 60.0_f64;
let injected_sigma = 5.0_f64;
let mut image = Array2::<f64>::zeros((rows, cols));
for i in 0..rows {
let z = (i as f64 - line_row) / injected_sigma;
let amplitude = (-0.5 * z * z).exp();
for j in 0..cols {
image[(i, j)] = amplitude;
}
}
let peak_at = |probe_sigma: f64| {
let response = convolve_gaussian_axis(
image.view(),
probe_sigma,
Axis(0),
Normalization::L2,
Boundary::Zero,
false,
);
response[(60, 0)]
};
let matched = peak_at(injected_sigma);
assert!(matched > peak_at(injected_sigma * 0.5));
assert!(matched > peak_at(injected_sigma * 2.0));
}
#[test]
fn gaussian_axis_selects_the_requested_axis_f64() {
let rows = 9usize;
let cols = 9usize;
let mut image = Array2::<f64>::zeros((rows, cols));
for i in 0..rows {
for j in 0..cols {
image[(i, j)] = (i as f64 - 4.0).powi(2);
}
}
let along_constant = convolve_gaussian_axis(
image.view(),
1.5,
Axis(1),
Normalization::Sum,
Boundary::Zero,
true,
);
for i in 0..rows {
for j in 0..cols {
assert!(approx_eq_f64(along_constant[(i, j)], image[(i, j)], 1e-9));
}
}
let along_varying = convolve_gaussian_axis(
image.view(),
1.5,
Axis(0),
Normalization::Sum,
Boundary::Zero,
true,
);
let mut differs = false;
for i in 0..rows {
for j in 0..cols {
if !approx_eq_f64(along_varying[(i, j)], image[(i, j)], 1e-6) {
differs = true;
}
}
}
assert!(differs, "smoothing the varying axis must change the image");
}
#[test]
fn renormalize_switch_controls_edge_behavior_f64() {
let image = Array2::<f64>::from_elem((20, 3), 5.0);
let renormed = convolve_gaussian_axis(
image.view(),
2.0,
Axis(0),
Normalization::Sum,
Boundary::Zero,
true,
);
for v in renormed.iter() {
assert!(approx_eq_f64(*v, 5.0, 1e-9));
}
let zero_pad = convolve_gaussian_axis(
image.view(),
2.0,
Axis(0),
Normalization::Sum,
Boundary::Zero,
false,
);
assert!(zero_pad[(0, 0)] < 5.0 - 1e-3, "edge should be darkened");
assert!(
approx_eq_f64(zero_pad[(10, 0)], 5.0, 1e-6),
"interior should be unaffected"
);
}
#[test]
fn works_with_f32() {
let mut image = Array2::<f32>::from_elem((6, 6), 3.0);
image[(2, 3)] = f32::NAN;
let psf = Array2::<f32>::from_elem((3, 3), 1.0 / 9.0);
let out = convolve_psf(image.view(), psf.view());
for v in out.iter() {
assert!(v.is_finite());
assert!((*v - 3.0).abs() <= 1e-4);
}
let line = convolve_gaussian_axis(
image.view(),
1.0,
Axis(1),
Normalization::L2,
Boundary::Reflect,
false,
);
assert_eq!(line.dim(), (6, 6));
}
}