1use std::collections::VecDeque;
4
5use rayon::prelude::*;
6use spatialrust_image::{Image, ImageView, ImageViewMut};
7
8use crate::dispatch::{
9 bounded_workers, items_per_worker, should_parallelize, LARGE_PARALLEL_COMPONENTS,
10};
11use crate::{
12 sobel, spatial_gradient_u8, spatial_gradient_u8_into, BorderMode, VisionError, VisionResult,
13};
14
15#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct CannyOptions {
18 pub low_threshold: f64,
20 pub high_threshold: f64,
22 pub aperture_size: usize,
24 pub l2_gradient: bool,
26}
27
28impl Default for CannyOptions {
29 fn default() -> Self {
30 Self { low_threshold: 100.0, high_threshold: 200.0, aperture_size: 3, l2_gradient: false }
31 }
32}
33
34impl CannyOptions {
35 fn validate(self) -> VisionResult<Self> {
36 if !self.low_threshold.is_finite()
37 || !self.high_threshold.is_finite()
38 || self.low_threshold < 0.0
39 || self.high_threshold < 0.0
40 {
41 return Err(VisionError::InvalidParameter(
42 "Canny thresholds must be finite and non-negative".into(),
43 ));
44 }
45 if !matches!(self.aperture_size, 3 | 5 | 7) {
46 return Err(VisionError::InvalidParameter(
47 "Canny aperture size must be 3, 5, or 7".into(),
48 ));
49 }
50 Ok(self)
51 }
52}
53
54#[derive(Clone, Debug, PartialEq)]
56pub struct CannyResult {
57 pub edges: Image<u8, 1>,
59 pub gradient_x: Image<f32, 1>,
61 pub gradient_y: Image<f32, 1>,
63 pub magnitude: Image<f32, 1>,
65 pub suppressed: Image<f32, 1>,
67}
68
69#[derive(Clone, Debug, Default)]
74pub struct CannyWorkspace {
75 gradient_x: Vec<i16>,
76 gradient_y: Vec<i16>,
77 magnitude_rows: Vec<i32>,
78 states: Vec<u8>,
79 frontier: Vec<usize>,
80}
81
82impl CannyWorkspace {
83 #[must_use]
85 pub const fn new() -> Self {
86 Self {
87 gradient_x: Vec::new(),
88 gradient_y: Vec::new(),
89 magnitude_rows: Vec::new(),
90 states: Vec::new(),
91 frontier: Vec::new(),
92 }
93 }
94
95 #[must_use]
97 pub fn capacity(&self) -> usize {
98 self.gradient_x.capacity().min(self.gradient_y.capacity()).min(self.states.capacity())
99 }
100
101 #[must_use]
103 pub fn allocated_bytes(&self) -> usize {
104 self.gradient_x.capacity() * std::mem::size_of::<i16>()
105 + self.gradient_y.capacity() * std::mem::size_of::<i16>()
106 + self.magnitude_rows.capacity() * std::mem::size_of::<i32>()
107 + self.states.capacity() * std::mem::size_of::<u8>()
108 + self.frontier.capacity() * std::mem::size_of::<usize>()
109 }
110
111 fn resize(&mut self, len: usize, magnitude_elements: usize) {
112 self.gradient_x.resize(len, 0);
113 self.gradient_y.resize(len, 0);
114 self.magnitude_rows.resize(magnitude_elements, 0);
115 self.states.resize(len, 1);
116 self.frontier.clear();
117 }
118}
119
120pub fn canny(input: ImageView<'_, u8, 1>, options: CannyOptions) -> VisionResult<Image<u8, 1>> {
122 let len = checked_len(input.width(), input.height())?;
123 let mut output = Image::try_new_with_metadata(
124 input.width(),
125 input.height(),
126 vec![0; len],
127 input.metadata(),
128 )?;
129 let mut workspace = CannyWorkspace::new();
130 canny_into(input, options, output.view_mut(), &mut workspace)?;
131 Ok(output)
132}
133
134pub fn canny_into(
141 input: ImageView<'_, u8, 1>,
142 options: CannyOptions,
143 mut output: ImageViewMut<'_, u8, 1>,
144 workspace: &mut CannyWorkspace,
145) -> VisionResult<()> {
146 let options = options.validate()?;
147 if output.width() != input.width() || output.height() != input.height() {
148 return Err(VisionError::ShapeMismatch(format!(
149 "Canny output must be {}x{}, found {}x{}",
150 input.width(),
151 input.height(),
152 output.width(),
153 output.height()
154 )));
155 }
156 if options.aperture_size != 3 {
157 let result = canny_with_intermediates(input, options)?;
158 for y in 0..input.height() {
159 let start = y * input.width();
160 output
161 .row_mut(y)
162 .expect("validated Canny output row")
163 .copy_from_slice(&result.edges.as_slice()[start..start + input.width()]);
164 }
165 return Ok(());
166 }
167
168 canny_3x3_into(input, options, &mut output, workspace)
169}
170
171fn checked_len(width: usize, height: usize) -> VisionResult<usize> {
172 width
173 .checked_mul(height)
174 .ok_or_else(|| VisionError::InvalidDimensions("Canny image dimensions overflow".into()))
175}
176
177fn canny_3x3_into(
178 input: ImageView<'_, u8, 1>,
179 options: CannyOptions,
180 output: &mut ImageViewMut<'_, u8, 1>,
181 workspace: &mut CannyWorkspace,
182) -> VisionResult<()> {
183 let width = input.width();
184 let height = input.height();
185 let len = checked_len(width, height)?;
186 let workers =
187 bounded_workers(len, height, LARGE_PARALLEL_COMPONENTS, rayon::current_num_threads());
188 let parallel = workers > 1;
189 let magnitude_elements = workers
190 .checked_mul(3)
191 .and_then(|rows| rows.checked_mul(width))
192 .ok_or_else(|| VisionError::InvalidDimensions("Canny magnitude ring overflows".into()))?;
193 workspace.resize(len, magnitude_elements);
194 spatial_gradient_u8_into(
195 input,
196 BorderMode::Replicate,
197 &mut workspace.gradient_x,
198 &mut workspace.gradient_y,
199 )?;
200
201 let (low, high) = canny_thresholds(options);
202 workspace.states.fill(1);
203 if parallel {
204 let rows_per_worker = items_per_worker(height, workers);
205 let gradient_x = &workspace.gradient_x;
206 let gradient_y = &workspace.gradient_y;
207 let (mut candidates, has_weak) = workspace
208 .states
209 .par_chunks_mut(rows_per_worker * width)
210 .zip(workspace.magnitude_rows.par_chunks_mut(3 * width))
211 .enumerate()
212 .map(|(chunk, (states, magnitude_rows))| {
213 let mut local_candidates = Vec::new();
214 let has_weak = classify_canny_rows(
215 chunk * rows_per_worker,
216 width,
217 height,
218 gradient_x,
219 gradient_y,
220 options.l2_gradient,
221 low,
222 high,
223 states,
224 magnitude_rows,
225 &mut local_candidates,
226 );
227 (local_candidates, has_weak)
228 })
229 .reduce(
230 || (Vec::new(), false),
231 |(mut left, left_weak), (mut right, right_weak)| {
232 left.append(&mut right);
233 (left, left_weak || right_weak)
234 },
235 );
236 workspace.frontier.clear();
237 workspace.frontier.append(&mut candidates);
238 if !has_weak {
239 workspace.frontier.clear();
240 }
241 } else {
242 workspace.frontier.clear();
243 let has_weak = classify_canny_rows(
244 0,
245 width,
246 height,
247 &workspace.gradient_x,
248 &workspace.gradient_y,
249 options.l2_gradient,
250 low,
251 high,
252 &mut workspace.states,
253 &mut workspace.magnitude_rows,
254 &mut workspace.frontier,
255 );
256 if !has_weak {
257 workspace.frontier.clear();
258 }
259 }
260
261 let mut seed_count = 0;
262 for candidate in 0..workspace.frontier.len() {
263 let index = workspace.frontier[candidate];
264 if has_strong_neighbor(&workspace.states, width, height, index) {
265 workspace.states[index] = 2;
266 workspace.frontier[seed_count] = index;
267 seed_count += 1;
268 }
269 }
270 workspace.frontier.truncate(seed_count);
271
272 while let Some(index) = workspace.frontier.pop() {
273 let x = index % width;
274 let y = index / width;
275 let x0 = x.saturating_sub(1);
276 let x1 = (x + 1).min(width.saturating_sub(1));
277 let y0 = y.saturating_sub(1);
278 let y1 = (y + 1).min(height.saturating_sub(1));
279 for ny in y0..=y1 {
280 for nx in x0..=x1 {
281 let neighbor = ny * width + nx;
282 if workspace.states[neighbor] == 0 {
283 workspace.states[neighbor] = 2;
284 workspace.frontier.push(neighbor);
285 }
286 }
287 }
288 }
289
290 if should_parallelize(len, height, LARGE_PARALLEL_COMPONENTS) && output.row_stride() == width {
291 output
292 .as_mut_slice()
293 .par_iter_mut()
294 .zip(&workspace.states)
295 .for_each(|(pixel, &state)| *pixel = if state == 2 { 255 } else { 0 });
296 } else {
297 for y in 0..height {
298 let start = y * width;
299 let target = output.row_mut(y).expect("validated Canny output row");
300 for (pixel, &state) in target.iter_mut().zip(&workspace.states[start..start + width]) {
301 *pixel = if state == 2 { 255 } else { 0 };
302 }
303 }
304 }
305 Ok(())
306}
307
308#[allow(clippy::too_many_arguments)]
309fn classify_canny_rows(
310 start_y: usize,
311 width: usize,
312 height: usize,
313 gradient_x: &[i16],
314 gradient_y: &[i16],
315 l2_gradient: bool,
316 low: i64,
317 high: i64,
318 states: &mut [u8],
319 magnitude_rows: &mut [i32],
320 candidates: &mut Vec<usize>,
321) -> bool {
322 if width == 0 || height == 0 || states.is_empty() {
323 return false;
324 }
325 debug_assert_eq!(magnitude_rows.len(), 3 * width);
326 let rows = states.len() / width;
327 fill_magnitude_row(
328 start_y.checked_sub(1),
329 width,
330 height,
331 gradient_x,
332 gradient_y,
333 l2_gradient,
334 &mut magnitude_rows[..width],
335 );
336 fill_magnitude_row(
337 Some(start_y),
338 width,
339 height,
340 gradient_x,
341 gradient_y,
342 l2_gradient,
343 &mut magnitude_rows[width..2 * width],
344 );
345 fill_magnitude_row(
346 start_y.checked_add(1),
347 width,
348 height,
349 gradient_x,
350 gradient_y,
351 l2_gradient,
352 &mut magnitude_rows[2 * width..],
353 );
354
355 let mut has_weak = false;
356 for local_y in 0..rows {
357 let y = start_y + local_y;
358 let previous_slot = local_y % 3;
359 let current_slot = (local_y + 1) % 3;
360 let next_slot = (local_y + 2) % 3;
361 if local_y != 0 {
362 let next_y = y.checked_add(1);
363 fill_magnitude_row(
364 next_y,
365 width,
366 height,
367 gradient_x,
368 gradient_y,
369 l2_gradient,
370 &mut magnitude_rows[next_slot * width..(next_slot + 1) * width],
371 );
372 }
373 let global_row = y * width;
374 let local_row = local_y * width;
375 for x in 0..width {
376 let index = global_row + x;
377 let magnitude = magnitude_rows[current_slot * width + x];
378 if i64::from(magnitude) <= low
379 || !is_directional_maximum_ring(
380 x,
381 y,
382 width,
383 height,
384 i32::from(gradient_x[index]),
385 i32::from(gradient_y[index]),
386 magnitude,
387 magnitude_rows,
388 previous_slot,
389 current_slot,
390 next_slot,
391 )
392 {
393 continue;
394 }
395 let state = &mut states[local_row + x];
396 if i64::from(magnitude) > high {
397 *state = 2;
398 } else {
399 *state = 0;
400 candidates.push(index);
401 has_weak = true;
402 }
403 }
404 }
405 has_weak
406}
407
408fn has_strong_neighbor(states: &[u8], width: usize, height: usize, index: usize) -> bool {
409 let x = index % width;
410 let y = index / width;
411 let x0 = x.saturating_sub(1);
412 let x1 = (x + 1).min(width.saturating_sub(1));
413 let y0 = y.saturating_sub(1);
414 let y1 = (y + 1).min(height.saturating_sub(1));
415 (y0..=y1).any(|ny| {
416 (x0..=x1).any(|nx| {
417 let neighbor = ny * width + nx;
418 neighbor != index && states[neighbor] == 2
419 })
420 })
421}
422
423#[allow(clippy::too_many_arguments)]
424fn fill_magnitude_row(
425 y: Option<usize>,
426 width: usize,
427 height: usize,
428 gradient_x: &[i16],
429 gradient_y: &[i16],
430 l2_gradient: bool,
431 output: &mut [i32],
432) {
433 let Some(y) = y.filter(|&y| y < height) else {
434 output.fill(0);
435 return;
436 };
437 let start = y * width;
438 let gradient_x = &gradient_x[start..start + width];
439 let gradient_y = &gradient_y[start..start + width];
440 if l2_gradient {
441 for ((magnitude, &x), &y) in output.iter_mut().zip(gradient_x).zip(gradient_y) {
442 let x = i32::from(x);
443 let y = i32::from(y);
444 *magnitude = x * x + y * y;
445 }
446 } else {
447 for ((magnitude, &x), &y) in output.iter_mut().zip(gradient_x).zip(gradient_y) {
448 *magnitude = i32::from(x).abs() + i32::from(y).abs();
449 }
450 }
451}
452
453#[inline]
454#[allow(clippy::too_many_arguments)]
455fn is_directional_maximum_ring(
456 x: usize,
457 y: usize,
458 width: usize,
459 height: usize,
460 gradient_x: i32,
461 gradient_y: i32,
462 magnitude: i32,
463 magnitudes: &[i32],
464 previous_slot: usize,
465 current_slot: usize,
466 next_slot: usize,
467) -> bool {
468 const TG22: i64 = 13_573;
469 let abs_x = i64::from(gradient_x.abs());
470 let abs_y_scaled = i64::from(gradient_y.abs()) << 15;
471 let tg22_x = abs_x * TG22;
472 let get = |offset_x: isize, offset_y: isize| {
473 let nx = x as isize + offset_x;
474 let ny = y as isize + offset_y;
475 if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
476 0
477 } else {
478 let slot = match offset_y {
479 -1 => previous_slot,
480 0 => current_slot,
481 1 => next_slot,
482 _ => unreachable!("Canny NMS only reads adjacent rows"),
483 };
484 magnitudes[slot * width + nx as usize]
485 }
486 };
487 if abs_y_scaled < tg22_x {
488 magnitude > get(-1, 0) && magnitude >= get(1, 0)
489 } else if abs_y_scaled > tg22_x + (abs_x << 16) {
490 magnitude > get(0, -1) && magnitude >= get(0, 1)
491 } else {
492 let sign = if (gradient_x ^ gradient_y) < 0 { -1 } else { 1 };
493 magnitude > get(-sign, -1) && magnitude > get(sign, 1)
494 }
495}
496
497fn canny_thresholds(options: CannyOptions) -> (i64, i64) {
498 let (mut low, mut high) = (options.low_threshold, options.high_threshold);
499 if options.aperture_size == 7 {
500 low /= 16.0;
501 high /= 16.0;
502 }
503 if low > high {
504 std::mem::swap(&mut low, &mut high);
505 }
506 if options.l2_gradient {
507 (
508 low.min(32767.0).mul_add(low.min(32767.0), 0.0).floor() as i64,
509 high.min(32767.0).mul_add(high.min(32767.0), 0.0).floor() as i64,
510 )
511 } else {
512 (low.floor() as i64, high.floor() as i64)
513 }
514}
515
516pub fn canny_with_intermediates(
518 input: ImageView<'_, u8, 1>,
519 options: CannyOptions,
520) -> VisionResult<CannyResult> {
521 let options = options.validate()?;
522 let width = input.width();
523 let height = input.height();
524 let len = width
525 .checked_mul(height)
526 .ok_or_else(|| VisionError::InvalidDimensions("Canny image dimensions overflow".into()))?;
527 let (gradient_x, gradient_y, gx, gy) = if options.aperture_size == 3 {
528 let (gradient_x_i16, gradient_y_i16) = spatial_gradient_u8(input, BorderMode::Replicate)?;
529 let gx = gradient_x_i16.as_slice().iter().map(|&value| i32::from(value)).collect();
530 let gy = gradient_y_i16.as_slice().iter().map(|&value| i32::from(value)).collect();
531 let gradient_x = gradient_x_i16.as_slice().iter().map(|&value| f32::from(value)).collect();
532 let gradient_y = gradient_y_i16.as_slice().iter().map(|&value| f32::from(value)).collect();
533 (
534 Image::try_new_with_metadata(width, height, gradient_x, input.metadata())?,
535 Image::try_new_with_metadata(width, height, gradient_y, input.metadata())?,
536 gx,
537 gy,
538 )
539 } else {
540 let scale = if options.aperture_size == 7 { 1.0 / 16.0 } else { 1.0 };
541 let gradient_x =
542 sobel(input, 1, 0, options.aperture_size, scale, 0.0, BorderMode::Replicate)?;
543 let gradient_y =
544 sobel(input, 0, 1, options.aperture_size, scale, 0.0, BorderMode::Replicate)?;
545 let gx = gradient_x
546 .as_slice()
547 .iter()
548 .map(|&value| round_i16_ties_even(value))
551 .collect::<Vec<_>>();
552 let gy = gradient_y
553 .as_slice()
554 .iter()
555 .map(|&value| round_i16_ties_even(value))
556 .collect::<Vec<_>>();
557 (gradient_x, gradient_y, gx, gy)
558 };
559 let comparison_magnitude = gx
560 .iter()
561 .zip(&gy)
562 .map(|(&x, &y)| {
563 if options.l2_gradient {
564 i64::from(x) * i64::from(x) + i64::from(y) * i64::from(y)
565 } else {
566 i64::from(x.abs() + y.abs())
567 }
568 })
569 .collect::<Vec<_>>();
570 let magnitude_values = comparison_magnitude
571 .iter()
572 .map(|&value| if options.l2_gradient { (value as f64).sqrt() as f32 } else { value as f32 })
573 .collect::<Vec<_>>();
574
575 let (mut low, mut high) = (options.low_threshold, options.high_threshold);
576 if options.aperture_size == 7 {
577 low /= 16.0;
578 high /= 16.0;
579 }
580 if low > high {
581 std::mem::swap(&mut low, &mut high);
582 }
583 let (low, high) = if options.l2_gradient {
584 (
585 low.min(32767.0).mul_add(low.min(32767.0), 0.0).floor() as i64,
586 high.min(32767.0).mul_add(high.min(32767.0), 0.0).floor() as i64,
587 )
588 } else {
589 (low.floor() as i64, high.floor() as i64)
590 };
591
592 let mut states = vec![1_u8; len];
593 let mut suppressed_values = vec![0.0_f32; len];
594 let mut strong = VecDeque::new();
595 for y in 0..height {
596 for x in 0..width {
597 let index = y * width + x;
598 let magnitude = comparison_magnitude[index];
599 if magnitude <= low
600 || !is_directional_maximum(
601 x,
602 y,
603 width,
604 height,
605 gx[index],
606 gy[index],
607 magnitude,
608 &comparison_magnitude,
609 )
610 {
611 continue;
612 }
613 suppressed_values[index] = magnitude_values[index];
614 if magnitude > high {
615 states[index] = 2;
616 strong.push_back(index);
617 } else {
618 states[index] = 0;
619 }
620 }
621 }
622
623 while let Some(index) = strong.pop_front() {
624 let x = index % width;
625 let y = index / width;
626 for dy in -1_isize..=1 {
627 for dx in -1_isize..=1 {
628 if dx == 0 && dy == 0 {
629 continue;
630 }
631 let nx = x as isize + dx;
632 let ny = y as isize + dy;
633 if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
634 continue;
635 }
636 let neighbor = ny as usize * width + nx as usize;
637 if states[neighbor] == 0 {
638 states[neighbor] = 2;
639 strong.push_back(neighbor);
640 }
641 }
642 }
643 }
644 let edges = states.into_iter().map(|state| if state == 2 { 255 } else { 0 }).collect();
645 let metadata = input.metadata();
646 Ok(CannyResult {
647 edges: Image::try_new_with_metadata(width, height, edges, metadata)?,
648 gradient_x,
649 gradient_y,
650 magnitude: Image::try_new_with_metadata(width, height, magnitude_values, metadata)?,
651 suppressed: Image::try_new_with_metadata(width, height, suppressed_values, metadata)?,
652 })
653}
654
655fn round_i16_ties_even(value: f32) -> i32 {
656 let value = value.clamp(f32::from(i16::MIN), f32::from(i16::MAX));
657 let lower = value.floor();
658 let fraction = value - lower;
659 if fraction < 0.5 {
660 lower as i32
661 } else if fraction > 0.5 {
662 lower as i32 + 1
663 } else {
664 let lower = lower as i32;
665 if lower & 1 == 0 {
666 lower
667 } else {
668 lower + 1
669 }
670 }
671}
672
673fn is_directional_maximum(
674 x: usize,
675 y: usize,
676 width: usize,
677 height: usize,
678 gradient_x: i32,
679 gradient_y: i32,
680 magnitude: i64,
681 magnitudes: &[i64],
682) -> bool {
683 const TG22: i64 = 13_573;
684 let abs_x = i64::from(gradient_x.abs());
685 let abs_y_scaled = i64::from(gradient_y.abs()) << 15;
686 let tg22_x = abs_x * TG22;
687 let get = |offset_x: isize, offset_y: isize| {
688 let nx = x as isize + offset_x;
689 let ny = y as isize + offset_y;
690 if nx < 0 || ny < 0 || nx >= width as isize || ny >= height as isize {
691 0
692 } else {
693 magnitudes[ny as usize * width + nx as usize]
694 }
695 };
696 if abs_y_scaled < tg22_x {
697 magnitude > get(-1, 0) && magnitude >= get(1, 0)
698 } else if abs_y_scaled > tg22_x + (abs_x << 16) {
699 magnitude > get(0, -1) && magnitude >= get(0, 1)
700 } else {
701 let sign = if (gradient_x ^ gradient_y) < 0 { -1 } else { 1 };
702 magnitude > get(-sign, -1) && magnitude > get(sign, 1)
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::{canny, canny_into, canny_with_intermediates, CannyOptions, CannyWorkspace};
709 use spatialrust_image::{Image, ImageRegion, ImageViewMut};
710
711 #[test]
712 fn finds_both_sides_of_bright_bar_on_strided_roi() {
713 let mut data = vec![0_u8; 9 * 7];
714 for y in 1..6 {
715 for x in 3..6 {
716 data[y * 9 + x] = 255;
717 }
718 }
719 let image = Image::<u8, 1>::try_new(9, 7, data).unwrap();
720 let roi = image.view().subview(ImageRegion::new(1, 1, 7, 5)).unwrap();
721 let edges = canny(
722 roi,
723 CannyOptions { low_threshold: 50.0, high_threshold: 100.0, ..Default::default() },
724 )
725 .unwrap();
726 assert!(edges.as_slice().iter().filter(|&&value| value == 255).count() >= 6);
727 assert!(edges.as_slice().iter().all(|&value| value == 0 || value == 255));
728 }
729
730 #[test]
731 fn intermediates_preserve_shape_and_signed_gradients() {
732 let image =
733 Image::<u8, 1>::try_new(5, 3, (0..3).flat_map(|_| [0, 10, 20, 30, 40]).collect())
734 .unwrap();
735 let result = canny_with_intermediates(image.view(), CannyOptions::default()).unwrap();
736 assert_eq!((result.edges.width(), result.edges.height()), (5, 3));
737 assert!(result.gradient_x.as_slice().iter().any(|&value| value > 0.0));
738 assert!(result.gradient_y.as_slice().iter().all(|&value| value == 0.0));
739 }
740
741 #[test]
742 fn flat_and_empty_images_have_no_edges() {
743 for image in [
744 Image::<u8, 1>::from_pixel(4, 3, [42]).unwrap(),
745 Image::<u8, 1>::try_new(0, 0, Vec::new()).unwrap(),
746 ] {
747 assert!(canny(image.view(), CannyOptions::default())
748 .unwrap()
749 .as_slice()
750 .iter()
751 .all(|&value| value == 0));
752 }
753 }
754
755 #[test]
756 fn validates_thresholds_and_aperture() {
757 let image = Image::<u8, 1>::from_pixel(1, 1, [0]).unwrap();
758 assert!(
759 canny(image.view(), CannyOptions { aperture_size: 4, ..Default::default() }).is_err()
760 );
761 assert!(canny(image.view(), CannyOptions { low_threshold: -1.0, ..Default::default() })
762 .is_err());
763 }
764
765 #[test]
766 fn fast_path_matches_inspectable_path_for_l1_and_l2() {
767 let data = (0..31 * 19).map(|index| ((index * 37 + index / 31 * 17) & 255) as u8).collect();
768 let image = Image::<u8, 1>::try_new(31, 19, data).unwrap();
769 for l2_gradient in [false, true] {
770 let options = CannyOptions {
771 low_threshold: 80.0,
772 high_threshold: 160.0,
773 l2_gradient,
774 ..Default::default()
775 };
776 assert_eq!(
777 canny(image.view(), options).unwrap(),
778 canny_with_intermediates(image.view(), options).unwrap().edges
779 );
780 }
781 }
782
783 #[test]
784 fn reusable_path_supports_strided_output_without_touching_padding() {
785 let image = Image::<u8, 1>::try_new(
786 7,
787 5,
788 (0..35).map(|index| ((index * 53) & 255) as u8).collect(),
789 )
790 .unwrap();
791 let expected = canny(image.view(), CannyOptions::default()).unwrap();
792 let mut storage = vec![17_u8; 5 * 11];
793 let output = ImageViewMut::<u8, 1>::new(7, 5, 11, &mut storage).unwrap();
794 let mut workspace = CannyWorkspace::new();
795 canny_into(image.view(), CannyOptions::default(), output, &mut workspace).unwrap();
796 for y in 0..5 {
797 assert_eq!(&storage[y * 11..y * 11 + 7], &expected.as_slice()[y * 7..y * 7 + 7]);
798 if y < 4 {
799 assert_eq!(&storage[y * 11 + 7..(y + 1) * 11], &[17; 4]);
800 }
801 }
802 }
803
804 #[test]
805 fn parallel_ring_matches_intermediates_and_avoids_full_magnitude_image() {
806 let (width, height) = (1_000, 1_000);
807 let data = (0..width * height)
808 .map(|index| if (index / width) % 80 < 3 { 255 } else { 0 })
809 .collect();
810 let image = Image::<u8, 1>::try_new(width, height, data).unwrap();
811 let options = CannyOptions {
812 low_threshold: 80.0,
813 high_threshold: 160.0,
814 l2_gradient: true,
815 ..Default::default()
816 };
817 let expected = canny_with_intermediates(image.view(), options).unwrap().edges;
818 let mut actual = Image::<u8, 1>::from_pixel(width, height, [0]).unwrap();
819 let mut workspace = CannyWorkspace::new();
820 canny_into(image.view(), options, actual.view_mut(), &mut workspace).unwrap();
821 assert_eq!(actual, expected);
822 assert!(workspace.allocated_bytes() < width * height * 6);
823 }
824}