Metadata-Version: 2.4
Name: pyxtrackers
Version: 2026.3.3
Summary: High-performance Cython implementations of SOTA multi-object trackers
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: vendor/lapjv/LICENSE
Requires-Dist: numpy>=1.22; python_version < "3.12"
Requires-Dist: numpy>=1.26; python_version >= "3.12" and python_version < "3.13"
Requires-Dist: numpy>=2.1; python_version >= "3.13"
Provides-Extra: dev
Requires-Dist: Cython>=3.0; extra == "dev"
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: filterpy>=1.4; extra == "dev"
Requires-Dist: lap>=0.5; extra == "dev"
Requires-Dist: pyinstaller>=6.19.0; extra == "dev"
Requires-Dist: altair>=5.0; extra == "dev"
Requires-Dist: vl-convert-python>=1.0; extra == "dev"
Dynamic: license-file

# PyxTrackers

High-performance Cython implementations of state-of-the-art multi-object tracking algorithms.

PyxTrackers provides drop-in replacements for three widely used MOT trackers, reimplemented in Cython for significant speedups while maintaining numerical equivalence with the original Python implementations.

> **Disclaimer**
> The Cython implementations are intended to provide the same tracking behavior as the original Python implementations.
> For SORT and OC-SORT, tests confirm the floating-point outputs are bit-wise identical to the original Python implementations.
> For ByteTrack, tests confirm the bounding boxes are within `1e-6` floating-point absolute precision error to the original Python implementations.

## Supported Trackers

| Tracker | Our Speedup (# objects < 80) | Description | Paper | GitHub |
|---------|------------------------------|-------------|-------|--------|
| **SORT** | **70-95x** | Simple Online and Realtime Tracking | [Bewley et al., 2016](https://arxiv.org/abs/1602.00763) | [abewley/sort](https://github.com/abewley/sort) |
| **ByteTrack** | **25-35x** | Multi-Object Tracking by Associating Every Detection Box | [Zhang et al., 2022](https://arxiv.org/abs/2110.06864) | [FoundationVision/ByteTrack](https://github.com/FoundationVision/ByteTrack) |
| **OC-SORT** | **40-60x** | Observation-Centric SORT | [Cao et al., 2023](https://arxiv.org/abs/2203.14360) | [noahcao/OC_SORT](https://github.com/noahcao/OC_SORT) |

## Performance

The plots below were generated by running `pytest --visualize`, which benchmarks all three trackers across a range of detection counts and records throughput (frames per second) for both the Python reference and Cython implementations (through import or cli).

![Tracker throughput: Python vs Cython](https://raw.githubusercontent.com/chanwutk/pyxtrackers/main/assets/throughput.png)
**Throughput** — frames processed per second as average detections per frame. Dashed lines are the original Python implementations; solid lines are the Cython reimplementations.

![Cython speedup over Python](https://raw.githubusercontent.com/chanwutk/pyxtrackers/main/assets/speedup.png)
**Speedup** — ratio of Python time to Cython time (higher = faster).

## Installation

### From PyPI

```bash
pip install pyxtrackers
```

### From source

```bash
git clone https://github.com/chanwutk/pyxtrackers.git
cd pyxtrackers
pip install -e .
```

### Requirements

- Python >= 3.10
- NumPy >= 1.22 (Python 3.10–3.11), >= 1.26 (Python 3.12), >= 2.1 (Python 3.13+)
- A C/C++ compiler (gcc, clang, or MSVC)
- Cython >= 3.0 (build-time only)

## Quick Start

```python
import numpy as np
from pyxtrackers import Sort, BYTETracker, OCSort

# --- SORT ---
tracker = Sort(max_age=30, min_hits=3, iou_threshold=0.3)

# Detections: [[x1, y1, x2, y2, score], ...]
detections = np.array([
    [100, 100, 200, 200, 0.9],
    [300, 300, 400, 400, 0.8],
], dtype=np.float64)

# Returns: [[x1, y1, x2, y2, track_id], ...]
tracked = tracker.update(detections)

# --- ByteTrack ---
tracker = BYTETracker(track_thresh=0.5, match_thresh=0.8, track_buffer=30)
tracked = tracker.update(detections)
# Returns: [[x1, y1, x2, y2, track_id], ...]

# --- OC-SORT ---
tracker = OCSort(det_thresh=0.3)
tracked = tracker.update(detections)
# Returns: [[x1, y1, x2, y2, track_id], ...]
```

All three trackers share the same interface: configuration in the constructor, only detections in `update()`.

<!-- If the input detections were produced at a different resolution than the original image, pass `img_info` and `img_size` to the constructor to enable automatic rescaling:

```python
tracker = BYTETracker(track_thresh=0.5, img_info=(1080, 1920), img_size=(608, 1088))
tracker = OCSort(det_thresh=0.3, img_info=(1080, 1920), img_size=(608, 1088))
``` -->

## How It Works

Each tracker is reimplemented in Cython using:

- **C structs** instead of Python classes for track state (zero Python object overhead)
- **`nogil` sections** for GIL-free computation in hot paths
- **`cdef` functions** for C-only internal calls with no Python dispatch overhead
- **Vendored LAPJV** C++ solver for linear assignment (https://github.com/gatagat/lap)
- **Customized Kalman filter** with fixed state space.

Kalman filter predict/update cycles, IOU computation, and the Hungarian algorithm all run at C speed.

### Compiler Optimization Flags

`setup.py` selects compiler flags based on the platform, compiler, and build context. The table below shows exactly what is applied in each scenario:

| Scenario | Base flags | Arch-specific flags | C++ standard |
|----------|-----------|-------------------|-------------|
| **Linux / macOS x86_64** (source install) | `-O3 -ffast-math` | `-march=native -mtune=native` | `-std=c++11` |
| **macOS ARM** (source install) | `-O3 -ffast-math` | `-mcpu=apple-m1` | `-std=c++11` |
| **macOS universal2 Python** | `-O3 -ffast-math` | _(none)_ | `-std=c++11` |
| **Pre-built wheels** (cibuildwheel / conda) | `-O3 -ffast-math` | _(none)_ | `-std=c++11` |
| **Pre-built wheels, macOS ARM** | `-O3 -ffast-math` | `-mcpu=apple-m1` | `-std=c++11` |
| **Windows** (MSVC) | `/O2 /fp:fast` | _(none)_ | `/std:c++14` |

**Key details:**

- **`-march=native`** tunes the binary for the exact CPU it's compiled on. Only used for source installs — pre-built wheels omit it so they run on any CPU of that architecture.
- **`-mcpu=apple-m1`** is the Apple ARM equivalent. Apple Clang doesn't support `-march=native` on ARM, so we target the M1 baseline (compatible with all M-series chips).
- **Universal2 Python** (CFLAGS contain both `-arch arm64` and `-arch x86_64`) gets no arch-specific flags because the compiler runs for both architectures in one pass.
- **`-ffast-math`** / **`/fp:fast`** allows the compiler to reorder floating-point operations for speed. This is appropriate here because tracking algorithms likely not require strict IEEE 754 compliance.
- Portable vs. source builds are auto-detected via the `CIBUILDWHEEL` and `CONDA_BUILD` environment variables.

We welcome discussion on flag choices — if you have suggestions or concerns about specific flags (e.g., `-ffast-math` behavior, architecture targets), please [open an issue](https://github.com/chanwutk/pyxtrackers/issues).

**Idea**: Different installation flags for different optimization option. For example, `pip install pyxtrackers[mcpu-apple-m1]`

## Project Structure

```
pyxtrackers/           # Installable Cython package
  sort/                # SORT tracker
  bytetrack/           # ByteTrack tracker
  ocsort/              # OC-SORT tracker
  cli.pyx              # Cython stdin/stdout CLI for cross-language interop
  cli_launcher.py      # Python launcher for PyInstaller entrypoint
references/            # Pure Python reference implementations (for testing)
tests/                 # Comparison tests verifying numerical equivalence
vendor/lapjv/          # Vendored C++ linear assignment solver
```

## Development

```bash
# Setup
python -m venv .venv
source .venv/bin/activate      # Linux/macOS
# .venv\Scripts\activate       # Windows
pip install -e ".[dev]"

# After editing .py files  → no action needed, changes take effect immediately
# After editing .pyx files → rebuild the changed extensions:
python setup.py build_ext --inplace

# Run tests
pytest tests/ -v
```

## Testing

Tests run both the Cython and Python reference implementations on identical detection sequences and verify numerical equivalence within 1e-6 pixel tolerance.

```bash
pytest                                          # All tests (skips binary-CLI tests)
pytest tests/ -v --cli-binary dist/pyxtrackers  # Include binary-CLI tests
```

`--cli-binary` takes the path to a PyInstaller-built binary. Build it first with `pyinstaller pyxtrackers.spec`, then pass the resulting `dist/pyxtrackers` (or `dist/pyxtrackers.exe` on Windows).

## Interoperable (CLI)

PyxTrackers includes a stdin/stdout CLI that lets any language invoke tracking via pipes. After installation, the `pyxtrackers` command is available.
<!-- 
The CLI runtime is compiled (`pyxtrackers/cli.pyx`) and uses native C stdio (`fgets`/`fwrite`) plus C numeric parsing (`strtod`) in the hot path for lower overhead than Python tokenization. -->

### Usage

```bash
pyxtrackers <tracker> [options]
```

The process reads one line per frame from stdin, runs the tracker, and writes one line per frame to stdout. Empty input lines (no detections) still advance the tracker state and produce an empty output line, preserving 1:1 line correspondence.

Parsing is strict fail-fast: malformed tokens raise `ValueError` and terminate with a non-zero exit code.

### Input format

Detections are space-separated, each with 5 comma-separated values:

```
x1,y1,x2,y2,score x1,y1,x2,y2,score ...
```

### Output format

Tracked objects are space-separated, each with 5 comma-separated values:

```
x1,y1,x2,y2,id x1,y1,x2,y2,id ...
```

### Examples

Pipe detections from a file:

```bash
cat detections.txt | pyxtrackers sort --min-hits 1 > tracks.txt
```

Inline:

```bash
echo "100,200,300,400,0.9 150,250,350,450,0.8" | pyxtrackers sort --min-hits 1
```

With ByteTrack and image scaling:

```bash
cat detections.txt | pyxtrackers bytetrack --track-thresh 0.5 --img-info 1080 1920 --img-size 608 1088
```

### Cross-language integration

Any language that can spawn a subprocess and read/write its stdin/stdout can use pyxtrackers. For example, in Node.js:

```javascript
const { spawn } = require('child_process');
const readline = require('readline');

const tracker = spawn('pyxtrackers', ['sort', '--min-hits', '1']);

const rl = readline.createInterface({ input: tracker.stdout });
rl.on('line', (line) => {
  // Each line: "x1,y1,x2,y2,id x1,y1,x2,y2,id ..."
  console.log('Tracks:', line);
});

// Send detections (one frame per line)
tracker.stdin.write('100,200,300,400,0.9 150,250,350,450,0.8\n');
tracker.stdin.write('105,205,305,405,0.9\n');
```

<!-- Or in Rust (pseudocode):

```rust
use std::process::{Command, Stdio};
use std::io::{Write, BufRead, BufReader};

let mut child = Command::new("pyxtrackers")
    .args(&["ocsort", "--det-thresh", "0.3"])
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .spawn()?;

let stdin = child.stdin.as_mut().unwrap();
writeln!(stdin, "100,200,300,400,0.9")?;

let reader = BufReader::new(child.stdout.take().unwrap());
for line in reader.lines() {
    println!("Tracks: {}", line?);
}
``` -->

## Roadmap

- [ ] Document code conversion process.
- [ ] Add back OC-SORT and BYTETrack's original interface.
- [ ] Publish to conda-forge.
- [ ] More rigorous testing. Multiple samples of each experiment.

## License

MIT
