use super::types::{BoundingBox, ChangeShape, ShapeStats};
pub fn compute_shape_stats(
mask: &[bool],
width: u32,
bbox: &BoundingBox,
pixel_count: u32,
) -> ShapeStats {
let bbox_area = (bbox.width as u64) * (bbox.height as u64);
if bbox_area == 0 || pixel_count == 0 {
return ShapeStats {
fill_ratio: 0.0,
border_ratio: 0.0,
inner_fill_ratio: 0.0,
center_density: 0.0,
row_occupancy: 0.0,
col_occupancy: 0.0,
};
}
let fill_ratio = pixel_count as f64 / bbox_area as f64;
let border_band = (bbox.width.min(bbox.height) / 4).min(12).max(1);
let mut border_count: u32 = 0;
let mut inner_count: u32 = 0;
let mut rows_hit = vec![false; bbox.height as usize];
let mut cols_hit = vec![false; bbox.width as usize];
let center_x0 = bbox.width / 4;
let center_x1 = bbox.width - bbox.width / 4;
let center_y0 = bbox.height / 4;
let center_y1 = bbox.height - bbox.height / 4;
let center_area = ((center_x1 - center_x0) as u64) * ((center_y1 - center_y0) as u64);
let mut center_count: u32 = 0;
for dy in 0..bbox.height {
let y = bbox.y + dy;
for dx in 0..bbox.width {
let x = bbox.x + dx;
let idx = (y * width + x) as usize;
if !mask[idx] {
continue;
}
rows_hit[dy as usize] = true;
cols_hit[dx as usize] = true;
let in_border = dx < border_band
|| dx >= bbox.width - border_band
|| dy < border_band
|| dy >= bbox.height - border_band;
if in_border {
border_count += 1;
} else {
inner_count += 1;
}
if dx >= center_x0 && dx < center_x1 && dy >= center_y0 && dy < center_y1 {
center_count += 1;
}
}
}
let border_ratio = border_count as f64 / pixel_count as f64;
let inner_area = bbox_area.saturating_sub(
(bbox.width as u64) * (border_band as u64) * 2
+ (bbox.height.saturating_sub(border_band * 2) as u64) * (border_band as u64) * 2,
);
let inner_fill_ratio = if inner_area > 0 {
inner_count as f64 / inner_area as f64
} else {
fill_ratio };
let center_density = if center_area > 0 {
center_count as f64 / center_area as f64
} else {
fill_ratio
};
let row_occupancy = rows_hit.iter().filter(|&&v| v).count() as f64 / bbox.height as f64;
let col_occupancy = cols_hit.iter().filter(|&&v| v).count() as f64 / bbox.width as f64;
ShapeStats {
fill_ratio,
border_ratio,
inner_fill_ratio,
center_density,
row_occupancy,
col_occupancy,
}
}
pub fn classify_shape(stats: &ShapeStats) -> ChangeShape {
if stats.fill_ratio > 0.65 {
return ChangeShape::SolidRegion;
}
if stats.inner_fill_ratio < 0.20
&& (stats.border_ratio > 0.60
|| (stats.center_density < 0.10
&& stats.border_ratio > 0.30
&& stats.fill_ratio < 0.50))
{
return ChangeShape::ContourFrame;
}
if stats.fill_ratio < 0.30 && stats.border_ratio > 0.45 {
return ChangeShape::EdgeDominated;
}
if stats.fill_ratio < 0.30 && stats.row_occupancy > 0.7 && stats.col_occupancy > 0.7 {
return ChangeShape::SparseDistributed;
}
ChangeShape::MixedRegion
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_solid_block() {
let mask = vec![true; 100]; let bbox = BoundingBox {
x: 0,
y: 0,
width: 10,
height: 10,
};
let stats = compute_shape_stats(&mask, 10, &bbox, 100);
assert!((stats.fill_ratio - 1.0).abs() < f64::EPSILON);
assert!((stats.row_occupancy - 1.0).abs() < f64::EPSILON);
assert!((stats.col_occupancy - 1.0).abs() < f64::EPSILON);
assert!(stats.inner_fill_ratio > 0.9);
assert!(stats.center_density > 0.9);
assert_eq!(classify_shape(&stats), ChangeShape::SolidRegion);
}
#[test]
fn test_hollow_rectangle() {
let width: u32 = 20;
let height: u32 = 20;
let mut mask = vec![false; (width * height) as usize];
let mut pixel_count: u32 = 0;
for y in 0..height {
for x in 0..width {
if x == 0 || x == width - 1 || y == 0 || y == height - 1 {
mask[(y * width + x) as usize] = true;
pixel_count += 1;
}
}
}
let bbox = BoundingBox {
x: 0,
y: 0,
width,
height,
};
let stats = compute_shape_stats(&mask, width, &bbox, pixel_count);
assert!(stats.fill_ratio < 0.30);
assert!(stats.border_ratio > 0.45);
assert!(stats.inner_fill_ratio < 0.05);
assert_eq!(classify_shape(&stats), ChangeShape::ContourFrame);
}
#[test]
fn test_thick_hollow_frame() {
let width: u32 = 40;
let height: u32 = 40;
let mut mask = vec![false; (width * height) as usize];
let mut pixel_count: u32 = 0;
for y in 0..height {
for x in 0..width {
if x < 4 || x >= width - 4 || y < 4 || y >= height - 4 {
mask[(y * width + x) as usize] = true;
pixel_count += 1;
}
}
}
let bbox = BoundingBox {
x: 0,
y: 0,
width,
height,
};
let stats = compute_shape_stats(&mask, width, &bbox, pixel_count);
assert!(stats.inner_fill_ratio < 0.05);
assert!(stats.center_density < 0.05);
assert_eq!(classify_shape(&stats), ChangeShape::ContourFrame);
}
#[test]
fn test_scattered_pixels() {
let width: u32 = 100;
let height: u32 = 100;
let mut mask = vec![false; (width * height) as usize];
let mut pixel_count: u32 = 0;
for y in 0..height {
let x = y % width;
mask[(y * width + x) as usize] = true;
pixel_count += 1;
}
let bbox = BoundingBox {
x: 0,
y: 0,
width,
height,
};
let stats = compute_shape_stats(&mask, width, &bbox, pixel_count);
assert!(stats.fill_ratio < 0.30);
assert!((stats.row_occupancy - 1.0).abs() < f64::EPSILON);
assert!((stats.col_occupancy - 1.0).abs() < f64::EPSILON);
assert_eq!(classify_shape(&stats), ChangeShape::SparseDistributed);
}
#[test]
fn test_center_density_concentrated() {
let width: u32 = 20;
let height: u32 = 20;
let mut mask = vec![false; (width * height) as usize];
let mut pixel_count: u32 = 0;
for y in 5..15 {
for x in 5..15 {
mask[(y * width + x) as usize] = true;
pixel_count += 1;
}
}
let bbox = BoundingBox {
x: 5,
y: 5,
width: 10,
height: 10,
};
let stats = compute_shape_stats(&mask, width, &bbox, pixel_count);
assert!(stats.center_density > 0.8);
assert_eq!(classify_shape(&stats), ChangeShape::SolidRegion);
}
#[test]
fn test_hollow_frame_empty_center() {
let width: u32 = 100;
let height: u32 = 100;
let mut mask = vec![false; (width * height) as usize];
let mut pixel_count: u32 = 0;
for y in 0..height {
for x in 0..width {
let on_outer = x < 15 || x >= width - 15 || y < 15 || y >= height - 15;
if on_outer && (x + y) % 5 == 0 {
mask[(y * width + x) as usize] = true;
pixel_count += 1;
}
}
}
let bbox = BoundingBox {
x: 0,
y: 0,
width,
height,
};
let stats = compute_shape_stats(&mask, width, &bbox, pixel_count);
assert!(stats.center_density < 0.10);
assert!(stats.inner_fill_ratio < 0.20);
assert_eq!(classify_shape(&stats), ChangeShape::ContourFrame);
}
#[test]
fn test_empty_bbox() {
let mask = vec![false; 100];
let bbox = BoundingBox {
x: 0,
y: 0,
width: 10,
height: 10,
};
let stats = compute_shape_stats(&mask, 10, &bbox, 0);
assert!((stats.fill_ratio).abs() < f64::EPSILON);
assert!((stats.border_ratio).abs() < f64::EPSILON);
assert!((stats.inner_fill_ratio).abs() < f64::EPSILON);
assert!((stats.center_density).abs() < f64::EPSILON);
assert!((stats.row_occupancy).abs() < f64::EPSILON);
assert!((stats.col_occupancy).abs() < f64::EPSILON);
}
}