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#[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<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
211fn 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), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), ];
238 let n = ((theta + TAU) % TAU) / TAU;
239 let i = (n * 8.0).round() as usize % 8;
240 neighbours[i]
241}
242
243fn 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 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
289fn 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 return Edge::new(0.0, 0.0);
302 }
303 let truncate = |f: f32| (f * 1e5).round() * 1e-6;
305
306 let mut select = 0;
313 let mut select_flip_bit = 1;
314
315 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 let end =
334 interpolated_magnitude < trunc_edge_magnitude
336 || *seek_magnitude > trunc_edge_magnitude && interpolated_magnitude < *seek_magnitude;
338 *seek_magnitude = interpolated_magnitude;
339 distances[select] += 1;
340
341 select ^= select_flip_bit;
343 if end {
344 if select_flip_bit == 0 {
345 break;
346 }
347 select_flip_bit = 0;
349 }
350 }
351
352 let is_apex =
356 distances[0] == distances[1]
359 || (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
373fn 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 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 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_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 .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 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 fn detect_vertical_line(detection: &Detection) -> usize {
562 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 let middle = detection.width() / 2;
575 assert!(middle - 1 <= line_x && line_x <= middle);
576 for y in 0..detection.height() {
578 let edge = detection.edges[line_x][y];
579 assert!(edge.magnitude > 0.0);
580 }
581 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 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 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 }
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}