A PyTorch implementation of a graph neural network (GNN) cell tracker for live-cell fluorescence time-lapse microscopy. The model assigns consistent track IDs to nuclei across frames, detects cell divisions (mitosis), and handles cell births and deaths.
This is a PyTorch port of the cell tracking model from Caliban, originally implemented in TensorFlow.
git clone https://github.com/sholtzen/torch-tracking.git
cd torch-tracking
pip install .
pip install -r requirements.txtDefault model weights are downloaded from DeepCell's model zoo. You must first generate an API key from DeepCell.org and add it to your terminal's config file. Once you do this, you can import the tracker from the package and instantiate a tracking instance using CellTracker(). This will download the model weights from DeepCell and move them to the canonical .deepcell folder in your home directory.
Note
If you plan to use a GPU for inference (which we suggest), you will have to make sure the PyTorch version installed is compatible with the CUDA version on your GPUs. If you are having problems with installation, check out this page to make sure you installed the correct PyTorch version that is compatible with your system's CUDA version. If you find that there is a version mismatch, follow the instructions on the page to pip install the correct PyTorch version.
The tracker operates on two numpy arrays:
| Array | Shape | dtype | Description |
|---|---|---|---|
movie |
(T, H, W, C) |
float32 |
Raw fluorescence image. C should be 1. |
annotation |
(T, H, W, 1) |
int |
Nuclear segmentation mask. Each integer label corresponds to one nucleus; background is 0. |
T— number of framesH,W— spatial dimensions (pixels)C— number of channels (always1for nuclear images)
The tracker internally rescales images to a resolution of 0.55 µm/pixel. If your data has a different resolution, set mpp when initializing the tracker.
Training data must be converted from .trk/.trks files (a tar-based format from Caliban) to Zarr arrays before training. Run:
python -m tracking.preprocessBy default this scans ~/.deepcell/tracking/ for *.trks files. For each file it produces:
<split>.zarr— raw arrays (X,y)<split>_proc.zarr— pre-extracted features ready for the dataloader<split>.json— ground-truth lineage records
The convert_trk_to_zarr function in tracking/preprocess.py can also be called directly:
from torch_tracking.preprocess import convert_trk_to_zarr
convert_trk_to_zarr('path/to/train.trks', out_dir='data/')The processed Zarr file (*_proc.zarr) stores pre-extracted per-cell features:
| Key | Shape | Description |
|---|---|---|
appearances |
(B, T, N, 32, 32, 1) |
32×32 image crop centered on each cell nucleus |
morphologies |
(B, T, N, 3) |
Per-cell morphological features: area, perimeter, eccentricity |
centroids |
(B, T, N, 2) |
Y and X centroid coordinates in pixels |
labels |
(B, T-1, N, N) |
Temporal adjacency matrix (ground truth): 1 = same cell, 2 = daughter, 0 = different cell, -1 = padding |
B— number of movies in the batchT— number of framesN— maximum number of cells across the dataset (padded with zeros)
import numpy as np
from torch_tracking.tracker import CellTracker
# Load your movie and segmentation masks
movie = np.load('movie.npy') # shape (T, H, W, 1), float32
annotation = np.load('masks.npy') # shape (T, H, W, 1), int
# Initialize the tracker
tracker = CellTracker(
checkpoint_dir='path/to/best_model.pt', # omit to use ~/.deepcell default
device='cuda',
mpp=0.65, # set to your image's microns-per-pixel
)
# Preprocess and embed the movie
tracker.preprocess_movie(movie=movie, annotation=annotation)
# Run tracking
tracker.track_cells()
# Retrieve results
y_tracked = tracker.y_tracked # (T, H, W, 1) integer label array
lineage = tracker.get_lineage_dict() # dict of track metadata
df = tracker.dataframe() # pandas DataFrame summarytracker.y_tracked — shape (T, H, W, 1). An integer label array where each unique integer corresponds to a tracked cell. Labels are consistent across frames: the same integer means the same cell.
tracker.get_lineage_dict() — returns a dictionary keyed by track label:
{
1: {
'label': 1,
'frames': [0, 1, 2, 3, ...], # frames this cell appeared in
'parent': None, # parent track label (if from division)
'daughters': [4, 5], # daughter track labels (if divided)
'frame_div': 12, # frame where division occurred
'capped': True # True if track ended due to division
},
...
}tracker.dataframe() — returns a pandas DataFrame with columns label, daughters, frame_div. Optional keyword arguments (cell_type, set, part, montage) add metadata columns.
Training requires processed Zarr files (see Preprocessing).
from pathlib import Path
from torch_tracking.model import GNNTrackingModel
from torch_tracking.loader import create_trk_dataloaders
from torch_tracking.training import Trainer
from torch_tracking.utils import create_optimizer, create_scheduler
config = {
"optimizer": "radam",
"learning_rate": 1e-3,
"weight_decay": 0,
"scheduler": "reduce_on_plateau",
"patience": 5,
"max_epochs": 50,
"batch_size": 8,
"n_layers": 2,
"num_workers": 4,
"clipnorm": 1.0,
"loss": "wcce",
"gamma": 1.0,
"dropout": 0,
"device": "cuda:0",
"data_precision": "bfloat16",
"stopping_metric": "loss",
"log_and_save": True,
}
model = GNNTrackingModel(
graph_layer='gat',
data_format='channels_last',
encoder_dim=64,
n_layers=config['n_layers'],
crop_size=32,
dropout=config['dropout'],
)
train_loader, val_loader, _ = create_trk_dataloaders(
train_path=Path.home() / '.deepcell/tracking/train_proc.zarr',
val_path=Path.home() / '.deepcell/tracking/val_proc.zarr',
batch_size=config['batch_size'],
distance_threshold=64,
num_workers=config['num_workers'],
)
optimizer = create_optimizer(model, config)
scheduler = create_scheduler(optimizer, config)
trainer = Trainer(
model=model,
train_loader=train_loader,
val_loader=val_loader,
optimizer=optimizer,
scheduler=scheduler,
device=config['device'],
checkpoint_dir='./checkpoints/',
log_dir='./logs/',
max_epochs=config['max_epochs'],
gradient_clip=config['clipnorm'],
loss=config['loss'],
gamma=config['gamma'],
class_weights=[1, 10, 100], # upweight mitosis class
data_precision=config['data_precision'],
stopping_metric=config['stopping_metric'],
config=config,
)
trainer.train()Checkpoints are saved to ./checkpoints/<timestamp>/best_model.pt. TensorBoard logs are written to ./logs/<timestamp>/.
| Parameter | Default | Description |
|---|---|---|
learning_rate |
1e-3 |
Initial learning rate for the optimizer |
optimizer |
"radam" |
Optimizer type. Options: "radam", "adam", "sgd" |
scheduler |
"reduce_on_plateau" |
LR scheduler. Options: "reduce_on_plateau", "step", "cosine" |
patience |
5 |
Epochs without improvement before LR is reduced (ReduceLROnPlateau) |
max_epochs |
50 |
Maximum number of training epochs |
batch_size |
8 |
Number of sequences per batch |
clipnorm |
1.0 |
Maximum gradient norm for clipping. Lower values (e.g. 0.001) stabilize training if gradients are exploding |
n_layers |
2 |
Number of GNN message-passing layers in the neighborhood encoder |
encoder_dim |
64 |
Feature dimension for all encoders and the GNN hidden state |
dropout |
0 |
Dropout rate in the decoder |
loss |
"wcce" |
Loss function. "wcce" = weighted cross-entropy; "focal" = focal loss |
gamma |
1.0 |
Focal loss exponent (only used when loss="focal") |
class_weights |
[1, 10, 100] |
Per-class weights for [no-link, same-cell, mitosis]. Mitosis events are rare so a high weight (e.g. 100) is recommended |
data_precision |
"bfloat16" |
Mixed-precision training mode. Options: "bfloat16", "float16", "float32" |
distance_threshold |
64 |
Pixel radius for GNN edges (cells farther apart are disconnected). Should match the value used at inference |
track_length |
8 |
Number of frames per training window. Should match the inference track_length |
stride |
1 |
Temporal stride when sliding the training window |
These are set on the CellTracker object and control the post-processing decisions made after the model scores each potential linkage.
| Parameter | Default | Description |
|---|---|---|
death |
0.999 |
Minimum model confidence to call a track "dead" (cell exited the field of view). Higher values make deaths rarer |
birth |
0.999 |
Minimum model confidence to call a detection a "new cell" rather than assigning it to an existing track. Higher values make spontaneous births rarer |
division |
0.5 |
Minimum model confidence for a new-cell event to be attributed to a cell division (mitosis). Lower values increase division sensitivity at the cost of more false positives |
distance_threshold |
64 |
Pixel radius for GNN adjacency at inference. Should match the value used during training |
track_length |
8 |
Number of historical frames used to build the LSTM context for each track. Longer histories give more context but require more memory |
mpp |
0.55 |
Microns per pixel of your input movie. The model was trained at 0.55 µm/pixel; images are rescaled to match this before inference |
device |
"cuda" |
PyTorch device string ("cuda", "cuda:1", "cpu", "mps") |
Tuning guidance:
divisionis the most impactful parameter. Runtracking/eval_sweep.pyto sweep over values (e.g.0.3–0.8) and pick the threshold that maximizes division F1 on a held-out set.birthanddeathrarely need to be changed from0.999unless you see many spurious track births or premature track terminations.mppmust be set correctly for your microscope. An incorrect value shifts the spatial scale of appearance crops and degrades accuracy.
To evaluate on a test set and produce metrics:
python -m tracking.evalThis reads ~/.deepcell/tracking/test.zarr and ~/.deepcell/tracking/test.json and writes results to eval_results.csv. Reported metrics include:
- Division precision / recall / F1 — how accurately the tracker detects mitotic events
aa_accuracy— assignment accuracy (fraction of cells correctly linked frame-to-frame)te_accuracy— track-end accuracy
To sweep over post-processing thresholds and find the best combination:
python -m tracking.eval_sweepResults are saved to metrics/postprocess_sweep.csv.
The model has two phases: a shared neighborhood encoder and separate training and inference branches.
Neighborhood Encoder (NeighborhoodEncoder)
Each cell in each frame is described by three features:
- Appearance — a 32×32 grayscale crop around the nucleus, encoded by a CNN (
AppearanceEncoder) - Morphology — area, perimeter, eccentricity, encoded by an MLP (
MorphologyEncoder) - Centroid — Y/X position in pixels, encoded by an MLP (
CentroidEncoder)
The three embeddings are summed and passed through a graph attention network (GAT). Edges connect any two cells within distance_threshold pixels of each other, allowing the model to learn context from neighboring cells.
Training Branch (TrainingBranch)
Operates on a window of track_length frames. Splits the sequence into "current" (frames 0:T-1) and "future" (frames 1:T) embeddings. An LSTM integrates the current embeddings over time. Pairwise centroid differences (within-frame and between-frame) are encoded by DeltaEncoder modules and a second LSTM. All features are merged and passed to the decoder, which predicts a temporal adjacency matrix (TAM) of shape (T-1, N_current, N_future, 3) where the 3 classes are: different cell, same cell, mitosis.
Inference Branch (InferenceBranch)
Accepts pre-computed embeddings for existing tracks (from frames up to T-1) and embeddings for cells in the new frame T. Applies the same LSTM and delta encoding, then decodes to produce linking probabilities. The CellTracker solves a Linear Assignment Problem (Hungarian algorithm) on the resulting cost matrix to assign detections to tracks frame by frame.