1use crate::error::{NdimageError, NdimageResult};
20use scirs2_core::ndarray::{s, Array1, Array2, Array3};
21use std::f64::consts::PI;
22
23pub fn gabor_feature_map(
46 image: &Array2<f64>,
47 frequencies: &[f64],
48 orientations: &[f64],
49) -> NdimageResult<Array3<f64>> {
50 let (rows, cols) = image.dim();
51 if rows == 0 || cols == 0 {
52 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
53 }
54 if frequencies.is_empty() || orientations.is_empty() {
55 return Err(NdimageError::InvalidInput(
56 "frequencies and orientations must be non-empty".into(),
57 ));
58 }
59
60 let n_channels = frequencies.len() * orientations.len();
61 let mut out = Array3::<f64>::zeros((rows, cols, n_channels));
62
63 let mut ch = 0;
64 for &freq in frequencies {
65 for &theta in orientations {
66 let response = apply_gabor_kernel(image, freq, theta)?;
67 for r in 0..rows {
68 for c in 0..cols {
69 out[[r, c, ch]] = response[[r, c]];
70 }
71 }
72 ch += 1;
73 }
74 }
75
76 Ok(out)
77}
78
79fn apply_gabor_kernel(
84 image: &Array2<f64>,
85 frequency: f64,
86 theta: f64,
87) -> NdimageResult<Array2<f64>> {
88 let (rows, cols) = image.dim();
89
90 let sigma = if frequency > 1e-12 {
91 1.0 / (2.0 * PI * frequency)
92 } else {
93 return Err(NdimageError::InvalidInput(
94 "Gabor frequency must be positive".into(),
95 ));
96 };
97 let sigma_x = sigma;
98 let sigma_y = sigma;
99
100 let half = (3.0 * sigma.max(sigma_x)).ceil() as usize;
101 let ksize = 2 * half + 1;
102
103 let mut kernel_real = Array2::<f64>::zeros((ksize, ksize));
105 let mut kernel_imag = Array2::<f64>::zeros((ksize, ksize));
106
107 let cos_t = theta.cos();
108 let sin_t = theta.sin();
109
110 for ky in 0..ksize {
111 for kx in 0..ksize {
112 let y = ky as f64 - half as f64;
113 let x = kx as f64 - half as f64;
114
115 let x_rot = x * cos_t + y * sin_t;
117 let y_rot = -x * sin_t + y * cos_t;
118
119 let gauss = (-0.5
120 * (x_rot * x_rot / (sigma_x * sigma_x) + y_rot * y_rot / (sigma_y * sigma_y)))
121 .exp();
122
123 let phase = 2.0 * PI * frequency * x_rot;
124 kernel_real[[ky, kx]] = gauss * phase.cos();
125 kernel_imag[[ky, kx]] = gauss * phase.sin();
126 }
127 }
128
129 let n_elems = (ksize * ksize) as f64;
132 let kr_mean = kernel_real.iter().sum::<f64>() / n_elems;
133 let ki_mean = kernel_imag.iter().sum::<f64>() / n_elems;
134 kernel_real.iter_mut().for_each(|v| *v -= kr_mean);
135 kernel_imag.iter_mut().for_each(|v| *v -= ki_mean);
136
137 let resp_real = convolve_reflect(image, &kernel_real)?;
141 let resp_imag = convolve_reflect(image, &kernel_imag)?;
142
143 let mut magnitude = Array2::<f64>::zeros((rows, cols));
145 for r in 0..rows {
146 for c in 0..cols {
147 magnitude[[r, c]] = (resp_real[[r, c]].powi(2) + resp_imag[[r, c]].powi(2)).sqrt();
148 }
149 }
150
151 Ok(magnitude)
152}
153
154fn convolve_same(image: &Array2<f64>, kernel: &Array2<f64>) -> NdimageResult<Array2<f64>> {
156 let (ih, iw) = image.dim();
157 let (kh, kw) = kernel.dim();
158 let ph = kh / 2;
159 let pw = kw / 2;
160
161 let mut out = Array2::<f64>::zeros((ih, iw));
162
163 for r in 0..ih {
164 for c in 0..iw {
165 let mut acc = 0.0;
166 for kr in 0..kh {
167 let ir = r as i64 + kr as i64 - ph as i64;
168 if ir < 0 || ir >= ih as i64 {
169 continue;
170 }
171 for kc in 0..kw {
172 let ic = c as i64 + kc as i64 - pw as i64;
173 if ic < 0 || ic >= iw as i64 {
174 continue;
175 }
176 acc += image[[ir as usize, ic as usize]] * kernel[[kr, kc]];
177 }
178 }
179 out[[r, c]] = acc;
180 }
181 }
182 Ok(out)
183}
184
185fn convolve_reflect(image: &Array2<f64>, kernel: &Array2<f64>) -> NdimageResult<Array2<f64>> {
191 let (ih, iw) = image.dim();
192 let (kh, kw) = kernel.dim();
193 let ph = kh / 2;
194 let pw = kw / 2;
195
196 let reflect = |idx: i64, size: i64| -> usize {
198 if size <= 1 {
199 return 0;
200 }
201 let mut i = idx;
202 let period = 2 * (size - 1);
204 i = ((i % period) + period) % period;
206 if i >= size {
207 i = period - i;
208 }
209 i as usize
210 };
211
212 let mut out = Array2::<f64>::zeros((ih, iw));
213
214 for r in 0..ih {
215 for c in 0..iw {
216 let mut acc = 0.0;
217 for kr in 0..kh {
218 let ir = reflect(r as i64 + kr as i64 - ph as i64, ih as i64);
219 for kc in 0..kw {
220 let ic = reflect(c as i64 + kc as i64 - pw as i64, iw as i64);
221 acc += image[[ir, ic]] * kernel[[kr, kc]];
222 }
223 }
224 out[[r, c]] = acc;
225 }
226 }
227 Ok(out)
228}
229
230fn kmeans(
238 data: &[Vec<f64>],
239 k: usize,
240 max_iter: usize,
241) -> NdimageResult<(Vec<usize>, Vec<Vec<f64>>)> {
242 if data.is_empty() || k == 0 {
243 return Err(NdimageError::InvalidInput(
244 "k-means: data must be non-empty and k >= 1".into(),
245 ));
246 }
247 let n = data.len();
248 let d = data[0].len();
249 let k_actual = k.min(n);
250
251 let mut centroids: Vec<Vec<f64>> = (0..k_actual).map(|i| data[i].clone()).collect();
253 let mut labels = vec![0usize; n];
254
255 for _iter in 0..max_iter {
256 let mut changed = false;
257
258 for (i, sample) in data.iter().enumerate() {
260 let best = nearest_centroid(sample, ¢roids);
261 if labels[i] != best {
262 labels[i] = best;
263 changed = true;
264 }
265 }
266
267 if !changed {
268 break;
269 }
270
271 let mut sums = vec![vec![0.0f64; d]; k_actual];
273 let mut counts = vec![0usize; k_actual];
274 for (i, sample) in data.iter().enumerate() {
275 let lbl = labels[i];
276 counts[lbl] += 1;
277 for dim in 0..d {
278 sums[lbl][dim] += sample[dim];
279 }
280 }
281 for k_idx in 0..k_actual {
282 if counts[k_idx] > 0 {
283 let cnt = counts[k_idx] as f64;
284 for dim in 0..d {
285 centroids[k_idx][dim] = sums[k_idx][dim] / cnt;
286 }
287 }
288 }
289 }
290
291 Ok((labels, centroids))
292}
293
294fn nearest_centroid(sample: &[f64], centroids: &[Vec<f64>]) -> usize {
295 let mut best_idx = 0;
296 let mut best_dist = f64::INFINITY;
297 for (idx, centroid) in centroids.iter().enumerate() {
298 let dist: f64 = sample
299 .iter()
300 .zip(centroid.iter())
301 .map(|(a, b)| (a - b).powi(2))
302 .sum();
303 if dist < best_dist {
304 best_dist = dist;
305 best_idx = idx;
306 }
307 }
308 best_idx
309}
310
311pub fn texture_segment_kmeans(
328 image: &Array2<f64>,
329 gabor_params: (&[f64], &[f64]),
330 n_clusters: usize,
331) -> NdimageResult<Array2<usize>> {
332 let (rows, cols) = image.dim();
333 let (frequencies, orientations) = gabor_params;
334
335 if n_clusters == 0 {
336 return Err(NdimageError::InvalidInput(
337 "n_clusters must be at least 1".into(),
338 ));
339 }
340
341 let feature_map = gabor_feature_map(image, frequencies, orientations)?;
342 let n_ch = feature_map.dim().2;
343
344 let mut data: Vec<Vec<f64>> = Vec::with_capacity(rows * cols);
346 for r in 0..rows {
347 for c in 0..cols {
348 let mut feat = Vec::with_capacity(n_ch);
349 for ch in 0..n_ch {
350 feat.push(feature_map[[r, c, ch]]);
351 }
352 data.push(feat);
353 }
354 }
355
356 let (labels, _) = kmeans(&data, n_clusters, 100)?;
357
358 let mut label_image = Array2::<usize>::zeros((rows, cols));
359 for r in 0..rows {
360 for c in 0..cols {
361 label_image[[r, c]] = labels[r * cols + c];
362 }
363 }
364
365 Ok(label_image)
366}
367
368fn lbp_code_at(image: &Array2<f64>, row: usize, col: usize, radius: f64, n_points: usize) -> u64 {
376 let (rows, cols) = image.dim();
377 let center = image[[row, col]];
378 let mut code = 0u64;
379
380 for p in 0..n_points {
381 let angle = 2.0 * PI * p as f64 / n_points as f64;
382 let sample_r = row as f64 - radius * angle.sin();
383 let sample_c = col as f64 + radius * angle.cos();
384
385 let r0 = sample_r.floor() as i64;
387 let c0 = sample_c.floor() as i64;
388 let fr = sample_r - r0 as f64;
389 let fc = sample_c - c0 as f64;
390
391 let clamp_r = |r: i64| r.max(0).min(rows as i64 - 1) as usize;
392 let clamp_c = |c: i64| c.max(0).min(cols as i64 - 1) as usize;
393
394 let v00 = image[[clamp_r(r0), clamp_c(c0)]];
395 let v01 = image[[clamp_r(r0), clamp_c(c0 + 1)]];
396 let v10 = image[[clamp_r(r0 + 1), clamp_c(c0)]];
397 let v11 = image[[clamp_r(r0 + 1), clamp_c(c0 + 1)]];
398
399 let val = (1.0 - fr) * (1.0 - fc) * v00
400 + (1.0 - fr) * fc * v01
401 + fr * (1.0 - fc) * v10
402 + fr * fc * v11;
403
404 if val >= center {
405 code |= 1 << p;
406 }
407 }
408
409 let mut min_code = code;
411 let mask = if n_points < 64 {
412 (1u64 << n_points) - 1
413 } else {
414 u64::MAX
415 };
416 let mut rotated = code;
417 for _ in 1..n_points {
418 rotated = ((rotated >> 1) | ((rotated & 1) << (n_points - 1))) & mask;
419 if rotated < min_code {
420 min_code = rotated;
421 }
422 }
423 min_code
424}
425
426pub fn lbp_segment(
440 image: &Array2<f64>,
441 radius: f64,
442 n_points: usize,
443 n_clusters: usize,
444) -> NdimageResult<Array2<usize>> {
445 let (rows, cols) = image.dim();
446 if rows == 0 || cols == 0 {
447 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
448 }
449 if n_points == 0 {
450 return Err(NdimageError::InvalidInput(
451 "n_points must be at least 1".into(),
452 ));
453 }
454 if n_clusters == 0 {
455 return Err(NdimageError::InvalidInput(
456 "n_clusters must be at least 1".into(),
457 ));
458 }
459 if radius <= 0.0 {
460 return Err(NdimageError::InvalidInput("radius must be positive".into()));
461 }
462
463 let mut data: Vec<Vec<f64>> = Vec::with_capacity(rows * cols);
465 for r in 0..rows {
466 for c in 0..cols {
467 let code = lbp_code_at(image, r, c, radius, n_points);
468 data.push(vec![code as f64]);
469 }
470 }
471
472 let (labels, _) = kmeans(&data, n_clusters, 100)?;
473
474 let mut label_image = Array2::<usize>::zeros((rows, cols));
475 for r in 0..rows {
476 for c in 0..cols {
477 label_image[[r, c]] = labels[r * cols + c];
478 }
479 }
480
481 Ok(label_image)
482}
483
484pub fn mrm_segment(image: &Array2<f64>, n_clusters: usize) -> NdimageResult<Array2<usize>> {
504 let (rows, cols) = image.dim();
505 if rows == 0 || cols == 0 {
506 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
507 }
508 if n_clusters == 0 {
509 return Err(NdimageError::InvalidInput(
510 "n_clusters must be at least 1".into(),
511 ));
512 }
513
514 let k = n_clusters.min(rows * cols);
515
516 let i_min = image.iter().cloned().fold(f64::INFINITY, f64::min);
518 let i_max = image.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
519 let range = (i_max - i_min).max(1e-12);
520
521 let mut labels = Array2::<usize>::zeros((rows, cols));
522 for r in 0..rows {
523 for c in 0..cols {
524 labels[[r, c]] =
525 (((image[[r, c]] - i_min) / range * k as f64).floor() as usize).min(k - 1);
526 }
527 }
528
529 let mut means = vec![0.0f64; k];
531 let mut counts = vec![0usize; k];
532 for r in 0..rows {
533 for c in 0..cols {
534 let lbl = labels[[r, c]];
535 means[lbl] += image[[r, c]];
536 counts[lbl] += 1;
537 }
538 }
539 for ki in 0..k {
540 if counts[ki] > 0 {
541 means[ki] /= counts[ki] as f64;
542 }
543 }
544
545 let beta = 0.5; let max_iter = 20;
547
548 let neighbors: [(i32, i32); 4] = [(-1, 0), (1, 0), (0, -1), (0, 1)];
549
550 for _iter in 0..max_iter {
551 let mut changed = false;
552 let old_labels = labels.clone();
553
554 for r in 0..rows {
555 for c in 0..cols {
556 let pixel = image[[r, c]];
557
558 let best_label = (0..k)
559 .min_by(|&ka, &kb| {
560 let ea = mrf_energy(
561 pixel,
562 means[ka],
563 ka,
564 r,
565 c,
566 &old_labels,
567 &neighbors,
568 beta,
569 rows,
570 cols,
571 );
572 let eb = mrf_energy(
573 pixel,
574 means[kb],
575 kb,
576 r,
577 c,
578 &old_labels,
579 &neighbors,
580 beta,
581 rows,
582 cols,
583 );
584 ea.partial_cmp(&eb).unwrap_or(std::cmp::Ordering::Equal)
585 })
586 .unwrap_or(0);
587
588 if labels[[r, c]] != best_label {
589 labels[[r, c]] = best_label;
590 changed = true;
591 }
592 }
593 }
594
595 means = vec![0.0f64; k];
597 counts = vec![0usize; k];
598 for r in 0..rows {
599 for c in 0..cols {
600 let lbl = labels[[r, c]];
601 means[lbl] += image[[r, c]];
602 counts[lbl] += 1;
603 }
604 }
605 for ki in 0..k {
606 if counts[ki] > 0 {
607 means[ki] /= counts[ki] as f64;
608 }
609 }
610
611 if !changed {
612 break;
613 }
614 }
615
616 Ok(labels)
617}
618
619fn mrf_energy(
621 pixel: f64,
622 mean: f64,
623 label: usize,
624 r: usize,
625 c: usize,
626 labels: &Array2<usize>,
627 neighbors: &[(i32, i32)],
628 beta: f64,
629 rows: usize,
630 cols: usize,
631) -> f64 {
632 let data_term = (pixel - mean).powi(2);
634
635 let mut prior_term = 0.0;
637 for &(dr, dc) in neighbors {
638 let nr = r as i64 + dr as i64;
639 let nc = c as i64 + dc as i64;
640 if nr >= 0 && nr < rows as i64 && nc >= 0 && nc < cols as i64 {
641 if labels[[nr as usize, nc as usize]] != label {
642 prior_term += beta;
643 }
644 }
645 }
646
647 data_term + prior_term
648}
649
650pub fn texture_features_patch(
671 image: &Array2<f64>,
672 y: usize,
673 x: usize,
674 patch_size: usize,
675) -> NdimageResult<Array1<f64>> {
676 let (rows, cols) = image.dim();
677 if patch_size < 3 || patch_size % 2 == 0 {
678 return Err(NdimageError::InvalidInput(
679 "patch_size must be odd and at least 3".into(),
680 ));
681 }
682 let half = patch_size / 2;
683 if y < half || x < half || y + half >= rows || x + half >= cols {
684 return Err(NdimageError::InvalidInput(
685 "Patch extends outside image boundaries".into(),
686 ));
687 }
688
689 let patch = image.slice(s![y - half..=y + half, x - half..=x + half]);
690
691 let n = patch.len() as f64;
693 let mean = patch.iter().sum::<f64>() / n;
694 let var = patch.iter().map(|&v| (v - mean).powi(2)).sum::<f64>() / n;
695 let std_dev = var.sqrt();
696
697 let freqs = [0.05, 0.1, 0.15, 0.2];
699 let thetas = [0.0, PI / 4.0, PI / 2.0, 3.0 * PI / 4.0];
700 let patch_owned = patch.to_owned();
701 let mut gabor_feats = Vec::with_capacity(16);
702 for &freq in &freqs {
703 for &theta in &thetas {
704 let resp = apply_gabor_kernel(&patch_owned, freq, theta)?;
705 let mag: f64 = resp.iter().sum::<f64>() / resp.len() as f64;
706 gabor_feats.push(mag);
707 }
708 }
709
710 let lbp_n_points = 8usize;
712 let lbp_radius = 1.0f64;
713 let n_lbp_bins = lbp_n_points + 2; let mut lbp_hist = vec![0.0f64; n_lbp_bins];
715 let ph = patch.nrows();
716 let pw = patch.ncols();
717 let mut lbp_count = 0.0f64;
718 for pr in 0..ph {
719 for pc in 0..pw {
720 let code = lbp_code_at(&patch_owned, pr, pc, lbp_radius, lbp_n_points);
721 let bin = (code as usize).min(n_lbp_bins - 1);
722 lbp_hist[bin] += 1.0;
723 lbp_count += 1.0;
724 }
725 }
726 if lbp_count > 0.0 {
727 for v in lbp_hist.iter_mut() {
728 *v /= lbp_count;
729 }
730 }
731
732 let mut features = Vec::with_capacity(2 + 16 + n_lbp_bins);
734 features.push(mean);
735 features.push(std_dev);
736 features.extend_from_slice(&gabor_feats);
737 features.extend_from_slice(&lbp_hist);
738
739 Ok(Array1::from_vec(features))
740}
741
742#[cfg(test)]
747mod tests {
748 use super::*;
749 use scirs2_core::ndarray::Array2;
750
751 fn uniform_image(rows: usize, cols: usize, val: f64) -> Array2<f64> {
752 Array2::from_elem((rows, cols), val)
753 }
754
755 fn striped_image(rows: usize, cols: usize) -> Array2<f64> {
756 Array2::from_shape_fn((rows, cols), |(_, c)| if c % 4 < 2 { 1.0 } else { 0.0 })
757 }
758
759 #[test]
760 fn test_gabor_feature_map_shape() {
761 let img = striped_image(16, 16);
762 let freqs = vec![0.1, 0.2];
763 let thetas = vec![0.0, PI / 2.0];
764 let feat = gabor_feature_map(&img, &freqs, &thetas).expect("gabor ok");
765 assert_eq!(feat.dim(), (16, 16, 4));
766 }
767
768 #[test]
769 fn test_gabor_feature_map_uniform_image() {
770 let img = uniform_image(12, 12, 0.5);
772 let feat = gabor_feature_map(&img, &[0.15], &[0.0]).expect("gabor ok");
773 for v in feat.iter() {
774 assert!(
775 *v < 0.01,
776 "Uniform image Gabor response should be ~0, got {v}"
777 );
778 }
779 }
780
781 #[test]
782 fn test_texture_segment_kmeans() {
783 let img = striped_image(20, 20);
784 let freqs = vec![0.1, 0.2];
785 let thetas = vec![0.0, PI / 2.0];
786 let labels = texture_segment_kmeans(&img, (&freqs, &thetas), 2).expect("segment ok");
787 assert_eq!(labels.dim(), (20, 20));
788 for &lbl in labels.iter() {
790 assert!(lbl < 2);
791 }
792 }
793
794 #[test]
795 fn test_lbp_segment() {
796 let img = striped_image(24, 24);
797 let labels = lbp_segment(&img, 1.0, 8, 2).expect("lbp ok");
798 assert_eq!(labels.dim(), (24, 24));
799 }
800
801 #[test]
802 fn test_mrm_segment() {
803 let img = striped_image(16, 16);
804 let labels = mrm_segment(&img, 2).expect("mrm ok");
805 assert_eq!(labels.dim(), (16, 16));
806 for &lbl in labels.iter() {
807 assert!(lbl < 2);
808 }
809 }
810
811 #[test]
812 fn test_texture_features_patch_shape() {
813 let img = striped_image(32, 32);
814 let feats = texture_features_patch(&img, 10, 10, 7).expect("patch ok");
815 assert_eq!(feats.len(), 28);
817 }
818
819 #[test]
820 fn test_texture_features_patch_invalid_patch_size() {
821 let img: Array2<f64> = Array2::zeros((32, 32));
822 let err = texture_features_patch(&img, 10, 10, 4); assert!(err.is_err());
824 }
825
826 #[test]
827 fn test_texture_features_patch_out_of_bounds() {
828 let img: Array2<f64> = Array2::zeros((16, 16));
829 let err = texture_features_patch(&img, 2, 8, 11);
831 assert!(err.is_err());
832 }
833}