#![allow(dead_code)]
use image::GrayImage;
#[derive(Debug, Clone)]
pub struct Contour {
pub points: Vec<(i32, i32)>,
}
impl Contour {
pub fn new() -> Self {
Self { points: Vec::new() }
}
pub fn len(&self) -> usize {
self.points.len()
}
pub fn is_empty(&self) -> bool {
self.points.is_empty()
}
}
pub fn find_contours(binary_img: &GrayImage) -> Vec<Contour> {
let (width, height) = binary_img.dimensions();
let mut label_map = vec![vec![0u32; width as usize]; height as usize];
let mut contours = Vec::with_capacity(50); let mut label = 1u32;
for y in 0..height {
for x in 0..width {
let ux = x as usize;
let uy = y as usize;
if binary_img.get_pixel(x, y)[0] > 127 && label_map[uy][ux] == 0 {
flood_fill_label(binary_img, &mut label_map, x as i32, y as i32, label, width as i32, height as i32);
label += 1;
}
}
}
for current_label in 1..label {
let mut _start_x = 0;
let mut _start_y = 0;
let mut found = false;
'outer: for y in 0..height {
for x in 0..width {
if label_map[y as usize][x as usize] == current_label {
_start_x = x as i32;
_start_y = y as i32;
found = true;
break 'outer;
}
}
}
if !found {
continue;
}
let boundary_pixels = extract_boundary(&label_map, current_label, width as i32, height as i32);
if boundary_pixels.len() >= 3 {
contours.push(Contour { points: boundary_pixels });
}
}
contours
}
fn flood_fill_label(
img: &GrayImage,
labels: &mut Vec<Vec<u32>>,
start_x: i32,
start_y: i32,
label: u32,
width: i32,
height: i32,
) {
let mut stack = Vec::with_capacity(256); stack.push((start_x, start_y));
while let Some((x, y)) = stack.pop() {
if x < 0 || x >= width || y < 0 || y >= height {
continue;
}
let ux = x as usize;
let uy = y as usize;
if labels[uy][ux] != 0 {
continue;
}
let pixel = img.get_pixel(x as u32, y as u32)[0];
if pixel <= 127 {
continue;
}
labels[uy][ux] = label;
if x + 1 < width { stack.push((x + 1, y)); }
if x > 0 { stack.push((x - 1, y)); }
if y + 1 < height { stack.push((x, y + 1)); }
if y > 0 { stack.push((x, y - 1)); }
}
}
fn is_boundary_pixel_label(
label_map: &[Vec<u32>],
x: i32,
y: i32,
target_label: u32,
width: i32,
height: i32,
) -> bool {
for (dx, dy) in [(0, -1), (1, 0), (0, 1), (-1, 0)].iter() {
let nx = x + dx;
let ny = y + dy;
if nx < 0 || nx >= width || ny < 0 || ny >= height {
return true;
}
if label_map[ny as usize][nx as usize] != target_label {
return true;
}
}
false
}
fn extract_boundary(
label_map: &[Vec<u32>],
target_label: u32,
width: i32,
height: i32,
) -> Vec<(i32, i32)> {
let mut boundary = Vec::with_capacity(100);
for y in 0..height {
for x in 0..width {
if label_map[y as usize][x as usize] == target_label &&
is_boundary_pixel_label(label_map, x, y, target_label, width, height) {
boundary.push((x, y));
}
}
}
boundary
}
fn follow_border(
img: &[Vec<u8>],
start_i: i32,
start_j: i32,
second_i: i32,
second_j: i32,
_nbd: i32,
border: &mut Vec<(i32, i32)>,
_lnbd_img: &mut [Vec<u8>],
) {
const DIR: [(i32, i32); 8] = [
(0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), (1, 0), (1, 1), ];
border.push((start_i, start_j));
let mut curr_i = start_i;
let mut curr_j = start_j;
let mut prev_i = second_i;
let mut prev_j = second_j;
let mut step_count = 0;
let max_steps = (img.len() * img[0].len()) * 2;
loop {
step_count += 1;
if step_count > max_steps {
break; }
let mut search_dir = 0;
for (idx, &(di, dj)) in DIR.iter().enumerate() {
if curr_i + di == prev_i && curr_j + dj == prev_j {
search_dir = (idx + 1) % 8;
break;
}
}
let mut found = false;
for k in 0..8 {
let dir_idx = (search_dir + k) % 8;
let (di, dj) = DIR[dir_idx];
let ni = curr_i + di;
let nj = curr_j + dj;
if ni >= 0 && ni < img.len() as i32 && nj >= 0 && nj < img[0].len() as i32 {
if img[ni as usize][nj as usize] >= 1 {
if ni == start_i && nj == start_j && border.len() > 2 {
return;
}
border.push((ni, nj));
prev_i = curr_i;
prev_j = curr_j;
curr_i = ni;
curr_j = nj;
found = true;
break;
}
}
}
if !found {
break; }
}
}
fn is_border_pixel(img: &GrayImage, x: i32, y: i32, width: i32, height: i32) -> bool {
for (dx, dy) in [(0, -1), (1, 0), (0, 1), (-1, 0)].iter() {
let nx = x + dx;
let ny = y + dy;
if nx < 0 || nx >= width || ny < 0 || ny >= height {
return true;
}
let neighbor_pixel = img.get_pixel(nx as u32, ny as u32)[0];
if neighbor_pixel <= 127 {
return true; }
}
false
}
fn trace_boundary(
img: &GrayImage,
visited: &mut Vec<Vec<bool>>,
start_x: i32,
start_y: i32,
width: i32,
height: i32,
) -> Option<Contour> {
let mut boundary_points = Vec::new();
const DIR: [(i32, i32); 8] = [
(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1), ];
let mut current_x = start_x;
let mut current_y = start_y;
let mut dir_idx = 0;
loop {
boundary_points.push((current_x, current_y));
let mut found = false;
for i in 0..8 {
let check_dir = (dir_idx + i) % 8;
let (dx, dy) = DIR[check_dir];
let nx = current_x + dx;
let ny = current_y + dy;
if nx < 0 || nx >= width || ny < 0 || ny >= height {
continue;
}
let pixel = img.get_pixel(nx as u32, ny as u32)[0];
if pixel > 127 {
current_x = nx;
current_y = ny;
dir_idx = if check_dir >= 2 { check_dir - 2 } else { check_dir + 6 };
found = true;
break;
}
}
if !found {
break; }
if current_x == start_x && current_y == start_y && boundary_points.len() > 1 {
break;
}
if boundary_points.len() > (width * height) as usize {
break;
}
}
flood_fill_visited(img, visited, start_x, start_y, width, height);
let simplified = simplify_contour(&boundary_points);
if simplified.len() >= 3 {
Some(Contour { points: simplified })
} else {
None
}
}
fn flood_fill_visited(
img: &GrayImage,
visited: &mut Vec<Vec<bool>>,
start_x: i32,
start_y: i32,
width: i32,
height: i32,
) {
let mut stack = vec![(start_x, start_y)];
while let Some((x, y)) = stack.pop() {
if x < 0 || x >= width || y < 0 || y >= height {
continue;
}
let ux = x as usize;
let uy = y as usize;
if visited[uy][ux] {
continue;
}
let pixel = img.get_pixel(x as u32, y as u32)[0];
if pixel <= 127 {
continue;
}
visited[uy][ux] = true;
stack.push((x + 1, y));
stack.push((x - 1, y));
stack.push((x, y + 1));
stack.push((x, y - 1));
stack.push((x + 1, y + 1));
stack.push((x - 1, y - 1));
stack.push((x + 1, y - 1));
stack.push((x - 1, y + 1));
}
}
fn simplify_contour(points: &[(i32, i32)]) -> Vec<(i32, i32)> {
if points.len() <= 3 {
return points.to_vec();
}
let mut simplified = Vec::new();
simplified.push(points[0]);
for i in 1..points.len() - 1 {
let prev = simplified.last().unwrap();
let curr = &points[i];
let next = &points[i + 1];
let dx1 = curr.0 - prev.0;
let dy1 = curr.1 - prev.1;
let dx2 = next.0 - curr.0;
let dy2 = next.1 - curr.1;
let cross = dx1 * dy2 - dy1 * dx2;
if cross.abs() > 0 {
simplified.push(*curr);
}
}
if let Some(last) = points.last() {
simplified.push(*last);
}
simplified
}
fn flood_fill(
img: &GrayImage,
labels: &mut Vec<Vec<i32>>,
x: i32,
y: i32,
label: i32,
width: i32,
height: i32,
) {
let mut stack = vec![(x, y)];
while let Some((cx, cy)) = stack.pop() {
if cx < 0 || cx >= width || cy < 0 || cy >= height {
continue;
}
let ux = cx as usize;
let uy = cy as usize;
if labels[uy][ux] != 0 {
continue;
}
let pixel = img.get_pixel(cx as u32, cy as u32)[0];
if pixel <= 127 {
continue;
}
labels[uy][ux] = label;
stack.push((cx + 1, cy));
stack.push((cx - 1, cy));
stack.push((cx, cy + 1));
stack.push((cx, cy - 1));
stack.push((cx + 1, cy + 1));
stack.push((cx - 1, cy - 1));
stack.push((cx + 1, cy - 1));
stack.push((cx - 1, cy + 1));
}
}
fn is_boundary_pixel(x: i32, y: i32, labels: &[Vec<i32>], width: i32, height: i32) -> bool {
let label = labels[y as usize][x as usize];
for (dx, dy) in [(0, -1), (1, 0), (0, 1), (-1, 0)].iter() {
let nx = x + dx;
let ny = y + dy;
if nx < 0 || nx >= width || ny < 0 || ny >= height {
return true; }
if labels[ny as usize][nx as usize] != label {
return true; }
}
false
}
#[allow(dead_code)]
fn trace_contour(
img: &GrayImage,
visited: &mut Vec<Vec<bool>>,
start_x: i32,
start_y: i32,
) -> Option<Contour> {
let (width, height) = img.dimensions();
let width = width as i32;
let height = height as i32;
let mut contour = Contour::new();
let mut current_x = start_x;
let mut current_y = start_y;
const DX: [i32; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
const DY: [i32; 8] = [-1, -1, 0, 1, 1, 1, 0, -1];
let mut dir = 0; let mut found_start = false;
loop {
if !found_start || current_x != start_x || current_y != start_y {
contour.points.push((current_x, current_y));
if current_y >= 0 && current_y < height && current_x >= 0 && current_x < width {
visited[current_y as usize][current_x as usize] = true;
}
} else if found_start {
break;
}
found_start = true;
let mut found_next = false;
for i in 0..8 {
let check_dir = (dir + i) % 8;
let nx = current_x + DX[check_dir];
let ny = current_y + DY[check_dir];
if nx >= 0 && nx < width && ny >= 0 && ny < height {
let pixel = img.get_pixel(nx as u32, ny as u32)[0];
if pixel > 127 {
current_x = nx;
current_y = ny;
dir = (check_dir + 5) % 8; found_next = true;
break;
}
}
}
if !found_next {
break; }
if contour.points.len() > (width * height) as usize {
break;
}
}
if contour.points.len() >= 3 {
Some(contour)
} else {
None
}
}
pub fn approx_simple(contour: &Contour) -> Contour {
if contour.points.len() <= 2 {
return contour.clone();
}
let mut result = Contour::new();
result.points.push(contour.points[0]);
let mut i = 1;
while i < contour.points.len() - 1 {
let prev = contour.points[i - 1];
let curr = contour.points[i];
let next = contour.points[i + 1];
let dx1 = curr.0 - prev.0;
let dy1 = curr.1 - prev.1;
let dx2 = next.0 - curr.0;
let dy2 = next.1 - curr.1;
if dx1 * dy2 != dy1 * dx2 {
result.points.push(curr);
}
i += 1;
}
if let Some(&last) = contour.points.last() {
result.points.push(last);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use image::{GrayImage, Luma};
#[test]
fn test_find_contours_simple() {
let mut img = GrayImage::new(10, 10);
for x in 2..8 {
for y in 2..8 {
img.put_pixel(x, y, Luma([255]));
}
}
let contours = find_contours(&img);
assert!(!contours.is_empty(), "Should find at least one contour");
}
}