Recipe: learned MOTR-style appearance tracker
Recipe: learned MOTR-style appearance tracker
The learned tracker is the cosine appearance tracker with its closed-form embedding filter replaced by two small learned modules, in the spirit of MOTR's query-propagation filter:
- a Propagator — the predict step: a residual MLP that nudges a track embedding forward in time and renormalizes it onto the unit sphere.
- a Fuser — the update step: a gated residual fuse of a track embedding with its matched detection's embedding.
LearnedProcess wraps the Propagator into the state's predict, and
LearnedObservation wraps the Fuser into the state's update on match. The
association is unchanged from cosine_tracker.md: Cosine("embedding")
gated by class. Because both modules are autograd-native, the same
cosine/Sinkhorn association objective used at inference trains them.
The two modules are trained once by
sources/unitrack/benchmarks/hota/train_learned.py
(python -m unitrack.benchmarks.hota.train_learned), which extracts detection
embeddings over a few train clips, assigns each a GT track id by mask-IoU, and
optimizes the propagated-then-matched embeddings to recover the GT
correspondences via a Sinkhorn soft-assignment loss. It writes a small
safetensors checkpoint that this factory loads; the factory raises a clear
FileNotFoundError pointing at the training script if the checkpoint is
absent, so the filter is never silently untrained.
import torch
import torch.nn.functional as F
from torch import nn
import unitrack
from unitrack.assignment import Associate, Jonker
from unitrack.costs import Cosine
from unitrack.data import TensorSpec
from unitrack.gates import ClassGate
from unitrack.lifecycle import IncludeAll, NoLifecycle
from unitrack.pipeline import Gated, Pipe
from unitrack.states import (
FromDetectionField,
Identity,
LearnedObservation,
LearnedProcess,
Replace,
State,
)
EMBED_DIM = 256
class Propagator(nn.Module):
"""Predict step: residual MLP over a track embedding, renormalized."""
def __init__(self, dim: int, hidden: int = 64) -> None:
super().__init__()
self.net = nn.Sequential(
nn.Linear(dim, hidden), nn.Tanh(), nn.Linear(hidden, dim)
)
def forward(self, x: torch.Tensor, dt: float = 1.0) -> torch.Tensor:
del dt # constant-rate propagation
return F.normalize(x + self.net(x), dim=-1)
class Fuser(nn.Module):
"""Update step: gated residual fuse of track + matched measurement."""
def __init__(self, dim: int, hidden: int = 64) -> None:
super().__init__()
self.gate = nn.Sequential(
nn.Linear(2 * dim, hidden),
nn.Tanh(),
nn.Linear(hidden, dim),
nn.Sigmoid(),
)
def forward(self, track: torch.Tensor, meas: torch.Tensor) -> torch.Tensor:
g = self.gate(torch.cat([track, meas], dim=-1))
return F.normalize(g * meas + (1 - g) * track, dim=-1)
def build_learned_tracker(
*,
checkpoint: str,
cost_threshold: float = 0.5,
embed_dim: int = EMBED_DIM,
) -> unitrack.Tracker:
from safetensors.torch import load_file
flat = load_file(checkpoint) # raises if the checkpoint is missing
prop, fuse = Propagator(embed_dim), Fuser(embed_dim)
prop.load_state_dict(
{
k[len("propagator.") :]: v
for k, v in flat.items()
if k.startswith("propagator.")
}
)
fuse.load_state_dict(
{k[len("fuser.") :]: v for k, v in flat.items() if k.startswith("fuser.")}
)
prop.eval()
fuse.eval()
inner = Pipe(
cost=Cosine("embedding"),
assoc=Associate(Jonker(threshold=cost_threshold)),
)
pipeline = Gated(gate=ClassGate("category"), then=inner)
return unitrack.Tracker(
root=pipeline,
states={
"embedding": State(
schema=TensorSpec(shape=(embed_dim,), dtype=torch.float32),
process=LearnedProcess("embedding", prop),
observation=LearnedObservation("embedding", "embedding", fuse),
init=FromDetectionField("embedding"),
),
"category": State(
schema=TensorSpec(shape=(), dtype=torch.int64),
process=Identity("category"),
observation=Replace("category"),
init=FromDetectionField("category"),
),
},
lifecycle=NoLifecycle(),
visibility=IncludeAll(),
)
The packaged factory (unitrack.benchmarks.hota.tracker.build_learned_tracker)
defaults checkpoint to the committed
benchmarks/hota/weights/learned_filter.safetensors.
Detection requirements: identical to cosine_tracker.md — an embedding(M, embed_dim) per instance and an int64 category (M,).