edge_detection/
edge.rs

1use image::{self, GenericImageView};
2use rayon::prelude::*;
3use std::f32::consts::*;
4use std::*;
5
6const TAU: f32 = PI * 2.0;
7
8#[inline(always)]
9fn clamp<T: PartialOrd>(f: T, lo: T, hi: T) -> T {
10    debug_assert!(lo < hi);
11    if f > hi {
12        hi
13    } else if f < lo {
14        lo
15    } else {
16        f
17    }
18}
19
20/// The result of a computation.
21#[derive(Clone)]
22pub struct Detection {
23    edges: Vec<Vec<Edge>>,
24}
25
26impl Detection {
27    /// Returns the width of the computed image.
28    pub fn width(&self) -> usize {
29        self.edges.len()
30    }
31
32    /// Returns the height of the computed image.
33    pub fn height(&self) -> usize {
34        self.edges[0].len()
35    }
36
37    /// Linearly interpolates the edge at the specified location.
38    ///
39    /// Similar to as if the edges detection were continuous.
40    pub fn interpolate(&self, x: f32, y: f32) -> Edge {
41        let ax = clamp(x.floor() as isize, 0, self.width() as isize - 1) as usize;
42        let ay = clamp(y.floor() as isize, 0, self.height() as isize - 1) as usize;
43        let bx = clamp(x.ceil() as isize, 0, self.width() as isize - 1) as usize;
44        let by = clamp(y.ceil() as isize, 0, self.height() as isize - 1) as usize;
45        let e1 = self.edges[ax][ay];
46        let e2 = self.edges[bx][ay];
47        let e3 = self.edges[ax][by];
48        let e4 = self.edges[bx][by];
49        let nx = (x.fract() + 1.0).fract();
50        let ny = (y.fract() + 1.0).fract();
51
52        let x1 = Edge {
53            magnitude: e1.magnitude * (1.0 - nx) + e2.magnitude * nx,
54            vec_x: e1.vec_x * (1.0 - nx) + e2.vec_x * nx,
55            vec_y: e1.vec_y * (1.0 - nx) + e2.vec_y * nx,
56        };
57        let x2 = Edge {
58            magnitude: e3.magnitude * (1.0 - nx) + e4.magnitude * nx,
59            vec_x: e3.vec_x * (1.0 - nx) + e4.vec_x * nx,
60            vec_y: e3.vec_y * (1.0 - nx) + e4.vec_y * nx,
61        };
62        Edge {
63            magnitude: x1.magnitude * (1.0 - ny) + x2.magnitude * ny,
64            vec_x: x1.vec_x * (1.0 - ny) + x2.vec_x * ny,
65            vec_y: x1.vec_y * (1.0 - ny) + x2.vec_y * ny,
66        }
67    }
68
69    /// Renders the detected edges to an image.
70    ///
71    /// The intensity of the pixel represents the magnitude of the change in brightnes while the
72    /// color represents the direction.
73    ///
74    /// Useful for debugging.
75    pub fn as_image(&self) -> image::DynamicImage {
76        let img = image::RgbImage::from_fn(self.width() as u32, self.height() as u32, |x, y| {
77            let (h, s, v) = {
78                let edge = &self[(x as usize, y as usize)];
79                ((edge.angle() + TAU) % TAU, 1.0, edge.magnitude())
80            };
81            let (r, g, b) = {
82                // http://www.rapidtables.com/convert/color/hsv-to-rgb.htm
83                let c = v * s;
84                let x = c * (1.0 - ((h / FRAC_PI_3) % 2.0 - 1.0).abs());
85                let m = v - c;
86                let (r, g, b) = match h {
87                    h if h < FRAC_PI_3 => (c, x, 0.0),
88                    h if h < FRAC_PI_3 * 2.0 => (x, c, 0.0),
89                    h if h < PI => (0.0, c, x),
90                    h if h < PI + FRAC_PI_3 => (0.0, x, c),
91                    h if h < PI + FRAC_PI_3 * 2.0 => (x, 0.0, c),
92                    h if h < TAU => (c, 0.0, x),
93                    _ => unreachable!(),
94                };
95                (r + m, g + m, b + m)
96            };
97            image::Rgb([
98                (r * 255.0).round() as u8,
99                (g * 255.0).round() as u8,
100                (b * 255.0).round() as u8,
101            ])
102        });
103        image::DynamicImage::ImageRgb8(img)
104    }
105}
106
107impl ops::Index<usize> for Detection {
108    type Output = Edge;
109    fn index(&self, index: usize) -> &Self::Output {
110        let x = index % self.width();
111        let y = index / self.height();
112        &self.edges[x][y]
113    }
114}
115
116impl ops::Index<(usize, usize)> for Detection {
117    type Output = Edge;
118    fn index(&self, index: (usize, usize)) -> &Self::Output {
119        &self.edges[index.0][index.1]
120    }
121}
122
123/// The computed result for a single pixel.
124#[derive(Copy, Clone, Debug)]
125pub struct Edge {
126    vec_x: f32,
127    vec_y: f32,
128    magnitude: f32,
129}
130
131impl Edge {
132    fn new(vec_x: f32, vec_y: f32) -> Edge {
133        let vec_x = FRAC_1_SQRT_2 * clamp(vec_x, -1.0, 1.0);
134        let vec_y = FRAC_1_SQRT_2 * clamp(vec_y, -1.0, 1.0);
135        let magnitude = f32::hypot(vec_x, vec_y);
136        debug_assert!(0.0 <= magnitude && magnitude <= 1.0);
137        let frac_1_mag = if magnitude != 0.0 {
138            magnitude.recip()
139        } else {
140            1.0
141        };
142        Edge {
143            vec_x: vec_x * frac_1_mag,
144            vec_y: vec_y * frac_1_mag,
145            magnitude,
146        }
147    }
148
149    /// The direction of the gradient in radians.
150    ///
151    /// This is a convenience function for `atan2(direction)`.
152    pub fn angle(&self) -> f32 {
153        f32::atan2(self.vec_y, self.vec_x)
154    }
155
156    /// Returns the direction of the edge scaled by it's magnitude.
157    pub fn dir(&self) -> (f32, f32) {
158        (self.vec_x * self.magnitude(), self.vec_y * self.magnitude())
159    }
160
161    /// Returns a normalized vector of the direction of the change in brightness
162    ///
163    /// The vector will point away from the detected line.
164    /// E.g. a vertical line separating a dark area on the left and light area on the right will
165    /// have it's direction point towards the light area on the right.
166    pub fn dir_norm(&self) -> (f32, f32) {
167        (self.vec_x, self.vec_y)
168    }
169
170    /// The absolute magnitude of the change in brightness.
171    ///
172    /// Between 0 and 1 inclusive.
173    pub fn magnitude(&self) -> f32 {
174        self.magnitude
175    }
176}
177
178/// Computes the canny edges of an image.
179///
180/// The variable `sigma` determines the size of the filter kernel which affects the precision and
181/// SNR of the computation:
182///
183/// * A small sigma (3.0<) creates a kernel which is able to discern fine details but is more prone
184///   to noise.
185/// * Larger values result in detail being lost and are thus best used for detecting large
186///   features. Computation time also increases.
187///
188/// The `weak_threshold` and `strong_threshold` determine what detected pixels are to be regarded
189/// as edges and which should be discarded. They are compared with the absolute magnitude of the
190/// change in brightness.
191///
192/// # Panics:
193/// * If either `strong_threshold` or `weak_threshold` are outisde the range of 0 to 1 inclusive.
194/// * If `strong_threshold` is less than `weak_threshold`.
195/// * If `image` contains no pixels (either it's width or height is 0).
196pub fn canny<T: Into<image::GrayImage>>(
197    image: T,
198    sigma: f32,
199    strong_threshold: f32,
200    weak_threshold: f32,
201) -> Detection {
202    let gs_image = image.into();
203    assert!(gs_image.width() > 0);
204    assert!(gs_image.height() > 0);
205    let edges = detect_edges(&gs_image, sigma);
206    let edges = minmax_suppression(&Detection { edges }, weak_threshold);
207    let edges = hysteresis(&edges, strong_threshold, weak_threshold);
208    Detection { edges }
209}
210
211/// Calculates a 2nd order 2D gaussian derivative with size sigma.
212fn filter_kernel(sigma: f32) -> (usize, Vec<(f32, f32)>) {
213    let size = (sigma * 10.0).round() as usize;
214    let mul_2_sigma_2 = 2.0 * sigma.powi(2);
215    let kernel = (0..size)
216        .flat_map(|y| {
217            (0..size).map(move |x| {
218                let (xf, yf) = (x as f32 - size as f32 / 2.0, y as f32 - size as f32 / 2.0);
219                let g = (-(xf.powi(2) + yf.powi(2)) / mul_2_sigma_2).exp() / mul_2_sigma_2;
220                (xf * g, yf * g)
221            })
222        })
223        .collect();
224    (size, kernel)
225}
226
227fn neighbour_pos_delta(theta: f32) -> (i32, i32) {
228    let neighbours = [
229        (1, 0),   // middle right
230        (1, 1),   // bottom right
231        (0, 1),   // center bottom
232        (-1, 1),  // bottom left
233        (-1, 0),  // middle left
234        (-1, -1), // top left
235        (0, -1),  // center top
236        (1, -1),  // top right
237    ];
238    let n = ((theta + TAU) % TAU) / TAU;
239    let i = (n * 8.0).round() as usize % 8;
240    neighbours[i]
241}
242
243/// Computes the edges in an image using the Canny Method.
244///
245/// `sigma` determines the radius of the Gaussian kernel.
246fn detect_edges(image: &image::GrayImage, sigma: f32) -> Vec<Vec<Edge>> {
247    let (width, height) = (image.width() as i32, image.height() as i32);
248    let (ksize, g_kernel) = filter_kernel(sigma);
249    let ks = ksize as i32;
250    (0..width)
251        .into_par_iter()
252        .map(|g_ix| {
253            let ix = g_ix;
254            let kernel = &g_kernel;
255            (0..height)
256                .into_par_iter()
257                .map(move |iy| {
258                    let mut sum_x = 0.0;
259                    let mut sum_y = 0.0;
260
261                    for kyi in 0..ks {
262                        let ky = kyi - ks / 2;
263                        for kxi in 0..ks {
264                            let kx = kxi - ks / 2;
265                            let k = unsafe {
266                                let i = (kyi * ks + kxi) as usize;
267                                debug_assert!(i < kernel.len());
268                                kernel.get_unchecked(i)
269                            };
270
271                            let pix = unsafe {
272                                // Clamp x and y within the image bounds so no non-existing borders are be
273                                // detected based on some background color outside image bounds.
274                                let x = clamp(ix + kx, 0, width - 1);
275                                let y = clamp(iy + ky, 0, height - 1);
276                                f32::from(image.unsafe_get_pixel(x as u32, y as u32).0[0])
277                            };
278                            sum_x += pix * k.0;
279                            sum_y += pix * k.1;
280                        }
281                    }
282                    Edge::new(sum_x / 255.0, sum_y / 255.0)
283                })
284                .collect()
285        })
286        .collect()
287}
288
289/// Narrows the width of detected edges down to a single pixel.
290fn minmax_suppression(edges: &Detection, weak_threshold: f32) -> Vec<Vec<Edge>> {
291    let (width, height) = (edges.edges.len(), edges.edges[0].len());
292    (0..width)
293        .into_par_iter()
294        .map(|x| {
295            (0..height)
296                .into_par_iter()
297                .map(|y| {
298                    let edge = edges.edges[x][y];
299                    if edge.magnitude < weak_threshold {
300                        // Skip distance computation for non-edges.
301                        return Edge::new(0.0, 0.0);
302                    }
303                    // Truncating the edge magnitudes helps mitigate rounding errors for thick edges.
304                    let truncate = |f: f32| (f * 1e5).round() * 1e-6;
305
306                    // Find out the current pixel represents the highest, most intense, point of an edge by
307                    // traveling in a direction perpendicular to the edge to see if there are any more
308                    // intense edges that are supposedly part of the current edge.
309                    //
310                    // We travel in both directions concurrently, this enables us to stop if one side
311                    // extends longer than the other, greatly improving performance.
312                    let mut select = 0;
313                    let mut select_flip_bit = 1;
314
315                    // The parameters and variables for each side.
316                    let directions = [1.0, -1.0];
317                    let mut distances = [0i32; 2];
318                    let mut seek_positions = [(x as f32, y as f32); 2];
319                    let mut seek_magnitudes = [truncate(edge.magnitude); 2];
320
321                    while (distances[0] - distances[1]).abs() <= 1 {
322                        let seek_pos = &mut seek_positions[select];
323                        let seek_magnitude = &mut seek_magnitudes[select];
324                        let direction = directions[select];
325
326                        seek_pos.0 += edge.dir_norm().0 * direction;
327                        seek_pos.1 += edge.dir_norm().1 * direction;
328                        let interpolated_magnitude =
329                            truncate(edges.interpolate(seek_pos.0, seek_pos.1).magnitude());
330
331                        let trunc_edge_magnitude = truncate(edge.magnitude);
332                        // Keep searching until either:
333                        let end =
334                    // The next edge has a lesser magnitude than the reference edge.
335                    interpolated_magnitude < trunc_edge_magnitude
336                    // The gradient increases, meaning we are going up against an (other) edge.
337                    || *seek_magnitude > trunc_edge_magnitude && interpolated_magnitude < *seek_magnitude;
338                        *seek_magnitude = interpolated_magnitude;
339                        distances[select] += 1;
340
341                        // Switch to the other side.
342                        select ^= select_flip_bit;
343                        if end {
344                            if select_flip_bit == 0 {
345                                break;
346                            }
347                            // After switching to the other side, we set the XOR bit to 0 so we stay there.
348                            select_flip_bit = 0;
349                        }
350                    }
351
352                    // Equal distances denote the middle of the edge.
353                    // A deviation of 1 is allowed for edges over two equal pixels, in which case, the
354                    // outer edge (near the dark side) is preferred.
355                    let is_apex =
356                // The distances are equal, the edge's width is odd, making the apex lie on a
357                // single pixel.
358                distances[0] == distances[1]
359                // There is a difference of 1, the edge's width is even, spreading the apex over
360                // two pixels. This is a special case to handle edges that run along either the X- or X-axis.
361                || (distances[0] - distances[1] == 1 && ((1.0 - edge.vec_x.abs()).abs() < 1e-5 || (1.0 - edge.vec_y.abs()).abs() < 1e-5));
362                    if is_apex {
363                        edge
364                    } else {
365                        Edge::new(0.0, 0.0)
366                    }
367                })
368                .collect()
369        })
370        .collect()
371}
372
373/// Links lines together and discards noise.
374fn hysteresis(edges: &[Vec<Edge>], strong_threshold: f32, weak_threshold: f32) -> Vec<Vec<Edge>> {
375    assert!(0.0 < strong_threshold && strong_threshold < 1.0);
376    assert!(0.0 < weak_threshold && weak_threshold < 1.0);
377    assert!(weak_threshold < strong_threshold);
378
379    let (width, height) = (edges.len(), edges.first().unwrap().len());
380    let mut edges_out: Vec<Vec<Edge>> = vec![vec![Edge::new(0.0, 0.0); height]; width];
381    for x in 0..width {
382        for y in 0..height {
383            if edges[x][y].magnitude < strong_threshold
384                || edges_out[x][y].magnitude >= strong_threshold
385            {
386                continue;
387            }
388
389            // Follow along the edge along both sides, preserving all edges which magnitude is at
390            // least weak_threshold.
391            for side in &[0.0, PI] {
392                let mut current_pos = (x, y);
393                loop {
394                    let edge = edges[current_pos.0][current_pos.1];
395                    edges_out[current_pos.0][current_pos.1] = edge;
396                    // Attempt to find the next line-segment of the edge in tree directions ahead.
397                    let (nb_pos, nb_magnitude) = [FRAC_PI_4, 0.0, -FRAC_PI_4]
398                        .iter()
399                        .map(|bearing| {
400                            neighbour_pos_delta(edge.angle() + FRAC_PI_2 + side + bearing)
401                        })
402                        // Filter out hypothetical neighbours that are outside image bounds.
403                        .filter_map(|(nb_dx, nb_dy)| {
404                            let nb_x = current_pos.0 as i32 + nb_dx;
405                            let nb_y = current_pos.1 as i32 + nb_dy;
406                            if 0 <= nb_x && nb_x < width as i32 && 0 <= nb_y && nb_y < height as i32
407                            {
408                                let nb = (nb_x as usize, nb_y as usize);
409                                Some((nb, edges[nb.0][nb.1].magnitude))
410                            } else {
411                                None
412                            }
413                        })
414                        // Select the neighbouring edge with the highest magnitude as the next
415                        // line-segment.
416                        .fold(((0, 0), 0.0), |(max_pos, max_mag), (pos, mag)| {
417                            if mag > max_mag {
418                                (pos, mag)
419                            } else {
420                                (max_pos, max_mag)
421                            }
422                        });
423                    if nb_magnitude < weak_threshold
424                        || edges_out[nb_pos.0][nb_pos.1].magnitude > weak_threshold
425                    {
426                        break;
427                    }
428                    current_pos = nb_pos;
429                }
430            }
431        }
432    }
433    edges_out
434}
435
436#[cfg(test)]
437mod tests {
438    use super::*;
439
440    fn edges_to_image(edges: &Vec<Vec<Edge>>) -> image::GrayImage {
441        let (width, height) = (edges.len(), edges.first().unwrap().len());
442        let mut image = image::GrayImage::from_pixel(width as u32, height as u32, image::Luma([0]));
443        for x in 0..width {
444            for y in 0..height {
445                let edge = edges[x][y];
446                *image.get_pixel_mut(x as u32, y as u32) =
447                    image::Luma([(edge.magnitude * 255.0).round() as u8]);
448            }
449        }
450        image
451    }
452
453    fn canny_output_stages<T: AsRef<str>>(
454        path: T,
455        sigma: f32,
456        strong_threshold: f32,
457        weak_threshold: f32,
458    ) -> Detection {
459        let path = path.as_ref();
460        let image = image::open(path).unwrap();
461        let edges = detect_edges(&image.to_luma8(), sigma);
462        let intermediage_d = Detection { edges };
463        intermediage_d
464            .as_image()
465            .save(format!("{}.0-vectors.png", path))
466            .unwrap();
467        edges_to_image(&intermediage_d.edges)
468            .save(format!("{}.1-edges.png", path))
469            .unwrap();
470        let edges = minmax_suppression(&intermediage_d, weak_threshold);
471        edges_to_image(&edges)
472            .save(format!("{}.2-minmax.png", path))
473            .unwrap();
474        let edges = hysteresis(&edges, strong_threshold, weak_threshold);
475        edges_to_image(&edges)
476            .save(format!("{}.3-hysteresis.png", path))
477            .unwrap();
478        let detection = Detection { edges };
479        detection
480            .as_image()
481            .save(format!("{}.4-result.png", path))
482            .unwrap();
483        detection
484    }
485
486    #[test]
487    fn neighbour_pos_delta_from_theta() {
488        let neighbours = [
489            (1, 0),
490            (1, 1),
491            (0, 1),
492            (-1, 1),
493            (-1, 0),
494            (-1, -1),
495            (0, -1),
496            (1, -1),
497        ];
498        for nb in neighbours.iter() {
499            let d = neighbour_pos_delta(f32::atan2(nb.1 as f32, nb.0 as f32));
500            assert_eq!(*nb, d);
501        }
502    }
503
504    #[test]
505    fn edge_new() {
506        let e = Edge::new(1.0, 0.0);
507        assert!(1.0 - 1e-6 < e.vec_x && e.vec_x < 1.0 + 1e-6);
508        assert!(-1e-5 < e.vec_y && e.vec_y < 1e-6);
509
510        let e = Edge::new(1.0, 1.0);
511        assert!(FRAC_1_SQRT_2 - 1e-5 < e.vec_x && e.vec_x < FRAC_1_SQRT_2 + 1e-6);
512        assert!(FRAC_1_SQRT_2 - 1e-5 < e.vec_y && e.vec_y < FRAC_1_SQRT_2 + 1e-6);
513        assert!(1.0 - 1e-6 < e.magnitude && e.magnitude < 1.0 + 1e-6);
514    }
515
516    #[test]
517    fn detection_interpolate() {
518        let dummy = |magnitude| Edge {
519            magnitude,
520            vec_x: 0.0,
521            vec_y: 0.0,
522        };
523        let d = Detection {
524            edges: vec![vec![dummy(2.0), dummy(8.0)], vec![dummy(4.0), dummy(16.0)]],
525        };
526        assert!((d.interpolate(0.0, 0.0).magnitude() - 2.0).abs() <= 1e-6);
527        assert!((d.interpolate(1.0, 0.0).magnitude() - 4.0).abs() <= 1e-6);
528        assert!((d.interpolate(0.0, 1.0).magnitude() - 8.0).abs() <= 1e-6);
529        assert!((d.interpolate(1.0, 1.0).magnitude() - 16.0).abs() <= 1e-6);
530        assert!((d.interpolate(0.5, 0.0).magnitude() - 3.0).abs() <= 1e-6);
531        assert!((d.interpolate(0.0, 0.5).magnitude() - 5.0).abs() <= 1e-6);
532        assert!((d.interpolate(0.5, 1.0).magnitude() - 12.0).abs() <= 1e-6);
533        assert!((d.interpolate(1.0, 0.5).magnitude() - 10.0).abs() <= 1e-6);
534    }
535
536    #[test]
537    fn kernel_integral_in_bounds() {
538        // The integral for the filter kernel should approximate 0.
539        for sigma_i in 1..200 {
540            let sigma = sigma_i as f32 / 10.0;
541            let (ksize, kernel) = filter_kernel(sigma);
542            assert!(ksize.pow(2) == kernel.len());
543            let mut sum_x = 0.0;
544            let mut sum_y = 0.0;
545            for (gx, gy) in kernel {
546                sum_x += gx;
547                sum_y += gy;
548            }
549            println!(
550                "sum = ({}, {}), sigma = {}, kernel_size = {}",
551                sum_x, sum_y, sigma, ksize
552            );
553            assert!(-0.0001 < sum_x && sum_x <= 0.0001);
554            assert!(-0.0001 < sum_y && sum_y <= 0.0001);
555        }
556    }
557
558    /// Tests whether a vertical line of 1px wide exists in the middle of the image.
559    ///
560    /// Returns the location of the line on the X-axis.
561    fn detect_vertical_line(detection: &Detection) -> usize {
562        // Find the line.
563        let mut line_x = None;
564        for x in 0..detection.width() {
565            if detection.edges[x][detection.height() / 2].magnitude > 0.5 {
566                if line_x.is_some() {
567                    panic!("the line is thicker than 1px");
568                }
569                line_x = Some(x)
570            }
571        }
572        let line_x = line_x.expect("no line detected");
573        // The line should be at about the middle of the image.
574        let middle = detection.width() / 2;
575        assert!(middle - 1 <= line_x && line_x <= middle);
576        // The line should be continuous.
577        for y in 0..detection.height() {
578            let edge = detection.edges[line_x][y];
579            assert!(edge.magnitude > 0.0);
580        }
581        // The line should be the only thing detected.
582        for x in 0..detection.width() {
583            if x == line_x {
584                continue;
585            }
586            for y in 0..detection.height() {
587                assert!(detection.edges[x][y].magnitude == 0.0);
588            }
589        }
590        line_x
591    }
592
593    #[test]
594    fn detect_vertical_line_simple() {
595        let d = canny_output_stages("testdata/line-simple.png", 1.2, 0.4, 0.05);
596        let x = detect_vertical_line(&d);
597        // The direction of the line's surface normal should follow the X-axis.
598        for y in 0..d.height() {
599            assert!(d.edges[x][y].angle().abs() < 1e-5);
600        }
601    }
602
603    #[test]
604    fn detect_vertical_line_fuzzy() {
605        let d = canny_output_stages("testdata/line-fuzzy.png", 2.0, 0.4, 0.05);
606        let x = detect_vertical_line(&d);
607        // The direction of the line's surface normal should follow the X-axis.
608        for y in 0..d.height() {
609            assert!(d.edges[x][y].angle().abs() < 0.01);
610        }
611    }
612
613    #[test]
614    fn detect_vertical_line_weakening() {
615        let d = canny_output_stages("testdata/line-weakening.png", 1.2, 0.7, 0.05);
616        detect_vertical_line(&d);
617        // The line vectors are not tested because they are distorted by the gradient.
618    }
619}
620
621#[cfg(all(test, feature = "unstable"))]
622mod benchmarks {
623    extern crate test;
624    use super::*;
625
626    static IMG_PATH: &str = "testdata/circle.png";
627
628    #[bench]
629    fn bench_filter_kernel_low_sigma(b: &mut test::Bencher) {
630        b.iter(|| filter_kernel(1.2));
631    }
632
633    #[bench]
634    fn bench_filter_kernel_high_sigma(b: &mut test::Bencher) {
635        b.iter(|| filter_kernel(5.0));
636    }
637
638    #[bench]
639    fn bench_detect_edges_low_sigma(b: &mut test::Bencher) {
640        let image = image::open(IMG_PATH).unwrap().to_luma8();
641        b.iter(|| detect_edges(&image, 1.2));
642    }
643
644    #[bench]
645    fn bench_detect_edges_high_sigma(b: &mut test::Bencher) {
646        let image = image::open(IMG_PATH).unwrap().to_luma8();
647        b.iter(|| detect_edges(&image, 5.0));
648    }
649
650    #[bench]
651    fn bench_minmax_suppression_low_sigma(b: &mut test::Bencher) {
652        let image = image::open(IMG_PATH).unwrap().to_luma8();
653        let edges = Detection {
654            edges: detect_edges(&image, 1.2),
655        };
656        b.iter(|| minmax_suppression(&edges, 0.01));
657    }
658
659    #[bench]
660    fn bench_minmax_suppression_high_sigma(b: &mut test::Bencher) {
661        let image = image::open(IMG_PATH).unwrap().to_luma8();
662        let edges = Detection {
663            edges: detect_edges(&image, 5.0),
664        };
665        b.iter(|| minmax_suppression(&edges, 0.01));
666    }
667
668    #[bench]
669    fn bench_hysteresis(b: &mut test::Bencher) {
670        let image = image::open(IMG_PATH).unwrap().to_luma8();
671        let edges = Detection {
672            edges: detect_edges(&image, 1.2),
673        };
674        let edges = minmax_suppression(&edges, 0.1);
675        b.iter(|| hysteresis(&edges, 0.4, 0.1));
676    }
677}