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