#[derive(Debug, Clone, PartialEq)]
pub struct IntensityFeatures {
pub mean: f64,
pub variance: f64,
pub skewness: f64,
pub kurtosis: f64,
pub entropy: f64,
pub uniformity: f64,
pub energy: f64,
pub median: f64,
pub p10: f64,
pub p25: f64,
pub p75: f64,
pub p90: f64,
pub iqr: f64,
pub range: f64,
pub mean_abs_deviation: f64,
pub robust_mean_abs_deviation: f64,
pub coefficient_variation: f64,
pub quartile_coef_dispersion: f64,
}
pub fn compute_intensity_features(image: &[f64], mask: &[bool]) -> Option<IntensityFeatures> {
if image.len() != mask.len() {
return None;
}
let values: Vec<f64> = image
.iter()
.zip(mask.iter())
.filter_map(|(&v, &m)| if m { Some(v) } else { None })
.collect();
let n = values.len();
if n == 0 {
return None;
}
let sum: f64 = values.iter().sum();
let mean = sum / n as f64;
let variance: f64 = values.iter().map(|&x| (x - mean).powi(2)).sum::<f64>() / n as f64;
let std = variance.sqrt();
let skewness = if std > 0.0 {
let m3 = values.iter().map(|&x| (x - mean).powi(3)).sum::<f64>() / n as f64;
m3 / (std * std * std)
} else {
0.0
};
let kurtosis = if std > 0.0 {
let m4 = values.iter().map(|&x| (x - mean).powi(4)).sum::<f64>() / n as f64;
m4 / (std * std * std * std) - 3.0
} else {
-3.0
};
let energy: f64 = values.iter().map(|&x| x * x).sum();
let min_val = values.iter().cloned().fold(f64::INFINITY, f64::min);
let max_val = values.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
const N_BINS: usize = 64;
let bin_width = if (max_val - min_val).abs() < 1e-15 {
1.0 } else {
(max_val - min_val) / N_BINS as f64
};
let mut bins = vec![0usize; N_BINS];
for &v in &values {
let idx = ((v - min_val) / bin_width).floor() as usize;
let idx = idx.min(N_BINS - 1);
bins[idx] += 1;
}
let nf = n as f64;
let probs: Vec<f64> = bins.iter().map(|&c| c as f64 / nf).collect();
let epsilon = 1e-15;
let entropy: f64 = probs
.iter()
.filter(|&&p| p > 0.0)
.map(|&p| -p * (p + epsilon).log2())
.sum();
let uniformity: f64 = probs.iter().map(|&p| p * p).sum();
let mut sorted = values.clone();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let percentile = |frac: f64| -> f64 {
let pos = frac * (n - 1) as f64;
let lo = pos.floor() as usize;
let hi = (lo + 1).min(n - 1);
let frac_part = pos - lo as f64;
sorted[lo] * (1.0 - frac_part) + sorted[hi] * frac_part
};
let median = percentile(0.5);
let p10 = percentile(0.10);
let p25 = percentile(0.25);
let p75 = percentile(0.75);
let p90 = percentile(0.90);
let iqr = p75 - p25;
let range = max_val - min_val;
let mean_abs_deviation: f64 = values.iter().map(|&x| (x - mean).abs()).sum::<f64>() / nf;
let mut abs_dev_from_median: Vec<f64> = values.iter().map(|&x| (x - median).abs()).collect();
abs_dev_from_median.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let robust_mean_abs_deviation = {
let m = abs_dev_from_median.len();
if m == 0 {
0.0
} else {
let pos = 0.5 * (m - 1) as f64;
let lo = pos.floor() as usize;
let hi = (lo + 1).min(m - 1);
let fp = pos - lo as f64;
abs_dev_from_median[lo] * (1.0 - fp) + abs_dev_from_median[hi] * fp
}
};
let coefficient_variation = if mean.abs() > 1e-15 {
std / mean.abs()
} else {
f64::NAN
};
let quartile_coef_dispersion = {
let denom = p75 + p25;
if denom.abs() > 1e-15 {
iqr / denom
} else {
f64::NAN
}
};
Some(IntensityFeatures {
mean,
variance,
skewness,
kurtosis,
entropy,
uniformity,
energy,
median,
p10,
p25,
p75,
p90,
iqr,
range,
mean_abs_deviation,
robust_mean_abs_deviation,
coefficient_variation,
quartile_coef_dispersion,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_basic_stats_known_array() {
let image = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let mask = vec![true; 5];
let f = compute_intensity_features(&image, &mask).expect("should return features");
assert!(approx_eq(f.mean, 3.0, 1e-10), "mean = {}", f.mean);
assert!(
approx_eq(f.variance, 2.0, 1e-10),
"variance = {}",
f.variance
);
assert!(approx_eq(f.range, 4.0, 1e-10), "range = {}", f.range);
assert!(approx_eq(f.median, 3.0, 1e-10), "median = {}", f.median);
assert!(approx_eq(f.iqr, 2.0, 0.01), "iqr = {}", f.iqr);
}
#[test]
fn test_kurtosis_uniform_approx() {
let image = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let mask = vec![true; 5];
let f = compute_intensity_features(&image, &mask).expect("features");
assert!(f.kurtosis.is_finite(), "kurtosis must be finite");
}
#[test]
fn test_entropy_positive() {
let image = vec![1.0f64, 2.0, 3.0, 4.0, 5.0];
let mask = vec![true; 5];
let f = compute_intensity_features(&image, &mask).expect("features");
assert!(
f.entropy > 0.0,
"entropy should be positive for varied values"
);
}
#[test]
fn test_energy() {
let image = vec![1.0f64, 2.0, 3.0];
let mask = vec![true; 3];
let f = compute_intensity_features(&image, &mask).expect("features");
assert!(approx_eq(f.energy, 14.0, 1e-10), "energy = {}", f.energy);
}
#[test]
fn test_mask_selects_subset() {
let image = vec![10.0f64, 1.0, 2.0, 3.0, 20.0];
let mask = vec![false, true, true, true, false];
let f = compute_intensity_features(&image, &mask).expect("features");
assert!(approx_eq(f.mean, 2.0, 1e-10), "mean = {}", f.mean);
}
#[test]
fn test_empty_mask_returns_none() {
let image = vec![1.0f64, 2.0];
let mask = vec![false, false];
assert!(compute_intensity_features(&image, &mask).is_none());
}
#[test]
fn test_constant_signal() {
let image = vec![5.0f64; 10];
let mask = vec![true; 10];
let f = compute_intensity_features(&image, &mask).expect("features");
assert!(approx_eq(f.mean, 5.0, 1e-10));
assert!(approx_eq(f.variance, 0.0, 1e-10));
assert!(approx_eq(f.skewness, 0.0, 1e-10));
}
}