use crate::{Coordinate, CoordinateF64};
use super::chain::ChainCode;
use super::polygon::{convex_hull, polygon_area, polygon_centroid, polygon_perimeter};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ContourKind {
Outer,
Hole,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Contour {
points: Vec<Coordinate>,
kind: ContourKind,
}
impl Contour {
pub(super) fn new(points: Vec<Coordinate>, kind: ContourKind) -> Self {
debug_assert!(
!points.is_empty(),
"a traced contour has at least one point"
);
Self { points, kind }
}
#[must_use]
pub fn points(&self) -> &[Coordinate] {
&self.points
}
#[must_use]
pub const fn kind(&self) -> ContourKind {
self.kind
}
#[must_use]
pub fn area(&self) -> f64 {
polygon_area(&self.points)
}
#[must_use]
pub fn perimeter(&self) -> f64 {
polygon_perimeter(&self.points)
}
#[must_use]
pub fn centroid(&self) -> Option<CoordinateF64> {
polygon_centroid(&self.points)
}
#[must_use]
pub fn circularity(&self) -> Option<f64> {
let p = self.perimeter();
if p == 0.0 {
return None;
}
Some(4.0 * std::f64::consts::PI * self.area() / (p * p))
}
#[must_use]
pub fn convex_hull(&self) -> Vec<Coordinate> {
convex_hull(&self.points)
}
#[must_use]
pub fn solidity(&self) -> Option<f64> {
let area = self.area();
if area == 0.0 {
return None;
}
let hull_area = polygon_area(&self.convex_hull());
if hull_area == 0.0 {
return None;
}
Some(area / hull_area)
}
#[must_use]
pub fn chain_code(&self) -> ChainCode {
ChainCode::from_contour(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ComponentContour {
pub(super) outer: Contour,
pub(super) holes: Vec<Contour>,
pub(super) enclosing: Option<usize>,
}
impl ComponentContour {
#[must_use]
pub fn outer(&self) -> &Contour {
&self.outer
}
#[must_use]
pub fn holes(&self) -> &[Contour] {
&self.holes
}
#[must_use]
pub const fn enclosing(&self) -> Option<usize> {
self.enclosing
}
#[must_use]
pub fn euler_number(&self) -> i64 {
1 - self.holes.len() as i64
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ContourHierarchy {
pub(super) components: Vec<ComponentContour>,
}
impl ContourHierarchy {
#[must_use]
pub fn components(&self) -> &[ComponentContour] {
&self.components
}
#[must_use]
pub fn component_for_label(&self, label: u32) -> Option<&ComponentContour> {
let index = usize::try_from(label.checked_sub(1)?).ok()?;
self.components.get(index)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn c(x: usize, y: usize) -> Coordinate {
Coordinate::new(x, y)
}
fn square_contour() -> Contour {
let points = [
(0, 0),
(1, 0),
(2, 0),
(3, 0),
(3, 1),
(3, 2),
(3, 3),
(2, 3),
(1, 3),
(0, 3),
(0, 2),
(0, 1),
]
.map(Coordinate::from)
.to_vec();
Contour::new(points, ContourKind::Outer)
}
#[test]
fn square_descriptors() {
let sq = square_contour();
assert_eq!(sq.area(), 9.0);
assert_eq!(sq.perimeter(), 12.0);
let ctr = sq.centroid().unwrap();
assert_eq!((ctr.x, ctr.y), (1.5, 1.5));
let circ = sq.circularity().unwrap();
assert!((circ - std::f64::consts::FRAC_PI_4).abs() < 1e-12);
assert_eq!(sq.solidity(), Some(1.0));
assert_eq!(sq.convex_hull().len(), 4);
}
#[test]
fn degenerate_contour_descriptors_are_absent_not_nan() {
let dot = Contour::new(vec![c(5, 5)], ContourKind::Outer);
assert_eq!(dot.area(), 0.0);
assert_eq!(dot.perimeter(), 0.0);
assert_eq!(dot.centroid(), None);
assert_eq!(dot.circularity(), None);
assert_eq!(dot.solidity(), None);
let line = Contour::new(vec![c(1, 2), c(2, 2), c(3, 2), c(2, 2)], ContourKind::Outer);
assert_eq!(line.area(), 0.0);
assert_eq!(line.perimeter(), 4.0);
assert_eq!(line.centroid(), None);
assert_eq!(line.circularity(), Some(0.0));
assert_eq!(line.solidity(), None);
let bent = Contour::new(vec![c(1, 1), c(2, 1), c(2, 2), c(2, 1)], ContourKind::Outer);
assert_eq!(bent.area(), 0.0);
assert_eq!(bent.perimeter(), 4.0);
assert!(polygon_area(&bent.convex_hull()) > 0.0);
assert_eq!(bent.solidity(), None);
assert_eq!(bent.circularity(), Some(0.0));
}
#[test]
fn concave_shape_solidity_below_one() {
let points = [
(0, 0),
(1, 0),
(2, 0),
(2, 1),
(1, 1),
(1, 2),
(2, 2),
(2, 3),
(1, 3),
(0, 3),
(0, 2),
(0, 1),
]
.map(Coordinate::from)
.to_vec();
let l = Contour::new(points, ContourKind::Outer);
let s = l.solidity().unwrap();
assert!(s < 1.0, "solidity {s} of a concave shape must be < 1");
assert!(s > 0.0);
}
#[test]
fn euler_numbers() {
let outer = square_contour();
let solid = ComponentContour {
outer: outer.clone(),
holes: vec![],
enclosing: None,
};
assert_eq!(solid.euler_number(), 1);
let two_holes = ComponentContour {
outer: outer.clone(),
holes: vec![
Contour::new(vec![c(1, 1)], ContourKind::Hole),
Contour::new(vec![c(2, 2)], ContourKind::Hole),
],
enclosing: None,
};
assert_eq!(two_holes.euler_number(), -1);
}
#[test]
fn component_for_label_is_one_based() {
let hierarchy = ContourHierarchy {
components: vec![ComponentContour {
outer: square_contour(),
holes: vec![],
enclosing: None,
}],
};
assert!(hierarchy.component_for_label(0).is_none());
assert!(hierarchy.component_for_label(1).is_some());
assert!(hierarchy.component_for_label(2).is_none());
}
}