1use crate::error::{NdimageError, NdimageResult};
7use crate::filters::{
8 convolve, gaussian_filter_f32, gradient_magnitude, prewitt, scharr, sobel, BorderMode,
9};
10use scirs2_core::ndarray::{Array, Array2, ArrayD, Ix2};
11use std::f32::consts::PI;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GradientMethod {
16 Sobel,
18 Prewitt,
20 Scharr,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EdgeDetectionAlgorithm {
27 Canny,
29 LoG,
31 Gradient,
33}
34
35#[derive(Debug, Clone)]
37pub struct EdgeDetectionConfig {
38 pub algorithm: EdgeDetectionAlgorithm,
40 pub gradient_method: GradientMethod,
42 pub sigma: f32,
44 pub low_threshold: f32,
46 pub high_threshold: f32,
48 pub border_mode: BorderMode,
50 pub return_magnitude: bool,
52}
53
54impl Default for EdgeDetectionConfig {
55 fn default() -> Self {
56 Self {
57 algorithm: EdgeDetectionAlgorithm::Canny,
58 gradient_method: GradientMethod::Sobel,
59 sigma: 1.0,
60 low_threshold: 0.1,
61 high_threshold: 0.2,
62 border_mode: BorderMode::Reflect,
63 return_magnitude: false,
64 }
65 }
66}
67
68#[allow(dead_code)]
117pub fn edge_detector(
118 image: &Array<f32, Ix2>,
119 config: EdgeDetectionConfig,
120) -> NdimageResult<Array<f32, Ix2>> {
121 match config.algorithm {
122 EdgeDetectionAlgorithm::Canny => {
123 Ok(canny_impl(
125 image,
126 config.sigma,
127 config.low_threshold,
128 config.high_threshold,
129 config.gradient_method,
130 config.border_mode,
131 ))
132 }
133 EdgeDetectionAlgorithm::LoG => {
134 let edges = laplacian_edges_impl(
135 image,
136 config.sigma,
137 config.low_threshold,
138 config.border_mode,
139 )?;
140
141 if !config.return_magnitude {
142 Ok(edges
144 .mapv(|v| v.abs() > config.low_threshold)
145 .mapv(|v| if v { 1.0 } else { 0.0 }))
146 } else {
147 Ok(edges)
148 }
149 }
150 EdgeDetectionAlgorithm::Gradient => {
151 let edges = gradient_edges_impl(
152 image,
153 config.gradient_method,
154 config.sigma,
155 config.border_mode,
156 )?;
157
158 if !config.return_magnitude {
159 Ok(edges
161 .mapv(|v| v > config.low_threshold)
162 .mapv(|v| if v { 1.0 } else { 0.0 }))
163 } else {
164 Ok(edges)
165 }
166 }
167 }
168}
169
170#[allow(dead_code)]
208pub fn canny(
209 image: &Array<f32, Ix2>,
210 sigma: f32,
211 low_threshold: f32,
212 high_threshold: f32,
213 method: Option<GradientMethod>,
214) -> Array<f32, Ix2> {
215 let method = method.unwrap_or(GradientMethod::Sobel);
216 let border_mode = BorderMode::Reflect;
217
218 canny_impl(
219 image,
220 sigma,
221 low_threshold,
222 high_threshold,
223 method,
224 border_mode,
225 )
226}
227
228#[allow(dead_code)]
230fn canny_impl(
231 image: &Array<f32, Ix2>,
232 sigma: f32,
233 low_threshold: f32,
234 high_threshold: f32,
235 method: GradientMethod,
236 mode: BorderMode,
237) -> Array<f32, Ix2> {
238 let image_dim = image.raw_dim();
239
240 let image_d = image.clone().into_dyn();
242 let smoothed =
243 gaussian_filter_f32(&image_d, sigma, Some(mode), None).unwrap_or_else(|_| image_d.clone());
244
245 let gradients = calculate_gradient(&smoothed, method, mode);
247 let gradient_x = &gradients.0;
248 let gradient_y = &gradients.1;
249
250 let (magnitude, direction) =
252 calculate_magnitude_and_direction(gradient_x, gradient_y, image_dim);
253
254 let suppressed = non_maximum_suppression(&magnitude, &direction);
256
257 hysteresis_thresholding(&suppressed, low_threshold, high_threshold)
259}
260
261#[allow(dead_code)]
263fn calculate_gradient(
264 image: &ArrayD<f32>,
265 method: GradientMethod,
266 mode: BorderMode,
267) -> (ArrayD<f32>, ArrayD<f32>) {
268 match method {
269 GradientMethod::Sobel => {
270 let gy = sobel(image, 0, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
271 let gx = sobel(image, 1, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
272 (gx, gy)
273 }
274 GradientMethod::Prewitt => {
275 let gy =
276 prewitt(image, 0, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
277 let gx =
278 prewitt(image, 1, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
279 (gx, gy)
280 }
281 GradientMethod::Scharr => {
282 let gy =
283 scharr(image, 0, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
284 let gx =
285 scharr(image, 1, Some(mode)).unwrap_or_else(|_| ArrayD::zeros(image.raw_dim()));
286 (gx, gy)
287 }
288 }
289}
290
291#[allow(dead_code)]
293fn calculate_magnitude_and_direction(
294 gradient_x: &ArrayD<f32>,
295 gradient_y: &ArrayD<f32>,
296 shape: Ix2,
297) -> (Array<f32, Ix2>, Array<f32, Ix2>) {
298 let magnitude = Array::<f32, Ix2>::zeros(shape);
299 let mut direction = Array::<f32, Ix2>::zeros(shape);
300
301 let mut mag_copy = Array::<f32, Ix2>::zeros(shape);
303
304 for (pos, _) in magnitude.indexed_iter() {
306 let idx_d = [pos.0, pos.1];
307 let gx = gradient_x[idx_d.as_ref()];
308 let gy = gradient_y[idx_d.as_ref()];
309
310 mag_copy[pos] = (gx * gx + gy * gy).sqrt();
312
313 let angle = gy.atan2(gx) * 180.0 / PI;
315 let angle = if angle < 0.0 { angle + 180.0 } else { angle };
316
317 direction[pos] = if !(22.5..157.5).contains(&angle) {
319 0.0 } else if (22.5..67.5).contains(&angle) {
321 45.0 } else if (67.5..112.5).contains(&angle) {
323 90.0 } else {
325 135.0 };
327 }
328
329 (mag_copy, direction)
330}
331
332#[allow(dead_code)]
334fn non_maximum_suppression(
335 magnitude: &Array<f32, Ix2>,
336 direction: &Array<f32, Ix2>,
337) -> Array<f32, Ix2> {
338 let shape = magnitude.dim();
339 let mut suppressed = Array::zeros(shape);
340
341 for row in 1..(shape.0 - 1) {
343 for col in 1..(shape.1 - 1) {
344 let dir = direction[(row, col)];
345 let mag = magnitude[(row, col)];
346
347 if mag == 0.0 {
349 continue;
350 }
351
352 let (neighbor1, neighbor2) = get_gradient_neighbors(row, col, dir, magnitude);
353
354 if mag >= neighbor1 && mag >= neighbor2 {
356 suppressed[(row, col)] = mag;
357 }
358 }
359 }
360
361 suppressed
362}
363
364#[allow(dead_code)]
366fn hysteresis_thresholding(
367 suppressed: &Array<f32, Ix2>,
368 low_threshold: f32,
369 high_threshold: f32,
370) -> Array<f32, Ix2> {
371 let shape = suppressed.dim();
372 let mut result = Array::from_elem(shape, 0.0);
373 let mut candidates = Vec::new();
374
375 for ((row, col), &val) in suppressed.indexed_iter() {
377 if val >= high_threshold {
378 result[(row, col)] = 1.0;
380
381 candidates.push((row, col));
383 } else if val >= low_threshold {
384 candidates.push((row, col));
386 }
387 }
388
389 let mut processed = Array::from_elem(shape, false);
391 for (row, col) in candidates.iter() {
392 if result[(*row, *col)] > 0.0 || processed[(*row, *col)] {
393 continue;
395 }
396
397 processed[(*row, *col)] = true;
398
399 if is_connected_to_strong_edge(*row, *col, &result) {
401 result[(*row, *col)] = 1.0;
402
403 let mut queue = Vec::with_capacity(8); queue.push((*row, *col));
406
407 while let Some((r, c)) = queue.pop() {
408 for nr in (r.saturating_sub(1))..=(r + 1).min(shape.0 - 1) {
410 for nc in (c.saturating_sub(1))..=(c + 1).min(shape.1 - 1) {
411 if processed[(nr, nc)] || result[(nr, nc)] > 0.0 {
412 continue;
413 }
414
415 if suppressed[(nr, nc)] >= low_threshold {
417 result[(nr, nc)] = 1.0;
418 processed[(nr, nc)] = true;
419 queue.push((nr, nc));
420 }
421 }
422 }
423 }
424 }
425 }
426
427 result
428}
429
430#[allow(dead_code)]
432fn get_gradient_neighbors(
433 row: usize,
434 col: usize,
435 direction: f32,
436 magnitude: &Array<f32, Ix2>,
437) -> (f32, f32) {
438 if direction == 0.0 {
440 (magnitude[(row, col - 1)], magnitude[(row, col + 1)])
441 }
442 else if direction == 45.0 {
444 (magnitude[(row - 1, col + 1)], magnitude[(row + 1, col - 1)])
445 }
446 else if direction == 90.0 {
448 (magnitude[(row - 1, col)], magnitude[(row + 1, col)])
449 }
450 else {
452 (magnitude[(row - 1, col - 1)], magnitude[(row + 1, col + 1)])
453 }
454}
455
456#[allow(dead_code)]
458fn is_connected_to_strong_edge(row: usize, col: usize, edges: &Array<f32, Ix2>) -> bool {
459 let shape = edges.dim();
460
461 for i in (row.saturating_sub(1))..=(row + 1).min(shape.0 - 1) {
462 for j in (col.saturating_sub(1))..=(col + 1).min(shape.1 - 1) {
463 if !(i == row && j == col) && edges[(i, j)] > 0.0 {
464 return true;
465 }
466 }
467 }
468 false
469}
470
471#[allow(dead_code)]
510pub fn laplacian_edges(
511 image: &Array<f32, Ix2>,
512 sigma: f32,
513 threshold: Option<f32>,
514 mode: Option<BorderMode>,
515) -> NdimageResult<Array<f32, Ix2>> {
516 let mode = mode.unwrap_or(BorderMode::Reflect);
517 Ok(laplacian_edges_impl(
518 image,
519 sigma,
520 threshold.unwrap_or(0.0),
521 mode,
522 )?)
523}
524
525#[allow(dead_code)]
527fn laplacian_edges_impl(
528 image: &Array<f32, Ix2>,
529 sigma: f32,
530 threshold: f32,
531 mode: BorderMode,
532) -> NdimageResult<Array<f32, Ix2>> {
533 let image_d = image.clone().into_dyn();
535
536 let smoothed =
538 gaussian_filter_f32(&image_d, sigma, Some(mode), None).unwrap_or_else(|_| image_d.clone());
539
540 let ksize = ((2.0 * (3.0 * sigma).ceil() + 1.0).max(3.0)) as usize;
542
543 let mut kernel = ArrayD::zeros(vec![ksize; image.ndim()]);
545 let center = ksize / 2;
546
547 let center_value = -2.0 * image.ndim() as f32;
550
551 let center_idx = vec![center; image.ndim()];
553 kernel[center_idx.as_slice()] = center_value;
554
555 for dim in 0..image.ndim() {
557 let mut idx = vec![center; image.ndim()];
558
559 if center > 0 {
561 idx[dim] = center - 1;
562 kernel[idx.as_slice()] = 1.0;
563 }
564
565 idx[dim] = center + 1;
567 if idx[dim] < ksize {
568 kernel[idx.as_slice()] = 1.0;
569 }
570 }
571
572 let laplacian = convolve(&smoothed, &kernel, Some(mode)).map_err(|e| {
574 NdimageError::ComputationError(format!("Laplacian convolution failed: {}", e))
575 })?;
576
577 let mut result_copy = Array::zeros(image.dim());
579 for i in 0..image.dim().0 {
580 for j in 0..image.dim().1 {
581 result_copy[(i, j)] = laplacian[[i, j]];
582 }
583 }
584
585 if threshold > 0.0 {
587 return Ok(result_copy.mapv(|v| if v.abs() > threshold { v } else { 0.0 }));
588 }
589
590 Ok(result_copy)
591}
592
593#[allow(dead_code)]
632pub fn gradient_edges(
633 image: &Array<f32, Ix2>,
634 method: Option<GradientMethod>,
635 sigma: Option<f32>,
636 mode: Option<BorderMode>,
637) -> NdimageResult<Array<f32, Ix2>> {
638 let method = method.unwrap_or(GradientMethod::Sobel);
639 let mode = mode.unwrap_or(BorderMode::Reflect);
640
641 Ok(gradient_edges_impl(
642 image,
643 method,
644 sigma.unwrap_or(0.0),
645 mode,
646 )?)
647}
648
649#[allow(dead_code)]
651fn gradient_edges_impl(
652 image: &Array<f32, Ix2>,
653 method: GradientMethod,
654 sigma: f32,
655 mode: BorderMode,
656) -> NdimageResult<Array<f32, Ix2>> {
657 let image_d = image.clone().into_dyn();
658
659 let processed = if sigma > 0.0 {
661 gaussian_filter_f32(&image_d, sigma, Some(mode), None).map_err(|e| {
662 NdimageError::ComputationError(format!("Gaussian smoothing failed: {}", e))
663 })?
664 } else {
665 image_d
666 };
667
668 let method_str = match method {
670 GradientMethod::Sobel => "sobel",
671 GradientMethod::Prewitt => "prewitt",
672 GradientMethod::Scharr => "scharr",
673 };
674
675 let magnitude = gradient_magnitude(&processed, Some(mode), Some(method_str)).map_err(|e| {
677 NdimageError::ComputationError(format!("Gradient magnitude calculation failed: {}", e))
678 })?;
679
680 let mut result_copy = Array::zeros(image.dim());
682
683 for i in 0..image.dim().0 {
684 for j in 0..image.dim().1 {
685 result_copy[(i, j)] = magnitude[[i, j]];
686 }
687 }
688
689 Ok(result_copy)
690}
691
692#[allow(dead_code)]
704pub fn sobel_edges(image: &ArrayD<f32>) -> NdimageResult<ArrayD<f32>> {
705 edge_detector_simple(image, Some(GradientMethod::Sobel), None)
706}
707
708#[allow(dead_code)]
744pub fn edge_detector_simple(
745 image: &ArrayD<f32>,
746 method: Option<GradientMethod>,
747 mode: Option<BorderMode>,
748) -> NdimageResult<ArrayD<f32>> {
749 let border_mode = mode.unwrap_or(BorderMode::Reflect);
750
751 let method_str = match method.unwrap_or(GradientMethod::Sobel) {
753 GradientMethod::Sobel => "sobel",
754 GradientMethod::Prewitt => "prewitt",
755 GradientMethod::Scharr => "scharr",
756 };
757
758 gradient_magnitude(image, Some(border_mode), Some(method_str))
759}
760
761#[cfg(test)]
762mod tests {
763 use super::*;
764 use scirs2_core::ndarray::array;
765
766 #[test]
767 fn test_canny_edge_detector() {
768 let image = array![
770 [0.0, 0.0, 0.0, 0.0, 0.0],
771 [0.0, 0.0, 0.0, 0.0, 0.0],
772 [0.0, 0.0, 1.0, 1.0, 1.0],
773 [0.0, 0.0, 1.0, 1.0, 1.0],
774 [0.0, 0.0, 1.0, 1.0, 1.0],
775 ];
776
777 let edges_sobel = canny(&image, 1.0, 0.1, 0.2, None);
779
780 assert!(
782 edges_sobel.fold(false, |acc, &x| acc || x > 0.0),
783 "No edges detected with Sobel"
784 );
785
786 let edges_scharr = canny(&image, 1.0, 0.1, 0.2, Some(GradientMethod::Scharr));
788
789 assert!(
791 edges_scharr.fold(false, |acc, &x| acc || x > 0.0),
792 "No edges detected with Scharr"
793 );
794 }
795
796 #[test]
797 fn test_unified_edge_detector() {
798 let image = array![
800 [0.0, 0.0, 0.0, 0.0, 0.0],
801 [0.0, 0.0, 0.0, 0.0, 0.0],
802 [0.0, 0.0, 1.0, 1.0, 1.0],
803 [0.0, 0.0, 1.0, 1.0, 1.0],
804 [0.0, 0.0, 1.0, 1.0, 1.0],
805 ];
806
807 let edges_default = edge_detector(&image, EdgeDetectionConfig::default())
809 .expect("edge_detector should succeed for test with default config");
810 assert!(
811 edges_default.fold(false, |acc, &x| acc || (x > 0.0)),
812 "Edges should be detected with default config"
813 );
814
815 let gradient_config = EdgeDetectionConfig {
817 algorithm: EdgeDetectionAlgorithm::Gradient,
818 gradient_method: GradientMethod::Sobel,
819 sigma: 1.0,
820 low_threshold: 0.1,
821 high_threshold: 0.2,
822 border_mode: BorderMode::Reflect,
823 return_magnitude: true,
824 };
825
826 let gradient_edges = edge_detector(&image, gradient_config)
827 .expect("edge_detector should succeed for test with gradient config");
828
829 let max_magnitude = gradient_edges.fold(0.0, |acc, &x| if x > acc { x } else { acc });
831 assert!(
832 max_magnitude > 0.1,
833 "Gradient magnitudes should be above threshold"
834 );
835
836 let log_config = EdgeDetectionConfig {
838 algorithm: EdgeDetectionAlgorithm::LoG,
839 sigma: 1.0,
840 low_threshold: 0.05,
841 ..EdgeDetectionConfig::default()
842 };
843
844 let log_edges = edge_detector(&image, log_config)
845 .expect("edge_detector should succeed for test with LoG config");
846
847 assert!(
849 log_edges.fold(false, |acc, &x| acc || (x > 0.0)),
850 "LoG should detect edges"
851 );
852 }
853
854 #[test]
855 fn test_gradient_edges() {
856 let image = array![
858 [0.0, 0.0, 0.0, 0.0, 0.0],
859 [0.0, 0.0, 0.0, 0.0, 0.0],
860 [0.0, 0.0, 1.0, 1.0, 1.0],
861 [0.0, 0.0, 1.0, 1.0, 1.0],
862 [0.0, 0.0, 1.0, 1.0, 1.0],
863 ];
864
865 let edges_sobel = gradient_edges(&image, Some(GradientMethod::Sobel), Some(1.0), None)
867 .expect("gradient_edges with Sobel should succeed for test");
868 let edges_prewitt = gradient_edges(&image, Some(GradientMethod::Prewitt), Some(1.0), None)
869 .expect("gradient_edges with Prewitt should succeed for test");
870 let edges_scharr = gradient_edges(&image, Some(GradientMethod::Scharr), Some(1.0), None)
871 .expect("gradient_edges with Scharr should succeed for test");
872
873 assert!(
875 edges_sobel.iter().any(|&x| x > 0.1),
876 "Sobel should detect edges"
877 );
878 assert!(
879 edges_prewitt.iter().any(|&x| x > 0.1),
880 "Prewitt should detect edges"
881 );
882 assert!(
883 edges_scharr.iter().any(|&x| x > 0.1),
884 "Scharr should detect edges"
885 );
886
887 let diagonalimage = array![
889 [1.0, 0.0, 0.0, 0.0, 0.0],
890 [0.0, 1.0, 0.0, 0.0, 0.0],
891 [0.0, 0.0, 1.0, 0.0, 0.0],
892 [0.0, 0.0, 0.0, 1.0, 0.0],
893 [0.0, 0.0, 0.0, 0.0, 1.0],
894 ];
895
896 let diag_sobel = gradient_edges(&diagonalimage, Some(GradientMethod::Sobel), None, None)
897 .expect("gradient_edges with Sobel should succeed for diagonal test");
898 let diag_scharr = gradient_edges(&diagonalimage, Some(GradientMethod::Scharr), None, None)
899 .expect("gradient_edges with Scharr should succeed for diagonal test");
900
901 let max_sobel = diag_sobel
903 .iter()
904 .fold(0.0, |acc, &x| if x > acc { x } else { acc });
905 let max_scharr = diag_scharr
906 .iter()
907 .fold(0.0, |acc, &x| if x > acc { x } else { acc });
908
909 assert!(
911 max_scharr > max_sobel,
912 "Scharr should have stronger diagonal response"
913 );
914 }
915
916 #[test]
917 fn test_laplacian_edges() {
918 let mut image = Array2::<f32>::zeros((5, 5));
920 image[[2, 2]] = 1.0;
921
922 let edges = laplacian_edges(&image, 1.0, None, None)
924 .expect("laplacian_edges should succeed for test");
925
926 assert!(edges.iter().any(|&x| x != 0.0), "LoG should detect edges");
928
929 assert!(
931 edges[[2, 2]] < 0.0,
932 "Center of point should have negative LoG value"
933 );
934
935 let edges_threshold = laplacian_edges(&image, 1.0, Some(0.1), None)
937 .expect("laplacian_edges with threshold should succeed for test");
938
939 let count_before = edges.iter().filter(|&&x| x != 0.0).count();
941 let count_after = edges_threshold.iter().filter(|&&x| x != 0.0).count();
942
943 assert!(
945 count_after <= count_before,
946 "Thresholding should reduce edge points"
947 );
948 }
949}