Metadata-Version: 2.4
Name: pytrackmate-cli
Version: 0.2.0
Summary: Python cell tracking pipeline inspired by TrackMate
Author: PyTrackMate Contributors
License-Expression: BSD-3-Clause
Keywords: tracking,cell-tracking,bioimage,napari,trackmate,microscopy
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
Classifier: Topic :: Scientific/Engineering :: Image Processing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy<2.5,>=1.24
Requires-Dist: pandas>=1.5
Requires-Dist: scipy>=1.10
Requires-Dist: trackpy>=0.6
Requires-Dist: tifffile>=2023.0
Requires-Dist: lxml>=4.9
Provides-Extra: skimage
Requires-Dist: scikit-image>=0.20; extra == "skimage"
Provides-Extra: cellpose
Requires-Dist: cellpose>=2.0; extra == "cellpose"
Requires-Dist: torch>=1.13; extra == "cellpose"
Provides-Extra: stardist
Requires-Dist: stardist>=0.9; extra == "stardist"
Requires-Dist: tensorflow>=2.20; extra == "stardist"
Provides-Extra: viz
Requires-Dist: matplotlib>=3.7; extra == "viz"
Requires-Dist: seaborn>=0.12; extra == "viz"
Provides-Extra: all
Requires-Dist: pytrackmate[skimage]; extra == "all"
Requires-Dist: pytrackmate[cellpose]; extra == "all"
Requires-Dist: pytrackmate[stardist]; extra == "all"
Requires-Dist: pytrackmate[viz]; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7.4; extra == "dev"
Requires-Dist: pytest-cov>=4.1; extra == "dev"
Requires-Dist: ruff>=0.1; extra == "dev"
Provides-Extra: progress
Requires-Dist: tqdm>=4.64; extra == "progress"
Provides-Extra: napari
Requires-Dist: napari>=0.4.18; extra == "napari"
Requires-Dist: magicgui>=0.7; extra == "napari"
Requires-Dist: qtpy>=2.3; extra == "napari"
Requires-Dist: PyQt6>=6.6; extra == "napari"
Dynamic: license-file

# PyTrackMate

A modular Python cell tracking pipeline inspired by [TrackMate](https://imagej.net/plugins/trackmate/) (Fiji), built for 2D+T microscopy (cell/nucleus) movies stored as TIFF stacks.

The pipeline follows the TrackMate architecture: **detect → measure spots → link into tracks → measure tracks → filter → export**, with a pluggable registry of detectors, trackers, and feature computers.

## Installation

```bash
# PyPI (package name is pytrackmate-cli; the import and command stay `pytrackmate`)
pip install pytrackmate-cli[skimage,viz,napari]
# or with uv
uv add pytrackmate-cli[skimage,viz,napari]

# Development
uv sync --extra skimage --extra viz --extra dev
```

Optional extras:

| Extra      | Provides                       |
| ---------- | ------------------------------ |
| `skimage`  | `threshold` / `watershed` / `log` detectors |
| `viz`      | matplotlib/seaborn visualization |
| `cellpose` | `cellpose` deep-learning detector |
| `stardist` | `stardist` deep-learning detector |
| `progress` | tqdm progress bars during detection |
| `all`      | everything above              |
| `dev`      | pytest, ruff                  |

## CLI

```bash
pytrackmate info                                   # list registered components
pytrackmate run input.tif output.xml               # default pipeline
pytrackmate run input.tif out.csv --format csv \
    --detector watershed --detector-kwargs '{"min_distance":5,"min_area":5,"sigma":1.0}' \
    --tracker trackpy --tracker-kwargs '{"search_range":5,"min_track_length":3}' \
    --workers 4 --progress \
    --spot-filter quality:min_quality=0.5 \
    --track-filter length:min_spots=3
```

`run` options:

| Option              | Default                                      | Description |
| ------------------- | -------------------------------------------- | ----------- |
| `--detector`        | `threshold`                                  | Detector name |
| `--detector-kwargs` | –                                            | JSON dict of detector kwargs |
| `--tracker`         | `trackpy`                                    | Tracker name |
| `--tracker-kwargs`  | –                                            | JSON dict of tracker kwargs |
| `--spot-features`   | `spot_intensity_mean spot_area spot_perimeter spot_circularity spot_radius` | Spot feature names |
| `--track-features`  | `track_displacement track_total_distance track_speed track_duration` | Track feature names |
| `--format`          | `xml`                                        | `xml` or `csv` |
| `--workers`         | `1`                                          | Number of parallel detection workers |
| `--progress`        | `false`                                      | Show progress bar during detection |
| `--pixel-size`      | auto (TIFF/OME) or `1.0`                     | µm per pixel calibration |
| `--time-interval`   | auto (TIFF/OME) or `1.0`                     | Seconds per frame calibration |
| `--spot-filter`     | –                                            | Spot filter (repeatable), e.g. `quality:min_quality=0.5` |
| `--track-filter`    | –                                            | Track filter (repeatable), e.g. `length:min_spots=5` |

XML output is TrackMate-compatible (`TrackMate` root with `Model`, spots, and tracks) and can be reopened in TrackMate/Fiji. CSV output writes `<output>` (spots) plus `<output>_tracks.csv` (tracks).

### CLI filter syntax

Filters use a colon-separated `type:key=value` syntax. Repeat the flag for multiple filters:

```bash
# Keep only high-quality spots:
--spot-filter quality:min_quality=0.5

# Filter spots by area:
--spot-filter feature:feature_name=spot_area:min_val=10:max_val=1000

# Filter spots by radius:
--spot-filter radius:min_radius=2:max_radius=10

# Keep tracks with at least 5 spots:
--track-filter length:min_spots=5

# Filter tracks by duration:
--track-filter duration:min_duration=10:max_duration=100

# Filter tracks by speed (needs track_speed feature computed):
--track-filter speed:min_speed=0.5:max_speed=5
```

Available filter types: `quality`, `feature`, `radius` (spot); `length`, `duration`, `speed`, `track_feature` (track).

## Python API

```python
from pytrackmate.core.model import Model
from pytrackmate.core.registry import create_detector, create_feature, create_tracker
from pytrackmate.pipeline.runner import Pipeline
from pytrackmate.io.image_reader import TiffReader
from pytrackmate.io.trackmate_xml import write_trackmate_xml

# 1. Programmatic pipeline
pipeline = Pipeline(image_reader=TiffReader())
pipeline.detector = create_detector("watershed", min_distance=5, min_area=5)
pipeline.spot_features = [create_feature("spot_intensity_mean"), create_feature("spot_area")]
pipeline.tracker = create_tracker("trackpy", search_range=5, min_track_length=3)
pipeline.track_features = [create_feature("track_displacement"), create_feature("track_duration")]
model = pipeline.run("movie.tif")

# Parallel detection with progress bar
model = pipeline.run("movie.tif", workers=4, progress=True)

# 2. Manual model building
model = Model()
for t, frame in enumerate(tiff_stack):
    for spot in detector.detect(frame, frame=t):
        model.add_spot(spot)
model = tracker.track(model)

# 3. Filtering
from pytrackmate.filtering import TrackLengthFilter, QualityFilter, TrackDurationFilter
pipeline.filter_spots(QualityFilter(min_quality=100).filter)
pipeline.filter_tracks(TrackLengthFilter(min_spots=5).filter)

# Or use the new CLI-compatible filter chaining:
pipeline.filter_spots(QualityFilter(min_quality=100)).filter_tracks(TrackLengthFilter(min_spots=5))

# 4. Visualization
from pytrackmate.visualization import (
    plot_detections, plot_track_map,
    plot_feature_scatter, plot_track_feature,
    render_tracking_video,
)
render_tracking_video(model, uint8_stack, "tracking.mp4", fps=10, track_history_length=30)
```

Note: when building a `Model` directly (not through `Pipeline.run`), import `pytrackmate.tracking` and `pytrackmate.features` first so the registry is populated.

## Architecture

```
pytrackmate/
├── core/            # Model (spots + tracks), Spot, Track, plugin registry
├── detection/       # threshold, watershed, log, cellpose, stardist detectors
├── features/        # spot (intensity, morphology) and track (motion, duration) features
├── filtering/       # spot filters (quality, feature threshold), track filters (length, duration, ...)
├── tracking/        # trackpy + kalman linking trackers
├── pipeline/        # Pipeline orchestrator (chainable run/filter steps)
├── io/              # TIFF reader, TrackMate XML reader/writer, CSV export
├── visualization/   # overlay plots, feature plots, tracking video renderer
└── cli/             # argparse CLI (`run`, `info`)
```

### Components

**Detectors** (`create_detector`)

| Name       | Description |
| ---------- | ----------- |
| `threshold`| Gaussian blur → threshold (otsu/yen/triangle/li/float) → connected components. Kwargs: `method`, `min_area`, `max_area`, `sigma` |
| `watershed`| Blur → threshold → distance transform → local maxima → watershed. Kwargs: `min_distance`, `min_area`, `max_area`, `sigma`, `threshold_method` |
| `log`      | Laplacian of Gaussian blob detection (TrackMate-style): fixed radius → LoG response → local maxima. Kwargs: `radius`, `threshold`, `min_distance`, `normalize`, `subpixel`, `min_area`, `max_area`, `exclude_border` |
| `cellpose` | Deep-learning cell segmentation (extra `cellpose`) |
| `stardist` | Star-convex cell segmentation (extra `stardist`) |

**Trackers** (`create_tracker`)

| Name     | Description |
| -------- | ----------- |
| `trackpy`| trackpy linking engine. Kwargs: `search_range`, `memory`, `adaptive_stop`, `adaptive_step`, `neighbor_strategy`, `link_strategy`, `min_track_length` |
| `kalman` | TrackMate-style Kalman-filter + LAP tracker (Jaqaman et al. 2008): per-track constant-velocity Kalman prediction, greedy linking within an initial radius, then a global linear-assignment solve gated by a max radius; gaps up to `max_frame_gap` are bridged with the predicted position; optional forward-backward linking. Kwargs: `max_search_radius`, `initial_search_radius`, `max_frame_gap`, `gain`, `quality_threshold`, `use_forward_backward_linking`, `min_track_length` |

**Spot features** (`create_feature`)

| Name                 | Description |
| -------------------- | ----------- |
| `spot_intensity_mean`| Mean intensity in contour |
| `spot_intensity_std` | Std of intensity in contour |
| `spot_intensity_min` | Min intensity in contour |
| `spot_intensity_max` | Max intensity in contour |
| `spot_intensity_total`| Sum of intensity in contour |
| `spot_area`          | Area in µm² (pixel² × pixel_size²) |
| `spot_perimeter`     | Perimeter length in µm (pixels × pixel_size) |
| `spot_circularity`   | 4π·area/perimeter² (dimensionless) |
| `spot_solidity`      | Area / convex-hull area (dimensionless) |
| `spot_radius`        | Equivalent radius √(area/π) in µm (falls back to detector `radius` feature) |

When a pixel size calibration is available (auto-extracted from TIFF/OME metadata, set with `--pixel-size`, or adjusted in the GUI's calibration box), length features are reported in **µm** and area features in **µm²**; circularity, solidity, intensity, and time features are dimensionless and unchanged. Without calibration, features are in pixel units (µm/px = 1.0).

A frame interval (seconds per frame — `--time-interval`, GUI calibration box, or auto-extracted from OME `TimeIncrement`/ImageJ `finterval`) converts time features to **seconds** (`track_duration`, `track_start_frame`, `track_end_frame` × s/frame) and `track_speed` to **µm/s**. Without it, time features stay in frames.

**Track features**

| Name                       | Description |
| -------------------------- | ----------- |
| `track_displacement`       | Euclidean distance first → last spot (µm) |
| `track_total_distance`     | Sum of per-step distances (µm) |
| `track_speed`              | Total distance / duration (µm/s when time calibrated) |
| `track_straightness`       | Displacement / total distance (dimensionless) |
| `track_confinement_ratio`  | Max distance from start / total distance (dimensionless) |
| `track_duration`           | Last frame − first frame (s when time calibrated) |
| `track_start_frame`        | First spot frame (s when time calibrated) |
| `track_end_frame`          | Last spot frame (s when time calibrated) |
| `track_gap_count`          | Number of missing frames between first and last |

**Filters**

| Class                       | Kind  | Description |
| --------------------------- | ----- | ----------- |
| `QualityFilter`             | spot  | Keep spots with quality ≥ `min_quality` |
| `FeatureThresholdFilter`    | spot  | Keep spots whose feature value is within `[min, max]` |
| `RadiusFilter`              | spot  | Keep spots whose radius is within `[min_radius, max_radius]` |
| `TrackLengthFilter`         | track | Keep tracks with ≥ `min_spots` spots |
| `TrackDurationFilter`       | track | Keep tracks with duration ≥ `min_frames` |
| `TrackSpeedFilter`          | track | Keep tracks whose speed is within `[min_speed, max_speed]` |
| `TrackFeatureThresholdFilter` | track | Keep tracks whose feature value is within `[min, max]` |

### Extending the registry

```python
from pytrackmate.core.registry import register_detector
from pytrackmate.detection.base import BaseDetector

@register_detector("my_detector")
class MyDetector(BaseDetector):
    def detect(self, image, frame=0):
        return [...]
```

## Development

```bash
uv run pytest                 # 194 tests
uv run ruff check src/ tests  # lint
```

## License

BSD-3-Clause
