Skip to main content

oxigdal_algorithms/raster/
texture.rs

1//! Texture analysis using Gray-Level Co-occurrence Matrix (GLCM)
2//!
3//! This module provides texture analysis algorithms based on GLCM and Haralick features.
4//! These features quantify the spatial arrangement of pixel intensities and are widely
5//! used in image classification, segmentation, and pattern recognition.
6//!
7//! # GLCM (Gray-Level Co-occurrence Matrix)
8//!
9//! The GLCM is a statistical method of examining texture that considers the spatial
10//! relationship of pixels. It calculates how often pairs of pixels with specific values
11//! occur in a specified spatial relationship.
12//!
13//! # Haralick Features
14//!
15//! Haralick features are statistics computed from the GLCM that capture different
16//! aspects of texture:
17//!
18//! - **Contrast**: Measures local intensity variation
19//! - **Correlation**: Measures linear dependency of gray levels
20//! - **Energy (Angular Second Moment)**: Measures textural uniformity
21//! - **Homogeneity (Inverse Difference Moment)**: Measures smoothness
22//! - **Entropy**: Measures randomness/complexity
23//! - **Dissimilarity**: Similar to contrast but with linear weighting
24//! - **Variance**: Measures dispersion around the mean
25//! - **Sum Average**: Average of sum of probabilities
26//! - **Sum Entropy**: Entropy of sum probabilities
27//! - **Difference Entropy**: Entropy of difference probabilities
28//! - **Information Measure of Correlation**: Mutual information measures
29//! - **Maximum Correlation Coefficient**: Maximum correlation in GLCM
30//!
31//! # Example
32//!
33//! ```ignore
34//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
35//! use oxigdal_algorithms::raster::texture::{
36//!     compute_glcm, compute_haralick_features, Direction, GlcmParams
37//! };
38//! use oxigdal_core::buffer::RasterBuffer;
39//! use oxigdal_core::types::RasterDataType;
40//!
41//! let src = RasterBuffer::zeros(100, 100, RasterDataType::UInt8);
42//! let params = GlcmParams::default();
43//!
44//! // Compute GLCM for horizontal direction
45//! let glcm = compute_glcm(&src, Direction::Horizontal, 1, &params)?;
46//!
47//! // Compute Haralick features
48//! let features = compute_haralick_features(&glcm);
49//! println!("Contrast: {}", features.contrast);
50//! println!("Energy: {}", features.energy);
51//! # Ok(())
52//! # }
53//! ```
54
55use crate::error::{AlgorithmError, Result};
56use oxigdal_core::buffer::RasterBuffer;
57
58#[cfg(feature = "parallel")]
59use rayon::prelude::*;
60
61/// Direction for GLCM computation
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Direction {
64    /// Horizontal (0°)
65    Horizontal,
66
67    /// Vertical (90°)
68    Vertical,
69
70    /// Diagonal down-right (45°)
71    Diagonal45,
72
73    /// Diagonal down-left (135°)
74    Diagonal135,
75
76    /// Custom direction (dx, dy)
77    Custom(i64, i64),
78}
79
80impl Direction {
81    /// Gets the offset for this direction
82    #[must_use]
83    pub fn offset(&self, distance: u32) -> (i64, i64) {
84        let d = distance as i64;
85        match self {
86            Self::Horizontal => (d, 0),
87            Self::Vertical => (0, d),
88            Self::Diagonal45 => (d, -d),
89            Self::Diagonal135 => (d, d),
90            Self::Custom(dx, dy) => (*dx * d, *dy * d),
91        }
92    }
93
94    /// Returns all standard directions
95    #[must_use]
96    pub fn all_standard() -> Vec<Self> {
97        vec![
98            Self::Horizontal,
99            Self::Vertical,
100            Self::Diagonal45,
101            Self::Diagonal135,
102        ]
103    }
104}
105
106/// Parameters for GLCM computation
107#[derive(Debug, Clone)]
108pub struct GlcmParams {
109    /// Number of gray levels to use (quantization)
110    pub gray_levels: usize,
111
112    /// Whether to normalize the GLCM
113    pub normalize: bool,
114
115    /// Whether to make the GLCM symmetric
116    pub symmetric: bool,
117
118    /// Window size for local GLCM computation (None = global)
119    pub window_size: Option<usize>,
120}
121
122impl Default for GlcmParams {
123    fn default() -> Self {
124        Self {
125            gray_levels: 256,
126            normalize: true,
127            symmetric: true,
128            window_size: None,
129        }
130    }
131}
132
133/// Gray-Level Co-occurrence Matrix
134#[derive(Debug, Clone)]
135pub struct Glcm {
136    /// The co-occurrence matrix (gray_levels x gray_levels)
137    matrix: Vec<Vec<f64>>,
138
139    /// Number of gray levels
140    gray_levels: usize,
141
142    /// Direction used
143    direction: Direction,
144
145    /// Distance used
146    distance: u32,
147
148    /// Whether the matrix is normalized
149    normalized: bool,
150}
151
152impl Glcm {
153    /// Creates a new GLCM with given size
154    #[must_use]
155    pub fn new(gray_levels: usize, direction: Direction, distance: u32) -> Self {
156        Self {
157            matrix: vec![vec![0.0; gray_levels]; gray_levels],
158            gray_levels,
159            direction,
160            distance,
161            normalized: false,
162        }
163    }
164
165    /// Gets the value at position (i, j)
166    #[must_use]
167    pub fn get(&self, i: usize, j: usize) -> f64 {
168        if i < self.gray_levels && j < self.gray_levels {
169            self.matrix[i][j]
170        } else {
171            0.0
172        }
173    }
174
175    /// Sets the value at position (i, j)
176    pub fn set(&mut self, i: usize, j: usize, value: f64) {
177        if i < self.gray_levels && j < self.gray_levels {
178            self.matrix[i][j] = value;
179        }
180    }
181
182    /// Increments the value at position (i, j)
183    pub fn increment(&mut self, i: usize, j: usize) {
184        if i < self.gray_levels && j < self.gray_levels {
185            self.matrix[i][j] += 1.0;
186        }
187    }
188
189    /// Normalizes the GLCM
190    pub fn normalize(&mut self) {
191        let sum: f64 = self.matrix.iter().flat_map(|row| row.iter()).sum();
192
193        if sum > 0.0 {
194            for row in &mut self.matrix {
195                for val in row {
196                    *val /= sum;
197                }
198            }
199            self.normalized = true;
200        }
201    }
202
203    /// Makes the GLCM symmetric
204    pub fn make_symmetric(&mut self) {
205        for i in 0..self.gray_levels {
206            for j in 0..self.gray_levels {
207                let avg = (self.matrix[i][j] + self.matrix[j][i]) / 2.0;
208                self.matrix[i][j] = avg;
209                self.matrix[j][i] = avg;
210            }
211        }
212    }
213
214    /// Returns the gray levels
215    #[must_use]
216    pub fn gray_levels(&self) -> usize {
217        self.gray_levels
218    }
219
220    /// Returns the direction
221    #[must_use]
222    pub fn direction(&self) -> Direction {
223        self.direction
224    }
225
226    /// Returns the distance
227    #[must_use]
228    pub fn distance(&self) -> u32 {
229        self.distance
230    }
231
232    /// Returns whether normalized
233    #[must_use]
234    pub fn is_normalized(&self) -> bool {
235        self.normalized
236    }
237
238    /// Returns a reference to the matrix
239    #[must_use]
240    pub fn matrix(&self) -> &[Vec<f64>] {
241        &self.matrix
242    }
243}
244
245/// Haralick texture features computed from GLCM
246#[derive(Debug, Clone, Default)]
247pub struct HaralickFeatures {
248    /// Contrast (variance of differences)
249    pub contrast: f64,
250
251    /// Correlation (linear dependency)
252    pub correlation: f64,
253
254    /// Energy/Angular Second Moment (uniformity)
255    pub energy: f64,
256
257    /// Homogeneity/Inverse Difference Moment (smoothness)
258    pub homogeneity: f64,
259
260    /// Entropy (randomness)
261    pub entropy: f64,
262
263    /// Dissimilarity (linear contrast)
264    pub dissimilarity: f64,
265
266    /// Variance
267    pub variance: f64,
268
269    /// Sum average
270    pub sum_average: f64,
271
272    /// Sum entropy
273    pub sum_entropy: f64,
274
275    /// Difference entropy
276    pub difference_entropy: f64,
277
278    /// Information measure of correlation 1
279    pub info_measure_corr1: f64,
280
281    /// Information measure of correlation 2
282    pub info_measure_corr2: f64,
283
284    /// Maximum correlation coefficient
285    pub max_correlation_coeff: f64,
286
287    /// Cluster shade
288    pub cluster_shade: f64,
289
290    /// Cluster prominence
291    pub cluster_prominence: f64,
292}
293
294/// Computes the Gray-Level Co-occurrence Matrix
295///
296/// # Arguments
297///
298/// * `src` - Source raster buffer
299/// * `direction` - Direction to compute co-occurrence
300/// * `distance` - Distance between pixel pairs
301/// * `params` - GLCM parameters
302///
303/// # Errors
304///
305/// Returns an error if the operation fails
306pub fn compute_glcm(
307    src: &RasterBuffer,
308    direction: Direction,
309    distance: u32,
310    params: &GlcmParams,
311) -> Result<Glcm> {
312    let width = src.width();
313    let height = src.height();
314
315    if params.gray_levels == 0 {
316        return Err(AlgorithmError::InvalidParameter {
317            parameter: "gray_levels",
318            message: "Gray levels must be greater than zero".to_string(),
319        });
320    }
321
322    // Quantize image to specified gray levels
323    let quantized = quantize_image(src, params.gray_levels)?;
324
325    let (dx, dy) = direction.offset(distance);
326    let mut glcm = Glcm::new(params.gray_levels, direction, distance);
327
328    // Compute co-occurrence matrix
329    for y in 0..height {
330        for x in 0..width {
331            let nx = x as i64 + dx;
332            let ny = y as i64 + dy;
333
334            if nx >= 0 && nx < width as i64 && ny >= 0 && ny < height as i64 {
335                let i = quantized.get_pixel(x, y).map_err(AlgorithmError::Core)? as usize;
336                let j = quantized
337                    .get_pixel(nx as u64, ny as u64)
338                    .map_err(AlgorithmError::Core)? as usize;
339
340                glcm.increment(i, j);
341            }
342        }
343    }
344
345    // Make symmetric if requested
346    if params.symmetric {
347        glcm.make_symmetric();
348    }
349
350    // Normalize if requested
351    if params.normalize {
352        glcm.normalize();
353    }
354
355    Ok(glcm)
356}
357
358/// Computes GLCM for multiple directions and averages
359///
360/// # Arguments
361///
362/// * `src` - Source raster buffer
363/// * `directions` - Directions to compute
364/// * `distance` - Distance between pixel pairs
365/// * `params` - GLCM parameters
366///
367/// # Errors
368///
369/// Returns an error if the operation fails
370pub fn compute_glcm_multi_direction(
371    src: &RasterBuffer,
372    directions: &[Direction],
373    distance: u32,
374    params: &GlcmParams,
375) -> Result<Glcm> {
376    if directions.is_empty() {
377        return Err(AlgorithmError::InvalidParameter {
378            parameter: "directions",
379            message: "At least one direction required".to_string(),
380        });
381    }
382
383    let mut avg_glcm = Glcm::new(params.gray_levels, directions[0], distance);
384
385    for direction in directions {
386        let glcm = compute_glcm(src, *direction, distance, params)?;
387
388        for i in 0..params.gray_levels {
389            for j in 0..params.gray_levels {
390                let val = avg_glcm.get(i, j) + glcm.get(i, j);
391                avg_glcm.set(i, j, val);
392            }
393        }
394    }
395
396    // Average
397    for i in 0..params.gray_levels {
398        for j in 0..params.gray_levels {
399            let val = avg_glcm.get(i, j) / directions.len() as f64;
400            avg_glcm.set(i, j, val);
401        }
402    }
403
404    Ok(avg_glcm)
405}
406
407/// Computes Haralick texture features from a GLCM
408///
409/// # Arguments
410///
411/// * `glcm` - Gray-Level Co-occurrence Matrix
412///
413/// # Returns
414///
415/// Haralick features structure with all computed features
416#[must_use]
417pub fn compute_haralick_features(glcm: &Glcm) -> HaralickFeatures {
418    let n = glcm.gray_levels();
419    let matrix = glcm.matrix();
420
421    // Compute marginal probabilities
422    let mut px = vec![0.0; n];
423    let mut py = vec![0.0; n];
424
425    for i in 0..n {
426        for j in 0..n {
427            px[i] += matrix[i][j];
428            py[j] += matrix[i][j];
429        }
430    }
431
432    // Compute means and standard deviations
433    let mut mu_x = 0.0;
434    let mut mu_y = 0.0;
435
436    for i in 0..n {
437        mu_x += i as f64 * px[i];
438        mu_y += i as f64 * py[i];
439    }
440
441    let mut sigma_x = 0.0;
442    let mut sigma_y = 0.0;
443
444    for i in 0..n {
445        sigma_x += (i as f64 - mu_x).powi(2) * px[i];
446        sigma_y += (i as f64 - mu_y).powi(2) * py[i];
447    }
448
449    sigma_x = sigma_x.sqrt();
450    sigma_y = sigma_y.sqrt();
451
452    // Compute sum and difference probabilities
453    let max_sum = 2 * (n - 1);
454    let mut p_x_plus_y = vec![0.0; max_sum + 1];
455    let mut p_x_minus_y = vec![0.0; n];
456
457    for i in 0..n {
458        for j in 0..n {
459            p_x_plus_y[i + j] += matrix[i][j];
460            let diff = (i as i64 - j as i64).unsigned_abs() as usize;
461            p_x_minus_y[diff] += matrix[i][j];
462        }
463    }
464
465    let mut features = HaralickFeatures::default();
466
467    // 1. Contrast
468    features.contrast = (0..n).map(|k| k.pow(2) as f64 * p_x_minus_y[k]).sum();
469
470    // 2. Correlation
471    if sigma_x > 0.0 && sigma_y > 0.0 {
472        features.correlation = (0..n)
473            .flat_map(|i| {
474                (0..n).map(move |j| {
475                    (i as f64 - mu_x) * (j as f64 - mu_y) * matrix[i][j] / (sigma_x * sigma_y)
476                })
477            })
478            .sum();
479    }
480
481    // 3. Energy (Angular Second Moment)
482    features.energy = (0..n)
483        .flat_map(|i| (0..n).map(move |j| matrix[i][j].powi(2)))
484        .sum();
485
486    // 4. Homogeneity (Inverse Difference Moment)
487    features.homogeneity = (0..n)
488        .flat_map(|i| (0..n).map(move |j| matrix[i][j] / (1.0 + (i as f64 - j as f64).powi(2))))
489        .sum();
490
491    // 5. Entropy
492    features.entropy = -(0..n)
493        .flat_map(|i| {
494            (0..n).map(move |j| {
495                let p = matrix[i][j];
496                if p > 0.0 { p * p.ln() } else { 0.0 }
497            })
498        })
499        .sum::<f64>();
500
501    // 6. Dissimilarity
502    features.dissimilarity = (0..n)
503        .flat_map(|i| (0..n).map(move |j| (i as f64 - j as f64).abs() * matrix[i][j]))
504        .sum();
505
506    // 7. Variance
507    let mu = (0..n)
508        .flat_map(|i| (0..n).map(move |j| (i + j) as f64 * matrix[i][j]))
509        .sum::<f64>()
510        / 2.0;
511
512    features.variance = (0..n)
513        .flat_map(|i| (0..n).map(move |j| ((i + j) as f64 / 2.0 - mu).powi(2) * matrix[i][j]))
514        .sum();
515
516    // 8. Sum Average
517    features.sum_average = (0..=max_sum).map(|k| k as f64 * p_x_plus_y[k]).sum();
518
519    // 9. Sum Entropy
520    features.sum_entropy = -(0..=max_sum)
521        .map(|k| {
522            let p = p_x_plus_y[k];
523            if p > 0.0 { p * p.ln() } else { 0.0 }
524        })
525        .sum::<f64>();
526
527    // 10. Difference Entropy
528    features.difference_entropy = -(0..n)
529        .map(|k| {
530            let p = p_x_minus_y[k];
531            if p > 0.0 { p * p.ln() } else { 0.0 }
532        })
533        .sum::<f64>();
534
535    // 11. Information Measures of Correlation
536    let hx = -px
537        .iter()
538        .map(|&p| if p > 0.0 { p * p.ln() } else { 0.0 })
539        .sum::<f64>();
540
541    let hy = -py
542        .iter()
543        .map(|&p| if p > 0.0 { p * p.ln() } else { 0.0 })
544        .sum::<f64>();
545
546    let hxy = features.entropy;
547
548    let px_clone = px.clone();
549    let py_clone = py.clone();
550    let hxy1 = -(0..n)
551        .flat_map(|i| {
552            let px = px_clone.clone();
553            let py = py_clone.clone();
554            (0..n).map(move |j| {
555                let p = matrix[i][j];
556                if p > 0.0 && px[i] > 0.0 && py[j] > 0.0 {
557                    p * (px[i] * py[j]).ln()
558                } else {
559                    0.0
560                }
561            })
562        })
563        .sum::<f64>();
564
565    let hxy2 = -(0..n)
566        .flat_map(|i| {
567            let px = px.clone();
568            let py = py.clone();
569            (0..n).map(move |j| {
570                let p = px[i] * py[j];
571                if p > 0.0 { p * p.ln() } else { 0.0 }
572            })
573        })
574        .sum::<f64>();
575
576    let max_hxy = hx.max(hy);
577    if max_hxy > 0.0 {
578        features.info_measure_corr1 = (hxy - hxy1) / max_hxy;
579    }
580
581    if hxy2 > hxy {
582        features.info_measure_corr2 = (1.0 - (-2.0 * (hxy2 - hxy)).exp()).sqrt();
583    }
584
585    // 12. Maximum Correlation Coefficient
586    // This is computationally expensive (requires eigenvalues)
587    // Simplified approximation
588    features.max_correlation_coeff = features.correlation.abs();
589
590    // 13. Cluster Shade
591    features.cluster_shade = (0..n)
592        .flat_map(|i| {
593            (0..n).map(move |j| (i as f64 + j as f64 - mu_x - mu_y).powi(3) * matrix[i][j])
594        })
595        .sum();
596
597    // 14. Cluster Prominence
598    features.cluster_prominence = (0..n)
599        .flat_map(|i| {
600            (0..n).map(move |j| (i as f64 + j as f64 - mu_x - mu_y).powi(4) * matrix[i][j])
601        })
602        .sum();
603
604    features
605}
606
607/// Computes a texture feature image for a single feature
608///
609/// # Arguments
610///
611/// * `src` - Source raster buffer
612/// * `feature_name` - Name of feature to compute
613/// * `direction` - Direction for GLCM
614/// * `distance` - Distance for GLCM
615/// * `window_size` - Size of moving window
616/// * `params` - GLCM parameters
617///
618/// # Errors
619///
620/// Returns an error if the operation fails
621pub fn compute_texture_feature_image(
622    src: &RasterBuffer,
623    feature_name: &str,
624    direction: Direction,
625    distance: u32,
626    window_size: usize,
627    params: &GlcmParams,
628) -> Result<RasterBuffer> {
629    if window_size % 2 == 0 {
630        return Err(AlgorithmError::InvalidParameter {
631            parameter: "window_size",
632            message: "Window size must be odd".to_string(),
633        });
634    }
635
636    let width = src.width();
637    let height = src.height();
638    let mut dst = RasterBuffer::zeros(width, height, oxigdal_core::types::RasterDataType::Float64);
639
640    let hw = (window_size / 2) as i64;
641
642    #[cfg(feature = "parallel")]
643    {
644        let results: Result<Vec<_>> = (hw as u64..(height - hw as u64))
645            .into_par_iter()
646            .map(|y| {
647                let mut row_data = Vec::new();
648                for x in hw as u64..(width - hw as u64) {
649                    let value = compute_local_texture_feature(
650                        src,
651                        x,
652                        y,
653                        window_size,
654                        feature_name,
655                        direction,
656                        distance,
657                        params,
658                    )?;
659                    row_data.push((x, value));
660                }
661                Ok((y, row_data))
662            })
663            .collect();
664
665        for (y, row_data) in results? {
666            for (x, value) in row_data {
667                dst.set_pixel(x, y, value).map_err(AlgorithmError::Core)?;
668            }
669        }
670    }
671
672    #[cfg(not(feature = "parallel"))]
673    {
674        for y in hw as u64..(height - hw as u64) {
675            for x in hw as u64..(width - hw as u64) {
676                let value = compute_local_texture_feature(
677                    src,
678                    x,
679                    y,
680                    window_size,
681                    feature_name,
682                    direction,
683                    distance,
684                    params,
685                )?;
686                dst.set_pixel(x, y, value).map_err(AlgorithmError::Core)?;
687            }
688        }
689    }
690
691    Ok(dst)
692}
693
694/// Computes texture feature for a local window
695fn compute_local_texture_feature(
696    src: &RasterBuffer,
697    cx: u64,
698    cy: u64,
699    window_size: usize,
700    feature_name: &str,
701    direction: Direction,
702    distance: u32,
703    params: &GlcmParams,
704) -> Result<f64> {
705    use oxigdal_core::types::RasterDataType;
706
707    let hw = (window_size / 2) as i64;
708
709    // Extract window
710    let mut window = RasterBuffer::zeros(
711        window_size as u64,
712        window_size as u64,
713        RasterDataType::Float64,
714    );
715
716    for wy in 0..window_size {
717        for wx in 0..window_size {
718            let sx = (cx as i64 + wx as i64 - hw) as u64;
719            let sy = (cy as i64 + wy as i64 - hw) as u64;
720            let val = src.get_pixel(sx, sy).map_err(AlgorithmError::Core)?;
721            window
722                .set_pixel(wx as u64, wy as u64, val)
723                .map_err(AlgorithmError::Core)?;
724        }
725    }
726
727    // Compute GLCM for window
728    let glcm = compute_glcm(&window, direction, distance, params)?;
729    let features = compute_haralick_features(&glcm);
730
731    // Extract requested feature
732    let value = match feature_name {
733        "contrast" => features.contrast,
734        "correlation" => features.correlation,
735        "energy" => features.energy,
736        "homogeneity" => features.homogeneity,
737        "entropy" => features.entropy,
738        "dissimilarity" => features.dissimilarity,
739        "variance" => features.variance,
740        "sum_average" => features.sum_average,
741        "sum_entropy" => features.sum_entropy,
742        "difference_entropy" => features.difference_entropy,
743        "info_measure_corr1" => features.info_measure_corr1,
744        "info_measure_corr2" => features.info_measure_corr2,
745        "max_correlation_coeff" => features.max_correlation_coeff,
746        "cluster_shade" => features.cluster_shade,
747        "cluster_prominence" => features.cluster_prominence,
748        _ => {
749            return Err(AlgorithmError::InvalidParameter {
750                parameter: "feature_name",
751                message: format!("Unknown feature: {}", feature_name),
752            });
753        }
754    };
755
756    Ok(value)
757}
758
759/// Quantizes an image to specified number of gray levels
760fn quantize_image(src: &RasterBuffer, gray_levels: usize) -> Result<RasterBuffer> {
761    use oxigdal_core::types::RasterDataType;
762
763    let width = src.width();
764    let height = src.height();
765
766    // Find min and max values
767    let mut min_val = f64::INFINITY;
768    let mut max_val = f64::NEG_INFINITY;
769
770    for y in 0..height {
771        for x in 0..width {
772            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
773            min_val = min_val.min(val);
774            max_val = max_val.max(val);
775        }
776    }
777
778    let range = max_val - min_val;
779    if range == 0.0 {
780        return Ok(RasterBuffer::zeros(width, height, RasterDataType::UInt8));
781    }
782
783    // Quantize
784    let mut quantized = RasterBuffer::zeros(width, height, RasterDataType::UInt8);
785    let scale = (gray_levels - 1) as f64 / range;
786
787    for y in 0..height {
788        for x in 0..width {
789            let val = src.get_pixel(x, y).map_err(AlgorithmError::Core)?;
790            let level = ((val - min_val) * scale).round() as u8;
791            let clamped = level.min((gray_levels - 1) as u8);
792            quantized
793                .set_pixel(x, y, f64::from(clamped))
794                .map_err(AlgorithmError::Core)?;
795        }
796    }
797
798    Ok(quantized)
799}
800
801/// Computes all Haralick features as images
802///
803/// # Arguments
804///
805/// * `src` - Source raster buffer
806/// * `direction` - Direction for GLCM
807/// * `distance` - Distance for GLCM
808/// * `window_size` - Size of moving window
809/// * `params` - GLCM parameters
810///
811/// # Errors
812///
813/// Returns an error if the operation fails
814#[allow(clippy::type_complexity)]
815pub fn compute_all_texture_features(
816    src: &RasterBuffer,
817    direction: Direction,
818    distance: u32,
819    window_size: usize,
820    params: &GlcmParams,
821) -> Result<Vec<(&'static str, RasterBuffer)>> {
822    let features = [
823        "contrast",
824        "correlation",
825        "energy",
826        "homogeneity",
827        "entropy",
828        "dissimilarity",
829        "variance",
830        "sum_average",
831        "sum_entropy",
832        "difference_entropy",
833    ];
834
835    let mut results = Vec::new();
836
837    for &feature in &features {
838        let image =
839            compute_texture_feature_image(src, feature, direction, distance, window_size, params)?;
840        results.push((feature, image));
841    }
842
843    Ok(results)
844}
845
846#[cfg(test)]
847mod tests {
848    use super::*;
849    use approx::assert_abs_diff_eq;
850    use oxigdal_core::types::RasterDataType;
851
852    #[test]
853    fn test_direction_offset() {
854        assert_eq!(Direction::Horizontal.offset(1), (1, 0));
855        assert_eq!(Direction::Vertical.offset(1), (0, 1));
856        assert_eq!(Direction::Diagonal45.offset(1), (1, -1));
857        assert_eq!(Direction::Diagonal135.offset(1), (1, 1));
858    }
859
860    #[test]
861    fn test_glcm_params_default() {
862        let params = GlcmParams::default();
863        assert_eq!(params.gray_levels, 256);
864        assert!(params.normalize);
865        assert!(params.symmetric);
866        assert!(params.window_size.is_none());
867    }
868
869    #[test]
870    fn test_glcm_creation() {
871        let glcm = Glcm::new(256, Direction::Horizontal, 1);
872        assert_eq!(glcm.gray_levels(), 256);
873        assert_eq!(glcm.direction(), Direction::Horizontal);
874        assert_eq!(glcm.distance(), 1);
875        assert!(!glcm.is_normalized());
876    }
877
878    #[test]
879    fn test_glcm_get_set() {
880        let mut glcm = Glcm::new(8, Direction::Horizontal, 1);
881        glcm.set(2, 3, 5.0);
882        assert_abs_diff_eq!(glcm.get(2, 3), 5.0);
883    }
884
885    #[test]
886    fn test_glcm_normalize() {
887        let mut glcm = Glcm::new(2, Direction::Horizontal, 1);
888        glcm.set(0, 0, 2.0);
889        glcm.set(0, 1, 3.0);
890        glcm.set(1, 0, 3.0);
891        glcm.set(1, 1, 2.0);
892
893        glcm.normalize();
894
895        assert!(glcm.is_normalized());
896        assert_abs_diff_eq!(glcm.get(0, 0), 0.2);
897        assert_abs_diff_eq!(glcm.get(0, 1), 0.3);
898    }
899
900    #[test]
901    fn test_compute_glcm() {
902        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
903
904        // Create a simple pattern
905        for y in 0..10 {
906            for x in 0..10 {
907                let val = if (x + y) % 2 == 0 { 0.0 } else { 255.0 };
908                src.set_pixel(x, y, val)
909                    .expect("setting pixel should succeed in test");
910            }
911        }
912
913        let params = GlcmParams {
914            gray_levels: 2,
915            normalize: true,
916            symmetric: true,
917            window_size: None,
918        };
919
920        let glcm = compute_glcm(&src, Direction::Horizontal, 1, &params)
921            .expect("compute_glcm should succeed in test");
922
923        assert!(glcm.is_normalized());
924        assert_eq!(glcm.gray_levels(), 2);
925    }
926
927    #[test]
928    fn test_haralick_features() {
929        let mut glcm = Glcm::new(2, Direction::Horizontal, 1);
930
931        // Create a simple GLCM
932        glcm.set(0, 0, 0.25);
933        glcm.set(0, 1, 0.25);
934        glcm.set(1, 0, 0.25);
935        glcm.set(1, 1, 0.25);
936
937        let features = compute_haralick_features(&glcm);
938
939        // Uniform distribution should have maximum entropy
940        assert!(features.energy > 0.0);
941        assert!(features.entropy > 0.0);
942        assert_abs_diff_eq!(features.energy, 0.25, epsilon = 0.01);
943    }
944
945    #[test]
946    fn test_quantize_image() {
947        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::Float64);
948
949        for y in 0..10 {
950            for x in 0..10 {
951                src.set_pixel(x, y, (x * 10 + y) as f64)
952                    .expect("setting pixel should succeed in test");
953            }
954        }
955
956        let quantized = quantize_image(&src, 8).expect("quantize_image should succeed in test");
957
958        // Check that all values are within range
959        for y in 0..10 {
960            for x in 0..10 {
961                let val = quantized
962                    .get_pixel(x, y)
963                    .expect("getting pixel should succeed in test");
964                assert!((0.0..8.0).contains(&val));
965            }
966        }
967    }
968
969    #[test]
970    fn test_multi_direction_glcm() {
971        let mut src = RasterBuffer::zeros(10, 10, RasterDataType::UInt8);
972
973        for y in 0..10 {
974            for x in 0..10 {
975                src.set_pixel(x, y, ((x + y) % 4 * 64) as f64)
976                    .expect("setting pixel should succeed in test");
977            }
978        }
979
980        let params = GlcmParams {
981            gray_levels: 4,
982            normalize: true,
983            symmetric: true,
984            window_size: None,
985        };
986
987        let directions = Direction::all_standard();
988        let glcm = compute_glcm_multi_direction(&src, &directions, 1, &params)
989            .expect("compute_glcm_multi_direction should succeed in test");
990
991        assert_eq!(glcm.gray_levels(), 4);
992    }
993
994    #[test]
995    fn test_texture_feature_names() {
996        let mut glcm = Glcm::new(4, Direction::Horizontal, 1);
997
998        // Create a simple non-uniform GLCM
999        glcm.set(0, 0, 0.5);
1000        glcm.set(1, 1, 0.3);
1001        glcm.set(2, 2, 0.2);
1002
1003        let features = compute_haralick_features(&glcm);
1004
1005        // All features should be finite
1006        assert!(features.contrast.is_finite());
1007        assert!(features.correlation.is_finite());
1008        assert!(features.energy.is_finite());
1009        assert!(features.homogeneity.is_finite());
1010        assert!(features.entropy.is_finite());
1011        assert!(features.dissimilarity.is_finite());
1012        assert!(features.variance.is_finite());
1013    }
1014}