2#include <ossia/detail/config.hpp>
4#include <ossia/detail/small_vector.hpp>
87template <std::
size_t N>
88struct point_tracker_config
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;
129 float accel_sigma = 0.67f;
131 float meas_std = 0.005f;
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;
144 float coast_time = 0.5f;
146 float revive_time = 2.f;
149 bool revive_reupdate =
true;
153 float min_cutoff = 1.f;
155 float deriv_cutoff = 1.f;
158 std::uint32_t slot_count = 8;
163 float slot_hold_time = 0.25f;
166template <std::
size_t N>
167struct point_detection
169 std::array<float, N> position{};
170 float confidence = 1.f;
173template <std::
size_t N>
177 std::int32_t
id = -1;
179 std::int32_t slot = -1;
183 double creation_time = 0.;
185 float time_since_seen = 0.f;
186 float confidence = 0.f;
187 bool reacquired =
false;
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;
210 std::array<one_euro_filter<float>, N> smoothers{};
213 std::uint32_t hit_history = 0;
214 std::uint32_t hits = 0;
215 std::uint32_t consecutive_misses = 0;
217 [[nodiscard]] std::array<float, N> position() const noexcept {
return kf.position(); }
218 [[nodiscard]] std::array<float, N> velocity() const noexcept {
return kf.velocity(); }
221 [[nodiscard]]
bool emitted(
bool include_provisional)
const noexcept
230 return include_provisional;
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;
248 void clear()
noexcept
257template <std::
size_t N>
261 using config = point_tracker_config<N>;
262 using detection = point_detection<N>;
263 using track = point_track<N>;
265 void configure(
const config& 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)
273 for(
auto& s : t.smoothers)
274 s.assign_parameters(m_proto);
276 if(t.slot >= std::int32_t(c.slot_count))
281 m_vacated.begin(), m_vacated.end(),
282 [&](
const vacated& v) { return v.slot >= std::int32_t(c.slot_count); }),
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; }
301 m_last_frame_time = 0.;
302 m_est_period = 1.f / 30.f;
314 void advance(
float dt)
318 refresh_states_and_expire();
329 const std::vector<std::int32_t>& update(
const detection* dets, std::size_t n,
float dt)
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);
339 auto& out = m_assign;
345 for(std::size_t i = 0; i < n; i++)
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));
354 m_t_match.assign(m_tracks.size(), -1);
355 m_d_used.assign(n, 0);
360 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
362 const auto& t = m_tracks[ti];
365 for(
const auto di : m_high)
368 if(admissible(t, dets[di], frame_dt,
true, c))
369 m_cands.push_back({c, std::int32_t(ti), di});
372 greedy_assignment(m_cands, m_t_match.data(), m_tracks.size(), m_d_used.data(), n);
379 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
381 if(m_t_match[ti] >= 0)
383 const auto& t = m_tracks[ti];
386 for(
const auto di : m_low)
389 if(admissible(t, dets[di], frame_dt,
false, c))
390 m_cands.push_back({c, std::int32_t(ti), di});
393 greedy_assignment(m_cands, m_t_match.data(), m_tracks.size(), m_d_used.data(), n);
397 for(std::size_t ti = 0; ti < m_tracks.size(); ti++)
399 auto& t = m_tracks[ti];
400 const auto di = m_t_match[ti];
403 apply_match(t, dets[di]);
409 t.consecutive_misses++;
414 for(std::size_t i = 0; i < n; i++)
416 if(m_d_used[i] || dets[i].confidence < m_cfg.new_conf)
418 out[i] = birth(dets[i]);
421 refresh_states_and_expire();
427 const std::vector<std::int32_t>& update(
const std::vector<detection>& dets,
float dt)
429 return update(dets.data(), dets.size(), dt);
436 static constexpr float conf_sigma_a = 2.f;
437 static constexpr float conf_meas_std = 0.05f;
442 std::array<float, N> position;
447 distance(
const std::array<float, N>& a,
const std::array<float, N>& b)
noexcept
450 for(std::size_t i = 0; i < N; i++)
452 const float d = a[i] - b[i];
455 return std::sqrt(d2);
458 void step_time(
float dt)
466 for(
auto& t : m_tracks)
468 t.kf.predict(dt, m_cfg.accel_sigma);
469 t.conf_kf.predict(dt, qc);
471 t.time_since_seen += dt;
480 const track& t,
const detection& d,
float frame_dt,
bool high_stage,
481 float& cost)
const noexcept
483 const auto pred = t.kf.position();
484 const float dist = distance(pred, d.position);
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,
513 if(t.kf.gating_distance2(d.position, m_cfg.meas_std * m_cfg.meas_std)
514 > m_cfg.mahalanobis_thresh)
519 cost = dist / gate_r;
529 if(m_cfg.dir_weight > 0.f && t.obs_count >= 2)
531 const int max_dt = std::min<int>(3, t.obs_count - 1);
532 float dir_cost = 0.f;
534 for(
int k = 1; k <= max_dt; k++)
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++)
545 const float v = h0[i] - hv[i];
546 const float w = d.position[i] - hw[i];
551 if(nv > 1e-8f && nw > 1e-8f)
554 = std::clamp(dot / (std::sqrt(nv) * std::sqrt(nw)), -1.f, 1.f);
555 dir_cost += std::acos(cosang) / 3.14159265f;
564 cost += m_cfg.dir_weight * (dir_cost / float(dir_terms));
573 if(m_cfg.conf_weight > 0.f)
578 else if(t.conf_prev >= 0.f)
579 chat = t.confidence + (t.confidence - t.conf_prev);
582 chat = std::clamp(chat, 0.f, 1.f);
583 cost += m_cfg.conf_weight * std::abs(chat - d.confidence);
588 void apply_match(track& t,
const detection& d)
590 const float r = std::max(m_cfg.meas_std * m_cfg.meas_std, 1e-12f);
591 const float gap = t.time_since_seen;
594 if(was_lost && m_cfg.revive_reupdate && t.hits > 0)
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++)
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);
616 t.kf.update(d.position, r);
619 if(gap > 2.f * m_est_period && gap > 1e-4f && t.hits > 0)
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);
628 t.hit_history = (t.hit_history << 1) | 1u;
630 t.consecutive_misses = 0;
631 t.time_since_seen = 0.f;
632 t.conf_prev = t.confidence;
633 t.confidence = d.confidence;
634 t.conf_kf.update(d.confidence, conf_meas_std * conf_meas_std);
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())
641 t.last_meas = d.position;
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)))
654 m_events.confirmed.push_back(t.id);
662 m_events.revived.push_back(t.id);
673 std::int32_t birth(
const detection& d)
677 t.creation_time = m_now;
678 t.confidence = d.confidence;
679 t.last_meas = d.position;
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);
691 d.confidence, conf_meas_std * conf_meas_std, conf_sigma_a * conf_sigma_a);
692 t.obs_hist[0] = d.position;
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)
701 m_events.confirmed.push_back(t.id);
704 m_tracks.push_back(std::move(t));
705 return m_tracks.back().id;
708 void refresh_states_and_expire()
712 const float missed = 1.5f * m_est_period;
713 for(
auto& t : m_tracks)
719 if(t.time_since_seen > missed)
723 if(t.time_since_seen > m_cfg.coast_time)
727 if(t.time_since_seen > m_cfg.coast_time + m_cfg.revive_time)
733 if(t.consecutive_misses > m_cfg.confirm_window - m_cfg.confirm_hits
734 || t.time_since_seen > m_cfg.coast_time)
746 m_tracks.begin(), m_tracks.end(),
748 if(t.state != track_state::expired)
751 m_events.exited.push_back(t.id);
759 for(
auto& t : m_tracks)
760 if(t.slot < 0 && t.emitted(false))
761 allocate_slot(t, false);
764 void smooth_outputs(
float dt)
766 for(
auto& t : m_tracks)
768 const auto p = t.kf.position();
771 for(std::size_t i = 0; i < N; i++)
772 t.filtered[i] = t.smoothers[i](p[i], dt);
783 bool slot_occupied(std::int32_t s)
const noexcept
785 for(
const auto& t : m_tracks)
791 bool slot_quarantined(std::int32_t s)
const noexcept
793 for(
const auto& v : m_vacated)
803 m_vacated.begin(), m_vacated.end(),
804 [&](
const vacated& v) { return m_now - v.time > m_cfg.slot_hold_time; }),
808 void take_vacated(std::int32_t s)
812 m_vacated.begin(), m_vacated.end(),
813 [&](
const vacated& v) { return v.slot == s; }),
817 void allocate_slot(track& t,
bool allow_steal =
true)
819 const auto count = std::int32_t(m_cfg.slot_count);
820 if(count <= 0 || t.slot >= 0)
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)
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))
844 take_vacated(best_slot);
851 const auto try_range = [&](
bool allow_quarantined) -> std::int32_t {
854 for(std::int32_t k = 0; k < count; k++)
856 const std::int32_t s = (m_rr_cursor + k) % count;
857 if(!slot_occupied(s) && (allow_quarantined || !slot_quarantined(s)))
859 m_rr_cursor = (s + 1) % count;
866 for(std::int32_t s = 0; s < count; s++)
867 if(!slot_occupied(s) && (allow_quarantined || !slot_quarantined(s)))
876 const std::int32_t s = try_range(
false);
888 track* victim =
nullptr;
889 for(
auto& o : m_tracks)
891 if(o.slot < 0 || &o == &t)
899 ? o.time_since_seen > victim->time_since_seen
900 : o.confidence < victim->confidence;
906 t.slot = victim->slot;
911 void free_slot(track& t)
915 m_vacated.push_back({t.slot, t.kf.position(), m_now});
920 one_euro_filter<float> m_proto{};
921 std::vector<track> m_tracks;
922 std::int32_t m_next_id = 1;
924 double m_last_frame_time = 0.;
925 float m_est_period = 1.f / 30.f;
926 std::int32_t m_rr_cursor = 0;
928 track_events m_events;
929 ossia::small_vector<vacated, 8> m_vacated;
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;
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