Skip to main content

grana_object/
surface.rs

1use crate::{Bounds, IntensityTable, Mesh, PackedMesh};
2
3#[derive(Clone, Copy, Debug, PartialEq)]
4pub struct SurfaceStatistics {
5    voxel_count: u64,
6    volume: f64,
7    area: f64,
8    sphericity: f64,
9    centroid: [f32; 3],
10    bounds: Bounds,
11}
12
13impl SurfaceStatistics {
14    pub fn new(
15        voxel_count: u64,
16        volume: f64,
17        area: f64,
18        centroid: [f32; 3],
19        bounds: Bounds,
20    ) -> Self {
21        assert!(voxel_count > 0, "a surface encloses at least one voxel");
22        assert!(
23            volume.is_finite() && volume > 0.0,
24            "a surface volume is a positive volume"
25        );
26        assert!(
27            area.is_finite() && area > 0.0,
28            "a surface area is a positive area"
29        );
30        assert!(
31            centroid.iter().all(|value| value.is_finite()),
32            "a surface centroid must be finite"
33        );
34        let sphericity =
35            (std::f64::consts::PI.cbrt() * (6.0 * volume).powf(2.0 / 3.0) / area).clamp(0.0, 1.0);
36        Self {
37            voxel_count,
38            volume,
39            area,
40            sphericity,
41            centroid,
42            bounds,
43        }
44    }
45
46    pub fn voxel_count(self) -> u64 {
47        self.voxel_count
48    }
49
50    pub fn volume(self) -> f64 {
51        self.volume
52    }
53
54    pub fn area(self) -> f64 {
55        self.area
56    }
57
58    pub fn sphericity(self) -> f64 {
59        self.sphericity
60    }
61
62    pub fn centroid(self) -> [f32; 3] {
63        self.centroid
64    }
65
66    pub fn bounds(self) -> Bounds {
67        self.bounds
68    }
69}
70
71#[derive(Clone, Debug, PartialEq)]
72pub struct SurfaceSet {
73    level: usize,
74    geometry: PackedMesh,
75    objects: Vec<SurfaceStatistics>,
76    intensities: IntensityTable,
77}
78
79impl SurfaceSet {
80    pub fn new(
81        level: usize,
82        mesh: Mesh,
83        objects: Vec<SurfaceStatistics>,
84        intensities: IntensityTable,
85    ) -> Self {
86        let geometry = PackedMesh::new(mesh);
87        assert_eq!(
88            geometry.object_count(),
89            objects.len(),
90            "every packed surface names an object"
91        );
92        assert_eq!(
93            geometry.is_empty(),
94            objects.is_empty(),
95            "the mesh and the object list agree on emptiness"
96        );
97        assert_eq!(
98            intensities.objects(),
99            objects.len(),
100            "every surface is measured in every channel"
101        );
102        Self {
103            level,
104            geometry,
105            objects,
106            intensities,
107        }
108    }
109
110    pub fn from_parts(
111        level: usize,
112        geometry: PackedMesh,
113        objects: Vec<SurfaceStatistics>,
114        intensities: IntensityTable,
115    ) -> Option<Self> {
116        if geometry.object_count() != objects.len()
117            || geometry.is_empty() != objects.is_empty()
118            || intensities.objects() != objects.len()
119        {
120            return None;
121        }
122        Some(Self {
123            level,
124            geometry,
125            objects,
126            intensities,
127        })
128    }
129
130    pub fn level(&self) -> usize {
131        self.level
132    }
133
134    pub fn geometry(&self) -> &PackedMesh {
135        &self.geometry
136    }
137
138    pub fn objects(&self) -> &[SurfaceStatistics] {
139        &self.objects
140    }
141
142    pub fn intensities(&self) -> &IntensityTable {
143        &self.intensities
144    }
145
146    pub fn object(&self, index: usize) -> Option<SurfaceStatistics> {
147        self.objects.get(index).copied()
148    }
149
150    pub fn object_count(&self) -> usize {
151        self.objects.len()
152    }
153
154    pub fn is_empty(&self) -> bool {
155        self.objects.is_empty()
156    }
157}