scirs2_ndimage/features/
edges.rs

1//! Edge detection algorithms
2//!
3//! This module provides functions for detecting edges in n-dimensional arrays,
4//! including gradient-based methods, zero-crossing methods, and other edge detection techniques.
5
6use 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/// Gradient calculation method for edge detection
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum GradientMethod {
16    /// Sobel operator (default)
17    Sobel,
18    /// Prewitt operator
19    Prewitt,
20    /// Scharr operator (better rotational symmetry)
21    Scharr,
22}
23
24/// Edge detection algorithm options
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum EdgeDetectionAlgorithm {
27    /// Canny edge detector (multi-stage algorithm)
28    Canny,
29    /// Laplacian of Gaussian (zero-crossing method)
30    LoG,
31    /// Simple gradient-based detection
32    Gradient,
33}
34
35/// Configuration for edge detection algorithms
36#[derive(Debug, Clone)]
37pub struct EdgeDetectionConfig {
38    /// Algorithm to use for edge detection
39    pub algorithm: EdgeDetectionAlgorithm,
40    /// Method to calculate gradients
41    pub gradient_method: GradientMethod,
42    /// Standard deviation for Gaussian blur applied before edge detection
43    pub sigma: f32,
44    /// Low threshold for hysteresis (for Canny edge detection)
45    pub low_threshold: f32,
46    /// High threshold for hysteresis (for Canny edge detection)
47    pub high_threshold: f32,
48    /// Border handling mode
49    pub border_mode: BorderMode,
50    /// Whether to return the edge magnitude (f32) or binary edges (bool)
51    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/// Unified edge detector function that works with different algorithms
69///
70/// This function provides a common interface for various edge detection methods,
71/// allowing users to choose the algorithm and configure parameters through a single function.
72///
73/// # Arguments
74///
75/// * `image` - Input image as a 2D array
76/// * `config` - Configuration for the edge detection algorithm
77///
78/// # Returns
79///
80/// * If `config.return_magnitude` is `false` (default), returns a boolean array where `true` indicates edge pixels
81/// * If `config.return_magnitude` is `true`, returns the original edge magnitude as floating-point values
82///
83/// # Example
84///
85/// ```
86/// use scirs2_core::ndarray::array;
87/// use scirs2_ndimage::features::{edge_detector, EdgeDetectionConfig, EdgeDetectionAlgorithm, GradientMethod};
88/// use scirs2_ndimage::filters::BorderMode;
89///
90/// // Use a larger test image to avoid overflow issues in doctests
91/// let image = array![
92///     [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
93///     [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
94///     [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
95///     [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0],
96///     [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0],
97///     [0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0],
98///     [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
99/// ];
100///
101/// // Default settings - Canny edge detection with Sobel gradient
102/// let edges = edge_detector(&image, EdgeDetectionConfig::default()).unwrap();
103///
104/// // Custom configuration - Gradient edge detection with Scharr operator and custom thresholds
105/// let custom_config = EdgeDetectionConfig {
106///     algorithm: EdgeDetectionAlgorithm::Gradient,
107///     gradient_method: GradientMethod::Scharr,
108///     sigma: 1.5,
109///     low_threshold: 0.05,
110///     high_threshold: 0.15,
111///     border_mode: BorderMode::Reflect,
112///     return_magnitude: true,
113/// };
114/// let edge_magnitudes = edge_detector(&image, custom_config).unwrap();
115/// ```
116#[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            // Already returns f32 values, so we just return it as is
124            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                // Threshold to binary edges
143                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                // Threshold to binary edges
160                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/// Canny edge detector
171///
172/// The Canny edge detector is an edge detection operator that uses a multi-stage algorithm
173/// to detect a wide range of edges in images. It was developed by John F. Canny in 1986.
174///
175/// # Arguments
176///
177/// * `image` - Input array, 2D array
178/// * `sigma` - Standard deviation of the Gaussian filter
179/// * `low_threshold` - Lower threshold for hysteresis
180/// * `high_threshold` - Upper threshold for hysteresis
181/// * `method` - Gradient calculation method (defaults to Sobel if None)
182///
183/// # Returns
184///
185/// * Array of edges, where `true` values indicate detected edges
186///
187/// # Example
188///
189/// ```
190/// use scirs2_core::ndarray::array;
191/// use scirs2_ndimage::features::{canny, GradientMethod};
192///
193/// let image = array![
194///     [0.0, 0.0, 0.0, 0.0, 0.0],
195///     [0.0, 0.0, 0.0, 0.0, 0.0],
196///     [0.0, 0.0, 1.0, 0.0, 0.0],
197///     [0.0, 0.0, 0.0, 0.0, 0.0],
198///     [0.0, 0.0, 0.0, 0.0, 0.0],
199/// ];
200///
201/// // Using default Sobel method
202/// let edges = canny(&image, 1.0, 0.1, 0.2, None);
203///
204/// // Using Scharr method for better edge detection
205/// let edges_scharr = canny(&image, 1.0, 0.1, 0.2, Some(GradientMethod::Scharr));
206/// ```
207#[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// Internal implementation of Canny edge detection with enhanced performance
229#[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    // Step 1: Gaussian filter to reduce noise
241    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    // Step 2: Calculate gradients using the specified method
246    let gradients = calculate_gradient(&smoothed, method, mode);
247    let gradient_x = &gradients.0;
248    let gradient_y = &gradients.1;
249
250    // Step 3: Calculate gradient magnitude and direction
251    let (magnitude, direction) =
252        calculate_magnitude_and_direction(gradient_x, gradient_y, image_dim);
253
254    // Step 4: Non-maximum suppression
255    let suppressed = non_maximum_suppression(&magnitude, &direction);
256
257    // Step 5: Double thresholding and edge tracking by hysteresis
258    hysteresis_thresholding(&suppressed, low_threshold, high_threshold)
259}
260
261// Calculate gradients using the specified method
262#[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// Calculate magnitude and direction from gradient components
292#[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    // Create a copy to avoid mutable borrow conflict
302    let mut mag_copy = Array::<f32, Ix2>::zeros(shape);
303
304    // Calculate gradient magnitude and direction
305    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        // Calculate magnitude using Euclidean distance
311        mag_copy[pos] = (gx * gx + gy * gy).sqrt();
312
313        // Calculate direction in degrees and convert to one of four directions (0, 45, 90, 135)
314        let angle = gy.atan2(gx) * 180.0 / PI;
315        let angle = if angle < 0.0 { angle + 180.0 } else { angle };
316
317        // Quantize the angle to 4 directions (0, 45, 90, 135 degrees)
318        direction[pos] = if !(22.5..157.5).contains(&angle) {
319            0.0 // 0 degrees (horizontal)
320        } else if (22.5..67.5).contains(&angle) {
321            45.0 // 45 degrees
322        } else if (67.5..112.5).contains(&angle) {
323            90.0 // 90 degrees (vertical)
324        } else {
325            135.0 // 135 degrees
326        };
327    }
328
329    (mag_copy, direction)
330}
331
332// Non-maximum suppression to thin edges
333#[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    // Skip the border pixels to avoid bounds checking
342    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 the magnitude is zero, skip further processing
348            if mag == 0.0 {
349                continue;
350            }
351
352            let (neighbor1, neighbor2) = get_gradient_neighbors(row, col, dir, magnitude);
353
354            // If the current pixel is a local maximum, keep it, otherwise suppress it
355            if mag >= neighbor1 && mag >= neighbor2 {
356                suppressed[(row, col)] = mag;
357            }
358        }
359    }
360
361    suppressed
362}
363
364// Hysteresis thresholding to connect edges
365#[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    // First pass: identify strong edges and potential candidates
376    for ((row, col), &val) in suppressed.indexed_iter() {
377        if val >= high_threshold {
378            // Mark as strong edge
379            result[(row, col)] = 1.0;
380
381            // Add strong edges to the candidates list to check their neighbors
382            candidates.push((row, col));
383        } else if val >= low_threshold {
384            // Potential edge candidate - will be processed in the next phase
385            candidates.push((row, col));
386        }
387    }
388
389    // Second pass: Process candidates using a queue-based approach for efficiency
390    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            // Skip already processed pixels or confirmed edges
394            continue;
395        }
396
397        processed[(*row, *col)] = true;
398
399        // If this candidate is connected to a strong edge, mark it
400        if is_connected_to_strong_edge(*row, *col, &result) {
401            result[(*row, *col)] = 1.0;
402
403            // Add neighbors to the queue for further processing
404            let mut queue = Vec::with_capacity(8); // Pre-allocate for efficiency
405            queue.push((*row, *col));
406
407            while let Some((r, c)) = queue.pop() {
408                // Check all 8 neighbors
409                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 this is a candidate edge, mark it and add to queue
416                        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/// Helper function to get the neighbors in the gradient direction
431#[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    // 0 degrees (horizontal)
439    if direction == 0.0 {
440        (magnitude[(row, col - 1)], magnitude[(row, col + 1)])
441    }
442    // 45 degrees
443    else if direction == 45.0 {
444        (magnitude[(row - 1, col + 1)], magnitude[(row + 1, col - 1)])
445    }
446    // 90 degrees (vertical)
447    else if direction == 90.0 {
448        (magnitude[(row - 1, col)], magnitude[(row + 1, col)])
449    }
450    // 135 degrees
451    else {
452        (magnitude[(row - 1, col - 1)], magnitude[(row + 1, col + 1)])
453    }
454}
455
456/// Helper function to check if a pixel is connected to a strong edge
457#[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/// Laplacian of Gaussian (LoG) edge detector
472///
473/// The Laplacian of Gaussian operator computes the second derivative of an image.
474/// It highlights regions of rapid intensity change and is often used for edge detection.
475/// This implementation first applies Gaussian smoothing, then computes the Laplacian.
476///
477/// # Arguments
478///
479/// * `image` - Input array
480/// * `sigma` - Standard deviation of the Gaussian filter
481/// * `threshold` - Optional threshold for zero-crossing detection (if None, no thresholding is applied)
482/// * `mode` - Border handling mode (defaults to Reflect)
483///
484/// # Returns
485///
486/// * Result containing the LoG filtered image
487///
488/// # Example
489///
490/// ```
491/// use scirs2_core::ndarray::array;
492/// use scirs2_ndimage::features::laplacian_edges;
493/// use scirs2_ndimage::filters::BorderMode;
494///
495/// let image = array![
496///     [0.0, 0.0, 0.0, 0.0, 0.0],
497///     [0.0, 0.0, 0.0, 0.0, 0.0],
498///     [0.0, 0.0, 1.0, 0.0, 0.0],
499///     [0.0, 0.0, 0.0, 0.0, 0.0],
500///     [0.0, 0.0, 0.0, 0.0, 0.0],
501/// ];
502///
503/// // Apply LoG filter with default settings
504/// let edges = laplacian_edges(&image, 1.0, None, None).unwrap();
505///
506/// // Apply LoG filter with thresholding
507/// let edges_threshold = laplacian_edges(&image, 1.0, Some(0.1), None).unwrap();
508/// ```
509#[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// Internal implementation of Laplacian of Gaussian edge detection
526#[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    // Convert to dynamic array for processing with our filter functions
534    let image_d = image.clone().into_dyn();
535
536    // First, apply Gaussian filter to reduce noise
537    let smoothed =
538        gaussian_filter_f32(&image_d, sigma, Some(mode), None).unwrap_or_else(|_| image_d.clone());
539
540    // Create Laplace kernel size based on sigma (typically 2*ceil(3*sigma) + 1)
541    let ksize = ((2.0 * (3.0 * sigma).ceil() + 1.0).max(3.0)) as usize;
542
543    // Create Laplace kernel
544    let mut kernel = ArrayD::zeros(vec![ksize; image.ndim()]);
545    let center = ksize / 2;
546
547    // Fill kernel with the discrete Laplacian operator
548    // For 2D: [0 1 0; 1 -4 1; 0 1 0]
549    let center_value = -2.0 * image.ndim() as f32;
550
551    // Set center to the sum of all other values (negative)
552    let center_idx = vec![center; image.ndim()];
553    kernel[center_idx.as_slice()] = center_value;
554
555    // Set direct neighbors to 1
556    for dim in 0..image.ndim() {
557        let mut idx = vec![center; image.ndim()];
558
559        // Set previous neighbor
560        if center > 0 {
561            idx[dim] = center - 1;
562            kernel[idx.as_slice()] = 1.0;
563        }
564
565        // Set next neighbor
566        idx[dim] = center + 1;
567        if idx[dim] < ksize {
568            kernel[idx.as_slice()] = 1.0;
569        }
570    }
571
572    // Apply convolution
573    let laplacian = convolve(&smoothed, &kernel, Some(mode)).map_err(|e| {
574        NdimageError::ComputationError(format!("Laplacian convolution failed: {}", e))
575    })?;
576
577    // Convert back to 2D array
578    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    // Apply thresholding if requested
586    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/// Gradient-based edge detection
594///
595/// Detects edges using the gradient magnitude calculated with the specified method.
596/// This is a simpler alternative to Canny edge detection when you don't need the
597/// same level of edge quality or performance.
598///
599/// # Arguments
600///
601/// * `image` - Input image
602/// * `method` - Gradient calculation method (defaults to Sobel if None)
603/// * `sigma` - Standard deviation of Gaussian filter applied before edge detection (if None, no smoothing is applied)
604/// * `mode` - Border handling mode (defaults to Reflect)
605///
606/// # Returns
607///
608/// * Edge magnitude as an array of floats
609///
610/// # Example
611///
612/// ```
613/// use scirs2_core::ndarray::array;
614/// use scirs2_ndimage::features::{gradient_edges, GradientMethod};
615/// use scirs2_ndimage::filters::BorderMode;
616///
617/// let image = array![
618///     [0.0, 0.0, 0.0, 0.0, 0.0],
619///     [0.0, 0.0, 0.0, 0.0, 0.0],
620///     [0.0, 0.0, 1.0, 0.0, 0.0],
621///     [0.0, 0.0, 0.0, 0.0, 0.0],
622///     [0.0, 0.0, 0.0, 0.0, 0.0],
623/// ];
624///
625/// // Using default Sobel method without smoothing
626/// let edges = gradient_edges(&image, None, None, None).unwrap();
627///
628/// // Using Scharr method with Gaussian smoothing
629/// let edges_scharr = gradient_edges(&image, Some(GradientMethod::Scharr), Some(1.0), None).unwrap();
630/// ```
631#[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// Internal implementation of gradient-based edge detection
650#[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    // Apply Gaussian smoothing if sigma > 0
660    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    // Map the gradient method to the corresponding string parameter for gradient_magnitude
669    let method_str = match method {
670        GradientMethod::Sobel => "sobel",
671        GradientMethod::Prewitt => "prewitt",
672        GradientMethod::Scharr => "scharr",
673    };
674
675    // Calculate gradient magnitude
676    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    // Convert back to 2D array
681    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/// Sobel edge detector (for backward compatibility)
693///
694/// A simplified wrapper around the sobel function that returns the magnitude of edges.
695///
696/// # Arguments
697///
698/// * `image` - Input array
699///
700/// # Returns
701///
702/// * Result containing the magnitude of edges
703#[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/// Enhanced edge detector (compatible with previous API)
709///
710/// A simplified wrapper around the gradient_magnitude function that returns the magnitude of edges.
711/// This function allows selecting different gradient methods for more flexibility.
712///
713/// # Arguments
714///
715/// * `image` - Input array
716/// * `method` - Gradient calculation method (defaults to Sobel if None)
717/// * `mode` - Border handling mode (defaults to Reflect)
718///
719/// # Returns
720///
721/// * Result containing the magnitude of edges
722///
723/// # Example
724///
725/// ```
726/// use scirs2_core::ndarray::array;
727/// use scirs2_ndimage::features::{edge_detector_simple, GradientMethod};
728///
729/// let image = array![
730///     [0.0, 0.0, 0.0, 0.0, 0.0],
731///     [0.0, 0.0, 0.0, 0.0, 0.0],
732///     [0.0, 0.0, 1.0, 0.0, 0.0],
733///     [0.0, 0.0, 0.0, 0.0, 0.0],
734///     [0.0, 0.0, 0.0, 0.0, 0.0],
735/// ].into_dyn();
736///
737/// // Using default Sobel method
738/// let edges = edge_detector_simple(&image, None, None).unwrap();
739///
740/// // Using Scharr method for better rotational invariance
741/// let edges_scharr = edge_detector_simple(&image, Some(GradientMethod::Scharr), None).unwrap();
742/// ```
743#[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    // Map the gradient method to the corresponding string parameter for gradient_magnitude
752    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        // Create a simple test image with a clear edge
769        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        // Apply Canny edge detection with default Sobel method
778        let edges_sobel = canny(&image, 1.0, 0.1, 0.2, None);
779
780        // Check that edges are detected
781        assert!(
782            edges_sobel.fold(false, |acc, &x| acc || x > 0.0),
783            "No edges detected with Sobel"
784        );
785
786        // Apply Canny with Scharr method
787        let edges_scharr = canny(&image, 1.0, 0.1, 0.2, Some(GradientMethod::Scharr));
788
789        // Check that edges are detected
790        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        // Create a simple test image with a clear edge
799        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        // Test with default config (Canny)
808        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        // Test with gradient edge detection
816        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        // Check that magnitudes are returned
830        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        // Test with Laplacian of Gaussian
837        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        // Check that edges are detected
848        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        // Create a simple test image with a clear edge
857        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        // Test different gradient methods
866        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        // Check that all methods detect edges
874        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        // Scharr should detect better gradient responses for diagonal edges
888        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        // Calculate the maximum magnitude for each
902        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        // Scharr should have higher response for diagonal edges
910        assert!(
911            max_scharr > max_sobel,
912            "Scharr should have stronger diagonal response"
913        );
914    }
915
916    #[test]
917    fn test_laplacian_edges() {
918        // Create a simple test image with a point
919        let mut image = Array2::<f32>::zeros((5, 5));
920        image[[2, 2]] = 1.0;
921
922        // Apply LoG filter
923        let edges = laplacian_edges(&image, 1.0, None, None)
924            .expect("laplacian_edges should succeed for test");
925
926        // Check that edges are detected
927        assert!(edges.iter().any(|&x| x != 0.0), "LoG should detect edges");
928
929        // Center should have negative value (for a bright point)
930        assert!(
931            edges[[2, 2]] < 0.0,
932            "Center of point should have negative LoG value"
933        );
934
935        // Apply with thresholding
936        let edges_threshold = laplacian_edges(&image, 1.0, Some(0.1), None)
937            .expect("laplacian_edges with threshold should succeed for test");
938
939        // Count non-zero values before and after thresholding
940        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        // Thresholding should reduce the number of non-zero values
944        assert!(
945            count_after <= count_before,
946            "Thresholding should reduce edge points"
947        );
948    }
949}