use crate::{Coordinate, Rectangle, Size};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ComponentStats {
pub area: u64,
pub bbox_min: Coordinate,
pub bbox_max_inclusive: Coordinate,
pub sum_x: u64,
pub sum_y: u64,
}
impl ComponentStats {
#[inline]
pub(super) fn from_seed(x: usize, y: usize) -> Self {
let c = Coordinate::new(x, y);
Self {
area: 1,
bbox_min: c,
bbox_max_inclusive: c,
sum_x: x as u64,
sum_y: y as u64,
}
}
#[inline]
pub(super) fn extend(&mut self, x: usize, y: usize) {
self.area += 1;
if x < self.bbox_min.x {
self.bbox_min.x = x;
}
if y < self.bbox_min.y {
self.bbox_min.y = y;
}
if x > self.bbox_max_inclusive.x {
self.bbox_max_inclusive.x = x;
}
if y > self.bbox_max_inclusive.y {
self.bbox_max_inclusive.y = y;
}
self.sum_x += x as u64;
self.sum_y += y as u64;
}
pub fn centroid(&self) -> (f64, f64) {
let inv = 1.0 / self.area as f64;
(self.sum_x as f64 * inv, self.sum_y as f64 * inv)
}
pub fn bbox(&self) -> Rectangle {
let w = self.bbox_max_inclusive.x - self.bbox_min.x + 1;
let h = self.bbox_max_inclusive.y - self.bbox_min.y + 1;
Rectangle::new(self.bbox_min, Size::new(w, h))
}
pub fn aspect_ratio(&self) -> f64 {
let r = self.bbox();
r.size.width as f64 / r.size.height as f64
}
}
pub(super) mod sink {
use super::ComponentStats;
pub(crate) trait StatsSink {
fn record(&mut self, compact_label: u64, first: bool, x: usize, y: usize);
}
pub(crate) struct NoStats;
impl StatsSink for NoStats {
#[inline(always)]
fn record(&mut self, _compact_label: u64, _first: bool, _x: usize, _y: usize) {}
}
pub(crate) struct WithStats<'a> {
pub(crate) out: &'a mut Vec<ComponentStats>,
}
impl StatsSink for WithStats<'_> {
#[inline]
fn record(&mut self, compact_label: u64, first: bool, x: usize, y: usize) {
if first {
debug_assert_eq!(self.out.len() as u64, compact_label - 1);
self.out.push(ComponentStats::from_seed(x, y));
} else {
self.out[(compact_label - 1) as usize].extend(x, y);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_seed_single_pixel() {
let s = ComponentStats::from_seed(3, 5);
assert_eq!(s.area, 1);
assert_eq!(s.bbox_min, Coordinate::new(3, 5));
assert_eq!(s.bbox_max_inclusive, Coordinate::new(3, 5));
assert_eq!(s.sum_x, 3);
assert_eq!(s.sum_y, 5);
assert_eq!(s.centroid(), (3.0, 5.0));
assert_eq!(
s.bbox(),
Rectangle::new(Coordinate::new(3, 5), Size::new(1, 1))
);
assert_eq!(s.aspect_ratio(), 1.0);
}
#[test]
fn extend_grows_area_and_bbox() {
let mut s = ComponentStats::from_seed(2, 2);
s.extend(5, 4);
s.extend(3, 1);
assert_eq!(s.area, 3);
assert_eq!(s.bbox_min, Coordinate::new(2, 1));
assert_eq!(s.bbox_max_inclusive, Coordinate::new(5, 4));
assert_eq!(s.sum_x, 2 + 5 + 3);
assert_eq!(s.sum_y, 2 + 4 + 1);
}
#[test]
fn centroid_of_centred_square() {
let mut s = ComponentStats::from_seed(1, 1);
for y in 1..=3usize {
for x in 1..=3usize {
if (x, y) != (1, 1) {
s.extend(x, y);
}
}
}
assert_eq!(s.area, 9);
let (cx, cy) = s.centroid();
assert!((cx - 2.0).abs() < 1e-12);
assert!((cy - 2.0).abs() < 1e-12);
}
#[test]
fn bbox_is_half_open_rectangle() {
let mut s = ComponentStats::from_seed(2, 3);
s.extend(7, 9);
let r = s.bbox();
assert_eq!(r.offset, Coordinate::new(2, 3));
assert_eq!(r.size, Size::new(6, 7));
}
#[test]
fn aspect_ratio_wide_versus_tall() {
let mut s = ComponentStats::from_seed(0, 0);
s.extend(5, 1);
assert!((s.aspect_ratio() - 3.0).abs() < 1e-12);
let mut t = ComponentStats::from_seed(0, 0);
t.extend(1, 5);
assert!((t.aspect_ratio() - (2.0 / 6.0)).abs() < 1e-12);
}
}