Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cibuildwheel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ jobs:
# regression tests unsuited to wheel smoke.
CIBW_TEST_COMMAND: >
python {project}/scripts/ci_test_setup.py {project} &&
python -c "import openptv2.algorithms.track_kernels_tracking, openptv2.algorithms.track3d; print('openmp import OK')" &&
python -c "import openptv2.algorithms.track_kernels_corr, openptv2.algorithms.track3d; print('openmp import OK')" &&
pytest
tests/unit/test_vec_utils.py
tests/unit/test_correspondences.py
Expand Down
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Welcome to the openptv2 documentation.
### User Documentation

- [Tracking Pipeline & Results Guide](tracking_guide.md) - Pipeline workflow, parameter guide, multi-pass tracking, and ptv_is.# output format
- [Particle Trackers](trackers.md) - Which tracker to use and why: basics, all eight engines, caveats, tips, upstream credits
- [Two-Phase Tracking](two-phase-tracking.md) - Two-Phase usage and parameters in depth
- [Lid-Driven Cavity Flow Tutorial](tutorials/cavity_flow_tutorial.md) - End-to-end 3D-PTV case study: Autocalibration, Tracer Shaking, Warmup, and 3D Trajectories
- [Aortic Pulsatile Flow Tutorial](aorta_tutorial.md) - Cloud-native 3D-PTV on complex aortic flow
- [Auto-Calibration with `openptv warmup`](tutorials/warmup_tutorial.md) - Standalone parameter/engine auto-tuning before tracking
Expand Down
89 changes: 89 additions & 0 deletions docs/plans/2026-09-20-tracking-kernels-dedup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Tracking-kernels dedup (ponytail-audit items 1-9)

Branch: `refactor/dedup-tracking-kernels` (off `main`). Committed and pushed (0e1200a0, 1b83cfa6); no PR yet.
Goal: remove duplicated / orphaned code in `src/openptv2/algorithms/track_kernels_*.py`
without changing behavior.

## What was found

The same Cython `cdef`/`nogil` kernels were copy-pasted across sibling modules
(cross-module C calls need a `.pxd`, so people copied instead). Bodies were
identical or differed only in decorators/comments. `track_kernels_pixel.pxd` and
`track_kernels_position.pxd` already existed as the sharing mechanism, so I
extended them rather than inventing anything.

## Done

Items 1-5, dedup. Owners:
- `track_kernels_pixel.py`: `_multimed_r_nlay_1layer` (added `exceptval(check=False)` + pxd entry),
`_point_to_pixel_out`, `_candsearch_in_pix_rest_nogil`, `_pixel_to_metric_out`,
`_dist_to_flat_out`, `_sorted_candidates_fast_out_nogil`, `candsearch_in_pix_fast_nogil`
(the pixel copy is the newer one with `max_cands`/`out_dists`; search's was a stale, dead copy).
- `track_kernels_position.py`: `_ray_tracing_out` (added exceptval + pxd), `_angle_acc_out`
(moved here from geom/corr, added exceptval + pxd), `_point_position_out`,
`assess_new_position_fast_nogil`.
- Copies deleted from geom, search, transform, corr. Importers use the existing
`if cython.compiled: cimport ... else: from .x import ...` pattern.
Import direction is acyclic: pixel <- position <- geom/transform/corr/batch.

Item 6: deleted the `track_kernels_tracking.py` shim. Repointed `track_kernels.py`,
`setup.py` (`ALGORITHMS_MODULES`), `.github/workflows/cibuildwheel.yml` (import smoke test),
and `tests/unit/test_track_kernels_tracking_coverage.py` (now imports from owners; shim
constants defined locally; `_mod` = the pixel module, which keeps the old inert-patch behavior).

Item 9: deleted 16 wrappers with no non-test callers, plus their tests and the two
re-exports in `track_kernels.py`:
`angle_acc_fast`, `_ray_tracing_fast`, `pixel_to_metric_fast`, `dist_to_flat_fast`,
`metric_to_pixel_fast`, `_metric_to_pixel_out`, `_flat_image_coord_fast`, `_img_coord_fast`,
`img_coord_batch_fast`, `flat_image_coord_batch_fast`, `point_position_fast`,
`ray_tracing_batch_fast`, `point_position_batch_fast`, `pixel_to_metric_batch_fast`,
`metric_to_pixel_batch_fast`, `sort_candidates_by_freq_fast`.
Also dropped two search test classes that only tested the deleted dead copies.

Net so far: roughly -2,200 lines from dedup, about -840 from item 9, plus tests.

## Verification status (updated 2026-09-21)

Clean Cython rebuild of the final tree: OK.
- Hot-path tests (`test_track`, `test_track3d`, `test_correspondences`, `test_track4be`): 49 passed = baseline.
- Full suite `uv run --no-sync pytest tests`: 2017 passed, 86 skipped, 39 deselected (12 min).
- Pure-Python fallback, kernel coverage files only
(`test_track_kernels_*_coverage.py`, 6 files): 266 passed.
- Pure-Python fallback over the whole `tests/unit/test_*_coverage.py` glob: 1227 passed, 24 failed
(38 min; the glob now matches 29 files, not the 16 CLAUDE.md mentions, so it is slow).
The failures I inspected (`test_epi_coverage`, `test_correspondences_coverage`, 14 of the 24)
are all `Coord2d.__init__() got an unexpected keyword argument 'pnr'` /
`Candidate.__init__() ... 'pnr'`: interpreted-mode constructor mismatch in modules this
branch does not touch. Not confirmed on `main`; the other 10 were not inspected.
- Running the suite rewrites tracked `test_data/test_cavity/img/*_targets`; `git checkout -- test_data` before committing.

## Remaining

1. Optional: confirm the 24 fallback failures also occur on `main` (build `main`, run
`tests/unit/test_epi_coverage.py tests/unit/test_correspondences_coverage.py` interpreted).
2. Optional perf sanity: `_angle_acc_out` was `ccall inline` inside corr and is now a cross-module
C call (lost inlining). Time a tracking run before/after; if it regressed, keep a private copy in corr.
3. Cosmetic: stray banner comments in `test_track_kernels_batch_coverage.py` (about lines 84-102) and
`test_track_kernels_transform_coverage.py` (lines 22, 352).
4. Open a PR.

## Deliberately skipped

- **Item 7** (merge forward/backward tracking loops in `track_kernels_corr.py`): not a clean
dedup. Normalised diff of `trackcorr_loop_fast` vs `trackback_loop_fast` shows about 670 of about 900
lines differ; forward uses the `_trackcorr_particle_fast` worker, backward is inline. Merging
changes core tracking logic and there is no golden-output regression data. Only attempt
with a recorded before/after trajectory comparison on a real dataset.
- **Item 8** (delete 4BE tracker: `track4be_loop_fast`, `track4be.py`, `plugins/four_be_tracking.py`):
it is registered in `tracking_registry.py` and benchmarked in about 10 scripts/notebooks
(`bench_*`, `benchmark_*`, `tracker_tutorial_dashboard.py`). Needs an explicit product decision.
Do not delete without Alex saying so.

## Gotchas

- Cimported names are not importable from Python: tests and non-cimporting modules must import
the `cpdef` names from the owner module (pixel/position), not from geom/transform.
- Cython pure mode + `.pxd`: `noexcept nogil` in the pxd requires `@cython.exceptval(check=False)`
on the `def`, or the signatures mismatch.
- Shell cwd drift: `cd` to the repo root at the start of every command.
- Use `git grep`, not `grep -r`, at the repo root (huge build dirs; a plain grep timed out).
156 changes: 156 additions & 0 deletions docs/trackers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
# Particle Trackers in openptv2

Start here if you want to know which tracker to use and why. For measured
numbers on a reference dataset, see [Tracker Tutorials](tracker-tutorials.md);
for pipeline and file formats, see the [Tracking Guide](tracking_guide.md);
for Two-Phase usage in depth, see [Two-Phase Tracking](two-phase-tracking.md).

## 1. The basics: what tracking is

A camera takes a picture every frame. In each picture, every particle is a
dot. In the next picture the dots have moved a little. **Tracking means
deciding which dot in picture 2 is the same particle as each dot in
picture 1.**

When particles are far apart and slow, this is easy: take the nearest dot.
It gets hard when particles cross each other, when one is hidden for a
frame, or when positions are noisy. Every tracker below is a different set
of rules for that decision. They differ in only four things:

1. **Guess** — where do you expect the particle next? (stay put, constant
velocity, smoothed curve, look at future frames, bigger time step)
2. **Score** — how do you rank candidates? (distance, acceleration, angle,
camera-image agreement)
3. **Conflicts** — two particles want the same dot: who wins? (first come,
cheapest first, best total pairing, nobody)
4. **Evidence** — what do you look at? Only 3D points, or also the original
camera images? Trackers that check the images survive bad 3D points;
trackers that ignore them are faster.

Two facts shape everything on this page. First, measured on synthetic flow
(see `docs/tracking-benchmark-results.md`), most wrong links come from
**missing detections across gaps (~60%)** and **cold starts (~20%)** — not
from the scoring rule. So gap handling matters more than clever costs.
Second, particles only ever **enter and exit** the volume; a track that ends
mid-volume is almost always an occlusion or a dropout, not a real exit.

## 2. All trackers at a glance

Select with `plugins.selected_tracking` in your parameters (GUI: Plugins
page). The `name` column is the exact preset string.

| preset (`name`) | idea in one line | looks at |
|---|---|---|
| `default`, `standard_forward`, `full_multipass`, `two_directional` (trackcorr) | Guess forward, confirm in every camera, accept smooth links | 2D targets + 3D |
| `priority_segment_3d` (Fast 3D / 3MA) | Cheapest (smoothest) links first, globally | 3D only |
| `4be` | Peek at frame n+2 before accepting; conflicts link to nobody | 3D only |
| `nearest_hungarian_3d` (MyPTV 3D) | Best total pairing per frame pair (Hungarian), survives gaps | 3D only |
| `myptv_2d_tracking` (MyPTV 2D) | Each camera tracks its own images, then cameras vote | 2D per camera |
| `predictive_gmm_3d` (proPTV) | Fit a smooth curve through history, predict from it | 3D only |
| `two_phase` (Two-Phase) | 3D search for candidates, per-camera images for ranking | 3D + 2D |
| `hybrid_deltat_3d` (Hybrid) | Match every N-th frame where motion beats noise, fill between | 3D only |

Details per tracker live in `src/openptv2/tracking_registry.py`
(`TRACKER_REGISTRY`) — the machine-readable version of this page.

## 3. Each tracker: caveats, tips, tricks

### trackcorr (`default`, `standard_forward`, …) — the original engine
The safest default. Predicts forward, checks candidates in every camera
image, accepts links with small acceleration and turning angle, and can run
backward plus gap relinking. **Caveat:** slowest of the bunch, and three
interacting parameters (`dvxmax`, `dacc`, `angle`). **Tip:** on noisy slow
flow, switch the angle limit off — the measured turning angle is mostly
noise then. If a particle is hidden in one camera, trackcorr usually still
gets it through the others.

### Fast 3D / 3MA (`priority_segment_3d`) — the fast one
Hundreds of thousands of particles per second, simplest mental model
(smoothest join wins). **Caveat:** blind to the images, so a ghost particle
sitting where the guess expects is accepted without question. **Tip:** use
it for clean, dense data and high throughput; distrust it where ghosts are
likely (poor calibration, few cameras).

### 4BE (`4be`) — the careful one
Looks one frame into the future before committing, and refuses contested
dots outright. **Caveat:** built for sparse clean data; on noisy/dense data
it produces the worst accelerations of all engines and leaves gaps
deliberately (gap bridging is off for its preset on purpose — it would
rebuild exactly the links 4BE declined). **Tip:** sparse lab data with
reliable detection; not turbulence.

### MyPTV 3D (`nearest_hungarian_3d`) — the fair one
Nobody grabs the nearest dot first: it finds the pairing with the lowest
*total* distance, so nobody is paired badly. Tracks survive short gaps
(`max_gap`), code is plain readable Python. **Caveat:** one frame pair at a
time — it cannot use what happens next. **Tip:** good first alternative to
the default; easiest engine to modify (`src/openptv2/plugins/myptv_3d_tracking.py`).

### MyPTV 2D (`myptv_2d_tracking`) — the per-camera voter
Each camera tracks its own movie; links with the most camera votes win.
**Caveat:** a link needs only one vote, so a single confused camera can
still create a bad link. **Tip:** reaches for it when 3D triangulation is
unreliable but the raw images are clean.

### proPTV (`predictive_gmm_3d`) — the smoother
Fits smooth curves through each path, so speeds and accelerations stay
sensible under noise. **Caveat (read first):** this port predicts from the
*smoothed current position*, not from an extrapolated one, and its search
radius is the same with or without history — so it currently behaves closer
to smoothed nearest-neighbour than to the predictive scheme of the paper.
Also dense data gets expensive. **Tip:** check the plugin README before
trusting its "predictive" label; fix the extrapolation first if you build
on it.

### Two-Phase (`two_phase`) — the hybrid
3D search lists candidates, per-camera image distances rank them, Hungarian
per connected group decides. No motion model — immune to bad guesses, but
fails once motion outruns particle spacing. Reported +74% multi-frame
trajectories over the default on a poorly-conditioned aorta dataset.
Comment on lines +105 to +109
**Caveat:** needs Cet 2D targets per camera in the store; falls back to
pure 3D (`leaf_weight=0`) without them. **Tips:** start with
`leaf_weight=1`, `v_max` at ~3× your typical step; see
[Two-Phase Tracking](two-phase-tracking.md) for the full parameter guide
including the shared-observation prototype flags.

### Hybrid multi-Δt (`hybrid_deltat_3d`) — the slow-flow specialist
When particles crawl, frame-to-frame steps drown in noise — so it matches
every N-th frame (where displacement beats noise) and fills the middle with
a smooth curve. **Caveat:** the only engine that changes the signal-to-noise
ratio instead of fighting it, but the smooth fill is wrong for fast or
curved motion. **Tip:** high frame rate + slow flow; set `stride` so the
coarse step clearly exceeds your 3D noise floor.

## 4. Upstream credit: MyPTV and proPTV are plugins, not forks

Two engines borrow ideas from outside projects. **We use them as plugins —
adapted concepts on openptv2's own data structures, not modified copies of
their code.** The full frameworks (triangulation pipelines, calibration,
backtracking/repair, smoothing toolboxes) live only in their own
repositories — use those projects directly if you need them. Both are
permissively MIT-licensed.

- **MyPTV** by Ron Shnapp — open-source Python 3D-PTV library.
Repository: <https://github.com/ronshnapp/MyPTV> ·
Paper: Shnapp, R. (2022). *MyPTV: A Python Package for 3D Particle
Tracking.* Journal of Open Source Software, 7(75), 4398.
<https://doi.org/10.21105/joss.04398> ·
What we adapted: per-camera 2D image-space tracking with multi-camera
consensus (`myptv_2d_tracking`), and kinematic prediction + assignment
matching in 3D (`nearest_hungarian_3d`).
- **proPTV** by Robin Barta and colleagues (DLR) — probabilistic PTV
framework, Python.
Repository: <https://github.com/RobinBarta/proPTV> ·
Paper: Barta, R. et al. (2024). *proPTV: A probabilistic particle
tracking velocimetry framework.* Journal of Computational Physics, 514,
113212. <https://doi.org/10.1016/j.jcp.2024.113212> ·
What we adapted: the small pure-numpy core (Gaussian-mixture / basis
approximation and Savitzky–Golay smoothing, vendored under
`src/openptv2/plugins/proptv/`), wired into the `predictive_gmm_3d`
plugin. The original's triangulation, probability model, backtracking
and repair are *not* ported.
- The classic engines descend from the OpenPTV/liboptv lineage
(<http://www.openptv.net>).

If you publish with these methods, please cite the original authors above
in addition to openptv2.
Loading
Loading