Tutorial: Choosing Tracking Parameters from Data Statistics¶
Tracking parameters (velocity, acceleration, angle limits) can be chosen systematically using simple statistics from your dataset. This approach minimizes trial-and-error and ensures robust, unambiguous tracking.
Step-by-step workflow¶
-
Run a quick probe script to compute:
- Maximum observed displacement per frame (velocity)
- Maximum observed acceleration
- Typical interparticle distance (spacing between particles in a frame)
-
Set parameters:
- Velocity window (
velocity_lims):- Set just above the maximum observed displacement, but below the typical interparticle distance to avoid ambiguity.
- Acceleration limit (
accel_lim):- Set just above the maximum observed acceleration.
- Angle limit (
angle_lim):- Set to a typical value for smooth motion (e.g., 20 gon ≈ 18°), or just above the maximum observed angle change if available.
- Velocity window (
Example (from Burgers dataset):¶
| Statistic | Value (example) |
|---|---|
| Max displacement | 0.08 mm/frame |
| Max acceleration | 0.09 mm/frame² |
| Interparticle distance | 1.53 mm |
Parameter selection:
- velocity_lims = [[-0.088, 0.088], [-0.088, 0.088], [-0.088, 0.088]] (10% above max displacement, but < interparticle distance)
- accel_lim = 0.099 (10% above max acceleration)
- angle_lim = 20 (gon)
Reference test¶
See the tracking parameter sensitivity tests in tests/batch/test_pyptv_batch.py and tests/batch/test_apply_optimizations.py.
These tests demonstrate the full workflow and can be used as a template for your own datasets.
Tracking Algorithms¶
OpenPTV implements two tracking strategies for particle trajectory reconstruction:
Case Study: Burgers Dataset Gap Relinking¶
For a detailed analysis of how these algorithms differ when particles re-appear after a gap, including: - 7 trajectories vs 6 trajectories from the same 5-frame dataset - Root cause analysis of the algorithmic divergence - Backward tracking recovery capabilities and limitations - Numerical insights for designing better tracking methods
See: Burgers Gap Relinking Case Study
Overview¶
| Feature | track.c |
track3d.c |
|---|---|---|
| Dimensionality | Multi-camera 2D→3D | Direct 3D |
| Candidate search | Projects 3D box to 2D per camera | Direct 3D box search |
| Candidate sorting | Frequency across cameras | Acceleration metric |
| Linking metric | Angle + acceleration (gon) | Acceleration (second derivative) |
| Particle addition | Yes | No |
| Backward tracking | Yes | No |
| Lines of code | ~1275 | ~203 |
track.c — Multi-Camera Tracking¶
Full pipeline for calibrated multi-camera setups. Handles the complete workflow from 2D image targets to 3D particle trajectories.
Key Functions¶
trackcorr_c_loop()— Main forward tracking looptrackback_c()— Backward tracking to fill gapssearchquader()— Projects 3D search volume to 2D image regions per cameracandsearch_in_pix()— Finds up to 4 nearest candidates in pixel spacesort_candidates_by_freq()— Ranks candidates by camera visibility countangle_acc()— Computes angle (gon) and acceleration between velocity vectorsadd_particle()— Inserts new particles from unmatched targets
Algorithm Flow¶
- For each particle in current frame:
- Predict next position using
2*curr - prev(linear extrapolation) - Project 3D search cuboid to 2D per camera via
searchquader() - Find candidates in pixel space via
candsearch_in_pix() - Sort candidates by frequency across cameras
- Evaluate candidates using angle + acceleration metric
- Link to best candidate if within thresholds (
dacc,dangle) - Optionally add new particles from unmatched targets
Use Case¶
Real experiments with 2–4 calibrated cameras. Robust to occlusions because it leverages multi-camera redundancy.
track3d.c — Direct 3D Tracking¶
Simplified tracking for pre-reconstructed 3D data. No camera projection needed.
Key Functions¶
track3d_loop()— Main tracking loopfind_candidates_in_3d()— Finds particles within 3D search box
Algorithm Flow¶
Three-level linking strategy:
- Level 1 — Particles with previous links:
- Predict:
2*curr - prev -
Search in 3D box defined by
dvxmax,dvymax,dvzmax -
Level 2 — No previous link, but neighbors have links:
- Compute average velocity from linked neighbors
-
Predict:
curr + avg_velocity -
Level 3 — No previous link, no neighbor links:
- Use current position as prediction
For each level: - Find candidates within 3D box - Sort by acceleration (second derivative) - Link to best unlinked candidate
Use Case¶
Pre-computed 3D data (e.g., rt_is.* files). Faster and simpler than full multi-camera tracking.
Candidate Selection Comparison¶
track.c — Multi-Camera Frequency¶
// Search in each camera's image space
for (cam = 0; cam < num_cams; cam++) {
register_closest_neighbs(targets[cam], ...);
}
// Sort by how many cameras see each candidate
num_cands = sort_candidates_by_freq(points, num_cams);
track3d.c — Direct 3D Box¶
// Simple box search in 3D
for (i = 0; i < frm->num_parts; i++) {
if (fabs(x - pos[0]) < dx &&
fabs(y - pos[1]) < dy &&
fabs(z - pos[2]) < dz) {
indices[count++] = i;
}
}
Linking Quality Metrics¶
track.c — Angle + Acceleration¶
Uses angle between velocity vectors in gon plus acceleration magnitude:
void angle_acc(vec3d start, vec3d pred, vec3d cand,
double *angle, double *acc)
{
vec3d v0, v1;
vec_subt(pred, start, v0); // predicted velocity
vec_subt(cand, start, v1); // actual velocity
*acc = vec_diff_norm(v0, v1);
*angle = (200./M_PI) * acos(vec_dot(v0, v1) / (vec_norm(v0) * vec_norm(v1)));
}
Decision: link if (acc < dacc AND angle < dangle) OR (acc < dacc/10).
track3d.c — Acceleration Only¶
Uses second derivative (acceleration) as the sole metric:
// Acceleration = |curr - 2*next + prev|
float acc = 0.0;
for (d = 0; d < 3; d++) {
float diff = curr[d] - 2*next[d] + prev[d];
acc += diff * diff;
}
decis[k] = sqrtf(acc);
Output Format¶
Both algorithms print per-step statistics:
track.c: step: 10000, curr: 998, next: 1043, links: 453, lost: 545, add: 1
track3d.c: track3d step: 10001, curr: 1, next: 1, links: 0
When to Use Which¶
| Scenario | Use |
|---|---|
| Raw 2D images + calibration | track.c |
| Pre-reconstructed 3D positions | track3d.c |
| Need backward gap filling | track.c |
| Fast iteration on 3D data | track3d.c |
| Multi-camera redundancy required | track.c |
How to Choose/Enable 3D Segment Tracking (track3d)¶
You can select and run the direct 3D segment tracking algorithm (track3d) over the standard multi-camera epipolar tracking algorithm (trackcorr) in three different ways:
1. Through the Desktop GUI¶
- Open the Parameter Editor in the GUI.
- Navigate to the Track Parameters (Tracking) tab.
- Locate the Tracking mode (0=Standard, 1=3D Seg): parameter.
- Set the value to:
0for Standard Epipolar tracking (trackcorr).1for 3D Segment tracking (track3d).- Save the parameters. The GUI and its interactive step-by-step previewers/visualizers (
tracking_previewandtracking_viz_panel) will conditionally load and execute the selected algorithm automatically.
2. Through Parameter Files (YAML & Legacy .par)¶
- YAML configuration: In your active YAML parameters, set the
track_modekey in thetracksection: - Legacy
.parconfiguration: Inparameters/track.par, the 10th line (if present) represents the tracking mode. Set it to1to enable 3D segment tracking: (Note: If the 10th line is absent, the system gracefully defaults to0/ Standard mode).
3. Via the Command-Line Batch Utility (pyptv_batch.py)¶
When executing batch processing from the CLI, you can force the use of 3D tracking using the --track3d option:
--track3d is not specified, the utility will fall back to reading the track_mode setting from the active parameter file.
Python Translation Status¶
Both track.c and track3d.c have been fully translated to Python in algorithms/track.py and algorithms/track3d.py. The Python implementations include Numba JIT-compiled fast paths.
Parity with C/Cython¶
| Dataset | track3d | trackcorr |
|---|---|---|
| Burgers (5 frames, 5 particles) | Exact match | Exact match |
| Cavity (4 frames, ~700 particles) | Exact match | Python produces more links (see below) |
| Synthetic (8 frames, 15 particles) | 99% recovery, 0 wrong | 100% recovery, 0 wrong |
Python Improvements Over C¶
The Python trackcorr_c_loop includes two improvements not present in the C code:
1. Phase 3: Losers Retry¶
When two particles compete for the same target in conflict resolution, C drops the loser permanently. Python lets the loser try its fallback candidates (2nd, 3rd best matches) if they're still unclaimed. On the cavity dataset, this recovers ~27 additional correct links.
# Phase 3: Losers retry with fallback candidates (claim unclaimed only)
for h in range(fb.buf[1].num_parts):
curr_path_inf = fb.buf[1].path_info[h]
if curr_path_inf.inlist > 1 and curr_path_inf.next == NEXT_NONE:
for ti in range(1, curr_path_inf.inlist):
cand = curr_path_inf.linkdecis[ti]
if fb.buf[2].path_info[cand].prev == PREV_NONE:
curr_path_inf.next = cand
fb.buf[2].path_info[cand].prev = h
break
2. Stale Buffer Fix¶
When step >= last - 2, no new frame can be loaded into the last buffer slot after rotation. C leaves stale data from a previous frame in that slot, which assess_new_position may search and produce spurious links. Python clears the slot:
fb.fb_next()
fb.write_frame_from_start(step)
if step < run_info.seq_par.last - 2:
fb.read_frame_at_end(step + 3, read_links=False)
else:
fb.buf[fb.buf_len - 1].num_parts = 0 # clear stale data
C count1 Overcounting Bug¶
The C code increments count1 inside the conflict resolution loop. When particle B loses a conflict to particle A (B processed first, B.next set to NEXT_NONE), B has already been counted. The final count1 is inflated. Python counts in a separate loop after all conflicts are resolved, producing the correct count. This explains why C's printf reports more links than actually appear in the output files.
Synthetic Test Suite¶
A synthetic test case (tests/unit/test_synthetic_tracking.py) validates both algorithms against known ground truth:
- 15 particles with diverse trajectories:
- 5 constant-velocity straight lines
- 3 constant-acceleration curved paths
- 2 near-miss paths (close approach)
- 2 parallel neighbors (3-unit separation)
- 2 actual crossing paths
- 1 late entry (appears at frame 3)
- 8 frames (10001–10008)
- 5 test cases: link correctness, recovery rate, trajectory distance validation, and trackcorr >= track3d comparison
High-Performance Parallelization (OpenMP & Cython)¶
To support high-throughput processing on large experimental datasets (e.g., thousands of frames with 1,000+ particles), OpenPTV2 includes two major OpenMP-parallelized optimization pipelines implemented under standard Cython 3 pure-Python mode:
1. Parallel Image Preprocessing (Approach C)¶
The preprocessing pipeline (including highpass filtering, thresholding, background subtraction, and image segmentation/detection) is parallelized across frames. * Mechanism: Rather than processing each camera sequence sequentially, OpenPTV2 chunks and distributes the image-processing workload across threads using OpenMP. * Speedup: Provides a 2.4x to 4x speedup on multi-core systems, greatly reducing batch initialization times. * Integration: Seamlessly integrated into both the Python CLI/batch processing backend and the modern Tkinter/ttkbootstrap GUI.
2. Particle-Level Parallel Tracking (Approach B)¶
The forward multi-camera tracking loop (trackcorr_loop_fast) is parallelized at the particle level, distributing calculations concurrently across OpenMP threads.
Algorithmic Design & Thread-Safety¶
Parallelizing a complex, stateful tracking pipeline requires careful handling of shared states, coordinate buffers, and memory management:
[ Parallel Particle Loop (prange schedule='guided', nogil) ]
├── 1. Linear prediction per particle
├── 2. 3D search box projection to 2D per camera
├── 3. Pixel candidate searches
├── 4. Candidate frequency sorting
└── 5. Log particle additions to Thread-Local buffers
│
▼
[ Sequential Post-Loop Phase (Single-Threaded) ]
├── 1. Gather & append additions deterministically
├── 2. Resolve link conflicts (Phase 1 & 2)
└── 3. Losers retry fallback candidates (Phase 3)
-
Lock-Free Parallel Computation: The tracking prediction, 2D epipolar-box projection (
searchquader), candidate coordinate filtering, candidate sorting, and cost-metric evaluation all run concurrently without lock overhead or the Python GIL. -
Zero-Allocation Thread-Local Scratch Space: Memory allocation inside parallel
nogilblocks triggers expensive runtime allocations or requires acquiring the GIL. To prevent this, we pre-allocate thread-local scratch arrays (such as ray-tracing coordinate arrays, pixel coordinates, and temporary lists) during initiation. -
Dynamic Thread-ID Safety & Defensive Clamping: Because OpenMP's
threadid()can occasionally return indices greater than the requested thread count due to runtime thread-pooling, OpenPTV2 dynamically pads scratch buffer allocations tomax_threads_alloc = max(num_threads, 64)and clamps indexing bounds defensively: -
Absolute Mathematical Determinism: Adding new particles or modifying candidate tables directly in parallel threads would lead to non-deterministic race conditions (and require locks). Instead, each thread writes its added particles and candidate tables into dedicated thread-local buffers. A single-threaded post-loop sequential block then collects and appends these records in a fixed, thread-independent order. This guarantees 100% mathematical parity and identical tracking results across any thread configuration.
-
Robust Bounds Checking: All candidate array insertions verify array bounds (e.g. checking if
0 <= cand_idx < targ_tnr.shape[1]) before performing any writes, preventing memory corruption orfree(): invalid next sizeerrors.