Standalone, self-contained MOT (Multi-Object Tracking) package for the D-Robotics RDK X5 board. Runs YOLO26 detection (6-class, int8 quantized) on the BPU and tracks with HybridSORT (default), ByteTrack, OCSort, OCLBoost, or BoT-SORT (StrongSORT is a stub). BoT-SORT, DeepOCSORT, and HybridSORT optionally fuse a committed MobileNetV4 ReID model. Input comes from a live MIPI camera (TROS
mipi_cam→ zero-copyHbmMsg1080P) or a local video file, with automatic file fallback when no camera is attached. Both Python and C++ implementations are provided and run out-of-the-box — the YOLO26s.binis committed undermodels/as the default, with YOLO26n and YOLO26l variants committed alongside (see Model variants).
- 🎯 BPU inference — YOLO26 6-class detection via TROS
hbm_runtime(Python) or the rawhbDNNC API (C++), NV12 input, ~19 ms/frame (YOLO26s). - 🔄 Multiple trackers — ByteTrack, OCSort, HybridSORT (default), OCLBoost (BoostTrack), BoT-SORT. HybridSORT, DeepOCSORT, and BoT-SORT optionally fuse a real MobileNetV4 ReID model (see ReID); the rest are pure-motion. StrongSORT is a documented stub. See Trackers.
- 📷 Live MIPI camera — TROS
mipi_camzero-copyHbmMsg1080Pshared memory; automatic fallback to a video file when no sensor is connected. - 🎞️ Local video — pass any
*.mp4/*.avipath to--source/--video. - 🐍 Python & C++ parity — same tracker library, two entry points.
- 🔧 reg-aware post-processing — auto-switches between YOLO26 LTRB decode
(
reg_max=1) and YOLO11 DFL16 decode (reg_max=16) so both model families work from one code path. - 📊 Three model scales — YOLO26s (committed default), YOLO26n (smallest),
YOLO26l (most accurate) share one deploy chain; swap by passing
--model. - 🆔 Optional ReID — a committed MobileNetV4 256-d ReID model fuses appearance into BoT-SORT / DeepOCSORT / HybridSORT (bicycle/motorcycle stay pure-motion). See ReID.
Six tracking algorithms are implemented as parallel pure-NumPy (Python) and
pure-C++ (Eigen) classes in the same library. Five are usable end-to-end
as pure-motion trackers (IoU/Kalman only, no ReID model); StrongSORT is a
stub that raises NotImplementedError because it fundamentally requires a
ReID embedding model, which is out of scope for this package.
Both the Python (mot_tros_demo.py) and C++ (mot_tros_demo) entry points
expose the same --tracker flag with the same names, so the two are
interchangeable. Default is hybridsort.
--tracker |
class | motion model | association strategy | notes |
|---|---|---|---|---|
bytetrack |
ByteTracker |
8-D Kalman (xywh + v) |
two-stage IoU (high/low-conf split) | fast, simple, strong default |
ocsort |
OcSortTracker |
7-D Kalman + velocity (OCF) |
observation-centric rescue + IoU | handles occlusion / short gaps; update() also takes the frame |
hybridsort (default) |
HybridSortTracker |
9-D Kalman (adds score & velocity) | three-stage: byte → OCR → TCM | pure-motion HybridSORT; best on this detector's lower-conf outputs |
occluboost |
OccluBoostTracker |
7-D Kalman + velocity | IoU + DLO boost (IoU/MHD/shape mix) | BoostTrack-style boosting of low-conf matches |
botsort |
BotSortTracker |
8-D Kalman (xywh + v) |
two-stage IoU + proximity fuse | BoT-SORT; optional ReID (min-fusion) via --reid-model |
strongsort |
StrongSortTracker |
— | — | stub — raise NotImplementedError; needs a ReID model |
The five usable trackers all have pure-motion paths (no appearance features, no camera-motion-compensation). BoT-SORT, DeepOCSORT, and HybridSORT additionally support optional ReID appearance fusion when a ReID model is attached (see ReID); without one they are byte-identical to their pure-motion form. The MOT17 HOTA column in the model table is each model's best tracker (e.g. YOLO26l + botsort = 60.84).
Three trackers — botsort, deepocsort, hybridsort — optionally fuse a
real MobileNetV4 Conv-Small ReID model (mnv4_reid_256x256_nv12.bin,
256-d float embedding, 256×256 NV12 input, 1.77 MB). When --reid-model is
set, update(dets, img) crops each high-confidence detection, runs it through
the BPU model, and fuses the L2-normalized embedding with the motion cost. Each
tracker uses its own fusion rule, mirroring boxmot 1:1:
- botsort — min-fusion:
cost = min(iou_cost, emb_cost), with an appearance gate (emb > appearance_thresh → 1) and an IoU-proximity gate (no overlap → ignore appearance). Track EMA α=0.9. - deepocsort — weighted injection:
final_cost = -(iou + angle + emb_sim), with an IoU gate before the adaptive-weight (computeAwMaxMetric) step. Track EMA is confidence-adaptive (α = af + (1-af)(1-trust)). - hybridsort — weighted injection into the 4-point VDC cost:
final = w0·(-(iou+angle)) + w1·emb_dist(emb is a cosine distance, opposite sign to DeepOCSORT). RequiresMOT_HYBRIDSORT_EG > 0(default 0 = ReID loaded but idle). Track EMA is confidence-weighted.
Class routing (v1): the model is a single joint person+vehicle model.
person(0), car(2), truck(4), bus(5) get real ReID features; bicycle(1) and
motorcycle(3) are routed to pure-motion (zero embedding → cosine distance
1.0 = appearance ignored → falls back to IoU). get_features(boxes, img, classes=None) returns a (N, 256) matrix row-aligned with the input boxes;
classes=None (backward-compat) extracts all.
# Python — pass the ReID model; appearance gate tightened to 0.5 (real ReID)
# (the loose 0.9 default was for the old placeholder classifier)
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 --tracker deepocsort \
--reid-model models/mnv4_reid_256x256_nv12.bin --reid-appearance-thresh 0.5
# HybridSORT additionally needs the appearance-term weight > 0
MOT_HYBRIDSORT_EG=0.5 python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 \
--tracker hybridsort --reid-model models/mnv4_reid_256x256_nv12.bin
# C++ (build with make -j2; 4+ cores stall the X5)
cd cpp && ./build.sh
./build/mot_tros_demo --model ../models/best_bpu_bayese_640x640_nv12.bin \
--video ../MOT17-02-DPM.mp4 --tracker botsort --reg 1 --classes-num 6 \
--reid-model ../models/mnv4_reid_256x256_nv12.bin --reid-appearance-thresh 0.5
# HybridSORT: MOT_HYBRIDSORT_EG=0.5 ./build/mot_tros_demo --tracker hybridsort ...The BPU
.binconsumes NV12 uint8 and applies mean/scale at compile time (data_mean_and_scale, ImageNet mean/std), so the ReID wrapper doesBGR→NV12(not boxmot's float-RGB /255+mean/std). The detector and ReID model coexist as independent hbDNN handles; inference is synchronous, so detect + reid run serially. The embedding output is float32 (backbone is int8 internally, output node stays float) — no dequant step needed; a guarded int8-dequant branch is present but stays a no-op. Input size (256×256) and embedding dim (256) are probed at runtime from the.bin, never hard-coded. Seemodels/README.mdfor the model card anddocs/REID_MNV4_BOARD_BENCH.mdfor the full board benchmark.
Benchmark models: detector = YOLO26s 640x640 NV12
(best_bpu_bayese_640x640_nv12.bin, 6 classes, reg_max=1); ReID =
MobileNetV4 Conv-Small 256x256 NV12 (mnv4_reid_256x256_nv12.bin,
256-d embedding).
ReID runs serially per crop (~3.5 ms/crop end-to-end, 1.08 ms BPU-only), so
it is the dominant cost when enabled. Measured with rendering disabled
(--no-render) so the numbers reflect detect+track+ReID, not draw overhead.
| path | tracker | ReID | avg FPS | ms/frame |
|---|---|---|---|---|
| Python | DeepOCSORT | OFF | 14.25 | 54.4 |
| Python | DeepOCSORT | ON (MNv4) | 7.71 | 112.7 |
| Python | HybridSORT | OFF | 11.46 | 70.7 |
| Python | HybridSORT | ON (MNv4) | 6.72 | 131.2 |
| C++ | DeepOCSORT | OFF | 23.30 | 25.4 |
| C++ | DeepOCSORT | ON (MNv4) | 11.88 | 65.0 |
| C++ | HybridSORT | OFF | 23.09 | 25.8 |
| C++ | HybridSORT | ON (MNv4) | 11.92 | 65.0 |
C++ is ~1.5× faster than Python with ReID ON (11.9 vs 7.7 FPS) — modest,
because per-crop ReID latency is BPU+NV12-bound and identical across languages
(3.48 ms C++ vs 3.49 ms Python). The real MNv4 model also beats the prior
placeholder (resnet18 logits) by 15–21% in Python FPS. Full decomposition
(per-crop BPU-only vs full pipeline, frame breakdown, stress test) in
docs/REID_MNV4_BOARD_BENCH.md.
The C++ summary now reports three per-frame medians: read/decode measures
source->read() (file I/O plus video decode), detect+track+ReID excludes
input and rendering, and read+detect+track+ReID is their per-frame sum. The
following controlled runs use --no-render, so they intentionally exclude
frame clone/drawing/display/video writing while including input read/decode.
| tracker | ReID | read/decode (ms) | detect+track+ReID (ms) | read+detect+track+ReID (ms) | avg FPS |
|---|---|---|---|---|---|
| HybridSORT | OFF | 17.04 | 25.61 | 42.70 | 23.15 |
| HybridSORT | ON (MNv4, MOT_HYBRIDSORT_EG=0.5) |
17.24 | 58.52 | 76.16 | 13.03 |
So, with image reading included, the measured median end-to-end compute path is
42.70 ms/frame without ReID and 76.16 ms/frame with ReID for this
1080p video. Wall-clock FPS is lower than 1000 / median because it also
includes occasional long-tail frame reads and loop/setup overhead. Reproduce
with:
# Pure motion
./cpp/build/mot_tros_demo --model models/best_bpu_bayese_640x640_nv12.bin \
--video MOT17-02-DPM.mp4 --tracker hybridsort --reg 1 --classes-num 6 \
--duration 20 --no-render
# ReID must have a non-zero HybridSORT appearance weight to execute.
MOT_HYBRIDSORT_EG=0.5 ./cpp/build/mot_tros_demo \
--model models/best_bpu_bayese_640x640_nv12.bin --video MOT17-02-DPM.mp4 \
--tracker hybridsort --reid-model models/mnv4_reid_256x256_nv12.bin \
--reg 1 --classes-num 6 --duration 20 --no-renderHOTA (%) on the two held-out MOT17 sequences, same detector (YOLO26s) and the same tracker configs — pure-motion vs the MNv4 joint ReID vs an OSNet-x0.25 ReID baseline. HybridSORT + MNv4 is the best combination (55.81, +5.58 over its OSNet variant and +11.26 over pure-motion), which is why HybridSORT is the default tracker. BoT-SORT and DeepOCSORT barely move — their motion model is already strong on these pedestrian sequences.
| tracker | 纯运动 (pure-motion) | MNv4-ReID | OSNet-ReID |
|---|---|---|---|
| BoTSORT | 53.53 | 53.60 | 53.60 |
| DeepOCSORT | 48.81 | 48.49 | 49.66 |
| HybridSORT | 44.55 | 55.81 | 50.23 |
Read-out: for HybridSORT the appearance term is the whole point — it lifts HOTA by ~11 points once a real ReID model is attached, and the lighter MNv4 (256-d, 1.77 MB) beats the heavier OSNet (512-d) by +5.58. For BoTSORT / DeepOCSORT the IoU/velocity motion model already captures most of the association signal on MOT17 pedestrians, so ReID adds little accuracy — pick them for speed, pick HybridSORT+ReID for accuracy.
⚠️ Benchmark caveat: always measure ReID overhead with rendering OFF. Withframe.clone()+drawTracks()+putText()enabled, rendering dominates (~40 ms/frame at 1080p) and masks the ReID cost — all configs converge to ~22 FPS regardless of ReID, which is misleading. And rememberMOT_HYBRIDSORT_EG(HybridSORT's appearance weight) defaults to 0, so HybridSORT's ReID is loaded but idle unless that env var is set.
# Python — now supports --tracker (mirrors the C++ flag)
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 --tracker botsort
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 --tracker ocsort --save out.mp4
# C++
cd cpp && ./build.sh
./mot_tros_demo --model ../models/best_bpu_bayese_640x640_nv12.bin \
--video ../MOT17-02-DPM.mp4 --tracker hybridsort \
--reg 1 --classes-num 6 --save out_cpp.mp4Common tuning flags (both entry points): --track-thresh (activation),
--match-thresh (association IoU/threshold), --track-buffer (max lost
frames). The Python demo also reads MOT_TRACK_THRESH / MOT_MATCH_THRESH
/ MOT_TRACK_BUFFER env vars; the C++ demo additionally takes
--min-conf and --det-freq.
The production model ships in models/:
| field | value |
|---|---|
| file | models/best_bpu_bayese_640x640_nv12.bin |
| architecture | YOLO26s, 6 classes (person bicycle car motorcycle truck bus) |
| reg_max | 1 (direct LTRB box decode, not DFL16) |
| input | 1×3×640×640, NV12 runtime / RGB train, data_scale 1/255 |
| outputs | 6 NHWC tensors (3 strides × cls/box) |
| quantization | int8, march=bayes-e, max-percentile 0.99995 calibration |
| size / MD5 | 10.75 MB / b438704015efbf46cb392ef9a69afa70 |
| board latency | ~19.4 ms / frame |
| merged-val mAP50 / mAP50-95 | 0.626 / 0.397 |
| MOT17 best HOTA (botsort) | 53.60 |
Trained on BDD100K + CrowdHuman + MOT17 (rare classes oversampled). Two
high-precision quantization variants (KL-divergence, per-layer mix) were
A/B-tested on the board and both measured worse than max-int8 (see
Quantization results and
docs/REPORT_NOTES.md), so max-int8 is the committed
model.
Three YOLO26 scales share one deploy chain (same 6 classes, reg_max=1, int8,
march=bayes-e, identical max-percentile calibration). All three bins are
committed under models/; YOLO26s is the default,
swap to n / l by passing --model.
| model | bin size | merged-val mAP50 / mAP50-95 | MOT17 best HOTA | board latency | committed |
|---|---|---|---|---|---|
| YOLO26s (default) | 10.75 MB | 0.626 / 0.397 | botsort 53.60 | 19.4 ms ✅ | ✅ |
| YOLO26n | 3.35 MB | 0.555 / 0.346 | hybridsort 50.49 | 10.0 ms ✅ | ✅ |
| YOLO26l | 26.74 MB | 0.688 / 0.443 | botsort 60.84 | 49.1 ms ✅ | ✅ |
Accuracy scales s→n→l as expected (l is +4.6 mAP50-95 over s, +7.2 MOT17 HOTA). Latency scales with bin size: n is ~2× faster than s, l is ~2.5× slower — all three measured on-board (100 held-out BDD imgs, min-of-7, std <0.5 ms, BPU inference only; full-pipeline e2e FPS on MOT17 video is n≈5.1 / s≈4.5 / l≈4.1). Correctness audited on-board across 3 dimensions (output-shape + box legality, class/score distribution, cross-scale IoU consistency) — all PASS, no degenerate boxes, no rare-class dropout. Swap any variant by passing
--model models/<bin>.bin--reg 1. Seedocs/BOARD_BENCH_NL.mdfor the full bench log.
Per-class mAP50 (merged val, 14370 imgs): person/car are the strongest across all scales; bicycle/motorcycle lag (small targets, fewer samples).
| class | YOLO26s | YOLO26n | YOLO26l |
|---|---|---|---|
| person | 0.816 | 0.767 | 0.862 |
| car | 0.796 | 0.751 | 0.829 |
| truck | 0.625 | 0.558 | 0.679 |
| bus | 0.617 | 0.546 | 0.672 |
| bicycle | 0.472 | 0.372 | 0.553 |
| motorcycle | 0.431 | 0.339 | 0.531 |
mot_tros/
├── mot_tros_demo.py # Python main: detect + multi-tracker (--tracker)
├── mot_detector.py # Detector wrapper (MOTDetector)
├── mot_tracker.py # Pure-NumPy trackers (HybridSort, ByteTrack, ...)
├── reid_extractor.py # BPU ReID wrapper (NV12 crops → 256-d L2-norm emb)
├── ultralytics_yolo_det.py # TROS hbm_runtime inference wrapper (reg-aware)
├── preprocess.py # BGR→NV12, letterbox
├── postprocess.py # DFL/LTRB decode, NMS, coord scaling
├── cam_capture.py # TROS mipi_cam ROS subscription (video fallback)
├── cam_fifo_bridge.py # Python→FIFO bridge that feeds the C++ binary
├── bench_*.py # MOT17 FPS / ReID latency / frame-breakdown benches
├── models/ # committed production .bin + model notes
├── tests/test_reid_fusion.py # ReID class-routing / pure-motion fallback tests
├── docs/REPORT_NOTES.md # report material: arch / deploy / quant experiments
├── docs/BOARD_BENCH_NL.md # n/s/l model board benchmark log
├── docs/REID_MNV4_BOARD_BENCH.md # MNv4 ReID board benchmark (speed + latency)
└── cpp/ # C++ implementation
├── CMakeLists.txt
├── mot_tros_demo.cc # main: FrameSource (file/V4L2/FIFO) + detect + track
├── yolo_detector.hpp # hbDNN-based YOLO detector (reg-aware, header-only)
├── reid_extractor.hpp # hbDNN-based ReID wrapper (header-only)
├── mot_tracker.{hpp,cpp} # 6 trackers (ByteTrack/OCSort/HybridSort/OCLBoost/BotSort + StrongSORT stub)
├── kalman_filter.{hpp,cpp}
├── build.sh # builds with -j2 (4+ cores stall the X5)
└── run_cpp.sh # run file or camera mode
- TROS humble:
source /opt/tros/humble/setup.bash - Python:
hbm_runtime,numpy,scipy,opencv-python(all preinstalled) - C++ build:
cmake,g++, OpenCV, Eigen3,/usr/include/dnn+libdnn - MIPI sensor: an sc132gs is connected to this board (1088×1280 bgr8).
The model is already in models/, so no download step is needed. For a test
video, drop one next to the scripts (e.g. symlink or copy MOT17-02-DPM.mp4),
or pass any local video file via --source / --video.
# Local video file (any mp4/avi on the board):
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4
# Live MIPI camera (with automatic file fallback):
python3 mot_tros_demo.py
# Override output / duration:
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 --save out.mp4 --duration 30Output: output_tros.mp4. Defaults target the bundled YOLO26s model
(6 classes, reg=1, score_thres=0.35). To run a YOLO11 model instead:
python3 mot_tros_demo.py --model <yolo11.bin> --reg 16 --classes-num 80Key flags: --reg (1=YOLO26 LTRB, 16=YOLO11 DFL), --classes-num,
--score-thres, --tracker, --duration.
cd cpp
./build.sh # cmake + make -j2
# Local video file, loop to ~60 s:
./mot_tros_demo --model ../models/best_bpu_bayese_640x640_nv12.bin \
--video ../MOT17-02-DPM.mp4 --tracker hybridsort \
--reg 1 --classes-num 6 --save output_tros_cpp.mp4
# Live MIPI camera (launches mipi_cam + bridge):
./run_cpp.sh cameraThe camera mode starts the TROS mipi_cam node (zero-copy HbmMsg1080P on
/hbmem_img), runs cam_fifo_bridge.py to decode frames and pipe them to the
C++ binary through a FIFO, and the C++ binary runs detection+tracking. This
keeps ROS out of the C++ build while still using the real MIPI sensor.
If no camera is connected, both Python and C++ fall back to MOT17-02-DPM.mp4
so the pipeline can always be validated. Defaults are YOLO26s (6 classes,
reg=1); pass --reg 16 --classes-num 80 for a YOLO11 model.
The C++ detector reg=1 path mirrors the Python
decode_ltrb_boxesmath but could not be compiled on x86 (nohbDNNheaders / aarch64 OpenCV). It is expected to build cleanly on the X5; if a tweak is needed it is localized to thecfg_.reg == 1branch incpp/yolo_detector.hpp.
Three int8 calibration strategies were A/B-tested on 100 held-out BDD images
(excluding the calibration set, rare classes oversampled), ab_compare.py,
min-of-3 latency. Lower is worse (fewer detections):
| calibration | detections @0.25 | detections @0.15 | latency |
|---|---|---|---|
| max-percentile (committed) | 1372 | 1789 | 19.4 ms |
| mix (per-layer cosine select) | 1234 (−10%) | 1615 (−10%) | 19.4 ms |
| kl (KL-divergence) | 1100 (−20%) | 1470 (−18%) | 19.4 ms |
Key findings (counter-intuitive): KL globally suppresses the classification
logit and drops rare classes (bicycle/motorcycle) entirely; mix repairs the
rare-class loss but still loses 10% of car/person. Mix's per-layer cosine
selection picked layers that were more cosine-similar to the float model on
all 6 outputs yet detected fewer objects — output cosine ≠ mAP. Full data
and methodology in docs/REPORT_NOTES.md.
- Python:
python3 mot_tros_demo.py --source MOT17-02-DPM.mp4 --reg 1 --classes-num 6 --save out.mp4→ person boxes + HybridSORT IDs render correctly (regression check: before the reg branch, DFL16 decode collapsed every box to zero). - C++:
cd cpp && ./build.sh && ./mot_tros_demo --model ../models/best_bpu_bayese_640x640_nv12.bin --video ../MOT17-02-DPM.mp4 --reg 1 --classes-num 6 --save out_cpp.mp4.
- The C++ tracker lib is identical to the
rdk_model_zooMOT sample; only the demo entry point was rewritten to support camera/FIFO sources and a target duration. - HybridSORT here ships with an optional ReID appearance branch (off by
default; enabled with
--reid-model+MOT_HYBRIDSORT_EG). With ReID off it is the pure-motion variant matching the other trackers in this repo. reg_max=1is why YOLO26 cannot reuse the YOLO11 DFL16 decode — seemodels/README.md.
Released under the Apache License 2.0. See LICENSE for details.