use crate::models::Hill;
const SMOOTH_WINDOW: usize = 1;
pub fn filter_large_baseline_hills(
hills: Vec<Hill>,
min_scans: usize,
peak_factor: f64,
) -> Vec<Hill> {
let n_before = hills.len();
let filtered: Vec<Hill> = hills
.into_iter()
.filter(|h| keep_hill(h, min_scans, peak_factor))
.collect();
let n_after = filtered.len();
log::info!(
"filter_large_baseline_hills: kept {}/{} hills ({} dropped as baseline-like)",
n_after,
n_before,
n_before - n_after,
);
filtered
}
fn keep_hill(hill: &Hill, min_scans: usize, peak_factor: f64) -> bool {
let profile = &hill.intensity_profile;
let n = profile.len();
if n < min_scans {
return true;
}
let smoothed = median_then_mean_smooth(profile, SMOOTH_WINDOW);
let max_unsmoothed: f32 = profile.iter().copied().fold(0.0f32, f32::max);
if max_unsmoothed <= 0.0 {
return true;
}
let edge_start = smoothed[0];
let edge_end = smoothed[n - 1];
if edge_start <= 0.0 || edge_end <= 0.0 {
return true;
}
let ratio_start = max_unsmoothed as f64 / edge_start as f64;
let ratio_end = max_unsmoothed as f64 / edge_end as f64;
ratio_start > peak_factor && ratio_end > peak_factor
}
fn median_then_mean_smooth(profile: &[f32], window: usize) -> Vec<f32> {
let n = profile.len();
let mut buf: Vec<f32> = profile.to_vec();
if n == 0 {
return buf;
}
let mut scratch: Vec<f32> = Vec::with_capacity(2 * window + 1);
for i in 0..n {
let lo = i.saturating_sub(window);
let hi = (i + window + 1).min(n);
scratch.clear();
scratch.extend_from_slice(&buf[lo..hi]);
scratch.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let m = scratch.len();
buf[i] = if m % 2 == 1 {
scratch[m / 2]
} else {
0.5 * (scratch[m / 2 - 1] + scratch[m / 2])
};
}
for i in 0..n {
let lo = i.saturating_sub(window);
let hi = (i + window + 1).min(n);
let sum: f32 = buf[lo..hi].iter().sum();
buf[i] = sum / (hi - lo) as f32;
}
buf
}
#[cfg(test)]
#[path = "filter_tests.rs"]
mod tests;