EagerMot
Paper: EagerMOT: 3D Multi-Object Tracking via Sensor Fusion
EagerMOT associates 3D detections with image detections, tracks objects in 3D,
and uses remaining image observations to sustain tracks when 3D association
fails. BoxMOT integrates the
authors' Python implementation
under boxmot/trackers/eagermot.
Required inputs
Use the Python tracker API with independent Detections and Detections3D
batches plus a CameraModel on every update. Both batches share a sample_id,
but their rows and lengths are independent. Pass an explicitly empty batch
when either detector has no observations. Map both detectors into the same
class-ID catalog before fusion.
- Image detections use AABB geometry and may be empty. A nonempty 3D observation
is needed to create a track; 2D observations can then sustain it through depth
dropouts. Optional masks are retained on image outputs. An optional
Framesupplies sample and dimension checks; RGB pixels and ReID embeddings are unused. Boxes3Dstores CPU-contiguousfloat32[N,7]values in the order(x, y, z, yaw, length, width, height). Position is the bottom-face center, dimensions are meters, and yaw is radians. Camera axes are x right, y down, z forward, with yaw about +y.CameraModel.projectionis a CPU-contiguousfloat32[3,4]matrix that projects camera coordinates to pixels.image_sizeis(height, width).- For a moving camera, supply its current
camera_to_worldrigid transform. Full rotation and translation transform box centers. The tracking state retains yaw-only cuboids, so their roll and pitch are approximated. Poses are optional: without them, motion is modeled in camera coordinates and includes any camera movement. Keep pose availability consistent throughout a sequence.
Use create_tracker(TrackerSpec("eagermot")) or boxmot.EagerMot for the
Python API. boxmot tune --dataset ./kitti-mots --tracker eagermot reads
these inputs from a multimodal sequence dataset.
The boxmot eval --tracker eagermot command also accepts --dataset ./kitti-mots. Image tracking, TrackingPipeline, and
cached perception replay cannot supply the required sensor inputs and reject
eagermot before running perception.
Use your own sensor data
Copy the sensor dataset template and supply synchronized images, 3D detections, camera projection, and evaluation annotations. Default mask evaluation also needs 2D detections with instance masks. Declare absolute camera-to-world poses when supplying ego-motion compensation:
mkdir -p ./my-sensor-dataset
cp boxmot/configs/datasets/sensor-fusion.yaml ./my-sensor-dataset/dataset.yaml
# Populate the sequence and prediction files described in the template README.
boxmot eval --dataset ./my-sensor-dataset --tracker eagermot --split val
boxmot tune --dataset ./my-sensor-dataset --tracker eagermot \
--split train --n-trials 50 --seed 0
Dataset splits, partition names, and sequence names such as drive-001 are
authored in dataset.yaml, using the same schema as the built-in datasets.
Its modalities select encodings and relative paths for images, annotations,
calibration, poses, and independent 2D/3D detections. Split overrides can
select other predictions. trackrcnn and kitti-detections identify file
encodings, so your own detector can export them. The template documents
the complete field order, mask encoding, coordinates, and empty-frame rules.
This workflow evaluates car and pedestrian segmentation tracking by default.
Image prediction masks and ground-truth instance PNGs are required for mask
evaluation and tuning. For 3D evaluation, provide 3D
tracking annotations instead of instance PNG ground truth; the detections_2d
modality is optional in that mode. Omit poses from the modalities when no
ego-motion data is available. Every declared input must still exist. Masks remain
optional for the Python tracker API.
It supports one camera per sequence, with fixed calibration and synchronized
sensor observations. Set dataset fps to your recording rate; it controls
timestamps and saved video playback speed. EagerMOT still advances one motion
step per image. Initial class profiles use the KITTI presets, so tune on your training
sequences and evaluate the saved best.yaml on held-out recordings.
Evaluate downloaded KITTI predictions
Run from the repository root with the existing environment and the mots
extra installed. This replays saved detector predictions on CPU and evaluates
segmentation tracking against KITTI MOTS instance PNGs:
uv run --no-sync python -m boxmot.engine.cli eval --tracker eagermot \
--dataset ./kitti-mots \
--split val \
--project runs/eagermot
The dataset stores each sequence's observations together and declares all
input paths and encodings in dataset.yaml:
kitti-mots/
dataset.yaml
sequences/training/0002/
images/000000.png
ground_truth/000000.png
calibration.txt
poses.npy
predictions/
trackrcnn/training/0002.txt
pointgnn-car-t2/training/0002/000000.txt
pointgnn-car-t3/training/0002/000000.txt
pointgnn-pedestrian/training/0002/000000.txt
KITTI sequence names have four digits and frame names have six digits. The default
MOTS validation split is 0002, 0006, 0007, 0008, 0010, 0013, 0014, 0016, 0018.
Add --sequence 0002 for a smaller run; repeat the option to select several
sequences. To evaluate all 21 annotated sequences, use --split fulltrain.
Sequences replay in parallel. The automatic worker count
uses at most the selected sequence count or the logical CPU count minus two,
with a minimum of one worker. Set --sequence-workers N to override it with a
positive integer cap, still bounded by the selected sequence count.
In dataset.yaml, the validation split can override detections_3d to use
T2 car predictions while training and full training use T3. Edit the modality
paths to change detector inputs. See the dataset schema and split overrides.
Testing images and calibration are retained, but this partition lacks evaluation
ground truth and the 2D predictions needed for mask evaluation and tuning.
Results are written under runs/eagermot/val (then val2, and so on):
metrics.json: mask HOTA, DetA, AssA, LocA, CLEAR and Identity metrics, including per-sequence results and class/detection averages. Percentage metrics use the 0–100 scale, with signed CLEAR scores where applicable.metrics.csv: combined results for each class and aggregate.mots/SEQUENCE.txt: official MOTS predictions with real, disjoint masks.videos/SEQUENCE.mp4: annotated video when--saveis enabled.run.json: dataset config, resolved modality formats/paths/options, selected sequences, and tracker presets.
By default, the runner uses the released car and pedestrian presets separately, with globally unique output identities. Image confidence resolves overlapping masks; empty masks are omitted. Every image frame advances tracking, including frames with no TrackR-CNN rows or missing PointGNN files. RGB pixels are not needed by EagerMOT; image headers establish the timeline and dimensions. Previewing or saving annotated videos reads the images for rendering.
PointGNN scores can exceed one. The reader maps each nonnegative score s to
s / (1 + s) for the canonical score contract. This is a bounded ranking
score, not a calibrated probability; the KITTI presets retain the zero 3D
score threshold, so this mapping does not change accepted detections or mask
association. Ego-motion arrays contain absolute camera-to-world poses and
are indexed directly, without accumulation.
These results do not reproduce the paper's benchmark setup: the image
predictions come from TrackR-CNN, detector checkpoint training provenance has
not been independently verified, and pedestrian detections use the supplied
trainval variant. The default command evaluates masks.
Evaluate 3D tracks
--eval-3d reports 3D tracking HOTA/MOTA/IDF1 using volumetric box IoU.
Declare ground_truth_3d with native KITTI tracking labels (training/label_02)
and run:
boxmot eval --dataset ./kitti-mots --tracker eagermot \
--split val --eval-3d --project runs/kitti-3d
This custom tracking protocol uses all supplied target 3D annotations. HOTA
uses IoU thresholds 0.05 through 0.95; MOTA and IDF1 use 0.5. Official KITTI
difficulty, visibility, truncation and DontCare filtering are not applied.
Per-image object labels and the official evaluator dependencies are unnecessary
for this mode. detections_2d and poses may be omitted; image files still define
frame alignment and dimensions, and camera projection remains required. Any 2D
observations and ego poses that are declared are consumed during tracking.
To add official 2D/3D AP40 (Easy / Moderate / Hard), declare
ground_truth_objects.
These must be native per-image object labels for the exact evaluated frames,
including fractional truncation. BoxMOT does not infer them from tracking labels
or match independently numbered datasets by filename.
Download and extract the official KITTI object devkit, then install once using a C++17 compiler and Boost headers:
boxmot install --extra trackeval --kitti-devkit /path/to/devkit_object
boxmot eval --dataset ./kitti-mots --tracker eagermot \
--split val --eval-3d --eval-ap --project runs/kitti-3d
Installation compiles a cached local harness around the external official
source. Evaluation never downloads or builds dependencies. Missing annotation
modalities or evaluators fail before replay when --eval-ap is requested.
The new split directory contains:
| Output | Contents |
|---|---|
metrics.json, metrics.csv |
Volumetric 3D tracking metrics, including per-sequence results |
detection_metrics.json, detection_metrics.csv |
With --eval-ap: official object AP40 by geometry, class and difficulty |
tracking_2d_metrics.json, tracking_2d_metrics.csv |
With --eval-ap: projected TrackEval KITTI 2D tracking metrics |
evaluation.json |
Protocol and input provenance |
kitti_3d/<sequence>.txt |
Spatial predictions with projected image boxes |
Official object scoring applies its own difficulty and DontCare rules;
the additional projected 2D tracking report uses TrackEval's KITTI preprocessing.
Adding --eval-ap leaves the main 3D tracking scores unchanged. Results describe the selected
local split, not a leaderboard submission. AP is shown as N/A for a
class/difficulty with no eligible ground truth. Short subsets may not span
all 40 official recall samples.
Ground-truth mask PNGs are not required or loaded in this mode. The same
saved sensor observations still drive tracking. --eval-masks and --eval-3d
cannot be combined; omitting both retains mask scoring. tune continues to
optimize mask HOTA. Use --class-config to evaluate saved per-class profiles,
and add --cache-inputs to reuse observations and the selected annotations.
Saved KITTI MOTS validation preset
boxmot/configs/trackers/presets/eagermot-kitti-mots-val.yaml contains the
best saved car and pedestrian profiles from a 200-trial Optuna run. Use both
profiles together:
boxmot eval --dataset ./kitti-mots --tracker eagermot \
--class-config boxmot/configs/trackers/presets/eagermot-kitti-mots-val.yaml
The winning trial scored 67.35 class-average mask HOTA on the nine validation
sequences (car: 78.57, pedestrian: 56.13). These are scores on the fitting
split. The YAML comments record the source run, detector inputs and sequences.
The preset uses the same car/pedestrian format as tuning's best.yaml.
Preview or save a sequence
Use --show to preview tracked masks, identities and classes, and --save to
write an annotated MP4 for each selected sequence. Add --show-3d to overlay
the estimated 3D bounding boxes:
uv run --no-sync python -m boxmot.engine.cli eval --tracker eagermot \
--dataset ./kitti-mots \
--class-config runs/eagermot-tune/val/best.yaml \
--sequence 0016 \
--show \
--save \
--show-3d \
--project runs/eagermot-3d-preview
--class-config loads the saved profiles once when evaluation starts. If the
tuner is still running, this uses a snapshot of its best completed trial;
later improvements do not change the ongoing evaluation. Omit --class-config
to use the default KITTI car and pedestrian presets.
This command writes runs/eagermot-3d-preview/val/videos/0016.mp4 at 10 FPS along
with the normal masks and metrics. Repeated runs create val2, and so on.
Remove --show on a headless machine. Press q or Esc to close the
preview while evaluation and video saving continue.
--show processes sequences one at a time on the main thread; --save alone
allows workers to render and save their assigned sequences in parallel.
--show-3d requires --show or --save. It projects EagerMOT's current 3D
track estimates through the sequence's full camera projection matrix. The
cuboids use PointGNN observations and the Kalman motion state, including
estimates sustained by a 2D observation when a 3D detection is missing.
Only confirmed tracks updated by at least one sensor in the current frame
are shown. A visible 3D-only track can appear without a mask; boxes behind the
camera are skipped. Cuboids and masks share the same global track IDs and
colors. The yaw-only box approximation described below still applies.
This overlay affects visualization only; MOTS predictions, metrics and tuning
remain unchanged.
Tune separate class profiles
With the mots and evolve extras installed, tune the kitti-mots dataset
through the main command:
The dataset config records
sequence locations, classes, splits, and all modality encodings and paths. Images
and ground truth are stored inside the dataset, so it can move independently
of the original downloads. You can pass
--dataset /path/to/kitti-mots/dataset.yaml from any working directory;
relative paths remain anchored to storage.root beside that YAML.
The dataset's default split is used unless you pass --split. Per-split
modality overrides can select different image or spatial prediction files. Every trial
evaluates car and pedestrian profiles together and maximizes their
class-average mask HOTA.
The sensor workflow runs one Optuna trial at a time on CPU with
--search-alg optuna. Sequences within each trial replay in parallel using the
same automatic worker count as evaluation. --sequence-workers N sets a
positive worker cap per trial. An explicit device must be cpu.
--max-concurrent-trials accepts 0 (default) or 1, keeping trials serial;
objective selectors must use HOTA. Perception and build options and
--resume-tune are unavailable for fusion datasets.
--project changes the results root from runs/eagermot-tune.
The trial count includes the first trial with the starting car and pedestrian
profiles, including any loaded or calibrated settings. Remaining trials independently
sample each class's parameters from the shared EagerMOT YAML search ranges. Distance and 3D IoU
thresholds are sampled only for their corresponding matching methods;
max_age_2d and asso_func stay fixed. Trials run serially on CPU using saved
predictions. The tuned det_thresh_3d applies to the transformed PointGNN
score s / (1 + s).
--sequence 0002 restricts tuning to one sequence; repeat it for several.
Results go to runs/eagermot-tune/val, then val2, and so on:
study.sqlite3: the Optuna study and trial history.run.json: input selection, sampling settings and search metadata.best.yaml: complete tracker profiles undercarandpedestrian, with grouped Kalman settings.trials/0000/metrics.jsonandtrials/0000/mots/SEQUENCE.txt: metrics and mask predictions for each numbered trial, starting with the baseline.
Pass the winning profiles to evaluation using --class-config:
uv run --no-sync python -m boxmot.engine.cli eval --tracker eagermot \
--dataset ./kitti-mots \
--split val \
--class-config runs/eagermot-tune/val/best.yaml \
--project runs/eagermot-tuned
This example reevaluates the fitting split. To measure generalization, tune and evaluate on separate sequences with detector checkpoints trained without the evaluation sequences.
Calibrate 3D Kalman noise
Add --calibrate-kf to eval or tune after supplying the optional
ground_truth_3d modality.
It requires 3D annotations with stable object identities; instance masks alone
cannot provide the required 3D trajectories. For KITTI tracking labels, configure
this modality's path as {partition}/label_02/{sequence}.txt, such as
training/label_02/0000.txt.
Download KITTI tracking labels separately if these files are absent. The dataset
continues to use its sequence calibration files and any declared ego poses:
boxmot eval --dataset ./kitti-mots --tracker eagermot \
--split train --calibrate-kf --project runs/eagermot-calibration
boxmot eval --dataset ./kitti-mots --tracker eagermot \
--split val --class-config runs/eagermot-calibration/train/kf-tuning/calibrated.yaml
Calibration matches 3D detections to annotations by class and 3D IoU, then uses the same coordinates as tracking: world coordinates with declared ego poses, or camera coordinates without them. It fits the same five covariance multipliers as 2D Kalman calibration, separately for cars and pedestrians. Process and initial covariance distinguish the seven measured box coordinates from their modeled velocities; measurement noise has one shared multiplier. The values scale covariance, not standard deviation. Ego poses remain fixed, and pose uncertainty is not fitted. When a class has too little evidence for a scale, calibration uses the estimate pooled across classes. If the pooled evidence is also insufficient, it retains that class's configured value. The report records the source and observation counts for each scale.
Each run saves complete class profiles in kf-tuning/calibrated.yaml and
calibration evidence in kf-tuning/calibration.json, including each sequence's
camera or world coordinate frame. Reuse the profiles with
--class-config in eval or tune. Each YAML profile carries its tracker,
backend, geometry, class, and Kalman state dimensions. Loading an incompatible
profile, including a changed kalman.is_angular state layout, fails before replay.
tune --calibrate-kf calibrates once before
Optuna starts; the fitted scales stay fixed by default. Select a scale explicitly,
such as --tune-kf measurement_noise_scale, to search around each class's fitted
value. Each class's kalman.is_angular setting stays fixed, and the grouped settings are
included in best.yaml. Prediction still
advances one frame per image; variable-time prediction is unsupported.
Loading profiles with --class-config also keeps their kalman.is_angular choices
fixed. Tuning without either flag can search the angular-motion choice.
Kalman settings live under kalman in each class profile. Covariance scales and
their timing metadata live under kalman.noise. For a manual class configuration:
car:
kalman:
is_angular: false
noise:
measurement_noise_scale: 1.0
pedestrian:
kalman:
is_angular: true
noise:
measurement_noise_scale: 1.0
Add --cache-inputs to reuse all declared sensor observations and the annotations
selected for scoring or calibration across trials and later runs. Calibration
consumes the same cached 3D inputs.
The cache stores unfiltered detections and packed masks; each trial still applies
its own thresholds and starts fresh trackers. Worker processes remain available
throughout the study. See input caching
for storage and invalidation behavior.
Sensor fusion example
This example supplies one synthetic camera and matching 2D/3D observations. Replace them with calibrated observations for each frame in a real sequence.
import torch
from boxmot import create_tracker
from boxmot.structures import Boxes, Boxes3D, CameraModel, Detections, Detections3D
from boxmot.trackers import TrackerSpec
tracker = create_tracker(TrackerSpec("eagermot"))
camera = CameraModel(
projection=torch.tensor(
[[100, 0, 100, 0], [0, 100, 50, 0], [0, 0, 1, 0]],
dtype=torch.float32,
),
image_size=(100, 200),
)
detections = Detections(
geometry=Boxes(torch.tensor([[78, 39, 122, 62]], dtype=torch.float32)),
scores=torch.tensor([0.95], dtype=torch.float32),
class_ids=torch.tensor([0], dtype=torch.int64),
sample_id="example:0",
)
detections_3d = Detections3D(
geometry=Boxes3D(torch.tensor([[0, 1, 10, 0, 4, 2, 2]], dtype=torch.float32)),
scores=torch.tensor([0.9], dtype=torch.float32),
class_ids=torch.tensor([0], dtype=torch.int64),
sample_id=detections.sample_id,
)
result = tracker.update(detections, detections_3d=detections_3d, camera=camera)
print(result.image_tracks.track_ids, result.spatial_tracks.track_ids)
print(result.spatial_tracks.geometry.values)
tracker.reset()
Outputs and lifecycle
update() returns MultimodalTracks with two independent collections:
image_trackscontains confirmed tracks with a current 2D observation. Detection indices reference the 2D batch; supplied masks stay row-aligned.spatial_trackscontains confirmed 3D tracks updated by either sensor in the current frame, including objects outside the camera image. Detection indices reference the 3D batch and are-1for updates supported only by 2D.
Join the collections by track_ids, not row position. Spatial boxes are
returned in the current camera coordinate system. To render 3D-only tracks in
an image, project their box corners with the camera matrix. Prediction-only
tracks remain internal and are not emitted.
As in the supplied source, a 3D observation starts a new track. A 2D-only
observation can sustain an existing track but does not create an identity.
Motion prediction advances on every frame, including frames with no 3D
detections; this corrects the source's prediction freeze during such dropouts.
Matching always respects class IDs, including when per_class=False.
Confirmation follows the source's initial startup grace period; after startup,
min_hits observations are required. max_age limits time without support
from either sensor, while max_age_2d controls confidence decay when image
support is absent. Spatial scores apply this decay to the latest 3D detector
confidence; even a new 3D-only track receives a reduced score, matching the
source's initial missing-image age. Image scores retain their current detector
confidence.
Configuration and scope
boxmot/configs/trackers/eagermot.yaml uses scalar defaults from the upstream
KITTI car preset. first_matching_method selects the 3D association metric;
distance_threshold is a positive maximum distance, while
iou_3d_threshold applies to 3D IoU matching. fusion_iou_threshold controls
2D/3D observation fusion and iou_threshold controls second-stage image
association. Set iou_threshold=1.0 to disable that second stage. Image
association supports asso_func="iou" only.
The integration supports one camera per sequence and includes the KITTI input reader used above. The nuScenes multiple-camera workflow, native C++ execution, and OBB image geometry are not included. Image association requires at least four box corners in front of the camera, following the upstream nuScenes visibility rule; the upstream KITTI projection path handles boxes behind the camera differently. Spatial output still retains objects without a valid image projection. Full ego poses transform centers exactly, while projection and 3D IoU use regenerated yaw-only cuboids rather than separately stored tilted corners. The pose yaw convention extends the source's XYZ Euler y angle beyond 90° to preserve full camera turns. These choices can change results relative to the original implementation.
The upstream MIT license is retained in the implementation package; the original checkout and dataset SDKs are not runtime dependencies. The integration has not reproduced the upstream KITTI or nuScenes benchmark results.
Python API
Pass algorithm settings through EagerMotConfig, for example
EagerMot(config=EagerMotConfig(distance_threshold=3.5)). Direct construction and
create_tracker("eagermot") use the same algorithm defaults. See the
tracker configuration guide for presets and component settings.
Bases: TrackerConfig
Configure fusion, 3D matching, and the image-only recovery stage.
Shared history and association settings are inherited from TrackerConfig.
Parameters:
-
max_age(int, default:3) –Consecutive updates without either sensor modality at which a track expires.
-
min_hits(int, default:1) –Observations needed for confirmation after initial warmup.
-
iou_threshold(float, default:0.3) –Minimum image IoU for second-stage recovery; 1 disables that stage.
-
asso_func(str, default:'iou') –Image association geometry; must be
iou. -
det_thresh(float, default:0.0) –Minimum confidence for 2D image detections.
-
det_thresh_3d(float, default:0.0) –Minimum confidence for 3D detections.
-
max_age_2d(int, default:3) –Missing-image age at which track confidence starts decaying.
-
fusion_iou_threshold(float, default:0.01) –Minimum image IoU for fusing projected 3D boxes with 2D detections of the same class.
-
first_matching_method(str, default:'dist_2d_full') –3D association using
dist_2d,dist_2d_dims,dist_2d_full, oriou_3d. The full distance includes center, dimension, and yaw disagreement. -
distance_threshold(float, default:3.5) –Positive maximum distance for distance-based first-stage matching.
-
iou_3d_threshold(float, default:0.01) –Minimum volumetric IoU when using
iou_3dmatching.
Bases: BaseTracker
Fuse independent 2D/3D detections and preserve identities across dropouts.
Call the inherited update with canonical Detections, an independent
detections_3d=Detections3D(...) batch, and camera=CameraModel(...).
An empty 3D batch explicitly represents a depth-detector dropout. A 3D
observation is needed to create a track; subsequent 2D observations can
sustain it without updating its 3D Kalman measurement.
The result contains current image observations in image_tracks and
current 3D estimates in spatial_tracks, sharing the same track IDs.
Optional segmentation masks are preserved on image tracks. The two output
collections have independent row counts and detection indices.
RGB pixels and ReID embeddings are unused. Camera projection is required;
camera-to-world ego poses are optional but must be provided consistently
throughout a sequence. Prediction advances once per update, using fixed
frame steps rather than capture timestamps.
Defaults reproduce the released KITTI car association/lifecycle profile.
Classes are always associated separately, including when per_class is
false. The scalar thresholds apply to every accepted detector class.
The five covariance scales multiply the 3D Kalman filter's source priors. Position includes xyz, yaw, and dimensions; velocity includes xyz derivatives and optional object yaw velocity. Prediction advances one frame per update, independent of capture timestamps and ego poses.
Parameters:
-
config(EagerMotConfig | None, default:None) –Immutable algorithm settings. None selects EagerMotConfig defaults.
-
kalman(KalmanConfig | None, default:None) –Immutable filter noise, timing, and supported behavior settings. None preserves tracker defaults.
-
**kwargs(Unpack[TrackerMetadataOptions], default:{}) –Runtime
per_class,class_ids, andclass_namessettings.