edge_detection/
edge.rs

1use image::{self, DynamicImage, GenericImageView, GrayImage};
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<DynamicImage>>(
197    image: T,
198    sigma: f32,
199    strong_threshold: f32,
200    weak_threshold: f32,
201) -> Detection {
202    let dyn_img: DynamicImage = image.into();
203    let gs_image: GrayImage = dyn_img.into_luma8();
204    assert!(gs_image.width() > 0);
205    assert!(gs_image.height() > 0);
206    let edges = detect_edges(&gs_image, sigma);
207    let edges = minmax_suppression(&Detection { edges }, weak_threshold);
208    let edges = hysteresis(&edges, strong_threshold, weak_threshold);
209    Detection { edges }
210}
211
212/// Calculates a 2nd order 2D gaussian derivative with size sigma.
213fn filter_kernel(sigma: f32) -> (usize, Vec<(f32, f32)>) {
214    let size = (sigma * 10.0).round() as usize;
215    let mul_2_sigma_2 = 2.0 * sigma.powi(2);
216    let kernel = (0..size)
217        .flat_map(|y| {
218            (0..size).map(move |x| {
219                let (xf, yf) = (x as f32 - size as f32 / 2.0, y as f32 - size as f32 / 2.0);
220                let g = (-(xf.powi(2) + yf.powi(2)) / mul_2_sigma_2).exp() / mul_2_sigma_2;
221                (xf * g, yf * g)
222            })
223        })
224        .collect();
225    (size, kernel)
226}
227
228fn neighbour_pos_delta(theta: f32) -> (i32, i32) {
229    let neighbours = [
230        (1, 0),   // middle right
231        (1, 1),   // bottom right
232        (0, 1),   // center bottom
233        (-1, 1),  // bottom left
234        (-1, 0),  // middle left
235        (-1, -1), // top left
236        (0, -1),  // center top
237        (1, -1),  // top right
238    ];
239    let n = ((theta + TAU) % TAU) / TAU;
240    let i = (n * 8.0).round() as usize % 8;
241    neighbours[i]
242}
243
244/// Computes the edges in an image using the Canny Method.
245///
246/// `sigma` determines the radius of the Gaussian kernel.
247fn detect_edges(image: &image::GrayImage, sigma: f32) -> Vec<Vec<Edge>> {
248    let (width, height) = (image.width() as i32, image.height() as i32);
249    let (ksize, g_kernel) = filter_kernel(sigma);
250    let ks = ksize as i32;
251    (0..width)
252        .into_par_iter()
253        .map(|g_ix| {
254            let ix = g_ix;
255            let kernel = &g_kernel;
256            (0..height)
257                .into_par_iter()
258                .map(move |iy| {
259                    let mut sum_x = 0.0;
260                    let mut sum_y = 0.0;
261
262                    for kyi in 0..ks {
263                        let ky = kyi - ks / 2;
264                        for kxi in 0..ks {
265                            let kx = kxi - ks / 2;
266                            let k = unsafe {
267                                let i = (kyi * ks + kxi) as usize;
268                                debug_assert!(i < kernel.len());
269                                kernel.get_unchecked(i)
270                            };
271
272                            let pix = unsafe {
273                                // Clamp x and y within the image bounds so no non-existing borders are be
274                                // detected based on some background color outside image bounds.
275                                let x = clamp(ix + kx, 0, width - 1);
276                                let y = clamp(iy + ky, 0, height - 1);
277                                f32::from(image.unsafe_get_pixel(x as u32, y as u32).0[0])
278                            };
279                            sum_x += pix * k.0;
280                            sum_y += pix * k.1;
281                        }
282                    }
283                    Edge::new(sum_x / 255.0, sum_y / 255.0)
284                })
285                .collect()
286        })
287        .collect()
288}
289
290/// Narrows the width of detected edges down to a single pixel.
291fn minmax_suppression(edges: &Detection, weak_threshold: f32) -> Vec<Vec<Edge>> {
292    let (width, height) = (edges.edges.len(), edges.edges[0].len());
293    (0..width)
294        .into_par_iter()
295        .map(|x| {
296            (0..height)
297                .into_par_iter()
298                .map(|y| {
299                    let edge = edges.edges[x][y];
300                    if edge.magnitude < weak_threshold {
301                        // Skip distance computation for non-edges.
302                        return Edge::new(0.0, 0.0);
303                    }
304                    // Truncating the edge magnitudes helps mitigate rounding errors for thick edges.
305                    let truncate = |f: f32| (f * 1e5).round() * 1e-6;
306
307                    // Find out the current pixel represents the highest, most intense, point of an edge by
308                    // traveling in a direction perpendicular to the edge to see if there are any more
309                    // intense edges that are supposedly part of the current edge.
310                    //
311                    // We travel in both directions concurrently, this enables us to stop if one side
312                    // extends longer than the other, greatly improving performance.
313                    let mut select = 0;
314                    let mut select_flip_bit = 1;
315
316                    // The parameters and variables for each side.
317                    let directions = [1.0, -1.0];
318                    let mut distances = [0i32; 2];
319                    let mut seek_positions = [(x as f32, y as f32); 2];
320                    let mut seek_magnitudes = [truncate(edge.magnitude); 2];
321
322                    while (distances[0] - distances[1]).abs() <= 1 {
323                        let seek_pos = &mut seek_positions[select];
324                        let seek_magnitude = &mut seek_magnitudes[select];
325                        let direction = directions[select];
326
327                        seek_pos.0 += edge.dir_norm().0 * direction;
328                        seek_pos.1 += edge.dir_norm().1 * direction;
329                        let interpolated_magnitude =
330                            truncate(edges.interpolate(seek_pos.0, seek_pos.1).magnitude());
331
332                        let trunc_edge_magnitude = truncate(edge.magnitude);
333                        // Keep searching until either:
334                        let end =
335                    // The next edge has a lesser magnitude than the reference edge.
336                    interpolated_magnitude < trunc_edge_magnitude
337                    // The gradient increases, meaning we are going up against an (other) edge.
338                    || *seek_magnitude > trunc_edge_magnitude && interpolated_magnitude < *seek_magnitude;
339                        *seek_magnitude = interpolated_magnitude;
340                        distances[select] += 1;
341
342                        // Switch to the other side.
343                        select ^= select_flip_bit;
344                        if end {
345                            if select_flip_bit == 0 {
346                                break;
347                            }
348                            // After switching to the other side, we set the XOR bit to 0 so we stay there.
349                            select_flip_bit = 0;
350                        }
351                    }
352
353                    // Equal distances denote the middle of the edge.
354                    // A deviation of 1 is allowed for edges over two equal pixels, in which case, the
355                    // outer edge (near the dark side) is preferred.
356                    let is_apex =
357                // The distances are equal, the edge's width is odd, making the apex lie on a
358                // single pixel.
359                distances[0] == distances[1]
360                // There is a difference of 1, the edge's width is even, spreading the apex over
361                // two pixels. This is a special case to handle edges that run along either the X- or X-axis.
362                || (distances[0] - distances[1] == 1 && ((1.0 - edge.vec_x.abs()).abs() < 1e-5 || (1.0 - edge.vec_y.abs()).abs() < 1e-5));
363                    if is_apex {
364                        edge
365                    } else {
366                        Edge::new(0.0, 0.0)
367                    }
368                })
369                .collect()
370        })
371        .collect()
372}
373
374/// Links lines together and discards noise.
375fn hysteresis(edges: &[Vec<Edge>], strong_threshold: f32, weak_threshold: f32) -> Vec<Vec<Edge>> {
376    assert!(0.0 < strong_threshold && strong_threshold < 1.0);
377    assert!(0.0 < weak_threshold && weak_threshold < 1.0);
378    assert!(weak_threshold < strong_threshold);
379
380    let (width, height) = (edges.len(), edges.first().unwrap().len());
381    let mut edges_out: Vec<Vec<Edge>> = vec![vec![Edge::new(0.0, 0.0); height]; width];
382    for x in 0..width {
383        for y in 0..height {
384            if edges[x][y].magnitude < strong_threshold
385                || edges_out[x][y].magnitude >= strong_threshold
386            {
387                continue;
388            }
389
390            // Follow along the edge along both sides, preserving all edges which magnitude is at
391            // least weak_threshold.
392            for side in &[0.0, PI] {
393                let mut current_pos = (x, y);
394                loop {
395                    let edge = edges[current_pos.0][current_pos.1];
396                    edges_out[current_pos.0][current_pos.1] = edge;
397                    // Attempt to find the next line-segment of the edge in tree directions ahead.
398                    let (nb_pos, nb_magnitude) = [FRAC_PI_4, 0.0, -FRAC_PI_4]
399                        .iter()
400                        .map(|bearing| {
401                            neighbour_pos_delta(edge.angle() + FRAC_PI_2 + side + bearing)
402                        })
403                        // Filter out hypothetical neighbours that are outside image bounds.
404                        .filter_map(|(nb_dx, nb_dy)| {
405                            let nb_x = current_pos.0 as i32 + nb_dx;
406                            let nb_y = current_pos.1 as i32 + nb_dy;
407                            if 0 <= nb_x && nb_x < width as i32 && 0 <= nb_y && nb_y < height as i32
408                            {
409                                let nb = (nb_x as usize, nb_y as usize);
410                                Some((nb, edges[nb.0][nb.1].magnitude))
411                            } else {
412                                None
413                            }
414                        })
415                        // Select the neighbouring edge with the highest magnitude as the next
416                        // line-segment.
417                        .fold(((0, 0), 0.0), |(max_pos, max_mag), (pos, mag)| {
418                            if mag > max_mag {
419                                (pos, mag)
420                            } else {
421                                (max_pos, max_mag)
422                            }
423                        });
424                    if nb_magnitude < weak_threshold
425                        || edges_out[nb_pos.0][nb_pos.1].magnitude > weak_threshold
426                    {
427                        break;
428                    }
429                    current_pos = nb_pos;
430                }
431            }
432        }
433    }
434    edges_out
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    fn edges_to_image(edges: &Vec<Vec<Edge>>) -> image::GrayImage {
442        let (width, height) = (edges.len(), edges.first().unwrap().len());
443        let mut image = image::GrayImage::from_pixel(width as u32, height as u32, image::Luma([0]));
444        for x in 0..width {
445            for y in 0..height {
446                let edge = edges[x][y];
447                *image.get_pixel_mut(x as u32, y as u32) =
448                    image::Luma([(edge.magnitude * 255.0).round() as u8]);
449            }
450        }
451        image
452    }
453
454    fn canny_output_stages<T: AsRef<str>>(
455        path: T,
456        sigma: f32,
457        strong_threshold: f32,
458        weak_threshold: f32,
459    ) -> Detection {
460        let path = path.as_ref();
461        let image = image::open(path).unwrap();
462        let edges = detect_edges(&image.to_luma8(), sigma);
463        let intermediage_d = Detection { edges };
464        intermediage_d
465            .as_image()
466            .save(format!("{}.0-vectors.png", path))
467            .unwrap();
468        edges_to_image(&intermediage_d.edges)
469            .save(format!("{}.1-edges.png", path))
470            .unwrap();
471        let edges = minmax_suppression(&intermediage_d, weak_threshold);
472        edges_to_image(&edges)
473            .save(format!("{}.2-minmax.png", path))
474            .unwrap();
475        let edges = hysteresis(&edges, strong_threshold, weak_threshold);
476        edges_to_image(&edges)
477            .save(format!("{}.3-hysteresis.png", path))
478            .unwrap();
479        let detection = Detection { edges };
480        detection
481            .as_image()
482            .save(format!("{}.4-result.png", path))
483            .unwrap();
484        detection
485    }
486
487    #[test]
488    fn neighbour_pos_delta_from_theta() {
489        let neighbours = [
490            (1, 0),
491            (1, 1),
492            (0, 1),
493            (-1, 1),
494            (-1, 0),
495            (-1, -1),
496            (0, -1),
497            (1, -1),
498        ];
499        for nb in neighbours.iter() {
500            let d = neighbour_pos_delta(f32::atan2(nb.1 as f32, nb.0 as f32));
501            assert_eq!(*nb, d);
502        }
503    }
504
505    #[test]
506    fn edge_new() {
507        let e = Edge::new(1.0, 0.0);
508        assert!(1.0 - 1e-6 < e.vec_x && e.vec_x < 1.0 + 1e-6);
509        assert!(-1e-5 < e.vec_y && e.vec_y < 1e-6);
510
511        let e = Edge::new(1.0, 1.0);
512        assert!(FRAC_1_SQRT_2 - 1e-5 < e.vec_x && e.vec_x < FRAC_1_SQRT_2 + 1e-6);
513        assert!(FRAC_1_SQRT_2 - 1e-5 < e.vec_y && e.vec_y < FRAC_1_SQRT_2 + 1e-6);
514        assert!(1.0 - 1e-6 < e.magnitude && e.magnitude < 1.0 + 1e-6);
515    }
516
517    #[test]
518    fn detection_interpolate() {
519        let dummy = |magnitude| Edge {
520            magnitude,
521            vec_x: 0.0,
522            vec_y: 0.0,
523        };
524        let d = Detection {
525            edges: vec![vec![dummy(2.0), dummy(8.0)], vec![dummy(4.0), dummy(16.0)]],
526        };
527        assert!((d.interpolate(0.0, 0.0).magnitude() - 2.0).abs() <= 1e-6);
528        assert!((d.interpolate(1.0, 0.0).magnitude() - 4.0).abs() <= 1e-6);
529        assert!((d.interpolate(0.0, 1.0).magnitude() - 8.0).abs() <= 1e-6);
530        assert!((d.interpolate(1.0, 1.0).magnitude() - 16.0).abs() <= 1e-6);
531        assert!((d.interpolate(0.5, 0.0).magnitude() - 3.0).abs() <= 1e-6);
532        assert!((d.interpolate(0.0, 0.5).magnitude() - 5.0).abs() <= 1e-6);
533        assert!((d.interpolate(0.5, 1.0).magnitude() - 12.0).abs() <= 1e-6);
534        assert!((d.interpolate(1.0, 0.5).magnitude() - 10.0).abs() <= 1e-6);
535    }
536
537    #[test]
538    fn kernel_integral_in_bounds() {
539        // The integral for the filter kernel should approximate 0.
540        for sigma_i in 1..200 {
541            let sigma = sigma_i as f32 / 10.0;
542            let (ksize, kernel) = filter_kernel(sigma);
543            assert!(ksize.pow(2) == kernel.len());
544            let mut sum_x = 0.0;
545            let mut sum_y = 0.0;
546            for (gx, gy) in kernel {
547                sum_x += gx;
548                sum_y += gy;
549            }
550            println!(
551                "sum = ({}, {}), sigma = {}, kernel_size = {}",
552                sum_x, sum_y, sigma, ksize
553            );
554            assert!(-0.0001 < sum_x && sum_x <= 0.0001);
555            assert!(-0.0001 < sum_y && sum_y <= 0.0001);
556        }
557    }
558
559    /// Tests whether a vertical line of 1px wide exists in the middle of the image.
560    ///
561    /// Returns the location of the line on the X-axis.
562    fn detect_vertical_line(detection: &Detection) -> usize {
563        // Find the line.
564        let mut line_x = None;
565        for x in 0..detection.width() {
566            if detection.edges[x][detection.height() / 2].magnitude > 0.5 {
567                if line_x.is_some() {
568                    panic!("the line is thicker than 1px");
569                }
570                line_x = Some(x)
571            }
572        }
573        let line_x = line_x.expect("no line detected");
574        // The line should be at about the middle of the image.
575        let middle = detection.width() / 2;
576        assert!(middle - 1 <= line_x && line_x <= middle);
577        // The line should be continuous.
578        for y in 0..detection.height() {
579            let edge = detection.edges[line_x][y];
580            assert!(edge.magnitude > 0.0);
581        }
582        // The line should be the only thing detected.
583        for x in 0..detection.width() {
584            if x == line_x {
585                continue;
586            }
587            for y in 0..detection.height() {
588                assert!(detection.edges[x][y].magnitude == 0.0);
589            }
590        }
591        line_x
592    }
593
594    #[test]
595    fn detect_vertical_line_simple() {
596        let d = canny_output_stages("testdata/line-simple.png", 1.2, 0.4, 0.05);
597        let x = detect_vertical_line(&d);
598        // The direction of the line's surface normal should follow the X-axis.
599        for y in 0..d.height() {
600            assert!(d.edges[x][y].angle().abs() < 1e-5);
601        }
602    }
603
604    #[test]
605    fn detect_vertical_line_fuzzy() {
606        let d = canny_output_stages("testdata/line-fuzzy.png", 2.0, 0.4, 0.05);
607        let x = detect_vertical_line(&d);
608        // The direction of the line's surface normal should follow the X-axis.
609        for y in 0..d.height() {
610            assert!(d.edges[x][y].angle().abs() < 0.01);
611        }
612    }
613
614    #[test]
615    fn detect_vertical_line_weakening() {
616        let d = canny_output_stages("testdata/line-weakening.png", 1.2, 0.7, 0.05);
617        detect_vertical_line(&d);
618        // The line vectors are not tested because they are distorted by the gradient.
619    }
620}
621
622#[cfg(all(test, feature = "unstable"))]
623mod benchmarks {
624    extern crate test;
625    use super::*;
626
627    static IMG_PATH: &str = "testdata/circle.png";
628
629    #[bench]
630    fn bench_filter_kernel_low_sigma(b: &mut test::Bencher) {
631        b.iter(|| filter_kernel(1.2));
632    }
633
634    #[bench]
635    fn bench_filter_kernel_high_sigma(b: &mut test::Bencher) {
636        b.iter(|| filter_kernel(5.0));
637    }
638
639    #[bench]
640    fn bench_detect_edges_low_sigma(b: &mut test::Bencher) {
641        let image = image::open(IMG_PATH).unwrap().to_luma8();
642        b.iter(|| detect_edges(&image, 1.2));
643    }
644
645    #[bench]
646    fn bench_detect_edges_high_sigma(b: &mut test::Bencher) {
647        let image = image::open(IMG_PATH).unwrap().to_luma8();
648        b.iter(|| detect_edges(&image, 5.0));
649    }
650
651    #[bench]
652    fn bench_minmax_suppression_low_sigma(b: &mut test::Bencher) {
653        let image = image::open(IMG_PATH).unwrap().to_luma8();
654        let edges = Detection {
655            edges: detect_edges(&image, 1.2),
656        };
657        b.iter(|| minmax_suppression(&edges, 0.01));
658    }
659
660    #[bench]
661    fn bench_minmax_suppression_high_sigma(b: &mut test::Bencher) {
662        let image = image::open(IMG_PATH).unwrap().to_luma8();
663        let edges = Detection {
664            edges: detect_edges(&image, 5.0),
665        };
666        b.iter(|| minmax_suppression(&edges, 0.01));
667    }
668
669    #[bench]
670    fn bench_hysteresis(b: &mut test::Bencher) {
671        let image = image::open(IMG_PATH).unwrap().to_luma8();
672        let edges = Detection {
673            edges: detect_edges(&image, 1.2),
674        };
675        let edges = minmax_suppression(&edges, 0.1);
676        b.iter(|| hysteresis(&edges, 0.4, 0.1));
677    }
678}