1use crate::error::Result;
7use image::{DynamicImage, GrayImage};
8use scirs2_core::ndarray::{Array2, Axis};
9
10#[derive(Debug, Clone, Copy)]
12pub enum GLCMDirection {
13 Horizontal,
15 Vertical,
17 Diagonal,
19 AntiDiagonal,
21}
22
23impl GLCMDirection {
24 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#[derive(Debug, Clone)]
37pub struct GLCMParams {
38 pub levels: usize,
40 pub distance: i32,
42 pub direction: GLCMDirection,
44 pub symmetric: bool,
46 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#[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 let quantized = quantize_image(&gray, params.levels);
79
80 let mut glcm = Array2::zeros((params.levels, params.levels));
82
83 let (dx, dy) = params.direction.get_offset(params.distance);
85
86 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 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 if params.symmetric {
101 glcm[[j, i]] += 1.0;
102 }
103 }
104 }
105 }
106
107 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#[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#[derive(Debug, Clone)]
139pub struct HaralickFeatures {
140 pub energy: f64,
142 pub contrast: f64,
144 pub correlation: f64,
146 pub homogeneity: f64,
148 pub entropy: f64,
150 pub dissimilarity: f64,
152 pub max_probability: f64,
154}
155
156#[allow(dead_code)]
166pub fn compute_haralick_features(glcm: &Array2<f64>) -> HaralickFeatures {
167 let (rows, cols) = glcm.dim();
168
169 let px = glcm.sum_axis(Axis(1));
171 let py = glcm.sum_axis(Axis(0));
172
173 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 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 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#[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, ¶ms)?;
269 let features = compute_haralick_features(&glcm);
270 all_features.push(features);
271 }
272
273 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#[derive(Debug, Clone)]
289pub struct ExtendedGLCMFeatures {
290 pub haralick: HaralickFeatures,
292 pub cluster_shade: f64,
294 pub cluster_prominence: f64,
296 pub sum_average: f64,
298 pub sum_variance: f64,
300 pub sum_entropy: f64,
302 pub diff_variance: f64,
304 pub diff_entropy: f64,
306}
307
308#[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 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 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 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 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 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#[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, ¶ms);
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 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}