From f68fa6f0b7d557a422b6fe9579910bd40de912a3 Mon Sep 17 00:00:00 2001 From: Camden Dixie O'Brien Date: Mon, 2 Nov 2026 11:15:27 +0000 Subject: [PATCH] Fix inner/outer detection in contour tracing --- examples/contours.rs | 34 +++++--- src/contour.rs | 189 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 187 insertions(+), 36 deletions(-) diff --git a/examples/contours.rs b/examples/contours.rs index 87b9c69..7681827 100644 --- a/examples/contours.rs +++ b/examples/contours.rs @@ -1,30 +1,38 @@ mod common; +use ccvl::kernel::Kernel; use ccvl::*; -fn draw(contour: &contour::Contour, img: &mut image::Image) { - let n = contour.vertices.len(); +fn draw( + img: &mut image::Image, + path: &Vec<(usize, usize)>, + colour: image::SRGBA, +) { + let n = path.len(); for i in 0..n { - let (x0, y0) = contour.vertices[i]; - let (x1, y1) = contour.vertices[(i + 1) % n]; - let colour = match contour.kind { - contour::Kind::Outer => colours::GREEN, - contour::Kind::Inner => colours::BLUE, - }; + let (x0, y0) = path[i]; + let (x1, y1) = path[(i + 1) % n]; draw::line(x0, y0, x1, y1, img, colour); } } +fn approx(a: f32, b: f32, epsilon: f32) -> bool { + (a - b).abs() <= epsilon +} + fn main() -> std::process::ExitCode { common::process_image(|mut img| { let greyscale = img.map(|p| image::Luminance::from(p)); - let thresholded = filter::threshold_auto(greyscale); - - let contours = contour::find(thresholded); + let blurred = kernel::gaussian::<5>(1.2).convolve_sat(&greyscale); + let thresholded = filter::threshold_auto(blurred); + let contours = + contour::find(&thresholded, image::Luminance { y: 0.0 }); for contour in contours { - draw(&contour, &mut img); + draw(&mut img, &contour.vertices, match contour.kind { + contour::Kind::Outer => colours::GREEN, + contour::Kind::Inner => colours::BLUE, + }); } - img }) } diff --git a/src/contour.rs b/src/contour.rs index 1d452b5..be2b061 100644 --- a/src/contour.rs +++ b/src/contour.rs @@ -45,12 +45,15 @@ trait Coordinate { fn set

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

, value: P); } -pub fn find(img: image::Image) -> Vec { +pub fn find

(img: &image::Image

, fg: P) -> Vec +where + P: PartialEq + Copy + Send + Sync, +{ let mut map = img.map(|p| { - if p.y == 1.0 { - Pixel::Background - } else { + if p == fg { Pixel::Foreground + } else { + Pixel::Background } }); @@ -67,7 +70,7 @@ pub fn find(img: image::Image) -> Vec { add(q, Kind::Inner, parent, &mut contours, &mut map); } else if let Some(label) = traced_up(&map, q) { parent = Some(label); - } else if let Some(label) = traced_down(&map, q) { + } else if let Some(label) = traced_down(&map, q) { parent = contours[label].parent; } } @@ -83,7 +86,8 @@ fn add( map: &mut image::Image, ) { let label = contours.len(); - let vertices = trace(map, start, kind.start_dir(), label); + let dir = kind.find_start_dir(start, map); + let vertices = trace(map, start, dir, label); contours.push(Contour::new(parent, kind, vertices)); if let Some(index) = parent { contours[index].add_child(label); @@ -98,15 +102,12 @@ fn trace( ) -> Vec<(usize, usize)> { let mut q = start_q; let mut dir = start_dir; - while background(img, dir.from(q)) { - dir.next(); - } let mut vertices = Vec::new(); loop { - if foreground(img, q) { - q.set(img, Pixel::TracedUp(label)); - } + if foreground(img, q) { + q.set(img, Pixel::TracedUp(label)); + } let prev_dir = dir; dir = dir.flip().next(); @@ -124,20 +125,17 @@ fn trace( dir = dir.next(); }; - if q == start_q || !q.in_bounds(img.extent) { - if dir != start_dir { - vertices.push(q); - } + if q == start_q && dir == start_dir || !q.in_bounds(img.extent) { break; } } - // println!(); // for y in 0..img.extent.y { + // print!(" "); // for x in 0..img.extent.x { // if let Some(label) = traced_up(img, (x, y)) { // print!("{:1}", label); - // } else if let Some(label) = traced_down(img, (x, y)) { + // } else if let Some(_) = traced_down(img, (x, y)) { // print!("-") // } else if foreground(img, (x, y)) { // print!("."); @@ -148,7 +146,7 @@ fn trace( // println!(); // } - vertices + vertices } fn foreground(img: &image::Image, q: (usize, usize)) -> bool { @@ -183,11 +181,19 @@ fn traced_down( } impl Kind { - fn start_dir(self) -> Dir { - match self { - Kind::Outer => Dir::E, - Kind::Inner => Dir::W, + fn find_start_dir( + self, + q: (usize, usize), + img: &image::Image, + ) -> Dir { + let mut dir = match self { + Kind::Outer => Dir::W, + Kind::Inner => Dir::E, + }; + while background(img, dir.from(q)) { + dir = dir.prev() } + dir.flip() } } @@ -205,6 +211,19 @@ impl Dir { } } + fn prev(self) -> Self { + match self { + Self::N => Self::NW, + Self::NE => Self::N, + Self::E => Self::NE, + Self::SE => Self::E, + Self::S => Self::SE, + Self::SW => Self::S, + Self::W => Self::SW, + Self::NW => Self::W, + } + } + fn flip(self) -> Self { match self { Self::N => Self::S, @@ -292,3 +311,127 @@ impl Default for Pixel { Self::Background } } + +#[cfg(test)] +#[rustfmt::skip] +mod tests { + use super::*; + + #[test] + fn simple_block() { + let img = image::Image:: { + extent: image::Extent { x: 4, y: 4 }, + pixels: vec![ + 0, 0, 0, 0, + 0, 1, 1, 0, + 0, 1, 1, 0, + 0, 0, 0, 0, + ] + }; + + let contours = find(&img, 1); + assert_eq!(contours.len(), 1); + + let contour = &contours[0]; + assert_eq!(contour.kind, Kind::Outer); + assert_eq!(contour.vertices, vec![ + (1, 1), (2, 1), (2, 2), (1, 2) + ]); + } + + #[test] + fn simple_block_with_hole() { + let img = image::Image:: { + extent: image::Extent { x: 8, y: 8 }, + pixels: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 1, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 0, 0, 1, 1, 0, + 0, 1, 1, 0, 0, 1, 1, 0, + 0, 1, 1, 1, 1, 1, 1, 0, + 0, 1, 1, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + }; + + let contours = find(&img, 1); + assert_eq!(contours.len(), 2); + + let contour = &contours[0]; + assert_eq!(contour.parent, None); + assert_eq!(contour.kind, Kind::Outer); + assert_eq!(contour.vertices, vec![ + (1, 1), (6, 1), (6, 6), (1, 6) + ]); + assert_eq!(contour.children, vec![ 1 ]); + + let contour = &contours[1]; + assert_eq!(contour.parent, Some(0)); + assert_eq!(contour.kind, Kind::Inner); + assert_eq!(contour.vertices, vec![ + (2, 3), (2, 4), (3, 5), (4, 5), + (5, 4), (5, 3), (4, 2), (3, 2), + ]); + } + + #[test] + fn diagonally_connected_block_with_hole() { + let img = image::Image:: { + extent: image::Extent { x: 8, y: 8 }, + pixels: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 1, 1, 0, 0, 1, 1, 0, + 0, 1, 1, 0, 0, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 1, 1, 1, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + ] + }; + + let contours = find(&img, 1); + assert_eq!(contours.len(), 2); + + let contour = &contours[0]; + assert_eq!(contour.parent, None); + assert_eq!(contour.kind, Kind::Outer); + assert_eq!(contour.vertices, vec![ + (3, 1), (6, 1), (6, 6), (3, 6), (3, 5), + (2, 4), (1, 4), (1, 3), (2, 3), (3, 2), + ]); + assert_eq!(contour.children, vec![ 1 ]); + + let contour = &contours[1]; + assert_eq!(contour.parent, Some(0)); + assert_eq!(contour.kind, Kind::Inner); + assert_eq!(contour.vertices, vec![ + (2, 3), (2, 4), (3, 5), (4, 5), + (5, 4), (5, 3), (4, 2), (3, 2), + ]); + } + + #[test] + fn contour_passes_through_start() { + let img = image::Image:: { + extent: image::Extent { x: 5, y: 5 }, + pixels: vec![ + 0, 0, 0, 0, 0, + 0, 0, 1, 1, 0, + 0, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, + ] + }; + + let contours = find(&img, 1); + assert_eq!(contours.len(), 1); + + let contour = &contours[0]; + assert_eq!(contour.kind, Kind::Outer); + assert_eq!(contour.vertices, vec![ + (2, 1), (3, 1), (2, 1), (1, 2), (1, 3), (1, 2) + ]); + } +}