use crate::error::{FFTError, FFTResult};
use crate::fft::{fft, ifft};
use scirs2_core::ndarray::Array2;
use scirs2_core::numeric::Complex64;
use std::f64::consts::PI;
pub fn fft2d(data: &Array2<Complex64>) -> FFTResult<Array2<Complex64>> {
let (rows, cols) = data.dim();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let mut work = Array2::<Complex64>::zeros((rows, cols));
for r in 0..rows {
let row_slice: Vec<Complex64> = data.row(r).iter().copied().collect();
let row_spectrum = fft(&row_slice, Some(cols))?;
for c in 0..cols {
work[[r, c]] = row_spectrum[c];
}
}
let mut result = Array2::<Complex64>::zeros((rows, cols));
for c in 0..cols {
let col_slice: Vec<Complex64> = (0..rows).map(|r| work[[r, c]]).collect();
let col_spectrum = fft(&col_slice, Some(rows))?;
for r in 0..rows {
result[[r, c]] = col_spectrum[r];
}
}
Ok(result)
}
pub fn ifft2d(spectrum: &Array2<Complex64>) -> FFTResult<Array2<Complex64>> {
let (rows, cols) = spectrum.dim();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let mut work = Array2::<Complex64>::zeros((rows, cols));
for r in 0..rows {
let row_slice: Vec<Complex64> = spectrum.row(r).iter().copied().collect();
let row_spatial = ifft(&row_slice, Some(cols))?;
for c in 0..cols {
work[[r, c]] = row_spatial[c];
}
}
let mut result = Array2::<Complex64>::zeros((rows, cols));
for c in 0..cols {
let col_slice: Vec<Complex64> = (0..rows).map(|r| work[[r, c]]).collect();
let col_spatial = ifft(&col_slice, Some(rows))?;
for r in 0..rows {
result[[r, c]] = col_spatial[r];
}
}
Ok(result)
}
pub fn fft2d_real(data: &Array2<f64>) -> FFTResult<Array2<Complex64>> {
let (rows, cols) = data.dim();
if rows == 0 || cols == 0 {
return Ok(Array2::zeros((rows, cols)));
}
let complex_data = Array2::from_shape_fn((rows, cols), |(r, c)| {
Complex64::new(data[[r, c]], 0.0)
});
fft2d(&complex_data)
}
pub fn fftshift_2d(spectrum: &Array2<Complex64>) -> Array2<Complex64> {
let (rows, cols) = spectrum.dim();
let row_shift = rows / 2;
let col_shift = cols / 2;
Array2::from_shape_fn((rows, cols), |(r, c)| {
let sr = (r + row_shift) % rows;
let sc = (c + col_shift) % cols;
spectrum[[sr, sc]]
})
}
pub fn fftfreqs_2d(
rows: usize,
cols: usize,
dr: f64,
dc: f64,
) -> FFTResult<(Array2<f64>, Array2<f64>)> {
if rows == 0 || cols == 0 {
return Err(FFTError::ValueError(
"rows and cols must be non-zero".to_string(),
));
}
if dr <= 0.0 {
return Err(FFTError::ValueError(format!(
"Row sample spacing dr={dr} must be > 0"
)));
}
if dc <= 0.0 {
return Err(FFTError::ValueError(format!(
"Column sample spacing dc={dc} must be > 0"
)));
}
let row_freqs = fftfreq_1d(rows, dr);
let col_freqs = fftfreq_1d(cols, dc);
let freq_rows = Array2::from_shape_fn((rows, cols), |(r, _c)| row_freqs[r]);
let freq_cols = Array2::from_shape_fn((rows, cols), |(_r, c)| col_freqs[c]);
Ok((freq_rows, freq_cols))
}
fn fftfreq_1d(n: usize, d: f64) -> Vec<f64> {
let scale = 1.0 / (n as f64 * d);
(0..n)
.map(|i| {
let k = if i <= n / 2 - (if n % 2 == 0 { 1 } else { 0 }) {
i as i64
} else {
i as i64 - n as i64
};
k as f64 * scale
})
.collect()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Window2D {
Rectangular,
Hann,
Hamming,
Blackman,
}
pub fn power_spectral_density_2d(
image: &Array2<f64>,
window: Window2D,
) -> FFTResult<Array2<f64>> {
let (rows, cols) = image.dim();
if rows == 0 || cols == 0 {
return Err(FFTError::ValueError(
"Image must have non-zero dimensions".to_string(),
));
}
let win_r = build_window_1d(rows, window);
let win_c = build_window_1d(cols, window);
let win_power: f64 = win_r.iter().map(|&w| w * w).sum::<f64>()
* win_c.iter().map(|&w| w * w).sum::<f64>();
let windowed = Array2::from_shape_fn((rows, cols), |(r, c)| {
Complex64::new(image[[r, c]] * win_r[r] * win_c[c], 0.0)
});
let spectrum = fft2d(&windowed)?;
let scale = if win_power > 0.0 {
1.0 / win_power
} else {
1.0
};
Ok(Array2::from_shape_fn((rows, cols), |(r, c)| {
spectrum[[r, c]].norm_sqr() * scale
}))
}
fn build_window_1d(n: usize, window: Window2D) -> Vec<f64> {
match window {
Window2D::Rectangular => vec![1.0; n],
Window2D::Hann => (0..n)
.map(|i| {
let x = 2.0 * PI * i as f64 / (n as f64 - 1.0).max(1.0);
0.5 * (1.0 - x.cos())
})
.collect(),
Window2D::Hamming => (0..n)
.map(|i| {
let x = 2.0 * PI * i as f64 / (n as f64 - 1.0).max(1.0);
0.54 - 0.46 * x.cos()
})
.collect(),
Window2D::Blackman => (0..n)
.map(|i| {
let x = 2.0 * PI * i as f64 / (n as f64 - 1.0).max(1.0);
0.42 - 0.5 * x.cos() + 0.08 * (2.0 * x).cos()
})
.collect(),
}
}
pub fn cross_spectrum_2d(
im1: &Array2<f64>,
im2: &Array2<f64>,
) -> FFTResult<Array2<Complex64>> {
if im1.shape() != im2.shape() {
return Err(FFTError::ValueError(format!(
"Input arrays must have the same shape: {:?} vs {:?}",
im1.shape(),
im2.shape()
)));
}
let x1 = fft2d_real(im1)?;
let x2 = fft2d_real(im2)?;
let (rows, cols) = x1.dim();
Ok(Array2::from_shape_fn((rows, cols), |(r, c)| {
x1[[r, c]] * x2[[r, c]].conj()
}))
}
pub fn coherence_2d(im1: &Array2<f64>, im2: &Array2<f64>) -> FFTResult<Array2<f64>> {
if im1.shape() != im2.shape() {
return Err(FFTError::ValueError(format!(
"Input arrays must have the same shape: {:?} vs {:?}",
im1.shape(),
im2.shape()
)));
}
let x1 = fft2d_real(im1)?;
let x2 = fft2d_real(im2)?;
let (rows, cols) = x1.dim();
Ok(Array2::from_shape_fn((rows, cols), |(r, c)| {
let s_xx = x1[[r, c]].norm_sqr();
let s_yy = x2[[r, c]].norm_sqr();
let s_xy = (x1[[r, c]] * x2[[r, c]].conj()).norm_sqr();
let denom = s_xx * s_yy;
if denom < 1e-30 {
0.0
} else {
(s_xy / denom).min(1.0)
}
}))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_fft2d_ifft2d_roundtrip() {
let data = Array2::from_shape_fn((8, 8), |(r, c)| {
Complex64::new((r as f64).sin() + (c as f64).cos(), r as f64 * 0.1)
});
let spectrum = fft2d(&data).expect("fft2d failed");
let recovered = ifft2d(&spectrum).expect("ifft2d failed");
for r in 0..8 {
for c in 0..8 {
assert_relative_eq!(data[[r, c]].re, recovered[[r, c]].re, epsilon = 1e-10);
assert_relative_eq!(data[[r, c]].im, recovered[[r, c]].im, epsilon = 1e-10);
}
}
}
#[test]
fn test_fft2d_shape_preserved() {
let data = Array2::from_shape_fn((5, 7), |(r, c)| Complex64::new((r + c) as f64, 0.0));
let s = fft2d(&data).expect("fft2d");
assert_eq!(s.shape(), data.shape());
}
#[test]
fn test_fft2d_dc_component() {
let n = 4;
let val = 3.0;
let data = Array2::from_shape_fn((n, n), |_| Complex64::new(val, 0.0));
let s = fft2d(&data).expect("fft2d");
let expected_dc = val * (n * n) as f64;
assert_relative_eq!(s[[0, 0]].re, expected_dc, epsilon = 1e-9);
assert_relative_eq!(s[[0, 0]].im, 0.0, epsilon = 1e-9);
}
#[test]
fn test_fft2d_real_dc() {
let n = 8;
let data = Array2::from_shape_fn((n, n), |(r, c)| r as f64 + c as f64);
let s = fft2d_real(&data).expect("fft2d_real");
let sum: f64 = data.iter().sum();
assert_relative_eq!(s[[0, 0]].re, sum, epsilon = 1e-8);
}
#[test]
fn test_fft2d_real_hermitian_symmetry() {
let rows = 6;
let cols = 8;
let data = Array2::from_shape_fn((rows, cols), |(r, c)| {
((r as f64 * 1.3 + c as f64 * 2.7) * 0.1).sin()
});
let s = fft2d_real(&data).expect("fft2d_real");
for r in 1..rows {
for c in 1..cols {
let conj_r = (rows - r) % rows;
let conj_c = (cols - c) % cols;
let x = s[[r, c]];
let cx = s[[conj_r, conj_c]].conj();
assert_relative_eq!(x.re, cx.re, epsilon = 1e-10);
assert_relative_eq!(x.im, cx.im, epsilon = 1e-10);
}
}
}
#[test]
fn test_fftshift_2d_dc_to_centre_even() {
let mut data = Array2::<Complex64>::zeros((4, 4));
data[[0, 0]] = Complex64::new(1.0, 0.0);
let shifted = fftshift_2d(&data);
assert_relative_eq!(shifted[[2, 2]].re, 1.0, epsilon = 1e-12);
}
#[test]
fn test_fftshift_2d_dc_to_centre_odd() {
let mut data = Array2::<Complex64>::zeros((5, 5));
data[[0, 0]] = Complex64::new(1.0, 0.0);
let shifted = fftshift_2d(&data);
assert_relative_eq!(shifted[[2, 2]].re, 1.0, epsilon = 1e-12);
}
#[test]
fn test_fftshift_2d_shape_preserved() {
let data = Array2::<Complex64>::zeros((6, 10));
let s = fftshift_2d(&data);
assert_eq!(s.shape(), data.shape());
}
#[test]
fn test_fftfreqs_2d_dc_is_zero() {
let (fr, fc) = fftfreqs_2d(8, 8, 1.0, 1.0).expect("fftfreqs_2d");
assert_eq!(fr[[0, 0]], 0.0);
assert_eq!(fc[[0, 0]], 0.0);
}
#[test]
fn test_fftfreqs_2d_shape() {
let (fr, fc) = fftfreqs_2d(4, 6, 0.5, 0.5).expect("fftfreqs_2d");
assert_eq!(fr.shape(), [4, 6]);
assert_eq!(fc.shape(), [4, 6]);
}
#[test]
fn test_fftfreqs_2d_invalid_spacing() {
assert!(fftfreqs_2d(4, 4, 0.0, 1.0).is_err());
assert!(fftfreqs_2d(4, 4, 1.0, -1.0).is_err());
}
#[test]
fn test_fftfreqs_2d_zero_size() {
assert!(fftfreqs_2d(0, 4, 1.0, 1.0).is_err());
assert!(fftfreqs_2d(4, 0, 1.0, 1.0).is_err());
}
#[test]
fn test_fftfreqs_2d_row_independence() {
let (fr, _fc) = fftfreqs_2d(4, 6, 1.0, 1.0).expect("fftfreqs_2d");
for r in 0..4 {
let f0 = fr[[r, 0]];
for c in 1..6 {
assert_eq!(fr[[r, c]], f0);
}
}
}
#[test]
fn test_psd_2d_non_negative() {
let data = Array2::from_shape_fn((8, 8), |(r, c)| {
((r as f64 + c as f64) * 0.5).sin()
});
for window in [Window2D::Rectangular, Window2D::Hann, Window2D::Hamming, Window2D::Blackman] {
let psd = power_spectral_density_2d(&data, window).expect("PSD");
for &v in psd.iter() {
assert!(v >= 0.0, "PSD value {v} is negative for {window:?}");
}
}
}
#[test]
fn test_psd_2d_zero_input() {
let data = Array2::<f64>::zeros((8, 8));
let psd = power_spectral_density_2d(&data, Window2D::Hann).expect("PSD");
for &v in psd.iter() {
assert_eq!(v, 0.0);
}
}
#[test]
fn test_psd_2d_shape() {
let data = Array2::<f64>::zeros((6, 10));
let psd = power_spectral_density_2d(&data, Window2D::Rectangular).expect("PSD");
assert_eq!(psd.shape(), [6, 10]);
}
#[test]
fn test_cross_spectrum_2d_shape() {
let a = Array2::<f64>::zeros((6, 8));
let b = Array2::<f64>::zeros((6, 8));
let cs = cross_spectrum_2d(&a, &b).expect("cross_spectrum_2d");
assert_eq!(cs.shape(), [6, 8]);
}
#[test]
fn test_cross_spectrum_2d_shape_mismatch() {
let a = Array2::<f64>::zeros((4, 8));
let b = Array2::<f64>::zeros((4, 6));
assert!(cross_spectrum_2d(&a, &b).is_err());
}
#[test]
fn test_cross_spectrum_2d_auto_is_psd() {
let data = Array2::from_shape_fn((8, 8), |(r, c)| {
((r as f64 * 0.7 + c as f64 * 1.1) * 0.2).cos()
});
let cs = cross_spectrum_2d(&data, &data).expect("cross_spectrum auto");
for r in 0..8 {
for c in 0..8 {
assert!(
cs[[r, c]].im.abs() < 1e-10,
"Auto cross-spectrum should be real, got im={} at ({r},{c})",
cs[[r, c]].im
);
assert!(cs[[r, c]].re >= -1e-10, "Auto cross-spectrum must be non-negative");
}
}
}
#[test]
fn test_coherence_2d_range() {
let a = Array2::from_shape_fn((8, 8), |(r, c)| ((r + c) as f64 * 0.3).sin());
let b = Array2::from_shape_fn((8, 8), |(r, c)| ((r * c) as f64 * 0.1).cos());
let coh = coherence_2d(&a, &b).expect("coherence_2d");
for &v in coh.iter() {
assert!(
(0.0..=1.0 + 1e-10).contains(&v),
"Coherence value {v} is out of [0, 1]"
);
}
}
#[test]
fn test_coherence_2d_identical_images() {
let data = Array2::from_shape_fn((8, 8), |(r, c)| (r + c) as f64 + 1.0);
let coh = coherence_2d(&data, &data).expect("coherence identical");
for &v in coh.iter() {
assert!(
(0.0..=1.0 + 1e-10).contains(&v),
"Coherence out of range: {v}"
);
if v > 0.0 {
assert!(
(v - 1.0).abs() < 1e-8,
"Self-coherence should be 1.0, got {v}"
);
}
}
}
#[test]
fn test_coherence_2d_shape_mismatch() {
let a = Array2::<f64>::zeros((4, 8));
let b = Array2::<f64>::zeros((4, 6));
assert!(coherence_2d(&a, &b).is_err());
}
#[test]
fn test_parseval_2d() {
let rows = 8;
let cols = 8;
let data = Array2::from_shape_fn((rows, cols), |(r, c)| {
Complex64::new(
(2.0 * PI * r as f64 / rows as f64).sin(),
(2.0 * PI * c as f64 / cols as f64).cos(),
)
});
let spectrum = fft2d(&data).expect("fft2d");
let energy_x: f64 = data.iter().map(|c| c.norm_sqr()).sum();
let energy_s: f64 = spectrum.iter().map(|c| c.norm_sqr()).sum();
let n2 = (rows * cols) as f64;
assert_relative_eq!(energy_s, n2 * energy_x, epsilon = 1e-8 * energy_s.max(1.0));
}
#[test]
fn test_fft2d_zero_size() {
let data = Array2::<Complex64>::zeros((0, 8));
let s = fft2d(&data).expect("fft2d zero rows");
assert_eq!(s.shape(), [0, 8]);
let data2 = Array2::<Complex64>::zeros((4, 0));
let s2 = fft2d(&data2).expect("fft2d zero cols");
assert_eq!(s2.shape(), [4, 0]);
}
}