use scirs2_core::ndarray::{Array, Array1, Array2, Dimension, Ix2, IxDyn};
use scirs2_core::numeric::{Float, FromPrimitive};
use scirs2_core::validation::check_positive;
use std::fmt::{Debug, Display};
#[cfg(feature = "simd")]
use scirs2_core::simd::{simd_add_f32, simd_add_f64, simd_scalar_mul_f32, simd_scalar_mul_f64};
use super::{pad_array, BorderMode};
use crate::error::{NdimageError, NdimageResult};
use crate::utils::safe_f64_to_float;
#[allow(dead_code)]
fn safe_i32_to_float<T: Float + FromPrimitive>(value: i32) -> NdimageResult<T> {
T::from_i32(value).ok_or_else(|| {
NdimageError::ComputationError(format!("Failed to convert i32 {} to float type", value))
})
}
#[allow(dead_code)]
fn safe_float_to_usize<T: Float>(value: T) -> NdimageResult<usize> {
value.to_usize().ok_or_else(|| {
NdimageError::ComputationError("Failed to convert float to usize".to_string())
})
}
#[allow(dead_code)]
pub fn bilateral_filter<T, D>(
input: &Array<T, D>,
sigma_spatial: T,
sigma_color: T,
mode: Option<BorderMode>,
) -> NdimageResult<Array<T, D>>
where
T: Float
+ Debug
+ Clone
+ Send
+ Sync
+ Display
+ FromPrimitive
+ std::ops::AddAssign
+ std::ops::DivAssign
+ 'static,
D: Dimension + 'static,
{
let border_mode = mode.unwrap_or(BorderMode::Reflect);
check_positive(sigma_spatial, "sigma_spatial").map_err(NdimageError::from)?;
check_positive(sigma_color, "sigma_color").map_err(NdimageError::from)?;
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.len() <= 1 {
return Ok(input.to_owned());
}
match input.ndim() {
1 => bilateral_filter_1d(input, sigma_spatial, sigma_color, &border_mode),
2 => bilateral_filter_2d(input, sigma_spatial, sigma_color, &border_mode),
_ => bilateral_filter_nd(input, sigma_spatial, sigma_color, &border_mode),
}
}
#[allow(dead_code)]
fn bilateral_filter_1d<T, D>(
input: &Array<T, D>,
sigma_spatial: T,
sigma_color: T,
mode: &BorderMode,
) -> NdimageResult<Array<T, D>>
where
T: Float + Debug + Clone + Display + FromPrimitive,
D: Dimension + 'static,
{
let input_1d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix1>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 1D array".into()))?;
let three = safe_f64_to_float::<T>(3.0)?;
let radius = safe_float_to_usize((sigma_spatial * three).ceil()).unwrap_or(3);
let kernel_size = 2 * radius + 1;
let mut output = Array1::zeros(input_1d.len());
let pad_width = vec![(radius, radius)];
let padded_input = pad_array(&input_1d, &pad_width, mode, None)?;
let mut spatial_weights = Array1::zeros(kernel_size);
let two = safe_f64_to_float::<T>(2.0)?;
let two_sigma_spatial_sq = two * sigma_spatial * sigma_spatial;
for k in 0..kernel_size {
let dist: T = safe_i32_to_float((k as i32) - (radius as i32))?;
spatial_weights[k] = (-dist * dist / two_sigma_spatial_sq).exp();
}
let two_sigma_color_sq = two * sigma_color * sigma_color;
for i in 0..input_1d.len() {
let center = i + radius;
let center_value = padded_input[center];
let mut weighted_sum = T::zero();
let mut weight_sum = T::zero();
for k in 0..kernel_size {
let neighbor_value = padded_input[center - radius + k];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[k] * color_weight;
weighted_sum = weighted_sum + neighbor_value * total_weight;
weight_sum = weight_sum + total_weight;
}
output[i] = if weight_sum > T::zero() {
weighted_sum / weight_sum
} else {
center_value
};
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 1D array".into()))
}
#[allow(dead_code)]
fn bilateral_filter_2d<T, D>(
input: &Array<T, D>,
sigma_spatial: T,
sigma_color: T,
mode: &BorderMode,
) -> NdimageResult<Array<T, D>>
where
T: Float + Debug + Clone + Display + FromPrimitive,
D: Dimension + 'static,
{
let input_2d = input
.to_owned()
.into_dimensionality::<Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D array".into()))?;
let (rows, cols) = input_2d.dim();
let three = safe_f64_to_float::<T>(3.0)?;
let radius = safe_float_to_usize((sigma_spatial * three).ceil()).unwrap_or(3);
let kernel_size = 2 * radius + 1;
let mut output = Array2::zeros((rows, cols));
let pad_width = vec![(radius, radius), (radius, radius)];
let padded_input = pad_array(&input_2d, &pad_width, mode, None)?;
let mut spatial_weights = Array2::zeros((kernel_size, kernel_size));
let two = safe_f64_to_float::<T>(2.0)?;
let two_sigma_spatial_sq = two * sigma_spatial * sigma_spatial;
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y_dist: T = safe_i32_to_float((dy as i32) - (radius as i32))?;
let x_dist: T = safe_i32_to_float((dx as i32) - (radius as i32))?;
let spatial_dist_sq: T = y_dist * y_dist + x_dist * x_dist;
spatial_weights[[dy, dx]] = (-spatial_dist_sq / two_sigma_spatial_sq).exp();
}
}
let two_sigma_color_sq = two * sigma_color * sigma_color;
for i in 0..rows {
for j in 0..cols {
let center_y = i + radius;
let center_x = j + radius;
let center_value = padded_input[[center_y, center_x]];
let mut weighted_sum = T::zero();
let mut weight_sum = T::zero();
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y = center_y - radius + dy;
let x = center_x - radius + dx;
let neighbor_value = padded_input[[y, x]];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[[dy, dx]] * color_weight;
weighted_sum = weighted_sum + neighbor_value * total_weight;
weight_sum = weight_sum + total_weight;
}
}
output[[i, j]] = if weight_sum > T::zero() {
weighted_sum / weight_sum
} else {
center_value
};
}
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D array".into()))
}
#[allow(dead_code)]
fn bilateral_filter_nd<T, D>(
input: &Array<T, D>,
sigma_spatial: T,
sigma_color: T,
mode: &BorderMode,
) -> NdimageResult<Array<T, D>>
where
T: Float + Debug + Clone + Display + FromPrimitive,
D: Dimension + 'static,
{
let three = safe_f64_to_float::<T>(3.0)?;
let radius = safe_float_to_usize((sigma_spatial * three).ceil()).unwrap_or(3);
let input_dyn = input.clone().into_dyn();
let mut output_dyn = Array::<T, IxDyn>::zeros(input_dyn.raw_dim());
let pad_width: Vec<(usize, usize)> = (0..input.ndim()).map(|_| (radius, radius)).collect();
let padded_input = pad_array(&input_dyn, &pad_width, mode, None)?;
let two = safe_f64_to_float::<T>(2.0)?;
let two_sigma_spatial_sq = two * sigma_spatial * sigma_spatial;
let two_sigma_color_sq = two * sigma_color * sigma_color;
for (idx, output_val) in output_dyn.indexed_iter_mut() {
let idx_vec = idx.as_array_view().to_vec();
let center_idx: Vec<usize> = idx_vec.iter().map(|&i| i + radius).collect();
let center_value = padded_input[&IxDyn(¢er_idx)];
let mut weighted_sum = T::zero();
let mut weight_sum = T::zero();
for offset in -(radius as i32)..=(radius as i32) {
let neighbor_idx = center_idx[0] as i32 + offset;
if neighbor_idx >= 0 && neighbor_idx < padded_input.len() as i32 {
let neighbor_value = padded_input[neighbor_idx as usize];
let dist: T = safe_i32_to_float(offset)?;
let spatial_dist_sq = dist * dist;
let spatial_weight = (-spatial_dist_sq / two_sigma_spatial_sq).exp();
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weight * color_weight;
weighted_sum = weighted_sum + neighbor_value * total_weight;
weight_sum = weight_sum + total_weight;
}
}
*output_val = if weight_sum > T::zero() {
weighted_sum / weight_sum
} else {
center_value
};
}
output_dyn.into_dimensionality::<D>().map_err(|_| {
NdimageError::DimensionError("Failed to convert back to original dimensions".into())
})
}
#[allow(dead_code)]
fn iterate_neighborhood<F>(_centeridx: &[usize], radius: usize, ndim: usize, mut callback: F)
where
F: FnMut(&[i32]),
{
let _kernel_size = 2 * radius + 1;
let mut offsets = vec![0i32; ndim];
for offset in &mut offsets {
*offset = -(radius as i32);
}
loop {
callback(&offsets);
let mut carry = true;
for i in (0..ndim).rev() {
if carry {
offsets[i] += 1;
if offsets[i] < (radius as i32) + 1 {
carry = false;
} else {
offsets[i] = -(radius as i32);
}
}
}
if carry {
break; }
}
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
pub fn bilateral_filter_simd_f32<D>(
input: &Array<f32, D>,
sigma_spatial: f32,
sigma_color: f32,
mode: Option<BorderMode>,
) -> NdimageResult<Array<f32, D>>
where
D: Dimension + 'static,
{
let border_mode = mode.unwrap_or(BorderMode::Reflect);
check_positive(sigma_spatial, "sigma_spatial").map_err(NdimageError::from)?;
check_positive(sigma_color, "sigma_color").map_err(NdimageError::from)?;
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.len() <= 1 {
return Ok(input.to_owned());
}
match input.ndim() {
1 => bilateral_filter_1d_simd_f32(input, sigma_spatial, sigma_color, &border_mode),
2 => bilateral_filter_2d_simd_f32(input, sigma_spatial, sigma_color, &border_mode),
_ => bilateral_filter(input, sigma_spatial, sigma_color, mode), }
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
pub fn bilateral_filter_simd_f64<D>(
input: &Array<f64, D>,
sigma_spatial: f64,
sigma_color: f64,
mode: Option<BorderMode>,
) -> NdimageResult<Array<f64, D>>
where
D: Dimension + 'static,
{
let border_mode = mode.unwrap_or(BorderMode::Reflect);
check_positive(sigma_spatial, "sigma_spatial").map_err(NdimageError::from)?;
check_positive(sigma_color, "sigma_color").map_err(NdimageError::from)?;
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.len() <= 1 {
return Ok(input.to_owned());
}
match input.ndim() {
1 => bilateral_filter_1d_simd_f64(input, sigma_spatial, sigma_color, &border_mode),
2 => bilateral_filter_2d_simd_f64(input, sigma_spatial, sigma_color, &border_mode),
_ => bilateral_filter(input, sigma_spatial, sigma_color, mode), }
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
fn bilateral_filter_1d_simd_f32<D>(
input: &Array<f32, D>,
sigma_spatial: f32,
sigma_color: f32,
mode: &BorderMode,
) -> NdimageResult<Array<f32, D>>
where
D: Dimension + 'static,
{
let input_1d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix1>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 1D array".into()))?;
let radius = ((sigma_spatial * 3.0).ceil() as usize).max(1);
let kernel_size = 2 * radius + 1;
let mut output = Array1::zeros(input_1d.len());
let pad_width = vec![(radius, radius)];
let padded_input = pad_array(&input_1d, &pad_width, mode, None)?;
let mut spatial_weights = Array1::zeros(kernel_size);
let two_sigma_spatial_sq = 2.0 * sigma_spatial * sigma_spatial;
for k in 0..kernel_size {
let dist = (k as i32 - radius as i32) as f32;
spatial_weights[k] = (-dist * dist / two_sigma_spatial_sq).exp();
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
for j in 0..input_1d.len() {
let center = j + radius;
let center_value = padded_input[center];
let mut weighted_sum = 0.0f32;
let mut weight_sum = 0.0f32;
let mut k = 0;
while k + 4 <= kernel_size {
let neighbor_values = Array1::from_vec(vec![
padded_input[center - radius + k],
padded_input[center - radius + k + 1],
padded_input[center - radius + k + 2],
padded_input[center - radius + k + 3],
]);
let center_values = Array1::from_elem(4, center_value);
let color_diffs = simd_add_f32(
&neighbor_values.view(),
&simd_scalar_mul_f32(¢er_values.view(), -1.0).view(),
);
for (idx, &diff) in color_diffs.iter().enumerate() {
let color_weight = (-diff * diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[k + idx] * color_weight;
weighted_sum += neighbor_values[idx] * total_weight;
weight_sum += total_weight;
}
k += 4;
}
for kk in k..kernel_size {
let neighbor_value = padded_input[center - radius + kk];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[kk] * color_weight;
weighted_sum += neighbor_value * total_weight;
weight_sum += total_weight;
}
output[j] = if weight_sum > 0.0 {
weighted_sum / weight_sum
} else {
center_value
};
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 1D array".into()))
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
fn bilateral_filter_1d_simd_f64<D>(
input: &Array<f64, D>,
sigma_spatial: f64,
sigma_color: f64,
mode: &BorderMode,
) -> NdimageResult<Array<f64, D>>
where
D: Dimension + 'static,
{
let input_1d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix1>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 1D array".into()))?;
let radius = ((sigma_spatial * 3.0).ceil() as usize).max(1);
let kernel_size = 2 * radius + 1;
let mut output = Array1::zeros(input_1d.len());
let pad_width = vec![(radius, radius)];
let padded_input = pad_array(&input_1d, &pad_width, mode, None)?;
let mut spatial_weights = Array1::zeros(kernel_size);
let two_sigma_spatial_sq = 2.0 * sigma_spatial * sigma_spatial;
for k in 0..kernel_size {
let dist = (k as i32 - radius as i32) as f64;
spatial_weights[k] = (-dist * dist / two_sigma_spatial_sq).exp();
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
for j in 0..input_1d.len() {
let center = j + radius;
let center_value = padded_input[center];
let mut weighted_sum = 0.0f64;
let mut weight_sum = 0.0f64;
let mut k = 0;
while k + 4 <= kernel_size {
let neighbor_values = Array1::from_vec(vec![
padded_input[center - radius + k],
padded_input[center - radius + k + 1],
padded_input[center - radius + k + 2],
padded_input[center - radius + k + 3],
]);
let center_values = Array1::from_elem(4, center_value);
let color_diffs = simd_add_f64(
&neighbor_values.view(),
&simd_scalar_mul_f64(¢er_values.view(), -1.0).view(),
);
for (idx, &diff) in color_diffs.iter().enumerate() {
let color_weight = (-diff * diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[k + idx] * color_weight;
weighted_sum += neighbor_values[idx] * total_weight;
weight_sum += total_weight;
}
k += 4;
}
for kk in k..kernel_size {
let neighbor_value = padded_input[center - radius + kk];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[kk] * color_weight;
weighted_sum += neighbor_value * total_weight;
weight_sum += total_weight;
}
output[j] = if weight_sum > 0.0 {
weighted_sum / weight_sum
} else {
center_value
};
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 1D array".into()))
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
fn bilateral_filter_2d_simd_f32<D>(
input: &Array<f32, D>,
sigma_spatial: f32,
sigma_color: f32,
mode: &BorderMode,
) -> NdimageResult<Array<f32, D>>
where
D: Dimension + 'static,
{
let input_2d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D array".into()))?;
let (rows, cols) = input_2d.dim();
let radius = ((sigma_spatial * 3.0).ceil() as usize).max(1);
let kernel_size = 2 * radius + 1;
let mut output = Array2::zeros((rows, cols));
let pad_width = vec![(radius, radius), (radius, radius)];
let padded_input = pad_array(&input_2d, &pad_width, mode, None)?;
let mut spatial_weights = Array2::zeros((kernel_size, kernel_size));
let two_sigma_spatial_sq = 2.0 * sigma_spatial * sigma_spatial;
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y_dist = (dy as i32 - radius as i32) as f32;
let x_dist = (dx as i32 - radius as i32) as f32;
let spatial_dist_sq = y_dist * y_dist + x_dist * x_dist;
spatial_weights[[dy, dx]] = (-spatial_dist_sq / two_sigma_spatial_sq).exp();
}
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
for i in 0..rows {
for j in 0..cols {
let center_y = i + radius;
let center_x = j + radius;
let center_value = padded_input[[center_y, center_x]];
let mut weighted_sum = 0.0f32;
let mut weight_sum = 0.0f32;
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y = center_y - radius + dy;
let x = center_x - radius + dx;
let neighbor_value = padded_input[[y, x]];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[[dy, dx]] * color_weight;
weighted_sum += neighbor_value * total_weight;
weight_sum += total_weight;
}
}
output[[i, j]] = if weight_sum > 0.0 {
weighted_sum / weight_sum
} else {
center_value
};
}
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D array".into()))
}
#[cfg(feature = "simd")]
#[allow(dead_code)]
fn bilateral_filter_2d_simd_f64<D>(
input: &Array<f64, D>,
sigma_spatial: f64,
sigma_color: f64,
mode: &BorderMode,
) -> NdimageResult<Array<f64, D>>
where
D: Dimension + 'static,
{
let input_2d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D array".into()))?;
let (rows, cols) = input_2d.dim();
let radius = ((sigma_spatial * 3.0).ceil() as usize).max(1);
let kernel_size = 2 * radius + 1;
let mut output = Array2::zeros((rows, cols));
let pad_width = vec![(radius, radius), (radius, radius)];
let padded_input = pad_array(&input_2d, &pad_width, mode, None)?;
let mut spatial_weights = Array2::zeros((kernel_size, kernel_size));
let two_sigma_spatial_sq = 2.0 * sigma_spatial * sigma_spatial;
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y_dist = (dy as i32 - radius as i32) as f64;
let x_dist = (dx as i32 - radius as i32) as f64;
let spatial_dist_sq = y_dist * y_dist + x_dist * x_dist;
spatial_weights[[dy, dx]] = (-spatial_dist_sq / two_sigma_spatial_sq).exp();
}
}
let two_sigma_color_sq = 2.0 * sigma_color * sigma_color;
for i in 0..rows {
for j in 0..cols {
let center_y = i + radius;
let center_x = j + radius;
let center_value = padded_input[[center_y, center_x]];
let mut weighted_sum = 0.0f64;
let mut weight_sum = 0.0f64;
for dy in 0..kernel_size {
for dx in 0..kernel_size {
let y = center_y - radius + dy;
let x = center_x - radius + dx;
let neighbor_value = padded_input[[y, x]];
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weights[[dy, dx]] * color_weight;
weighted_sum += neighbor_value * total_weight;
weight_sum += total_weight;
}
}
output[[i, j]] = if weight_sum > 0.0 {
weighted_sum / weight_sum
} else {
center_value
};
}
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D array".into()))
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::{Array1, Array2};
#[test]
fn test_bilateral_filter_1d() {
let signal = Array1::from_vec(vec![1.0, 1.0, 1.0, 5.0, 5.0, 5.0]);
let result = bilateral_filter(&signal, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for 1D test");
assert_eq!(result.shape(), signal.shape());
assert!(result[2] < result[3]); }
#[test]
fn test_bilateral_filter_2d() {
let mut image = Array2::zeros((5, 5));
for i in 0..5 {
for j in 0..2 {
image[[i, j]] = 1.0;
}
for j in 3..5 {
image[[i, j]] = 5.0;
}
}
let result = bilateral_filter(&image, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for 2D test");
assert_eq!(result.shape(), image.shape());
assert!(result[[2, 0]] < 3.0); assert!(result[[2, 4]] > 3.0); }
#[test]
fn test_bilateral_filter_uniform_region() {
let image = Array2::from_elem((5, 5), 3.0);
let result = bilateral_filter(&image, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for uniform region test");
for &val in result.iter() {
assert_abs_diff_eq!(val, 3.0, epsilon = 1e-2);
}
}
#[test]
fn test_bilateral_filter_noise_suppression() {
let mut signal = Array1::from_elem(10, 2.0);
signal[5] = 10.0;
let result = bilateral_filter(&signal, 1.0, 2.0, None)
.expect("bilateral_filter should succeed for noise suppression test");
assert!(result[5] > 2.0); assert!(result[5] < 10.0); }
#[test]
fn test_bilateral_filter_invalid_sigma() {
let array = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = bilateral_filter(&array, -1.0, 1.0, None);
assert!(result.is_err());
let result = bilateral_filter(&array, 1.0, -1.0, None);
assert!(result.is_err());
let result = bilateral_filter(&array, 0.0, 1.0, None);
assert!(result.is_err());
}
#[cfg(feature = "simd")]
#[test]
fn test_bilateral_filter_simd_f32() {
let signal = Array1::from_vec(vec![1.0f32, 1.0, 1.0, 5.0, 5.0, 5.0]);
let regular_result = bilateral_filter(&signal, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for SIMD f32 test");
let simd_result = bilateral_filter_simd_f32(&signal, 1.0, 1.0, None)
.expect("bilateral_filter_simd_f32 should succeed");
for i in 0..signal.len() {
assert_abs_diff_eq!(regular_result[i], simd_result[i], epsilon = 1e-6);
}
}
#[cfg(feature = "simd")]
#[test]
fn test_bilateral_filter_simd_f64() {
let signal = Array1::from_vec(vec![1.0f64, 1.0, 1.0, 5.0, 5.0, 5.0]);
let regular_result = bilateral_filter(&signal, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for SIMD f64 test");
let simd_result = bilateral_filter_simd_f64(&signal, 1.0, 1.0, None)
.expect("bilateral_filter_simd_f64 should succeed");
for i in 0..signal.len() {
assert_abs_diff_eq!(regular_result[i], simd_result[i], epsilon = 1e-6);
}
}
#[cfg(feature = "simd")]
#[test]
fn test_bilateral_filter_simd_2d_f32() {
let mut image = Array2::zeros((5, 5));
for i in 0..5 {
for j in 0..2 {
image[[i, j]] = 1.0f32;
}
for j in 3..5 {
image[[i, j]] = 5.0f32;
}
}
let regular_result = bilateral_filter(&image, 1.0, 1.0, None)
.expect("bilateral_filter should succeed for 2D SIMD test");
let simd_result = bilateral_filter_simd_f32(&image, 1.0, 1.0, None)
.expect("bilateral_filter_simd_f32 should succeed for 2D test");
for i in 0..5 {
for j in 0..5 {
assert_abs_diff_eq!(regular_result[[i, j]], simd_result[[i, j]], epsilon = 1e-6);
}
}
}
}
#[derive(Debug, Clone)]
pub struct MultiScaleBilateralConfig {
pub levels: usize,
pub spatial_sigmas: Vec<f64>,
pub color_sigmas: Vec<f64>,
pub downsample_factor: f64,
pub mode: BorderMode,
}
impl Default for MultiScaleBilateralConfig {
fn default() -> Self {
Self {
levels: 3,
spatial_sigmas: vec![0.5, 1.0, 2.0],
color_sigmas: vec![0.5, 1.0, 2.0],
downsample_factor: 0.5,
mode: BorderMode::Reflect,
}
}
}
#[allow(dead_code)]
pub fn multi_scale_bilateral_filter<T, D>(
input: &Array<T, D>,
config: &MultiScaleBilateralConfig,
) -> NdimageResult<Array<T, D>>
where
T: Float
+ FromPrimitive
+ Debug
+ Clone
+ Send
+ Sync
+ std::fmt::Display
+ std::ops::AddAssign
+ std::ops::DivAssign
+ 'static,
D: Dimension + 'static,
{
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if config.levels == 0 {
return Err(NdimageError::InvalidInput(
"Number of levels must be greater than 0".into(),
));
}
if config.spatial_sigmas.len() != config.levels || config.color_sigmas.len() != config.levels {
return Err(NdimageError::InvalidInput(
"Sigma arrays must have same length as number of levels".into(),
));
}
if config.levels == 1 {
let spatial_sigma = safe_f64_to_float::<T>(config.spatial_sigmas[0])?;
let color_sigma = safe_f64_to_float::<T>(config.color_sigmas[0])?;
return bilateral_filter(input, spatial_sigma, color_sigma, Some(config.mode));
}
let mut pyramid = vec![input.clone()];
for level in 1..config.levels {
let previmage = &pyramid[level - 1];
let downsampled = downsampleimage(previmage, config.downsample_factor)?;
pyramid.push(downsampled);
}
let mut filtered_pyramid = Vec::with_capacity(config.levels);
for (level, image) in pyramid.iter().enumerate() {
let spatial_sigma = safe_f64_to_float::<T>(config.spatial_sigmas[level])?;
let color_sigma = safe_f64_to_float::<T>(config.color_sigmas[level])?;
let filtered = bilateral_filter(image, spatial_sigma, color_sigma, Some(config.mode))?;
filtered_pyramid.push(filtered);
}
let mut result = filtered_pyramid.pop().expect("Operation failed");
for level in (0..config.levels - 1).rev() {
result = upsampleimage(&result, &filtered_pyramid[level])?;
let alpha = safe_f64_to_float::<T>(0.7)?; result = blend_arrays(&result, &filtered_pyramid[level], alpha)?;
}
Ok(result)
}
#[allow(dead_code)]
pub fn adaptive_bilateral_filter<T, D>(
input: &Array<T, D>,
base_spatial_sigma: T,
base_color_sigma: T,
adaptation_factor: T,
mode: Option<BorderMode>,
) -> NdimageResult<Array<T, D>>
where
T: Float
+ FromPrimitive
+ Debug
+ Clone
+ Send
+ Sync
+ std::fmt::Display
+ std::ops::AddAssign
+ std::ops::DivAssign
+ 'static,
D: Dimension + 'static,
{
let border_mode = mode.unwrap_or(BorderMode::Reflect);
scirs2_core::validation::check_positive(base_spatial_sigma, "base_spatial_sigma")
.map_err(NdimageError::from)?;
scirs2_core::validation::check_positive(base_color_sigma, "base_color_sigma")
.map_err(NdimageError::from)?;
if adaptation_factor < T::zero() || adaptation_factor > T::one() {
return Err(NdimageError::InvalidInput(
"Adaptation _factor must be between 0.0 and 1.0".into(),
));
}
if input.ndim() == 0 {
return Err(NdimageError::InvalidInput(
"Input array cannot be 0-dimensional".into(),
));
}
if input.ndim() == 2 {
return adaptive_bilateral_filter_2d(
input,
base_spatial_sigma,
base_color_sigma,
adaptation_factor,
&border_mode,
);
}
bilateral_filter(
input,
base_spatial_sigma,
base_color_sigma,
Some(border_mode),
)
}
#[allow(dead_code)]
fn adaptive_bilateral_filter_2d<T, D>(
input: &Array<T, D>,
base_spatial_sigma: T,
base_color_sigma: T,
adaptation_factor: T,
mode: &BorderMode,
) -> NdimageResult<Array<T, D>>
where
T: Float + FromPrimitive + Debug + Clone + Send + Sync + 'static,
D: Dimension + 'static,
{
let input_2d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D array".into()))?;
let (rows, cols) = input_2d.dim();
let mut output = Array2::zeros((rows, cols));
let variance_map = compute_local_variance(&input_2d, 3)?;
let max_variance = variance_map.iter().fold(T::zero(), |acc, &x| acc.max(x));
let window_size = 5;
let radius = window_size / 2;
let pad_width = vec![(radius, radius), (radius, radius)];
let padded_input = super::pad_array(&input_2d, &pad_width, mode, None)?;
for i in 0..rows {
for j in 0..cols {
let center_y = i + radius;
let center_x = j + radius;
let center_value = padded_input[[center_y, center_x]];
let local_variance = variance_map[[i, j]];
let variance_ratio = if max_variance > T::zero() {
local_variance / max_variance
} else {
T::zero()
};
let adaptive_spatial =
base_spatial_sigma * (T::one() + adaptation_factor * variance_ratio);
let adaptive_color = base_color_sigma
* (T::one() - adaptation_factor * variance_ratio * safe_f64_to_float::<T>(0.5)?);
let filtered_value = apply_bilateral_window(
&padded_input.view(),
center_y,
center_x,
center_value,
adaptive_spatial,
adaptive_color,
window_size,
)?;
output[[i, j]] = filtered_value;
}
}
output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D array".into()))
}
#[allow(dead_code)]
fn compute_local_variance<T>(input: &Array2<T>, windowsize: usize) -> NdimageResult<Array2<T>>
where
T: Float + FromPrimitive + Debug + Clone + 'static,
{
let (rows, cols) = input.dim();
let mut variance = Array2::zeros((rows, cols));
let radius = windowsize / 2;
let pad_width = vec![(radius, radius), (radius, radius)];
let padded = super::pad_array(input, &pad_width, &BorderMode::Reflect, None)?;
for i in 0..rows {
for j in 0..cols {
let center_y = i + radius;
let center_x = j + radius;
let mut sum = T::zero();
let mut sum_sq = T::zero();
let mut count = T::zero();
for dy in 0..windowsize {
for dx in 0..windowsize {
let y = center_y - radius + dy;
let x = center_x - radius + dx;
let value = padded[[y, x]];
sum = sum + value;
sum_sq = sum_sq + value * value;
count = count + T::one();
}
}
let mean = sum / count;
let variance_val = (sum_sq / count) - (mean * mean);
variance[[i, j]] = variance_val.max(T::zero());
}
}
Ok(variance)
}
#[allow(dead_code)]
fn apply_bilateral_window<T>(
padded_input: &scirs2_core::ndarray::ArrayView2<T>,
center_y: usize,
center_x: usize,
center_value: T,
spatial_sigma: T,
color_sigma: T,
window_size: usize,
) -> NdimageResult<T>
where
T: Float + FromPrimitive + Debug + Clone + 'static,
{
let radius = window_size / 2;
let two = safe_f64_to_float::<T>(2.0)?;
let two_sigma_spatial_sq = two * spatial_sigma * spatial_sigma;
let two_sigma_color_sq = two * color_sigma * color_sigma;
let mut weighted_sum = T::zero();
let mut weight_sum = T::zero();
for dy in 0..window_size {
for dx in 0..window_size {
let _y = center_y - radius + dy;
let _x = center_x - radius + dx;
let neighbor_value = padded_input[[_y, _x]];
let y_dist = safe_i32_to_float::<T>((dy as i32) - (radius as i32))?;
let x_dist = safe_i32_to_float::<T>((dx as i32) - (radius as i32))?;
let spatial_dist_sq: T = y_dist * y_dist + x_dist * x_dist;
let spatial_weight = (-spatial_dist_sq / two_sigma_spatial_sq).exp();
let color_diff = neighbor_value - center_value;
let color_weight = (-color_diff * color_diff / two_sigma_color_sq).exp();
let total_weight = spatial_weight * color_weight;
weighted_sum = weighted_sum + neighbor_value * total_weight;
weight_sum = weight_sum + total_weight;
}
}
Ok(if weight_sum > T::zero() {
weighted_sum / weight_sum
} else {
center_value
})
}
#[allow(dead_code)]
fn downsampleimage<T, D>(input: &Array<T, D>, factor: f64) -> NdimageResult<Array<T, D>>
where
T: Float + FromPrimitive + Debug + Clone + 'static,
D: Dimension + 'static,
{
if factor <= 0.0 || factor >= 1.0 {
return Err(NdimageError::InvalidInput(
"Downsample factor must be between 0 and 1".into(),
));
}
let oldshape = input.shape();
let mut newshape = Vec::with_capacity(oldshape.len());
for &dim_size in oldshape {
let new_size = ((dim_size as f64) * factor).max(1.0) as usize;
newshape.push(new_size);
}
let output_dyn = scirs2_core::ndarray::ArrayD::<T>::zeros(newshape.clone());
let output = output_dyn.into_dimensionality::<D>().map_err(|_| {
NdimageError::DimensionError("Failed to convert output array to correct dimension".into())
})?;
if input.ndim() == 2 && newshape.len() == 2 {
let input_2d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D".into()))?;
let mut output_2d = Array2::zeros((newshape[0], newshape[1]));
for i in 0..newshape[0] {
for j in 0..newshape[1] {
let src_i = ((i as f64) / factor).min((oldshape[0] - 1) as f64) as usize;
let src_j = ((j as f64) / factor).min((oldshape[1] - 1) as f64) as usize;
output_2d[[i, j]] = input_2d[[src_i, src_j]];
}
}
return output_2d
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D".into()));
}
Ok(input.clone())
}
#[allow(dead_code)]
fn upsampleimage<T, D>(input: &Array<T, D>, target: &Array<T, D>) -> NdimageResult<Array<T, D>>
where
T: Float + FromPrimitive + Debug + Clone + 'static,
D: Dimension + 'static,
{
let inputshape = input.shape();
let targetshape = target.shape();
if inputshape.len() != targetshape.len() {
return Err(NdimageError::DimensionError(
"Input and target must have same number of dimensions".into(),
));
}
if input.ndim() == 2 {
let input_2d = input
.to_owned()
.into_dimensionality::<scirs2_core::ndarray::Ix2>()
.map_err(|_| NdimageError::DimensionError("Failed to convert to 2D".into()))?;
let mut output = Array2::zeros((targetshape[0], targetshape[1]));
let scale_y = (inputshape[0] as f64) / (targetshape[0] as f64);
let scale_x = (inputshape[1] as f64) / (targetshape[1] as f64);
for i in 0..targetshape[0] {
for j in 0..targetshape[1] {
let src_i = ((i as f64) * scale_y).min((inputshape[0] - 1) as f64) as usize;
let src_j = ((j as f64) * scale_x).min((inputshape[1] - 1) as f64) as usize;
output[[i, j]] = input_2d[[src_i, src_j]];
}
}
return output
.into_dimensionality::<D>()
.map_err(|_| NdimageError::DimensionError("Failed to convert back from 2D".into()));
}
Ok(target.clone())
}
#[allow(dead_code)]
fn blend_arrays<T, D>(a: &Array<T, D>, b: &Array<T, D>, alpha: T) -> NdimageResult<Array<T, D>>
where
T: Float + FromPrimitive + Debug + Clone + 'static,
D: Dimension + 'static,
{
if a.shape() != b.shape() {
return Err(NdimageError::DimensionError(
"Arrays must have same shape for blending".into(),
));
}
let one_minus_alpha = T::one() - alpha;
let mut result = Array::zeros(a.raw_dim());
for ((aa, bb), rr) in a.iter().zip(b.iter()).zip(result.iter_mut()) {
*rr = *aa * alpha + *bb * one_minus_alpha;
}
Ok(result)
}