Loading...
Searching...
No Matches
PointTracker.hpp
1#pragma once
2
3/* SPDX-License-Identifier: GPL-3.0-or-later */
4
5#include <ossia/detail/flat_map.hpp>
6#include <ossia/math/point_tracker.hpp>
7#include <ossia/network/value/value.hpp>
8
9#include <halp/audio.hpp>
10#include <halp/callback.hpp>
11#include <halp/controls.enums.hpp>
12#include <halp/controls.hpp>
13#include <halp/layout.hpp>
14#include <halp/meta.hpp>
15#include <halp/value_types.hpp>
16
17#include <cmath>
18
19#include <string>
20#include <vector>
21
22namespace avnd_tools
23{
24
25// Point Tracker — the association + filtering + lifecycle layer that turns
26// per-frame sets of bare detections (points, optionally with a confidence)
27// into stable identified tracks over time. It is NOT a detector: feed it
28// whatever produces points — Blob stats centroids, pose keypoints, TUIO/OSC
29// cursors, custom scripts — and map the resulting ids to audio and visuals.
30//
31// Coordinate space: whatever the upstream source uses, consistently. The
32// computer-vision objects in the tree normalize to [0,1] on x and y; nothing
33// anywhere normalizes z, so in 3D use whatever unit your source speaks and
34// scale Max Speed & friends accordingly (all speed/noise knobs are in the
35// input's coordinate units).
36//
37// Input format ("Points"): a list, each element being one detection —
38// * a vec2f / vec3f (vec3f in 2D = x, y, confidence;
39// in 3D a vec4f = x, y, z, confidence),
40// * a sub-list of numbers [x, y, (z), (confidence)],
41// * a map with keys position/pos/centroid/point (+ confidence/score/conf),
42// which is what Blob stats / Blob sort emit,
43// * or a flat list of plain numbers [x, y, x, y, ...] (stride 2 in 2D, 3 in
44// 3D) for the whole frame at once.
45//
46// Two-tier output, the key latency decision: a track confirmed over 3 frames
47// at 30 fps costs 100 ms of onset latency — an order of magnitude over the
48// ~10 ms budget of musical control. So provisional tracks are emitted
49// immediately (flagged, for triggers) while confirmed tracks form the stable
50// set for continuous mappings. Turn "Emit Unconfirmed" off to opt out.
51//
52// Identity: `id` is persistent and NEVER reused; `creation_time` is the true
53// identity if ids are ever reset. `slot` is a dense reusable index in
54// [0, Slot Count[ for mapping tracks to a fixed bank of voices/parameters,
55// recycled through a quarantine hold so a brief exit/re-entry does not hand an
56// object's slot to a stranger.
57
58enum class TrackerMotionGate
59{
60 MaxSpeed,
61 Mahalanobis,
62 Off
63};
64enum class TrackerAllocation
65{
66 LowestFree,
67 RoundRobin,
68 NearestVacated
69};
70enum class TrackerSteal
71{
72 Never,
73 Stalest,
74 LowestConfidence
75};
76enum class TrackerOrder
77{
78 Id,
79 Slot,
80 Age,
81 Confidence,
82 DistanceToAnchor
83};
84enum class TrackerFormat
85{
86 Compact,
87 Slots
88};
89
90template <std::size_t N>
92{
93 static_assert(N == 2 || N == 3);
94 using position_type
95 = std::conditional_t<N == 2, halp::xy_type<float>, halp::xyz_type<float>>;
96 using tracker_type = ossia::point_tracker<N>;
97 using detection_type = ossia::point_detection<N>;
98
99 // One emitted track. Serialized as a map (field names); the positions inside
100 // use halp::xy_type / halp::xyz_type on purpose: they carry no field names
101 // and therefore encode as plain vec2f / vec3f in both directions.
103 {
104 int id = -1;
105 int slot = -1;
106 std::string state; // provisional | confirmed | coasting | revived
107 double creation_time = 0.;
108 float age = 0.f;
109 float time_since_seen = 0.f;
110 position_type position{}; // smoothed (One-Euro), lead-compensated
111 position_type position_raw{}; // last raw measurement
112 position_type velocity{}; // units per second
113 float confidence = 0.f;
114 bool provisional = false;
115 bool reacquired = false;
116
117 halp_field_names(
118 id, slot, state, creation_time, age, time_since_seen, position,
119 position_raw, velocity, confidence, provisional, reacquired);
120 };
121
122 struct ins
123 {
124 struct : halp::val_port<"Points", std::vector<ossia::value>>
125 {
126 // Dimension-specific: advertising a z coordinate on the 2D process sends
127 // people looking for a control that is not there.
128 halp_meta(
129 description,
130 N == 2
131 ? "Detections for this frame. Accepts a list of vec2f, a list of "
132 "vec3f read as (x, y, confidence), sub-lists [x, y] or "
133 "[x, y, confidence], {position, confidence} maps as emitted by "
134 "Blob stats, or one flat list of numbers [x, y, x, y, ...]."
135 : "Detections for this frame. Accepts a list of vec3f, a list of "
136 "vec4f read as (x, y, z, confidence), sub-lists [x, y, z] or "
137 "[x, y, z, confidence], {position, confidence} maps, or one "
138 "flat list of numbers [x, y, z, x, y, z, ...].")
139 void update(auto& self) { self.points_dirty = true; }
140 } points;
141
142 // --- Association ---
143 struct : halp::spinbox_f32<"Max Speed", halp::range{0.01, 100., 2.}>
144 {
145 halp_meta(
146 description,
147 "Fastest plausible object speed, in coordinate units per second - the "
148 "primary association gate. 2 m/s suits human-scale motion in metric "
149 "spaces; in [0,1] camera space 2.0 means crossing the frame in 0.5 s.")
150 } max_speed;
151 // Comboboxes rather than enum_t throughout: the enum ("choices") widget
152 // lays every option out side by side, so a handful of multi-word options
153 // spans the whole process width and wraps. A dropdown costs one line.
154 struct : halp::combobox_t<"Motion Gate", TrackerMotionGate>
155 {
156 halp_meta(
157 description,
158 "How to reject implausible jumps: Max Speed (analytic budget "
159 "max_speed*dt*(1 + lost/coast)), Mahalanobis (chi-square on the "
160 "Kalman innovation), or Off.")
161 struct range
162 {
163 std::string_view values[3]{"Max speed", "Mahalanobis", "Off"};
164 TrackerMotionGate init{TrackerMotionGate::MaxSpeed};
165 };
166 } motion_gate;
167 struct : halp::toggle<"Two-Stage Association", halp::default_on_toggle>
168 {
169 halp_meta(
170 description,
171 "ByteTrack: after matching high-confidence detections, let "
172 "low-confidence ones sustain still-unmatched recent tracks (they "
173 "never birth or revive). Worth 1-10 IDF1 points on crowded scenes.")
174 } two_stage;
175 struct : halp::knob_f32<"High Confidence", halp::range{0., 1., 0.5}>
176 {
177 halp_meta(
178 description,
179 "Detections at or above this confidence are matched first and can "
180 "sustain any track. Set it around the confidence your detector gives "
181 "clearly-visible objects.")
182 } high_conf;
183 struct : halp::knob_f32<"Low Confidence", halp::range{0., 1., 0.1}>
184 {
185 halp_meta(
186 description,
187 "Detections between this and High Confidence only run in the "
188 "second-stage recovery: they can sustain an existing track through an "
189 "occlusion dip but never start or revive one. Below this they are "
190 "dropped.")
191 } low_conf;
192 struct : halp::knob_f32<"New Track Confidence", halp::range{0., 1., 0.6}>
193 {
194 halp_meta(description, "Minimum confidence for a detection to start a new track.")
195 } new_conf;
196 struct : halp::knob_f32<"Direction Consistency", halp::range{0., 1., 0.2}>
197 {
198 halp_meta(
199 description,
200 "OC-SORT: penalize associations that reverse the track's direction of "
201 "motion, averaged over 1-3 frame baselines so one noisy frame does "
202 "not fake a turn. Helps crossings; 0 = off.")
203 } dir_weight;
204 struct : halp::knob_f32<"Confidence Modeling", halp::range{0., 1.5, 1.}>
205 {
206 halp_meta(
207 description,
208 "Hybrid-SORT: prefer the detection whose confidence continues the "
209 "track's confidence trend (an occluded object's confidence sinks "
210 "smoothly, so the trend tells crossing objects apart when position "
211 "cannot). Needs a detector with meaningful per-detection confidence; "
212 "0 = off.")
213 } conf_weight;
214
215 // --- Motion model ---
216 struct : halp::spinbox_f32<"Motion Noise", halp::range{0.001, 50., 0.67}>
217 {
218 halp_meta(
219 description,
220 "Kalman process noise: expected acceleration (units/s^2). Human "
221 "motion peaks near 2 m/s^2; 0.67 fits that as 3 sigma. Raise for "
222 "erratic motion, lower for very smooth coasting.")
223 } accel_sigma;
224 struct : halp::spinbox_f32<"Position Noise", halp::range{0.0001, 1., 0.005}>
225 {
226 halp_meta(
227 description,
228 N == 2
229 ? "Detector jitter (standard deviation, coordinate units). Match it "
230 "to your detector's actual noise: the association gates widen by "
231 "this amount. Understating it makes a track reject its own noisy "
232 "detections and be reborn under a new id; overstating it widens "
233 "the gates until neighbouring entities compete for the same "
234 "detection and swap."
235 : "Detector jitter (standard deviation, coordinate units). Match it "
236 "to your detector's actual noise: the association gates widen by "
237 "this amount. Understating it makes a track reject its own noisy "
238 "detections and be reborn under a new id; overstating it widens "
239 "the gates until neighbouring entities compete for the same "
240 "detection and swap. This matters more in 3D than in 2D: noise "
241 "on three axes gives a radial error of sigma*sqrt(3) against a "
242 "gate that does not grow with the dimension.")
243 } meas_noise;
244
245 // --- Confirmation ---
246 struct : halp::spinbox_f32<"Confirm Time", halp::range{0., 2000., 100.}>
247 {
248 halp_meta(
249 description,
250 "Minimum age (milliseconds, not frames) before a track can be "
251 "confirmed - stable across frame rates.")
252 } confirm_time;
253 struct : halp::spinbox_i32<"Confirm Hits", halp::range{1, 31, 3}>
254 {
255 halp_meta(
256 description,
257 "A track is confirmed once it was detected in this many of the last "
258 "Confirm Window detection frames (and is older than Confirm Time). "
259 "Raise to demand more evidence before a track joins the stable set.")
260 } confirm_hits;
261 struct : halp::spinbox_i32<"Confirm Window", halp::range{1, 31, 5}>
262 {
263 halp_meta(
264 description,
265 "Size of the recent-frames window that Confirm Hits is counted over. "
266 "A wider window tolerates more dropped frames on the way to "
267 "confirmation.")
268 } confirm_window;
269 struct : halp::knob_f32<"Instant Confirm Above", halp::range{0., 1.001, 0.9}>
270 {
271 halp_meta(
272 description,
273 "A detection at or above this confidence confirms its track "
274 "immediately, skipping M-of-N. Set above 1 to disable.")
275 } instant_confirm;
276 struct : halp::toggle<"Emit Unconfirmed", halp::default_on_toggle>
277 {
278 halp_meta(
279 description,
280 "Emit provisional tracks immediately (flagged) instead of sitting out "
281 "the ~100 ms confirmation latency. For musical onsets, keep this on "
282 "and filter on the `provisional` field where it matters.")
283 } emit_unconfirmed;
284
285 // --- Lifecycle ---
286 struct : halp::spinbox_f32<"Coast Time", halp::range{0., 5000., 500.}>
287 {
288 halp_meta(
289 description,
290 "After a miss, keep emitting the Kalman-predicted position for this "
291 "long (ms) before the track goes lost.")
292 } coast_time;
293 struct : halp::toggle<"Revive", halp::default_on_toggle>
294 {
295 halp_meta(
296 description,
297 "Keep lost tracks in memory after coasting ends, so an object that "
298 "reappears nearby gets its old id back instead of a new one.")
299 } revive;
300 struct : halp::spinbox_f32<"Revive Time", halp::range{0., 10000., 2000.}>
301 {
302 halp_meta(
303 description,
304 "After coasting ends, keep the lost track (unemitted) for this long "
305 "(ms); a matching detection revives it with its id intact.")
306 } revive_time;
307 struct : halp::toggle<"Re-update on Revive", halp::default_on_toggle>
308 {
309 halp_meta(
310 description,
311 "OC-SORT ORU: on revival, re-run the filter along a virtual "
312 "trajectory across the gap, killing the post-occlusion lurch.")
313 } revive_reupdate;
314
315 // --- Output smoothing ---
316 struct : halp::toggle<"Smooth", halp::default_on_toggle>
317 {
318 halp_meta(
319 description,
320 "One-Euro filter on the output positions (per track, per axis): "
321 "removes detector jitter at rest with minimal lag during motion. "
322 "Tune with Min Cutoff and Beta.")
323 } smooth;
324 struct : halp::log_hslider_f32<"Min Cutoff", halp::range{0.005, 10., 1.}>
325 {
326 halp_meta(
327 description, "One-Euro cutoff at rest, Hz. Lower = smoother = laggier.")
328 } min_cutoff;
329 struct : halp::hslider_f32<"Beta", halp::range{0., 100., 1.}>
330 {
331 halp_meta(
332 description,
333 "One-Euro speed coefficient: how much motion raises the cutoff. "
334 "MediaPipe ships 10-80 on landmarks; raise it if fast motion lags.")
335 } beta;
336 struct : halp::spinbox_f32<"Prediction Lead", halp::range{0., 100., 0.}>
337 {
338 halp_meta(
339 description,
340 "Output position = estimate + velocity * lead (ms): compensates "
341 "downstream latency at the price of overshoot.")
342 } lead;
343 struct : halp::spinbox_f32<"Output Deadband", halp::range{0., 0.1, 0.0005}>
344 {
345 halp_meta(
346 description,
347 "Suppress output changes smaller than this distance (coordinate "
348 "units; ~half a pixel in normalized camera space). 0 = off.")
349 } deadband;
350
351 // --- Slots ---
352 struct : halp::spinbox_i32<"Slot Count", halp::range{0, 64, 8}>
353 {
354 halp_meta(
355 description,
356 "Size of the dense reusable slot bank (for mapping to a fixed set of "
357 "voices/parameters). 0 disables slots.")
358 } slot_count;
359 struct : halp::combobox_t<"Allocation", TrackerAllocation>
360 {
361 halp_meta(
362 description,
363 "How a newly confirmed track picks a slot: Lowest Free (deterministic, "
364 "cv.jit-style), Round Robin (spread reuse over the bank, so a dead "
365 "voice gets time to fade), or Nearest Vacated (an object re-entering "
366 "where one just left inherits that slot).")
367 struct range
368 {
369 std::string_view values[3]{"Lowest free", "Round robin", "Nearest vacated"};
370 TrackerAllocation init{TrackerAllocation::LowestFree};
371 };
372 } allocation;
373 struct : halp::combobox_t<"Steal Policy", TrackerSteal>
374 {
375 halp_meta(
376 description,
377 "When every slot is taken and a new track confirms: Never (it stays "
378 "unslotted until one frees up), or steal from the Stalest (unseen the "
379 "longest) or Lowest Confidence track.")
380 struct range
381 {
382 std::string_view values[3]{"Never", "Stalest", "Lowest confidence"};
383 TrackerSteal init{TrackerSteal::Never};
384 };
385 } steal;
386 struct : halp::spinbox_f32<"Hold Time", halp::range{0., 5000., 250.}>
387 {
388 halp_meta(
389 description,
390 "Quarantine (ms): a vacated slot is not handed to a new track for "
391 "this long, so a brief dropout does not shuffle the whole bank.")
392 } hold_time;
393
394 // --- Output ---
395 struct : halp::combobox_t<"Order By", TrackerOrder>
396 {
397 halp_meta(description, "Ordering of the compact outputs.")
398 struct range
399 {
400 std::string_view values[5]{"Id", "Slot", "Age", "Confidence", "Distance to anchor"};
401 TrackerOrder init{TrackerOrder::Id};
402 };
403 } order_by;
404 struct
405 : std::conditional_t<
406 N == 2, halp::xy_spinboxes_f32<"Anchor", halp::range{-1000., 1000., 0.}>,
407 halp::xyz_spinboxes_f32<"Anchor", halp::range{-1000., 1000., 0.}>>
408 {
409 halp_meta(
410 description, "Reference point for the Distance To Anchor ordering.")
411 } anchor;
412 struct : halp::combobox_t<"Data Format", TrackerFormat>
413 {
414 halp_meta(
415 description,
416 "Positions/Ids layout: Compact = live tracks back-to-back in Order By "
417 "order; Slots = Slot Count fixed positions indexed by slot, "
418 "zero-padded (GPU/voice-bank friendly).")
419 struct range
420 {
421 std::string_view values[2]{"Compact", "Slots"};
422 TrackerFormat init{TrackerFormat::Compact};
423 };
424 } format;
425
426 struct : halp::impulse_button<"Reset IDs">
427 {
428 halp_meta(description, "Forget every track and restart ids from 1.")
429 void update(auto& self) { self.reset_requested = true; }
430 } reset_ids;
431 } inputs;
432
433 struct
434 {
435 struct
436 {
437 halp_meta(name, "Tracks")
438 halp_meta(
439 description,
440 "Every emitted track as a record: id, slot, state, creation_time, "
441 "age, time_since_seen, position, position_raw, velocity, confidence, "
442 "provisional, reacquired.")
443 std::vector<track_record> value;
444 } tracks;
445
446 struct
447 {
448 halp_meta(name, "Count")
449 halp_meta(
450 description,
451 "Number of currently emitted tracks (confirmed + coasting + revived, "
452 "plus provisional ones when Emit Unconfirmed is on).")
453 int value{};
454 } count;
455
456 struct
457 {
458 halp_meta(name, "Positions")
459 halp_meta(
460 description,
461 "Flat [x,y,...] float list of the smoothed track positions, in the "
462 "chosen Data Format. Cables directly into Point2D View, Array to "
463 "Mesh, and other renderers.")
464 std::vector<float> value;
465 } positions;
466
467 struct
468 {
469 halp_meta(name, "Ids")
470 halp_meta(
471 description,
472 "Track id per entry of Positions (-1 for an empty slot in Slots "
473 "format).")
474 std::vector<int> value;
475 } ids;
476
477 struct : halp::callback<"Entered", int>
478 {
479 halp_meta(
480 description,
481 "Fires with the track id the instant a detection births a new track "
482 "(before confirmation) - the lowest-latency onset signal.")
483 } entered;
484 struct : halp::callback<"Confirmed", int>
485 {
486 halp_meta(
487 description,
488 "Fires with the track id when a track passes confirmation and joins "
489 "the stable set.")
490 } confirmed;
491 struct : halp::callback<"Exited", int>
492 {
493 halp_meta(
494 description,
495 "Fires with the track id when a track is removed for good (coast and "
496 "revival windows exhausted). Use it to release voices/mappings.")
497 } exited;
498 struct : halp::callback<"Revived", int>
499 {
500 halp_meta(
501 description,
502 "Fires with the track id when a lost track is re-acquired with its "
503 "identity intact.")
504 } revived;
505 } outputs;
506
507 // Grouped by the question being answered rather than by pipeline order:
508 // "which detection belongs to which track", "when does a track begin and
509 // end", "how steady is the output", "how do tracks map onto a fixed bank",
510 // "what comes out". Every control appears in exactly one tab; the Points
511 // inlet is a cable, not a control, so it has no widget here.
512 struct ui
513 {
514 halp_meta(name, "Point Tracker")
515 halp_meta(layout, halp::layouts::tabs)
516 halp_meta(background, halp::colors::background_mid)
517
518 struct
519 {
520 halp_meta(name, "Association")
521 halp_meta(layout, halp::layouts::hbox)
522
523 struct
524 {
525 halp_meta(name, "Gating")
526 halp_meta(layout, halp::layouts::vbox)
527 halp::item<&ins::motion_gate> motion_gate;
528 halp::item<&ins::max_speed> max_speed;
529 halp::item<&ins::accel_sigma> accel_sigma;
530 halp::item<&ins::meas_noise> meas_noise;
531 } gating;
532
533 halp::spacing sp1{.width = 12, .height = 1};
534
535 struct
536 {
537 halp_meta(name, "Confidence")
538 halp_meta(layout, halp::layouts::vbox)
539 halp::item<&ins::two_stage> two_stage;
540 halp::item<&ins::high_conf> high_conf;
541 halp::item<&ins::low_conf> low_conf;
542 halp::item<&ins::new_conf> new_conf;
543 } confidence;
544
545 halp::spacing sp2{.width = 12, .height = 1};
546
547 struct
548 {
549 halp_meta(name, "Extra cues")
550 halp_meta(layout, halp::layouts::vbox)
551 halp::item<&ins::dir_weight> dir_weight;
552 halp::item<&ins::conf_weight> conf_weight;
553 } cues;
554 } association_tab;
555
556 struct
557 {
558 halp_meta(name, "Lifecycle")
559 halp_meta(layout, halp::layouts::hbox)
560
561 struct
562 {
563 halp_meta(name, "Confirmation")
564 halp_meta(layout, halp::layouts::vbox)
565 halp::item<&ins::confirm_time> confirm_time;
566 halp::item<&ins::confirm_hits> confirm_hits;
567 halp::item<&ins::confirm_window> confirm_window;
568 halp::item<&ins::instant_confirm> instant_confirm;
569 halp::item<&ins::emit_unconfirmed> emit_unconfirmed;
570 } confirmation;
571
572 halp::spacing sp1{.width = 12, .height = 1};
573
574 struct
575 {
576 halp_meta(name, "Coast & revive")
577 halp_meta(layout, halp::layouts::vbox)
578 halp::item<&ins::coast_time> coast_time;
579 halp::item<&ins::revive> revive;
580 halp::item<&ins::revive_time> revive_time;
581 halp::item<&ins::revive_reupdate> revive_reupdate;
582 } coasting;
583 } lifecycle_tab;
584
585 struct
586 {
587 halp_meta(name, "Smoothing")
588 halp_meta(layout, halp::layouts::hbox)
589
590 struct
591 {
592 halp_meta(name, "Filter")
593 halp_meta(layout, halp::layouts::vbox)
594 halp::item<&ins::smooth> smooth;
595 halp::item<&ins::min_cutoff> min_cutoff;
596 halp::item<&ins::beta> beta;
597 } filter;
598
599 halp::spacing sp1{.width = 12, .height = 1};
600
601 struct
602 {
603 halp_meta(name, "Latency")
604 halp_meta(layout, halp::layouts::vbox)
605 halp::item<&ins::lead> lead;
606 halp::item<&ins::deadband> deadband;
607 } latency;
608 } smoothing_tab;
609
610 struct
611 {
612 halp_meta(name, "Slots & output")
613 halp_meta(layout, halp::layouts::hbox)
614
615 struct
616 {
617 halp_meta(name, "Bank")
618 halp_meta(layout, halp::layouts::vbox)
619 halp::item<&ins::slot_count> slot_count;
620 halp::item<&ins::allocation> allocation;
621 } bank;
622
623 halp::spacing sp1{.width = 12, .height = 1};
624
625 struct
626 {
627 halp_meta(name, "Reuse")
628 halp_meta(layout, halp::layouts::vbox)
629 halp::item<&ins::steal> steal;
630 halp::item<&ins::hold_time> hold_time;
631 halp::item<&ins::reset_ids> reset_ids;
632 } reuse;
633
634 halp::spacing sp2{.width = 12, .height = 1};
635
636 struct
637 {
638 halp_meta(name, "Output")
639 halp_meta(layout, halp::layouts::vbox)
640 halp::item<&ins::format> format;
641 halp::item<&ins::order_by> order_by;
642 halp::item<&ins::anchor> anchor;
643 } output;
644 } slots_tab;
645 };
646
647 using tick = halp::tick_musical;
648
649 void prepare(halp::setup s)
650 {
651 if(s.rate > 0)
652 m_rate = s.rate;
653 }
654
655 void operator()(const halp::tick_musical& tk)
656 {
657 // Real elapsed time for this tick.
658 double dt = 0.;
659 if(tk.position_in_nanoseconds > 0 && m_last_ns > 0
660 && tk.position_in_nanoseconds > m_last_ns)
661 dt = (tk.position_in_nanoseconds - m_last_ns) * 1e-9;
662 else if(tk.frames > 0)
663 dt = tk.frames / m_rate;
664 if(tk.position_in_nanoseconds > 0)
665 m_last_ns = tk.position_in_nanoseconds;
666 dt = std::clamp(dt, 0., 2.);
667
668 if(reset_requested)
669 {
670 reset_requested = false;
671 m_tracker.reset();
672 m_last_out.clear();
673 }
674
675 apply_config();
676
677 bool recompute = false;
678 if(points_dirty)
679 {
680 points_dirty = false;
681 parse_detections();
682 m_tracker.update(m_dets.data(), m_dets.size(), float(dt));
683 recompute = true;
684 }
685 else
686 {
687 m_tracker.advance(float(dt));
688 // While the source is silent, re-emit coasting predictions at roughly
689 // the source's own cadence so downstream keeps moving smoothly.
690 m_silent_acc += dt;
691 if(m_silent_acc >= m_tracker.estimated_period() && has_emitted_tracks())
692 recompute = true;
693 }
694
695 const auto& ev = m_tracker.events();
696 if(!ev.entered.empty() || !ev.confirmed.empty() || !ev.exited.empty()
697 || !ev.revived.empty())
698 recompute = true;
699
700 for(auto id : ev.entered)
701 outputs.entered(id);
702 for(auto id : ev.confirmed)
703 outputs.confirmed(id);
704 for(auto id : ev.revived)
705 outputs.revived(id);
706 for(auto id : ev.exited)
707 {
708 m_last_out.erase(id);
709 outputs.exited(id);
710 }
711
712 if(recompute)
713 {
714 m_silent_acc = 0.;
715 emit_outputs();
716 }
717 }
718
719 bool points_dirty = false;
720 bool reset_requested = false;
721
722private:
723 using track_t = typename tracker_type::track;
724
725 void apply_config()
726 {
727 auto& c = m_cfg;
728 c.max_speed = std::max(inputs.max_speed.value, 1e-4f);
729 switch(inputs.motion_gate.value)
730 {
731 case TrackerMotionGate::MaxSpeed:
732 c.gate = ossia::track_motion_gate::max_speed;
733 break;
734 case TrackerMotionGate::Mahalanobis:
735 c.gate = ossia::track_motion_gate::mahalanobis;
736 break;
737 case TrackerMotionGate::Off:
738 c.gate = ossia::track_motion_gate::off;
739 break;
740 }
741 c.two_stage = inputs.two_stage.value;
742 c.high_conf = inputs.high_conf.value;
743 c.low_conf = inputs.low_conf.value;
744 c.new_conf = inputs.new_conf.value;
745 c.dir_weight = inputs.dir_weight.value;
746 c.conf_weight = inputs.conf_weight.value;
747 c.accel_sigma = inputs.accel_sigma.value;
748 c.meas_std = inputs.meas_noise.value;
749 c.confirm_time = inputs.confirm_time.value * 1e-3f;
750 c.confirm_hits = std::uint32_t(std::max(inputs.confirm_hits.value, 1));
751 c.confirm_window = std::uint32_t(std::clamp(inputs.confirm_window.value, 1, 31));
752 c.instant_confirm = inputs.instant_confirm.value;
753 c.coast_time = inputs.coast_time.value * 1e-3f;
754 c.revive = inputs.revive.value;
755 c.revive_time = inputs.revive_time.value * 1e-3f;
756 c.revive_reupdate = inputs.revive_reupdate.value;
757 c.smooth = inputs.smooth.value;
758 c.min_cutoff = std::max(inputs.min_cutoff.value, 0.001f);
759 c.beta = inputs.beta.value;
760 c.slot_count = std::uint32_t(std::max(inputs.slot_count.value, 0));
761 switch(inputs.allocation.value)
762 {
763 case TrackerAllocation::LowestFree:
764 c.allocation = ossia::track_slot_allocation::lowest_free;
765 break;
766 case TrackerAllocation::RoundRobin:
767 c.allocation = ossia::track_slot_allocation::round_robin;
768 break;
769 case TrackerAllocation::NearestVacated:
770 c.allocation = ossia::track_slot_allocation::nearest_vacated;
771 break;
772 }
773 switch(inputs.steal.value)
774 {
775 case TrackerSteal::Never:
776 c.steal = ossia::track_slot_steal::never;
777 break;
778 case TrackerSteal::Stalest:
779 c.steal = ossia::track_slot_steal::stalest;
780 break;
781 case TrackerSteal::LowestConfidence:
782 c.steal = ossia::track_slot_steal::lowest_confidence;
783 break;
784 }
785 c.slot_hold_time = inputs.hold_time.value * 1e-3f;
786 m_tracker.configure(c);
787 }
788
789 static bool number_like(const ossia::value& v) noexcept
790 {
791 const auto t = v.get_type();
792 return t == ossia::val_type::FLOAT || t == ossia::val_type::INT
793 || t == ossia::val_type::BOOL;
794 }
795
796 static float to_float(const ossia::value& v) noexcept
797 {
798 switch(v.get_type())
799 {
800 case ossia::val_type::FLOAT:
801 return *v.target<float>();
802 case ossia::val_type::INT:
803 return float(*v.target<int>());
804 case ossia::val_type::BOOL:
805 return *v.target<bool>() ? 1.f : 0.f;
806 default:
807 return 0.f;
808 }
809 }
810
811 // One element of the input list -> one detection. Returns false if the
812 // element is not something point-like.
813 bool parse_element(const ossia::value& v, detection_type& out) noexcept
814 {
815 switch(v.get_type())
816 {
817 case ossia::val_type::VEC2F: {
818 const auto& a = *v.target<ossia::vec2f>();
819 if constexpr(N == 2)
820 {
821 out.position = {a[0], a[1]};
822 out.confidence = 1.f;
823 return true;
824 }
825 return false; // a 2D point has no meaning as a 3D detection
826 }
827 case ossia::val_type::VEC3F: {
828 const auto& a = *v.target<ossia::vec3f>();
829 if constexpr(N == 2)
830 {
831 out.position = {a[0], a[1]};
832 out.confidence = a[2]; // x, y, confidence
833 }
834 else
835 {
836 out.position = {a[0], a[1], a[2]};
837 out.confidence = 1.f;
838 }
839 return true;
840 }
841 case ossia::val_type::VEC4F: {
842 const auto& a = *v.target<ossia::vec4f>();
843 if constexpr(N == 2)
844 {
845 out.position = {a[0], a[1]};
846 out.confidence = a[2];
847 }
848 else
849 {
850 out.position = {a[0], a[1], a[2]};
851 out.confidence = a[3]; // x, y, z, confidence
852 }
853 return true;
854 }
855 case ossia::val_type::LIST: {
856 const auto& l = *v.target<std::vector<ossia::value>>();
857 if(l.size() < N)
858 return false;
859 for(std::size_t i = 0; i < N; i++)
860 {
861 if(!number_like(l[i]))
862 return false;
863 out.position[i] = to_float(l[i]);
864 }
865 out.confidence = (l.size() > N && number_like(l[N])) ? to_float(l[N]) : 1.f;
866 return true;
867 }
868 case ossia::val_type::MAP: {
869 const auto& m = *v.target<ossia::value_map_type>();
870 bool has_pos = false;
871 out.confidence = 1.f;
872 for(const auto& [k, val] : m)
873 {
874 if(k == "position" || k == "pos" || k == "centroid" || k == "point")
875 {
876 detection_type sub;
877 if(parse_element(val, sub))
878 {
879 out.position = sub.position;
880 has_pos = true;
881 }
882 }
883 else if(k == "confidence" || k == "score" || k == "conf")
884 {
885 if(number_like(val))
886 out.confidence = to_float(val);
887 }
888 }
889 return has_pos;
890 }
891 default:
892 return false;
893 }
894 }
895
896 void parse_detections()
897 {
898 m_dets.clear();
899 const auto& in = inputs.points.value;
900 if(in.empty())
901 return;
902
903 // A flat frame of plain numbers: [x, y, (z), x, y, (z), ...]
904 if(number_like(in[0]))
905 {
906 for(std::size_t i = 0; i + N <= in.size(); i += N)
907 {
908 detection_type d;
909 bool ok = true;
910 for(std::size_t k = 0; k < N; k++)
911 {
912 if(!number_like(in[i + k]))
913 {
914 ok = false;
915 break;
916 }
917 d.position[k] = to_float(in[i + k]);
918 }
919 if(!ok)
920 break;
921 d.confidence = 1.f;
922 if(std::isfinite(d.position[0]))
923 m_dets.push_back(d);
924 if(m_dets.size() >= 1024)
925 break;
926 }
927 return;
928 }
929
930 for(const auto& v : in)
931 {
932 detection_type d;
933 if(parse_element(v, d) && std::isfinite(d.position[0])
934 && std::isfinite(d.position[1]))
935 m_dets.push_back(d);
936 if(m_dets.size() >= 1024)
937 break;
938 }
939 }
940
941 bool has_emitted_tracks() const noexcept
942 {
943 for(const auto& t : m_tracker.tracks())
944 if(t.emitted(inputs.emit_unconfirmed.value))
945 return true;
946 return false;
947 }
948
949 static const char* state_name(ossia::track_state s) noexcept
950 {
951 switch(s)
952 {
953 case ossia::track_state::provisional:
954 return "provisional";
955 case ossia::track_state::confirmed:
956 return "confirmed";
957 case ossia::track_state::coasting:
958 return "coasting";
959 case ossia::track_state::revived:
960 return "revived";
961 case ossia::track_state::lost:
962 return "lost";
963 case ossia::track_state::expired:
964 return "expired";
965 }
966 return "?";
967 }
968
969 // Smoothed position + prediction lead + per-track deadband.
970 std::array<float, N> output_position(const track_t& t)
971 {
972 std::array<float, N> p = t.filtered;
973 const float lead_s = inputs.lead.value * 1e-3f;
974 if(lead_s > 0.f)
975 {
976 const auto v = t.velocity();
977 for(std::size_t i = 0; i < N; i++)
978 p[i] += v[i] * lead_s;
979 }
980
981 const float db = inputs.deadband.value;
982 if(db > 0.f)
983 {
984 auto it = m_last_out.find(t.id);
985 if(it != m_last_out.end())
986 {
987 float d2 = 0.f;
988 for(std::size_t i = 0; i < N; i++)
989 {
990 const float d = p[i] - it->second[i];
991 d2 += d * d;
992 }
993 if(d2 < db * db)
994 return it->second; // hold the previous output
995 it->second = p;
996 }
997 else
998 {
999 m_last_out.emplace(t.id, p);
1000 }
1001 }
1002 return p;
1003 }
1004
1005 static position_type to_position(const std::array<float, N>& a) noexcept
1006 {
1007 if constexpr(N == 2)
1008 return {a[0], a[1]};
1009 else
1010 return {a[0], a[1], a[2]};
1011 }
1012
1013 void emit_outputs()
1014 {
1015 // Collect emitted tracks
1016 m_order.clear();
1017 for(const auto& t : m_tracker.tracks())
1018 if(t.emitted(inputs.emit_unconfirmed.value))
1019 m_order.push_back(&t);
1020
1021 // Order the compact view
1022 const auto key_less = [this](const track_t* a, const track_t* b) {
1023 switch(inputs.order_by.value)
1024 {
1025 case TrackerOrder::Id:
1026 return a->id < b->id;
1027 case TrackerOrder::Slot: {
1028 // Unslotted tracks last, then by id for determinism
1029 const auto sa = a->slot < 0 ? INT32_MAX : a->slot;
1030 const auto sb = b->slot < 0 ? INT32_MAX : b->slot;
1031 return sa != sb ? sa < sb : a->id < b->id;
1032 }
1033 case TrackerOrder::Age:
1034 return a->age != b->age ? a->age > b->age : a->id < b->id;
1035 case TrackerOrder::Confidence:
1036 return a->confidence != b->confidence ? a->confidence > b->confidence
1037 : a->id < b->id;
1038 case TrackerOrder::DistanceToAnchor: {
1039 const float da = anchor_distance2(*a), db_ = anchor_distance2(*b);
1040 return da != db_ ? da < db_ : a->id < b->id;
1041 }
1042 }
1043 return a->id < b->id;
1044 };
1045 std::sort(m_order.begin(), m_order.end(), key_less);
1046
1047 // Records
1048 auto& recs = outputs.tracks.value;
1049 recs.clear();
1050 recs.reserve(m_order.size());
1051 for(const track_t* t : m_order)
1052 {
1053 track_record r;
1054 r.id = t->id;
1055 r.slot = t->slot;
1056 r.state = state_name(t->state);
1057 r.creation_time = t->creation_time;
1058 r.age = t->age;
1059 r.time_since_seen = t->time_since_seen;
1060 r.position = to_position(output_position(*t));
1061 r.position_raw = to_position(t->last_meas);
1062 r.velocity = to_position(t->velocity());
1063 r.confidence = t->confidence;
1064 r.provisional = t->state == ossia::track_state::provisional;
1065 r.reacquired = t->reacquired;
1066 recs.push_back(std::move(r));
1067 }
1068 outputs.count.value = int(m_order.size());
1069
1070 // Flat positions + ids
1071 auto& pos = outputs.positions.value;
1072 auto& ids = outputs.ids.value;
1073 pos.clear();
1074 ids.clear();
1075 if(inputs.format.value == TrackerFormat::Slots)
1076 {
1077 const int slots = std::max(inputs.slot_count.value, 0);
1078 pos.assign(std::size_t(slots) * N, 0.f);
1079 ids.assign(std::size_t(slots), -1);
1080 for(const track_t* t : m_order)
1081 {
1082 if(t->slot < 0 || t->slot >= slots)
1083 continue;
1084 const auto p = output_position(*t);
1085 for(std::size_t i = 0; i < N; i++)
1086 pos[std::size_t(t->slot) * N + i] = p[i];
1087 ids[std::size_t(t->slot)] = t->id;
1088 }
1089 }
1090 else
1091 {
1092 pos.reserve(m_order.size() * N);
1093 ids.reserve(m_order.size());
1094 for(const track_t* t : m_order)
1095 {
1096 const auto p = output_position(*t);
1097 for(std::size_t i = 0; i < N; i++)
1098 pos.push_back(p[i]);
1099 ids.push_back(t->id);
1100 }
1101 }
1102 }
1103
1104 float anchor_distance2(const track_t& t) const noexcept
1105 {
1106 const auto& a = inputs.anchor.value;
1107 const auto p = t.filtered;
1108 float d2 = (p[0] - a.x) * (p[0] - a.x) + (p[1] - a.y) * (p[1] - a.y);
1109 if constexpr(N == 3)
1110 d2 += (p[2] - a.z) * (p[2] - a.z);
1111 return d2;
1112 }
1113
1114private:
1115 tracker_type m_tracker;
1116 typename tracker_type::config m_cfg;
1117 std::vector<detection_type> m_dets;
1118 std::vector<const track_t*> m_order;
1119 ossia::flat_map<std::int32_t, std::array<float, N>> m_last_out;
1120 double m_rate = 48000.;
1121 std::int64_t m_last_ns = 0;
1122 double m_silent_acc = 0.;
1123};
1124
1126{
1127 halp_meta(name, "Point Tracker 2D")
1128 halp_meta(c_name, "avnd_point_tracker_2d")
1129 halp_meta(category, "Spatial/Tracking")
1130 halp_meta(author, "ossia team")
1131 halp_meta(
1132 description,
1133 "Turn flickering 2D detections (blobs, keypoints, TUIO cursors) into "
1134 "stable identified tracks: association, Kalman + One-Euro filtering, "
1135 "confirm/coast/revive lifecycle, persistent ids and voice-bank slots.")
1136 halp_meta(uuid, "2ef5f23f-2d22-4163-b299-976c9d9bd1c8")
1137};
1138
1140{
1141 halp_meta(name, "Point Tracker 3D")
1142 halp_meta(c_name, "avnd_point_tracker_3d")
1143 halp_meta(category, "Spatial/Tracking")
1144 halp_meta(author, "ossia team")
1145 halp_meta(
1146 description,
1147 "Turn flickering 3D detections into stable identified tracks: "
1148 "association, Kalman + One-Euro filtering, confirm/coast/revive "
1149 "lifecycle, persistent ids and voice-bank slots.")
1150 halp_meta(uuid, "fa258031-25db-462f-8cc6-b3f049c8f6a7")
1151};
1152
1153}
The id_base_t class.
Definition Identifier.hpp:59
STL namespace.
Definition PointTracker.hpp:1126
Definition PointTracker.hpp:1140
Definition PointTracker.hpp:123
Definition PointTracker.hpp:103
Definition PointTracker.hpp:513
Definition PointTracker.hpp:92
Definition MIDISync.hpp:126