Skip to main content

scirs2_vision/feature/
glcm.rs

1//! Gray Level Co-occurrence Matrix (GLCM) for texture analysis
2//!
3//! GLCM is a statistical method of examining texture that considers
4//! the spatial relationship of pixels.
5
6use crate::error::Result;
7use image::{DynamicImage, GrayImage};
8use scirs2_core::ndarray::{Array2, Axis};
9
10/// Direction for GLCM computation
11#[derive(Debug, Clone, Copy)]
12pub enum GLCMDirection {
13    /// Horizontal (0 degrees)
14    Horizontal,
15    /// Vertical (90 degrees)
16    Vertical,
17    /// Diagonal (45 degrees)
18    Diagonal,
19    /// Anti-diagonal (135 degrees)
20    AntiDiagonal,
21}
22
23impl GLCMDirection {
24    /// Get offset for this direction
25    fn get_offset(&self, distance: i32) -> (i32, i32) {
26        match self {
27            GLCMDirection::Horizontal => (distance, 0),
28            GLCMDirection::Vertical => (0, distance),
29            GLCMDirection::Diagonal => (distance, distance),
30            GLCMDirection::AntiDiagonal => (distance, -distance),
31        }
32    }
33}
34
35/// Parameters for GLCM computation
36#[derive(Debug, Clone)]
37pub struct GLCMParams {
38    /// Number of gray levels to quantize to
39    pub levels: usize,
40    /// Distance between pixel pairs
41    pub distance: i32,
42    /// Direction to compute GLCM
43    pub direction: GLCMDirection,
44    /// Whether to make the matrix symmetric
45    pub symmetric: bool,
46    /// Whether to normalize the matrix
47    pub normalize: bool,
48}
49
50impl Default for GLCMParams {
51    fn default() -> Self {
52        Self {
53            levels: 8,
54            distance: 1,
55            direction: GLCMDirection::Horizontal,
56            symmetric: true,
57            normalize: true,
58        }
59    }
60}
61
62/// Compute Gray Level Co-occurrence Matrix
63///
64/// # Arguments
65///
66/// * `img` - Input grayscale image
67/// * `params` - GLCM parameters
68///
69/// # Returns
70///
71/// * Result containing the GLCM as a 2D array
72#[allow(dead_code)]
73pub fn computeglcm(img: &DynamicImage, params: &GLCMParams) -> Result<Array2<f64>> {
74    let gray = img.to_luma8();
75    let (width, height) = gray.dimensions();
76
77    // Quantize the image to specified levels
78    let quantized = quantize_image(&gray, params.levels);
79
80    // Initialize GLCM
81    let mut glcm = Array2::zeros((params.levels, params.levels));
82
83    // Get direction offset
84    let (dx, dy) = params.direction.get_offset(params.distance);
85
86    // Compute co-occurrences
87    for y in 0..height as i32 {
88        for x in 0..width as i32 {
89            let x2 = x + dx;
90            let y2 = y + dy;
91
92            // Check bounds
93            if x2 >= 0 && x2 < width as i32 && y2 >= 0 && y2 < height as i32 {
94                let i = quantized[[y as usize, x as usize]];
95                let j = quantized[[y2 as usize, x2 as usize]];
96
97                glcm[[i, j]] += 1.0;
98
99                // Add symmetric pair
100                if params.symmetric {
101                    glcm[[j, i]] += 1.0;
102                }
103            }
104        }
105    }
106
107    // Normalize if requested
108    if params.normalize {
109        let sum = glcm.sum();
110        if sum > 0.0 {
111            glcm /= sum;
112        }
113    }
114
115    Ok(glcm)
116}
117
118/// Quantize image to specified number of levels
119#[allow(dead_code)]
120fn quantize_image(img: &GrayImage, levels: usize) -> Array2<usize> {
121    let (width, height) = img.dimensions();
122    let mut quantized = Array2::zeros((height as usize, width as usize));
123
124    let scale = 256.0 / levels as f32;
125
126    for y in 0..height {
127        for x in 0..width {
128            let value = img.get_pixel(x, y)[0] as f32;
129            let level = (value / scale).floor() as usize;
130            quantized[[y as usize, x as usize]] = level.min(levels - 1);
131        }
132    }
133
134    quantized
135}
136
137/// Haralick texture features from GLCM
138#[derive(Debug, Clone)]
139pub struct HaralickFeatures {
140    /// Angular Second Moment (Energy)
141    pub energy: f64,
142    /// Contrast
143    pub contrast: f64,
144    /// Correlation
145    pub correlation: f64,
146    /// Homogeneity (Inverse Difference Moment)
147    pub homogeneity: f64,
148    /// Entropy
149    pub entropy: f64,
150    /// Dissimilarity
151    pub dissimilarity: f64,
152    /// Maximum probability
153    pub max_probability: f64,
154}
155
156/// Compute Haralick texture features from GLCM
157///
158/// # Arguments
159///
160/// * `glcm` - Gray Level Co-occurrence Matrix
161///
162/// # Returns
163///
164/// * Haralick features
165#[allow(dead_code)]
166pub fn compute_haralick_features(glcm: &Array2<f64>) -> HaralickFeatures {
167    let (rows, cols) = glcm.dim();
168
169    // Compute marginal probabilities
170    let px = glcm.sum_axis(Axis(1));
171    let py = glcm.sum_axis(Axis(0));
172
173    // Compute means
174    let mut mean_x = 0.0;
175    let mut mean_y = 0.0;
176
177    for i in 0..rows {
178        mean_x += i as f64 * px[i];
179        mean_y += i as f64 * py[i];
180    }
181
182    // Compute standard deviations
183    let mut std_x = 0.0;
184    let mut std_y = 0.0;
185
186    for i in 0..rows {
187        std_x += (i as f64 - mean_x).powi(2) * px[i];
188        std_y += (i as f64 - mean_y).powi(2) * py[i];
189    }
190
191    std_x = std_x.sqrt();
192    std_y = std_y.sqrt();
193
194    // Compute features
195    let mut energy = 0.0;
196    let mut contrast = 0.0;
197    let mut correlation = 0.0;
198    let mut homogeneity = 0.0;
199    let mut entropy = 0.0;
200    let mut dissimilarity = 0.0;
201    let mut max_probability = 0.0f64;
202
203    for i in 0..rows {
204        for j in 0..cols {
205            let p = glcm[[i, j]];
206
207            if p > 0.0 {
208                energy += p * p;
209                contrast += (i as f64 - j as f64).powi(2) * p;
210                homogeneity += p / (1.0 + (i as f64 - j as f64).abs());
211                entropy -= p * p.ln();
212                dissimilarity += (i as f64 - j as f64).abs() * p;
213                max_probability = max_probability.max(p);
214
215                if std_x > 0.0 && std_y > 0.0 {
216                    correlation +=
217                        ((i as f64 - mean_x) * (j as f64 - mean_y) * p) / (std_x * std_y);
218                }
219            }
220        }
221    }
222
223    HaralickFeatures {
224        energy,
225        contrast,
226        correlation,
227        homogeneity,
228        entropy,
229        dissimilarity,
230        max_probability,
231    }
232}
233
234/// Compute GLCM for multiple directions and aggregate features
235///
236/// # Arguments
237///
238/// * `img` - Input image
239/// * `distance` - Distance parameter
240/// * `levels` - Number of gray levels
241///
242/// # Returns
243///
244/// * Average Haralick features across all directions
245#[allow(dead_code)]
246pub fn compute_multi_directionglcm_features(
247    img: &DynamicImage,
248    distance: i32,
249    levels: usize,
250) -> Result<HaralickFeatures> {
251    let directions = [
252        GLCMDirection::Horizontal,
253        GLCMDirection::Vertical,
254        GLCMDirection::Diagonal,
255        GLCMDirection::AntiDiagonal,
256    ];
257
258    let mut all_features = Vec::new();
259
260    for direction in &directions {
261        let params = GLCMParams {
262            levels,
263            distance,
264            direction: *direction,
265            ..Default::default()
266        };
267
268        let glcm = computeglcm(img, &params)?;
269        let features = compute_haralick_features(&glcm);
270        all_features.push(features);
271    }
272
273    // Average features
274    let n = all_features.len() as f64;
275
276    Ok(HaralickFeatures {
277        energy: all_features.iter().map(|f| f.energy).sum::<f64>() / n,
278        contrast: all_features.iter().map(|f| f.contrast).sum::<f64>() / n,
279        correlation: all_features.iter().map(|f| f.correlation).sum::<f64>() / n,
280        homogeneity: all_features.iter().map(|f| f.homogeneity).sum::<f64>() / n,
281        entropy: all_features.iter().map(|f| f.entropy).sum::<f64>() / n,
282        dissimilarity: all_features.iter().map(|f| f.dissimilarity).sum::<f64>() / n,
283        max_probability: all_features.iter().map(|f| f.max_probability).sum::<f64>() / n,
284    })
285}
286
287/// Extended GLCM features including higher-order statistics
288#[derive(Debug, Clone)]
289pub struct ExtendedGLCMFeatures {
290    /// Basic Haralick features
291    pub haralick: HaralickFeatures,
292    /// Cluster shade
293    pub cluster_shade: f64,
294    /// Cluster prominence
295    pub cluster_prominence: f64,
296    /// Sum average
297    pub sum_average: f64,
298    /// Sum variance
299    pub sum_variance: f64,
300    /// Sum entropy
301    pub sum_entropy: f64,
302    /// Difference variance
303    pub diff_variance: f64,
304    /// Difference entropy
305    pub diff_entropy: f64,
306}
307
308/// Compute extended GLCM features
309#[allow(dead_code)]
310pub fn compute_extendedglcm_features(glcm: &Array2<f64>) -> ExtendedGLCMFeatures {
311    let haralick = compute_haralick_features(glcm);
312    let (n, _) = glcm.dim();
313
314    // Compute p_x+y and p_x-y
315    let mut p_sum = vec![0.0; 2 * n - 1];
316    let mut p_diff = vec![0.0; n];
317
318    for i in 0..n {
319        for j in 0..n {
320            let p = glcm[[i, j]];
321            p_sum[i + j] += p;
322            p_diff[(i as i32 - j as i32).unsigned_abs() as usize] += p;
323        }
324    }
325
326    // Compute sum and difference statistics
327    let mut sum_average = 0.0;
328    let mut sum_entropy = 0.0;
329    let mut diff_entropy = 0.0;
330
331    for (k, &p_sum_k) in p_sum.iter().enumerate() {
332        if p_sum_k > 0.0 {
333            sum_average += k as f64 * p_sum_k;
334            sum_entropy -= p_sum_k * p_sum_k.ln();
335        }
336    }
337
338    for &p_diff_k in &p_diff {
339        if p_diff_k > 0.0 {
340            diff_entropy -= p_diff_k * p_diff_k.ln();
341        }
342    }
343
344    // Compute sum variance
345    let mut sum_variance = 0.0;
346    for (k, &p_sum_k) in p_sum.iter().enumerate() {
347        sum_variance += (k as f64 - sum_average).powi(2) * p_sum_k;
348    }
349
350    // Compute difference variance
351    let mut diff_average = 0.0;
352    for (k, &p_diff_k) in p_diff.iter().enumerate() {
353        diff_average += k as f64 * p_diff_k;
354    }
355
356    let mut diff_variance = 0.0;
357    for (k, &p_diff_k) in p_diff.iter().enumerate() {
358        diff_variance += (k as f64 - diff_average).powi(2) * p_diff_k;
359    }
360
361    // Compute cluster shade and prominence
362    let (_px, _py, mean_x, mean_y) = compute_marginals(glcm);
363
364    let mut cluster_shade = 0.0;
365    let mut cluster_prominence = 0.0;
366
367    for i in 0..n {
368        for j in 0..n {
369            let term = i as f64 - mean_x + j as f64 - mean_y;
370            cluster_shade += term.powi(3) * glcm[[i, j]];
371            cluster_prominence += term.powi(4) * glcm[[i, j]];
372        }
373    }
374
375    ExtendedGLCMFeatures {
376        haralick,
377        cluster_shade,
378        cluster_prominence,
379        sum_average,
380        sum_variance,
381        sum_entropy,
382        diff_variance,
383        diff_entropy,
384    }
385}
386
387/// Compute marginal probabilities and means
388#[allow(dead_code)]
389fn compute_marginals(glcm: &Array2<f64>) -> (Vec<f64>, Vec<f64>, f64, f64) {
390    let (n, _) = glcm.dim();
391
392    let px = glcm.sum_axis(Axis(1)).to_vec();
393    let py = glcm.sum_axis(Axis(0)).to_vec();
394
395    let mut mean_x = 0.0;
396    let mut mean_y = 0.0;
397
398    for i in 0..n {
399        mean_x += i as f64 * px[i];
400        mean_y += i as f64 * py[i];
401    }
402
403    (px, py, mean_x, mean_y)
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    #[test]
411    fn testglcm_basic() {
412        let img = DynamicImage::new_luma8(10, 10);
413        let params = GLCMParams::default();
414
415        let result = computeglcm(&img, &params);
416        assert!(result.is_ok());
417
418        let glcm = result.expect("Operation failed");
419        assert_eq!(glcm.dim(), (8, 8));
420    }
421
422    #[test]
423    fn test_haralick_features() {
424        let mut glcm = Array2::zeros((4, 4));
425        glcm[[0, 0]] = 0.25;
426        glcm[[1, 1]] = 0.25;
427        glcm[[2, 2]] = 0.25;
428        glcm[[3, 3]] = 0.25;
429
430        let features = compute_haralick_features(&glcm);
431
432        // Perfect diagonal should have high energy and low contrast
433        assert!(features.energy > 0.0);
434        assert_eq!(features.contrast, 0.0);
435    }
436
437    #[test]
438    fn test_multi_direction() {
439        let img = DynamicImage::new_luma8(20, 20);
440        let result = compute_multi_directionglcm_features(&img, 1, 8);
441        assert!(result.is_ok());
442    }
443
444    #[test]
445    fn test_quantization() {
446        let img = GrayImage::new(4, 4);
447        let quantized = quantize_image(&img, 4);
448
449        assert_eq!(quantized.dim(), (4, 4));
450        assert!(quantized.iter().all(|&v| v < 4));
451    }
452}