pub mod glszm;
pub mod laws;
pub mod legacy;
pub mod ngtdm;
pub mod rlm;
pub use glszm::{compute_glszm, glszm_features, GlszmConnectivity, GlszmFeatures, GlszmResult};
pub use laws::{
laws_all_kernels, laws_rotation_invariant_features, laws_texture_energy_full, LawsFullConfig,
LawsFullResult, LawsKernelName, LawsVector as LawsVectorFull,
};
pub use ngtdm::{compute_ngtdm, ngtdm_features, NgtdmFeatures, NgtdmResult};
pub use rlm::{compute_rlm, rlm_features, RlmDirection, RlmFeatures, RlmResult};
pub use legacy::{
compute_glcm, gabor_filter_bank as legacy_gabor_filter_bank, glcm_features,
glcm_features_from_image, laws_texture_energy as legacy_laws_texture_energy, lbp_basic,
lbp_histogram, lbp_rotation_invariant, lbp_uniform, quantize_image, GaborBankConfig,
GaborBankResult, GlcmFeatures, GlcmOffset, LawsConfig, LawsResult,
LawsVector as LawsVectorLegacy,
};
use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::Array2;
#[derive(Debug, Clone)]
pub struct QuantizationConfig {
pub n_levels: usize,
pub min_val: Option<f64>,
pub max_val: Option<f64>,
}
impl Default for QuantizationConfig {
fn default() -> Self {
Self {
n_levels: 8,
min_val: None,
max_val: None,
}
}
}
pub fn quantize_image_u8(
image: &Array2<f64>,
config: &QuantizationConfig,
) -> NdimageResult<Array2<u8>> {
if config.n_levels < 2 {
return Err(NdimageError::InvalidInput(
"n_levels must be at least 2".into(),
));
}
let (rows, cols) = image.dim();
if rows == 0 || cols == 0 {
return Err(NdimageError::InvalidInput("Image must not be empty".into()));
}
if config.n_levels > 256 {
return Err(NdimageError::InvalidInput(
"n_levels must be <= 256 for u8 quantization".into(),
));
}
let i_min = config
.min_val
.unwrap_or_else(|| image.iter().copied().fold(f64::INFINITY, f64::min));
let i_max = config
.max_val
.unwrap_or_else(|| image.iter().copied().fold(f64::NEG_INFINITY, f64::max));
let range = i_max - i_min;
if range < 1e-15 {
return Ok(Array2::zeros(image.dim()));
}
let scale = (config.n_levels as f64 - 1.0) / range;
let max_level = (config.n_levels - 1) as f64;
let result = image.mapv(|v| {
let scaled = ((v - i_min) * scale).round().clamp(0.0, max_level);
scaled as u8
});
Ok(result)
}
#[derive(Debug, Clone, Default)]
pub struct TextureFeatures {
pub rlm: Option<RlmFeatures>,
pub glszm: Option<GlszmFeatures>,
pub ngtdm: Option<NgtdmFeatures>,
pub laws_features: Option<Vec<f64>>,
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
#[test]
fn test_quantize_image_u8_basic() {
let img = Array2::from_shape_fn((4, 4), |(i, j)| (i * 4 + j) as f64);
let config = QuantizationConfig {
n_levels: 4,
..Default::default()
};
let q = quantize_image_u8(&img, &config).expect("quantize failed");
assert!(q.iter().all(|&v| v < 4));
assert_eq!(q[[0, 0]], 0);
assert_eq!(q[[3, 3]], 3);
}
#[test]
fn test_quantize_image_u8_uniform() {
let img = Array2::from_elem((4, 4), 42.0);
let config = QuantizationConfig::default();
let q = quantize_image_u8(&img, &config).expect("quantize failed");
assert!(q.iter().all(|&v| v == 0));
}
#[test]
fn test_quantize_image_u8_custom_range() {
let img = Array2::from_shape_fn((4, 4), |(i, j)| (i * 4 + j) as f64);
let config = QuantizationConfig {
n_levels: 16,
min_val: Some(0.0),
max_val: Some(15.0),
};
let q = quantize_image_u8(&img, &config).expect("quantize failed");
assert_eq!(q[[0, 0]], 0);
assert_eq!(q[[3, 3]], 15);
}
#[test]
fn test_quantize_image_u8_errors() {
let img = Array2::from_elem((4, 4), 0.0);
let config = QuantizationConfig {
n_levels: 1,
..Default::default()
};
assert!(quantize_image_u8(&img, &config).is_err());
let empty = Array2::<f64>::zeros((0, 0));
let config2 = QuantizationConfig::default();
assert!(quantize_image_u8(&empty, &config2).is_err());
}
#[test]
fn test_texture_features_default() {
let tf = TextureFeatures::default();
assert!(tf.rlm.is_none());
assert!(tf.glszm.is_none());
assert!(tf.ngtdm.is_none());
assert!(tf.laws_features.is_none());
}
}