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
+ ![Throughput vs simultaneous objects: the C++ port runs 71-91x faster than the NumPy reference while producing identical trajectories](bench/speedup.svg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: scikit-build-core 1.0.3
3
+ Root-Is-Purelib: false
4
+ Tag: cp313-cp313-win_amd64
5
+
@@ -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