Skip to main content

oxigeo_algorithms/simd/
texture_simd.rs

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