///|
/// Represents a closed boundary contour with an ordered array of 2D points.
pub(all) struct Contour {
points : Array[Point2D]
} derive(Debug, Eq)
///|
/// Creates a new Contour.
pub fn Contour::new(points : Array[Point2D]) -> Contour {
{ points, }
}
///|
/// Computes the enclosed geometric area using Gauss's Shoelace formula.
pub fn Contour::area(self : Contour) -> Double {
let n = self.points.length()
if n < 3 {
return 0.0
}
let mut sum = 0.0
for i in 0.. Double {
let n = self.points.length()
if n < 2 {
return 0.0
}
let mut total = 0.0
for i in 0.. Point2D {
let n = self.points.length()
if n == 0 {
return { x: 0.0, y: 0.0 }
}
let mut sx = 0.0
let mut sy = 0.0
for p in self.points {
sx += p.x
sy += p.y
}
{ x: sx / n.to_double(), y: sy / n.to_double() }
}
///|
/// Computes bounding rectangle enclosing this contour.
pub fn Contour::bounding_rect(self : Contour) -> Rect2D {
let n = self.points.length()
if n == 0 {
return { x: 0.0, y: 0.0, width: 0.0, height: 0.0 }
}
let mut min_x = self.points[0].x
let mut max_x = self.points[0].x
let mut min_y = self.points[0].y
let mut max_y = self.points[0].y
for p in self.points {
if p.x < min_x {
min_x = p.x
}
if p.x > max_x {
max_x = p.x
}
if p.y < min_y {
min_y = p.y
}
if p.y > max_y {
max_y = p.y
}
}
{ x: min_x, y: min_y, width: max_x - min_x, height: max_y - min_y }
}
///|
/// Computes 7 Hu invariant moments from contour points.
pub fn Contour::hu_moments(self : Contour) -> Array[Double] {
let n = self.points.length()
if n < 3 {
return Array::make(7, 0.0)
}
// Spatial moments m_pq using Green's theorem on polygon
let mut m00 = 0.0
let mut m10 = 0.0
let mut m01 = 0.0
let mut m20 = 0.0
let mut m02 = 0.0
let mut m11 = 0.0
let mut m30 = 0.0
let mut m03 = 0.0
let mut m21 = 0.0
let mut m12 = 0.0
for i in 0.. Array[Contour] {
let w = binary_img.width
let h = binary_img.height
let min_pts = min_points
let contours : Array[Contour] = []
let visited = Array::make(w * h, false)
// 8 neighborhood offsets in clockwise order: E, SE, S, SW, W, NW, N, NE
let dxs : Array[Int] = [1, 1, 0, -1, -1, -1, 0, 1]
let dys : Array[Int] = [0, 1, 1, 1, 0, -1, -1, -1]
for y in 0.. 0 && !visited[y * w + x] {
// Check if starting border pixel (has background neighbor)
let is_border = x == 0 ||
y == 0 ||
x == w - 1 ||
y == h - 1 ||
binary_img.get(x - 1, y) == 0 ||
binary_img.get(x + 1, y) == 0 ||
binary_img.get(x, y - 1) == 0 ||
binary_img.get(x, y + 1) == 0
if is_border {
let points : Array[Point2D] = []
let start_x = x
let start_y = y
let mut curr_x = x
let mut curr_y = y
let mut dir = 0
let max_trace = w * h * 2
let mut trace_count = 0
for ;; {
points.push(Point2D::new(curr_x.to_double(), curr_y.to_double()))
visited[curr_y * w + curr_x] = true
trace_count += 1
// Search next border pixel in clockwise order starting from (dir + 5) % 8
let mut next_found = false
let search_start = (dir + 5) % 8
for step in 0..<8 {
let next_dir = (search_start + step) % 8
let nx = curr_x + dxs[next_dir]
let ny = curr_y + dys[next_dir]
if nx >= 0 && nx < w && ny >= 0 && ny < h {
if binary_img.get(nx, ny) > 0 {
curr_x = nx
curr_y = ny
dir = next_dir
next_found = true
break
}
}
}
if !next_found || trace_count >= max_trace {
break
}
if curr_x == start_x && curr_y == start_y && points.length() >= 3 {
break
}
}
if points.length() >= min_pts {
contours.push(Contour::new(points))
}
}
}
}
}
contours
}