1#[derive(Clone, Copy, Debug, PartialEq)]
2pub struct TrackNode {
3 time: u32,
4 object: usize,
5 centroid: [f32; 3],
6}
7
8impl TrackNode {
9 pub fn new(time: u32, object: usize, centroid: [f32; 3]) -> Self {
10 assert!(
11 centroid.iter().all(|value| value.is_finite()),
12 "a track node lies at a finite centroid"
13 );
14 Self {
15 time,
16 object,
17 centroid,
18 }
19 }
20
21 pub fn time(self) -> u32 {
22 self.time
23 }
24
25 pub fn object(self) -> usize {
26 self.object
27 }
28
29 pub fn centroid(self) -> [f32; 3] {
30 self.centroid
31 }
32}
33
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct TrackStatistics {
36 frames: u32,
37 duration: f64,
38 length: f64,
39 displacement: f64,
40 speed: f64,
41 straightness: f64,
42}
43
44impl TrackStatistics {
45 pub fn frames(self) -> u32 {
46 self.frames
47 }
48
49 pub fn duration(self) -> f64 {
50 self.duration
51 }
52
53 pub fn length(self) -> f64 {
54 self.length
55 }
56
57 pub fn displacement(self) -> f64 {
58 self.displacement
59 }
60
61 pub fn speed(self) -> f64 {
62 self.speed
63 }
64
65 pub fn straightness(self) -> f64 {
66 self.straightness
67 }
68}
69
70#[derive(Clone, Debug, PartialEq)]
71pub struct Track {
72 parent: Option<u32>,
73 nodes: Vec<TrackNode>,
74}
75
76impl Track {
77 pub fn new(parent: Option<u32>, nodes: Vec<TrackNode>) -> Self {
78 assert!(!nodes.is_empty(), "a track holds at least one node");
79 for pair in nodes.windows(2) {
80 assert!(
81 pair[1].time > pair[0].time,
82 "track nodes follow the time axis"
83 );
84 }
85 Self { parent, nodes }
86 }
87
88 pub fn parent(&self) -> Option<u32> {
89 self.parent
90 }
91
92 pub fn nodes(&self) -> &[TrackNode] {
93 &self.nodes
94 }
95
96 pub fn start(&self) -> u32 {
97 self.nodes.first().expect("a track holds nodes").time
98 }
99
100 pub fn end(&self) -> u32 {
101 self.nodes.last().expect("a track holds nodes").time
102 }
103
104 pub fn statistics(&self, frame_interval: f32) -> TrackStatistics {
105 assert!(
106 frame_interval.is_finite() && frame_interval > 0.0,
107 "the frame interval is a positive duration"
108 );
109 let first = self.nodes.first().expect("a track holds nodes");
110 let last = self.nodes.last().expect("a track holds nodes");
111 let frames = last.time - first.time + 1;
112 let duration = f64::from(frames - 1) * f64::from(frame_interval);
113 let length = self
114 .nodes
115 .windows(2)
116 .map(|pair| distance(pair[0].centroid, pair[1].centroid))
117 .sum::<f64>();
118 let displacement = distance(first.centroid, last.centroid);
119 let speed = if duration > 0.0 {
120 length / duration
121 } else {
122 0.0
123 };
124 let straightness = if length > 0.0 {
125 (displacement / length).clamp(0.0, 1.0)
126 } else {
127 0.0
128 };
129 TrackStatistics {
130 frames,
131 duration,
132 length,
133 displacement,
134 speed,
135 straightness,
136 }
137 }
138}
139
140#[derive(Clone, Debug, PartialEq)]
141pub struct TrackSet {
142 frame_interval: f32,
143 tracks: Vec<Track>,
144 links: Vec<(u32, usize, u32)>,
145 divisions: usize,
146}
147
148impl TrackSet {
149 pub fn new(frame_interval: f32, tracks: Vec<Track>) -> Self {
150 assert!(
151 frame_interval.is_finite() && frame_interval > 0.0,
152 "the frame interval is a positive duration"
153 );
154 let mut children = vec![0usize; tracks.len()];
155 for (index, track) in tracks.iter().enumerate() {
156 if let Some(parent) = track.parent() {
157 let parent = parent as usize;
158 assert!(parent < index, "a track follows its parent");
159 assert!(
160 tracks[parent].end() < track.start(),
161 "a child track starts after its parent ends"
162 );
163 children[parent] += 1;
164 }
165 }
166 for count in &children {
167 assert!(
168 *count == 0 || *count == 2,
169 "a division produces exactly two daughter tracks"
170 );
171 }
172 let divisions = children.iter().filter(|count| **count == 2).count();
173 let mut links = Vec::new();
174 for (index, track) in tracks.iter().enumerate() {
175 for node in track.nodes() {
176 links.push((node.time(), node.object(), index as u32));
177 }
178 }
179 links.sort_unstable();
180 assert!(
181 links
182 .windows(2)
183 .all(|pair| (pair[0].0, pair[0].1) != (pair[1].0, pair[1].1)),
184 "every detection belongs to one track"
185 );
186 Self {
187 frame_interval,
188 tracks,
189 links,
190 divisions,
191 }
192 }
193
194 pub fn frame_interval(&self) -> f32 {
195 self.frame_interval
196 }
197
198 pub fn tracks(&self) -> &[Track] {
199 &self.tracks
200 }
201
202 pub fn len(&self) -> usize {
203 self.tracks.len()
204 }
205
206 pub fn is_empty(&self) -> bool {
207 self.tracks.is_empty()
208 }
209
210 pub fn node_count(&self) -> usize {
211 self.links.len()
212 }
213
214 pub fn divisions(&self) -> usize {
215 self.divisions
216 }
217
218 pub fn track(&self, index: usize) -> Option<&Track> {
219 self.tracks.get(index)
220 }
221
222 pub fn track_at(&self, time: u32, object: usize) -> Option<usize> {
223 self.links
224 .binary_search_by_key(&(time, object), |link| (link.0, link.1))
225 .ok()
226 .map(|index| self.links[index].2 as usize)
227 }
228
229 pub fn statistics(&self, index: usize) -> Option<TrackStatistics> {
230 self.track(index)
231 .map(|track| track.statistics(self.frame_interval))
232 }
233
234 pub fn children(&self, index: usize) -> impl Iterator<Item = usize> + '_ {
235 self.tracks
236 .iter()
237 .enumerate()
238 .filter(move |(_, track)| track.parent() == Some(index as u32))
239 .map(|(child, _)| child)
240 }
241
242 pub fn mesh(&self, radius: f32) -> crate::Mesh {
243 assert!(
244 radius.is_finite() && radius > 0.0,
245 "a track tube radius must be a positive length"
246 );
247 let mut vertices = Vec::new();
248 let mut indices = Vec::new();
249 for (index, track) in self.tracks.iter().enumerate() {
250 let object = index as u32;
251 let mut points = Vec::with_capacity(track.nodes().len() + 1);
252 if let Some(parent) = track.parent() {
253 let parent = self
254 .track(parent as usize)
255 .expect("the parent track exists");
256 let end = parent.nodes().last().expect("the parent holds nodes");
257 points.push(glam::Vec3::from(end.centroid()));
258 }
259 points.extend(
260 track
261 .nodes()
262 .iter()
263 .map(|node| glam::Vec3::from(node.centroid())),
264 );
265 deduplicate(&mut points);
266 match points.len() {
267 0 => {}
268 1 => crate::push_sphere(
269 &mut vertices,
270 &mut indices,
271 points[0].to_array(),
272 radius,
273 object,
274 ),
275 _ => {
276 let positions = points
277 .iter()
278 .map(|point| point.to_array())
279 .collect::<Vec<_>>();
280 let radii = vec![radius; positions.len()];
281 crate::push_tube(&mut vertices, &mut indices, &positions, &radii, object);
282 }
283 }
284 }
285 crate::Mesh::new(vertices, indices)
286 }
287}
288
289fn deduplicate(points: &mut Vec<glam::Vec3>) {
290 let mut unique = Vec::with_capacity(points.len());
291 for point in points.drain(..) {
292 if unique
293 .last()
294 .is_none_or(|last: &glam::Vec3| last.distance_squared(point) > 1.0e-12)
295 {
296 unique.push(point);
297 }
298 }
299 *points = unique;
300}
301
302fn distance(left: [f32; 3], right: [f32; 3]) -> f64 {
303 (0..3)
304 .map(|axis| f64::from(right[axis] - left[axis]).powi(2))
305 .sum::<f64>()
306 .sqrt()
307}