use ndarray::{Array, ArrayD, IxDyn};
use rustfft::num_complex::Complex;
use image::GrayImage;
use rustfft::FftPlanner;
use nalgebra::DMatrix;
use image::RgbImage;
pub fn fft2d(matrix: &DMatrix<Complex<f64>>) -> DMatrix<Complex<f64>> {
let rows = matrix.nrows();
let cols = matrix.ncols();
let mut planner = FftPlanner::new();
let mut result = matrix.clone();
for i in 0..rows {
let mut row_data: Vec<Complex<f64>> = (0..cols).map(|j| matrix[(i, j)]).collect();
let fft = planner.plan_fft_forward(cols);
fft.process(&mut row_data);
for j in 0..cols {
result[(i, j)] = row_data[j];
}
}
for j in 0..cols {
let mut col_data: Vec<Complex<f64>> = (0..rows).map(|i| result[(i, j)]).collect();
let fft = planner.plan_fft_forward(rows);
fft.process(&mut col_data);
for i in 0..rows {
result[(i, j)] = col_data[i];
}
}
result
}
pub fn ifft2d(matrix: &DMatrix<Complex<f64>>) -> DMatrix<Complex<f64>> {
let rows = matrix.nrows();
let cols = matrix.ncols();
let mut planner = FftPlanner::new();
let mut result = matrix.clone();
for i in 0..rows {
let mut row_data: Vec<Complex<f64>> = (0..cols).map(|j| matrix[(i, j)]).collect();
let ifft = planner.plan_fft_inverse(cols);
ifft.process(&mut row_data);
for j in 0..cols {
result[(i, j)] = row_data[j] / Complex::new(cols as f64, 0.0);
}
}
for j in 0..cols {
let mut col_data: Vec<Complex<f64>> = (0..rows).map(|i| result[(i, j)]).collect();
let ifft = planner.plan_fft_inverse(rows);
ifft.process(&mut col_data);
for i in 0..rows {
result[(i, j)] = col_data[i] / Complex::new(rows as f64, 0.0);
}
}
result
}
pub fn get_nd_butterworth_filter(
shape: &[usize],
factor: f64,
order: f64,
high_pass: bool,
_real: bool,
squared_butterworth: bool,
) -> ArrayD<Complex<f64>> {
let wfilt = Array::from_shape_fn(IxDyn(shape), |idx| {
let len = shape.len();
let mut distance2 = 0.0;
for i in 0..len {
let mut freq = idx[i] as f64;
if freq > shape[i] as f64 / 2.0 {
freq -= shape[i] as f64;
}
freq /= shape[i] as f64;
distance2 += freq.powi(2);
}
let radius = distance2.sqrt();
let response = 1.0 / (1.0 + (radius / factor).powf(order * 2.0));
let response = if high_pass {
(radius / factor).powf(order * 2.0) / (1.0 + (radius / factor).powf(order * 2.0))
} else {
response
};
let response = if squared_butterworth { response.powi(2) } else { response };
Complex::new(response, 0.0)
});
wfilt
}
pub fn pad_image(image: &GrayImage, npad: usize) -> (DMatrix<Complex<f64>>, (u32, u32)) {
let (original_width, original_height) = image.dimensions();
let padded_width = (original_width as usize + 2 * npad).next_power_of_two() as u32;
let padded_height = (original_height as usize + 2 * npad).next_power_of_two() as u32;
let mut padded_image = GrayImage::new(padded_width, padded_height);
let x_offset = (padded_width - original_width) / 2;
let y_offset = (padded_height - original_height) / 2;
for y in 0..original_height {
for x in 0..original_width {
let pixel = image.get_pixel(x, y);
padded_image.put_pixel(x + x_offset, y + y_offset, *pixel);
}
}
let complex_matrix = DMatrix::from_iterator(
padded_height as usize,
padded_width as usize,
padded_image.pixels().map(|p| Complex::new(p.0[0] as f64 / 255.0, 0.0))
);
(complex_matrix, (original_width, original_height))
}
fn extract_original_portion(filtered_image: &GrayImage, original_dimensions: (u32, u32)) -> GrayImage {
let (original_width, original_height) = original_dimensions;
let (current_width, current_height) = filtered_image.dimensions();
if original_width == current_width && original_height == current_height {
return filtered_image.clone();
}
let x_offset = (current_width - original_width) / 2;
let y_offset = (current_height - original_height) / 2;
image::imageops::crop_imm(filtered_image, x_offset, y_offset, original_width, original_height)
.to_image()
}
fn apply_fft_and_filter(
padded_image: &DMatrix<Complex<f64>>,
butterworth_filter: &DMatrix<Complex<f64>>,
) -> GrayImage {
let fft_image = fft2d(padded_image);
assert_eq!(fft_image.nrows(), butterworth_filter.nrows());
assert_eq!(fft_image.ncols(), butterworth_filter.ncols());
let mut filtered_image = DMatrix::zeros(fft_image.nrows(), fft_image.ncols());
for i in 0..fft_image.nrows() {
for j in 0..fft_image.ncols() {
filtered_image[(i, j)] = fft_image[(i, j)] * butterworth_filter[(i, j)];
}
}
let ifft_image = ifft2d(&filtered_image);
let real_ifft: Vec<f64> = ifft_image.iter().map(|c| c.re).collect();
let min_val = real_ifft.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = real_ifft.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if (max_val - min_val) > f64::EPSILON {
let scale = 255.0 / (max_val - min_val);
GrayImage::from_raw(
ifft_image.ncols() as u32,
ifft_image.nrows() as u32,
real_ifft.iter().map(|&val| {
let scaled_val = ((val - min_val) * scale).min(255.0).max(0.0) as u8;
scaled_val
}).collect()
).unwrap()
} else {
GrayImage::new(ifft_image.ncols() as u32, ifft_image.nrows() as u32)
}
}
pub fn butterworth(
image: &GrayImage,
cutoff_frequency_ratio: f64,
high_pass: bool,
order: f64,
squared_butterworth: bool,
npad: usize,
) -> (GrayImage, DMatrix<Complex<f64>>) {
if cutoff_frequency_ratio < 0.0 || cutoff_frequency_ratio > 0.5 {
panic!("cutoff_frequency_ratio should be in the range [0, 0.5]");
}
let (padded_image, original_dimensions) = pad_image(image, npad);
let fft_shape = &[padded_image.nrows(), padded_image.ncols()];
let butterworth_filter_ndarray = get_nd_butterworth_filter(
fft_shape,
cutoff_frequency_ratio,
order,
high_pass,
true,
squared_butterworth,
);
let butterworth_filter = DMatrix::from_iterator(
fft_shape[0],
fft_shape[1],
butterworth_filter_ndarray.iter().cloned()
);
let filtered_image = apply_fft_and_filter(&padded_image, &butterworth_filter);
let original_portion = extract_original_portion(&filtered_image, original_dimensions);
(original_portion, butterworth_filter)
}
pub fn butterworth_color(
image: &RgbImage,
cutoff_frequency_ratio: f64,
high_pass: bool,
order: f64,
squared_butterworth: bool,
npad: usize,
) -> (RgbImage, DMatrix<Complex<f64>>) {
let (width, height) = image.dimensions();
let mut filtered_image = RgbImage::new(width, height);
let mut filter = None;
for channel in 0..3 {
let mut gray_channel = GrayImage::new(width, height);
for y in 0..height {
for x in 0..width {
let pixel = image.get_pixel(x, y);
gray_channel.put_pixel(x, y, image::Luma([pixel[channel]]));
}
}
let (filtered_channel, channel_filter) = crate::butterworth(
&gray_channel,
cutoff_frequency_ratio,
high_pass,
order,
squared_butterworth,
npad,
);
if channel == 0 {
filter = Some(channel_filter);
}
for y in 0..height {
for x in 0..width {
let mut pixel = *filtered_image.get_pixel(x, y);
pixel[channel] = filtered_channel.get_pixel(x, y)[0];
filtered_image.put_pixel(x, y, pixel);
}
}
}
(filtered_image, filter.unwrap())
}