Source code for pose3d.tracker
"""
Cross-frame 3D track identity for the 3D Pose Reconstruction plugin
(analysis/run_pose3d.py). Greedy nearest-3D-centroid assignment between
consecutive analysed ticks — the same greedy-nearest-neighbor idiom
analysis/motion/centroid_tracker.py's CentroidTracker._assign() already
uses for 2D pixel centroids, deliberately reimplemented here for 3D
room-mm centroids rather than shared/generalized, matching this codebase's
established per-plugin-owns-its-math precedent (e.g. item 18's gaze/pose
iris-heuristic duplication).
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
[docs]
@dataclass
class TrackedPerson3D:
track_id: int
last_tick: int
last_centroid_room: np.ndarray
[docs]
class PersonTracker3D:
def __init__(self, max_gap_ticks: int = 5, max_jump_mm: float = 400.0) -> None:
self.max_gap_ticks = max_gap_ticks
self.max_jump_mm = max_jump_mm
self._tracks: dict = {}
self._next_id = 0
[docs]
def update(self, tick: int, cluster_centroids) -> list:
"""Returns one track_id per input centroid, same order. Existing
tracks within max_jump_mm are matched greedily (nearest pair
first); unmatched existing tracks age by (tick - last_tick) and are
dropped once that exceeds max_gap_ticks; unmatched new centroids
get a fresh monotonically-increasing track_id."""
if not cluster_centroids:
self._age_all(tick)
return []
active_ids = list(self._tracks.keys())
result_ids = [-1] * len(cluster_centroids)
matched_cents: set = set()
if active_ids:
track_pos = np.array([self._tracks[tid].last_centroid_room for tid in active_ids])
cent_pos = np.array(cluster_centroids)
diffs = track_pos[:, np.newaxis, :] - cent_pos[np.newaxis, :, :]
cost = np.linalg.norm(diffs, axis=2) # (n_tracks, n_centroids)
while True:
if cost.size == 0:
break
min_idx = np.unravel_index(np.argmin(cost), cost.shape)
ti, ci = int(min_idx[0]), int(min_idx[1])
if cost[ti, ci] > self.max_jump_mm:
break
tid = active_ids[ti]
self._tracks[tid].last_tick = tick
self._tracks[tid].last_centroid_room = cluster_centroids[ci]
result_ids[ci] = tid
matched_cents.add(ci)
cost[ti, :] = np.inf
cost[:, ci] = np.inf
for ci, centroid in enumerate(cluster_centroids):
if ci not in matched_cents:
new_id = self._next_id
self._next_id += 1
self._tracks[new_id] = TrackedPerson3D(
track_id=new_id, last_tick=tick, last_centroid_room=np.asarray(centroid)
)
result_ids[ci] = new_id
self._age_all(tick)
return result_ids
def _age_all(self, tick: int) -> None:
stale = [tid for tid, t in self._tracks.items() if tick - t.last_tick > self.max_gap_ticks]
for tid in stale:
del self._tracks[tid]