1use pulp::Arch;
4use rayon::prelude::*;
5use spatialrust_image::{Image, ImageView};
6
7use crate::dispatch::{
8 bounded_workers, items_per_worker, LARGE_PARALLEL_COMPONENTS, ROW_PARALLEL_COMPONENTS,
9};
10use crate::{
11 filter2d_f32, separable_filter, separable_filter_f32, BorderMode, Kernel1D, Kernel2D,
12 PixelComponent, VisionError, VisionResult,
13};
14
15#[path = "bilateral.rs"]
16mod bilateral;
17#[path = "median.rs"]
18mod median;
19
20pub fn median_blur<T: PixelComponent, const CHANNELS: usize>(
25 input: ImageView<'_, T, CHANNELS>,
26 kernel_size: usize,
27 border: BorderMode<T, CHANNELS>,
28) -> VisionResult<Image<T, CHANNELS>> {
29 median::run(input, kernel_size, border)
30}
31
32pub fn bilateral_filter<T: PixelComponent, const CHANNELS: usize>(
39 input: ImageView<'_, T, CHANNELS>,
40 diameter: usize,
41 sigma_color: f64,
42 sigma_space: f64,
43 border: BorderMode<T, CHANNELS>,
44) -> VisionResult<Image<T, CHANNELS>> {
45 bilateral::run(input, diameter, sigma_color, sigma_space, border)
46}
47pub fn sobel<T: PixelComponent, const CHANNELS: usize>(
49 input: ImageView<'_, T, CHANNELS>,
50 dx: usize,
51 dy: usize,
52 kernel_size: usize,
53 scale: f64,
54 delta: f64,
55 border: BorderMode<T, CHANNELS>,
56) -> VisionResult<Image<f32, CHANNELS>> {
57 validate_derivative(dx, dy, kernel_size, scale, delta)?;
58 let mut kernel_x = derivative_kernel(kernel_size, dx)?;
59 let kernel_y = derivative_kernel(kernel_size, dy)?;
60 kernel_x =
61 Kernel1D::try_new(kernel_x.coefficients().iter().map(|value| value * scale).collect())?;
62 separable_filter_f32(input, &kernel_x, &kernel_y, delta, border)
63}
64
65pub fn sobel_3x3_u8(
72 input: ImageView<'_, u8, 1>,
73 dx: usize,
74 dy: usize,
75 scale: f64,
76 delta: f64,
77 border: BorderMode<u8, 1>,
78) -> VisionResult<Image<f32, 1>> {
79 let len = input
80 .width()
81 .checked_mul(input.height())
82 .ok_or_else(|| VisionError::InvalidDimensions("Sobel output size overflows".into()))?;
83 let mut output = vec![0.0; len];
84 sobel_3x3_u8_into(input, dx, dy, scale, delta, border, &mut output)?;
85 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
86}
87
88pub fn sobel_3x3_u8_into(
90 input: ImageView<'_, u8, 1>,
91 dx: usize,
92 dy: usize,
93 scale: f64,
94 delta: f64,
95 border: BorderMode<u8, 1>,
96 output: &mut [f32],
97) -> VisionResult<()> {
98 validate_derivative(dx, dy, 3, scale, delta)?;
99 if dx + dy != 1 {
100 return Err(VisionError::InvalidParameter(
101 "specialized 3x3 Sobel requires derivative order (1, 0) or (0, 1)".into(),
102 ));
103 }
104 if !matches!(border, BorderMode::Replicate | BorderMode::Reflect101) {
105 return Err(VisionError::InvalidParameter(
106 "specialized 3x3 Sobel supports only Replicate and Reflect101 borders".into(),
107 ));
108 }
109 let len = input
110 .width()
111 .checked_mul(input.height())
112 .ok_or_else(|| VisionError::InvalidDimensions("Sobel output size overflows".into()))?;
113 if output.len() != len {
114 return Err(VisionError::ShapeMismatch(format!(
115 "Sobel output needs {len} elements, found {}",
116 output.len()
117 )));
118 }
119 if len == 0 {
120 return Ok(());
121 }
122
123 if scale == 1.0 && delta == 0.0 {
124 sobel_3x3_identity_into(input, dx, border, output);
125 return Ok(());
126 }
127
128 let width = input.width();
129 let height = input.height();
130 let arch = Arch::new();
131 let workers =
132 bounded_workers(len, height, ROW_PARALLEL_COMPONENTS, rayon::current_num_threads());
133 if workers > 1 {
134 let rows_per_worker = items_per_worker(height, workers);
135 output.par_chunks_mut(rows_per_worker * width).enumerate().for_each(|(chunk, output)| {
136 arch.dispatch(|| {
137 sobel_3x3_rows_dispatch(
138 input,
139 dx,
140 scale,
141 delta,
142 border,
143 chunk * rows_per_worker,
144 output,
145 );
146 });
147 });
148 } else {
149 arch.dispatch(|| {
150 sobel_3x3_rows_dispatch(input, dx, scale, delta, border, 0, output);
151 });
152 }
153 Ok(())
154}
155
156pub fn sobel_abs_3x3_u8(
161 input: ImageView<'_, u8, 1>,
162 dx: usize,
163 dy: usize,
164 border: BorderMode<u8, 1>,
165) -> VisionResult<Image<u8, 1>> {
166 let len = input.width().checked_mul(input.height()).ok_or_else(|| {
167 VisionError::InvalidDimensions("absolute Sobel output size overflows".into())
168 })?;
169 let mut output = vec![0; len];
170 sobel_abs_3x3_u8_into(input, dx, dy, border, &mut output)?;
171 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
172}
173
174pub fn sobel_abs_3x3_u8_into(
176 input: ImageView<'_, u8, 1>,
177 dx: usize,
178 dy: usize,
179 border: BorderMode<u8, 1>,
180 output: &mut [u8],
181) -> VisionResult<()> {
182 validate_derivative(dx, dy, 3, 1.0, 0.0)?;
183 if dx + dy != 1 {
184 return Err(VisionError::InvalidParameter(
185 "absolute 3x3 Sobel requires derivative order (1, 0) or (0, 1)".into(),
186 ));
187 }
188 if !matches!(border, BorderMode::Replicate | BorderMode::Reflect101) {
189 return Err(VisionError::InvalidParameter(
190 "absolute 3x3 Sobel supports only Replicate and Reflect101 borders".into(),
191 ));
192 }
193 let len = input.width().checked_mul(input.height()).ok_or_else(|| {
194 VisionError::InvalidDimensions("absolute Sobel output size overflows".into())
195 })?;
196 if output.len() != len {
197 return Err(VisionError::ShapeMismatch(format!(
198 "absolute Sobel output needs {len} elements, found {}",
199 output.len()
200 )));
201 }
202 if len == 0 {
203 return Ok(());
204 }
205
206 sobel_abs_or_threshold_3x3_into(input, dx, border, None, output);
207 Ok(())
208}
209
210pub fn sobel_threshold_3x3_u8(
216 input: ImageView<'_, u8, 1>,
217 dx: usize,
218 dy: usize,
219 threshold: u8,
220 border: BorderMode<u8, 1>,
221) -> VisionResult<Image<u8, 1>> {
222 let len = input
223 .width()
224 .checked_mul(input.height())
225 .ok_or_else(|| VisionError::InvalidDimensions("Sobel mask size overflows".into()))?;
226 let mut output = vec![0; len];
227 sobel_threshold_3x3_u8_into(input, dx, dy, threshold, border, &mut output)?;
228 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
229}
230
231pub fn sobel_threshold_3x3_u8_into(
233 input: ImageView<'_, u8, 1>,
234 dx: usize,
235 dy: usize,
236 threshold: u8,
237 border: BorderMode<u8, 1>,
238 output: &mut [u8],
239) -> VisionResult<()> {
240 validate_derivative(dx, dy, 3, 1.0, 0.0)?;
241 if dx + dy != 1 {
242 return Err(VisionError::InvalidParameter(
243 "Sobel threshold requires derivative order (1, 0) or (0, 1)".into(),
244 ));
245 }
246 if !matches!(border, BorderMode::Replicate | BorderMode::Reflect101) {
247 return Err(VisionError::InvalidParameter(
248 "Sobel threshold supports only Replicate and Reflect101 borders".into(),
249 ));
250 }
251 let len = input
252 .width()
253 .checked_mul(input.height())
254 .ok_or_else(|| VisionError::InvalidDimensions("Sobel mask size overflows".into()))?;
255 if output.len() != len {
256 return Err(VisionError::ShapeMismatch(format!(
257 "Sobel mask needs {len} elements, found {}",
258 output.len()
259 )));
260 }
261 if len == 0 {
262 return Ok(());
263 }
264 sobel_abs_or_threshold_3x3_into(input, dx, border, Some(threshold), output);
265 Ok(())
266}
267
268fn sobel_abs_or_threshold_3x3_into(
269 input: ImageView<'_, u8, 1>,
270 dx: usize,
271 border: BorderMode<u8, 1>,
272 threshold: Option<u8>,
273 output: &mut [u8],
274) {
275 let width = input.width();
276 let height = input.height();
277 let len = output.len();
278 let workers =
279 bounded_workers(len, height, ROW_PARALLEL_COMPONENTS, rayon::current_num_threads());
280 let rows_per_worker = items_per_worker(height, workers);
281 let mut scratch = vec![0_i16; workers * 3 * width];
282 scratch
283 .par_chunks_mut(3 * width)
284 .zip(output.par_chunks_mut(rows_per_worker * width))
285 .enumerate()
286 .for_each(|(chunk, (scratch, output))| {
287 sobel_abs_3x3_stripe(
288 input,
289 dx,
290 border,
291 threshold,
292 chunk * rows_per_worker,
293 scratch,
294 output,
295 );
296 });
297}
298
299fn sobel_abs_3x3_stripe(
300 input: ImageView<'_, u8, 1>,
301 dx: usize,
302 border: BorderMode<u8, 1>,
303 threshold: Option<u8>,
304 start_y: usize,
305 scratch: &mut [i16],
306 output: &mut [u8],
307) {
308 let width = input.width();
309 let height = input.height();
310 let mut slots = [0, 1, 2];
311 let (top_y, bottom_y) = gradient_neighbors(start_y, height, border);
312 sobel_horizontal_row(input, top_y, dx, border, ring_row_mut(scratch, width, slots[0]));
313 sobel_horizontal_row(input, start_y, dx, border, ring_row_mut(scratch, width, slots[1]));
314 sobel_horizontal_row(input, bottom_y, dx, border, ring_row_mut(scratch, width, slots[2]));
315
316 let stripe_rows = output.len() / width;
317 for (local_y, output) in output.chunks_mut(width).enumerate() {
318 let y = start_y + local_y;
319 let top = ring_row(scratch, width, slots[0]);
320 let middle = ring_row(scratch, width, slots[1]);
321 let bottom = ring_row(scratch, width, slots[2]);
322 if dx == 1 {
323 for (((output, &top), &middle), &bottom) in
324 output.iter_mut().zip(top).zip(middle).zip(bottom)
325 {
326 *output = threshold_sobel(top + 2 * middle + bottom, threshold);
327 }
328 } else {
329 for ((output, &top), &bottom) in output.iter_mut().zip(top).zip(bottom) {
330 *output = threshold_sobel(bottom - top, threshold);
331 }
332 }
333 if local_y + 1 < stripe_rows {
334 slots.rotate_left(1);
335 let (_, next_bottom_y) = gradient_neighbors(y + 1, height, border);
336 sobel_horizontal_row(
337 input,
338 next_bottom_y,
339 dx,
340 border,
341 ring_row_mut(scratch, width, slots[2]),
342 );
343 }
344 }
345}
346
347#[inline(always)]
348fn saturating_abs_u8(value: i16) -> u8 {
349 value.unsigned_abs().min(u16::from(u8::MAX)) as u8
350}
351
352#[inline(always)]
353fn threshold_sobel(value: i16, threshold: Option<u8>) -> u8 {
354 let value = saturating_abs_u8(value);
355 threshold.map_or(value, |threshold| u8::from(value > threshold) * u8::MAX)
356}
357
358fn sobel_3x3_identity_into(
359 input: ImageView<'_, u8, 1>,
360 dx: usize,
361 border: BorderMode<u8, 1>,
362 output: &mut [f32],
363) {
364 let width = input.width();
365 let height = input.height();
366 let len = width * height;
367 let workers =
368 bounded_workers(len, height, ROW_PARALLEL_COMPONENTS, rayon::current_num_threads());
369 let rows_per_worker = items_per_worker(height, workers);
370 let mut scratch = vec![0_i16; workers * 3 * width];
371 scratch
372 .par_chunks_mut(3 * width)
373 .zip(output.par_chunks_mut(rows_per_worker * width))
374 .enumerate()
375 .for_each(|(chunk, (scratch, output))| {
376 let start_y = chunk * rows_per_worker;
377 sobel_3x3_identity_stripe(input, dx, border, start_y, scratch, output);
378 });
379}
380
381fn sobel_3x3_identity_stripe(
382 input: ImageView<'_, u8, 1>,
383 dx: usize,
384 border: BorderMode<u8, 1>,
385 start_y: usize,
386 scratch: &mut [i16],
387 output: &mut [f32],
388) {
389 let width = input.width();
390 let height = input.height();
391 let mut slots = [0, 1, 2];
392 let (top_y, bottom_y) = gradient_neighbors(start_y, height, border);
393 sobel_horizontal_row(input, top_y, dx, border, ring_row_mut(scratch, width, slots[0]));
394 sobel_horizontal_row(input, start_y, dx, border, ring_row_mut(scratch, width, slots[1]));
395 sobel_horizontal_row(input, bottom_y, dx, border, ring_row_mut(scratch, width, slots[2]));
396
397 let stripe_rows = output.len() / width;
398 for (local_y, output) in output.chunks_mut(width).enumerate() {
399 let y = start_y + local_y;
400 let top = ring_row(scratch, width, slots[0]);
401 let middle = ring_row(scratch, width, slots[1]);
402 let bottom = ring_row(scratch, width, slots[2]);
403 if dx == 1 {
404 for (((output, &top), &middle), &bottom) in
405 output.iter_mut().zip(top).zip(middle).zip(bottom)
406 {
407 *output = (top + 2 * middle + bottom) as f32;
408 }
409 } else {
410 for ((output, &top), &bottom) in output.iter_mut().zip(top).zip(bottom) {
411 *output = (bottom - top) as f32;
412 }
413 }
414
415 if local_y + 1 < stripe_rows {
416 slots.rotate_left(1);
417 let next_y = y + 1;
418 let (_, next_bottom_y) = gradient_neighbors(next_y, height, border);
419 sobel_horizontal_row(
420 input,
421 next_bottom_y,
422 dx,
423 border,
424 ring_row_mut(scratch, width, slots[2]),
425 );
426 }
427 }
428}
429
430fn sobel_horizontal_row(
431 input: ImageView<'_, u8, 1>,
432 y: usize,
433 dx: usize,
434 border: BorderMode<u8, 1>,
435 output: &mut [i16],
436) {
437 let width = input.width();
438 let row = input.row(y).expect("Sobel horizontal row in bounds");
439 if width == 1 {
440 output[0] = if dx == 1 { 0 } else { 4 * i16::from(row[0]) };
441 return;
442 }
443 let (left, _) = gradient_neighbors(0, width, border);
444 output[0] = if dx == 1 {
445 i16::from(row[1]) - i16::from(row[left])
446 } else {
447 i16::from(row[left]) + 2 * i16::from(row[0]) + i16::from(row[1])
448 };
449 if dx == 1 {
450 for (output, (&left, &right)) in
451 output[1..width - 1].iter_mut().zip(row[..width - 2].iter().zip(&row[2..]))
452 {
453 *output = i16::from(right) - i16::from(left);
454 }
455 } else {
456 for (output, ((&left, ¢er), &right)) in output[1..width - 1]
457 .iter_mut()
458 .zip(row[..width - 2].iter().zip(&row[1..width - 1]).zip(&row[2..]))
459 {
460 *output = i16::from(left) + 2 * i16::from(center) + i16::from(right);
461 }
462 }
463 let x = width - 1;
464 let (_, right) = gradient_neighbors(x, width, border);
465 output[x] = if dx == 1 {
466 i16::from(row[right]) - i16::from(row[x - 1])
467 } else {
468 i16::from(row[x - 1]) + 2 * i16::from(row[x]) + i16::from(row[right])
469 };
470}
471
472fn ring_row(scratch: &[i16], width: usize, slot: usize) -> &[i16] {
473 &scratch[slot * width..(slot + 1) * width]
474}
475
476fn ring_row_mut(scratch: &mut [i16], width: usize, slot: usize) -> &mut [i16] {
477 &mut scratch[slot * width..(slot + 1) * width]
478}
479
480#[inline]
481fn sobel_3x3_rows_dispatch(
482 input: ImageView<'_, u8, 1>,
483 dx: usize,
484 scale: f64,
485 delta: f64,
486 border: BorderMode<u8, 1>,
487 start_y: usize,
488 output: &mut [f32],
489) {
490 if scale == 1.0 && delta == 0.0 {
491 if dx == 1 {
492 sobel_x_3x3_rows(input, border, start_y, output);
493 } else {
494 sobel_y_3x3_rows(input, border, start_y, output);
495 }
496 } else {
497 sobel_3x3_rows(input, dx, scale, delta, border, start_y, output);
498 }
499}
500
501fn sobel_x_3x3_rows(
502 input: ImageView<'_, u8, 1>,
503 border: BorderMode<u8, 1>,
504 start_y: usize,
505 output: &mut [f32],
506) {
507 let width = input.width();
508 let height = input.height();
509 for (local_y, output) in output.chunks_mut(width).enumerate() {
510 let y = start_y + local_y;
511 let (top_y, bottom_y) = gradient_neighbors(y, height, border);
512 let top = input.row(top_y).expect("Sobel X row in bounds");
513 let middle = input.row(y).expect("Sobel X row in bounds");
514 let bottom = input.row(bottom_y).expect("Sobel X row in bounds");
515 if width == 1 {
516 output[0] = 0.0;
517 continue;
518 }
519 let (left, _) = gradient_neighbors(0, width, border);
520 output[0] = sobel_x_value(top, middle, bottom, left, 1) as f32;
521 for (
522 ((output, (top_left, top_right)), (middle_left, middle_right)),
523 (bottom_left, bottom_right),
524 ) in output[1..width - 1]
525 .iter_mut()
526 .zip(top[..width - 2].iter().zip(&top[2..]))
527 .zip(middle[..width - 2].iter().zip(&middle[2..]))
528 .zip(bottom[..width - 2].iter().zip(&bottom[2..]))
529 {
530 *output = (i16::from(*top_right) - i16::from(*top_left)
531 + 2 * (i16::from(*middle_right) - i16::from(*middle_left))
532 + i16::from(*bottom_right)
533 - i16::from(*bottom_left)) as f32;
534 }
535 let x = width - 1;
536 let (_, right) = gradient_neighbors(x, width, border);
537 output[x] = sobel_x_value(top, middle, bottom, x - 1, right) as f32;
538 }
539}
540
541fn sobel_y_3x3_rows(
542 input: ImageView<'_, u8, 1>,
543 border: BorderMode<u8, 1>,
544 start_y: usize,
545 output: &mut [f32],
546) {
547 let width = input.width();
548 let height = input.height();
549 for (local_y, output) in output.chunks_mut(width).enumerate() {
550 let y = start_y + local_y;
551 let (top_y, bottom_y) = gradient_neighbors(y, height, border);
552 let top = input.row(top_y).expect("Sobel Y row in bounds");
553 let bottom = input.row(bottom_y).expect("Sobel Y row in bounds");
554 if width == 1 {
555 output[0] = (4 * (i16::from(bottom[0]) - i16::from(top[0]))) as f32;
556 continue;
557 }
558 let (left, _) = gradient_neighbors(0, width, border);
559 output[0] = sobel_y_value(top, bottom, left, 0, 1) as f32;
560 for (
561 (output, ((top_left, top_center), top_right)),
562 ((bottom_left, bottom_center), bottom_right),
563 ) in output[1..width - 1]
564 .iter_mut()
565 .zip(top[..width - 2].iter().zip(&top[1..width - 1]).zip(&top[2..]))
566 .zip(bottom[..width - 2].iter().zip(&bottom[1..width - 1]).zip(&bottom[2..]))
567 {
568 *output = (i16::from(*bottom_left) - i16::from(*top_left)
569 + 2 * (i16::from(*bottom_center) - i16::from(*top_center))
570 + i16::from(*bottom_right)
571 - i16::from(*top_right)) as f32;
572 }
573 let x = width - 1;
574 let (_, right) = gradient_neighbors(x, width, border);
575 output[x] = sobel_y_value(top, bottom, x - 1, x, right) as f32;
576 }
577}
578
579#[inline(always)]
580fn sobel_x_value(top: &[u8], middle: &[u8], bottom: &[u8], left: usize, right: usize) -> i16 {
581 i16::from(top[right]) - i16::from(top[left])
582 + 2 * (i16::from(middle[right]) - i16::from(middle[left]))
583 + i16::from(bottom[right])
584 - i16::from(bottom[left])
585}
586
587#[inline(always)]
588fn sobel_y_value(top: &[u8], bottom: &[u8], left: usize, center: usize, right: usize) -> i16 {
589 i16::from(bottom[left]) - i16::from(top[left])
590 + 2 * (i16::from(bottom[center]) - i16::from(top[center]))
591 + i16::from(bottom[right])
592 - i16::from(top[right])
593}
594
595fn sobel_3x3_rows(
596 input: ImageView<'_, u8, 1>,
597 dx: usize,
598 scale: f64,
599 delta: f64,
600 border: BorderMode<u8, 1>,
601 start_y: usize,
602 output: &mut [f32],
603) {
604 let width = input.width();
605 let height = input.height();
606 for (local_y, output) in output.chunks_mut(width).enumerate() {
607 let y = start_y + local_y;
608 let (top_y, bottom_y) = gradient_neighbors(y, height, border);
609 let top = input.row(top_y).expect("Sobel row in bounds");
610 let middle = input.row(y).expect("Sobel row in bounds");
611 let bottom = input.row(bottom_y).expect("Sobel row in bounds");
612 if width == 1 {
613 output[0] = scaled_sobel_3x3_pixel(top, middle, bottom, 0, 0, 0, dx, scale, delta);
614 continue;
615 }
616 let (left, _) = gradient_neighbors(0, width, border);
617 output[0] = scaled_sobel_3x3_pixel(top, middle, bottom, left, 0, 1, dx, scale, delta);
618 for (x, value) in output.iter_mut().enumerate().take(width - 1).skip(1) {
619 *value = scaled_sobel_3x3_pixel(top, middle, bottom, x - 1, x, x + 1, dx, scale, delta);
620 }
621 let x = width - 1;
622 let (_, right) = gradient_neighbors(x, width, border);
623 output[x] = scaled_sobel_3x3_pixel(top, middle, bottom, x - 1, x, right, dx, scale, delta);
624 }
625}
626
627#[inline(always)]
628#[allow(clippy::too_many_arguments)]
629fn scaled_sobel_3x3_pixel(
630 top: &[u8],
631 middle: &[u8],
632 bottom: &[u8],
633 left: usize,
634 center: usize,
635 right: usize,
636 dx: usize,
637 scale: f64,
638 delta: f64,
639) -> f32 {
640 let value = if dx == 1 {
641 i16::from(top[right]) + 2 * i16::from(middle[right]) + i16::from(bottom[right])
642 - i16::from(top[left])
643 - 2 * i16::from(middle[left])
644 - i16::from(bottom[left])
645 } else {
646 i16::from(bottom[left]) + 2 * i16::from(bottom[center]) + i16::from(bottom[right])
647 - i16::from(top[left])
648 - 2 * i16::from(top[center])
649 - i16::from(top[right])
650 };
651 f64::from(value).mul_add(scale, delta) as f32
652}
653
654pub fn spatial_gradient_u8(
661 input: ImageView<'_, u8, 1>,
662 border: BorderMode<u8, 1>,
663) -> VisionResult<(Image<i16, 1>, Image<i16, 1>)> {
664 let len = input
665 .width()
666 .checked_mul(input.height())
667 .ok_or_else(|| VisionError::InvalidDimensions("spatial gradient size overflows".into()))?;
668 let mut gradient_x = vec![0; len];
669 let mut gradient_y = vec![0; len];
670 spatial_gradient_u8_into(input, border, &mut gradient_x, &mut gradient_y)?;
671 Ok((
672 Image::try_new_with_metadata(input.width(), input.height(), gradient_x, input.metadata())?,
673 Image::try_new_with_metadata(input.width(), input.height(), gradient_y, input.metadata())?,
674 ))
675}
676
677pub fn spatial_gradient_u8_into(
683 input: ImageView<'_, u8, 1>,
684 border: BorderMode<u8, 1>,
685 gradient_x: &mut [i16],
686 gradient_y: &mut [i16],
687) -> VisionResult<()> {
688 if !matches!(border, BorderMode::Replicate | BorderMode::Reflect101) {
689 return Err(VisionError::InvalidParameter(
690 "spatial gradient supports only Replicate and Reflect101 borders".into(),
691 ));
692 }
693 let len = input
694 .width()
695 .checked_mul(input.height())
696 .ok_or_else(|| VisionError::InvalidDimensions("spatial gradient size overflows".into()))?;
697 validate_gradient_output(gradient_x, len, "gradient_x")?;
698 validate_gradient_output(gradient_y, len, "gradient_y")?;
699 if len == 0 {
700 return Ok(());
701 }
702
703 let width = input.width();
704 let height = input.height();
705 let arch = Arch::new();
706 let workers =
707 bounded_workers(len, height, LARGE_PARALLEL_COMPONENTS, rayon::current_num_threads());
708 if workers > 1 {
709 let rows_per_worker = items_per_worker(height, workers);
710 gradient_x
711 .par_chunks_mut(rows_per_worker * width)
712 .zip(gradient_y.par_chunks_mut(rows_per_worker * width))
713 .enumerate()
714 .for_each(|(chunk, (gradient_x, gradient_y))| {
715 arch.dispatch(|| {
716 spatial_gradient_rows(
717 input,
718 border,
719 chunk * rows_per_worker,
720 gradient_x,
721 gradient_y,
722 );
723 });
724 });
725 } else {
726 arch.dispatch(|| spatial_gradient_rows(input, border, 0, gradient_x, gradient_y));
727 }
728 Ok(())
729}
730
731pub fn sobel_l1_magnitude_u8(
736 input: ImageView<'_, u8, 1>,
737 border: BorderMode<u8, 1>,
738) -> VisionResult<Image<i16, 1>> {
739 let len = input
740 .width()
741 .checked_mul(input.height())
742 .ok_or_else(|| VisionError::InvalidDimensions("Sobel magnitude size overflows".into()))?;
743 let mut magnitude = vec![0; len];
744 sobel_l1_magnitude_u8_into(input, border, &mut magnitude)?;
745 Ok(Image::try_new_with_metadata(input.width(), input.height(), magnitude, input.metadata())?)
746}
747
748pub fn sobel_l1_magnitude_u8_into(
750 input: ImageView<'_, u8, 1>,
751 border: BorderMode<u8, 1>,
752 magnitude: &mut [i16],
753) -> VisionResult<()> {
754 if !matches!(border, BorderMode::Replicate | BorderMode::Reflect101) {
755 return Err(VisionError::InvalidParameter(
756 "Sobel L1 magnitude supports only Replicate and Reflect101 borders".into(),
757 ));
758 }
759 let len = input
760 .width()
761 .checked_mul(input.height())
762 .ok_or_else(|| VisionError::InvalidDimensions("Sobel magnitude size overflows".into()))?;
763 validate_gradient_output(magnitude, len, "magnitude")?;
764 if len == 0 {
765 return Ok(());
766 }
767 let width = input.width();
768 let height = input.height();
769 let arch = Arch::new();
770 let workers =
771 bounded_workers(len, height, LARGE_PARALLEL_COMPONENTS, rayon::current_num_threads());
772 if workers > 1 {
773 let rows_per_worker = items_per_worker(height, workers);
774 magnitude.par_chunks_mut(rows_per_worker * width).enumerate().for_each(
775 |(chunk, magnitude)| {
776 arch.dispatch(|| {
777 sobel_l1_rows(input, border, chunk * rows_per_worker, magnitude);
778 });
779 },
780 );
781 } else {
782 arch.dispatch(|| sobel_l1_rows(input, border, 0, magnitude));
783 }
784 Ok(())
785}
786
787fn validate_gradient_output(output: &[i16], len: usize, name: &str) -> VisionResult<()> {
788 if output.len() != len {
789 return Err(VisionError::ShapeMismatch(format!(
790 "{name} needs {len} elements, found {}",
791 output.len()
792 )));
793 }
794 Ok(())
795}
796
797fn spatial_gradient_rows(
798 input: ImageView<'_, u8, 1>,
799 border: BorderMode<u8, 1>,
800 start_y: usize,
801 gradient_x: &mut [i16],
802 gradient_y: &mut [i16],
803) {
804 let width = input.width();
805 let height = input.height();
806 for (local_y, (gradient_x, gradient_y)) in
807 gradient_x.chunks_mut(width).zip(gradient_y.chunks_mut(width)).enumerate()
808 {
809 let y = start_y + local_y;
810 let (top_y, bottom_y) = gradient_neighbors(y, height, border);
811 let top = input.row(top_y).expect("gradient row in bounds");
812 let middle = input.row(y).expect("gradient row in bounds");
813 let bottom = input.row(bottom_y).expect("gradient row in bounds");
814 if width == 1 {
815 write_spatial_gradient_pixel(top, middle, bottom, 0, 0, 0, gradient_x, gradient_y);
816 continue;
817 }
818 let (left, _) = gradient_neighbors(0, width, border);
819 write_spatial_gradient_pixel(top, middle, bottom, left, 0, 1, gradient_x, gradient_y);
820 for ((((top, middle), bottom), gradient_x), gradient_y) in top
821 .windows(3)
822 .zip(middle.windows(3))
823 .zip(bottom.windows(3))
824 .zip(gradient_x[1..width - 1].iter_mut())
825 .zip(gradient_y[1..width - 1].iter_mut())
826 {
827 let top_left = i16::from(top[0]);
828 let top_middle = i16::from(top[1]);
829 let top_right = i16::from(top[2]);
830 let middle_left = i16::from(middle[0]);
831 let middle_right = i16::from(middle[2]);
832 let bottom_left = i16::from(bottom[0]);
833 let bottom_middle = i16::from(bottom[1]);
834 let bottom_right = i16::from(bottom[2]);
835 *gradient_x = top_right + 2 * middle_right + bottom_right
836 - top_left
837 - 2 * middle_left
838 - bottom_left;
839 *gradient_y = bottom_left + 2 * bottom_middle + bottom_right
840 - top_left
841 - 2 * top_middle
842 - top_right;
843 }
844 let x = width - 1;
845 let (_, right) = gradient_neighbors(x, width, border);
846 write_spatial_gradient_pixel(top, middle, bottom, x - 1, x, right, gradient_x, gradient_y);
847 }
848}
849
850fn sobel_l1_rows(
851 input: ImageView<'_, u8, 1>,
852 border: BorderMode<u8, 1>,
853 start_y: usize,
854 magnitude: &mut [i16],
855) {
856 let width = input.width();
857 let height = input.height();
858 for (local_y, magnitude) in magnitude.chunks_mut(width).enumerate() {
859 let y = start_y + local_y;
860 let (top_y, bottom_y) = gradient_neighbors(y, height, border);
861 let top = input.row(top_y).expect("Sobel magnitude row in bounds");
862 let middle = input.row(y).expect("Sobel magnitude row in bounds");
863 let bottom = input.row(bottom_y).expect("Sobel magnitude row in bounds");
864 if width == 1 {
865 magnitude[0] = sobel_l1_pixel(top, middle, bottom, 0, 0, 0);
866 continue;
867 }
868 let (left, _) = gradient_neighbors(0, width, border);
869 magnitude[0] = sobel_l1_pixel(top, middle, bottom, left, 0, 1);
870 for (((top, middle), bottom), magnitude) in top
871 .windows(3)
872 .zip(middle.windows(3))
873 .zip(bottom.windows(3))
874 .zip(magnitude[1..width - 1].iter_mut())
875 {
876 *magnitude = sobel_l1_window(top, middle, bottom);
877 }
878 let x = width - 1;
879 let (_, right) = gradient_neighbors(x, width, border);
880 magnitude[x] = sobel_l1_pixel(top, middle, bottom, x - 1, x, right);
881 }
882}
883
884#[inline(always)]
885fn sobel_l1_window(top: &[u8], middle: &[u8], bottom: &[u8]) -> i16 {
886 sobel_l1_values(top[0], top[1], top[2], middle[0], middle[2], bottom[0], bottom[1], bottom[2])
887}
888
889#[inline(always)]
890#[allow(clippy::too_many_arguments)]
891fn sobel_l1_pixel(
892 top: &[u8],
893 middle: &[u8],
894 bottom: &[u8],
895 left: usize,
896 center: usize,
897 right: usize,
898) -> i16 {
899 sobel_l1_values(
900 top[left],
901 top[center],
902 top[right],
903 middle[left],
904 middle[right],
905 bottom[left],
906 bottom[center],
907 bottom[right],
908 )
909}
910
911#[inline(always)]
912#[allow(clippy::too_many_arguments)]
913fn sobel_l1_values(
914 top_left: u8,
915 top_middle: u8,
916 top_right: u8,
917 middle_left: u8,
918 middle_right: u8,
919 bottom_left: u8,
920 bottom_middle: u8,
921 bottom_right: u8,
922) -> i16 {
923 let gradient_x = i16::from(top_right) + 2 * i16::from(middle_right) + i16::from(bottom_right)
924 - i16::from(top_left)
925 - 2 * i16::from(middle_left)
926 - i16::from(bottom_left);
927 let gradient_y =
928 i16::from(bottom_left) + 2 * i16::from(bottom_middle) + i16::from(bottom_right)
929 - i16::from(top_left)
930 - 2 * i16::from(top_middle)
931 - i16::from(top_right);
932 gradient_x.abs() + gradient_y.abs()
933}
934
935#[inline(always)]
936#[allow(clippy::too_many_arguments)]
937fn write_spatial_gradient_pixel(
938 top: &[u8],
939 middle: &[u8],
940 bottom: &[u8],
941 left: usize,
942 center: usize,
943 right: usize,
944 gradient_x: &mut [i16],
945 gradient_y: &mut [i16],
946) {
947 let top_left = i16::from(top[left]);
948 let top_middle = i16::from(top[center]);
949 let top_right = i16::from(top[right]);
950 let middle_left = i16::from(middle[left]);
951 let middle_right = i16::from(middle[right]);
952 let bottom_left = i16::from(bottom[left]);
953 let bottom_middle = i16::from(bottom[center]);
954 let bottom_right = i16::from(bottom[right]);
955 gradient_x[center] =
956 top_right + 2 * middle_right + bottom_right - top_left - 2 * middle_left - bottom_left;
957 gradient_y[center] =
958 bottom_left + 2 * bottom_middle + bottom_right - top_left - 2 * top_middle - top_right;
959}
960
961fn gradient_neighbors(index: usize, length: usize, border: BorderMode<u8, 1>) -> (usize, usize) {
962 if length <= 1 {
963 return (0, 0);
964 }
965 let left = if index > 0 {
966 index - 1
967 } else if matches!(border, BorderMode::Reflect101) {
968 1
969 } else {
970 0
971 };
972 let right = if index + 1 < length {
973 index + 1
974 } else if matches!(border, BorderMode::Reflect101) {
975 length - 2
976 } else {
977 length - 1
978 };
979 (left, right)
980}
981
982pub fn scharr<T: PixelComponent, const CHANNELS: usize>(
984 input: ImageView<'_, T, CHANNELS>,
985 dx: usize,
986 dy: usize,
987 scale: f64,
988 delta: f64,
989 border: BorderMode<T, CHANNELS>,
990) -> VisionResult<Image<f32, CHANNELS>> {
991 if dx + dy != 1 || dx > 1 || dy > 1 {
992 return Err(VisionError::InvalidParameter(
993 "Scharr requires derivative order (1, 0) or (0, 1)".into(),
994 ));
995 }
996 if !scale.is_finite() || !delta.is_finite() {
997 return Err(VisionError::InvalidParameter("scale and delta must be finite".into()));
998 }
999 let derivative = Kernel1D::try_new(vec![-scale, 0.0, scale])?;
1000 let smoothing = Kernel1D::try_new(vec![3.0, 10.0, 3.0])?;
1001 if dx == 1 {
1002 separable_filter_f32(input, &derivative, &smoothing, delta, border)
1003 } else {
1004 separable_filter_f32(input, &smoothing, &derivative, delta, border)
1005 }
1006}
1007
1008pub fn laplacian<T: PixelComponent, const CHANNELS: usize>(
1010 input: ImageView<'_, T, CHANNELS>,
1011 kernel_size: usize,
1012 scale: f64,
1013 delta: f64,
1014 border: BorderMode<T, CHANNELS>,
1015) -> VisionResult<Image<f32, CHANNELS>> {
1016 if kernel_size == 1 {
1017 if !scale.is_finite() || !delta.is_finite() {
1018 return Err(VisionError::InvalidParameter("scale and delta must be finite".into()));
1019 }
1020 let kernel = Kernel2D::try_new(
1021 3,
1022 3,
1023 vec![0.0, scale, 0.0, scale, -4.0 * scale, scale, 0.0, scale, 0.0],
1024 )?;
1025 return filter2d_f32(input, &kernel, delta, border);
1026 }
1027 validate_derivative(2, 0, kernel_size, scale, delta)?;
1028 let dxx = sobel(input, 2, 0, kernel_size, scale, 0.0, border)?;
1029 let dyy = sobel(input, 0, 2, kernel_size, scale, 0.0, border)?;
1030 let output = dxx
1031 .as_slice()
1032 .iter()
1033 .zip(dyy.as_slice())
1034 .map(|(&x, &y)| (f64::from(x) + f64::from(y) + delta) as f32)
1035 .collect();
1036 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
1037}
1038
1039pub fn pyr_down<T: PixelComponent, const CHANNELS: usize>(
1041 input: ImageView<'_, T, CHANNELS>,
1042 border: BorderMode<T, CHANNELS>,
1043) -> VisionResult<Image<T, CHANNELS>> {
1044 let kernel =
1045 Kernel1D::try_new(vec![1.0 / 16.0, 4.0 / 16.0, 6.0 / 16.0, 4.0 / 16.0, 1.0 / 16.0])?;
1046 let blurred = separable_filter(input, &kernel, &kernel, 0.0, border)?;
1047 let width = input.width().div_ceil(2);
1048 let height = input.height().div_ceil(2);
1049 let mut output = Vec::with_capacity(width * height * CHANNELS);
1050 for y in 0..height {
1051 for x in 0..width {
1052 output.extend_from_slice(blurred.get(x * 2, y * 2).expect("pyramid sample in bounds"));
1053 }
1054 }
1055 Ok(Image::try_new_with_metadata(width, height, output, input.metadata())?)
1056}
1057
1058pub fn pyr_up<T: PixelComponent, const CHANNELS: usize>(
1060 input: ImageView<'_, T, CHANNELS>,
1061 border: BorderMode<T, CHANNELS>,
1062) -> VisionResult<Image<T, CHANNELS>> {
1063 let width = input
1064 .width()
1065 .checked_mul(2)
1066 .ok_or_else(|| VisionError::InvalidDimensions("pyramid width overflow".into()))?;
1067 let height = input
1068 .height()
1069 .checked_mul(2)
1070 .ok_or_else(|| VisionError::InvalidDimensions("pyramid height overflow".into()))?;
1071 let mut expanded =
1072 Image::<T, CHANNELS>::from_pixel(width, height, std::array::from_fn(|_| T::from_f64(0.0)))?;
1073 expanded.set_metadata(input.metadata())?;
1074 for y in 0..input.height() {
1075 for x in 0..input.width() {
1076 *expanded.get_mut(x * 2, y * 2).expect("expanded coordinate in bounds") =
1077 *input.get(x, y).expect("source coordinate in bounds");
1078 }
1079 }
1080 let kernel = Kernel1D::try_new(vec![1.0 / 8.0, 4.0 / 8.0, 6.0 / 8.0, 4.0 / 8.0, 1.0 / 8.0])?;
1081 separable_filter(expanded.view(), &kernel, &kernel, 0.0, border)
1082}
1083
1084pub fn build_gaussian_pyramid<T: PixelComponent, const CHANNELS: usize>(
1086 input: ImageView<'_, T, CHANNELS>,
1087 levels: usize,
1088 border: BorderMode<T, CHANNELS>,
1089) -> VisionResult<Vec<Image<T, CHANNELS>>> {
1090 if levels == 0 {
1091 return Err(VisionError::InvalidParameter("pyramid levels must be non-zero".into()));
1092 }
1093 let mut packed = Vec::with_capacity(input.width() * input.height() * CHANNELS);
1094 for y in 0..input.height() {
1095 for x in 0..input.width() {
1096 packed.extend_from_slice(input.get(x, y).expect("input coordinate in bounds"));
1097 }
1098 }
1099 let first =
1100 Image::try_new_with_metadata(input.width(), input.height(), packed, input.metadata())?;
1101 let mut pyramid = Vec::with_capacity(levels);
1102 pyramid.push(first);
1103 while pyramid.len() < levels {
1104 let next = pyr_down(pyramid.last().expect("level zero exists").view(), border)?;
1105 pyramid.push(next);
1106 }
1107 Ok(pyramid)
1108}
1109
1110fn validate_odd_size(size: usize, name: &str) -> VisionResult<()> {
1111 if size == 0 || size % 2 == 0 {
1112 return Err(VisionError::InvalidParameter(format!(
1113 "{name} kernel size must be positive and odd"
1114 )));
1115 }
1116 Ok(())
1117}
1118
1119fn validate_positive_finite(value: f64, name: &str) -> VisionResult<()> {
1120 if !value.is_finite() || value <= 0.0 {
1121 return Err(VisionError::InvalidParameter(format!("{name} must be finite and positive")));
1122 }
1123 Ok(())
1124}
1125
1126fn validate_derivative(
1127 dx: usize,
1128 dy: usize,
1129 kernel_size: usize,
1130 scale: f64,
1131 delta: f64,
1132) -> VisionResult<()> {
1133 if !matches!(kernel_size, 3 | 5 | 7) {
1134 return Err(VisionError::InvalidParameter("Sobel kernel size must be 3, 5, or 7".into()));
1135 }
1136 if dx + dy == 0 || dx > 2 || dy > 2 || dx >= kernel_size || dy >= kernel_size {
1137 return Err(VisionError::InvalidParameter(
1138 "Sobel derivative orders must total at least one and each be at most two".into(),
1139 ));
1140 }
1141 if !scale.is_finite() || !delta.is_finite() {
1142 return Err(VisionError::InvalidParameter("scale and delta must be finite".into()));
1143 }
1144 Ok(())
1145}
1146
1147fn derivative_kernel(size: usize, order: usize) -> VisionResult<Kernel1D> {
1148 let mut polynomial = vec![1.0];
1149 for _ in 0..order {
1150 polynomial = convolve_coefficients(&polynomial, &[-1.0, 1.0]);
1151 }
1152 for _ in 0..(size - 1 - order) {
1153 polynomial = convolve_coefficients(&polynomial, &[1.0, 1.0]);
1154 }
1155 Kernel1D::try_new(polynomial)
1156}
1157
1158fn convolve_coefficients(left: &[f64], right: &[f64]) -> Vec<f64> {
1159 let mut output = vec![0.0; left.len() + right.len() - 1];
1160 for (i, &a) in left.iter().enumerate() {
1161 for (j, &b) in right.iter().enumerate() {
1162 output[i + j] += a * b;
1163 }
1164 }
1165 output
1166}
1167
1168#[cfg(test)]
1169mod tests {
1170 use super::{
1171 bilateral_filter, build_gaussian_pyramid, laplacian, median_blur, pyr_down, scharr, sobel,
1172 sobel_3x3_u8, sobel_3x3_u8_into, sobel_abs_3x3_u8, sobel_abs_3x3_u8_into,
1173 sobel_l1_magnitude_u8, sobel_l1_magnitude_u8_into, sobel_threshold_3x3_u8,
1174 sobel_threshold_3x3_u8_into, spatial_gradient_u8, spatial_gradient_u8_into,
1175 };
1176 use crate::BorderMode;
1177 use spatialrust_image::{Image, ImageRegion};
1178
1179 #[test]
1180 fn median_removes_impulse_and_accepts_strided_roi() {
1181 let parent =
1182 Image::<u8, 1>::try_new(5, 3, vec![0, 0, 0, 0, 0, 0, 10, 255, 10, 0, 0, 10, 10, 10, 0])
1183 .unwrap();
1184 let roi = parent.view().subview(ImageRegion::new(1, 0, 3, 3)).unwrap();
1185 let output = median_blur(roi, 3, BorderMode::Replicate).unwrap();
1186 assert_eq!(output[(1, 1)][0], 10);
1187 }
1188
1189 #[test]
1190 fn bilateral_preserves_sharp_constant_regions() {
1191 let image = Image::<u8, 1>::try_new(5, 1, vec![0, 0, 0, 255, 255]).unwrap();
1192 let output = bilateral_filter(image.view(), 3, 5.0, 1.0, BorderMode::Replicate).unwrap();
1193 assert_eq!(output.as_slice(), image.as_slice());
1194 }
1195
1196 #[test]
1197 fn sobel_and_scharr_detect_horizontal_ramp() {
1198 let image =
1199 Image::<u8, 1>::try_new(5, 3, (0..3).flat_map(|_| [0, 10, 20, 30, 40]).collect())
1200 .unwrap();
1201 let sx = sobel(image.view(), 1, 0, 3, 1.0, 0.0, BorderMode::Reflect101).unwrap();
1202 let sy = sobel(image.view(), 0, 1, 3, 1.0, 0.0, BorderMode::Reflect101).unwrap();
1203 let scharr_x = scharr(image.view(), 1, 0, 1.0, 0.0, BorderMode::Reflect101).unwrap();
1204 assert_eq!(sx[(2, 1)][0], 80.0);
1205 assert_eq!(sy[(2, 1)][0], 0.0);
1206 assert_eq!(scharr_x[(2, 1)][0], 320.0);
1207 }
1208
1209 #[test]
1210 fn paired_spatial_gradient_matches_independent_sobel_for_strided_input() {
1211 let parent = Image::<u8, 1>::try_new(
1212 9,
1213 6,
1214 (0..54).map(|index| ((index * 37 + 11) % 256) as u8).collect(),
1215 )
1216 .unwrap();
1217 let input = parent.view().subview(ImageRegion::new(1, 1, 7, 4)).unwrap();
1218 for border in [BorderMode::Replicate, BorderMode::Reflect101] {
1219 let (gradient_x, gradient_y) = spatial_gradient_u8(input, border).unwrap();
1220 let expected_x = sobel(input, 1, 0, 3, 1.0, 0.0, border).unwrap();
1221 let expected_y = sobel(input, 0, 1, 3, 1.0, 0.0, border).unwrap();
1222 assert_eq!(
1223 gradient_x.as_slice(),
1224 expected_x.as_slice().iter().map(|value| *value as i16).collect::<Vec<_>>()
1225 );
1226 assert_eq!(
1227 gradient_y.as_slice(),
1228 expected_y.as_slice().iter().map(|value| *value as i16).collect::<Vec<_>>()
1229 );
1230 assert_eq!(gradient_x.metadata(), input.metadata());
1231 assert_eq!(gradient_y.metadata(), input.metadata());
1232 }
1233 }
1234
1235 #[test]
1236 fn specialized_sobel_matches_generic_for_strided_input() {
1237 let parent = Image::<u8, 1>::try_new(
1238 11,
1239 8,
1240 (0..88).map(|index| ((index * 41 + 19) % 256) as u8).collect(),
1241 )
1242 .unwrap();
1243 let input = parent.view().subview(ImageRegion::new(1, 1, 9, 6)).unwrap();
1244 for border in [BorderMode::Replicate, BorderMode::Reflect101] {
1245 for (dx, dy) in [(1, 0), (0, 1)] {
1246 let expected = sobel(input, dx, dy, 3, 0.75, -2.5, border).unwrap();
1247 let actual = sobel_3x3_u8(input, dx, dy, 0.75, -2.5, border).unwrap();
1248 assert_eq!(actual, expected);
1249 assert_eq!(actual.metadata(), input.metadata());
1250 }
1251 }
1252 }
1253
1254 #[test]
1255 fn specialized_sobel_into_validates_contract() {
1256 let image = Image::<u8, 1>::try_new(3, 2, vec![0, 1, 2, 3, 4, 5]).unwrap();
1257 let mut output = vec![0.0; 6];
1258 sobel_3x3_u8_into(image.view(), 1, 0, 1.0, 0.0, BorderMode::Reflect101, &mut output)
1259 .unwrap();
1260 assert!(sobel_3x3_u8_into(
1261 image.view(),
1262 1,
1263 0,
1264 1.0,
1265 0.0,
1266 BorderMode::Reflect101,
1267 &mut output[..5],
1268 )
1269 .is_err());
1270 assert!(sobel_3x3_u8_into(
1271 image.view(),
1272 1,
1273 1,
1274 1.0,
1275 0.0,
1276 BorderMode::Reflect101,
1277 &mut output,
1278 )
1279 .is_err());
1280 assert!(sobel_3x3_u8_into(image.view(), 1, 0, 1.0, 0.0, BorderMode::Wrap, &mut output,)
1281 .is_err());
1282 }
1283
1284 #[test]
1285 fn absolute_sobel_matches_saturated_specialized_derivative() {
1286 let parent = Image::<u8, 1>::try_new(
1287 12,
1288 9,
1289 (0..108).map(|index| ((index * 67 + 13) % 256) as u8).collect(),
1290 )
1291 .unwrap();
1292 let input = parent.view().subview(ImageRegion::new(1, 1, 10, 7)).unwrap();
1293 for border in [BorderMode::Replicate, BorderMode::Reflect101] {
1294 for (dx, dy) in [(1, 0), (0, 1)] {
1295 let derivative = sobel_3x3_u8(input, dx, dy, 1.0, 0.0, border).unwrap();
1296 let expected = derivative
1297 .as_slice()
1298 .iter()
1299 .map(|value| value.abs().min(255.0) as u8)
1300 .collect::<Vec<_>>();
1301 let actual = sobel_abs_3x3_u8(input, dx, dy, border).unwrap();
1302 assert_eq!(actual.as_slice(), expected);
1303 let mut reused = vec![0; input.width() * input.height()];
1304 sobel_abs_3x3_u8_into(input, dx, dy, border, &mut reused).unwrap();
1305 assert_eq!(reused, expected);
1306 }
1307 }
1308 }
1309
1310 #[test]
1311 fn thresholded_sobel_matches_absolute_response() {
1312 let image = Image::<u8, 1>::try_new(
1313 17,
1314 13,
1315 (0..221).map(|index| ((index * 29 + 7) % 256) as u8).collect(),
1316 )
1317 .unwrap();
1318 for (dx, dy) in [(1, 0), (0, 1)] {
1319 let absolute = sobel_abs_3x3_u8(image.view(), dx, dy, BorderMode::Reflect101).unwrap();
1320 let expected = absolute
1321 .as_slice()
1322 .iter()
1323 .map(|&value| u8::from(value > 96) * u8::MAX)
1324 .collect::<Vec<_>>();
1325 let actual =
1326 sobel_threshold_3x3_u8(image.view(), dx, dy, 96, BorderMode::Reflect101).unwrap();
1327 assert_eq!(actual.as_slice(), expected);
1328 let mut reused = vec![0; expected.len()];
1329 sobel_threshold_3x3_u8_into(
1330 image.view(),
1331 dx,
1332 dy,
1333 96,
1334 BorderMode::Reflect101,
1335 &mut reused,
1336 )
1337 .unwrap();
1338 assert_eq!(reused, expected);
1339 }
1340 }
1341
1342 #[test]
1343 fn paired_spatial_gradient_into_validates_outputs_and_borders() {
1344 let image = Image::<u8, 1>::try_new(3, 2, vec![0, 1, 2, 3, 4, 5]).unwrap();
1345 let mut gradient_x = vec![0; 6];
1346 let mut gradient_y = vec![0; 6];
1347 spatial_gradient_u8_into(
1348 image.view(),
1349 BorderMode::Reflect101,
1350 &mut gradient_x,
1351 &mut gradient_y,
1352 )
1353 .unwrap();
1354 assert!(spatial_gradient_u8_into(
1355 image.view(),
1356 BorderMode::Reflect101,
1357 &mut gradient_x[..5],
1358 &mut gradient_y,
1359 )
1360 .is_err());
1361 assert!(spatial_gradient_u8_into(
1362 image.view(),
1363 BorderMode::Wrap,
1364 &mut gradient_x,
1365 &mut gradient_y,
1366 )
1367 .is_err());
1368 }
1369
1370 #[test]
1371 fn fused_sobel_l1_matches_paired_gradients_for_strided_input() {
1372 let parent = Image::<u8, 1>::try_new(
1373 10,
1374 7,
1375 (0..70).map(|index| ((index * 53 + 7) % 256) as u8).collect(),
1376 )
1377 .unwrap();
1378 let input = parent.view().subview(ImageRegion::new(1, 1, 8, 5)).unwrap();
1379 for border in [BorderMode::Replicate, BorderMode::Reflect101] {
1380 let (gradient_x, gradient_y) = spatial_gradient_u8(input, border).unwrap();
1381 let magnitude = sobel_l1_magnitude_u8(input, border).unwrap();
1382 let expected = gradient_x
1383 .as_slice()
1384 .iter()
1385 .zip(gradient_y.as_slice())
1386 .map(|(&x, &y)| x.abs() + y.abs())
1387 .collect::<Vec<_>>();
1388 assert_eq!(magnitude.as_slice(), expected);
1389 assert_eq!(magnitude.metadata(), input.metadata());
1390 }
1391 }
1392
1393 #[test]
1394 fn fused_sobel_l1_into_validates_output() {
1395 let image = Image::<u8, 1>::try_new(3, 2, vec![0, 1, 2, 3, 4, 5]).unwrap();
1396 let mut output = vec![0; 6];
1397 sobel_l1_magnitude_u8_into(image.view(), BorderMode::Replicate, &mut output).unwrap();
1398 assert!(sobel_l1_magnitude_u8_into(image.view(), BorderMode::Replicate, &mut output[..5])
1399 .is_err());
1400 }
1401
1402 #[test]
1403 fn laplacian_of_constant_is_zero() {
1404 let image = Image::<u16, 1>::from_pixel(7, 5, [42]).unwrap();
1405 for size in [1, 3, 5, 7] {
1406 let output = laplacian(image.view(), size, 1.0, 0.0, BorderMode::Reflect101).unwrap();
1407 assert!(output.as_slice().iter().all(|value| value.abs() < f32::EPSILON));
1408 }
1409 }
1410
1411 #[test]
1412 fn pyramid_dimensions_follow_ceil_halving() {
1413 let image = Image::<f32, 3>::from_pixel(7, 5, [1.0, 2.0, 3.0]).unwrap();
1414 let down = pyr_down(image.view(), BorderMode::Reflect101).unwrap();
1415 assert_eq!((down.width(), down.height()), (4, 3));
1416 let pyramid = build_gaussian_pyramid(image.view(), 4, BorderMode::Reflect101).unwrap();
1417 assert_eq!(
1418 pyramid.iter().map(|level| (level.width(), level.height())).collect::<Vec<_>>(),
1419 vec![(7, 5), (4, 3), (2, 2), (1, 1)]
1420 );
1421 }
1422
1423 #[test]
1424 fn invalid_parameters_are_rejected() {
1425 let image = Image::<u8, 1>::try_new(1, 1, vec![0]).unwrap();
1426 assert!(median_blur(image.view(), 2, BorderMode::Replicate).is_err());
1427 assert!(bilateral_filter(image.view(), 0, 1.0, 1.0, BorderMode::Replicate).is_err());
1428 assert!(sobel(image.view(), 0, 0, 3, 1.0, 0.0, BorderMode::Replicate).is_err());
1429 }
1430}