visiontrack-cpp 0.1.0__cp313-cp313-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: visiontrack-cpp
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A C++ ByteTrack that is bit-for-bit identical to its NumPy reference, and ~70x faster
|
|
5
|
+
Keywords: multi-object-tracking,bytetrack,kalman-filter,computer-vision,cpp,pybind11,numerical-reproducibility
|
|
6
|
+
Author: Rushikesh Hulage
|
|
7
|
+
License: MIT
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: MacOS
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Programming Language :: C++
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
22
|
+
Project-URL: Homepage, https://visiontrack.hulage.in
|
|
23
|
+
Project-URL: Repository, https://github.com/hulagerushikesh/visiontrack-cpp
|
|
24
|
+
Project-URL: Issues, https://github.com/hulagerushikesh/visiontrack-cpp/issues
|
|
25
|
+
Project-URL: Reference, https://github.com/hulagerushikesh/visiontrack
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: numpy>=1.24
|
|
28
|
+
Provides-Extra: test
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
30
|
+
Requires-Dist: visiontrack-mot==0.2.0; extra == "test"
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# visiontrack-cpp
|
|
34
|
+
|
|
35
|
+
An optimized C++ ByteTrack, **parity-gated** against the
|
|
36
|
+
[`visiontrack-mot`](https://pypi.org/project/visiontrack-mot/) NumPy reference.
|
|
37
|
+
|
|
38
|
+
This is a performance-engineering project, not a research one. It answers a
|
|
39
|
+
single question: **how fast can an honest ByteTrack go, and what exactly buys
|
|
40
|
+
the speed?** The research findings live in the sibling project,
|
|
41
|
+
[visiontrack.hulage.in](https://visiontrack.hulage.in).
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install visiontrack-cpp
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+

|
|
48
|
+
|
|
49
|
+
Both halves of that figure come from one script. `bench/compare.py` feeds both
|
|
50
|
+
trackers the same detections, compares the full output streams bit-for-bit, and
|
|
51
|
+
only then reports a timing — so the speedup is, by construction, a ratio between
|
|
52
|
+
two identical computations. It refuses to print a number it has not verified,
|
|
53
|
+
and has no flag to skip the check.
|
|
54
|
+
|
|
55
|
+
## The rule
|
|
56
|
+
|
|
57
|
+
> The NumPy implementation is the oracle. A faster tracker that changes the
|
|
58
|
+
> numbers is worthless.
|
|
59
|
+
|
|
60
|
+
Parity is proven before any optimization is attempted, so every later speedup is
|
|
61
|
+
demonstrably behaviour-preserving rather than hopefully so.
|
|
62
|
+
|
|
63
|
+
## Why a separate repo
|
|
64
|
+
|
|
65
|
+
VisionTrack's claim is a from-scratch tracker whose core is readable NumPy. A
|
|
66
|
+
C++ core inside that repo would destroy the claim — a reader could no longer
|
|
67
|
+
tell which implementation produced a published number. Keeping them apart also
|
|
68
|
+
keeps the compiler toolchain, CMake build and platform wheels out of a project
|
|
69
|
+
that needs none of them.
|
|
70
|
+
|
|
71
|
+
Because `visiontrack-mot` is on PyPI, this repo depends on the reference the way
|
|
72
|
+
any third party would: a pinned, versioned dependency. The dependency runs one
|
|
73
|
+
way and never back.
|
|
74
|
+
|
|
75
|
+
## Status
|
|
76
|
+
|
|
77
|
+
**All four phases resolved.** Phases 1, 2 and 4 are complete. **Phase 3 (GPU)
|
|
78
|
+
is closed by measurement rather than left blocked**: the only GPU-shaped work
|
|
79
|
+
here is gating's `N·M` independent 4×4 solves, and it does not pay. Apple GPUs
|
|
80
|
+
have no float64, so a bit-exact Metal path does not exist; and even granting a
|
|
81
|
+
float64 GPU, Amdahl caps the win at 1.82× on synthetic scenes while dispatch
|
|
82
|
+
overhead exceeds the *entire* gating computation on real ones — MOT17-09's
|
|
83
|
+
median frame spends 0.85 µs there. [PHASE3.md](PHASE3.md) has the arithmetic
|
|
84
|
+
and [`bench/gpu_feasibility.py`](bench/gpu_feasibility.py) reproduces it.
|
|
85
|
+
|
|
86
|
+
**Phase 1 — all six milestones done.** Both trackers produce
|
|
87
|
+
**identical `(frame, track_id, box, score)` streams** over the full 525 frames of
|
|
88
|
+
MOT17-09 on all three detector variants — 7,545 observations, zero divergence —
|
|
89
|
+
and identical HOTA/IDF1/CLEAR-MOT to the last bit. Geometry, the Kalman filter
|
|
90
|
+
and the solver are bit-identical underneath. Two inexactnesses remain, both
|
|
91
|
+
measured and bounded rather than assumed away. 200 tests passing in ~5 seconds.
|
|
92
|
+
|
|
93
|
+
Run the harness yourself: [`parity/run_parity.py`](parity/run_parity.py), latest
|
|
94
|
+
output in [`parity/REPORT.md`](parity/REPORT.md).
|
|
95
|
+
|
|
96
|
+
| # | milestone | gate | state |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| 1 | Scaffold: CMake + pybind11 + Eigen | `import visiontrack_cpp` succeeds | ✅ |
|
|
99
|
+
| 2 | Geometry (IoU/GIoU, box formats) | unit parity vs NumPy | ✅ |
|
|
100
|
+
| 3 | 8-state Kalman filter (Eigen) | unit parity incl. batched gating | ✅ |
|
|
101
|
+
| 4 | Hungarian solver | unit parity **and** identical tie-breaking | ✅ |
|
|
102
|
+
| 5 | Track FSM + `ByteTracker.update` | trajectory parity, one sequence | ✅ |
|
|
103
|
+
| 6 | Parity harness + report | trajectory parity, all MOT17-09 variants | ✅ |
|
|
104
|
+
|
|
105
|
+
### Parity is not a property of one machine
|
|
106
|
+
|
|
107
|
+
Bit-exactness is a claim about a compiled binary, so it has to be checked on
|
|
108
|
+
every toolchain that compiles one. CI builds twelve wheels and runs the suite
|
|
109
|
+
against each, and all twelve agree with the reference exactly:
|
|
110
|
+
|
|
111
|
+
| platform | compiler | NumPy's LAPACK | NumPy versions | result |
|
|
112
|
+
|---|---|---|---|---|
|
|
113
|
+
| macOS arm64 | clang | Accelerate | 2.2.6, 2.4.6, 2.5.3 | 190 passed, 0 failed |
|
|
114
|
+
| Linux x86_64 | gcc | OpenBLAS | 2.2.6, 2.4.6, 2.5.3 | 190 passed, 0 failed |
|
|
115
|
+
| Windows x86_64 | MSVC | OpenBLAS | 2.2.6, 2.4.6, 2.5.3 | 190 passed, 0 failed |
|
|
116
|
+
|
|
117
|
+
Three compilers, two architectures, and — the part worth pausing on — **two
|
|
118
|
+
different LAPACK implementations**. The Kalman gain is solved, not inverted, so
|
|
119
|
+
the reference calls `np.linalg.solve` and the C++ calls Eigen. Those agreeing
|
|
120
|
+
to the last bit against both Accelerate and OpenBLAS is stronger evidence than
|
|
121
|
+
any single platform could give: a solve that merely looked right would not
|
|
122
|
+
match two independent LAPACKs.
|
|
123
|
+
|
|
124
|
+
Every wheel also has to pass [`ci/check_oracle.py`](ci/check_oracle.py) before
|
|
125
|
+
its tests run. It refuses to compare against a NumPy that pip built from
|
|
126
|
+
source, because such a NumPy links whatever LAPACK the build host had and is
|
|
127
|
+
therefore not the reference. That check exists because its absence sent three
|
|
128
|
+
CI runs chasing a divergence that was never in the tracker — see
|
|
129
|
+
[PHASE4.md](PHASE4.md#the-oracle-has-to-be-the-reference-build).
|
|
130
|
+
|
|
131
|
+
Milestone 4 was the risky one — see *Tie-breaking* below for what it cost.
|
|
132
|
+
Milestone 5 is where the parity gate stopped being a claim and became a result.
|
|
133
|
+
|
|
134
|
+
## The harness
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
python parity/run_parity.py # the report, to stdout
|
|
138
|
+
python parity/run_parity.py --with-tests --scaling # + unit parity + throughput sweep
|
|
139
|
+
python parity/run_parity.py --markdown parity/REPORT.md --json parity/report.json
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Exit status is 0 only if every variant reaches trajectory parity, so it drops
|
|
143
|
+
into CI unchanged. It needs the MOT17 cache from the sibling `visiontrack`
|
|
144
|
+
checkout; point `VISIONTRACK_MOT17_CACHE` elsewhere if yours lives somewhere
|
|
145
|
+
else, and it skips cleanly when the cache is absent.
|
|
146
|
+
|
|
147
|
+
The harness is itself tested — including that it *detects* an injected
|
|
148
|
+
one-ULP divergence. A gate that cannot fail proves nothing.
|
|
149
|
+
|
|
150
|
+
## Install
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
pip install visiontrack-cpp
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Wheels are built for CPython 3.10-3.13 on macOS 11+ arm64, Linux x86_64
|
|
157
|
+
(manylinux_2_28, so glibc ≥ 2.28) and Windows x86_64. Anywhere else — Linux
|
|
158
|
+
aarch64, an older glibc, a newer Python — pip falls back to the sdist, which
|
|
159
|
+
needs a C++17 compiler and CMake ≥ 3.20; Eigen it will fetch itself if the
|
|
160
|
+
machine has none.
|
|
161
|
+
|
|
162
|
+
The Linux floor is glibc 2.28 rather than the older `manylinux2014` because
|
|
163
|
+
NumPy ≥ 2.4 requires glibc 2.27 and publishes no `manylinux2014` wheel. On an
|
|
164
|
+
older host pip would compile NumPy from source, and a source-built NumPy is
|
|
165
|
+
not the reference the parity claim is made against.
|
|
166
|
+
|
|
167
|
+
### Building from source
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
brew install eigen # optional; see below
|
|
171
|
+
pip install -e ".[test]"
|
|
172
|
+
pytest
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Eigen resolution has three steps, in order: an installed Eigen found by CMake
|
|
176
|
+
config, then the usual system include paths, then a **pinned** download of
|
|
177
|
+
Eigen 3.4.0. Two switches control it:
|
|
178
|
+
|
|
179
|
+
| flag | effect |
|
|
180
|
+
|---|---|
|
|
181
|
+
| `-DVT_SYSTEM_EIGEN=OFF` | ignore the installed Eigen and use the pinned one |
|
|
182
|
+
| `-DVT_FETCH_EIGEN=OFF` | never download; fail if no Eigen is installed |
|
|
183
|
+
|
|
184
|
+
The download is pinned rather than floating because Eigen picks different
|
|
185
|
+
kernels between versions, and this package's claim is bit-for-bit agreement
|
|
186
|
+
with NumPy. A floating dependency would make a wheel's numerics depend on the
|
|
187
|
+
day it was built. `VT_SYSTEM_EIGEN=OFF` is what a release wheel wants: it makes
|
|
188
|
+
the binary's arithmetic a property of this repo rather than of whatever the
|
|
189
|
+
build host had in `/opt/homebrew`.
|
|
190
|
+
|
|
191
|
+
Parity has been confirmed under **both** Eigen 3.4.0 and 3.5.0 — full suite and
|
|
192
|
+
the trajectory gate, same 7,545 observations, zero divergence either way. That
|
|
193
|
+
is two data points, not a guarantee for every Eigen version, which is why the
|
|
194
|
+
fetch is pinned.
|
|
195
|
+
|
|
196
|
+
### Checking a build is sound
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
>>> import visiontrack_cpp as vt
|
|
200
|
+
>>> vt.build_info()
|
|
201
|
+
{'module_version': '0.1.0', 'eigen_version': '3.5.0',
|
|
202
|
+
'pybind11_version': '3.1', 'cxx_standard': 201703, 'fp_contract_off': True}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
`fp_contract_off` is the one to read. It is measured against the binary that
|
|
206
|
+
was actually built — not read off a macro — and `False` means the compiler
|
|
207
|
+
fused a multiply-add somewhere and this build cannot match NumPy. See
|
|
208
|
+
*Load-bearing build flags* below for why that is fatal rather than cosmetic.
|
|
209
|
+
|
|
210
|
+
## The parity gate
|
|
211
|
+
|
|
212
|
+
Metric-level agreement is **not** the gate. Association is discrete: a 1e-16
|
|
213
|
+
difference in one cost entry can flip an assignment, change a track ID, and
|
|
214
|
+
cascade through every later frame — while MOTA barely moves. Metric parity would
|
|
215
|
+
pass a visibly wrong tracker.
|
|
216
|
+
|
|
217
|
+
The gate is trajectory-level, in three tiers:
|
|
218
|
+
|
|
219
|
+
1. **Unit parity** — each ported function vs its NumPy original on randomized
|
|
220
|
+
and degenerate inputs.
|
|
221
|
+
2. **Trajectory parity** *(the real gate)* — identical `(frame, track_id, box)`
|
|
222
|
+
streams on MOT17-09, across all three cached detector variants.
|
|
223
|
+
3. **Metric parity** — a backstop, scored by the reference's own `eval/` so both
|
|
224
|
+
trackers are measured by the same code.
|
|
225
|
+
|
|
226
|
+
### Load-bearing build flags
|
|
227
|
+
|
|
228
|
+
`CMakeLists.txt` sets **`-ffp-contract=off`**. Do not remove it. clang defaults
|
|
229
|
+
to `-ffp-contract=fast` at `-O2`/`-O3`, which fuses `a + b*c` into a single FMA
|
|
230
|
+
instruction that rounds **once** where NumPy rounds twice. That is a 1-ULP
|
|
231
|
+
difference on every height-scaled term of the Kalman process noise. It is
|
|
232
|
+
invisible in a single call, compounds over a trajectory, and is exactly the kind
|
|
233
|
+
of drift that flips a near-tied association thousands of frames later.
|
|
234
|
+
|
|
235
|
+
The FMA result is arguably *more accurate*. It is still wrong here: correctness
|
|
236
|
+
in this repo means "identical to the oracle", not "closer to the real number".
|
|
237
|
+
|
|
238
|
+
### The one place parity is not exact
|
|
239
|
+
|
|
240
|
+
`gating_distance` Cholesky-factorizes the innovation covariance, then calls
|
|
241
|
+
`np.linalg.solve` on that factor. Measuring rather than assuming produced two
|
|
242
|
+
findings:
|
|
243
|
+
|
|
244
|
+
- NumPy's `cholesky` is bit-identical to the textbook Cholesky–Banachiewicz
|
|
245
|
+
ordering, so the port uses that instead of Eigen's `LLT`, which rounds
|
|
246
|
+
differently.
|
|
247
|
+
- NumPy's `solve` reaches Accelerate's LAPACK, and its output matches **neither**
|
|
248
|
+
a textbook LU with partial pivoting **nor** the reciprocal-scaling variant that
|
|
249
|
+
LAPACK's own reference `dgetf2` describes. Matching it exactly would mean
|
|
250
|
+
reimplementing a specific vendor kernel.
|
|
251
|
+
|
|
252
|
+
So gating carries a few-ULP residual, asserted at `rtol=1e-14`. The tests also
|
|
253
|
+
assert that no track/measurement pair disagrees about passing the chi-square
|
|
254
|
+
gate. Whether the residual ever changes a real association is a question only
|
|
255
|
+
milestone 5's trajectory parity can answer.
|
|
256
|
+
|
|
257
|
+
### Trajectory parity — the result
|
|
258
|
+
|
|
259
|
+
The gate the whole project is built around now passes. Over MOT17-09's full 525
|
|
260
|
+
frames, driven by the same cached public detections the reference's own
|
|
261
|
+
evaluator uses:
|
|
262
|
+
|
|
263
|
+
| variant | observations | divergences |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| MOT17-09-DPM | 1,162 | 0 |
|
|
266
|
+
| MOT17-09-FRCNN | 2,922 | 0 |
|
|
267
|
+
| MOT17-09-SDP | 3,461 | 0 |
|
|
268
|
+
|
|
269
|
+
Identical means identical: same track IDs, same boxes bit-for-bit, same scores,
|
|
270
|
+
same frames. Not "within tolerance".
|
|
271
|
+
|
|
272
|
+
That matters more than the unit tests it rests on, because a tracker is a
|
|
273
|
+
feedback loop — its output at frame N is part of its input at frame N+1. One
|
|
274
|
+
flipped association does not stay one flipped association; it renames a track
|
|
275
|
+
and every later frame inherits the rename. Unit parity cannot see that.
|
|
276
|
+
|
|
277
|
+
**What this did not prove.** Milestone 3 left `gating_distance` carrying a
|
|
278
|
+
few-ULP residual, and stages 1 and 3 compare that value against the chi-square
|
|
279
|
+
threshold — so in principle a pair sitting on the threshold could be gated
|
|
280
|
+
differently by the two implementations. Trajectory parity passing does not mean
|
|
281
|
+
the residual is harmless; it means the residual did not fire on these frames. So
|
|
282
|
+
it was measured instead:
|
|
283
|
+
|
|
284
|
+
| over MOT17-09 | |
|
|
285
|
+
|---|---|
|
|
286
|
+
| gated pairs evaluated | 57,905 |
|
|
287
|
+
| max \|reference − port\| residual | 1.09e-11 |
|
|
288
|
+
| closest any pair came to the gate | 7.56e-04 |
|
|
289
|
+
| margin ÷ residual, at the worst pair | 2.8e+11 |
|
|
290
|
+
| gate decisions that disagreed | 0 |
|
|
291
|
+
|
|
292
|
+
Eleven orders of magnitude of headroom on this data. That is an empirical
|
|
293
|
+
statement about MOT17-09, not a proof — a different sequence could sit closer.
|
|
294
|
+
The test asserts a margin of 1e6 residuals rather than merely "no
|
|
295
|
+
disagreement", so it fails while there is still room to investigate rather than
|
|
296
|
+
after a silent flip.
|
|
297
|
+
|
|
298
|
+
Because real data never lands on a gate boundary, no trajectory test can reach
|
|
299
|
+
that case. The boundary tests construct it directly, one ULP either side of
|
|
300
|
+
both gates.
|
|
301
|
+
|
|
302
|
+
### The other place parity is not exact
|
|
303
|
+
|
|
304
|
+
`appearance_distance` ends in `tf @ df.T`, which NumPy routes to Accelerate's
|
|
305
|
+
BLAS. It is **not** bit-exact — about 1–2 ULP, at every embedding dimension
|
|
306
|
+
tested including 4.
|
|
307
|
+
|
|
308
|
+
This is the exact mirror of the Kalman finding, and the pair is worth stating
|
|
309
|
+
together:
|
|
310
|
+
|
|
311
|
+
- In the Kalman filter, **clang** fused `a + b*c` into an FMA and NumPy did not.
|
|
312
|
+
`-ffp-contract=off` fixed it.
|
|
313
|
+
- In appearance, **Accelerate** is the one fusing, and the port cannot follow.
|
|
314
|
+
|
|
315
|
+
Three candidate explanations were measured against `np.dot` at dimension 4 and
|
|
316
|
+
all three were rejected: a naive left-to-right dot product, a pairwise one, and
|
|
317
|
+
even an exact-then-round-once chain (a perfect FMA). It is Accelerate's own
|
|
318
|
+
blocked kernel — the same dead end as the gating solve.
|
|
319
|
+
|
|
320
|
+
It is tolerated because `w_app` is `0.0` in every Phase 1 path, so the term
|
|
321
|
+
never reaches the solver, and a test pins that default.
|
|
322
|
+
|
|
323
|
+
### Phase 1 scope guards
|
|
324
|
+
|
|
325
|
+
`TrackerConfig` accepts `use_gmc`, `use_oru` and `w_ocm` — and **refuses to
|
|
326
|
+
build if any of them is enabled**. Those are research extensions the plan leaves
|
|
327
|
+
in Python. Accepting them and quietly ignoring them would produce a tracker that
|
|
328
|
+
is parity-clean on the default path and silently wrong the moment someone flips
|
|
329
|
+
a switch, which is the worst available failure mode for a project whose entire
|
|
330
|
+
claim is parity.
|
|
331
|
+
|
|
332
|
+
### Tie-breaking
|
|
333
|
+
|
|
334
|
+
When two assignments have equal cost, the *implementation* picks the winner, not
|
|
335
|
+
the mathematics. Both solvers minimize the same sum and may legitimately return
|
|
336
|
+
different optima. Four behaviours turned out to be load-bearing:
|
|
337
|
+
|
|
338
|
+
- **The transpose** in `linear_assignment` when `rows > cols`. `>` not `>=`: a
|
|
339
|
+
square matrix is *not* transposed, and transposing changes the scan order.
|
|
340
|
+
- **Both tie-break comparisons** are strictly-less, so the lowest column index
|
|
341
|
+
wins. Relaxing either to `<=` still returns an optimal assignment, and a
|
|
342
|
+
different one.
|
|
343
|
+
- **The slack expression order**: `cost - u - v`, two left-to-right
|
|
344
|
+
subtractions, not `cost - (u + v)`.
|
|
345
|
+
|
|
346
|
+
The last one is the interesting one, because it nearly escaped. Regrouping the
|
|
347
|
+
slack changed the *values* in 82 of 2400 random matrices but never changed the
|
|
348
|
+
answer — it looked cosmetic. A targeted search over mixed magnitudes with
|
|
349
|
+
ULP-level ties found that it does flip the assignment, roughly 5 times in 6000.
|
|
350
|
+
Those five witnesses are pinned as a regression test.
|
|
351
|
+
|
|
352
|
+
Every one of these was verified by **mutation**: each behaviour was deliberately
|
|
353
|
+
broken and the suite re-run. All four fail loudly (13–16 tests each). A fifth
|
|
354
|
+
candidate — splitting the fused `minv`/`delta` scan into two passes — leaves the
|
|
355
|
+
suite green, correctly: each `minv[j]` is already final when compared, so the
|
|
356
|
+
fused scan yields the same minimum with the same tie-break.
|
|
357
|
+
|
|
358
|
+
Random float costs almost never tie, so a suite built on them would have passed
|
|
359
|
+
all five mutants. The tests are weighted towards degenerate input instead: an
|
|
360
|
+
exhaustive sweep of all 512 3×3 binary matrices, all 729 ternary 2×3 and 3×2
|
|
361
|
+
matrices, constant and circulant matrices, and saturated `1 - IoU` costs where
|
|
362
|
+
most box pairs do not overlap and are therefore tied at exactly 1.0 — which is
|
|
363
|
+
what the tracker actually feeds the solver.
|
|
364
|
+
|
|
365
|
+
When trajectory parity fails, the fix is a frame-indexed diff of the first
|
|
366
|
+
divergent assignment — **never** a loosened tolerance.
|
|
367
|
+
|
|
368
|
+
## Throughput — where the plan was wrong
|
|
369
|
+
|
|
370
|
+
Phase 1 was supposed to produce no speedup. The plan said so explicitly, and
|
|
371
|
+
gave a reason: a naive C++ port of vectorized NumPy is often *slower*, because
|
|
372
|
+
"NumPy's inner loops are already compiled BLAS-adjacent code".
|
|
373
|
+
|
|
374
|
+
Measured, that prediction is wrong by a wide margin — **55–74× on real
|
|
375
|
+
sequences, 71–91× across synthetic scenes from 5 to 400 simultaneous objects**
|
|
376
|
+
([`bench/BENCHMARK.md`](bench/BENCHMARK.md)).
|
|
377
|
+
|
|
378
|
+
> **Why this differs from the parity report.** `parity/REPORT.md` puts the real
|
|
379
|
+
> sequences at 35–55×, and it is measuring something subtly different: it runs
|
|
380
|
+
> both trackers in the *same* loop, one frame at a time, un-warmed and once.
|
|
381
|
+
> Sharing a loop with NumPy costs the C++ tracker 21% (9.55 → 11.59 µs on
|
|
382
|
+
> MOT17-09-SDP, measured directly) because NumPy's allocations evict its
|
|
383
|
+
> working set between calls — while costing NumPy itself 0.3%, since 2 µs of
|
|
384
|
+
> interference is nothing against a 700 µs frame. The bias is asymmetric and it
|
|
385
|
+
> lands entirely on the smaller number. The harness's job is the parity gate,
|
|
386
|
+
> where interleaving is exactly right — the two trackers must see identical
|
|
387
|
+
> state at identical times. For throughput, [`bench/compare.py`](bench/compare.py)
|
|
388
|
+
> is the measurement of record: each tracker timed alone, warmed, min-of-9.
|
|
389
|
+
|
|
390
|
+
The reason matters more than the number, and two tempting explanations are both
|
|
391
|
+
wrong:
|
|
392
|
+
|
|
393
|
+
- **Not `Detection` construction.** It looked like the NumPy timing might be
|
|
394
|
+
inflated by object construction the array-based C++ entry point skips.
|
|
395
|
+
Measured separately: at most 3% of a frame.
|
|
396
|
+
- **Not NumPy per-call overhead on small matrices.** Plausible — MOT17-09's
|
|
397
|
+
median cost matrix is 9×7 — but it predicts the advantage collapsing as the
|
|
398
|
+
problem grows. It does not: the ratio stays flat out to 400×400.
|
|
399
|
+
|
|
400
|
+
Profiling the reference gives the real answer, in two parts:
|
|
401
|
+
|
|
402
|
+
- **`_kuhn_munkres` is not vectorized NumPy at all.** It is an interpreted
|
|
403
|
+
O(n³) triple loop, and the largest single entry in the profile. The plan's
|
|
404
|
+
premise is simply false for that function.
|
|
405
|
+
- **The per-track calls operate on 4- and 8-element arrays.** `xyah_to_xyxy`
|
|
406
|
+
runs ~8,850 times per 60 frames, `kalman.update` ~2,950, each on one track. At
|
|
407
|
+
that size NumPy's per-call machinery dwarfs the arithmetic it performs.
|
|
408
|
+
|
|
409
|
+
So the hot path was never the vectorized numerics the plan had in mind — it was
|
|
410
|
+
Python-level work on tiny arrays, and that is what the port removed.
|
|
411
|
+
|
|
412
|
+
This *revises* Phase 2 rather than confirming it. The plan wanted its headline to
|
|
413
|
+
be "LAPJV replaced the O(n³) Hungarian and the numbers did not move". Much of
|
|
414
|
+
that cost turns out to have been interpreter overhead the port has already
|
|
415
|
+
eliminated, so the remaining algorithmic win should be expected to be **smaller**
|
|
416
|
+
than the plan assumed. It now has a fair baseline to be measured against, which
|
|
417
|
+
is what Phase 1 was for.
|
|
418
|
+
|
|
419
|
+
## Honest expectations, and how they turned out
|
|
420
|
+
|
|
421
|
+
Phase 1 was planned on the expectation of **no speedup** — that a first-draft
|
|
422
|
+
C++ port might even be *slower* than vectorized NumPy, whose inner loops are
|
|
423
|
+
already compiled. The deliverable was meant to be the harness alone.
|
|
424
|
+
|
|
425
|
+
The harness was delivered and the prediction was refuted: 55–74× on real
|
|
426
|
+
sequences, for the reasons in *Throughput* above. The speedup is a real
|
|
427
|
+
measurement, but it is not the achievement — it is a baseline, obtained from a
|
|
428
|
+
deliberately unoptimized port with no SIMD, no LAPJV and no memory-layout work.
|
|
429
|
+
|
|
430
|
+
What Phase 1 actually produced is the thing that makes any later number
|
|
431
|
+
believable: a gate that fails on a single flipped association. Without it, "50×
|
|
432
|
+
faster" would be an unfalsifiable claim about a tracker nobody had checked.
|
|
433
|
+
|
|
434
|
+
## New to C++ / CMake / pybind11?
|
|
435
|
+
|
|
436
|
+
[`learning/LEARNING.md`](learning/LEARNING.md) explains this project from zero — what a compiler
|
|
437
|
+
does, why Python is slow but NumPy isn't, what each tool in the build is for,
|
|
438
|
+
and why parity is checked on full output streams rather than accuracy scores.
|
|
439
|
+
No prior C++ assumed.
|
|
440
|
+
|
|
441
|
+
## Licence
|
|
442
|
+
|
|
443
|
+
MIT.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
visiontrack_cpp.cp313-win_amd64.pyd,sha256=5Mp__Gpylc5yducbjOqv6RWtj_B2TEIRp6fmAjMG6ac,399360
|
|
2
|
+
visiontrack_cpp-0.1.0.dist-info/METADATA,sha256=RYPsl8Q6SZy5dXmcfFqtVU7E4nwIeg07Dn0G3MaVmmY,21385
|
|
3
|
+
visiontrack_cpp-0.1.0.dist-info/WHEEL,sha256=dXz53UX_wVrENU3pg7IjBHl1TfATF0jKSXtQDtrFnLw,105
|
|
4
|
+
visiontrack_cpp-0.1.0.dist-info/licenses/LICENSE,sha256=xOECO3LzLFWQqNk5v34PSFAu1hOKZP-h9lLspaVJI8Y,1094
|
|
5
|
+
visiontrack_cpp-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rushikesh Hulage
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
Binary file
|