1use std::collections::BTreeMap;
4
5use rayon::prelude::*;
6use spatialrust_image::{ColorSpace, Image, ImageMetadata, ImageView};
7
8use crate::{BoundingBox2, VisionError, VisionResult};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
12pub enum Connectivity {
13 Four,
15 #[default]
17 Eight,
18}
19
20#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct BinaryMask {
23 image: Image<u8, 1>,
24}
25
26impl BinaryMask {
27 pub fn try_new(width: usize, height: usize, data: Vec<u8>) -> VisionResult<Self> {
29 if data.iter().any(|&value| value > 1) {
30 return Err(VisionError::InvalidParameter(
31 "binary mask values must be zero or one".to_owned(),
32 ));
33 }
34 let metadata = ImageMetadata { color_space: ColorSpace::Label, ..Default::default() };
35 Ok(Self { image: Image::try_new_with_metadata(width, height, data, metadata)? })
36 }
37
38 pub fn from_threshold(input: ImageView<'_, f32, 1>, threshold: f32) -> VisionResult<Self> {
40 if !threshold.is_finite() {
41 return Err(VisionError::InvalidParameter("mask threshold must be finite".to_owned()));
42 }
43 let mut data = Vec::with_capacity(input.width() * input.height());
44 for y in 0..input.height() {
45 for x in 0..input.width() {
46 let value = input.get(x, y).expect("coordinate in bounds")[0];
47 data.push(u8::from(value.is_finite() && value >= threshold));
48 }
49 }
50 Self::try_new(input.width(), input.height(), data)
51 }
52
53 #[must_use]
55 pub const fn width(&self) -> usize {
56 self.image.width()
57 }
58
59 #[must_use]
61 pub const fn height(&self) -> usize {
62 self.image.height()
63 }
64
65 #[must_use]
67 pub fn image(&self) -> &Image<u8, 1> {
68 &self.image
69 }
70
71 #[must_use]
73 pub fn view(&self) -> ImageView<'_, u8, 1> {
74 self.image.view()
75 }
76
77 #[must_use]
79 pub fn contains(&self, x: usize, y: usize) -> bool {
80 self.image.get(x, y).is_some_and(|pixel| pixel[0] != 0)
81 }
82
83 #[must_use]
85 pub fn area(&self) -> usize {
86 self.image.as_slice().iter().filter(|&&value| value != 0).count()
87 }
88
89 #[must_use]
91 pub fn into_image(self) -> Image<u8, 1> {
92 self.image
93 }
94}
95
96pub fn distance_transform_edt(mask: &BinaryMask) -> VisionResult<Image<f32, 1>> {
106 distance_transform_edt_with_spacing(mask, 1.0, 1.0)
107}
108
109#[derive(Debug, Default)]
115pub struct DistanceTransformWorkspace {
116 horizontal: Vec<u16>,
117 transposed: Vec<u16>,
118 column_major: Vec<f32>,
119}
120
121impl DistanceTransformWorkspace {
122 #[must_use]
124 pub const fn new() -> Self {
125 Self { horizontal: Vec::new(), transposed: Vec::new(), column_major: Vec::new() }
126 }
127
128 #[must_use]
130 pub fn capacity(&self) -> usize {
131 self.horizontal.capacity().min(self.transposed.capacity()).min(self.column_major.capacity())
132 }
133
134 fn prepare(&mut self, len: usize) {
135 self.horizontal.resize(len, u16::MAX);
136 self.transposed.resize(len, 0);
137 self.column_major.resize(len, 0.0);
138 }
139}
140
141pub fn distance_transform_edt_into(
147 mask: &BinaryMask,
148 output: &mut [f32],
149 workspace: &mut DistanceTransformWorkspace,
150) -> VisionResult<()> {
151 distance_transform_edt_u8_into(
152 mask.image().as_slice(),
153 mask.width(),
154 mask.height(),
155 output,
156 workspace,
157 )
158}
159
160pub fn distance_transform_edt_u8_into(
166 input: &[u8],
167 width: usize,
168 height: usize,
169 output: &mut [f32],
170 workspace: &mut DistanceTransformWorkspace,
171) -> VisionResult<()> {
172 let len = width.checked_mul(height).ok_or_else(|| {
173 VisionError::InvalidDimensions("distance-transform size overflows".into())
174 })?;
175 if input.len() != len {
176 return Err(VisionError::ShapeMismatch(format!(
177 "distance-transform input needs {len} elements, found {}",
178 input.len()
179 )));
180 }
181 if output.len() != len {
182 return Err(VisionError::ShapeMismatch(format!(
183 "distance-transform output needs {len} elements, found {}",
184 output.len()
185 )));
186 }
187 if len == 0 {
188 return Ok(());
189 }
190 if !input.contains(&0) {
191 return Err(VisionError::InvalidParameter(
192 "distance transform requires at least one background pixel".to_owned(),
193 ));
194 }
195 if width > u16::MAX as usize || height > u16::MAX as usize {
196 return Err(VisionError::InvalidDimensions(
197 "reusable unit distance transform supports dimensions up to 65535".to_owned(),
198 ));
199 }
200 workspace.prepare(len);
201 distance_transform_unit_into(input, width, height, workspace, output);
202 Ok(())
203}
204
205pub fn distance_transform_edt_with_spacing(
211 mask: &BinaryMask,
212 spacing_x: f32,
213 spacing_y: f32,
214) -> VisionResult<Image<f32, 1>> {
215 if !spacing_x.is_finite() || spacing_x <= 0.0 {
216 return Err(VisionError::InvalidParameter(
217 "distance-transform x spacing must be finite and positive".to_owned(),
218 ));
219 }
220 if !spacing_y.is_finite() || spacing_y <= 0.0 {
221 return Err(VisionError::InvalidParameter(
222 "distance-transform y spacing must be finite and positive".to_owned(),
223 ));
224 }
225
226 let width = mask.width();
227 let height = mask.height();
228 let len = width.checked_mul(height).ok_or_else(|| {
229 VisionError::InvalidDimensions("distance-transform size overflows".into())
230 })?;
231 if len == 0 {
232 return Ok(Image::try_new_with_metadata(
233 width,
234 height,
235 Vec::new(),
236 ImageMetadata { color_space: ColorSpace::Gray, ..Default::default() },
237 )?);
238 }
239 if mask.area() == len {
240 return Err(VisionError::InvalidParameter(
241 "distance transform requires at least one background pixel".to_owned(),
242 ));
243 }
244
245 if spacing_x == 1.0
246 && spacing_y == 1.0
247 && width <= u16::MAX as usize
248 && height <= u16::MAX as usize
249 {
250 let distances = distance_transform_unit(mask.image().as_slice(), width, height);
251 return Ok(Image::try_new_with_metadata(
252 width,
253 height,
254 distances,
255 ImageMetadata { color_space: ColorSpace::Gray, ..Default::default() },
256 )?);
257 }
258
259 let mut horizontal = vec![f64::INFINITY; len];
260 horizontal_binary_distance_rows(
261 mask.image().as_slice(),
262 width,
263 f64::from(spacing_x).powi(2),
264 &mut horizontal,
265 );
266
267 let mut transposed = vec![0.0_f64; len];
271 transpose_f64(&horizontal, width, height, &mut transposed);
272 drop(horizontal);
273 let mut column_major = vec![0.0_f32; len];
274 vertical_distance_columns(&transposed, height, f64::from(spacing_y).powi(2), &mut column_major);
275 let mut distances = vec![0.0_f32; len];
276 transpose_f32(&column_major, height, width, &mut distances);
277 Ok(Image::try_new_with_metadata(
278 width,
279 height,
280 distances,
281 ImageMetadata { color_space: ColorSpace::Gray, ..Default::default() },
282 )?)
283}
284
285fn distance_transform_unit(mask: &[u8], width: usize, height: usize) -> Vec<f32> {
286 let mut workspace = DistanceTransformWorkspace::new();
287 workspace.prepare(mask.len());
288 let mut distances = vec![0.0_f32; mask.len()];
289 distance_transform_unit_into(mask, width, height, &mut workspace, &mut distances);
290 distances
291}
292
293fn distance_transform_unit_into(
294 mask: &[u8],
295 width: usize,
296 height: usize,
297 workspace: &mut DistanceTransformWorkspace,
298 distances: &mut [f32],
299) {
300 let all_rows_have_background =
301 horizontal_binary_distance_rows_u16(mask, width, &mut workspace.horizontal);
302 transpose_u16(&workspace.horizontal, width, height, &mut workspace.transposed);
303 vertical_distance_columns_u16(
304 &workspace.transposed,
305 height,
306 all_rows_have_background,
307 &mut workspace.column_major,
308 );
309 transpose_f32(&workspace.column_major, height, width, distances);
310}
311
312fn horizontal_binary_distance_rows_u16(mask: &[u8], width: usize, output: &mut [u16]) -> bool {
313 const PARALLEL_THRESHOLD: usize = 128 * 1024;
314 let height = mask.len() / width;
315 let threads = worker_count(height);
316 if threads == 1 || mask.len() < PARALLEL_THRESHOLD {
317 return horizontal_binary_distance_rows_u16_serial(mask, width, output);
318 }
319 let rows_per_worker = height.div_ceil(threads);
320 mask.par_chunks(rows_per_worker * width)
321 .zip(output.par_chunks_mut(rows_per_worker * width))
322 .map(|(worker_mask, worker_output)| {
323 horizontal_binary_distance_rows_u16_serial(worker_mask, width, worker_output)
324 })
325 .all(std::convert::identity)
326}
327
328fn horizontal_binary_distance_rows_u16_serial(
329 mask: &[u8],
330 width: usize,
331 output: &mut [u16],
332) -> bool {
333 let mut all_rows_have_background = true;
334 for (mask_row, output_row) in mask.chunks_exact(width).zip(output.chunks_exact_mut(width)) {
335 let Some(first_background) = mask_row.iter().position(|&value| value == 0) else {
336 output_row.fill(u16::MAX);
337 all_rows_have_background = false;
338 continue;
339 };
340 for (x, value) in output_row[..first_background].iter_mut().enumerate() {
341 *value = (first_background - x) as u16;
342 }
343 output_row[first_background] = 0;
344 let mut distance = 0_u16;
345 for x in first_background + 1..width {
346 if mask_row[x] == 0 {
347 distance = 0;
348 } else {
349 distance += 1;
350 }
351 output_row[x] = distance;
352 }
353
354 let last_background =
355 mask_row.iter().rposition(|&value| value == 0).expect("row has a background pixel");
356 distance = 0;
357 for x in (0..last_background).rev() {
358 if mask_row[x] == 0 {
359 distance = 0;
360 } else {
361 distance += 1;
362 }
363 output_row[x] = output_row[x].min(distance);
364 }
365 }
366 all_rows_have_background
367}
368
369fn transpose_u16(input: &[u16], width: usize, height: usize, output: &mut [u16]) {
370 const PARALLEL_THRESHOLD: usize = 128 * 1024;
371 let threads = worker_count(width);
372 if threads == 1 || input.len() < PARALLEL_THRESHOLD {
373 transpose_u16_columns(input, width, height, 0, output);
374 return;
375 }
376 let task_count = threads.saturating_mul(2).min(width);
378 let columns_per_worker = width.div_ceil(task_count);
379 output.par_chunks_mut(columns_per_worker * height).enumerate().for_each(
380 |(worker_index, worker_output)| {
381 let x_start = worker_index * columns_per_worker;
382 transpose_u16_columns(input, width, height, x_start, worker_output);
383 },
384 );
385}
386
387fn transpose_u16_columns(
388 input: &[u16],
389 width: usize,
390 height: usize,
391 x_start: usize,
392 output: &mut [u16],
393) {
394 const TILE: usize = 32;
395 let column_count = output.len() / height;
396 for y_start in (0..height).step_by(TILE) {
397 for local_x_start in (0..column_count).step_by(TILE) {
398 let y_end = (y_start + TILE).min(height);
399 let local_x_end = (local_x_start + TILE).min(column_count);
400 for y in y_start..y_end {
401 for local_x in local_x_start..local_x_end {
402 output[local_x * height + y] = input[y * width + x_start + local_x];
403 }
404 }
405 }
406 }
407}
408
409fn vertical_distance_columns_u16(
410 columns: &[u16],
411 height: usize,
412 all_finite: bool,
413 output: &mut [f32],
414) {
415 const PARALLEL_THRESHOLD: usize = 128 * 1024;
416 let width = columns.len() / height;
417 let threads = worker_count(width);
418 if threads == 1 || columns.len() < PARALLEL_THRESHOLD {
419 if all_finite {
420 vertical_distance_column_range_u16::<true>(columns, height, output);
421 } else {
422 vertical_distance_column_range_u16::<false>(columns, height, output);
423 }
424 return;
425 }
426 let task_count = threads.saturating_mul(2).min(width);
429 let columns_per_worker = width.div_ceil(task_count);
430 if all_finite {
431 vertical_distance_columns_u16_parallel::<true>(columns, height, columns_per_worker, output);
432 } else {
433 vertical_distance_columns_u16_parallel::<false>(
434 columns,
435 height,
436 columns_per_worker,
437 output,
438 );
439 }
440}
441
442fn vertical_distance_columns_u16_parallel<const ALL_FINITE: bool>(
443 columns: &[u16],
444 height: usize,
445 columns_per_worker: usize,
446 output: &mut [f32],
447) {
448 columns
449 .par_chunks(columns_per_worker * height)
450 .zip(output.par_chunks_mut(columns_per_worker * height))
451 .for_each(|(worker_input, worker_output)| {
452 vertical_distance_column_range_u16::<ALL_FINITE>(worker_input, height, worker_output);
453 });
454}
455
456fn vertical_distance_column_range_u16<const ALL_FINITE: bool>(
457 columns: &[u16],
458 height: usize,
459 output: &mut [f32],
460) {
461 let mut sites = vec![0_usize; height];
462 let mut boundaries = vec![0.0_f64; height.saturating_add(1)];
463 let mut heights = vec![0.0_f64; height];
464 for (input_column, output_column) in
465 columns.chunks_exact(height).zip(output.chunks_exact_mut(height))
466 {
467 distance_transform_1d_u16::<ALL_FINITE>(
468 input_column,
469 output_column,
470 &mut sites,
471 &mut boundaries,
472 &mut heights,
473 );
474 }
475}
476
477fn distance_transform_1d_u16<const ALL_FINITE: bool>(
478 input: &[u16],
479 output: &mut [f32],
480 sites: &mut [usize],
481 boundaries: &mut [f64],
482 heights: &mut [f64],
483) {
484 for (coordinate, (&horizontal, height)) in input.iter().zip(heights.iter_mut()).enumerate() {
485 let coordinate = coordinate as f64;
486 let horizontal = f64::from(horizontal);
487 *height = coordinate * coordinate + horizontal * horizontal;
488 }
489 let first = if ALL_FINITE {
490 0
491 } else {
492 input.iter().position(|&value| value != u16::MAX).expect("background exists")
493 };
494 let mut envelope_end = 0;
495 sites[0] = first;
496 boundaries[0] = f64::NEG_INFINITY;
497 boundaries[1] = f64::INFINITY;
498 for (q, &horizontal) in input.iter().enumerate().skip(first + 1) {
499 if !ALL_FINITE && horizontal == u16::MAX {
500 continue;
501 }
502 let mut intersection = parabola_intersection_u16(heights, q, sites[envelope_end]);
503 while envelope_end > 0 && intersection <= boundaries[envelope_end] {
504 envelope_end -= 1;
505 intersection = parabola_intersection_u16(heights, q, sites[envelope_end]);
506 }
507 envelope_end += 1;
508 sites[envelope_end] = q;
509 boundaries[envelope_end] = intersection;
510 boundaries[envelope_end + 1] = f64::INFINITY;
511 }
512 let mut envelope = 0;
513 for (q, distance) in output.iter_mut().enumerate() {
514 while boundaries[envelope + 1] < q as f64 {
515 envelope += 1;
516 }
517 let site = sites[envelope];
518 let delta = q.abs_diff(site) as u64;
519 let horizontal = u64::from(input[site]);
520 *distance = ((delta * delta + horizontal * horizontal) as f32).sqrt();
521 }
522}
523
524fn parabola_intersection_u16(heights: &[f64], right: usize, left: usize) -> f64 {
525 (heights[right] - heights[left]) / (2.0 * (right - left) as f64)
526}
527
528fn transpose_f64(input: &[f64], width: usize, height: usize, output: &mut [f64]) {
529 const PARALLEL_THRESHOLD: usize = 128 * 1024;
530 let threads = worker_count(width);
531 if threads == 1 || input.len() < PARALLEL_THRESHOLD {
532 transpose_f64_columns(input, width, height, 0, output);
533 return;
534 }
535 let columns_per_worker = width.div_ceil(threads);
536 output.par_chunks_mut(columns_per_worker * height).enumerate().for_each(
537 |(worker_index, worker_output)| {
538 let x_start = worker_index * columns_per_worker;
539 transpose_f64_columns(input, width, height, x_start, worker_output);
540 },
541 );
542}
543
544fn transpose_f64_columns(
545 input: &[f64],
546 width: usize,
547 height: usize,
548 x_start: usize,
549 output: &mut [f64],
550) {
551 const TILE: usize = 32;
552 let column_count = output.len() / height;
553 for y_start in (0..height).step_by(TILE) {
554 for local_x_start in (0..column_count).step_by(TILE) {
555 let y_end = (y_start + TILE).min(height);
556 let local_x_end = (local_x_start + TILE).min(column_count);
557 for y in y_start..y_end {
558 for local_x in local_x_start..local_x_end {
559 output[local_x * height + y] = input[y * width + x_start + local_x];
560 }
561 }
562 }
563 }
564}
565
566fn transpose_f32(input: &[f32], width: usize, height: usize, output: &mut [f32]) {
567 const PARALLEL_THRESHOLD: usize = 128 * 1024;
568 let threads = worker_count(width);
569 if threads == 1 || input.len() < PARALLEL_THRESHOLD {
570 transpose_f32_columns(input, width, height, 0, output);
571 return;
572 }
573 let columns_per_worker = width.div_ceil(threads);
574 output.par_chunks_mut(columns_per_worker * height).enumerate().for_each(
575 |(worker_index, worker_output)| {
576 let x_start = worker_index * columns_per_worker;
577 transpose_f32_columns(input, width, height, x_start, worker_output);
578 },
579 );
580}
581
582fn transpose_f32_columns(
583 input: &[f32],
584 width: usize,
585 height: usize,
586 x_start: usize,
587 output: &mut [f32],
588) {
589 const TILE: usize = 32;
590 let column_count = output.len() / height;
591 for y_start in (0..height).step_by(TILE) {
592 for local_x_start in (0..column_count).step_by(TILE) {
593 let y_end = (y_start + TILE).min(height);
594 let local_x_end = (local_x_start + TILE).min(column_count);
595 for y in y_start..y_end {
596 for local_x in local_x_start..local_x_end {
597 output[local_x * height + y] = input[y * width + x_start + local_x];
598 }
599 }
600 }
601 }
602}
603
604fn horizontal_binary_distance_rows(
605 mask: &[u8],
606 width: usize,
607 spacing_squared: f64,
608 output: &mut [f64],
609) {
610 const PARALLEL_THRESHOLD: usize = 128 * 1024;
611 let height = mask.len() / width;
612 let threads = worker_count(height);
613 if threads == 1 || mask.len() < PARALLEL_THRESHOLD {
614 horizontal_binary_distance_rows_serial(mask, width, spacing_squared, output);
615 return;
616 }
617 let rows_per_worker = height.div_ceil(threads);
618 mask.par_chunks(rows_per_worker * width)
619 .zip(output.par_chunks_mut(rows_per_worker * width))
620 .for_each(|(worker_mask, worker_output)| {
621 horizontal_binary_distance_rows_serial(
622 worker_mask,
623 width,
624 spacing_squared,
625 worker_output,
626 );
627 });
628}
629
630fn horizontal_binary_distance_rows_serial(
631 mask: &[u8],
632 width: usize,
633 spacing_squared: f64,
634 output: &mut [f64],
635) {
636 for (mask_row, output_row) in mask.chunks_exact(width).zip(output.chunks_exact_mut(width)) {
637 let mut last_background = None;
638 for x in 0..width {
639 if mask_row[x] == 0 {
640 last_background = Some(x);
641 output_row[x] = 0.0;
642 } else if let Some(background) = last_background {
643 let delta = (x - background) as f64;
644 output_row[x] = spacing_squared * delta * delta;
645 }
646 }
647
648 let mut next_background = None;
649 for x in (0..width).rev() {
650 if mask_row[x] == 0 {
651 next_background = Some(x);
652 } else if let Some(background) = next_background {
653 let delta = (background - x) as f64;
654 output_row[x] = output_row[x].min(spacing_squared * delta * delta);
655 }
656 }
657 }
658}
659
660fn vertical_distance_columns(
661 columns: &[f64],
662 height: usize,
663 spacing_squared: f64,
664 output: &mut [f32],
665) {
666 const PARALLEL_THRESHOLD: usize = 128 * 1024;
667 let width = columns.len() / height;
668 let threads = worker_count(width);
669 if threads == 1 || columns.len() < PARALLEL_THRESHOLD {
670 vertical_distance_column_range(columns, height, spacing_squared, output);
671 return;
672 }
673
674 let columns_per_worker = width.div_ceil(threads);
675 columns
676 .par_chunks(columns_per_worker * height)
677 .zip(output.par_chunks_mut(columns_per_worker * height))
678 .for_each(|(worker_input, worker_output)| {
679 vertical_distance_column_range(worker_input, height, spacing_squared, worker_output);
680 });
681}
682
683fn worker_count(independent_items: usize) -> usize {
684 rayon::current_num_threads().min(independent_items)
685}
686
687fn vertical_distance_column_range(
688 columns: &[f64],
689 height: usize,
690 spacing_squared: f64,
691 output: &mut [f32],
692) {
693 let mut sites = vec![0_usize; height];
694 let mut boundaries = vec![0.0_f64; height.saturating_add(1)];
695 for (input_column, output_column) in
696 columns.chunks_exact(height).zip(output.chunks_exact_mut(height))
697 {
698 distance_transform_1d_to_f32(
699 input_column,
700 spacing_squared,
701 output_column,
702 &mut sites,
703 &mut boundaries,
704 );
705 }
706}
707
708fn distance_transform_1d_to_f32(
709 input: &[f64],
710 coordinate_scale_squared: f64,
711 output: &mut [f32],
712 sites: &mut [usize],
713 boundaries: &mut [f64],
714) {
715 debug_assert_eq!(input.len(), output.len());
716 let Some(first) = input.iter().position(|value| value.is_finite()) else {
717 output.fill(f32::INFINITY);
718 return;
719 };
720 let mut envelope_end = 0;
721 sites[0] = first;
722 boundaries[0] = f64::NEG_INFINITY;
723 boundaries[1] = f64::INFINITY;
724 for q in first + 1..input.len() {
725 if !input[q].is_finite() {
726 continue;
727 }
728 let mut intersection =
729 parabola_intersection(input, coordinate_scale_squared, q, sites[envelope_end]);
730 while envelope_end > 0 && intersection <= boundaries[envelope_end] {
731 envelope_end -= 1;
732 intersection =
733 parabola_intersection(input, coordinate_scale_squared, q, sites[envelope_end]);
734 }
735 envelope_end += 1;
736 sites[envelope_end] = q;
737 boundaries[envelope_end] = intersection;
738 boundaries[envelope_end + 1] = f64::INFINITY;
739 }
740
741 let mut envelope = 0;
742 for (q, distance) in output.iter_mut().enumerate() {
743 while boundaries[envelope + 1] < q as f64 {
744 envelope += 1;
745 }
746 let site = sites[envelope];
747 let delta = q as f64 - site as f64;
748 *distance = coordinate_scale_squared.mul_add(delta * delta, input[site]).sqrt() as f32;
749 }
750}
751
752fn parabola_intersection(
753 input: &[f64],
754 coordinate_scale_squared: f64,
755 right: usize,
756 left: usize,
757) -> f64 {
758 let right_coordinate = right as f64;
759 let left_coordinate = left as f64;
760 let right_height =
761 coordinate_scale_squared.mul_add(right_coordinate * right_coordinate, input[right]);
762 let left_height =
763 coordinate_scale_squared.mul_add(left_coordinate * left_coordinate, input[left]);
764 (right_height - left_height)
765 / (2.0 * coordinate_scale_squared * (right_coordinate - left_coordinate))
766}
767
768#[derive(Clone, Debug, PartialEq, Eq)]
770pub struct LabelImage {
771 image: Image<u32, 1>,
772}
773
774impl LabelImage {
775 #[must_use]
777 pub const fn width(&self) -> usize {
778 self.image.width()
779 }
780
781 #[must_use]
783 pub const fn height(&self) -> usize {
784 self.image.height()
785 }
786
787 #[must_use]
789 pub fn as_slice(&self) -> &[u32] {
790 self.image.as_slice()
791 }
792
793 #[must_use]
795 pub fn get(&self, x: usize, y: usize) -> Option<u32> {
796 self.image.get(x, y).map(|pixel| pixel[0])
797 }
798
799 #[must_use]
801 pub fn image(&self) -> &Image<u32, 1> {
802 &self.image
803 }
804
805 #[must_use]
807 pub fn into_image(self) -> Image<u32, 1> {
808 self.image
809 }
810}
811
812#[derive(Clone, Copy, Debug, PartialEq)]
814pub struct ComponentStats {
815 pub label: u32,
817 pub area: usize,
819 pub bbox: BoundingBox2,
821 pub centroid: [f64; 2],
823}
824
825#[derive(Clone, Debug, PartialEq)]
827pub struct ConnectedComponents {
828 pub labels: LabelImage,
830 pub components: Vec<ComponentStats>,
832}
833
834pub fn connected_components(
836 mask: &BinaryMask,
837 connectivity: Connectivity,
838) -> VisionResult<ConnectedComponents> {
839 connected_components_u8(mask.width(), mask.height(), mask.image.as_slice(), connectivity)
840}
841
842pub fn connected_components_u8(
849 width: usize,
850 height: usize,
851 pixels: &[u8],
852 connectivity: Connectivity,
853) -> VisionResult<ConnectedComponents> {
854 let expected = width
855 .checked_mul(height)
856 .ok_or_else(|| VisionError::InvalidDimensions("mask dimensions overflow".to_owned()))?;
857 if pixels.len() != expected {
858 return Err(VisionError::InvalidDimensions(format!(
859 "mask storage has {} pixels, expected {expected}",
860 pixels.len()
861 )));
862 }
863 let mut labels = vec![0_u32; width * height];
864 let mut runs = Vec::new();
865 let mut previous_runs = 0..0;
866
867 for y in 0..height {
868 let current_start = runs.len();
869 let row = &pixels[y * width..(y + 1) * width];
870 let mut x = 0;
871 let mut previous_cursor = previous_runs.start;
872 while x < width {
873 while x < width && row[x] == 0 {
874 x += 1;
875 }
876 if x == width {
877 break;
878 }
879 let start = x;
880 while x < width && row[x] != 0 {
881 x += 1;
882 }
883 let end = x;
884 let run_index = runs.len();
885 runs.push(ComponentRun { y, start, end, parent: run_index });
886
887 while previous_cursor < previous_runs.end
888 && run_precedes(runs[previous_cursor], start, connectivity)
889 {
890 previous_cursor += 1;
891 }
892 let mut overlap = previous_cursor;
893 while overlap < previous_runs.end && !run_follows(runs[overlap], end, connectivity) {
894 union_component_runs(&mut runs, run_index, overlap);
895 overlap += 1;
896 }
897 }
898 previous_runs = current_start..runs.len();
899 }
900
901 let mut root_labels = vec![0_u32; runs.len()];
902 let mut accumulators = Vec::<ComponentAccumulator>::new();
903 let mut next_label = 1_u32;
904 for run_index in 0..runs.len() {
905 let run = runs[run_index];
906 let root = component_run_root(&mut runs, run_index);
907 let label = if root_labels[root] == 0 {
908 let label = next_label;
909 root_labels[root] = label;
910 accumulators.push(ComponentAccumulator::new(run));
911 next_label = next_label
912 .checked_add(1)
913 .ok_or_else(|| VisionError::InvalidDimensions("too many components".to_owned()))?;
914 label
915 } else {
916 root_labels[root]
917 };
918 labels[run.y * width + run.start..run.y * width + run.end].fill(label);
919 accumulators[label as usize - 1].include(run);
920 }
921
922 let components = accumulators
923 .into_iter()
924 .enumerate()
925 .map(|(index, accumulator)| accumulator.finish(index as u32 + 1))
926 .collect();
927 let metadata = ImageMetadata { color_space: ColorSpace::Label, ..Default::default() };
928 let image = Image::try_new_with_metadata(width, height, labels, metadata)?;
929 Ok(ConnectedComponents { labels: LabelImage { image }, components })
930}
931
932#[derive(Clone, Copy)]
933struct ComponentRun {
934 y: usize,
935 start: usize,
936 end: usize,
937 parent: usize,
938}
939
940fn run_precedes(run: ComponentRun, start: usize, connectivity: Connectivity) -> bool {
941 match connectivity {
942 Connectivity::Four => run.end <= start,
943 Connectivity::Eight => run.end < start,
944 }
945}
946
947fn run_follows(run: ComponentRun, end: usize, connectivity: Connectivity) -> bool {
948 match connectivity {
949 Connectivity::Four => run.start >= end,
950 Connectivity::Eight => run.start > end,
951 }
952}
953
954fn component_run_root(runs: &mut [ComponentRun], mut index: usize) -> usize {
955 let mut root = index;
956 while runs[root].parent != root {
957 root = runs[root].parent;
958 }
959 while runs[index].parent != index {
960 let parent = runs[index].parent;
961 runs[index].parent = root;
962 index = parent;
963 }
964 root
965}
966
967fn union_component_runs(runs: &mut [ComponentRun], left: usize, right: usize) {
968 let left_root = component_run_root(runs, left);
969 let right_root = component_run_root(runs, right);
970 if left_root != right_root {
971 let (root, child) =
972 if left_root < right_root { (left_root, right_root) } else { (right_root, left_root) };
973 runs[child].parent = root;
974 }
975}
976
977struct ComponentAccumulator {
978 area: usize,
979 min_x: usize,
980 min_y: usize,
981 max_x: usize,
982 max_y: usize,
983 sum_x: f64,
984 sum_y: f64,
985}
986
987impl ComponentAccumulator {
988 fn new(run: ComponentRun) -> Self {
989 Self {
990 area: 0,
991 min_x: run.start,
992 min_y: run.y,
993 max_x: run.end,
994 max_y: run.y + 1,
995 sum_x: 0.0,
996 sum_y: 0.0,
997 }
998 }
999
1000 fn include(&mut self, run: ComponentRun) {
1001 let length = run.end - run.start;
1002 self.area += length;
1003 self.min_x = self.min_x.min(run.start);
1004 self.min_y = self.min_y.min(run.y);
1005 self.max_x = self.max_x.max(run.end);
1006 self.max_y = self.max_y.max(run.y + 1);
1007 self.sum_x += length as f64 * (run.start as f64 + run.end as f64) * 0.5;
1008 self.sum_y += length as f64 * (run.y as f64 + 0.5);
1009 }
1010
1011 fn finish(self, label: u32) -> ComponentStats {
1012 ComponentStats {
1013 label,
1014 area: self.area,
1015 bbox: BoundingBox2 {
1016 x_min: self.min_x as f32,
1017 y_min: self.min_y as f32,
1018 x_max: self.max_x as f32,
1019 y_max: self.max_y as f32,
1020 },
1021 centroid: [self.sum_x / self.area as f64, self.sum_y / self.area as f64],
1022 }
1023 }
1024}
1025
1026#[derive(Clone, Debug, PartialEq, Eq)]
1028pub struct Contour {
1029 pub points: Vec<[i32; 2]>,
1031}
1032
1033pub fn find_contours(mask: &BinaryMask) -> Vec<Contour> {
1035 type Point = (i32, i32);
1036 let mut edges: BTreeMap<Point, Vec<Point>> = BTreeMap::new();
1037 let foreground = |x: isize, y: isize| {
1038 x >= 0
1039 && y >= 0
1040 && (x as usize) < mask.width()
1041 && (y as usize) < mask.height()
1042 && mask.contains(x as usize, y as usize)
1043 };
1044 for y in 0..mask.height() as isize {
1045 for x in 0..mask.width() as isize {
1046 if !foreground(x, y) {
1047 continue;
1048 }
1049 let x = x as i32;
1050 let y = y as i32;
1051 if !foreground(x as isize, y as isize - 1) {
1052 add_edge(&mut edges, (x, y), (x + 1, y));
1053 }
1054 if !foreground(x as isize + 1, y as isize) {
1055 add_edge(&mut edges, (x + 1, y), (x + 1, y + 1));
1056 }
1057 if !foreground(x as isize, y as isize + 1) {
1058 add_edge(&mut edges, (x + 1, y + 1), (x, y + 1));
1059 }
1060 if !foreground(x as isize - 1, y as isize) {
1061 add_edge(&mut edges, (x, y + 1), (x, y));
1062 }
1063 }
1064 }
1065
1066 let mut contours = Vec::new();
1067 while let Some((&start, _)) = edges.iter().next() {
1068 let mut current = start;
1069 let mut points = Vec::new();
1070 loop {
1071 points.push([current.0, current.1]);
1072 let Some(next) = take_edge(&mut edges, current) else {
1073 break;
1074 };
1075 current = next;
1076 if current == start {
1077 break;
1078 }
1079 }
1080 if points.len() >= 3 {
1081 contours.push(Contour { points });
1082 }
1083 }
1084 contours
1085}
1086
1087fn add_edge(edges: &mut BTreeMap<(i32, i32), Vec<(i32, i32)>>, start: (i32, i32), end: (i32, i32)) {
1088 edges.entry(start).or_default().push(end);
1089}
1090
1091fn take_edge(
1092 edges: &mut BTreeMap<(i32, i32), Vec<(i32, i32)>>,
1093 start: (i32, i32),
1094) -> Option<(i32, i32)> {
1095 let values = edges.get_mut(&start)?;
1096 values.sort_unstable();
1097 let end = values.remove(0);
1098 if values.is_empty() {
1099 edges.remove(&start);
1100 }
1101 Some(end)
1102}
1103
1104pub fn approximate_polygon(contour: &Contour, epsilon: f64) -> VisionResult<Contour> {
1106 if !epsilon.is_finite() || epsilon < 0.0 {
1107 return Err(VisionError::InvalidParameter(
1108 "polygon epsilon must be finite and non-negative".to_owned(),
1109 ));
1110 }
1111 if contour.points.len() <= 3 || epsilon == 0.0 {
1112 return Ok(contour.clone());
1113 }
1114 let first = contour.points[0];
1117 let split = contour
1118 .points
1119 .iter()
1120 .enumerate()
1121 .max_by_key(|(_, point)| squared_distance(**point, first))
1122 .map_or(0, |(index, _)| index);
1123 let mut left = rdp(&contour.points[..=split], epsilon);
1124 let mut wrapped = contour.points[split..].to_vec();
1125 wrapped.push(first);
1126 let right = rdp(&wrapped, epsilon);
1127 left.extend(right.into_iter().skip(1).take_while(|point| *point != first));
1128 Ok(Contour { points: left })
1129}
1130
1131fn rdp(points: &[[i32; 2]], epsilon: f64) -> Vec<[i32; 2]> {
1132 if points.len() <= 2 {
1133 return points.to_vec();
1134 }
1135 let first = points[0];
1136 let last = points[points.len() - 1];
1137 let mut max_distance = 0.0;
1138 let mut split = 0;
1139 for (index, &point) in points.iter().enumerate().take(points.len() - 1).skip(1) {
1140 let distance = point_segment_distance(point, first, last);
1141 if distance > max_distance {
1142 max_distance = distance;
1143 split = index;
1144 }
1145 }
1146 if max_distance <= epsilon {
1147 return vec![first, last];
1148 }
1149 let mut left = rdp(&points[..=split], epsilon);
1150 let right = rdp(&points[split..], epsilon);
1151 left.extend(right.into_iter().skip(1));
1152 left
1153}
1154
1155fn squared_distance(a: [i32; 2], b: [i32; 2]) -> i64 {
1156 let dx = i64::from(a[0]) - i64::from(b[0]);
1157 let dy = i64::from(a[1]) - i64::from(b[1]);
1158 dx * dx + dy * dy
1159}
1160
1161fn point_segment_distance(point: [i32; 2], start: [i32; 2], end: [i32; 2]) -> f64 {
1162 let px = f64::from(point[0]);
1163 let py = f64::from(point[1]);
1164 let sx = f64::from(start[0]);
1165 let sy = f64::from(start[1]);
1166 let dx = f64::from(end[0] - start[0]);
1167 let dy = f64::from(end[1] - start[1]);
1168 let length_squared = dx.mul_add(dx, dy * dy);
1169 if length_squared == 0.0 {
1170 return (px - sx).hypot(py - sy);
1171 }
1172 let t = ((px - sx).mul_add(dx, (py - sy) * dy) / length_squared).clamp(0.0, 1.0);
1173 (px - sx - t * dx).hypot(py - sy - t * dy)
1174}
1175
1176#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
1178pub enum RleOrder {
1179 #[default]
1181 RowMajor,
1182 CocoColumnMajor,
1184}
1185
1186#[derive(Clone, Debug, PartialEq, Eq)]
1188pub struct MaskRle {
1189 pub width: usize,
1191 pub height: usize,
1193 pub order: RleOrder,
1195 pub counts: Vec<usize>,
1197}
1198
1199#[must_use]
1201pub fn encode_rle(mask: &BinaryMask, order: RleOrder) -> MaskRle {
1202 let mut counts = vec![0_usize];
1203 let mut current = 0_u8;
1204 for index in 0..mask.width() * mask.height() {
1205 let (x, y) = linear_coordinate(index, mask.width(), mask.height(), order);
1206 let value = u8::from(mask.contains(x, y));
1207 if value == current {
1208 *counts.last_mut().expect("initial run exists") += 1;
1209 } else {
1210 counts.push(1);
1211 current = value;
1212 }
1213 }
1214 MaskRle { width: mask.width(), height: mask.height(), order, counts }
1215}
1216
1217pub fn decode_rle(rle: &MaskRle) -> VisionResult<BinaryMask> {
1219 let total = rle
1220 .width
1221 .checked_mul(rle.height)
1222 .ok_or_else(|| VisionError::InvalidDimensions("RLE dimensions overflow".to_owned()))?;
1223 let encoded = rle.counts.iter().try_fold(0_usize, |sum, &count| sum.checked_add(count));
1224 if encoded != Some(total) {
1225 return Err(VisionError::InvalidParameter(
1226 "RLE run lengths do not match mask dimensions".to_owned(),
1227 ));
1228 }
1229 let mut data = vec![0_u8; total];
1230 let mut linear = 0;
1231 let mut value = 0_u8;
1232 for &count in &rle.counts {
1233 for _ in 0..count {
1234 let (x, y) = linear_coordinate(linear, rle.width, rle.height, rle.order);
1235 data[y * rle.width + x] = value;
1236 linear += 1;
1237 }
1238 value ^= 1;
1239 }
1240 BinaryMask::try_new(rle.width, rle.height, data)
1241}
1242
1243fn linear_coordinate(index: usize, width: usize, height: usize, order: RleOrder) -> (usize, usize) {
1244 match order {
1245 RleOrder::RowMajor => (index % width, index / width),
1246 RleOrder::CocoColumnMajor => (index / height, index % height),
1247 }
1248}
1249
1250#[derive(Clone, Debug, PartialEq)]
1252pub struct DepthMap {
1253 image: Image<f32, 1>,
1254}
1255
1256impl DepthMap {
1257 pub fn try_new(width: usize, height: usize, data: Vec<f32>) -> VisionResult<Self> {
1259 if data.iter().any(|value| value.is_infinite()) {
1260 return Err(VisionError::InvalidParameter(
1261 "depth values may be finite or NaN, but not infinite".to_owned(),
1262 ));
1263 }
1264 let metadata = ImageMetadata { color_space: ColorSpace::Depth, ..Default::default() };
1265 Ok(Self { image: Image::try_new_with_metadata(width, height, data, metadata)? })
1266 }
1267
1268 #[must_use]
1270 pub fn image(&self) -> &Image<f32, 1> {
1271 &self.image
1272 }
1273
1274 pub fn valid_mask(&self, min_depth: f32, max_depth: f32) -> VisionResult<BinaryMask> {
1276 if !min_depth.is_finite() || min_depth < 0.0 || max_depth.is_nan() || max_depth < min_depth
1277 {
1278 return Err(VisionError::InvalidParameter("invalid depth range".to_owned()));
1279 }
1280 BinaryMask::try_new(
1281 self.image.width(),
1282 self.image.height(),
1283 self.image
1284 .as_slice()
1285 .iter()
1286 .map(|&value| {
1287 u8::from(value.is_finite() && value >= min_depth && value <= max_depth)
1288 })
1289 .collect(),
1290 )
1291 }
1292}
1293
1294#[derive(Clone, Debug, PartialEq)]
1296pub struct ConfidenceMap {
1297 image: Image<f32, 1>,
1298}
1299
1300impl ConfidenceMap {
1301 pub fn try_new(width: usize, height: usize, data: Vec<f32>) -> VisionResult<Self> {
1303 if data.iter().any(|value| !value.is_finite() || !(0.0..=1.0).contains(value)) {
1304 return Err(VisionError::InvalidParameter(
1305 "confidence values must be finite and in [0, 1]".to_owned(),
1306 ));
1307 }
1308 Ok(Self { image: Image::try_new(width, height, data)? })
1309 }
1310
1311 #[must_use]
1313 pub fn image(&self) -> &Image<f32, 1> {
1314 &self.image
1315 }
1316
1317 #[must_use]
1319 pub const fn width(&self) -> usize {
1320 self.image.width()
1321 }
1322
1323 #[must_use]
1325 pub const fn height(&self) -> usize {
1326 self.image.height()
1327 }
1328}
1329
1330#[derive(Clone, Debug, PartialEq)]
1332pub struct FlowField {
1333 image: Image<f32, 2>,
1334}
1335
1336impl FlowField {
1337 pub fn try_new(width: usize, height: usize, data: Vec<f32>) -> VisionResult<Self> {
1339 if data.iter().any(|value| value.is_infinite()) {
1340 return Err(VisionError::InvalidParameter(
1341 "flow values may be finite or NaN, but not infinite".to_owned(),
1342 ));
1343 }
1344 Ok(Self { image: Image::try_new(width, height, data)? })
1345 }
1346
1347 #[must_use]
1349 pub fn image(&self) -> &Image<f32, 2> {
1350 &self.image
1351 }
1352
1353 pub fn to_remap(&self) -> VisionResult<(Image<f32, 1>, Image<f32, 1>)> {
1355 let mut map_x = Vec::with_capacity(self.image.width() * self.image.height());
1356 let mut map_y = Vec::with_capacity(self.image.width() * self.image.height());
1357 for y in 0..self.image.height() {
1358 for x in 0..self.image.width() {
1359 let flow = self.image.get(x, y).expect("coordinate in bounds");
1360 map_x.push(x as f32 + flow[0]);
1361 map_y.push(y as f32 + flow[1]);
1362 }
1363 }
1364 Ok((
1365 Image::try_new(self.image.width(), self.image.height(), map_x)?,
1366 Image::try_new(self.image.width(), self.image.height(), map_y)?,
1367 ))
1368 }
1369}
1370
1371#[derive(Clone, Debug, PartialEq)]
1373pub struct PointMap {
1374 image: Image<f32, 3>,
1375}
1376
1377impl PointMap {
1378 pub fn try_new(width: usize, height: usize, data: Vec<f32>) -> VisionResult<Self> {
1380 if data.iter().any(|value| value.is_infinite()) {
1381 return Err(VisionError::InvalidParameter(
1382 "point-map values may be finite or NaN, but not infinite".to_owned(),
1383 ));
1384 }
1385 Ok(Self { image: Image::try_new(width, height, data)? })
1386 }
1387
1388 #[must_use]
1390 pub fn image(&self) -> &Image<f32, 3> {
1391 &self.image
1392 }
1393
1394 #[must_use]
1396 pub const fn width(&self) -> usize {
1397 self.image.width()
1398 }
1399
1400 #[must_use]
1402 pub const fn height(&self) -> usize {
1403 self.image.height()
1404 }
1405
1406 pub fn valid_mask(&self) -> VisionResult<BinaryMask> {
1408 let mut data = Vec::with_capacity(self.image.width() * self.image.height());
1409 for y in 0..self.image.height() {
1410 for x in 0..self.image.width() {
1411 let point = self.image.get(x, y).expect("coordinate in bounds");
1412 data.push(u8::from(point.iter().all(|value| value.is_finite())));
1413 }
1414 }
1415 BinaryMask::try_new(self.image.width(), self.image.height(), data)
1416 }
1417}
1418
1419#[cfg(test)]
1420mod tests {
1421 use super::{
1422 approximate_polygon, connected_components, connected_components_u8, decode_rle,
1423 distance_transform_edt, distance_transform_edt_u8_into,
1424 distance_transform_edt_with_spacing, encode_rle, find_contours, BinaryMask, Connectivity,
1425 DepthMap, DistanceTransformWorkspace, FlowField, PointMap, RleOrder,
1426 };
1427 use spatialrust_image::Image;
1428 use std::collections::VecDeque;
1429
1430 #[test]
1431 fn threshold_and_components_find_two_regions() {
1432 let scores = Image::<f32, 1>::try_new(
1433 4,
1434 3,
1435 vec![1.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
1436 )
1437 .unwrap();
1438 let mask = BinaryMask::from_threshold(scores.view(), 0.5).unwrap();
1439 let result = connected_components(&mask, Connectivity::Four).unwrap();
1440 assert_eq!(result.components.len(), 2);
1441 assert_eq!(result.components[0].area, 4);
1442 assert_eq!(result.components[1].area, 2);
1443 assert_eq!(result.components[0].centroid, [1.0, 1.0]);
1444 }
1445
1446 #[test]
1447 fn run_length_components_match_pixel_flood_fill() {
1448 let mut state = 0x9e37_79b9_u32;
1449 for connectivity in [Connectivity::Four, Connectivity::Eight] {
1450 for case in 0..96 {
1451 let width = 1 + case % 37;
1452 let height = 1 + (case * 11) % 29;
1453 let cutoff = match case % 4 {
1454 0 => 40,
1455 1 => 250,
1456 2 => 600,
1457 _ => 850,
1458 };
1459 let data = (0..width * height)
1460 .map(|_| {
1461 state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1462 u8::from(state % 1_000 < cutoff)
1463 })
1464 .collect::<Vec<_>>();
1465 let mask = BinaryMask::try_new(width, height, data.clone()).unwrap();
1466 let actual = connected_components(&mask, connectivity).unwrap();
1467 let expected = flood_fill_labels(&data, width, height, connectivity);
1468 assert_eq!(actual.labels.as_slice(), expected, "case {case:?} {connectivity:?}");
1469
1470 let max_label = expected.iter().copied().max().unwrap_or(0) as usize;
1471 assert_eq!(actual.components.len(), max_label);
1472 for component in &actual.components {
1473 let mut area = 0;
1474 let mut min_x = width;
1475 let mut min_y = height;
1476 let mut max_x = 0;
1477 let mut max_y = 0;
1478 let mut sum_x = 0.0;
1479 let mut sum_y = 0.0;
1480 for (index, &label) in expected.iter().enumerate() {
1481 if label == component.label {
1482 let (x, y) = (index % width, index / width);
1483 area += 1;
1484 min_x = min_x.min(x);
1485 min_y = min_y.min(y);
1486 max_x = max_x.max(x + 1);
1487 max_y = max_y.max(y + 1);
1488 sum_x += x as f64 + 0.5;
1489 sum_y += y as f64 + 0.5;
1490 }
1491 }
1492 assert_eq!(component.area, area);
1493 assert_eq!(component.bbox.x_min, min_x as f32);
1494 assert_eq!(component.bbox.y_min, min_y as f32);
1495 assert_eq!(component.bbox.x_max, max_x as f32);
1496 assert_eq!(component.bbox.y_max, max_y as f32);
1497 assert_eq!(component.centroid, [sum_x / area as f64, sum_y / area as f64]);
1498 }
1499 }
1500 }
1501 }
1502
1503 #[test]
1504 fn borrowed_u8_components_accept_all_nonzero_values() {
1505 let pixels = [0, 2, 0, 255, 0, 7];
1506 let result = connected_components_u8(3, 2, &pixels, Connectivity::Eight).unwrap();
1507 assert_eq!(result.labels.as_slice(), [0, 1, 0, 1, 0, 1]);
1508 assert_eq!(result.components[0].area, 3);
1509 assert!(connected_components_u8(3, 2, &pixels[..5], Connectivity::Eight).is_err());
1510 }
1511
1512 fn flood_fill_labels(
1513 data: &[u8],
1514 width: usize,
1515 height: usize,
1516 connectivity: Connectivity,
1517 ) -> Vec<u32> {
1518 let offsets: &[(isize, isize)] = match connectivity {
1519 Connectivity::Four => &[(0, -1), (-1, 0), (1, 0), (0, 1)],
1520 Connectivity::Eight => {
1521 &[(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
1522 }
1523 };
1524 let mut labels = vec![0_u32; data.len()];
1525 let mut queue = VecDeque::new();
1526 let mut next_label = 1_u32;
1527 for seed in 0..data.len() {
1528 if data[seed] == 0 || labels[seed] != 0 {
1529 continue;
1530 }
1531 labels[seed] = next_label;
1532 queue.push_back((seed % width, seed / width));
1533 while let Some((x, y)) = queue.pop_front() {
1534 for &(dx, dy) in offsets {
1535 let Some(nx) = x.checked_add_signed(dx) else {
1536 continue;
1537 };
1538 let Some(ny) = y.checked_add_signed(dy) else {
1539 continue;
1540 };
1541 if nx >= width || ny >= height {
1542 continue;
1543 }
1544 let index = ny * width + nx;
1545 if data[index] != 0 && labels[index] == 0 {
1546 labels[index] = next_label;
1547 queue.push_back((nx, ny));
1548 }
1549 }
1550 }
1551 next_label += 1;
1552 }
1553 labels
1554 }
1555
1556 #[test]
1557 fn contours_trace_outer_and_hole_loops() {
1558 let mask = BinaryMask::try_new(3, 3, vec![1, 1, 1, 1, 0, 1, 1, 1, 1]).unwrap();
1559 let contours = find_contours(&mask);
1560 assert_eq!(contours.len(), 2);
1561 assert!(contours.iter().all(|contour| contour.points.len() >= 4));
1562 let simplified = approximate_polygon(&contours[0], 0.1).unwrap();
1563 assert!(simplified.points.len() <= contours[0].points.len());
1564 }
1565
1566 #[test]
1567 fn rle_roundtrips_both_orders() {
1568 let mask = BinaryMask::try_new(3, 2, vec![0, 1, 1, 1, 0, 1]).unwrap();
1569 for order in [RleOrder::RowMajor, RleOrder::CocoColumnMajor] {
1570 let encoded = encode_rle(&mask, order);
1571 assert_eq!(decode_rle(&encoded).unwrap(), mask);
1572 }
1573 }
1574
1575 #[test]
1576 fn exact_distance_transform_matches_known_grid() {
1577 let mask = BinaryMask::try_new(4, 3, vec![0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]).unwrap();
1578 let distances = distance_transform_edt(&mask).unwrap();
1579 let expected = [
1580 0.0,
1581 1.0,
1582 2.0,
1583 3.0,
1584 1.0,
1585 2.0_f32.sqrt(),
1586 5.0_f32.sqrt(),
1587 10.0_f32.sqrt(),
1588 2.0,
1589 5.0_f32.sqrt(),
1590 8.0_f32.sqrt(),
1591 13.0_f32.sqrt(),
1592 ];
1593 for (&actual, expected) in distances.as_slice().iter().zip(expected) {
1594 assert!((actual - expected).abs() <= 1e-6, "{actual} != {expected}");
1595 }
1596 }
1597
1598 #[test]
1599 fn distance_transform_respects_anisotropic_spacing() {
1600 let mask = BinaryMask::try_new(3, 2, vec![0, 1, 1, 1, 1, 1]).unwrap();
1601 let distances = distance_transform_edt_with_spacing(&mask, 2.0, 3.0).unwrap();
1602 let expected = [0.0, 2.0, 4.0, 3.0, 13.0_f32.sqrt(), 5.0];
1603 for (&actual, expected) in distances.as_slice().iter().zip(expected) {
1604 assert!((actual - expected).abs() <= 1e-6, "{actual} != {expected}");
1605 }
1606 }
1607
1608 #[test]
1609 fn unit_distance_transform_matches_brute_force_on_irregular_mask() {
1610 let (width, height) = (31, 23);
1611 let data = (0..width * height)
1612 .map(|index| u8::from(index % 37 != 0 && index % 101 != 7))
1613 .collect::<Vec<_>>();
1614 let background = data
1615 .iter()
1616 .enumerate()
1617 .filter(|(_, value)| **value == 0)
1618 .map(|(index, _)| (index % width, index / width))
1619 .collect::<Vec<_>>();
1620 let mask = BinaryMask::try_new(width, height, data).unwrap();
1621 let actual = distance_transform_edt(&mask).unwrap();
1622 for y in 0..height {
1623 for x in 0..width {
1624 let expected = background
1625 .iter()
1626 .map(|&(bx, by)| (x.abs_diff(bx) as f32).hypot(y.abs_diff(by) as f32))
1627 .fold(f32::INFINITY, f32::min);
1628 assert_eq!(actual[(x, y)][0], expected);
1629 }
1630 }
1631 }
1632
1633 #[test]
1634 fn reusable_distance_transform_accepts_packed_255_masks() {
1635 let input = [0_u8, 255, 255, 255, 255, 255];
1636 let mut output = [0.0_f32; 6];
1637 let mut workspace = DistanceTransformWorkspace::new();
1638 distance_transform_edt_u8_into(&input, 3, 2, &mut output, &mut workspace).unwrap();
1639 assert_eq!(output, [0.0, 1.0, 2.0, 1.0, 2.0_f32.sqrt(), 5.0_f32.sqrt()]);
1640 let capacity = workspace.capacity();
1641 distance_transform_edt_u8_into(&input, 3, 2, &mut output, &mut workspace).unwrap();
1642 assert_eq!(workspace.capacity(), capacity);
1643 assert!(distance_transform_edt_u8_into(&input, 3, 2, &mut output[..5], &mut workspace,)
1644 .is_err());
1645 }
1646
1647 #[test]
1648 fn distance_transform_defines_empty_and_rejects_missing_background() {
1649 let empty = BinaryMask::try_new(0, 0, Vec::new()).unwrap();
1650 assert!(distance_transform_edt(&empty).unwrap().as_slice().is_empty());
1651
1652 let foreground = BinaryMask::try_new(2, 2, vec![1; 4]).unwrap();
1653 assert!(distance_transform_edt(&foreground).is_err());
1654 let background = BinaryMask::try_new(2, 2, vec![0; 4]).unwrap();
1655 assert_eq!(distance_transform_edt(&background).unwrap().as_slice(), &[0.0; 4]);
1656 assert!(distance_transform_edt_with_spacing(&background, 0.0, 1.0).is_err());
1657 assert!(distance_transform_edt_with_spacing(&background, 1.0, f32::NAN).is_err());
1658 }
1659
1660 #[test]
1661 fn depth_flow_and_point_maps_expose_validity() {
1662 let depth = DepthMap::try_new(2, 1, vec![1.0, f32::NAN]).unwrap();
1663 assert_eq!(depth.valid_mask(0.1, 10.0).unwrap().image().as_slice(), &[1, 0]);
1664 let flow = FlowField::try_new(2, 1, vec![1.0, 2.0, -1.0, 0.0]).unwrap();
1665 let (mx, my) = flow.to_remap().unwrap();
1666 assert_eq!(mx.as_slice(), &[1.0, 0.0]);
1667 assert_eq!(my.as_slice(), &[2.0, 0.0]);
1668 let points = PointMap::try_new(2, 1, vec![0.0, 0.0, 1.0, f32::NAN, 0.0, 1.0]).unwrap();
1669 assert_eq!(points.valid_mask().unwrap().image().as_slice(), &[1, 0]);
1670 }
1671}