1use crate::error::{AlgorithmError, Result};
56use oxigdal_core::buffer::RasterBuffer;
57
58#[cfg(feature = "parallel")]
59use rayon::prelude::*;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum Direction {
64 Horizontal,
66
67 Vertical,
69
70 Diagonal45,
72
73 Diagonal135,
75
76 Custom(i64, i64),
78}
79
80impl Direction {
81 #[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 #[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#[derive(Debug, Clone)]
108pub struct GlcmParams {
109 pub gray_levels: usize,
111
112 pub normalize: bool,
114
115 pub symmetric: bool,
117
118 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#[derive(Debug, Clone)]
135pub struct Glcm {
136 matrix: Vec<Vec<f64>>,
138
139 gray_levels: usize,
141
142 direction: Direction,
144
145 distance: u32,
147
148 normalized: bool,
150}
151
152impl Glcm {
153 #[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 #[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 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 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 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 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 #[must_use]
216 pub fn gray_levels(&self) -> usize {
217 self.gray_levels
218 }
219
220 #[must_use]
222 pub fn direction(&self) -> Direction {
223 self.direction
224 }
225
226 #[must_use]
228 pub fn distance(&self) -> u32 {
229 self.distance
230 }
231
232 #[must_use]
234 pub fn is_normalized(&self) -> bool {
235 self.normalized
236 }
237
238 #[must_use]
240 pub fn matrix(&self) -> &[Vec<f64>] {
241 &self.matrix
242 }
243}
244
245#[derive(Debug, Clone, Default)]
247pub struct HaralickFeatures {
248 pub contrast: f64,
250
251 pub correlation: f64,
253
254 pub energy: f64,
256
257 pub homogeneity: f64,
259
260 pub entropy: f64,
262
263 pub dissimilarity: f64,
265
266 pub variance: f64,
268
269 pub sum_average: f64,
271
272 pub sum_entropy: f64,
274
275 pub difference_entropy: f64,
277
278 pub info_measure_corr1: f64,
280
281 pub info_measure_corr2: f64,
283
284 pub max_correlation_coeff: f64,
286
287 pub cluster_shade: f64,
289
290 pub cluster_prominence: f64,
292}
293
294pub 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 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 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 if params.symmetric {
347 glcm.make_symmetric();
348 }
349
350 if params.normalize {
352 glcm.normalize();
353 }
354
355 Ok(glcm)
356}
357
358pub 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 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#[must_use]
417pub fn compute_haralick_features(glcm: &Glcm) -> HaralickFeatures {
418 let n = glcm.gray_levels();
419 let matrix = glcm.matrix();
420
421 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 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 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 features.contrast = (0..n).map(|k| k.pow(2) as f64 * p_x_minus_y[k]).sum();
469
470 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 features.energy = (0..n)
483 .flat_map(|i| (0..n).map(move |j| matrix[i][j].powi(2)))
484 .sum();
485
486 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 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 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 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 features.sum_average = (0..=max_sum).map(|k| k as f64 * p_x_plus_y[k]).sum();
518
519 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 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 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 features.max_correlation_coeff = features.correlation.abs();
589
590 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 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
607pub 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
694fn 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 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 let glcm = compute_glcm(&window, direction, distance, params)?;
729 let features = compute_haralick_features(&glcm);
730
731 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
759fn 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 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 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#[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 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, ¶ms)
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 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 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 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, ¶ms)
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 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 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}