use medians::Medianf64;
fn silx_window_median(values: &mut [f64]) -> f64 {
let n = values.len();
debug_assert!(n > 0, "silx_window_median requires a non-empty window");
if n & 1 == 1 {
(&*values).medf_unchecked()
} else {
let pivot = n / 2;
values.select_nth_unstable_by(pivot, |a, b| a.total_cmp(b));
values[pivot]
}
}
fn nearest_index(index: isize, length: usize) -> usize {
if index < 0 {
0
} else if index as usize >= length {
length - 1
} else {
index as usize
}
}
pub fn median_filter_2d(
data: &[f64],
width: usize,
height: usize,
kernel_h: usize,
kernel_w: usize,
conditional: bool,
) -> Vec<f64> {
assert_eq!(
data.len(),
width * height,
"median_filter_2d: data length {} != width*height {}",
data.len(),
width * height
);
assert!(
kernel_h >= 1 && kernel_w >= 1,
"median_filter_2d: kernel dimensions must be >= 1"
);
assert!(
kernel_h % 2 == 1 && kernel_w % 2 == 1,
"median_filter_2d: kernel dimensions must be odd (silx odd-kernel assertion)"
);
let mut output = vec![0.0_f64; data.len()];
if width == 0 || height == 0 {
return output;
}
let half_y = (kernel_h - 1) / 2;
let half_x = (kernel_w - 1) / 2;
let mut window: Vec<f64> = Vec::with_capacity(kernel_h * kernel_w);
for y in 0..height {
for x in 0..width {
window.clear();
for ky in 0..kernel_h {
let win_y = y as isize + ky as isize - half_y as isize;
let iy = nearest_index(win_y, height);
for kx in 0..kernel_w {
let win_x = x as isize + kx as isize - half_x as isize;
let ix = nearest_index(win_x, width);
let value = data[iy * width + ix];
if !value.is_nan() {
window.push(value);
}
}
}
let center = data[y * width + x];
if window.is_empty() {
output[y * width + x] = f64::NAN;
continue;
}
if conditional {
let mut win_min = window[0];
let mut win_max = window[0];
for &v in &window[1..] {
if v > win_max {
win_max = v;
}
if v < win_min {
win_min = v;
}
}
if center == win_max || center == win_min {
output[y * width + x] = silx_window_median(&mut window);
} else {
output[y * width + x] = center;
}
} else {
output[y * width + x] = silx_window_median(&mut window);
}
}
}
output
}
pub fn median_filter_1d(
data: &[f64],
width: usize,
height: usize,
kernel_width: usize,
conditional: bool,
) -> Vec<f64> {
median_filter_2d(data, width, height, kernel_width, 1, conditional)
}
#[derive(Clone, Debug, PartialEq)]
pub struct PixelHistogram {
pub edges: Vec<f64>,
pub counts: Vec<u64>,
pub min: f64,
pub max: f64,
pub mean: f64,
pub std: f64,
pub sum: f64,
pub n_bins: usize,
}
pub fn pixel_intensity_histogram(pixels: &[f64], n_bins: Option<usize>) -> Option<PixelHistogram> {
let mut finite_count: usize = 0;
let mut min = f64::INFINITY;
let mut max = f64::NEG_INFINITY;
for &v in pixels {
if v.is_finite() {
finite_count += 1;
if v < min {
min = v;
}
if v > max {
max = v;
}
}
}
if finite_count == 0 {
return None;
}
let mut non_nan_count: usize = 0;
let mut sum = 0.0_f64;
for &v in pixels {
if !v.is_nan() {
non_nan_count += 1;
sum += v;
}
}
let mean = sum / non_nan_count as f64;
let mut var_acc = 0.0_f64;
for &v in pixels {
if !v.is_nan() {
let d = v - mean;
var_acc += d * d;
}
}
let std = (var_acc / non_nan_count as f64).sqrt();
let guessed = (pixels.len() as f64).sqrt().floor() as usize;
let nbins = n_bins.unwrap_or_else(|| guessed.min(1024)).max(2);
let (g_min, g_max) = if min == max {
if min == 0.0 {
(-0.01, 0.01)
} else {
let a = min * 0.99;
let b = min * 1.01;
if a <= b { (a, b) } else { (b, a) }
}
} else {
(min, max)
};
let range = g_max - g_min;
let mut edges = Vec::with_capacity(nbins + 1);
for k in 0..nbins {
edges.push(g_min + k as f64 * (range / nbins as f64));
}
edges.push(g_max);
let mut counts = vec![0_u64; nbins];
for &v in pixels {
if !v.is_finite() {
continue; }
if v < g_min {
continue; }
let bin = if v < g_max {
(((v - g_min) * nbins as f64) / range) as usize
} else if v == g_max {
nbins - 1
} else {
continue; };
let bin = bin.min(nbins - 1);
counts[bin] += 1;
}
Some(PixelHistogram {
edges,
counts,
min,
max,
mean,
std,
sum,
n_bins: nbins,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn median_filter_2d_removes_salt_spike() {
let data = [
1.0, 1.0, 1.0, 1.0, 100.0, 1.0, 1.0, 1.0, 1.0,
];
let out = median_filter_2d(&data, 3, 3, 3, 3, false);
assert_eq!(out[4], 1.0, "salt spike at center should be removed");
}
#[test]
fn median_filter_2d_interior_median_is_sorted_middle() {
let data = [
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0,
];
let out = median_filter_2d(&data, 3, 3, 3, 3, false);
assert_eq!(out[4], 5.0);
}
#[test]
fn median_filter_2d_edge_nearest_clamping() {
let data = [
10.0, 20.0, 30.0, 40.0, 50.0, 60.0, 70.0, 80.0, 90.0,
];
let out = median_filter_2d(&data, 3, 3, 3, 3, false);
assert_eq!(out[0], 20.0, "corner pixel uses nearest-clamped window");
}
#[test]
fn median_filter_2d_constant_image_unchanged() {
let data = vec![7.0; 5 * 4];
let out = median_filter_2d(&data, 5, 4, 3, 3, false);
assert!(out.iter().all(|&v| v == 7.0));
}
#[test]
fn median_filter_2d_conditional_keeps_non_extremum_fixes_extremum() {
let data = [1.0, 2.0, 100.0, 4.0, 5.0];
let out = median_filter_2d(&data, 5, 1, 1, 3, true);
assert_eq!(out[2], 4.0, "extremum center replaced by window median");
assert_eq!(out[1], 2.0, "non-extremum center kept unchanged");
}
#[test]
fn median_filter_2d_unconditional_replaces_non_extremum() {
let data = [1.0, 2.0, 100.0, 4.0, 5.0];
let out = median_filter_2d(&data, 5, 1, 1, 3, false);
assert_eq!(out[1], 2.0);
assert_eq!(out[2], 4.0);
}
#[test]
fn median_filter_2d_nan_ignored_even_count_takes_higher_central() {
let data = [10.0, 20.0, f64::NAN, 40.0];
let out = median_filter_2d(&data, 4, 1, 1, 3, false);
assert_eq!(
out[1], 20.0,
"even valid count takes higher of two centrals"
);
}
#[test]
fn median_filter_2d_all_nan_window_is_nan() {
let data = [f64::NAN, f64::NAN, f64::NAN];
let out = median_filter_2d(&data, 3, 1, 1, 3, false);
assert!(out[1].is_nan(), "all-NaN window must produce NaN");
}
#[test]
fn median_filter_2d_conditional_nan_center_propagates() {
let data = [1.0, f64::NAN, 3.0];
let out = median_filter_2d(&data, 3, 1, 1, 3, true);
assert!(
out[1].is_nan(),
"NaN center is not an extremum, propagates unchanged"
);
}
#[test]
fn median_filter_1d_matches_2d_with_height_kernel() {
let data = [1.0, 2.0, 100.0, 4.0, 5.0];
let out = median_filter_1d(&data, 1, 5, 3, false);
assert_eq!(out[2], 4.0);
let direct = median_filter_2d(&data, 1, 5, 3, 1, false);
assert_eq!(out, direct);
}
#[test]
fn median_filter_2d_kernel_one_is_identity() {
let data = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0];
let out = median_filter_2d(&data, 3, 2, 1, 1, false);
assert_eq!(out, data.to_vec());
}
#[test]
fn histogram_default_bin_count_is_sqrt_floor() {
let pixels: Vec<f64> = (0..100).map(|i| i as f64).collect();
let h = pixel_intensity_histogram(&pixels, None).expect("finite pixels");
assert_eq!(h.n_bins, 10, "100 pixels -> floor(sqrt(100)) = 10 bins");
assert_eq!(h.counts.len(), 10);
assert_eq!(h.edges.len(), 11, "edges = n_bins + 1");
}
#[test]
fn histogram_exact_counts_last_bin_closed() {
let pixels: Vec<f64> = (0..=10).map(|i| i as f64).collect();
let h = pixel_intensity_histogram(&pixels, Some(5)).expect("finite pixels");
assert_eq!(h.n_bins, 5);
assert_eq!(h.counts, vec![2, 2, 2, 2, 3]);
assert_eq!(h.counts.iter().sum::<u64>(), 11, "every pixel is counted");
assert_eq!(h.min, 0.0);
assert_eq!(h.max, 10.0);
assert_eq!(h.edges[0], 0.0);
assert_eq!(*h.edges.last().unwrap(), 10.0);
}
#[test]
fn histogram_statistics_match_known_values() {
let pixels = [1.0, 2.0, 3.0, 4.0, 5.0];
let h = pixel_intensity_histogram(&pixels, Some(4)).expect("finite pixels");
assert_eq!(h.min, 1.0);
assert_eq!(h.max, 5.0);
assert_eq!(h.sum, 15.0);
assert_eq!(h.mean, 3.0);
assert!((h.std - 2.0_f64.sqrt()).abs() < 1e-12);
}
#[test]
fn histogram_stats_propagate_inf_while_range_excludes_it() {
let pixels = [0.0, 1.0, f64::NAN, 2.0, f64::INFINITY, 3.0];
let h = pixel_intensity_histogram(&pixels, Some(4)).expect("finite pixels");
assert_eq!(h.min, 0.0);
assert_eq!(h.max, 3.0);
assert_eq!(h.sum, f64::INFINITY);
assert_eq!(h.mean, f64::INFINITY);
assert!(h.std.is_nan());
assert_eq!(h.counts.iter().sum::<u64>(), 4);
assert_eq!(h.n_bins, 4);
}
#[test]
fn histogram_stats_skip_nan_and_stay_finite() {
let pixels = [0.0, 1.0, f64::NAN, 2.0, 3.0];
let h = pixel_intensity_histogram(&pixels, Some(4)).expect("finite pixels");
assert_eq!(h.sum, 6.0);
assert_eq!(h.mean, 1.5);
assert!((h.std - 1.25_f64.sqrt()).abs() < 1e-12);
assert_eq!(h.counts.iter().sum::<u64>(), 4);
}
#[test]
fn histogram_all_equal_degenerate_range() {
let pixels = [5.0; 8];
let h = pixel_intensity_histogram(&pixels, Some(4)).expect("finite pixels");
assert_eq!(h.min, 5.0);
assert_eq!(h.max, 5.0);
assert!((h.edges[0] - 4.95).abs() < 1e-12);
assert!((h.edges.last().unwrap() - 5.05).abs() < 1e-12);
assert_eq!(h.counts.iter().sum::<u64>(), 8);
}
#[test]
fn histogram_all_zero_degenerate_range() {
let pixels = [0.0; 4];
let h = pixel_intensity_histogram(&pixels, Some(2)).expect("finite pixels");
assert_eq!(h.min, 0.0);
assert_eq!(h.max, 0.0);
assert!((h.edges[0] - (-0.01)).abs() < 1e-12);
assert!((h.edges.last().unwrap() - 0.01).abs() < 1e-12);
assert_eq!(h.counts.iter().sum::<u64>(), 4);
}
#[test]
fn histogram_all_non_finite_is_none() {
let pixels = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
assert!(pixel_intensity_histogram(&pixels, None).is_none());
assert!(pixel_intensity_histogram(&[], None).is_none());
}
#[test]
fn histogram_bin_count_floored_at_two() {
let h = pixel_intensity_histogram(&[3.0, f64::NAN], None).expect("one finite pixel");
assert_eq!(h.n_bins, 2);
}
#[test]
fn histogram_bin_guess_counts_nan_pixels() {
let pixels: Vec<f64> = (0..100)
.map(|i| if i < 36 { f64::NAN } else { i as f64 })
.collect();
let h = pixel_intensity_histogram(&pixels, None).expect("finite pixels present");
assert_eq!(h.n_bins, 10, "100 total elements -> floor(sqrt(100)) = 10");
assert_eq!(
h.counts.iter().sum::<u64>(),
64,
"only the finite pixels are counted"
);
assert_eq!(h.min, 36.0, "range comes from the finite pixels only");
}
#[test]
fn histogram_interior_floor_formula() {
let pixels = [0.0, 2.5, 4.0];
let h = pixel_intensity_histogram(&pixels, Some(4)).expect("finite pixels");
assert_eq!(h.counts, vec![1, 0, 1, 1]);
}
}