OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
point_tracker.hpp
Go to the documentation of this file.
1#pragma once
2#include <ossia/detail/config.hpp>
3
4#include <ossia/detail/small_vector.hpp>
7
8#include <cmath>
9
10#include <algorithm>
11#include <array>
12#include <bit>
13#include <cstdint>
14#include <limits>
15#include <vector>
16
53namespace ossia
54{
55
56enum class track_state : std::uint8_t
57{
59 confirmed,
60 coasting,
61 revived,
62 lost,
63 expired
64};
65
66enum class track_motion_gate : std::uint8_t
67{
68 off,
69 max_speed,
71};
72
73enum class track_slot_allocation : std::uint8_t
74{
78};
79
80enum class track_slot_steal : std::uint8_t
81{
82 never,
83 stalest,
85};
86
87template <std::size_t N>
88struct point_tracker_config
89{
90 // --- Association ---------------------------------------------------------
94 float max_speed = 2.f;
103 float mahalanobis_thresh = N == 3 ? 16.2662f : 13.8155f;
106 bool two_stage = true;
107 float high_conf = 0.5f;
108 float low_conf = 0.1f;
109 float new_conf = 0.6f;
114 float dir_weight = 0.2f;
124 float conf_weight = 1.0f;
125
126 // --- Motion filter -------------------------------------------------------
129 float accel_sigma = 0.67f;
131 float meas_std = 0.005f;
132
133 // --- Confirmation --------------------------------------------------------
136 float confirm_time = 0.100f;
137 std::uint32_t confirm_hits = 3;
138 std::uint32_t confirm_window = 5;
140 float instant_confirm = 0.9f;
141
142 // --- Lifecycle -----------------------------------------------------------
144 float coast_time = 0.5f;
145 bool revive = true;
146 float revive_time = 2.f;
149 bool revive_reupdate = true;
150
151 // --- Output smoothing (One-Euro, per track per axis) ---------------------
152 bool smooth = true;
153 float min_cutoff = 1.f;
154 float beta = 1.f;
155 float deriv_cutoff = 1.f;
156
157 // --- Slots ---------------------------------------------------------------
158 std::uint32_t slot_count = 8;
163 float slot_hold_time = 0.25f;
164};
165
166template <std::size_t N>
167struct point_detection
168{
169 std::array<float, N> position{};
170 float confidence = 1.f;
171};
172
173template <std::size_t N>
174struct point_track
175{
177 std::int32_t id = -1;
179 std::int32_t slot = -1;
183 double creation_time = 0.;
184 float age = 0.f;
185 float time_since_seen = 0.f;
186 float confidence = 0.f;
187 bool reacquired = false;
188
190 kalman_point_filter<N> kf{};
192 kalman_point_filter<N> kf_at_meas{};
194 std::array<float, N> filtered{};
196 std::array<float, N> last_meas{};
200 std::array<std::array<float, N>, 4> obs_hist{};
201 std::uint8_t obs_count = 0;
204 kalman_pv_filter conf_kf{};
208 float conf_prev = -1.f;
209
210 std::array<one_euro_filter<float>, N> smoothers{};
211
213 std::uint32_t hit_history = 0;
214 std::uint32_t hits = 0;
215 std::uint32_t consecutive_misses = 0;
216
217 [[nodiscard]] std::array<float, N> position() const noexcept { return kf.position(); }
218 [[nodiscard]] std::array<float, N> velocity() const noexcept { return kf.velocity(); }
219
221 [[nodiscard]] bool emitted(bool include_provisional) const noexcept
222 {
223 switch(state)
224 {
228 return true;
230 return include_provisional;
231 default:
232 return false;
233 }
234 }
235};
236
242{
243 ossia::small_vector<std::int32_t, 8> entered;
244 ossia::small_vector<std::int32_t, 8> confirmed;
245 ossia::small_vector<std::int32_t, 8> exited;
246 ossia::small_vector<std::int32_t, 8> revived;
247
248 void clear() noexcept
249 {
250 entered.clear();
251 confirmed.clear();
252 exited.clear();
253 revived.clear();
254 }
255};
256
257template <std::size_t N>
258class point_tracker
259{
260public:
261 using config = point_tracker_config<N>;
262 using detection = point_detection<N>;
263 using track = point_track<N>;
264
265 void configure(const config& c)
266 {
267 m_cfg = c;
268 m_proto.min_cutoff = c.min_cutoff;
269 m_proto.beta = c.beta;
270 m_proto.d_cutoff = c.deriv_cutoff;
271 for(auto& t : m_tracks)
272 {
273 for(auto& s : t.smoothers)
274 s.assign_parameters(m_proto);
275 // A slot beyond a shrunk slot count is gone; no quarantine for those.
276 if(t.slot >= std::int32_t(c.slot_count))
277 t.slot = -1;
278 }
279 m_vacated.erase(
280 std::remove_if(
281 m_vacated.begin(), m_vacated.end(),
282 [&](const vacated& v) { return v.slot >= std::int32_t(c.slot_count); }),
283 m_vacated.end());
284 }
285
286 const config& get_config() const noexcept { return m_cfg; }
287 const std::vector<track>& tracks() const noexcept { return m_tracks; }
288 const track_events& events() const noexcept { return m_events; }
289 double now() const noexcept { return m_now; }
291 float estimated_period() const noexcept { return m_est_period; }
292
294 void reset()
295 {
296 m_tracks.clear();
297 m_vacated.clear();
298 m_events.clear();
299 m_next_id = 1;
300 m_now = 0.;
301 m_last_frame_time = 0.;
302 m_est_period = 1.f / 30.f;
303 m_rr_cursor = 0;
304 }
305
314 void advance(float dt)
315 {
316 m_events.clear();
317 step_time(dt);
318 refresh_states_and_expire();
319 smooth_outputs(dt);
320 }
321
329 const std::vector<std::int32_t>& update(const detection* dets, std::size_t n, float dt)
330 {
331 m_events.clear();
332 step_time(dt);
333
334 // Real spacing between detection frames (advance() may have run inbetween).
335 const float frame_dt = std::clamp(float(m_now - m_last_frame_time), 1e-4f, 10.f);
336 m_last_frame_time = m_now;
337 m_est_period = 0.9f * m_est_period + 0.1f * std::min(frame_dt, 1.f);
338
339 auto& out = m_assign;
340 out.assign(n, -1);
341
342 // Split by confidence (ByteTrack)
343 m_high.clear();
344 m_low.clear();
345 for(std::size_t i = 0; i < n; i++)
346 {
347 const float c = dets[i].confidence;
348 if(c >= m_cfg.high_conf)
349 m_high.push_back(std::int32_t(i));
350 else if(m_cfg.two_stage && c >= m_cfg.low_conf)
351 m_low.push_back(std::int32_t(i));
352 }
353
354 m_t_match.assign(m_tracks.size(), -1);
355 m_d_used.assign(n, 0);
356
357 // Stage 1: all tracks (lost ones included when revival is on) vs
358 // high-confidence detections.
359 m_cands.clear();
360 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
361 {
362 const auto& t = m_tracks[ti];
363 if(t.state == track_state::lost && !m_cfg.revive)
364 continue;
365 for(const auto di : m_high)
366 {
367 float c;
368 if(admissible(t, dets[di], frame_dt, true, c))
369 m_cands.push_back({c, std::int32_t(ti), di});
370 }
371 }
372 greedy_assignment(m_cands, m_t_match.data(), m_tracks.size(), m_d_used.data(), n);
373
374 // Stage 2: still-unmatched recent tracks vs low-confidence detections.
375 // Lost tracks are excluded: a low-confidence blip must not revive.
376 if(!m_low.empty())
377 {
378 m_cands.clear();
379 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
380 {
381 if(m_t_match[ti] >= 0)
382 continue;
383 const auto& t = m_tracks[ti];
384 if(t.state == track_state::lost)
385 continue;
386 for(const auto di : m_low)
387 {
388 float c;
389 if(admissible(t, dets[di], frame_dt, false, c))
390 m_cands.push_back({c, std::int32_t(ti), di});
391 }
392 }
393 greedy_assignment(m_cands, m_t_match.data(), m_tracks.size(), m_d_used.data(), n);
394 }
395
396 // Apply matches / misses.
397 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
398 {
399 auto& t = m_tracks[ti];
400 const auto di = m_t_match[ti];
401 if(di >= 0)
402 {
403 apply_match(t, dets[di]);
404 out[di] = t.id;
405 }
406 else
407 {
408 t.hit_history <<= 1;
409 t.consecutive_misses++;
410 }
411 }
412
413 // Births: unmatched detections above the birth threshold.
414 for(std::size_t i = 0; i < n; i++)
415 {
416 if(m_d_used[i] || dets[i].confidence < m_cfg.new_conf)
417 continue;
418 out[i] = birth(dets[i]);
419 }
420
421 refresh_states_and_expire();
422 smooth_outputs(dt);
423 return out;
424 }
425
427 const std::vector<std::int32_t>& update(const std::vector<detection>& dets, float dt)
428 {
429 return update(dets.data(), dets.size(), dt);
430 }
431
432private:
433 // TCM internals. Not exposed: conf_weight is the tunable; these only shape
434 // how fast the per-track confidence estimate adapts. Confidence lives in
435 // [0,1] and detector jitter on it is typically a few percent.
436 static constexpr float conf_sigma_a = 2.f; // confidence units / s^2
437 static constexpr float conf_meas_std = 0.05f; // confidence jitter std
438
439 struct vacated
440 {
441 std::int32_t slot;
442 std::array<float, N> position;
443 double time;
444 };
445
446 static float
447 distance(const std::array<float, N>& a, const std::array<float, N>& b) noexcept
448 {
449 float d2 = 0.f;
450 for(std::size_t i = 0; i < N; i++)
451 {
452 const float d = a[i] - b[i];
453 d2 += d * d;
454 }
455 return std::sqrt(d2);
456 }
457
458 void step_time(float dt)
459 {
460 if(dt < 0.f)
461 dt = 0.f;
462 m_now += dt;
463 if(dt > 0.f)
464 {
465 const auto qc = kalman_pv_filter::cwna(conf_sigma_a, dt);
466 for(auto& t : m_tracks)
467 {
468 t.kf.predict(dt, m_cfg.accel_sigma);
469 t.conf_kf.predict(dt, qc);
470 t.age += dt;
471 t.time_since_seen += dt;
472 }
473 }
474 }
475
479 bool admissible(
480 const track& t, const detection& d, float frame_dt, bool high_stage,
481 float& cost) const noexcept
482 {
483 const auto pred = t.kf.position();
484 const float dist = distance(pred, d.position);
485
486 // The gate the config documents: one frame's travel budget, widened
487 // sub-linearly with how long the track has been unseen.
488 //
489 // The measurement-noise allowance is not optional. `dist` compares a
490 // predicted position against a *noisy* measurement, so it is on the order
491 // of meas_std even for a perfectly tracked, perfectly stationary object.
492 // Without the term, any detector whose noise approaches max_speed * dt
493 // (0.033 units at 2 units/s and 60 fps) has its own detections gated out:
494 // the track then coasts, dies and is reborn with a new id, several hundred
495 // times a minute, with nothing else in the scene to confuse it with.
496 // The revive gate below already carries the same allowance.
497 const float coast = std::max(m_cfg.coast_time, 1e-3f);
498 const float gate_r = std::max(
499 m_cfg.max_speed * frame_dt * (1.f + t.time_since_seen / coast)
500 + 3.f * m_cfg.meas_std,
501 1e-6f);
502
503 switch(m_cfg.gate)
504 {
506 break;
508 if(dist > gate_r)
509 return false;
510 break;
512 // Covariance growth over missed frames widens this gate naturally.
513 if(t.kf.gating_distance2(d.position, m_cfg.meas_std * m_cfg.meas_std)
514 > m_cfg.mahalanobis_thresh)
515 return false;
516 break;
517 }
518
519 cost = dist / gate_r;
520
521 // OC-SORT direction consistency, multi-dt (OC-SORT sec. 3.2 + Table 7):
522 // prefer the detection that continues the track's OBSERVED motion,
523 // summing the angle term over temporal baselines of 1, 2 and 3 detection
524 // frames. A single-frame direction is dominated by measurement noise; a
525 // longer baseline averages it out (their gains stop at dt=3 and decline
526 // at dt=5, so we stop at 3). Observed displacements, not the Kalman
527 // velocity: the Kalman estimate is effectively a dt~1 quantity, and the
528 // real variable here is the temporal baseline.
529 if(m_cfg.dir_weight > 0.f && t.obs_count >= 2)
530 {
531 const int max_dt = std::min<int>(3, t.obs_count - 1);
532 float dir_cost = 0.f;
533 int dir_terms = 0;
534 for(int k = 1; k <= max_dt; k++)
535 {
536 // v: track displacement over the last k observation frames.
537 // w: displacement from the observation k frames before the candidate
538 // to the candidate detection (the same k-frame span, one frame on).
539 const auto& h0 = t.obs_hist[0];
540 const auto& hv = t.obs_hist[std::size_t(k)];
541 const auto& hw = t.obs_hist[std::size_t(k - 1)];
542 float nv = 0.f, nw = 0.f, dot = 0.f;
543 for(std::size_t i = 0; i < N; i++)
544 {
545 const float v = h0[i] - hv[i];
546 const float w = d.position[i] - hw[i];
547 nv += v * v;
548 nw += w * w;
549 dot += v * w;
550 }
551 if(nv > 1e-8f && nw > 1e-8f)
552 {
553 const float cosang
554 = std::clamp(dot / (std::sqrt(nv) * std::sqrt(nw)), -1.f, 1.f);
555 dir_cost += std::acos(cosang) / 3.14159265f;
556 dir_terms++;
557 }
558 }
559 // Averaged over the available baselines so the term keeps the [0,1]
560 // scale (and the 0.2 default weight) of the single-dt original;
561 // measured: the raw sum triples the penalty on true matches whose
562 // one-frame direction is noise, and costs id switches under clutter.
563 if(dir_terms > 0)
564 cost += m_cfg.dir_weight * (dir_cost / float(dir_terms));
565 }
566
567 // Hybrid-SORT TCM: a track's detection confidence trends smoothly (it
568 // sinks as an occluder approaches, rises on exit), so the predicted
569 // confidence discriminates between tracks exactly when their positions
570 // are entangled. Kalman prediction in the high-confidence stage; in the
571 // low-confidence stage a two-point linear extrapolation instead, because
572 // confidence jumps at occlusion boundaries and the Kalman lags there.
573 if(m_cfg.conf_weight > 0.f)
574 {
575 float chat;
576 if(high_stage)
577 chat = t.conf_kf.p;
578 else if(t.conf_prev >= 0.f)
579 chat = t.confidence + (t.confidence - t.conf_prev);
580 else
581 chat = t.confidence;
582 chat = std::clamp(chat, 0.f, 1.f);
583 cost += m_cfg.conf_weight * std::abs(chat - d.confidence);
584 }
585 return true;
586 }
587
588 void apply_match(track& t, const detection& d)
589 {
590 const float r = std::max(m_cfg.meas_std * m_cfg.meas_std, 1e-12f);
591 const float gap = t.time_since_seen;
592 const bool was_lost = t.state == track_state::lost;
593
594 if(was_lost && m_cfg.revive_reupdate && t.hits > 0)
595 {
596 // OC-SORT ORU: restore the filter to its state at the last measurement
597 // and re-run it along a straight virtual trajectory across the gap, so
598 // the revived track carries a sane velocity instead of lurching from the
599 // coasted prediction to the new position.
600 t.kf = t.kf_at_meas;
601 const int steps
602 = std::clamp(int(std::lround(gap / std::max(m_est_period, 1e-3f))), 1, 32);
603 const float step_dt = gap / float(steps);
604 for(int k = 1; k <= steps; k++)
605 {
606 const float a = float(k) / float(steps);
607 std::array<float, N> z;
608 for(std::size_t i = 0; i < N; i++)
609 z[i] = t.last_meas[i] + a * (d.position[i] - t.last_meas[i]);
610 t.kf.predict(step_dt, m_cfg.accel_sigma);
611 t.kf.update(z, r);
612 }
613 }
614 else
615 {
616 t.kf.update(d.position, r);
617 // Velocity re-seed after a shorter gap: the observation-implied velocity
618 // beats the drifted one.
619 if(gap > 2.f * m_est_period && gap > 1e-4f && t.hits > 0)
620 {
621 std::array<float, N> v;
622 for(std::size_t i = 0; i < N; i++)
623 v[i] = (d.position[i] - t.last_meas[i]) / gap;
624 t.kf.set_velocity(v);
625 }
626 }
627
628 t.hit_history = (t.hit_history << 1) | 1u;
629 t.hits++;
630 t.consecutive_misses = 0;
631 t.time_since_seen = 0.f;
632 t.conf_prev = t.confidence; // c[t-2] for the linear TCM extrapolation
633 t.confidence = d.confidence;
634 t.conf_kf.update(d.confidence, conf_meas_std * conf_meas_std);
635 // Push into the observed-position ring (obs_hist[0] tracks last_meas).
636 for(std::size_t k = t.obs_hist.size() - 1; k > 0; k--)
637 t.obs_hist[k] = t.obs_hist[k - 1];
638 t.obs_hist[0] = d.position;
639 if(t.obs_count < t.obs_hist.size())
640 t.obs_count++;
641 t.last_meas = d.position;
642 t.kf_at_meas = t.kf;
643
644 switch(t.state)
645 {
647 const auto window_mask
648 = m_cfg.confirm_window >= 32 ? ~0u : ((1u << m_cfg.confirm_window) - 1u);
649 const auto window_hits = std::popcount(t.hit_history & window_mask);
650 if(d.confidence >= m_cfg.instant_confirm
651 || (t.age >= m_cfg.confirm_time && window_hits >= int(m_cfg.confirm_hits)))
652 {
653 t.state = track_state::confirmed;
654 m_events.confirmed.push_back(t.id);
655 allocate_slot(t);
656 }
657 break;
658 }
660 t.state = track_state::revived;
661 t.reacquired = true;
662 m_events.revived.push_back(t.id);
663 break;
666 t.state = track_state::confirmed;
667 break;
668 default:
669 break;
670 }
671 }
672
673 std::int32_t birth(const detection& d)
674 {
675 track t;
676 t.id = m_next_id++;
677 t.creation_time = m_now;
678 t.confidence = d.confidence;
679 t.last_meas = d.position;
680 t.hit_history = 1;
681 t.hits = 1;
682 // Initial uncertainty: position at measurement noise, velocity wide open
683 // (up to max_speed, 1 sigma at half of it).
684 const float vp = std::max(m_cfg.meas_std * m_cfg.meas_std, 1e-12f);
685 const float sv = 0.5f * std::max(m_cfg.max_speed, 1e-3f);
686 t.kf.initiate(d.position, vp, sv * sv);
687 t.kf_at_meas = t.kf;
688 // TCM confidence filter: position at the detector's confidence jitter,
689 // rate wide open enough to latch onto an occlusion ramp quickly.
690 t.conf_kf.initiate(
691 d.confidence, conf_meas_std * conf_meas_std, conf_sigma_a * conf_sigma_a);
692 t.obs_hist[0] = d.position;
693 t.obs_count = 1;
694 t.filtered = d.position;
695 for(auto& s : t.smoothers)
696 s.assign_parameters(m_proto);
697 m_events.entered.push_back(t.id);
698 if(d.confidence >= m_cfg.instant_confirm)
699 {
700 t.state = track_state::confirmed;
701 m_events.confirmed.push_back(t.id);
702 allocate_slot(t);
703 }
704 m_tracks.push_back(std::move(t));
705 return m_tracks.back().id;
706 }
707
708 void refresh_states_and_expire()
709 {
710 // Missed-frame transitions are time-based so that they also happen when the
711 // detector goes silent and only advance() is being called.
712 const float missed = 1.5f * m_est_period;
713 for(auto& t : m_tracks)
714 {
715 switch(t.state)
716 {
719 if(t.time_since_seen > missed)
720 t.state = track_state::coasting;
721 break;
723 if(t.time_since_seen > m_cfg.coast_time)
724 t.state = m_cfg.revive ? track_state::lost : track_state::expired;
725 break;
727 if(t.time_since_seen > m_cfg.coast_time + m_cfg.revive_time)
728 t.state = track_state::expired;
729 break;
731 // A tentative track may only miss as many consecutive frames as the
732 // M-of-N window allows; it also dies with the coast window.
733 if(t.consecutive_misses > m_cfg.confirm_window - m_cfg.confirm_hits
734 || t.time_since_seen > m_cfg.coast_time)
735 t.state = track_state::expired;
736 break;
737 default:
738 break;
739 }
740 }
741
742 // Exactly-once exit: the event fires here and the track is removed in the
743 // same sweep, so it can never be reported dead twice.
744 m_tracks.erase(
745 std::remove_if(
746 m_tracks.begin(), m_tracks.end(),
747 [&](track& t) {
748 if(t.state != track_state::expired)
749 return false;
750 free_slot(t);
751 m_events.exited.push_back(t.id);
752 return true;
753 }),
754 m_tracks.end());
755
756 // Confirmed tracks left unslotted (quarantine, or a full bank) retry as
757 // slots free up - without stealing, so one slot cannot ping-pong between
758 // two tracks.
759 for(auto& t : m_tracks)
760 if(t.slot < 0 && t.emitted(false))
761 allocate_slot(t, false);
762 }
763
764 void smooth_outputs(float dt)
765 {
766 for(auto& t : m_tracks)
767 {
768 const auto p = t.kf.position();
769 if(m_cfg.smooth)
770 {
771 for(std::size_t i = 0; i < N; i++)
772 t.filtered[i] = t.smoothers[i](p[i], dt);
773 }
774 else
775 {
776 t.filtered = p;
777 }
778 }
779 }
780
781 // --- Slots ---------------------------------------------------------------
782
783 bool slot_occupied(std::int32_t s) const noexcept
784 {
785 for(const auto& t : m_tracks)
786 if(t.slot == s)
787 return true;
788 return false;
789 }
790
791 bool slot_quarantined(std::int32_t s) const noexcept
792 {
793 for(const auto& v : m_vacated)
794 if(v.slot == s)
795 return true;
796 return false;
797 }
798
799 void purge_vacated()
800 {
801 m_vacated.erase(
802 std::remove_if(
803 m_vacated.begin(), m_vacated.end(),
804 [&](const vacated& v) { return m_now - v.time > m_cfg.slot_hold_time; }),
805 m_vacated.end());
806 }
807
808 void take_vacated(std::int32_t s)
809 {
810 m_vacated.erase(
811 std::remove_if(
812 m_vacated.begin(), m_vacated.end(),
813 [&](const vacated& v) { return v.slot == s; }),
814 m_vacated.end());
815 }
816
817 void allocate_slot(track& t, bool allow_steal = true)
818 {
819 const auto count = std::int32_t(m_cfg.slot_count);
820 if(count <= 0 || t.slot >= 0)
821 return;
822 purge_vacated();
823
824 // A track re-entering near a spot something recently left is, more often
825 // than not, the same physical object: give it the vacated slot back.
826 if(m_cfg.allocation == track_slot_allocation::nearest_vacated && !m_vacated.empty())
827 {
828 const auto pos = t.kf.position();
829 float best = std::numeric_limits<float>::max();
830 std::int32_t best_slot = -1;
831 for(const auto& v : m_vacated)
832 {
833 const float radius
834 = m_cfg.max_speed * float(m_now - v.time) + 10.f * m_cfg.meas_std;
835 const float d = distance(pos, v.position);
836 if(d <= radius && d < best && !slot_occupied(v.slot))
837 {
838 best = d;
839 best_slot = v.slot;
840 }
841 }
842 if(best_slot >= 0)
843 {
844 take_vacated(best_slot);
845 t.slot = best_slot;
846 return;
847 }
848 }
849
850 // Free and not quarantined, in the policy's order.
851 const auto try_range = [&](bool allow_quarantined) -> std::int32_t {
852 if(m_cfg.allocation == track_slot_allocation::round_robin)
853 {
854 for(std::int32_t k = 0; k < count; k++)
855 {
856 const std::int32_t s = (m_rr_cursor + k) % count;
857 if(!slot_occupied(s) && (allow_quarantined || !slot_quarantined(s)))
858 {
859 m_rr_cursor = (s + 1) % count;
860 return s;
861 }
862 }
863 }
864 else
865 {
866 for(std::int32_t s = 0; s < count; s++)
867 if(!slot_occupied(s) && (allow_quarantined || !slot_quarantined(s)))
868 return s;
869 }
870 return -1;
871 };
872
873 // Quarantined slots are strictly held back: an unslotted track retries
874 // every step (see refresh_states_and_expire), so it picks the slot up as
875 // soon as the hold lapses.
876 const std::int32_t s = try_range(false);
877 if(s >= 0)
878 {
879 take_vacated(s);
880 t.slot = s;
881 return;
882 }
883
884 // All slots owned: steal, if the policy allows. Only at confirmation time -
885 // a retry must not steal, or two tracks would trade one slot forever.
886 if(!allow_steal || m_cfg.steal == track_slot_steal::never)
887 return;
888 track* victim = nullptr;
889 for(auto& o : m_tracks)
890 {
891 if(o.slot < 0 || &o == &t)
892 continue;
893 if(!victim)
894 {
895 victim = &o;
896 continue;
897 }
898 const bool better = m_cfg.steal == track_slot_steal::stalest
899 ? o.time_since_seen > victim->time_since_seen
900 : o.confidence < victim->confidence;
901 if(better)
902 victim = &o;
903 }
904 if(victim)
905 {
906 t.slot = victim->slot;
907 victim->slot = -1;
908 }
909 }
910
911 void free_slot(track& t)
912 {
913 if(t.slot < 0)
914 return;
915 m_vacated.push_back({t.slot, t.kf.position(), m_now});
916 t.slot = -1;
917 }
918
919 config m_cfg{};
920 one_euro_filter<float> m_proto{};
921 std::vector<track> m_tracks;
922 std::int32_t m_next_id = 1;
923 double m_now = 0.;
924 double m_last_frame_time = 0.;
925 float m_est_period = 1.f / 30.f;
926 std::int32_t m_rr_cursor = 0;
927
928 track_events m_events;
929 ossia::small_vector<vacated, 8> m_vacated;
930
931 // Per-frame scratch, reused so the hot path does not allocate once warm.
932 std::vector<std::int32_t> m_assign;
933 std::vector<std::int32_t> m_high, m_low, m_t_match;
934 std::vector<char> m_d_used;
935 std::vector<match_candidate> m_cands;
936};
937
938}
Definition git_info.h:7
track_slot_allocation
Definition point_tracker.hpp:74
@ round_robin
Cycle through the slots, spreading reuse over time.
@ nearest_vacated
Prefer a recently-vacated slot near the track's position.
@ lowest_free
Lowest free slot index (cv.jit-style, deterministic).
track_motion_gate
Definition point_tracker.hpp:67
@ max_speed
Analytic: displacement <= max_speed * dt * (1 + lost/coast).
@ off
No motion gate: nearest-neighbour within the cost ranking.
@ mahalanobis
Kalman gating distance against a chi-square threshold.
track_state
Definition point_tracker.hpp:57
@ lost
Past the coast window; kept for revival, not emitted.
@ coasting
Missed recently; position is the Kalman prediction.
@ revived
Re-acquired after being lost (transient, one frame).
@ expired
Terminal; the track is removed right after this is set.
@ provisional
Seen, but not yet confirmed: usable for triggers, flagged.
@ confirmed
Passed M-of-N + time confirmation: the stable set.
void greedy_assignment(std::vector< match_candidate > &candidates, std::int32_t *track_match, std::size_t n_tracks, char *det_used, std::size_t n_dets) noexcept
Greedy bipartite matching: repeatedly take the cheapest remaining candidate whose track and detection...
Definition tracking.hpp:227
track_slot_steal
Definition point_tracker.hpp:81
@ lowest_confidence
Steal from the track with the lowest confidence.
@ stalest
Steal from the track unseen for the longest time.
@ never
A confirmed track without a free slot stays unslotted.
static OSSIA_INLINE process_noise cwna(float sigma_a, float dt) noexcept
Discretised continuous-white-noise-acceleration process noise.
Definition tracking.hpp:56
Lifecycle notifications of one step. Ids, not indices: by the time an exit is reported the track is n...
Definition point_tracker.hpp:242