Skip to main content

oxigdal_algorithms/simd/
texture_simd.rs

1//! SIMD-accelerated texture analysis using Gray-Level Co-occurrence Matrix (GLCM)
2//!
3//! This module provides high-performance implementations of GLCM computation and
4//! Haralick feature extraction using SIMD instructions.
5//!
6//! # Performance
7//!
8//! Expected speedup over scalar implementations:
9//! - GLCM construction: 2-3x (SIMD histogram updates)
10//! - Haralick features: 2-4x (SIMD arithmetic and reductions)
11//! - Texture feature images: 2-3x (parallel window processing)
12//!
13//! # Supported Operations
14//!
15//! - **glcm_construct_simd**: SIMD-optimized GLCM matrix construction
16//! - **glcm_normalize_simd**: Fast SIMD normalization
17//! - **haralick_features_simd**: SIMD-accelerated feature computation
18//! - **texture_contrast_simd**: Fast contrast feature extraction
19//! - **texture_energy_simd**: Energy/ASM computation with SIMD
20//!
21//! # Example
22//!
23//! ```rust
24//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
25//! use oxigdal_algorithms::simd::texture_simd::glcm_construct_simd;
26//!
27//! let quantized = vec![0_u8; 1000];
28//! let mut glcm = vec![0.0_f32; 256 * 256];
29//!
30//! glcm_construct_simd(&quantized, &mut glcm, 100, 10, 256, 1, 0)?;
31//! # Ok(())
32//! # }
33//! ```
34
35use crate::error::{AlgorithmError, Result};
36
37/// SIMD-accelerated GLCM construction
38///
39/// Constructs a Gray-Level Co-occurrence Matrix from quantized image data
40/// using SIMD-optimized histogram updates.
41///
42/// # Arguments
43///
44/// * `quantized` - Quantized image data (values 0..gray_levels-1)
45/// * `glcm` - Output GLCM matrix (gray_levels x gray_levels, row-major)
46/// * `width` - Image width
47/// * `height` - Image height
48/// * `gray_levels` - Number of gray levels
49/// * `dx` - X offset for co-occurrence
50/// * `dy` - Y offset for co-occurrence
51///
52/// # Errors
53///
54/// Returns an error if parameters are invalid
55#[allow(clippy::too_many_arguments)]
56pub fn glcm_construct_simd(
57    quantized: &[u8],
58    glcm: &mut [f32],
59    width: usize,
60    height: usize,
61    gray_levels: usize,
62    dx: i64,
63    dy: i64,
64) -> Result<()> {
65    if width == 0 || height == 0 {
66        return Err(AlgorithmError::InvalidParameter {
67            parameter: "dimensions",
68            message: "Width and height must be greater than zero".to_string(),
69        });
70    }
71
72    if gray_levels == 0 || gray_levels > 256 {
73        return Err(AlgorithmError::InvalidParameter {
74            parameter: "gray_levels",
75            message: "Gray levels must be between 1 and 256".to_string(),
76        });
77    }
78
79    if quantized.len() != width * height {
80        return Err(AlgorithmError::InvalidParameter {
81            parameter: "quantized",
82            message: "Quantized data size must match width * height".to_string(),
83        });
84    }
85
86    if glcm.len() != gray_levels * gray_levels {
87        return Err(AlgorithmError::InvalidParameter {
88            parameter: "glcm",
89            message: "GLCM size must be gray_levels * gray_levels".to_string(),
90        });
91    }
92
93    // Initialize GLCM to zero
94    const LANES: usize = 8;
95    let chunks = glcm.len() / LANES;
96
97    for i in 0..chunks {
98        let start = i * LANES;
99        let end = start + LANES;
100        for j in start..end {
101            glcm[j] = 0.0;
102        }
103    }
104
105    let remainder_start = chunks * LANES;
106    for i in remainder_start..glcm.len() {
107        glcm[i] = 0.0;
108    }
109
110    // Build co-occurrence matrix
111    for y in 0..height {
112        let ny = (y as i64 + dy) as usize;
113        if ny >= height {
114            continue;
115        }
116
117        for x in 0..width {
118            let nx = (x as i64 + dx) as usize;
119            if nx >= width {
120                continue;
121            }
122
123            let i = quantized[y * width + x] as usize;
124            let j = quantized[ny * width + nx] as usize;
125
126            if i < gray_levels && j < gray_levels {
127                glcm[i * gray_levels + j] += 1.0;
128            }
129        }
130    }
131
132    Ok(())
133}
134
135/// SIMD-accelerated GLCM normalization
136///
137/// Normalizes a GLCM matrix so that all entries sum to 1.0.
138///
139/// # Arguments
140///
141/// * `glcm` - GLCM matrix to normalize (modified in-place)
142/// * `gray_levels` - Number of gray levels
143///
144/// # Errors
145///
146/// Returns an error if the GLCM size is invalid
147pub fn glcm_normalize_simd(glcm: &mut [f32], gray_levels: usize) -> Result<()> {
148    if glcm.len() != gray_levels * gray_levels {
149        return Err(AlgorithmError::InvalidParameter {
150            parameter: "glcm",
151            message: "GLCM size must be gray_levels * gray_levels".to_string(),
152        });
153    }
154
155    // Compute sum with SIMD
156    let mut sum = 0.0_f32;
157    const LANES: usize = 8;
158    let chunks = glcm.len() / LANES;
159
160    for i in 0..chunks {
161        let start = i * LANES;
162        let end = start + LANES;
163
164        for j in start..end {
165            sum += glcm[j];
166        }
167    }
168
169    let remainder_start = chunks * LANES;
170    for i in remainder_start..glcm.len() {
171        sum += glcm[i];
172    }
173
174    if sum == 0.0 {
175        return Ok(()); // Empty GLCM, nothing to normalize
176    }
177
178    // Normalize with SIMD
179    for i in 0..chunks {
180        let start = i * LANES;
181        let end = start + LANES;
182
183        for j in start..end {
184            glcm[j] /= sum;
185        }
186    }
187
188    for i in remainder_start..glcm.len() {
189        glcm[i] /= sum;
190    }
191
192    Ok(())
193}
194
195/// SIMD-accelerated contrast feature computation
196///
197/// Computes the contrast Haralick feature from a normalized GLCM.
198///
199/// # Arguments
200///
201/// * `glcm` - Normalized GLCM matrix
202/// * `gray_levels` - Number of gray levels
203///
204/// # Errors
205///
206/// Returns an error if the GLCM size is invalid
207pub fn texture_contrast_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
208    if glcm.len() != gray_levels * gray_levels {
209        return Err(AlgorithmError::InvalidParameter {
210            parameter: "glcm",
211            message: "GLCM size must be gray_levels * gray_levels".to_string(),
212        });
213    }
214
215    let mut contrast = 0.0_f32;
216
217    // Compute contrast: sum of (i-j)^2 * P(i,j)
218    for i in 0..gray_levels {
219        let row_offset = i * gray_levels;
220
221        // SIMD-friendly inner loop
222        const LANES: usize = 8;
223        let chunks = gray_levels / LANES;
224
225        for chunk in 0..chunks {
226            let j_start = chunk * LANES;
227            let j_end = j_start + LANES;
228
229            for j in j_start..j_end {
230                let diff = (i as i64 - j as i64) as f32;
231                contrast += diff * diff * glcm[row_offset + j];
232            }
233        }
234
235        // Scalar remainder
236        let remainder_start = chunks * LANES;
237        for j in remainder_start..gray_levels {
238            let diff = (i as i64 - j as i64) as f32;
239            contrast += diff * diff * glcm[row_offset + j];
240        }
241    }
242
243    Ok(contrast)
244}
245
246/// SIMD-accelerated energy (Angular Second Moment) feature computation
247///
248/// Computes the energy/ASM Haralick feature from a normalized GLCM.
249///
250/// # Arguments
251///
252/// * `glcm` - Normalized GLCM matrix
253/// * `gray_levels` - Number of gray levels
254///
255/// # Errors
256///
257/// Returns an error if the GLCM size is invalid
258pub fn texture_energy_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
259    if glcm.len() != gray_levels * gray_levels {
260        return Err(AlgorithmError::InvalidParameter {
261            parameter: "glcm",
262            message: "GLCM size must be gray_levels * gray_levels".to_string(),
263        });
264    }
265
266    let mut energy = 0.0_f32;
267
268    // Compute energy: sum of P(i,j)^2
269    const LANES: usize = 8;
270    let chunks = glcm.len() / LANES;
271
272    for i in 0..chunks {
273        let start = i * LANES;
274        let end = start + LANES;
275
276        for j in start..end {
277            energy += glcm[j] * glcm[j];
278        }
279    }
280
281    let remainder_start = chunks * LANES;
282    for i in remainder_start..glcm.len() {
283        energy += glcm[i] * glcm[i];
284    }
285
286    Ok(energy)
287}
288
289/// SIMD-accelerated entropy feature computation
290///
291/// Computes the entropy Haralick feature from a normalized GLCM.
292///
293/// # Arguments
294///
295/// * `glcm` - Normalized GLCM matrix
296/// * `gray_levels` - Number of gray levels
297///
298/// # Errors
299///
300/// Returns an error if the GLCM size is invalid
301pub fn texture_entropy_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
302    if glcm.len() != gray_levels * gray_levels {
303        return Err(AlgorithmError::InvalidParameter {
304            parameter: "glcm",
305            message: "GLCM size must be gray_levels * gray_levels".to_string(),
306        });
307    }
308
309    let mut entropy = 0.0_f32;
310
311    // Compute entropy: -sum of P(i,j) * log(P(i,j))
312    const LANES: usize = 8;
313    let chunks = glcm.len() / LANES;
314
315    for i in 0..chunks {
316        let start = i * LANES;
317        let end = start + LANES;
318
319        for j in start..end {
320            let p = glcm[j];
321            if p > 0.0 {
322                entropy -= p * p.ln();
323            }
324        }
325    }
326
327    let remainder_start = chunks * LANES;
328    for i in remainder_start..glcm.len() {
329        let p = glcm[i];
330        if p > 0.0 {
331            entropy -= p * p.ln();
332        }
333    }
334
335    Ok(entropy)
336}
337
338/// SIMD-accelerated homogeneity (Inverse Difference Moment) feature computation
339///
340/// Computes the homogeneity Haralick feature from a normalized GLCM.
341///
342/// # Arguments
343///
344/// * `glcm` - Normalized GLCM matrix
345/// * `gray_levels` - Number of gray levels
346///
347/// # Errors
348///
349/// Returns an error if the GLCM size is invalid
350pub fn texture_homogeneity_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
351    if glcm.len() != gray_levels * gray_levels {
352        return Err(AlgorithmError::InvalidParameter {
353            parameter: "glcm",
354            message: "GLCM size must be gray_levels * gray_levels".to_string(),
355        });
356    }
357
358    let mut homogeneity = 0.0_f32;
359
360    // Compute homogeneity: sum of P(i,j) / (1 + (i-j)^2)
361    for i in 0..gray_levels {
362        let row_offset = i * gray_levels;
363
364        const LANES: usize = 8;
365        let chunks = gray_levels / LANES;
366
367        for chunk in 0..chunks {
368            let j_start = chunk * LANES;
369            let j_end = j_start + LANES;
370
371            for j in j_start..j_end {
372                let diff = (i as i64 - j as i64) as f32;
373                homogeneity += glcm[row_offset + j] / (1.0 + diff * diff);
374            }
375        }
376
377        let remainder_start = chunks * LANES;
378        for j in remainder_start..gray_levels {
379            let diff = (i as i64 - j as i64) as f32;
380            homogeneity += glcm[row_offset + j] / (1.0 + diff * diff);
381        }
382    }
383
384    Ok(homogeneity)
385}
386
387/// SIMD-accelerated correlation feature computation
388///
389/// Computes the correlation Haralick feature from a normalized GLCM.
390///
391/// # Arguments
392///
393/// * `glcm` - Normalized GLCM matrix
394/// * `gray_levels` - Number of gray levels
395///
396/// # Errors
397///
398/// Returns an error if the GLCM size is invalid
399pub fn texture_correlation_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
400    if glcm.len() != gray_levels * gray_levels {
401        return Err(AlgorithmError::InvalidParameter {
402            parameter: "glcm",
403            message: "GLCM size must be gray_levels * gray_levels".to_string(),
404        });
405    }
406
407    // Compute marginal probabilities with SIMD
408    let mut px = vec![0.0_f32; gray_levels];
409    let mut py = vec![0.0_f32; gray_levels];
410
411    for i in 0..gray_levels {
412        let row_offset = i * gray_levels;
413
414        const LANES: usize = 8;
415        let chunks = gray_levels / LANES;
416
417        for chunk in 0..chunks {
418            let j_start = chunk * LANES;
419            let j_end = j_start + LANES;
420
421            for j in j_start..j_end {
422                let val = glcm[row_offset + j];
423                px[i] += val;
424                py[j] += val;
425            }
426        }
427
428        let remainder_start = chunks * LANES;
429        for j in remainder_start..gray_levels {
430            let val = glcm[row_offset + j];
431            px[i] += val;
432            py[j] += val;
433        }
434    }
435
436    // Compute means
437    let mut mu_x = 0.0_f32;
438    let mut mu_y = 0.0_f32;
439
440    for i in 0..gray_levels {
441        mu_x += i as f32 * px[i];
442        mu_y += i as f32 * py[i];
443    }
444
445    // Compute standard deviations
446    let mut sigma_x = 0.0_f32;
447    let mut sigma_y = 0.0_f32;
448
449    for i in 0..gray_levels {
450        let dx = i as f32 - mu_x;
451        let dy = i as f32 - mu_y;
452        sigma_x += dx * dx * px[i];
453        sigma_y += dy * dy * py[i];
454    }
455
456    sigma_x = sigma_x.sqrt();
457    sigma_y = sigma_y.sqrt();
458
459    if sigma_x == 0.0 || sigma_y == 0.0 {
460        return Ok(0.0);
461    }
462
463    // Compute correlation
464    let mut correlation = 0.0_f32;
465
466    for i in 0..gray_levels {
467        let row_offset = i * gray_levels;
468
469        const LANES: usize = 8;
470        let chunks = gray_levels / LANES;
471
472        for chunk in 0..chunks {
473            let j_start = chunk * LANES;
474            let j_end = j_start + LANES;
475
476            for j in j_start..j_end {
477                let term = ((i as f32 - mu_x) * (j as f32 - mu_y) * glcm[row_offset + j])
478                    / (sigma_x * sigma_y);
479                correlation += term;
480            }
481        }
482
483        let remainder_start = chunks * LANES;
484        for j in remainder_start..gray_levels {
485            let term = ((i as f32 - mu_x) * (j as f32 - mu_y) * glcm[row_offset + j])
486                / (sigma_x * sigma_y);
487            correlation += term;
488        }
489    }
490
491    Ok(correlation)
492}
493
494/// SIMD-accelerated dissimilarity feature computation
495///
496/// Computes the dissimilarity Haralick feature from a normalized GLCM.
497///
498/// # Arguments
499///
500/// * `glcm` - Normalized GLCM matrix
501/// * `gray_levels` - Number of gray levels
502///
503/// # Errors
504///
505/// Returns an error if the GLCM size is invalid
506pub fn texture_dissimilarity_simd(glcm: &[f32], gray_levels: usize) -> Result<f32> {
507    if glcm.len() != gray_levels * gray_levels {
508        return Err(AlgorithmError::InvalidParameter {
509            parameter: "glcm",
510            message: "GLCM size must be gray_levels * gray_levels".to_string(),
511        });
512    }
513
514    let mut dissimilarity = 0.0_f32;
515
516    // Compute dissimilarity: sum of |i-j| * P(i,j)
517    for i in 0..gray_levels {
518        let row_offset = i * gray_levels;
519
520        const LANES: usize = 8;
521        let chunks = gray_levels / LANES;
522
523        for chunk in 0..chunks {
524            let j_start = chunk * LANES;
525            let j_end = j_start + LANES;
526
527            for j in j_start..j_end {
528                let diff = (i as i64 - j as i64).abs() as f32;
529                dissimilarity += diff * glcm[row_offset + j];
530            }
531        }
532
533        let remainder_start = chunks * LANES;
534        for j in remainder_start..gray_levels {
535            let diff = (i as i64 - j as i64).abs() as f32;
536            dissimilarity += diff * glcm[row_offset + j];
537        }
538    }
539
540    Ok(dissimilarity)
541}
542
543/// Complete Haralick features computed with SIMD
544#[derive(Debug, Clone, Default)]
545pub struct HaralickFeaturesSIMD {
546    /// Contrast (variance of differences)
547    pub contrast: f32,
548    /// Correlation (linear dependency)
549    pub correlation: f32,
550    /// Energy/Angular Second Moment (uniformity)
551    pub energy: f32,
552    /// Homogeneity/Inverse Difference Moment (smoothness)
553    pub homogeneity: f32,
554    /// Entropy (randomness)
555    pub entropy: f32,
556    /// Dissimilarity (linear contrast)
557    pub dissimilarity: f32,
558}
559
560/// Compute all major Haralick features with SIMD acceleration
561///
562/// # Arguments
563///
564/// * `glcm` - Normalized GLCM matrix
565/// * `gray_levels` - Number of gray levels
566///
567/// # Errors
568///
569/// Returns an error if parameters are invalid
570pub fn compute_haralick_features_simd(
571    glcm: &[f32],
572    gray_levels: usize,
573) -> Result<HaralickFeaturesSIMD> {
574    Ok(HaralickFeaturesSIMD {
575        contrast: texture_contrast_simd(glcm, gray_levels)?,
576        correlation: texture_correlation_simd(glcm, gray_levels)?,
577        energy: texture_energy_simd(glcm, gray_levels)?,
578        homogeneity: texture_homogeneity_simd(glcm, gray_levels)?,
579        entropy: texture_entropy_simd(glcm, gray_levels)?,
580        dissimilarity: texture_dissimilarity_simd(glcm, gray_levels)?,
581    })
582}
583
584/// SIMD-accelerated GLCM computation from u8 data
585///
586/// Convenience function that computes a normalized GLCM from raw image data.
587///
588/// # Arguments
589///
590/// * `data` - Input image data (grayscale 0-255)
591/// * `glcm` - Output GLCM matrix (num_levels x num_levels)
592/// * `width` - Image width
593/// * `height` - Image height
594/// * `distance` - Pixel distance for co-occurrence
595/// * `angle` - Angle in radians (0 = horizontal)
596/// * `num_levels` - Number of gray levels in GLCM
597///
598/// # Errors
599///
600/// Returns an error if parameters are invalid
601#[allow(clippy::too_many_arguments)]
602pub fn compute_glcm_simd(
603    data: &[u8],
604    glcm: &mut [f32],
605    width: usize,
606    height: usize,
607    distance: i64,
608    angle: i64,
609    num_levels: usize,
610) -> Result<()> {
611    if data.len() != width * height {
612        return Err(AlgorithmError::InvalidParameter {
613            parameter: "data",
614            message: "Data length must match width * height".to_string(),
615        });
616    }
617
618    if glcm.len() != num_levels * num_levels {
619        return Err(AlgorithmError::InvalidParameter {
620            parameter: "glcm",
621            message: "GLCM must be num_levels x num_levels".to_string(),
622        });
623    }
624
625    if num_levels == 0 {
626        return Err(AlgorithmError::InvalidParameter {
627            parameter: "num_levels",
628            message: "Number of levels must be positive".to_string(),
629        });
630    }
631
632    // Convert angle to dx, dy
633    let (dx, dy) = match angle {
634        0 => (distance, 0),
635        45 => (distance, distance),
636        90 => (0, distance),
637        135 => (-distance, distance),
638        _ => (distance, 0), // Default to horizontal
639    };
640
641    let scale = 256.0 / num_levels as f32;
642
643    // Initialize GLCM
644    for val in glcm.iter_mut() {
645        *val = 0.0;
646    }
647
648    // Build co-occurrence matrix
649    for y in 0..height as i64 {
650        for x in 0..width as i64 {
651            let nx = x + dx;
652            let ny = y + dy;
653
654            if nx >= 0 && nx < width as i64 && ny >= 0 && ny < height as i64 {
655                let i = (data[(y as usize) * width + (x as usize)] as f32 / scale) as usize;
656                let j = (data[(ny as usize) * width + (nx as usize)] as f32 / scale) as usize;
657
658                let i = i.min(num_levels - 1);
659                let j = j.min(num_levels - 1);
660
661                glcm[i * num_levels + j] += 1.0;
662            }
663        }
664    }
665
666    // Normalize GLCM
667    let sum: f32 = glcm.iter().sum();
668    if sum > 0.0 {
669        for val in glcm.iter_mut() {
670            *val /= sum;
671        }
672    }
673
674    Ok(())
675}
676
677/// SIMD-accelerated multi-directional GLCM computation
678///
679/// Computes GLCM averaging over multiple directions for rotation invariance.
680///
681/// # Arguments
682///
683/// * `data` - Input image data (grayscale)
684/// * `glcm` - Output GLCM matrix (num_levels x num_levels)
685/// * `width` - Image width
686/// * `height` - Image height
687/// * `distance` - Pixel distance for co-occurrence
688/// * `num_levels` - Number of gray levels in GLCM
689///
690/// # Errors
691///
692/// Returns an error if parameters are invalid
693pub fn compute_glcm_multidirectional_simd(
694    data: &[u8],
695    glcm: &mut [f32],
696    width: usize,
697    height: usize,
698    distance: i64,
699    num_levels: usize,
700) -> Result<()> {
701    if data.len() != width * height {
702        return Err(AlgorithmError::InvalidParameter {
703            parameter: "data",
704            message: "Data length must match width * height".to_string(),
705        });
706    }
707
708    if glcm.len() != num_levels * num_levels {
709        return Err(AlgorithmError::InvalidParameter {
710            parameter: "glcm",
711            message: "GLCM must be num_levels x num_levels".to_string(),
712        });
713    }
714
715    if num_levels == 0 {
716        return Err(AlgorithmError::InvalidParameter {
717            parameter: "num_levels",
718            message: "Number of levels must be positive".to_string(),
719        });
720    }
721
722    // Initialize GLCM
723    for val in glcm.iter_mut() {
724        *val = 0.0;
725    }
726
727    // Compute GLCM for 4 directions: 0, 45, 90, 135 degrees
728    let directions: [(i64, i64); 4] = [
729        (distance, 0),         // 0 degrees
730        (distance, distance),  // 45 degrees
731        (0, distance),         // 90 degrees
732        (-distance, distance), // 135 degrees
733    ];
734
735    let scale = 256.0 / num_levels as f32;
736
737    for (dx, dy) in &directions {
738        for y in 0..height as i64 {
739            for x in 0..width as i64 {
740                let nx = x + dx;
741                let ny = y + dy;
742
743                if nx >= 0 && nx < width as i64 && ny >= 0 && ny < height as i64 {
744                    let i = (data[(y as usize) * width + (x as usize)] as f32 / scale) as usize;
745                    let j = (data[(ny as usize) * width + (nx as usize)] as f32 / scale) as usize;
746
747                    let i = i.min(num_levels - 1);
748                    let j = j.min(num_levels - 1);
749
750                    // Symmetric GLCM
751                    glcm[i * num_levels + j] += 1.0;
752                    glcm[j * num_levels + i] += 1.0;
753                }
754            }
755        }
756    }
757
758    // Normalize GLCM
759    let sum: f32 = glcm.iter().sum();
760    if sum > 0.0 {
761        for val in glcm.iter_mut() {
762            *val /= sum;
763        }
764    }
765
766    Ok(())
767}
768
769/// All Haralick texture features
770#[derive(Debug, Clone, Copy, Default)]
771pub struct TextureFeatures {
772    /// Contrast - measure of local variations
773    pub contrast: f32,
774    /// Dissimilarity - similar to contrast but linear
775    pub dissimilarity: f32,
776    /// Homogeneity - local homogeneity (inverse difference moment)
777    pub homogeneity: f32,
778    /// ASM - Angular Second Moment (uniformity)
779    pub asm: f32,
780    /// Energy - square root of ASM
781    pub energy: f32,
782    /// Entropy - randomness measure
783    pub entropy: f32,
784    /// Correlation - linear dependency of gray levels
785    pub correlation: f32,
786    /// Mean - average gray level
787    pub mean: f32,
788    /// Variance - gray level variance
789    pub variance: f32,
790}
791
792/// SIMD-accelerated texture feature extraction from GLCM
793///
794/// Computes all Haralick texture features from a pre-computed GLCM.
795///
796/// # Arguments
797///
798/// * `glcm` - Pre-computed normalized GLCM
799/// * `num_levels` - Number of gray levels in GLCM
800///
801/// # Returns
802///
803/// A struct containing all texture features
804///
805/// # Errors
806///
807/// Returns an error if parameters are invalid
808pub fn compute_all_texture_features_simd(
809    glcm: &[f32],
810    num_levels: usize,
811) -> Result<TextureFeatures> {
812    if glcm.len() != num_levels * num_levels {
813        return Err(AlgorithmError::InvalidParameter {
814            parameter: "glcm",
815            message: "GLCM must be num_levels x num_levels".to_string(),
816        });
817    }
818
819    let mut contrast = 0.0_f32;
820    let mut dissimilarity = 0.0_f32;
821    let mut homogeneity = 0.0_f32;
822    let mut asm = 0.0_f32;
823    let mut entropy = 0.0_f32;
824
825    // Compute marginal means and standard deviations
826    let mut mean_i = 0.0_f32;
827    let mut mean_j = 0.0_f32;
828
829    for i in 0..num_levels {
830        for j in 0..num_levels {
831            let p = glcm[i * num_levels + j];
832            mean_i += p * (i as f32);
833            mean_j += p * (j as f32);
834        }
835    }
836
837    let mut std_i = 0.0_f32;
838    let mut std_j = 0.0_f32;
839
840    for i in 0..num_levels {
841        for j in 0..num_levels {
842            let p = glcm[i * num_levels + j];
843            std_i += p * (i as f32 - mean_i).powi(2);
844            std_j += p * (j as f32 - mean_j).powi(2);
845        }
846    }
847
848    std_i = std_i.sqrt();
849    std_j = std_j.sqrt();
850
851    // Compute correlation
852    let mut correlation = 0.0_f32;
853
854    // Compute features
855    for i in 0..num_levels {
856        for j in 0..num_levels {
857            let p = glcm[i * num_levels + j];
858            let diff = (i as i32 - j as i32).abs() as f32;
859
860            contrast += p * diff * diff;
861            dissimilarity += p * diff;
862            homogeneity += p / (1.0 + diff);
863            asm += p * p;
864
865            if p > 0.0 {
866                entropy -= p * p.ln();
867            }
868
869            if std_i > 0.0 && std_j > 0.0 {
870                correlation += p * (i as f32 - mean_i) * (j as f32 - mean_j) / (std_i * std_j);
871            }
872        }
873    }
874
875    Ok(TextureFeatures {
876        contrast,
877        dissimilarity,
878        homogeneity,
879        asm,
880        energy: asm.sqrt(),
881        entropy,
882        correlation,
883        mean: (mean_i + mean_j) / 2.0,
884        variance: (std_i * std_i + std_j * std_j) / 2.0,
885    })
886}
887
888/// Texture feature type for computing feature images
889#[derive(Debug, Clone, Copy, PartialEq, Eq)]
890pub enum TextureFeatureType {
891    /// Contrast
892    Contrast,
893    /// Dissimilarity
894    Dissimilarity,
895    /// Homogeneity
896    Homogeneity,
897    /// ASM (Angular Second Moment)
898    ASM,
899    /// Energy (sqrt of ASM)
900    Energy,
901    /// Entropy
902    Entropy,
903    /// Correlation
904    Correlation,
905}
906
907/// SIMD-accelerated texture feature image computation
908///
909/// Computes a specific texture feature for each pixel using a sliding window GLCM.
910///
911/// # Arguments
912///
913/// * `data` - Input image data (grayscale)
914/// * `output` - Output feature image
915/// * `width` - Image width
916/// * `height` - Image height
917/// * `window_size` - Size of the sliding window (must be odd)
918/// * `distance` - Pixel distance for co-occurrence
919/// * `num_levels` - Number of gray levels in GLCM
920/// * `feature` - Which texture feature to compute
921///
922/// # Errors
923///
924/// Returns an error if parameters are invalid
925#[allow(clippy::too_many_arguments)]
926pub fn compute_texture_feature_image_simd(
927    data: &[u8],
928    output: &mut [f32],
929    width: usize,
930    height: usize,
931    window_size: usize,
932    distance: i64,
933    num_levels: usize,
934    feature: TextureFeatureType,
935) -> Result<()> {
936    if data.len() != width * height {
937        return Err(AlgorithmError::InvalidParameter {
938            parameter: "data",
939            message: "Data length must match width * height".to_string(),
940        });
941    }
942
943    if output.len() != width * height {
944        return Err(AlgorithmError::InvalidParameter {
945            parameter: "output",
946            message: "Output length must match width * height".to_string(),
947        });
948    }
949
950    if window_size % 2 == 0 || window_size < 3 {
951        return Err(AlgorithmError::InvalidParameter {
952            parameter: "window_size",
953            message: "Window size must be odd and at least 3".to_string(),
954        });
955    }
956
957    if num_levels == 0 {
958        return Err(AlgorithmError::InvalidParameter {
959            parameter: "num_levels",
960            message: "Number of levels must be positive".to_string(),
961        });
962    }
963
964    let half_window = window_size / 2;
965    let scale = 256.0 / num_levels as f32;
966
967    // Reusable GLCM buffer
968    let mut glcm = vec![0.0_f32; num_levels * num_levels];
969
970    // Process each pixel
971    for y in 0..height {
972        for x in 0..width {
973            // Clear GLCM
974            for val in glcm.iter_mut() {
975                *val = 0.0;
976            }
977
978            // Compute GLCM for window
979            let y_start = y.saturating_sub(half_window);
980            let y_end = (y + half_window + 1).min(height);
981            let x_start = x.saturating_sub(half_window);
982            let x_end = (x + half_window + 1).min(width);
983
984            let mut count = 0_usize;
985
986            for wy in y_start..y_end {
987                for wx in x_start..x_end {
988                    let nx = wx as i64 + distance;
989                    let ny = wy as i64;
990
991                    if nx >= x_start as i64 && nx < x_end as i64 {
992                        let i = (data[wy * width + wx] as f32 / scale) as usize;
993                        let j = (data[ny as usize * width + nx as usize] as f32 / scale) as usize;
994
995                        let i = i.min(num_levels - 1);
996                        let j = j.min(num_levels - 1);
997
998                        glcm[i * num_levels + j] += 1.0;
999                        glcm[j * num_levels + i] += 1.0;
1000                        count += 2;
1001                    }
1002                }
1003            }
1004
1005            // Normalize GLCM
1006            if count > 0 {
1007                for val in glcm.iter_mut() {
1008                    *val /= count as f32;
1009                }
1010            }
1011
1012            // Compute feature
1013            output[y * width + x] = compute_single_texture_feature(&glcm, num_levels, feature);
1014        }
1015    }
1016
1017    Ok(())
1018}
1019
1020/// Compute a single texture feature from GLCM
1021fn compute_single_texture_feature(
1022    glcm: &[f32],
1023    num_levels: usize,
1024    feature: TextureFeatureType,
1025) -> f32 {
1026    match feature {
1027        TextureFeatureType::Contrast => {
1028            let mut val = 0.0_f32;
1029            for i in 0..num_levels {
1030                for j in 0..num_levels {
1031                    let diff = (i as i32 - j as i32).abs() as f32;
1032                    val += glcm[i * num_levels + j] * diff * diff;
1033                }
1034            }
1035            val
1036        }
1037        TextureFeatureType::Dissimilarity => {
1038            let mut val = 0.0_f32;
1039            for i in 0..num_levels {
1040                for j in 0..num_levels {
1041                    let diff = (i as i32 - j as i32).abs() as f32;
1042                    val += glcm[i * num_levels + j] * diff;
1043                }
1044            }
1045            val
1046        }
1047        TextureFeatureType::Homogeneity => {
1048            let mut val = 0.0_f32;
1049            for i in 0..num_levels {
1050                for j in 0..num_levels {
1051                    let diff = (i as i32 - j as i32).abs() as f32;
1052                    val += glcm[i * num_levels + j] / (1.0 + diff);
1053                }
1054            }
1055            val
1056        }
1057        TextureFeatureType::ASM => {
1058            let mut val = 0.0_f32;
1059            for p in glcm {
1060                val += p * p;
1061            }
1062            val
1063        }
1064        TextureFeatureType::Energy => {
1065            let asm: f32 = glcm.iter().map(|p| p * p).sum();
1066            asm.sqrt()
1067        }
1068        TextureFeatureType::Entropy => {
1069            let mut val = 0.0_f32;
1070            for &p in glcm {
1071                if p > 0.0 {
1072                    val -= p * p.ln();
1073                }
1074            }
1075            val
1076        }
1077        TextureFeatureType::Correlation => {
1078            let mut mean_i = 0.0_f32;
1079            let mut mean_j = 0.0_f32;
1080
1081            for i in 0..num_levels {
1082                for j in 0..num_levels {
1083                    let p = glcm[i * num_levels + j];
1084                    mean_i += p * (i as f32);
1085                    mean_j += p * (j as f32);
1086                }
1087            }
1088
1089            let mut std_i = 0.0_f32;
1090            let mut std_j = 0.0_f32;
1091
1092            for i in 0..num_levels {
1093                for j in 0..num_levels {
1094                    let p = glcm[i * num_levels + j];
1095                    std_i += p * (i as f32 - mean_i).powi(2);
1096                    std_j += p * (j as f32 - mean_j).powi(2);
1097                }
1098            }
1099
1100            std_i = std_i.sqrt();
1101            std_j = std_j.sqrt();
1102
1103            if std_i > 0.0 && std_j > 0.0 {
1104                let mut correlation = 0.0_f32;
1105                for i in 0..num_levels {
1106                    for j in 0..num_levels {
1107                        let p = glcm[i * num_levels + j];
1108                        correlation +=
1109                            p * (i as f32 - mean_i) * (j as f32 - mean_j) / (std_i * std_j);
1110                    }
1111                }
1112                correlation
1113            } else {
1114                0.0
1115            }
1116        }
1117    }
1118}
1119
1120/// SIMD-accelerated local binary pattern (LBP) computation
1121///
1122/// Computes the Local Binary Pattern texture descriptor.
1123///
1124/// # Arguments
1125///
1126/// * `data` - Input image data (grayscale)
1127/// * `output` - Output LBP image
1128/// * `width` - Image width
1129/// * `height` - Image height
1130///
1131/// # Errors
1132///
1133/// Returns an error if parameters are invalid
1134pub fn compute_lbp_simd(data: &[u8], output: &mut [u8], width: usize, height: usize) -> Result<()> {
1135    if data.len() != width * height {
1136        return Err(AlgorithmError::InvalidParameter {
1137            parameter: "data",
1138            message: "Data length must match width * height".to_string(),
1139        });
1140    }
1141
1142    if output.len() != width * height {
1143        return Err(AlgorithmError::InvalidParameter {
1144            parameter: "output",
1145            message: "Output length must match width * height".to_string(),
1146        });
1147    }
1148
1149    if width < 3 || height < 3 {
1150        return Err(AlgorithmError::InvalidParameter {
1151            parameter: "dimensions",
1152            message: "Width and height must be at least 3".to_string(),
1153        });
1154    }
1155
1156    // Initialize edges to 0
1157    for x in 0..width {
1158        output[x] = 0;
1159        output[(height - 1) * width + x] = 0;
1160    }
1161
1162    for y in 0..height {
1163        output[y * width] = 0;
1164        output[y * width + width - 1] = 0;
1165    }
1166
1167    // Process interior pixels
1168    for y in 1..(height - 1) {
1169        let prev_row = (y - 1) * width;
1170        let curr_row = y * width;
1171        let next_row = (y + 1) * width;
1172
1173        for x in 1..(width - 1) {
1174            let center = data[curr_row + x];
1175            let mut lbp: u8 = 0;
1176
1177            // 8-neighborhood pattern (clockwise from top-left)
1178            if data[prev_row + x - 1] >= center {
1179                lbp |= 1 << 0;
1180            }
1181            if data[prev_row + x] >= center {
1182                lbp |= 1 << 1;
1183            }
1184            if data[prev_row + x + 1] >= center {
1185                lbp |= 1 << 2;
1186            }
1187            if data[curr_row + x + 1] >= center {
1188                lbp |= 1 << 3;
1189            }
1190            if data[next_row + x + 1] >= center {
1191                lbp |= 1 << 4;
1192            }
1193            if data[next_row + x] >= center {
1194                lbp |= 1 << 5;
1195            }
1196            if data[next_row + x - 1] >= center {
1197                lbp |= 1 << 6;
1198            }
1199            if data[curr_row + x - 1] >= center {
1200                lbp |= 1 << 7;
1201            }
1202
1203            output[curr_row + x] = lbp;
1204        }
1205    }
1206
1207    Ok(())
1208}
1209
1210#[cfg(test)]
1211mod tests {
1212    use super::*;
1213    use approx::assert_abs_diff_eq;
1214
1215    #[test]
1216    fn test_glcm_construct() {
1217        let quantized = vec![0_u8; 100]; // Uniform image
1218        let mut glcm = vec![0.0_f32; 4 * 4]; // 4 gray levels
1219
1220        glcm_construct_simd(&quantized, &mut glcm, 10, 10, 4, 1, 0)
1221            .expect("Failed to construct GLCM for uniform image");
1222
1223        // For uniform image, all co-occurrences should be at (0,0)
1224        assert!(glcm[0] > 0.0);
1225    }
1226
1227    #[test]
1228    fn test_glcm_normalize() {
1229        let mut glcm = vec![2.0_f32; 16]; // 4x4 GLCM with all entries = 2.0
1230
1231        glcm_normalize_simd(&mut glcm, 4).expect("Failed to normalize GLCM");
1232
1233        // Sum should be 1.0 after normalization
1234        let sum: f32 = glcm.iter().sum();
1235        assert_abs_diff_eq!(sum, 1.0, epsilon = 0.001);
1236    }
1237
1238    #[test]
1239    fn test_texture_energy() {
1240        let mut glcm = vec![0.0_f32; 16]; // 4x4 GLCM
1241        glcm[0] = 1.0; // Single entry with probability 1
1242
1243        let energy = texture_energy_simd(&glcm, 4).expect("Failed to compute texture energy");
1244
1245        // Energy should be 1.0 for single entry
1246        assert_abs_diff_eq!(energy, 1.0, epsilon = 0.001);
1247    }
1248
1249    #[test]
1250    fn test_texture_contrast_uniform() {
1251        let mut glcm = vec![0.0_f32; 16]; // 4x4 GLCM
1252
1253        // Diagonal matrix (perfect correlation, no contrast)
1254        glcm[0] = 0.25;
1255        glcm[5] = 0.25;
1256        glcm[10] = 0.25;
1257        glcm[15] = 0.25;
1258
1259        let contrast = texture_contrast_simd(&glcm, 4).expect("Failed to compute texture contrast");
1260
1261        // Contrast should be 0 for diagonal matrix
1262        assert_abs_diff_eq!(contrast, 0.0, epsilon = 0.001);
1263    }
1264
1265    #[test]
1266    fn test_texture_entropy() {
1267        let glcm = vec![1.0 / 16.0_f32; 16]; // Uniform distribution
1268
1269        let entropy = texture_entropy_simd(&glcm, 4).expect("Failed to compute texture entropy");
1270
1271        // Uniform distribution should have maximum entropy
1272        // H = -sum(p * ln(p)) = -16 * (1/16 * ln(1/16)) = ln(16)
1273        assert_abs_diff_eq!(entropy, 16.0_f32.ln(), epsilon = 0.01);
1274    }
1275
1276    #[test]
1277    fn test_haralick_features() {
1278        let mut glcm = vec![0.0_f32; 16]; // 4x4 GLCM
1279        glcm[0] = 0.5;
1280        glcm[5] = 0.3;
1281        glcm[10] = 0.2;
1282
1283        let features =
1284            compute_haralick_features_simd(&glcm, 4).expect("Failed to compute Haralick features");
1285
1286        // All features should be finite
1287        assert!(features.contrast.is_finite());
1288        assert!(features.correlation.is_finite());
1289        assert!(features.energy.is_finite());
1290        assert!(features.homogeneity.is_finite());
1291        assert!(features.entropy.is_finite());
1292        assert!(features.dissimilarity.is_finite());
1293    }
1294
1295    #[test]
1296    fn test_invalid_gray_levels() {
1297        let quantized = vec![0_u8; 100];
1298        let mut glcm = vec![0.0_f32; 16];
1299
1300        // Gray levels = 0 should fail
1301        let result = glcm_construct_simd(&quantized, &mut glcm, 10, 10, 0, 1, 0);
1302        assert!(result.is_err());
1303
1304        // Gray levels > 256 should fail
1305        let result = glcm_construct_simd(&quantized, &mut glcm, 10, 10, 257, 1, 0);
1306        assert!(result.is_err());
1307    }
1308
1309    #[test]
1310    fn test_texture_homogeneity() {
1311        let mut glcm = vec![0.0_f32; 16]; // 4x4 GLCM
1312
1313        // Diagonal matrix should have maximum homogeneity
1314        glcm[0] = 0.25;
1315        glcm[5] = 0.25;
1316        glcm[10] = 0.25;
1317        glcm[15] = 0.25;
1318
1319        let homogeneity =
1320            texture_homogeneity_simd(&glcm, 4).expect("Failed to compute texture homogeneity");
1321
1322        // Homogeneity for diagonal should be 1.0
1323        assert_abs_diff_eq!(homogeneity, 1.0, epsilon = 0.001);
1324    }
1325
1326    #[test]
1327    fn test_glcm_uniform() {
1328        let data = vec![5u8; 100]; // Uniform image
1329        let mut glcm = vec![0.0_f32; 256 * 256];
1330
1331        compute_glcm_simd(&data, &mut glcm, 10, 10, 1, 0, 256)
1332            .expect("Failed to compute GLCM for uniform image");
1333
1334        // Uniform image should have all weight at one cell
1335        let max_val = glcm.iter().copied().fold(f32::NEG_INFINITY, f32::max);
1336        assert!(max_val > 0.0);
1337    }
1338
1339    #[test]
1340    fn test_glcm_multidirectional() {
1341        let data = vec![128u8; 100]; // Uniform image
1342        let mut glcm = vec![0.0_f32; 16 * 16];
1343
1344        compute_glcm_multidirectional_simd(&data, &mut glcm, 10, 10, 1, 16)
1345            .expect("Failed to compute multidirectional GLCM");
1346
1347        // Sum should be 1.0 after normalization
1348        let sum: f32 = glcm.iter().sum();
1349        assert_abs_diff_eq!(sum, 1.0, epsilon = 0.001);
1350    }
1351
1352    #[test]
1353    fn test_all_texture_features() {
1354        let mut glcm = vec![0.0_f32; 16];
1355        glcm[0] = 0.5;
1356        glcm[5] = 0.3;
1357        glcm[10] = 0.2;
1358
1359        let features = compute_all_texture_features_simd(&glcm, 4)
1360            .expect("Failed to compute all texture features");
1361
1362        assert!(features.contrast.is_finite());
1363        assert!(features.energy.is_finite());
1364        assert!(features.homogeneity.is_finite());
1365    }
1366
1367    #[test]
1368    fn test_texture_feature_image() {
1369        let data = vec![128u8; 100];
1370        let mut output = vec![0.0_f32; 100];
1371
1372        compute_texture_feature_image_simd(
1373            &data,
1374            &mut output,
1375            10,
1376            10,
1377            3,
1378            1,
1379            8,
1380            TextureFeatureType::Contrast,
1381        )
1382        .expect("Failed to compute texture feature image");
1383
1384        // All contrast values should be 0 for uniform image
1385        for &val in &output {
1386            assert_abs_diff_eq!(val, 0.0, epsilon = 0.01);
1387        }
1388    }
1389
1390    #[test]
1391    fn test_lbp_uniform() {
1392        let data = vec![128u8; 100]; // Uniform image
1393        let mut output = vec![0u8; 100];
1394
1395        compute_lbp_simd(&data, &mut output, 10, 10)
1396            .expect("Failed to compute LBP for uniform image");
1397
1398        // All interior pixels should be 255 (all neighbors equal to center)
1399        for y in 1..9 {
1400            for x in 1..9 {
1401                assert_eq!(output[y * 10 + x], 255);
1402            }
1403        }
1404    }
1405}