1use pulp::Arch;
4use rayon::prelude::*;
5use spatialrust_image::{Image, ImageView};
6
7use crate::border::{fetch, map_index};
8use crate::dispatch::{
9 bounded_workers, items_per_worker, should_parallelize, LARGE_PARALLEL_COMPONENTS, ROWS_PER_TILE,
10};
11use crate::{BorderMode, PixelComponent, VisionError, VisionResult};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
15pub enum MorphologyShape {
16 Rect,
18 Cross,
20 Ellipse,
22 Diamond,
24}
25
26#[derive(Clone, Debug, PartialEq, Eq)]
28pub struct StructuringElement {
29 width: usize,
30 height: usize,
31 anchor_x: usize,
32 anchor_y: usize,
33 mask: Vec<bool>,
34}
35
36impl StructuringElement {
37 pub fn try_new(shape: MorphologyShape, width: usize, height: usize) -> VisionResult<Self> {
39 Self::try_new_with_anchor(shape, width, height, width / 2, height / 2)
40 }
41
42 pub fn try_new_with_anchor(
44 shape: MorphologyShape,
45 width: usize,
46 height: usize,
47 anchor_x: usize,
48 anchor_y: usize,
49 ) -> VisionResult<Self> {
50 validate_dimensions(width, height, anchor_x, anchor_y)?;
51 let mut mask = vec![false; width * height];
52 match shape {
53 MorphologyShape::Rect => mask.fill(true),
54 MorphologyShape::Cross => {
55 for y in 0..height {
56 for x in 0..width {
57 mask[y * width + x] = x == anchor_x || y == anchor_y;
58 }
59 }
60 }
61 MorphologyShape::Ellipse => fill_ellipse(&mut mask, width, height),
62 MorphologyShape::Diamond => {
63 let rx = anchor_x.max(width - 1 - anchor_x).max(1);
64 let ry = anchor_y.max(height - 1 - anchor_y).max(1);
65 for y in 0..height {
66 for x in 0..width {
67 let dx = x.abs_diff(anchor_x) as f64 / rx as f64;
68 let dy = y.abs_diff(anchor_y) as f64 / ry as f64;
69 mask[y * width + x] = dx + dy <= 1.0 + f64::EPSILON;
70 }
71 }
72 }
73 }
74 Self::try_from_mask(width, height, anchor_x, anchor_y, mask)
75 }
76
77 pub fn try_from_mask(
79 width: usize,
80 height: usize,
81 anchor_x: usize,
82 anchor_y: usize,
83 mask: Vec<bool>,
84 ) -> VisionResult<Self> {
85 validate_dimensions(width, height, anchor_x, anchor_y)?;
86 let expected = width
87 .checked_mul(height)
88 .ok_or_else(|| VisionError::InvalidParameter("structuring element overflows".into()))?;
89 if mask.len() != expected {
90 return Err(VisionError::ShapeMismatch(format!(
91 "structuring element needs {expected} mask values, found {}",
92 mask.len()
93 )));
94 }
95 if !mask.iter().any(|&active| active) {
96 return Err(VisionError::InvalidParameter(
97 "structuring element must contain an active sample".into(),
98 ));
99 }
100 Ok(Self { width, height, anchor_x, anchor_y, mask })
101 }
102
103 #[must_use]
105 pub const fn width(&self) -> usize {
106 self.width
107 }
108
109 #[must_use]
111 pub const fn height(&self) -> usize {
112 self.height
113 }
114
115 #[must_use]
117 pub const fn anchor(&self) -> (usize, usize) {
118 (self.anchor_x, self.anchor_y)
119 }
120
121 #[must_use]
123 pub fn mask(&self) -> &[bool] {
124 &self.mask
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
130pub enum MorphologyOperation {
131 Open,
133 Close,
135 Gradient,
137 TopHat,
139 BlackHat,
141}
142
143pub fn erode<T: PixelComponent, const CHANNELS: usize>(
145 input: ImageView<'_, T, CHANNELS>,
146 element: &StructuringElement,
147 iterations: usize,
148 border: BorderMode<T, CHANNELS>,
149) -> VisionResult<Image<T, CHANNELS>> {
150 repeat_extreme(input, element, iterations, border, Extreme::Minimum)
151}
152
153pub fn dilate<T: PixelComponent, const CHANNELS: usize>(
155 input: ImageView<'_, T, CHANNELS>,
156 element: &StructuringElement,
157 iterations: usize,
158 border: BorderMode<T, CHANNELS>,
159) -> VisionResult<Image<T, CHANNELS>> {
160 repeat_extreme(input, element, iterations, border, Extreme::Maximum)
161}
162
163pub fn morphology_ex<T: PixelComponent, const CHANNELS: usize>(
165 input: ImageView<'_, T, CHANNELS>,
166 operation: MorphologyOperation,
167 element: &StructuringElement,
168 iterations: usize,
169 border: BorderMode<T, CHANNELS>,
170) -> VisionResult<Image<T, CHANNELS>> {
171 let original = pack(input)?;
172 match operation {
173 MorphologyOperation::Open => {
174 let eroded = erode(input, element, iterations, border)?;
175 dilate(eroded.view(), element, iterations, border)
176 }
177 MorphologyOperation::Close => {
178 let dilated = dilate(input, element, iterations, border)?;
179 erode(dilated.view(), element, iterations, border)
180 }
181 MorphologyOperation::Gradient => {
182 let high = dilate(input, element, iterations, border)?;
183 let low = erode(input, element, iterations, border)?;
184 subtract(high.view(), low.view())
185 }
186 MorphologyOperation::TopHat => {
187 let opened =
188 morphology_ex(input, MorphologyOperation::Open, element, iterations, border)?;
189 subtract(original.view(), opened.view())
190 }
191 MorphologyOperation::BlackHat => {
192 let closed =
193 morphology_ex(input, MorphologyOperation::Close, element, iterations, border)?;
194 subtract(closed.view(), original.view())
195 }
196 }
197}
198
199pub fn erode_rect_u8(
205 input: ImageView<'_, u8, 1>,
206 element: &StructuringElement,
207 iterations: usize,
208 border: BorderMode<u8, 1>,
209) -> VisionResult<Image<u8, 1>> {
210 let mut output = vec![0; input.width() * input.height()];
211 let mut workspace = RectMorphologyWorkspace::new();
212 erode_rect_u8_into(input, element, iterations, border, &mut output, &mut workspace)?;
213 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
214}
215
216pub fn dilate_rect_u8(
218 input: ImageView<'_, u8, 1>,
219 element: &StructuringElement,
220 iterations: usize,
221 border: BorderMode<u8, 1>,
222) -> VisionResult<Image<u8, 1>> {
223 let mut output = vec![0; input.width() * input.height()];
224 let mut workspace = RectMorphologyWorkspace::new();
225 dilate_rect_u8_into(input, element, iterations, border, &mut output, &mut workspace)?;
226 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
227}
228
229pub fn morphology_rect_u8(
231 input: ImageView<'_, u8, 1>,
232 operation: MorphologyOperation,
233 element: &StructuringElement,
234 iterations: usize,
235 border: BorderMode<u8, 1>,
236) -> VisionResult<Image<u8, 1>> {
237 let mut output = vec![0; input.width() * input.height()];
238 let mut workspace = RectMorphologyWorkspace::new();
239 morphology_rect_u8_into(
240 input,
241 operation,
242 element,
243 iterations,
244 border,
245 &mut output,
246 &mut workspace,
247 )?;
248 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
249}
250
251#[derive(Debug, Default)]
258pub struct RectMorphologyWorkspace {
259 current: Vec<u8>,
260 horizontal: Vec<u8>,
261 transposed: Vec<u8>,
262 filtered: Vec<u8>,
263 output: Vec<u8>,
264 padded: Vec<u8>,
265 prefix: Vec<u8>,
266 suffix: Vec<u8>,
267 line_buffers: Vec<LineBuffers>,
268 centered_5x5_active: bool,
269}
270
271impl RectMorphologyWorkspace {
272 #[must_use]
274 pub const fn new() -> Self {
275 Self {
276 current: Vec::new(),
277 horizontal: Vec::new(),
278 transposed: Vec::new(),
279 filtered: Vec::new(),
280 output: Vec::new(),
281 padded: Vec::new(),
282 prefix: Vec::new(),
283 suffix: Vec::new(),
284 line_buffers: Vec::new(),
285 centered_5x5_active: false,
286 }
287 }
288
289 #[must_use]
291 pub fn capacity(&self) -> usize {
292 let active =
293 self.current.capacity().min(self.horizontal.capacity()).min(self.output.capacity());
294 if self.centered_5x5_active {
295 active
296 } else {
297 active.min(self.transposed.capacity()).min(self.filtered.capacity())
298 }
299 }
300
301 #[must_use]
303 pub fn worker_capacity(&self) -> usize {
304 self.line_buffers.len()
305 }
306
307 #[must_use]
309 pub fn line_capacity(&self) -> usize {
310 if self.line_buffers.is_empty() {
311 return self.padded.capacity().min(self.prefix.capacity()).min(self.suffix.capacity());
312 }
313 self.line_buffers
314 .iter()
315 .map(|buffers| {
316 buffers
317 .padded
318 .capacity()
319 .min(buffers.prefix.capacity())
320 .min(buffers.suffix.capacity())
321 })
322 .min()
323 .unwrap_or(0)
324 }
325}
326
327pub fn erode_rect_u8_into(
329 input: ImageView<'_, u8, 1>,
330 element: &StructuringElement,
331 iterations: usize,
332 border: BorderMode<u8, 1>,
333 output: &mut [u8],
334 workspace: &mut RectMorphologyWorkspace,
335) -> VisionResult<()> {
336 validate_output_len(output, input.width(), input.height())?;
337 run_rect_sequence(input, element, border, &[(Extreme::Minimum, iterations)], workspace)?;
338 output.copy_from_slice(&workspace.current);
339 Ok(())
340}
341
342pub fn dilate_rect_u8_into(
344 input: ImageView<'_, u8, 1>,
345 element: &StructuringElement,
346 iterations: usize,
347 border: BorderMode<u8, 1>,
348 output: &mut [u8],
349 workspace: &mut RectMorphologyWorkspace,
350) -> VisionResult<()> {
351 validate_output_len(output, input.width(), input.height())?;
352 run_rect_sequence(input, element, border, &[(Extreme::Maximum, iterations)], workspace)?;
353 output.copy_from_slice(&workspace.current);
354 Ok(())
355}
356
357#[allow(clippy::too_many_arguments)]
362pub fn morphology_rect_u8_into(
363 input: ImageView<'_, u8, 1>,
364 operation: MorphologyOperation,
365 element: &StructuringElement,
366 iterations: usize,
367 border: BorderMode<u8, 1>,
368 output: &mut [u8],
369 workspace: &mut RectMorphologyWorkspace,
370) -> VisionResult<()> {
371 validate_output_len(output, input.width(), input.height())?;
372 match operation {
373 MorphologyOperation::Open => run_rect_sequence(
374 input,
375 element,
376 border,
377 &[(Extreme::Minimum, iterations), (Extreme::Maximum, iterations)],
378 workspace,
379 )?,
380 MorphologyOperation::Close => run_rect_sequence(
381 input,
382 element,
383 border,
384 &[(Extreme::Maximum, iterations), (Extreme::Minimum, iterations)],
385 workspace,
386 )?,
387 MorphologyOperation::Gradient => {
388 run_rect_sequence(
389 input,
390 element,
391 border,
392 &[(Extreme::Maximum, iterations)],
393 workspace,
394 )?;
395 output.copy_from_slice(&workspace.current);
396 run_rect_sequence(
397 input,
398 element,
399 border,
400 &[(Extreme::Minimum, iterations)],
401 workspace,
402 )?;
403 for (high, &low) in output.iter_mut().zip(&workspace.current) {
404 *high = high.saturating_sub(low);
405 }
406 return Ok(());
407 }
408 MorphologyOperation::TopHat => {
409 pack_u8_into(input, output);
410 run_rect_sequence(
411 input,
412 element,
413 border,
414 &[(Extreme::Minimum, iterations), (Extreme::Maximum, iterations)],
415 workspace,
416 )?;
417 for (original, &opened) in output.iter_mut().zip(&workspace.current) {
418 *original = original.saturating_sub(opened);
419 }
420 return Ok(());
421 }
422 MorphologyOperation::BlackHat => {
423 run_rect_sequence(
424 input,
425 element,
426 border,
427 &[(Extreme::Maximum, iterations), (Extreme::Minimum, iterations)],
428 workspace,
429 )?;
430 copy_subtract_input(input, &workspace.current, output);
431 return Ok(());
432 }
433 }
434 output.copy_from_slice(&workspace.current);
435 Ok(())
436}
437
438#[derive(Clone, Copy)]
439enum Extreme {
440 Minimum,
441 Maximum,
442}
443
444fn validate_rect(element: &StructuringElement) -> VisionResult<()> {
445 if element.mask.iter().all(|&active| active) {
446 Ok(())
447 } else {
448 Err(VisionError::InvalidParameter(
449 "rectangular morphology fast path requires a full rectangular mask".into(),
450 ))
451 }
452}
453
454fn run_rect_sequence(
455 input: ImageView<'_, u8, 1>,
456 element: &StructuringElement,
457 border: BorderMode<u8, 1>,
458 stages: &[(Extreme, usize)],
459 workspace: &mut RectMorphologyWorkspace,
460) -> VisionResult<()> {
461 validate_rect(element)?;
462 let (width, height) = (input.width(), input.height());
463 workspace.current.resize(width * height, 0);
464 pack_u8_into(input, &mut workspace.current);
465 for &(extreme, iterations) in stages {
466 for _ in 0..iterations {
467 let current = std::mem::take(&mut workspace.current);
468 workspace.apply(
469 ¤t,
470 width,
471 height,
472 element.width,
473 element.height,
474 element.anchor_x,
475 element.anchor_y,
476 border,
477 extreme,
478 );
479 workspace.current = current;
480 std::mem::swap(&mut workspace.current, &mut workspace.output);
481 }
482 }
483 Ok(())
484}
485
486fn validate_output_len(output: &[u8], width: usize, height: usize) -> VisionResult<()> {
487 let len = width
488 .checked_mul(height)
489 .ok_or_else(|| VisionError::InvalidDimensions("morphology output size overflows".into()))?;
490 if output.len() != len {
491 return Err(VisionError::ShapeMismatch(format!(
492 "morphology output needs {len} elements, found {}",
493 output.len()
494 )));
495 }
496 Ok(())
497}
498
499fn pack_u8_into(input: ImageView<'_, u8, 1>, output: &mut [u8]) {
500 for y in 0..input.height() {
501 let start = y * input.width();
502 output[start..start + input.width()]
503 .copy_from_slice(input.row(y).expect("input row in bounds"));
504 }
505}
506
507fn copy_subtract_input(input: ImageView<'_, u8, 1>, high: &[u8], output: &mut [u8]) {
508 for y in 0..input.height() {
509 let start = y * input.width();
510 for ((value, &closed), &original) in output[start..start + input.width()]
511 .iter_mut()
512 .zip(&high[start..start + input.width()])
513 .zip(input.row(y).expect("input row in bounds"))
514 {
515 *value = closed.saturating_sub(original);
516 }
517 }
518}
519
520impl RectMorphologyWorkspace {
521 #[allow(clippy::too_many_arguments)]
522 fn apply(
523 &mut self,
524 input: &[u8],
525 width: usize,
526 height: usize,
527 kernel_width: usize,
528 kernel_height: usize,
529 anchor_x: usize,
530 anchor_y: usize,
531 border: BorderMode<u8, 1>,
532 extreme: Extreme,
533 ) {
534 let len = width * height;
535 self.centered_5x5_active = false;
536 if kernel_width == 5
537 && kernel_height == 5
538 && anchor_x == 2
539 && anchor_y == 2
540 && matches!(border, BorderMode::Replicate)
541 {
542 self.apply_centered_5x5(input, width, height, extreme);
543 return;
544 }
545 if should_parallelize(len, width.max(height), LARGE_PARALLEL_COMPONENTS) {
546 self.apply_parallel(
547 input,
548 width,
549 height,
550 kernel_width,
551 kernel_height,
552 anchor_x,
553 anchor_y,
554 border,
555 extreme,
556 );
557 return;
558 }
559 self.horizontal.resize(len, 0);
560 for y in 0..height {
561 let start = y * width;
562 filter_line(
563 &input[start..start + width],
564 &mut self.horizontal[start..start + width],
565 kernel_width,
566 anchor_x,
567 border,
568 extreme,
569 &mut self.padded,
570 &mut self.prefix,
571 &mut self.suffix,
572 );
573 }
574
575 self.transposed.resize(len, 0);
576 transpose_blocked(&self.horizontal, &mut self.transposed, width, height);
577 self.filtered.resize(len, 0);
578 for x in 0..width {
579 let start = x * height;
580 filter_line(
581 &self.transposed[start..start + height],
582 &mut self.filtered[start..start + height],
583 kernel_height,
584 anchor_y,
585 border,
586 extreme,
587 &mut self.padded,
588 &mut self.prefix,
589 &mut self.suffix,
590 );
591 }
592 self.output.resize(len, 0);
593 transpose_blocked(&self.filtered, &mut self.output, height, width);
594 }
595
596 fn apply_centered_5x5(&mut self, input: &[u8], width: usize, height: usize, extreme: Extreme) {
597 let len = width * height;
598 self.centered_5x5_active = true;
599 self.horizontal.resize(len, 0);
600 if should_parallelize(len, width.max(height), LARGE_PARALLEL_COMPONENTS) {
601 let workers = bounded_workers(
602 len,
603 width.max(height),
604 LARGE_PARALLEL_COMPONENTS,
605 rayon::current_num_threads(),
606 );
607 self.line_buffers.resize_with(workers, LineBuffers::default);
608 let line_len = width.max(height) + 4;
609 for buffers in &mut self.line_buffers {
610 buffers.padded.reserve(line_len.saturating_sub(buffers.padded.len()));
611 buffers.prefix.reserve(line_len.saturating_sub(buffers.prefix.len()));
612 buffers.suffix.reserve(line_len.saturating_sub(buffers.suffix.len()));
613 }
614 }
615 let arch = Arch::new();
616 if should_parallelize(len, height, LARGE_PARALLEL_COMPONENTS) {
617 self.horizontal.par_chunks_mut(width * ROWS_PER_TILE).enumerate().for_each(
618 |(block, outputs)| {
619 arch.dispatch(|| {
620 for (row, output) in outputs.chunks_mut(width).enumerate() {
621 let y = block * ROWS_PER_TILE + row;
622 filter_centered_5_replicate(
623 &input[y * width..(y + 1) * width],
624 output,
625 extreme,
626 );
627 }
628 });
629 },
630 );
631 } else {
632 arch.dispatch(|| {
633 for (source, output) in input.chunks(width).zip(self.horizontal.chunks_mut(width)) {
634 filter_centered_5_replicate(source, output, extreme);
635 }
636 });
637 }
638
639 self.output.resize(len, 0);
640 if should_parallelize(len, height, LARGE_PARALLEL_COMPONENTS) {
641 self.output.par_chunks_mut(width * ROWS_PER_TILE).enumerate().for_each(
642 |(block, outputs)| {
643 arch.dispatch(|| {
644 for (row, output) in outputs.chunks_mut(width).enumerate() {
645 filter_vertical_centered_5_replicate(
646 &self.horizontal,
647 width,
648 height,
649 block * ROWS_PER_TILE + row,
650 output,
651 extreme,
652 );
653 }
654 });
655 },
656 );
657 } else {
658 arch.dispatch(|| {
659 for (y, output) in self.output.chunks_mut(width).enumerate() {
660 filter_vertical_centered_5_replicate(
661 &self.horizontal,
662 width,
663 height,
664 y,
665 output,
666 extreme,
667 );
668 }
669 });
670 }
671 }
672
673 #[allow(clippy::too_many_arguments)]
674 fn apply_parallel(
675 &mut self,
676 input: &[u8],
677 width: usize,
678 height: usize,
679 kernel_width: usize,
680 kernel_height: usize,
681 anchor_x: usize,
682 anchor_y: usize,
683 border: BorderMode<u8, 1>,
684 extreme: Extreme,
685 ) {
686 let len = width * height;
687 let workers = bounded_workers(
688 len,
689 width.max(height),
690 LARGE_PARALLEL_COMPONENTS,
691 rayon::current_num_threads(),
692 );
693 self.line_buffers.resize_with(workers, LineBuffers::default);
694 self.horizontal.resize(len, 0);
695 let horizontal_rows = items_per_worker(height, workers);
696 self.horizontal
697 .par_chunks_mut(horizontal_rows * width)
698 .zip(self.line_buffers.par_iter_mut())
699 .enumerate()
700 .for_each(|(chunk, (outputs, buffers))| {
701 let first_y = chunk * horizontal_rows;
702 for (local_y, output) in outputs.chunks_mut(width).enumerate() {
703 let start = (first_y + local_y) * width;
704 filter_line(
705 &input[start..start + width],
706 output,
707 kernel_width,
708 anchor_x,
709 border,
710 extreme,
711 &mut buffers.padded,
712 &mut buffers.prefix,
713 &mut buffers.suffix,
714 );
715 }
716 });
717
718 self.transposed.resize(len, 0);
719 self.transposed.par_chunks_mut(height).enumerate().for_each(|(x, output)| {
720 for (y, value) in output.iter_mut().enumerate() {
721 *value = self.horizontal[y * width + x];
722 }
723 });
724 self.filtered.resize(len, 0);
725 let vertical_rows = items_per_worker(width, workers);
726 self.filtered
727 .par_chunks_mut(vertical_rows * height)
728 .zip(self.line_buffers.par_iter_mut())
729 .enumerate()
730 .for_each(|(chunk, (outputs, buffers))| {
731 let first_x = chunk * vertical_rows;
732 for (local_x, output) in outputs.chunks_mut(height).enumerate() {
733 let start = (first_x + local_x) * height;
734 filter_line(
735 &self.transposed[start..start + height],
736 output,
737 kernel_height,
738 anchor_y,
739 border,
740 extreme,
741 &mut buffers.padded,
742 &mut buffers.prefix,
743 &mut buffers.suffix,
744 );
745 }
746 });
747 self.output.resize(len, 0);
748 self.output.par_chunks_mut(width).enumerate().for_each(|(y, output)| {
749 for (x, value) in output.iter_mut().enumerate() {
750 *value = self.filtered[x * height + y];
751 }
752 });
753 }
754}
755
756fn filter_centered_5_replicate(input: &[u8], output: &mut [u8], extreme: Extreme) {
757 if input.len() < 5 {
758 for (x, value) in output.iter_mut().enumerate() {
759 let mut result = input[x.saturating_sub(2)];
760 for offset in 1..5 {
761 let source = (x + offset).saturating_sub(2).min(input.len() - 1);
762 result = extreme_u8(result, input[source], extreme);
763 }
764 *value = result;
765 }
766 return;
767 }
768 output[0] = extreme_u8(extreme_u8(input[0], input[1], extreme), input[2], extreme);
769 output[1] = extreme_u8(
770 extreme_u8(extreme_u8(input[0], input[1], extreme), input[2], extreme),
771 input[3],
772 extreme,
773 );
774 for x in 2..input.len() - 2 {
775 let left = extreme_u8(input[x - 2], input[x - 1], extreme);
776 let middle = extreme_u8(input[x], input[x + 1], extreme);
777 output[x] = extreme_u8(extreme_u8(left, middle, extreme), input[x + 2], extreme);
778 }
779 let last = input.len() - 1;
780 output[last - 1] = extreme_u8(
781 extreme_u8(extreme_u8(input[last - 3], input[last - 2], extreme), input[last - 1], extreme),
782 input[last],
783 extreme,
784 );
785 output[last] =
786 extreme_u8(extreme_u8(input[last - 2], input[last - 1], extreme), input[last], extreme);
787}
788
789fn filter_vertical_centered_5_replicate(
790 input: &[u8],
791 width: usize,
792 height: usize,
793 y: usize,
794 output: &mut [u8],
795 extreme: Extreme,
796) {
797 let y0 = y.saturating_sub(2);
798 let y1 = y.saturating_sub(1);
799 let y3 = (y + 1).min(height - 1);
800 let y4 = (y + 2).min(height - 1);
801 let row0 = &input[y0 * width..(y0 + 1) * width];
802 let row1 = &input[y1 * width..(y1 + 1) * width];
803 let row2 = &input[y * width..(y + 1) * width];
804 let row3 = &input[y3 * width..(y3 + 1) * width];
805 let row4 = &input[y4 * width..(y4 + 1) * width];
806 for x in 0..width {
807 let upper = extreme_u8(row0[x], row1[x], extreme);
808 let lower = extreme_u8(row3[x], row4[x], extreme);
809 output[x] = extreme_u8(extreme_u8(upper, row2[x], extreme), lower, extreme);
810 }
811}
812
813#[derive(Debug, Default)]
814struct LineBuffers {
815 padded: Vec<u8>,
816 prefix: Vec<u8>,
817 suffix: Vec<u8>,
818}
819
820#[allow(clippy::too_many_arguments)]
821fn filter_line(
822 input: &[u8],
823 output: &mut [u8],
824 kernel: usize,
825 anchor: usize,
826 border: BorderMode<u8, 1>,
827 extreme: Extreme,
828 padded: &mut Vec<u8>,
829 prefix: &mut Vec<u8>,
830 suffix: &mut Vec<u8>,
831) {
832 if input.is_empty() {
833 return;
834 }
835 let padded_len = input.len() + kernel - 1;
836 padded.resize(padded_len, 0);
837 prefix.resize(padded_len, 0);
838 suffix.resize(padded_len, 0);
839 let constant = match border {
840 BorderMode::Constant([value]) => value,
841 _ => 0,
842 };
843 for (index, value) in padded.iter_mut().enumerate() {
844 let source = index as isize - anchor as isize;
845 *value = map_index(source, input.len(), border).map_or(constant, |mapped| input[mapped]);
846 }
847
848 for block_start in (0..padded_len).step_by(kernel) {
849 let block_end = (block_start + kernel).min(padded_len);
850 prefix[block_start] = padded[block_start];
851 for index in block_start + 1..block_end {
852 prefix[index] = extreme_u8(prefix[index - 1], padded[index], extreme);
853 }
854 suffix[block_end - 1] = padded[block_end - 1];
855 for index in (block_start..block_end - 1).rev() {
856 suffix[index] = extreme_u8(suffix[index + 1], padded[index], extreme);
857 }
858 }
859 for (index, value) in output.iter_mut().enumerate() {
860 *value = extreme_u8(suffix[index], prefix[index + kernel - 1], extreme);
861 }
862}
863
864#[inline(always)]
865fn extreme_u8(left: u8, right: u8, extreme: Extreme) -> u8 {
866 match extreme {
867 Extreme::Minimum => left.min(right),
868 Extreme::Maximum => left.max(right),
869 }
870}
871
872fn transpose_blocked(input: &[u8], output: &mut [u8], width: usize, height: usize) {
873 const BLOCK: usize = 32;
874 for y0 in (0..height).step_by(BLOCK) {
875 for x0 in (0..width).step_by(BLOCK) {
876 let y1 = (y0 + BLOCK).min(height);
877 let x1 = (x0 + BLOCK).min(width);
878 for y in y0..y1 {
879 for x in x0..x1 {
880 output[x * height + y] = input[y * width + x];
881 }
882 }
883 }
884 }
885}
886
887fn repeat_extreme<T: PixelComponent, const CHANNELS: usize>(
888 input: ImageView<'_, T, CHANNELS>,
889 element: &StructuringElement,
890 iterations: usize,
891 border: BorderMode<T, CHANNELS>,
892 extreme: Extreme,
893) -> VisionResult<Image<T, CHANNELS>> {
894 let mut output = pack(input)?;
895 for _ in 0..iterations {
896 output = extreme_once(output.view(), element, border, extreme)?;
897 }
898 Ok(output)
899}
900
901fn extreme_once<T: PixelComponent, const CHANNELS: usize>(
902 input: ImageView<'_, T, CHANNELS>,
903 element: &StructuringElement,
904 border: BorderMode<T, CHANNELS>,
905 extreme: Extreme,
906) -> VisionResult<Image<T, CHANNELS>> {
907 let mut output = Vec::with_capacity(input.width() * input.height() * CHANNELS);
908 for y in 0..input.height() {
909 for x in 0..input.width() {
910 let mut values = [0.0; CHANNELS];
911 let mut initialized = false;
912 for ey in 0..element.height {
913 for ex in 0..element.width {
914 if !element.mask[ey * element.width + ex] {
915 continue;
916 }
917 let pixel = fetch(
918 input,
919 x as isize + ex as isize - element.anchor_x as isize,
920 y as isize + ey as isize - element.anchor_y as isize,
921 border,
922 );
923 if !initialized {
924 values = pixel.map(PixelComponent::to_f64);
925 initialized = true;
926 } else {
927 for channel in 0..CHANNELS {
928 let candidate = pixel[channel].to_f64();
929 let ordering = candidate.total_cmp(&values[channel]);
930 if matches!(extreme, Extreme::Minimum) && ordering.is_lt()
931 || matches!(extreme, Extreme::Maximum) && ordering.is_gt()
932 {
933 values[channel] = candidate;
934 }
935 }
936 }
937 }
938 }
939 output.extend(values.map(T::from_f64));
940 }
941 }
942 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
943}
944
945fn subtract<T: PixelComponent, const CHANNELS: usize>(
946 left: ImageView<'_, T, CHANNELS>,
947 right: ImageView<'_, T, CHANNELS>,
948) -> VisionResult<Image<T, CHANNELS>> {
949 if left.width() != right.width() || left.height() != right.height() {
950 return Err(VisionError::ShapeMismatch(
951 "morphology subtraction dimensions must match".into(),
952 ));
953 }
954 let mut output = Vec::with_capacity(left.width() * left.height() * CHANNELS);
955 for y in 0..left.height() {
956 for x in 0..left.width() {
957 let a = left.get(x, y).expect("left coordinate in bounds");
958 let b = right.get(x, y).expect("right coordinate in bounds");
959 output.extend(std::array::from_fn::<_, CHANNELS, _>(|channel| {
960 T::from_f64(a[channel].to_f64() - b[channel].to_f64())
961 }));
962 }
963 }
964 Ok(Image::try_new_with_metadata(left.width(), left.height(), output, left.metadata())?)
965}
966
967fn pack<T: PixelComponent, const CHANNELS: usize>(
968 input: ImageView<'_, T, CHANNELS>,
969) -> VisionResult<Image<T, CHANNELS>> {
970 let mut output = Vec::with_capacity(input.width() * input.height() * CHANNELS);
971 for y in 0..input.height() {
972 for x in 0..input.width() {
973 output.extend_from_slice(input.get(x, y).expect("input coordinate in bounds"));
974 }
975 }
976 Ok(Image::try_new_with_metadata(input.width(), input.height(), output, input.metadata())?)
977}
978
979fn validate_dimensions(
980 width: usize,
981 height: usize,
982 anchor_x: usize,
983 anchor_y: usize,
984) -> VisionResult<()> {
985 if width == 0 || height == 0 {
986 return Err(VisionError::InvalidParameter(
987 "structuring element dimensions must be non-zero".into(),
988 ));
989 }
990 if anchor_x >= width || anchor_y >= height {
991 return Err(VisionError::InvalidParameter(format!(
992 "structuring element anchor ({anchor_x}, {anchor_y}) is outside {width}x{height}"
993 )));
994 }
995 width
996 .checked_mul(height)
997 .ok_or_else(|| VisionError::InvalidParameter("structuring element overflows".into()))?;
998 Ok(())
999}
1000
1001fn fill_ellipse(mask: &mut [bool], width: usize, height: usize) {
1002 let center_x = width / 2;
1003 let center_y = height / 2;
1004 let radius_y = center_y.max(1) as f64;
1005 for y in 0..height {
1006 let dy = y.abs_diff(center_y) as f64;
1007 if dy > radius_y {
1008 continue;
1009 }
1010 let extent =
1011 (center_x as f64 * (1.0 - dy * dy / (radius_y * radius_y)).sqrt()).round() as usize;
1012 let start = center_x.saturating_sub(extent);
1013 let end = (center_x + extent).min(width - 1);
1014 for x in start..=end {
1015 mask[y * width + x] = true;
1016 }
1017 }
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022 use super::{
1023 dilate, dilate_rect_u8, erode, erode_rect_u8, morphology_ex, morphology_rect_u8,
1024 morphology_rect_u8_into, MorphologyOperation, MorphologyShape, RectMorphologyWorkspace,
1025 StructuringElement,
1026 };
1027 use crate::BorderMode;
1028 use spatialrust_image::{Image, ImageRegion};
1029
1030 #[test]
1031 fn built_in_masks_have_expected_3x3_layouts() {
1032 let rect = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1033 let cross = StructuringElement::try_new(MorphologyShape::Cross, 3, 3).unwrap();
1034 let ellipse = StructuringElement::try_new(MorphologyShape::Ellipse, 3, 3).unwrap();
1035 let diamond = StructuringElement::try_new(MorphologyShape::Diamond, 3, 3).unwrap();
1036 assert_eq!(rect.mask().iter().filter(|&&v| v).count(), 9);
1037 assert_eq!(cross.mask().iter().filter(|&&v| v).count(), 5);
1038 assert_eq!(ellipse.mask().iter().filter(|&&v| v).count(), 5);
1039 assert_eq!(diamond.mask().iter().filter(|&&v| v).count(), 5);
1040 }
1041
1042 #[test]
1043 fn erode_and_dilate_match_known_extrema_on_roi() {
1044 let parent = Image::<u8, 1>::try_new(5, 3, (0..15).collect()).unwrap();
1045 let roi = parent.view().subview(ImageRegion::new(1, 0, 3, 3)).unwrap();
1046 let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1047 let low = erode(roi, &element, 1, BorderMode::Replicate).unwrap();
1048 let high = dilate(roi, &element, 1, BorderMode::Replicate).unwrap();
1049 assert_eq!(low[(1, 1)][0], 1);
1050 assert_eq!(high[(1, 1)][0], 13);
1051 }
1052
1053 #[test]
1054 fn opening_removes_isolated_impulse() {
1055 let image = Image::<u16, 1>::try_new(3, 3, vec![0, 0, 0, 0, 100, 0, 0, 0, 0]).unwrap();
1056 let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1057 let output = morphology_ex(
1058 image.view(),
1059 MorphologyOperation::Open,
1060 &element,
1061 1,
1062 BorderMode::Constant([0]),
1063 )
1064 .unwrap();
1065 assert!(output.as_slice().iter().all(|&value| value == 0));
1066 }
1067
1068 #[test]
1069 fn gradient_is_dilate_minus_erode_for_float() {
1070 let image = Image::<f32, 1>::try_new(3, 1, vec![1.0, 4.0, 9.0]).unwrap();
1071 let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 1).unwrap();
1072 let output = morphology_ex(
1073 image.view(),
1074 MorphologyOperation::Gradient,
1075 &element,
1076 1,
1077 BorderMode::Replicate,
1078 )
1079 .unwrap();
1080 assert_eq!(output.as_slice(), &[3.0, 8.0, 5.0]);
1081 }
1082
1083 #[test]
1084 fn zero_iterations_return_packed_copy() {
1085 let image = Image::<u8, 3>::from_pixel(2, 2, [1, 2, 3]).unwrap();
1086 let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1087 assert_eq!(erode(image.view(), &element, 0, BorderMode::Replicate).unwrap(), image);
1088 }
1089
1090 #[test]
1091 fn invalid_masks_are_rejected() {
1092 assert!(StructuringElement::try_from_mask(2, 2, 0, 0, vec![false; 4]).is_err());
1093 assert!(StructuringElement::try_from_mask(2, 2, 0, 0, vec![true; 3]).is_err());
1094 }
1095
1096 #[test]
1097 fn rectangular_u8_fast_path_matches_generic_for_anchors_borders_and_iterations() {
1098 let mut state = 0x9e37_79b9_u32;
1099 let pixels = (0..63)
1100 .map(|_| {
1101 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1102 (state >> 24) as u8
1103 })
1104 .collect();
1105 let image = Image::<u8, 1>::try_new(9, 7, pixels).unwrap();
1106 let elements = [
1107 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 1, 1, 0, 0).unwrap(),
1108 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 2, 0, 1).unwrap(),
1109 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 5, 3, 4, 0).unwrap(),
1110 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 11, 9, 5, 4).unwrap(),
1111 ];
1112 let borders = [
1113 BorderMode::Constant([37]),
1114 BorderMode::Replicate,
1115 BorderMode::Reflect,
1116 BorderMode::Reflect101,
1117 BorderMode::Wrap,
1118 ];
1119
1120 for element in &elements {
1121 for &border in &borders {
1122 for iterations in 0..=2 {
1123 assert_eq!(
1124 erode_rect_u8(image.view(), element, iterations, border).unwrap(),
1125 erode(image.view(), element, iterations, border).unwrap()
1126 );
1127 assert_eq!(
1128 dilate_rect_u8(image.view(), element, iterations, border).unwrap(),
1129 dilate(image.view(), element, iterations, border).unwrap()
1130 );
1131 }
1132 }
1133 }
1134 }
1135
1136 #[test]
1137 fn rectangular_u8_composites_match_generic() {
1138 let image = Image::<u8, 1>::try_new(
1139 7,
1140 5,
1141 (0..35).map(|index| ((index * 47 + index * index * 3) % 256) as u8).collect(),
1142 )
1143 .unwrap();
1144 let element =
1145 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 3, 1, 2).unwrap();
1146 for operation in [
1147 MorphologyOperation::Open,
1148 MorphologyOperation::Close,
1149 MorphologyOperation::Gradient,
1150 MorphologyOperation::TopHat,
1151 MorphologyOperation::BlackHat,
1152 ] {
1153 for iterations in 0..=2 {
1154 assert_eq!(
1155 morphology_rect_u8(
1156 image.view(),
1157 operation,
1158 &element,
1159 iterations,
1160 BorderMode::Replicate,
1161 )
1162 .unwrap(),
1163 morphology_ex(
1164 image.view(),
1165 operation,
1166 &element,
1167 iterations,
1168 BorderMode::Replicate,
1169 )
1170 .unwrap()
1171 );
1172 }
1173 }
1174 }
1175
1176 #[test]
1177 fn rectangular_fast_path_rejects_sparse_masks() {
1178 let image = Image::<u8, 1>::from_pixel(3, 3, [1]).unwrap();
1179 let cross = StructuringElement::try_new(MorphologyShape::Cross, 3, 3).unwrap();
1180 assert!(erode_rect_u8(image.view(), &cross, 1, BorderMode::Replicate).is_err());
1181 }
1182
1183 #[test]
1184 fn rectangular_into_matches_allocating_path_for_strided_input() {
1185 let parent = Image::<u8, 1>::try_new(
1186 13,
1187 9,
1188 (0..117).map(|index| ((index * 61 + 17) & 255) as u8).collect(),
1189 )
1190 .unwrap();
1191 let input = parent.view().subview(ImageRegion::new(2, 1, 9, 7)).unwrap();
1192 let element =
1193 StructuringElement::try_new_with_anchor(MorphologyShape::Rect, 4, 6, 1, 4).unwrap();
1194 let mut workspace = RectMorphologyWorkspace::new();
1195 let mut output = vec![0; input.width() * input.height()];
1196
1197 for operation in [
1198 MorphologyOperation::Open,
1199 MorphologyOperation::Close,
1200 MorphologyOperation::Gradient,
1201 MorphologyOperation::TopHat,
1202 MorphologyOperation::BlackHat,
1203 ] {
1204 morphology_rect_u8_into(
1205 input,
1206 operation,
1207 &element,
1208 2,
1209 BorderMode::Reflect101,
1210 &mut output,
1211 &mut workspace,
1212 )
1213 .unwrap();
1214 assert_eq!(
1215 output,
1216 morphology_rect_u8(input, operation, &element, 2, BorderMode::Reflect101,)
1217 .unwrap()
1218 .into_vec()
1219 );
1220 }
1221 }
1222
1223 #[test]
1224 fn rectangular_workspace_reuses_full_image_and_worker_capacity() {
1225 let image = Image::<u8, 1>::try_new(
1226 1000,
1227 1000,
1228 (0..1_000_000).map(|index| ((index * 29 + index / 1000) & 255) as u8).collect(),
1229 )
1230 .unwrap();
1231 let element = StructuringElement::try_new(MorphologyShape::Rect, 5, 5).unwrap();
1232 let mut workspace = RectMorphologyWorkspace::new();
1233 let mut output = vec![0; 1_000_000];
1234 morphology_rect_u8_into(
1235 image.view(),
1236 MorphologyOperation::Open,
1237 &element,
1238 1,
1239 BorderMode::Replicate,
1240 &mut output,
1241 &mut workspace,
1242 )
1243 .unwrap();
1244 let capacity = workspace.capacity();
1245 let workers = workspace.worker_capacity();
1246 let line_capacity = workspace.line_capacity();
1247 assert!(capacity >= output.len());
1248 assert!(workers >= 1);
1249 assert!(line_capacity >= 1004);
1250
1251 morphology_rect_u8_into(
1252 image.view(),
1253 MorphologyOperation::Close,
1254 &element,
1255 1,
1256 BorderMode::Replicate,
1257 &mut output,
1258 &mut workspace,
1259 )
1260 .unwrap();
1261 assert_eq!(workspace.capacity(), capacity);
1262 assert_eq!(workspace.worker_capacity(), workers);
1263 assert_eq!(workspace.line_capacity(), line_capacity);
1264 }
1265
1266 #[test]
1267 fn rectangular_into_validates_output_length() {
1268 let image = Image::<u8, 1>::from_pixel(4, 3, [9]).unwrap();
1269 let element = StructuringElement::try_new(MorphologyShape::Rect, 3, 3).unwrap();
1270 let mut workspace = RectMorphologyWorkspace::new();
1271 assert!(morphology_rect_u8_into(
1272 image.view(),
1273 MorphologyOperation::Open,
1274 &element,
1275 1,
1276 BorderMode::Replicate,
1277 &mut [0; 11],
1278 &mut workspace,
1279 )
1280 .is_err());
1281 }
1282}