use crate::error::{NdimageError, NdimageResult};
use scirs2_core::ndarray::Array2;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LawsVector {
L5,
E5,
S5,
W5,
R5,
}
impl LawsVector {
pub fn coefficients(&self) -> [f64; 5] {
match self {
LawsVector::L5 => [1.0, 4.0, 6.0, 4.0, 1.0],
LawsVector::E5 => [-1.0, -2.0, 0.0, 2.0, 1.0],
LawsVector::S5 => [-1.0, 0.0, 2.0, 0.0, -1.0],
LawsVector::W5 => [-1.0, 2.0, 0.0, -2.0, 1.0],
LawsVector::R5 => [1.0, -4.0, 6.0, -4.0, 1.0],
}
}
pub fn all() -> [LawsVector; 5] {
[
LawsVector::L5,
LawsVector::E5,
LawsVector::S5,
LawsVector::W5,
LawsVector::R5,
]
}
pub fn name(&self) -> &'static str {
match self {
LawsVector::L5 => "L5",
LawsVector::E5 => "E5",
LawsVector::S5 => "S5",
LawsVector::W5 => "W5",
LawsVector::R5 => "R5",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LawsKernelName {
pub row: LawsVector,
pub col: LawsVector,
}
impl LawsKernelName {
pub fn new(row: LawsVector, col: LawsVector) -> Self {
Self { row, col }
}
pub fn display_name(&self) -> String {
format!("{}{}", self.row.name(), self.col.name())
}
}
#[derive(Debug, Clone)]
pub struct LawsFullConfig {
pub pairs: Vec<LawsKernelName>,
pub window_size: usize,
pub remove_dc: bool,
}
impl Default for LawsFullConfig {
fn default() -> Self {
Self {
pairs: Vec::new(), window_size: 15,
remove_dc: true,
}
}
}
#[derive(Debug, Clone)]
pub struct LawsFullResult {
pub kernel_names: Vec<LawsKernelName>,
pub energy_maps: Vec<Array2<f64>>,
pub feature_vector: Vec<f64>,
}
pub fn laws_all_kernels() -> Vec<(LawsKernelName, Array2<f64>)> {
let vecs = LawsVector::all();
let mut kernels = Vec::with_capacity(25);
for &row_v in &vecs {
let r = row_v.coefficients();
for &col_v in &vecs {
let c = col_v.coefficients();
let mut kernel = Array2::<f64>::zeros((5, 5));
for i in 0..5 {
for j in 0..5 {
kernel[[i, j]] = r[i] * c[j];
}
}
kernels.push((LawsKernelName::new(row_v, col_v), kernel));
}
}
kernels
}
pub fn laws_texture_energy_full(
image: &Array2<f64>,
config: Option<LawsFullConfig>,
) -> NdimageResult<LawsFullResult> {
let cfg = config.unwrap_or_default();
let (ny, nx) = image.dim();
if ny < 5 || nx < 5 {
return Err(NdimageError::InvalidInput(
"Image must be at least 5x5 for Laws filters".into(),
));
}
if cfg.window_size == 0 || cfg.window_size % 2 == 0 {
return Err(NdimageError::InvalidInput(
"window_size must be an odd positive integer".into(),
));
}
let pairs: Vec<LawsKernelName> = if cfg.pairs.is_empty() {
let vecs = LawsVector::all();
let mut all = Vec::with_capacity(25);
for &row_v in &vecs {
for &col_v in &vecs {
all.push(LawsKernelName::new(row_v, col_v));
}
}
all
} else {
cfg.pairs
};
let processed = if cfg.remove_dc {
remove_dc_component(image, cfg.window_size)
} else {
image.clone()
};
let mut kernel_names = Vec::with_capacity(pairs.len());
let mut energy_maps = Vec::with_capacity(pairs.len());
let mut feature_vector = Vec::with_capacity(pairs.len());
for name in &pairs {
let r = name.row.coefficients();
let c = name.col.coefficients();
let mut kernel = Array2::<f64>::zeros((5, 5));
for i in 0..5 {
for j in 0..5 {
kernel[[i, j]] = r[i] * c[j];
}
}
let response = convolve_2d(&processed, &kernel);
let energy = compute_local_energy(&response, cfg.window_size);
let mean_e = if energy.is_empty() {
0.0
} else {
energy.sum() / energy.len() as f64
};
kernel_names.push(*name);
feature_vector.push(mean_e);
energy_maps.push(energy);
}
Ok(LawsFullResult {
kernel_names,
energy_maps,
feature_vector,
})
}
pub fn laws_rotation_invariant_features(
image: &Array2<f64>,
window_size: Option<usize>,
) -> NdimageResult<Vec<(String, f64)>> {
let ws = window_size.unwrap_or(15);
let config = LawsFullConfig {
pairs: Vec::new(), window_size: ws,
remove_dc: true,
};
let result = laws_texture_energy_full(image, Some(config))?;
let vecs = LawsVector::all();
let mut energy_lookup: std::collections::HashMap<(LawsVector, LawsVector), f64> =
std::collections::HashMap::new();
for (i, name) in result.kernel_names.iter().enumerate() {
energy_lookup.insert((name.row, name.col), result.feature_vector[i]);
}
let mut features = Vec::with_capacity(15);
for (idx_a, &va) in vecs.iter().enumerate() {
for &vb in &vecs[idx_a..] {
let e_ab = energy_lookup.get(&(va, vb)).copied().unwrap_or(0.0);
let e_ba = energy_lookup.get(&(vb, va)).copied().unwrap_or(0.0);
let sym_energy = (e_ab + e_ba) / 2.0;
let name = format!("{}{}", va.name(), vb.name());
features.push((name, sym_energy));
}
}
Ok(features)
}
fn remove_dc_component(image: &Array2<f64>, window: usize) -> Array2<f64> {
let (ny, nx) = image.dim();
let half = (window / 2) as i64;
let mut out = Array2::<f64>::zeros((ny, nx));
for i in 0..ny {
for j in 0..nx {
let mut sum = 0.0f64;
let mut count = 0.0f64;
for di in -half..=half {
for dj in -half..=half {
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni >= 0 && ni < ny as i64 && nj >= 0 && nj < nx as i64 {
sum += image[[ni as usize, nj as usize]];
count += 1.0;
}
}
}
out[[i, j]] = image[[i, j]] - sum / count.max(1.0);
}
}
out
}
fn convolve_2d(image: &Array2<f64>, kernel: &Array2<f64>) -> Array2<f64> {
let (ny, nx) = image.dim();
let (ky, kx) = kernel.dim();
let half_ky = (ky / 2) as i64;
let half_kx = (kx / 2) as i64;
let mut out = Array2::<f64>::zeros((ny, nx));
for i in 0..ny {
for j in 0..nx {
let mut sum = 0.0;
for ki in 0..ky {
for kj in 0..kx {
let ii = i as i64 + ki as i64 - half_ky;
let jj = j as i64 + kj as i64 - half_kx;
if ii >= 0 && ii < ny as i64 && jj >= 0 && jj < nx as i64 {
sum += image[[ii as usize, jj as usize]] * kernel[[ki, kj]];
}
}
}
out[[i, j]] = sum;
}
}
out
}
fn compute_local_energy(image: &Array2<f64>, window: usize) -> Array2<f64> {
let (ny, nx) = image.dim();
let half = (window / 2) as i64;
let mut out = Array2::<f64>::zeros((ny, nx));
for i in 0..ny {
for j in 0..nx {
let mut sum = 0.0f64;
let mut count = 0.0f64;
for di in -half..=half {
for dj in -half..=half {
let ni = i as i64 + di;
let nj = j as i64 + dj;
if ni >= 0 && ni < ny as i64 && nj >= 0 && nj < nx as i64 {
sum += image[[ni as usize, nj as usize]].abs();
count += 1.0;
}
}
}
out[[i, j]] = sum / count.max(1.0);
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array2;
fn make_test_image() -> Array2<f64> {
Array2::from_shape_fn((16, 16), |(i, j)| {
((i as f64 / 4.0).sin() * (j as f64 / 4.0).cos()) * 128.0 + 128.0
})
}
#[test]
fn test_laws_all_kernels_count() {
let kernels = laws_all_kernels();
assert_eq!(kernels.len(), 25, "Should have 25 Laws kernels");
for (name, kernel) in &kernels {
assert_eq!(
kernel.dim(),
(5, 5),
"Kernel {} should be 5x5",
name.display_name()
);
}
}
#[test]
fn test_laws_l5l5_is_averaging() {
let kernels = laws_all_kernels();
let l5l5 = &kernels[0].1; assert_eq!(kernels[0].0.display_name(), "L5L5");
for &v in l5l5.iter() {
assert!(v >= 0.0, "L5L5 should have all non-negative values");
}
let total: f64 = l5l5.iter().sum();
assert!(
(total - 256.0).abs() < 1e-10,
"L5L5 sum should be 256, got {}",
total
);
}
#[test]
fn test_laws_e5_detects_edges() {
let e5 = LawsVector::E5.coefficients();
let sum: f64 = e5.iter().sum();
assert!(sum.abs() < 1e-10, "E5 should sum to 0");
let mut img = Array2::<f64>::zeros((16, 16));
for i in 0..16 {
for j in 8..16 {
img[[i, j]] = 100.0;
}
}
let config = LawsFullConfig {
pairs: vec![
LawsKernelName::new(LawsVector::L5, LawsVector::E5),
LawsKernelName::new(LawsVector::L5, LawsVector::L5),
],
window_size: 5,
remove_dc: false,
};
let result = laws_texture_energy_full(&img, Some(config)).expect("laws");
let e_l5e5 = result.feature_vector[0];
assert!(
e_l5e5 > 0.0,
"L5E5 should detect the edge, energy = {}",
e_l5e5
);
}
#[test]
fn test_laws_uniform_image_zero_energy() {
let img = Array2::from_elem((16, 16), 100.0);
let config = LawsFullConfig {
pairs: vec![
LawsKernelName::new(LawsVector::L5, LawsVector::E5),
LawsKernelName::new(LawsVector::E5, LawsVector::S5),
LawsKernelName::new(LawsVector::S5, LawsVector::S5),
],
window_size: 5,
remove_dc: true,
};
let result = laws_texture_energy_full(&img, Some(config)).expect("laws");
for (i, &v) in result.feature_vector.iter().enumerate() {
assert!(
v < 1e-10,
"Uniform image should have ~0 energy for kernel {}, got {}",
result.kernel_names[i].display_name(),
v
);
}
}
#[test]
fn test_laws_full_25_kernels() {
let img = make_test_image();
let result = laws_texture_energy_full(&img, None).expect("laws");
assert_eq!(result.energy_maps.len(), 25);
assert_eq!(result.feature_vector.len(), 25);
assert_eq!(result.kernel_names.len(), 25);
for &v in &result.feature_vector {
assert!(v >= 0.0, "Energy should be non-negative");
assert!(v.is_finite(), "Energy should be finite");
}
}
#[test]
fn test_laws_rotation_invariant() {
let img = make_test_image();
let features = laws_rotation_invariant_features(&img, Some(5)).expect("ri features");
assert_eq!(
features.len(),
15,
"Should have 15 rotation-invariant features"
);
assert_eq!(features[0].0, "L5L5");
assert_eq!(features[1].0, "L5E5");
for (name, val) in &features {
assert!(
*val >= 0.0 && val.is_finite(),
"Feature {} should be non-negative and finite, got {}",
name,
val
);
}
}
#[test]
fn test_laws_rotation_invariant_symmetry() {
let img = make_test_image();
let full_config = LawsFullConfig {
window_size: 5,
..Default::default()
};
let full = laws_texture_energy_full(&img, Some(full_config)).expect("full");
let ri = laws_rotation_invariant_features(&img, Some(5)).expect("ri");
let mut e_l5e5 = 0.0;
let mut e_e5l5 = 0.0;
for (i, name) in full.kernel_names.iter().enumerate() {
if name.display_name() == "L5E5" {
e_l5e5 = full.feature_vector[i];
}
if name.display_name() == "E5L5" {
e_e5l5 = full.feature_vector[i];
}
}
let ri_l5e5 = ri
.iter()
.find(|(n, _)| n == "L5E5")
.map(|(_, v)| *v)
.unwrap_or(0.0);
let expected = (e_l5e5 + e_e5l5) / 2.0;
assert!(
(ri_l5e5 - expected).abs() < 1e-10,
"RI feature should be average of symmetric pair: {} vs {}",
ri_l5e5,
expected
);
}
#[test]
fn test_laws_errors() {
let small = Array2::<f64>::zeros((3, 3));
assert!(laws_texture_energy_full(&small, None).is_err());
let img = make_test_image();
let config = LawsFullConfig {
window_size: 4, ..Default::default()
};
assert!(laws_texture_energy_full(&img, Some(config)).is_err());
}
#[test]
fn test_laws_vector_properties() {
let l5: f64 = LawsVector::L5.coefficients().iter().sum();
assert!((l5 - 16.0).abs() < 1e-10);
for v in &[
LawsVector::E5,
LawsVector::S5,
LawsVector::W5,
LawsVector::R5,
] {
let sum: f64 = v.coefficients().iter().sum();
assert!(
sum.abs() < 1e-10,
"{} should sum to 0, got {}",
v.name(),
sum
);
}
}
}