use std::collections::HashMap;
use oxigdal_core::vector::Coordinate;
use crate::error::{AlgorithmError, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Connectivity {
Four,
Eight,
}
impl Default for Connectivity {
fn default() -> Self {
Self::Eight
}
}
const DIR8_DX: [i32; 8] = [1, 1, 0, -1, -1, -1, 0, 1];
const DIR8_DY: [i32; 8] = [0, 1, 1, 1, 0, -1, -1, -1];
const DIR4_DX: [i32; 4] = [1, 0, -1, 0];
const DIR4_DY: [i32; 4] = [0, 1, 0, -1];
#[derive(Debug, Clone)]
pub(crate) struct PixelRing {
pub label: u32,
pub coords: Vec<(f64, f64)>,
pub signed_area: f64,
}
impl PixelRing {
pub fn is_exterior(&self) -> bool {
self.signed_area >= 0.0
}
}
#[derive(Debug, Clone)]
pub(crate) struct ClassifiedPolygon {
pub label: u32,
pub exterior: Vec<(f64, f64)>,
pub holes: Vec<Vec<(f64, f64)>>,
}
pub(crate) fn trace_boundaries(
labels: &[u32],
width: usize,
height: usize,
connectivity: Connectivity,
) -> Result<Vec<ClassifiedPolygon>> {
if labels.len() != width * height {
return Err(AlgorithmError::InvalidInput(
"label grid size does not match width*height".to_string(),
));
}
let mut label_set: Vec<u32> = Vec::new();
{
let mut seen = std::collections::HashSet::new();
for &lbl in labels {
if lbl != 0 && seen.insert(lbl) {
label_set.push(lbl);
}
}
}
label_set.sort_unstable();
let mut all_rings: Vec<PixelRing> = Vec::new();
let mut visited = vec![false; width * height];
for &target_label in &label_set {
for v in visited.iter_mut() {
*v = false;
}
let rings = trace_label_boundaries(
labels,
width,
height,
target_label,
connectivity,
&mut visited,
);
all_rings.extend(rings);
}
classify_and_assemble(all_rings)
}
fn trace_label_boundaries(
labels: &[u32],
width: usize,
height: usize,
target: u32,
connectivity: Connectivity,
visited: &mut [bool],
) -> Vec<PixelRing> {
let mut rings = Vec::new();
for y in 0..height {
for x in 0..width {
let idx = y * width + x;
if labels[idx] != target || visited[idx] {
continue;
}
if !is_boundary_pixel(labels, width, height, x, y, target) {
continue;
}
if let Some(ring) =
trace_single_ring(labels, width, height, x, y, target, connectivity, visited)
{
if ring.coords.len() >= 4 {
rings.push(ring);
}
}
}
}
rings
}
fn is_boundary_pixel(
labels: &[u32],
width: usize,
height: usize,
x: usize,
y: usize,
target: u32,
) -> bool {
let neighbors: [(i32, i32); 4] = [(1, 0), (-1, 0), (0, 1), (0, -1)];
for (dx, dy) in neighbors {
let nx = x as i32 + dx;
let ny = y as i32 + dy;
if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
return true; }
if labels[ny as usize * width + nx as usize] != target {
return true;
}
}
false
}
fn trace_single_ring(
labels: &[u32],
width: usize,
height: usize,
start_x: usize,
start_y: usize,
target: u32,
connectivity: Connectivity,
visited: &mut [bool],
) -> Option<PixelRing> {
let num_dirs = match connectivity {
Connectivity::Eight => 8usize,
Connectivity::Four => 4usize,
};
let dx_table: &[i32] = match connectivity {
Connectivity::Eight => &DIR8_DX,
Connectivity::Four => &DIR4_DX,
};
let dy_table: &[i32] = match connectivity {
Connectivity::Eight => &DIR8_DY,
Connectivity::Four => &DIR4_DY,
};
let mut coords: Vec<(f64, f64)> = Vec::new();
coords.push((start_x as f64 + 0.5, start_y as f64 + 0.5));
visited[start_y * width + start_x] = true;
let mut cx = start_x;
let mut cy = start_y;
let mut dir = 0usize;
for d in 0..num_dirs {
let nx = cx as i32 + dx_table[d];
let ny = cy as i32 + dy_table[d];
if !in_bounds_and_target(labels, width, height, nx, ny, target) {
dir = (d + num_dirs / 2) % num_dirs;
break;
}
}
let max_iter = width * height * 4;
let mut iter_count = 0;
loop {
iter_count += 1;
if iter_count > max_iter {
break;
}
let mut found = false;
let search_start = (dir + 1) % num_dirs;
for step in 0..num_dirs {
let d = (search_start + step) % num_dirs;
let nx = cx as i32 + dx_table[d];
let ny = cy as i32 + dy_table[d];
if in_bounds_and_target(labels, width, height, nx, ny, target) {
let nxu = nx as usize;
let nyu = ny as usize;
if nxu == start_x && nyu == start_y && coords.len() > 2 {
coords.push((start_x as f64 + 0.5, start_y as f64 + 0.5));
let signed_area = compute_signed_area(&coords);
return Some(PixelRing {
label: target,
coords,
signed_area,
});
}
cx = nxu;
cy = nyu;
visited[cy * width + cx] = true;
coords.push((cx as f64 + 0.5, cy as f64 + 0.5));
dir = (d + num_dirs / 2) % num_dirs;
found = true;
break;
}
}
if !found {
break;
}
}
if coords.len() >= 3 {
let first = coords[0];
let last = coords[coords.len() - 1];
if (first.0 - last.0).abs() > f64::EPSILON || (first.1 - last.1).abs() > f64::EPSILON {
coords.push(first);
}
let signed_area = compute_signed_area(&coords);
return Some(PixelRing {
label: target,
coords,
signed_area,
});
}
if coords.len() <= 2 {
let px = start_x as f64;
let py = start_y as f64;
let square = vec![
(px, py),
(px + 1.0, py),
(px + 1.0, py + 1.0),
(px, py + 1.0),
(px, py),
];
let signed_area = compute_signed_area(&square);
return Some(PixelRing {
label: target,
coords: square,
signed_area,
});
}
None
}
#[inline]
fn in_bounds_and_target(
labels: &[u32],
width: usize,
height: usize,
nx: i32,
ny: i32,
target: u32,
) -> bool {
if nx < 0 || ny < 0 || nx >= width as i32 || ny >= height as i32 {
return false;
}
labels[ny as usize * width + nx as usize] == target
}
pub(crate) fn compute_signed_area(coords: &[(f64, f64)]) -> f64 {
if coords.len() < 3 {
return 0.0;
}
let mut area = 0.0_f64;
let n = coords.len();
for i in 0..n {
let j = (i + 1) % n;
area += coords[i].0 * coords[j].1;
area -= coords[j].0 * coords[i].1;
}
area * 0.5
}
fn classify_and_assemble(rings: Vec<PixelRing>) -> Result<Vec<ClassifiedPolygon>> {
let mut by_label: HashMap<u32, Vec<PixelRing>> = HashMap::new();
for ring in rings {
by_label.entry(ring.label).or_default().push(ring);
}
let mut polygons = Vec::new();
for (label, mut label_rings) in by_label {
let mut exteriors: Vec<Vec<(f64, f64)>> = Vec::new();
let mut holes: Vec<Vec<(f64, f64)>> = Vec::new();
label_rings.sort_by(|a, b| {
b.signed_area
.abs()
.partial_cmp(&a.signed_area.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
for ring in &label_rings {
if ring.is_exterior() {
let mut coords = ring.coords.clone();
if compute_signed_area(&coords) < 0.0 {
coords.reverse();
}
exteriors.push(coords);
} else {
let mut coords = ring.coords.clone();
if compute_signed_area(&coords) > 0.0 {
coords.reverse();
}
holes.push(coords);
}
}
if exteriors.is_empty() && !holes.is_empty() {
let mut largest = holes.remove(0);
largest.reverse(); exteriors.push(largest);
}
if exteriors.len() == 1 {
polygons.push(ClassifiedPolygon {
label,
exterior: exteriors.into_iter().next().unwrap_or_default(),
holes,
});
} else {
let mut poly_holes: Vec<Vec<Vec<(f64, f64)>>> = vec![Vec::new(); exteriors.len()];
for hole in holes {
if let Some(test_point) = hole.first() {
let mut best_idx = 0usize;
let mut best_area = f64::MAX;
for (i, ext) in exteriors.iter().enumerate() {
if point_in_ring(test_point.0, test_point.1, ext) {
let area = compute_signed_area(ext).abs();
if area < best_area {
best_area = area;
best_idx = i;
}
}
}
poly_holes[best_idx].push(hole);
}
}
for (i, ext) in exteriors.into_iter().enumerate() {
let h = if i < poly_holes.len() {
std::mem::take(&mut poly_holes[i])
} else {
Vec::new()
};
polygons.push(ClassifiedPolygon {
label,
exterior: ext,
holes: h,
});
}
}
}
polygons.sort_by_key(|p| p.label);
Ok(polygons)
}
fn point_in_ring(px: f64, py: f64, ring: &[(f64, f64)]) -> bool {
if ring.len() < 3 {
return false;
}
let mut inside = false;
let n = ring.len();
let mut j = n - 1;
for i in 0..n {
let (xi, yi) = ring[i];
let (xj, yj) = ring[j];
if ((yi > py) != (yj > py)) && (px < (xj - xi) * (py - yi) / (yj - yi) + xi) {
inside = !inside;
}
j = i;
}
inside
}
pub(crate) fn extract_pixel_edge_boundaries(
labels: &[u32],
width: usize,
height: usize,
) -> Result<Vec<ClassifiedPolygon>> {
if labels.len() != width * height {
return Err(AlgorithmError::InvalidInput(
"label grid size does not match width*height".to_string(),
));
}
let mut label_set: Vec<u32> = Vec::new();
{
let mut seen = std::collections::HashSet::new();
for &lbl in labels {
if lbl != 0 && seen.insert(lbl) {
label_set.push(lbl);
}
}
}
label_set.sort_unstable();
let mut all_polygons = Vec::new();
for &target in &label_set {
let mask: Vec<bool> = labels.iter().map(|&l| l == target).collect();
let edges = extract_boundary_edges(&mask, width, height);
let rings = assemble_edge_rings(&edges);
let mut exteriors: Vec<Vec<(f64, f64)>> = Vec::new();
let mut holes: Vec<Vec<(f64, f64)>> = Vec::new();
for ring in &rings {
let area = compute_signed_area(ring);
if area >= 0.0 {
exteriors.push(ring.clone());
} else {
holes.push(ring.clone());
}
}
if exteriors.is_empty() && !holes.is_empty() {
holes.sort_by(|a, b| {
compute_signed_area(b)
.abs()
.partial_cmp(&compute_signed_area(a).abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut largest = holes.remove(0);
largest.reverse();
exteriors.push(largest);
}
if exteriors.len() == 1 {
all_polygons.push(ClassifiedPolygon {
label: target,
exterior: exteriors.into_iter().next().unwrap_or_default(),
holes,
});
} else {
let mut poly_holes: Vec<Vec<Vec<(f64, f64)>>> = vec![Vec::new(); exteriors.len()];
for hole in holes {
if let Some(test_point) = hole.first() {
let mut best_idx = 0usize;
let mut best_area = f64::MAX;
for (i, ext) in exteriors.iter().enumerate() {
if point_in_ring(test_point.0, test_point.1, ext) {
let area = compute_signed_area(ext).abs();
if area < best_area {
best_area = area;
best_idx = i;
}
}
}
poly_holes[best_idx].push(hole);
}
}
for (i, ext) in exteriors.into_iter().enumerate() {
let h = if i < poly_holes.len() {
std::mem::take(&mut poly_holes[i])
} else {
Vec::new()
};
all_polygons.push(ClassifiedPolygon {
label: target,
exterior: ext,
holes: h,
});
}
}
}
all_polygons.sort_by_key(|p| p.label);
Ok(all_polygons)
}
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
struct EdgeSegment {
x0: i32,
y0: i32,
x1: i32,
y1: i32,
}
fn extract_boundary_edges(mask: &[bool], width: usize, height: usize) -> Vec<EdgeSegment> {
let mut edges = Vec::new();
for y in 0..height {
for x in 0..width {
if !mask[y * width + x] {
continue;
}
if y == 0 || !mask[(y - 1) * width + x] {
edges.push(EdgeSegment {
x0: x as i32,
y0: y as i32,
x1: x as i32 + 1,
y1: y as i32,
});
}
if y == height - 1 || !mask[(y + 1) * width + x] {
edges.push(EdgeSegment {
x0: x as i32 + 1,
y0: y as i32 + 1,
x1: x as i32,
y1: y as i32 + 1,
});
}
if x == 0 || !mask[y * width + x - 1] {
edges.push(EdgeSegment {
x0: x as i32,
y0: y as i32 + 1,
x1: x as i32,
y1: y as i32,
});
}
if x == width - 1 || !mask[y * width + x + 1] {
edges.push(EdgeSegment {
x0: x as i32 + 1,
y0: y as i32,
x1: x as i32 + 1,
y1: y as i32 + 1,
});
}
}
}
edges
}
fn assemble_edge_rings(edges: &[EdgeSegment]) -> Vec<Vec<(f64, f64)>> {
if edges.is_empty() {
return Vec::new();
}
let mut adjacency: HashMap<(i32, i32), Vec<usize>> = HashMap::with_capacity(edges.len());
for (i, edge) in edges.iter().enumerate() {
adjacency.entry((edge.x0, edge.y0)).or_default().push(i);
}
let mut used = vec![false; edges.len()];
let mut rings: Vec<Vec<(f64, f64)>> = Vec::new();
for start_idx in 0..edges.len() {
if used[start_idx] {
continue;
}
let mut ring = Vec::new();
let mut current_idx = start_idx;
loop {
if used[current_idx] {
break;
}
used[current_idx] = true;
let edge = &edges[current_idx];
ring.push((edge.x0 as f64, edge.y0 as f64));
let end_point = (edge.x1, edge.y1);
let mut found_next = false;
if let Some(candidates) = adjacency.get(&end_point) {
for &cand_idx in candidates {
if !used[cand_idx] {
current_idx = cand_idx;
found_next = true;
break;
}
}
}
if !found_next {
break;
}
}
if ring.len() >= 3 {
let first = ring[0];
ring.push(first);
rings.push(ring);
}
}
rings
}
pub(crate) fn transform_coords(
coords: &[(f64, f64)],
gt: &oxigdal_core::types::GeoTransform,
) -> Vec<Coordinate> {
coords
.iter()
.map(|&(px, py)| {
let (wx, wy) = gt.pixel_to_world(px, py);
Coordinate::new_2d(wx, wy)
})
.collect()
}
pub(crate) fn pixel_coords_to_coordinates(coords: &[(f64, f64)]) -> Vec<Coordinate> {
coords
.iter()
.map(|&(px, py)| Coordinate::new_2d(px, py))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signed_area_ccw_square() {
let coords = vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)];
let area = compute_signed_area(&coords);
assert!(
area > 0.0,
"CCW square should have positive area, got {area}"
);
assert!((area - 1.0).abs() < 1e-10);
}
#[test]
fn test_signed_area_cw_square() {
let coords = vec![(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)];
let area = compute_signed_area(&coords);
assert!(
area < 0.0,
"CW square should have negative area, got {area}"
);
assert!((area + 1.0).abs() < 1e-10);
}
#[test]
fn test_signed_area_degenerate() {
let coords = vec![(0.0, 0.0), (1.0, 0.0)];
assert!((compute_signed_area(&coords)).abs() < 1e-10);
}
#[test]
fn test_signed_area_empty() {
assert!((compute_signed_area(&[])).abs() < 1e-10);
}
#[test]
fn test_point_in_ring_inside() {
let ring = vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
];
assert!(point_in_ring(5.0, 5.0, &ring));
}
#[test]
fn test_point_in_ring_outside() {
let ring = vec![
(0.0, 0.0),
(10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
(0.0, 0.0),
];
assert!(!point_in_ring(15.0, 5.0, &ring));
}
#[test]
fn test_point_in_ring_degenerate() {
assert!(!point_in_ring(0.0, 0.0, &[]));
assert!(!point_in_ring(0.0, 0.0, &[(0.0, 0.0), (1.0, 0.0)]));
}
#[test]
fn test_is_boundary_pixel_center() {
let labels = vec![1, 1, 1, 1, 1, 1, 1, 1, 1];
assert!(!is_boundary_pixel(&labels, 3, 3, 1, 1, 1));
}
#[test]
fn test_is_boundary_pixel_edge() {
let labels = vec![1, 1, 1, 1, 1, 1, 1, 1, 1];
assert!(is_boundary_pixel(&labels, 3, 3, 0, 0, 1));
}
#[test]
fn test_is_boundary_pixel_adjacent_to_different() {
let labels = vec![1, 2, 1, 1, 1, 1, 1, 1, 1];
assert!(is_boundary_pixel(&labels, 3, 3, 0, 0, 1));
}
#[test]
fn test_extract_edges_single_pixel() {
let mask = vec![true];
let edges = extract_boundary_edges(&mask, 1, 1);
assert_eq!(edges.len(), 4);
}
#[test]
fn test_extract_edges_2x2_solid() {
let mask = vec![true, true, true, true];
let edges = extract_boundary_edges(&mask, 2, 2);
assert_eq!(edges.len(), 8);
}
#[test]
fn test_assemble_edge_rings_square() {
let edges = vec![
EdgeSegment {
x0: 0,
y0: 0,
x1: 1,
y1: 0,
},
EdgeSegment {
x0: 1,
y0: 0,
x1: 1,
y1: 1,
},
EdgeSegment {
x0: 1,
y0: 1,
x1: 0,
y1: 1,
},
EdgeSegment {
x0: 0,
y0: 1,
x1: 0,
y1: 0,
},
];
let rings = assemble_edge_rings(&edges);
assert_eq!(rings.len(), 1);
assert_eq!(rings[0].len(), 5); }
#[test]
fn test_pixel_edge_single_pixel() {
let labels = vec![1u32];
let result = extract_pixel_edge_boundaries(&labels, 1, 1);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some());
let polys = polys.as_ref();
assert!(polys.is_some_and(|p| p.len() == 1));
if let Some(polys) = polys {
assert_eq!(polys[0].label, 1);
assert!(polys[0].exterior.len() >= 4);
}
}
#[test]
fn test_pixel_edge_two_regions() {
let labels = vec![1u32, 2];
let result = extract_pixel_edge_boundaries(&labels, 2, 1);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some());
let polys = polys.as_ref();
assert!(polys.is_some_and(|p| p.len() == 2));
}
#[test]
fn test_pixel_edge_with_hole() {
#[rustfmt::skip]
let labels = vec![
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1,
];
let result = extract_pixel_edge_boundaries(&labels, 5, 5);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some());
if let Some(polys) = polys {
assert_eq!(polys.len(), 1);
assert_eq!(polys[0].label, 1);
assert!(!polys[0].exterior.is_empty());
assert_eq!(polys[0].holes.len(), 1);
}
}
#[test]
fn test_pixel_edge_empty_grid() {
let labels = vec![0u32; 9];
let result = extract_pixel_edge_boundaries(&labels, 3, 3);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some_and(|p| p.is_empty()));
}
#[test]
fn test_pixel_edge_size_mismatch() {
let labels = vec![1u32, 2, 3]; let result = extract_pixel_edge_boundaries(&labels, 2, 2); assert!(result.is_err());
}
#[test]
fn test_transform_coords() {
let gt = oxigdal_core::types::GeoTransform::north_up(100.0, 200.0, 10.0, -10.0);
let pixel_coords = vec![(0.0, 0.0), (1.0, 1.0)];
let world_coords = transform_coords(&pixel_coords, >);
assert_eq!(world_coords.len(), 2);
assert!((world_coords[0].x - 100.0).abs() < 1e-10);
assert!((world_coords[0].y - 200.0).abs() < 1e-10);
assert!((world_coords[1].x - 110.0).abs() < 1e-10);
assert!((world_coords[1].y - 190.0).abs() < 1e-10);
}
#[test]
fn test_pixel_coords_to_coordinates() {
let coords = vec![(1.5, 2.5), (3.0, 4.0)];
let result = pixel_coords_to_coordinates(&coords);
assert_eq!(result.len(), 2);
assert!((result[0].x - 1.5).abs() < 1e-10);
assert!((result[0].y - 2.5).abs() < 1e-10);
}
#[test]
fn test_connectivity_default() {
assert_eq!(Connectivity::default(), Connectivity::Eight);
}
#[test]
fn test_trace_boundaries_single_region() {
let labels = vec![1u32; 9];
let result = trace_boundaries(&labels, 3, 3, Connectivity::Eight);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some());
if let Some(polys) = polys {
assert!(!polys.is_empty());
assert_eq!(polys[0].label, 1);
}
}
#[test]
fn test_trace_boundaries_two_separate_regions() {
let labels = vec![1u32, 1, 0, 2, 2];
let result = trace_boundaries(&labels, 5, 1, Connectivity::Four);
assert!(result.is_ok());
let polys = result.ok();
assert!(polys.is_some());
if let Some(polys) = polys {
assert_eq!(polys.len(), 2);
}
}
#[test]
fn test_trace_boundaries_size_mismatch() {
let labels = vec![1u32; 5];
let result = trace_boundaries(&labels, 3, 3, Connectivity::Eight);
assert!(result.is_err());
}
#[test]
fn test_pixel_ring_is_exterior() {
let ring = PixelRing {
label: 1,
coords: vec![(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0)],
signed_area: 1.0,
};
assert!(ring.is_exterior());
let hole = PixelRing {
label: 1,
coords: vec![(0.0, 0.0), (0.0, 1.0), (1.0, 1.0), (1.0, 0.0), (0.0, 0.0)],
signed_area: -1.0,
};
assert!(!hole.is_exterior());
}
}