From 88a25f3812b836f1340470a171c7c894496ffa84 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Thu, 29 Oct 2026 10:28:53 +0000 Subject: [PATCH] Implement basic contour tracing with Suzuki-Abe algorithm --- src/contour.rs | 244 +++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 245 insertions(+) create mode 100644 src/contour.rs diff --git a/src/contour.rs b/src/contour.rs new file mode 100644 index 0000000..3f59110 --- /dev/null +++ b/src/contour.rs @@ -0,0 +1,244 @@ +use crate::*; + +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Kind { + Outer, + Inner, +} + +#[derive(Debug, PartialEq)] +pub struct Contour { + pub trace: Vec<(usize, usize)>, + pub children: Vec, + pub parent: Option, + pub kind: Kind, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum Pixel { + Foreground, + Background, + Traced(usize), +} + +#[derive(Clone, Copy, Debug, PartialEq)] +enum Dir { + N, + NE, + E, + SE, + S, + SW, + W, + NW, +} + +trait Coordinate { + fn prev_x(&self) -> Self; + fn next_x(&self) -> Self; + fn prev_y(&self) -> Self; + fn next_y(&self) -> Self; + + fn in_bounds(&self, bounds: image::Extent) -> bool; + fn get(&self, img: &image::Image

) -> Option

; + fn set

(&self, img: &mut image::Image

, value: P); +} + +pub fn find(img: image::Image) -> Vec { + let mut map = img.map(|p| { + if p.y == 1.0 { + Pixel::Background + } else { + Pixel::Foreground + } + }); + + let mut contours = Vec::new(); + for y in 0..map.extent.y { + let mut parent = None; + for x in 0..map.extent.x { + let q = (x, y); + if foreground(&map, q) { + let prev_bg = background(&map, q.prev_x()); + let next_bg = background(&map, q.next_x()); + if prev_bg || next_bg { + let (dir, kind) = if prev_bg { + (Dir::W, Kind::Outer) + } else { + (Dir::E, Kind::Inner) + }; + let label = contours.len(); + let trace = trace(&mut map, q, dir, label); + contours.push(Contour::new(parent, kind, trace)); + if let Some(index) = parent { + contours[index].add_child(label); + } + } + } else if let Some(label) = traced(&map, q) { + if background(&map, q.prev_x()) { + parent = Some(label); + } else if background(&map, q.next_x()) { + parent = contours[label].parent; + } + } + } + } + contours +} + +fn trace( + img: &mut image::Image, + start_q: (usize, usize), + mut dir: Dir, + label: usize, +) -> Vec<(usize, usize)> { + let mut q = start_q; + dir = dir.next(); + + let mut path = Vec::new(); + 'outer: loop { + if !q.in_bounds(img.extent) { + break; + } + q.set(img, Pixel::Traced(label)); + path.push(q); + + let start = dir; + while background(img, dir.from(q)) { + dir = dir.next(); + if dir == start { + break 'outer; + } + } + q = dir.from(q); + dir = dir.flip().next(); + + if q == start_q { + break; + } + } + path +} + +fn foreground(img: &image::Image, q: (usize, usize)) -> bool { + match q.get(img) { + Some(Pixel::Foreground) => true, + _ => false, + } +} + +fn background(img: &image::Image, q: (usize, usize)) -> bool { + match q.get(img) { + Some(Pixel::Background) => true, + _ => false, + } +} + +fn traced(img: &image::Image, q: (usize, usize)) -> Option { + match q.get(img) { + Some(Pixel::Traced(label)) => Some(label), + _ => None, + } +} + +impl Dir { + fn next(self) -> Self { + match self { + Self::N => Self::NE, + Self::NE => Self::E, + Self::E => Self::SE, + Self::SE => Self::S, + Self::S => Self::SW, + Self::SW => Self::W, + Self::W => Self::NW, + Self::NW => Self::N, + } + } + + fn flip(self) -> Self { + match self { + Self::N => Self::S, + Self::NE => Self::SW, + Self::E => Self::W, + Self::SE => Self::NW, + Self::S => Self::N, + Self::SW => Self::NE, + Self::W => Self::E, + Self::NW => Self::SE, + } + } + + fn from(&self, q: C) -> C { + match self { + Self::N => q.prev_y(), + Self::NE => q.prev_y().next_x(), + Self::E => q.next_x(), + Self::SE => q.next_y().next_x(), + Self::S => q.next_y(), + Self::SW => q.next_y().prev_x(), + Self::W => q.prev_x(), + Self::NW => q.prev_y().prev_x(), + } + } +} + +impl Coordinate for (usize, usize) { + fn prev_x(&self) -> Self { + (self.0.wrapping_sub(1), self.1) + } + + fn next_x(&self) -> Self { + (self.0 + 1, self.1) + } + + fn prev_y(&self) -> Self { + (self.0, self.1.wrapping_sub(1)) + } + + fn next_y(&self) -> Self { + (self.0, self.1 + 1) + } + + fn in_bounds(&self, bounds: image::Extent) -> bool { + self.0 < bounds.x && self.1 < bounds.y + } + + fn get(&self, img: &image::Image

) -> Option

{ + if self.in_bounds(img.extent) { + Some(img.at(self.0, self.1)) + } else { + None + } + } + + fn set

(&self, img: &mut image::Image

, value: P) { + if self.in_bounds(img.extent) { + *img.at_mut(self.0, self.1) = value; + } + } +} + +impl Contour { + fn new( + parent: Option, + kind: Kind, + trace: Vec<(usize, usize)>, + ) -> Self { + Self { + trace, + children: Vec::new(), + parent, + kind, + } + } + + fn add_child(&mut self, child: usize) { + self.children.push(child); + } +} + +impl Default for Pixel { + fn default() -> Self { + Self::Background + } +} diff --git a/src/lib.rs b/src/lib.rs index b75f47b..d538bd8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod contour; pub mod filter; pub mod hough; pub mod image;