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#[derive(Clone)]
22pub struct Detection {
23 edges: Vec<Vec<Edge>>,
24}
25
26impl Detection {
27 pub fn width(&self) -> usize {
29 self.edges.len()
30 }
31
32 pub fn height(&self) -> usize {
34 self.edges[0].len()
35 }
36
37 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 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 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#[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 pub fn angle(&self) -> f32 {
153 f32::atan2(self.vec_y, self.vec_x)
154 }
155
156 pub fn dir(&self) -> (f32, f32) {
158 (self.vec_x * self.magnitude(), self.vec_y * self.magnitude())
159 }
160
161 pub fn dir_norm(&self) -> (f32, f32) {
167 (self.vec_x, self.vec_y)
168 }
169
170 pub fn magnitude(&self) -> f32 {
174 self.magnitude
175 }
176}
177
178pub 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
212fn 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), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), ];
239 let n = ((theta + TAU) % TAU) / TAU;
240 let i = (n * 8.0).round() as usize % 8;
241 neighbours[i]
242}
243
244fn 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 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
290fn 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 return Edge::new(0.0, 0.0);
303 }
304 let truncate = |f: f32| (f * 1e5).round() * 1e-6;
306
307 let mut select = 0;
314 let mut select_flip_bit = 1;
315
316 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 let end =
335 interpolated_magnitude < trunc_edge_magnitude
337 || *seek_magnitude > trunc_edge_magnitude && interpolated_magnitude < *seek_magnitude;
339 *seek_magnitude = interpolated_magnitude;
340 distances[select] += 1;
341
342 select ^= select_flip_bit;
344 if end {
345 if select_flip_bit == 0 {
346 break;
347 }
348 select_flip_bit = 0;
350 }
351 }
352
353 let is_apex =
357 distances[0] == distances[1]
360 || (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
374fn 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 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 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_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 .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 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 fn detect_vertical_line(detection: &Detection) -> usize {
563 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 let middle = detection.width() / 2;
576 assert!(middle - 1 <= line_x && line_x <= middle);
577 for y in 0..detection.height() {
579 let edge = detection.edges[line_x][y];
580 assert!(edge.magnitude > 0.0);
581 }
582 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 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 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 }
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}