use super::simd_accelerated;
use crate::*;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
#[derive(Debug, Clone, Copy)]
pub enum WaveletType {
Haar,
Daubechies(usize),
Biorthogonal,
Coiflets,
}
#[derive(Debug, Clone, Copy)]
pub enum DecompositionMode {
SingleLevel,
MultiLevel(usize),
WaveletPacket,
}
#[derive(Debug, Clone)]
pub struct FeatureConfig {
pub energy: bool,
pub entropy: bool,
pub moments: bool,
pub contrast: bool,
pub homogeneity: bool,
pub correlation: bool,
}
impl Default for FeatureConfig {
fn default() -> Self {
Self {
energy: true,
entropy: true,
moments: true,
contrast: true,
homogeneity: true,
correlation: false, }
}
}
#[derive(Debug, Clone)]
pub struct WaveletFeatureExtractor {
wavelet_type: WaveletType,
decomposition_levels: usize,
decomposition_mode: DecompositionMode,
feature_config: FeatureConfig,
normalize_features: bool,
window_size: usize,
feature_type_selection: Option<crate::time_series_features::WaveletFeatureType>,
}
impl WaveletFeatureExtractor {
pub fn new() -> Self {
Self {
wavelet_type: WaveletType::Haar,
decomposition_levels: 3,
decomposition_mode: DecompositionMode::MultiLevel(3),
feature_config: FeatureConfig::default(),
normalize_features: true,
window_size: 3,
feature_type_selection: None,
}
}
pub fn wavelet_type(mut self, wavelet_type: WaveletType) -> Self {
self.wavelet_type = wavelet_type;
self
}
pub fn decomposition_levels(mut self, levels: usize) -> Self {
self.decomposition_levels = levels.clamp(1, 8);
self.decomposition_mode = DecompositionMode::MultiLevel(levels);
self
}
pub fn wavelet_levels(self, levels: usize) -> Self {
self.decomposition_levels(levels)
}
pub fn feature_type(
mut self,
feature_type: crate::time_series_features::WaveletFeatureType,
) -> Self {
self.feature_type_selection = Some(feature_type);
self
}
pub fn decomposition_mode(mut self, mode: DecompositionMode) -> Self {
self.decomposition_mode = mode;
self
}
pub fn feature_config(mut self, config: FeatureConfig) -> Self {
self.feature_config = config;
self
}
pub fn enable_all_features(mut self) -> Self {
self.feature_config = FeatureConfig {
energy: true,
entropy: true,
moments: true,
contrast: true,
homogeneity: true,
correlation: true,
};
self
}
pub fn normalize_features(mut self, normalize: bool) -> Self {
self.normalize_features = normalize;
self
}
pub fn window_size(mut self, size: usize) -> Self {
self.window_size = size.clamp(3, 15) | 1; self
}
pub fn extract_features(&self, image: &ArrayView2<Float>) -> SklResult<Array1<Float>> {
let (height, width) = image.dim();
if height < 8 || width < 8 {
return Err(SklearsError::InvalidInput(
"Image too small for wavelet analysis (minimum 8x8)".to_string(),
));
}
let coefficients = self.wavelet_decomposition(image)?;
let mut features = Vec::new();
let last_level = coefficients.len().saturating_sub(1);
for (level, subbands) in coefficients.iter().enumerate() {
for (band_name, coeffs) in subbands {
let is_approximation = band_name == "LL";
if is_approximation && level != last_level {
continue;
}
let band_features = self.extract_subband_features(coeffs, level, band_name)?;
features.extend(band_features);
}
}
let mut feature_vector = Array1::from_vec(features);
if self.normalize_features {
self.normalize_feature_vector(&mut feature_vector);
}
Ok(feature_vector)
}
fn wavelet_decomposition(
&self,
image: &ArrayView2<Float>,
) -> SklResult<Vec<std::collections::HashMap<String, Array2<Float>>>> {
let mut coefficients = Vec::new();
let mut current_image = image.to_owned();
match self.decomposition_mode {
DecompositionMode::SingleLevel => {
let subbands = self.single_level_decomposition(¤t_image.view())?;
coefficients.push(subbands);
}
DecompositionMode::MultiLevel(levels) => {
for _level in 0..levels {
let subbands = self.single_level_decomposition(¤t_image.view())?;
if let Some(approx) = subbands.get("LL") {
current_image = approx.clone();
}
coefficients.push(subbands);
if current_image.nrows() < 4 || current_image.ncols() < 4 {
break;
}
}
}
DecompositionMode::WaveletPacket => {
let subbands = self.single_level_decomposition(¤t_image.view())?;
coefficients.push(subbands);
}
}
Ok(coefficients)
}
fn single_level_decomposition(
&self,
image: &ArrayView2<Float>,
) -> SklResult<std::collections::HashMap<String, Array2<Float>>> {
let (height, width) = image.dim();
let new_height = height / 2;
let new_width = width / 2;
let mut subbands = std::collections::HashMap::new();
match self.wavelet_type {
WaveletType::Haar => {
subbands.insert(
"LL".to_string(),
self.haar_decomposition_ll(image, new_height, new_width),
);
subbands.insert(
"LH".to_string(),
self.haar_decomposition_lh(image, new_height, new_width),
);
subbands.insert(
"HL".to_string(),
self.haar_decomposition_hl(image, new_height, new_width),
);
subbands.insert(
"HH".to_string(),
self.haar_decomposition_hh(image, new_height, new_width),
);
}
WaveletType::Daubechies(_order) => {
subbands.insert(
"LL".to_string(),
self.haar_decomposition_ll(image, new_height, new_width),
);
subbands.insert(
"LH".to_string(),
self.haar_decomposition_lh(image, new_height, new_width),
);
subbands.insert(
"HL".to_string(),
self.haar_decomposition_hl(image, new_height, new_width),
);
subbands.insert(
"HH".to_string(),
self.haar_decomposition_hh(image, new_height, new_width),
);
}
_ => {
subbands.insert(
"LL".to_string(),
self.haar_decomposition_ll(image, new_height, new_width),
);
subbands.insert(
"LH".to_string(),
self.haar_decomposition_lh(image, new_height, new_width),
);
subbands.insert(
"HL".to_string(),
self.haar_decomposition_hl(image, new_height, new_width),
);
subbands.insert(
"HH".to_string(),
self.haar_decomposition_hh(image, new_height, new_width),
);
}
}
Ok(subbands)
}
fn haar_decomposition_ll(
&self,
image: &ArrayView2<Float>,
new_height: usize,
new_width: usize,
) -> Array2<Float> {
let mut ll = Array2::zeros((new_height, new_width));
for y in 0..new_height {
for x in 0..new_width {
ll[[y, x]] = (image[[2 * y, 2 * x]]
+ image[[2 * y, 2 * x + 1]]
+ image[[2 * y + 1, 2 * x]]
+ image[[2 * y + 1, 2 * x + 1]])
/ 4.0;
}
}
ll
}
fn haar_decomposition_lh(
&self,
image: &ArrayView2<Float>,
new_height: usize,
new_width: usize,
) -> Array2<Float> {
let mut lh = Array2::zeros((new_height, new_width));
for y in 0..new_height {
for x in 0..new_width {
lh[[y, x]] = (image[[2 * y, 2 * x]] - image[[2 * y, 2 * x + 1]]
+ image[[2 * y + 1, 2 * x]]
- image[[2 * y + 1, 2 * x + 1]])
/ 4.0;
}
}
lh
}
fn haar_decomposition_hl(
&self,
image: &ArrayView2<Float>,
new_height: usize,
new_width: usize,
) -> Array2<Float> {
let mut hl = Array2::zeros((new_height, new_width));
for y in 0..new_height {
for x in 0..new_width {
hl[[y, x]] = (image[[2 * y, 2 * x]] + image[[2 * y, 2 * x + 1]]
- image[[2 * y + 1, 2 * x]]
- image[[2 * y + 1, 2 * x + 1]])
/ 4.0;
}
}
hl
}
fn haar_decomposition_hh(
&self,
image: &ArrayView2<Float>,
new_height: usize,
new_width: usize,
) -> Array2<Float> {
let mut hh = Array2::zeros((new_height, new_width));
for y in 0..new_height {
for x in 0..new_width {
hh[[y, x]] =
(image[[2 * y, 2 * x]] - image[[2 * y, 2 * x + 1]] - image[[2 * y + 1, 2 * x]]
+ image[[2 * y + 1, 2 * x + 1]])
/ 4.0;
}
}
hh
}
fn extract_subband_features(
&self,
coefficients: &Array2<Float>,
level: usize,
band_name: &str,
) -> SklResult<Vec<Float>> {
let _ = level;
let _ = band_name;
let values: Vec<f64> = coefficients.iter().cloned().collect();
if values.is_empty() {
return Ok(Vec::new());
}
let mean = values.iter().sum::<f64>() / values.len() as f64;
let variance = values.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / values.len() as f64;
let std_dev = variance.sqrt();
let energy = values.iter().map(|x| x * x).sum::<f64>();
let entropy = self.compute_entropy(&values)?;
let skewness = self.compute_skewness(&values, mean, std_dev);
let kurtosis = self.compute_kurtosis(&values, mean, std_dev);
let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let contrast = self.compute_contrast(coefficients);
let homogeneity = self.compute_homogeneity(coefficients);
if let Some(feature_type) = self.feature_type_selection {
let features = match feature_type {
crate::time_series_features::WaveletFeatureType::Basic => {
vec![mean, std_dev, energy, entropy]
}
crate::time_series_features::WaveletFeatureType::Extended => vec![
mean,
std_dev,
energy,
entropy,
skewness,
kurtosis,
contrast,
homogeneity,
],
crate::time_series_features::WaveletFeatureType::Mean => vec![mean],
crate::time_series_features::WaveletFeatureType::Variance => vec![variance],
crate::time_series_features::WaveletFeatureType::Energy => vec![energy],
crate::time_series_features::WaveletFeatureType::Entropy => vec![entropy],
crate::time_series_features::WaveletFeatureType::Skewness => vec![skewness],
crate::time_series_features::WaveletFeatureType::Kurtosis => vec![kurtosis],
crate::time_series_features::WaveletFeatureType::MaxCoeff => vec![max_val],
crate::time_series_features::WaveletFeatureType::MinCoeff => vec![min_val],
};
return Ok(features);
}
let mut features = Vec::new();
if self.feature_config.energy {
let normalized_energy = energy / values.len() as f64;
features.push(energy);
features.push(normalized_energy);
}
if self.feature_config.entropy {
features.push(entropy);
}
if self.feature_config.moments {
features.push(mean);
features.push(variance);
features.push(skewness);
features.push(kurtosis);
features.push(max_val);
features.push(min_val);
}
if self.feature_config.contrast {
features.push(contrast);
}
if self.feature_config.homogeneity {
features.push(homogeneity);
}
if self.feature_config.correlation {
let correlation = self.compute_correlation(coefficients);
features.push(correlation);
}
Ok(features)
}
fn compute_entropy(&self, values: &[f64]) -> SklResult<f64> {
let mut histogram = vec![0; 256];
let min_val = values.iter().fold(f64::INFINITY, |a, &b| a.min(b));
let max_val = values.iter().fold(f64::NEG_INFINITY, |a, &b| a.max(b));
if (max_val - min_val).abs() < f64::EPSILON {
return Ok(0.0);
}
for &val in values {
let bin = ((val - min_val) / (max_val - min_val) * 255.0) as usize;
histogram[bin.min(255)] += 1;
}
let total = values.len() as f64;
let probabilities: Vec<f64> = histogram
.iter()
.map(|&count| if count > 0 { count as f64 / total } else { 0.0 })
.filter(|&p| p > 0.0)
.collect();
Ok(compute_entropy_fallback(&probabilities))
}
fn compute_skewness(&self, values: &[f64], mean: f64, std_dev: f64) -> f64 {
if std_dev < f64::EPSILON {
return 0.0;
}
compute_skewness_fallback(values, mean, std_dev)
}
fn compute_kurtosis(&self, values: &[f64], mean: f64, std_dev: f64) -> f64 {
if std_dev < f64::EPSILON {
return 0.0;
}
simd_accelerated::simd_compute_kurtosis(values, mean, std_dev)
}
fn compute_contrast(&self, coefficients: &Array2<Float>) -> f64 {
let (height, width) = coefficients.dim();
let mut contrast = 0.0;
let mut count = 0;
for y in 1..height - 1 {
for x in 1..width - 1 {
let center = coefficients[[y, x]];
let neighbors = [
coefficients[[y - 1, x - 1]],
coefficients[[y - 1, x]],
coefficients[[y - 1, x + 1]],
coefficients[[y, x - 1]],
coefficients[[y, x + 1]],
coefficients[[y + 1, x - 1]],
coefficients[[y + 1, x]],
coefficients[[y + 1, x + 1]],
];
for &neighbor in &neighbors {
contrast += (center - neighbor).abs();
count += 1;
}
}
}
if count > 0 {
contrast / count as f64
} else {
0.0
}
}
fn compute_homogeneity(&self, coefficients: &Array2<Float>) -> f64 {
let (height, width) = coefficients.dim();
let mut homogeneity = 0.0;
let mut count = 0;
for y in 1..height - 1 {
for x in 1..width - 1 {
let center = coefficients[[y, x]];
let neighbors = [
coefficients[[y - 1, x - 1]],
coefficients[[y - 1, x]],
coefficients[[y - 1, x + 1]],
coefficients[[y, x - 1]],
coefficients[[y, x + 1]],
coefficients[[y + 1, x - 1]],
coefficients[[y + 1, x]],
coefficients[[y + 1, x + 1]],
];
for &neighbor in &neighbors {
homogeneity += 1.0 / (1.0 + (center - neighbor).abs());
count += 1;
}
}
}
if count > 0 {
homogeneity / count as f64
} else {
0.0
}
}
fn compute_correlation(&self, coefficients: &Array2<Float>) -> f64 {
let (height, width) = coefficients.dim();
if height < 2 || width < 2 {
return 0.0;
}
let mut sum_x = 0.0;
let mut sum_y = 0.0;
let mut sum_xy = 0.0;
let mut sum_x2 = 0.0;
let mut sum_y2 = 0.0;
let mut count = 0;
for y in 0..height {
for x in 0..width - 1 {
let x_val = coefficients[[y, x]];
let y_val = coefficients[[y, x + 1]];
sum_x += x_val;
sum_y += y_val;
sum_xy += x_val * y_val;
sum_x2 += x_val * x_val;
sum_y2 += y_val * y_val;
count += 1;
}
}
if count == 0 {
return 0.0;
}
let n = count as f64;
let numerator = n * sum_xy - sum_x * sum_y;
let denominator = ((n * sum_x2 - sum_x * sum_x) * (n * sum_y2 - sum_y * sum_y)).sqrt();
if denominator.abs() < f64::EPSILON {
0.0
} else {
numerator / denominator
}
}
fn normalize_feature_vector(&self, features: &mut Array1<Float>) {
let norm = features.iter().map(|x| x * x).sum::<Float>().sqrt();
if norm > Float::EPSILON {
features.mapv_inplace(|x| x / norm);
}
}
}
impl Default for WaveletFeatureExtractor {
fn default() -> Self {
Self::new()
}
}
fn compute_entropy_fallback(probabilities: &[f64]) -> f64 {
let mut entropy = 0.0;
for &prob in probabilities {
if prob > 0.0 {
entropy -= prob * prob.log2();
}
}
entropy
}
fn compute_skewness_fallback(values: &[f64], mean: f64, std_dev: f64) -> f64 {
if std_dev == 0.0 {
return 0.0;
}
let n = values.len() as f64;
let sum: f64 = values.iter().map(|&x| ((x - mean) / std_dev).powi(3)).sum();
sum / n
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_wavelet_extractor_creation() {
let extractor = WaveletFeatureExtractor::new();
assert_eq!(extractor.decomposition_levels, 3);
assert!(matches!(extractor.wavelet_type, WaveletType::Haar));
assert!(extractor.normalize_features);
}
#[test]
fn test_wavelet_extractor_builder() {
let extractor = WaveletFeatureExtractor::new()
.wavelet_type(WaveletType::Daubechies(4))
.decomposition_levels(2)
.normalize_features(false);
assert_eq!(extractor.decomposition_levels, 2);
assert!(matches!(extractor.wavelet_type, WaveletType::Daubechies(4)));
assert!(!extractor.normalize_features);
}
#[test]
fn test_extract_features_small_image() {
let image = Array2::from_shape_vec((4, 4), (0..16).map(|x| x as f64).collect())
.expect("operation should succeed");
let extractor = WaveletFeatureExtractor::new();
let result = extractor.extract_features(&image.view());
assert!(result.is_err()); }
#[test]
fn test_extract_features_valid_image() {
let image = Array2::from_shape_vec((16, 16), (0..256).map(|x| x as f64 / 256.0).collect())
.expect("operation should succeed");
let extractor = WaveletFeatureExtractor::new();
let features = extractor
.extract_features(&image.view())
.expect("operation should succeed");
assert!(!features.is_empty());
assert!(features.len() >= 20); }
#[test]
fn test_haar_decomposition() {
let extractor = WaveletFeatureExtractor::new();
let image = Array2::from_shape_vec(
(4, 4),
vec![
1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0,
16.0,
],
)
.expect("operation should succeed");
let subbands = extractor
.single_level_decomposition(&image.view())
.expect("operation should succeed");
assert_eq!(subbands.len(), 4); assert!(subbands.contains_key("LL"));
assert!(subbands.contains_key("LH"));
assert!(subbands.contains_key("HL"));
assert!(subbands.contains_key("HH"));
if let Some(ll) = subbands.get("LL") {
assert_eq!(ll.dim(), (2, 2));
}
}
#[test]
fn test_entropy_computation() {
let extractor = WaveletFeatureExtractor::new();
let values = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let entropy = extractor
.compute_entropy(&values)
.expect("operation should succeed");
assert!(entropy >= 0.0);
assert!(entropy.is_finite());
}
#[test]
fn test_contrast_computation() {
let extractor = WaveletFeatureExtractor::new();
let coefficients =
Array2::from_shape_vec((3, 3), vec![1.0, 5.0, 1.0, 5.0, 9.0, 5.0, 1.0, 5.0, 1.0])
.expect("operation should succeed");
let contrast = extractor.compute_contrast(&coefficients);
assert!(contrast > 0.0); }
#[test]
fn test_homogeneity_computation() {
let extractor = WaveletFeatureExtractor::new();
let coefficients = Array2::ones((5, 5));
let homogeneity = extractor.compute_homogeneity(&coefficients);
assert_eq!(homogeneity, 1.0); }
#[test]
fn test_feature_normalization() {
let extractor = WaveletFeatureExtractor::new();
let mut features = Array1::from_vec(vec![3.0, 4.0, 0.0]);
extractor.normalize_feature_vector(&mut features);
let norm: f64 = features.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!((norm - 1.0).abs() < 1e-10);
}
#[test]
fn test_feature_config() {
let config = FeatureConfig {
energy: true,
entropy: false,
moments: true,
contrast: false,
homogeneity: false,
correlation: false,
};
let extractor = WaveletFeatureExtractor::new().feature_config(config);
assert!(extractor.feature_config.energy);
assert!(!extractor.feature_config.entropy);
assert!(extractor.feature_config.moments);
}
}