use crate::{Coordinate, CoordinateF64, 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(at: Coordinate) -> Self {
Self {
area: 1,
bbox_min: at,
bbox_max_inclusive: at,
sum_x: at.x as u64,
sum_y: at.y as u64,
}
}
#[inline]
pub(super) fn extend(&mut self, at: Coordinate) {
self.area += 1;
if at.x < self.bbox_min.x {
self.bbox_min.x = at.x;
}
if at.y < self.bbox_min.y {
self.bbox_min.y = at.y;
}
if at.x > self.bbox_max_inclusive.x {
self.bbox_max_inclusive.x = at.x;
}
if at.y > self.bbox_max_inclusive.y {
self.bbox_max_inclusive.y = at.y;
}
self.sum_x += at.x as u64;
self.sum_y += at.y as u64;
}
pub fn centroid(&self) -> CoordinateF64 {
let inv = 1.0 / self.area as f64;
CoordinateF64::new(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::super::measurements::BlobMeasurements;
use super::ComponentStats;
use crate::Coordinate;
pub(crate) trait StatsSink {
const NEEDS_BOUNDARY: bool = false;
fn record(&mut self, compact_label: u32, first: bool, at: Coordinate, is_boundary: bool);
}
pub(crate) struct NoStats;
impl StatsSink for NoStats {
#[inline(always)]
fn record(
&mut self,
_compact_label: u32,
_first: bool,
_at: Coordinate,
_is_boundary: bool,
) {
}
}
pub(crate) struct WithStats<'a> {
pub(crate) out: &'a mut Vec<ComponentStats>,
}
impl StatsSink for WithStats<'_> {
#[inline]
fn record(&mut self, compact_label: u32, first: bool, at: Coordinate, _is_boundary: bool) {
if first {
debug_assert_eq!(self.out.len(), (compact_label - 1) as usize);
self.out.push(ComponentStats::from_seed(at));
} else {
self.out[(compact_label - 1) as usize].extend(at);
}
}
}
pub(crate) struct WithMeasurements<'a> {
pub(crate) out: &'a mut Vec<BlobMeasurements>,
}
impl StatsSink for WithMeasurements<'_> {
const NEEDS_BOUNDARY: bool = true;
#[inline]
fn record(&mut self, compact_label: u32, first: bool, at: Coordinate, is_boundary: bool) {
if first {
debug_assert_eq!(self.out.len(), (compact_label - 1) as usize);
self.out.push(BlobMeasurements::from_seed(at, is_boundary));
} else {
self.out[(compact_label - 1) as usize].extend(at, is_boundary);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_seed_single_pixel() {
let s = ComponentStats::from_seed(Coordinate::new(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(), CoordinateF64::new(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(Coordinate::new(2, 2));
s.extend(Coordinate::new(5, 4));
s.extend(Coordinate::new(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(Coordinate::new(1, 1));
for y in 1..=3usize {
for x in 1..=3usize {
if (x, y) != (1, 1) {
s.extend(Coordinate::new(x, y));
}
}
}
assert_eq!(s.area, 9);
let c = s.centroid();
assert!((c.x - 2.0).abs() < 1e-12);
assert!((c.y - 2.0).abs() < 1e-12);
}
#[test]
fn bbox_is_half_open_rectangle() {
let mut s = ComponentStats::from_seed(Coordinate::new(2, 3));
s.extend(Coordinate::new(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(Coordinate::new(0, 0));
s.extend(Coordinate::new(5, 1));
assert!((s.aspect_ratio() - 3.0).abs() < 1e-12);
let mut t = ComponentStats::from_seed(Coordinate::new(0, 0));
t.extend(Coordinate::new(1, 5));
assert!((t.aspect_ratio() - (2.0 / 6.0)).abs() < 1e-12);
}
}