Guide

STracking is a python framework to develop particles tracking pipeline. This library has been developed to track intra-cellular object in microscopy 2D+t and 3D+t images, but can be use for any spots tracking application in 2D+t and 3D+t images.

A particles tracking pipeline is decomposed into sequential steps. First the particles are detected individually and independently in each time frame of the image. Then a linker algorithm is used to link particles between frames and form the tracks. Then we calculate the properties of the particles (size, intensity…) and the features of the tracks (length, distance…) to analyse them. A final step is the tracks filtering that uses the properties and features to select tracks of interest.

Library components

The STracking library is made of one module per step of the pipeline. Plus one module for data containers and on module for pipeline:

  • Containers: SParticles and STracks containers based on napari points and track layer data structures to store particles and tracks

  • Detectors: define a detector interface and implementations of particle detection algorithm for 2D and 3D image sequences

  • Linkers: define a linker interface and implementation of particle linkers (or trackers) for 2D and 3D image sequences

  • properties: define an interface and implementations of algorithms to measure properties of particles (intensity…)

  • feature: define an interface and implementations of algorithms to measure tracks properties (length, displacement…)

  • filters: define an interface and implementations of algorithms to select tracks

  • pipeline: Define a class to run a STracking pipeline defined in a json file

Containers

The containers module has two classes SParticles and STracks to facilitate the management of the particles and tracks data and metadata. The containers have been designed to be compatible with the napari layers data structures.

A SParticles object contains a list of particles and their metadata in a 2D+t or 3D+t image. It contains 3 attributes:

dataarray (N, D+1)

Coordinates for N points in D+1 dimensions. ID,T,(Z),Y,X. The first axis is the integer ID of the track. D is either 3 or 4 for planar or volumetric time series respectively.

propertiesdict {str: array (N,)}, DataFrame

Properties for each point. Each property should be an array of length N, where N is the number of points.

scaletuple of float

Scale factors for the image data.

A STracks object contains a list of tracks and their metadata in a 2D+t or 3D+t image. It contains 5 attributes:

dataarray (N, D+1)

Coordinates for N points in D+1 dimensions. ID,T,(Z),Y,X. The first axis is the integer ID of the track. D is either 3 or 4 for planar or volumetric time series respectively.

propertiesdict {str: array (N,)}, DataFrame

Properties for each point. Each property should be an array of length N, where N is the number of points.

graphdict {int: list}

Graph representing associations between tracks. Dictionary defines the mapping between a track ID and the parents of the track. This can be one (the track has one parent, and the parent has >=1 child) in the case of track splitting, or more than one (the track has multiple parents, but only one child) in the case of track merging. See examples/tracks_3d_with_graph.py

features: dict {str: dict}

Properties for each tracks. Each feature should be an map of trackID=feature. Ex: features[‘length’][12]=25.2

scaletuple of float

Scale factors for the image data.

Detectors

SDetector are objects with the same interface. They have a run method that takes a numpy array ( 2D+t or 3D+t image) as an input and returns detections in a SParticles object. The parameters of the detector have to be passed to it constructor.

from stracking.detectors import DoGDetector
...
detector = DoGDetector(min_sigma=4, max_sigma=5, threshold=0.2)
particles = detector.run(image)
...

To create a new detector developers just need to inherit SDetector:

from stracking.detectors import SDetector

class MyDetector(SDetector):
    def __init__(self):
        super().__init__()

    def run(self, image, scale=None):
        # Implement the detector here
        spots_ = ...
        return SParticles(data=spots_, properties={}, scale=scale)

Linkers

SLinker are objects with the same interface. They have a run method that takes the detections (in a SParticles object) and optionally a numpy array (the 2D+t or 3D+t image), and return the calculated tracks in a STracks object. The parameters of a linker have to be passed in the constructor. For example, the SPLinker (Shortest Path) linker need a cost function, and a frame gap parameters:

from stracking.linkers import SPLinker, EuclideanCost
...
euclidean_cost = EuclideanCost(max_cost=3000)
my_tracker = SPLinker(cost=euclidean_cost, gap=1)
tracks = my_tracker.run(particles)
...

To create a new linker developers just need to inherit SLinker:

from stracking.linkers import SDetector

class MyLinker(SLinker):
    def __init__(self, cost=None):
        super().__init__(cost)

    def run(self, particles, image=None):
        # Implement the linker here
        mydata = ...
        return STracks(data=mydata, properties=None, graph={}, scale=particles.scale)

Properties

SProperty based objects are objects with the same interface. They have a run method that takes the detections (in a SParticles object) and a numpy array (the 2D+t or 3D+t image), and returns the input SParticles where the calculated properties have been added to the SParticles.properties dictionary. All the SProperty parameters have to be send to the constructor. Here is an example with the IntensityProperty algorithm that calculate the min, max, mean and std intensities inside the spots using a given radius:

from stracking.properties import IntensityProperty
...
property_calc = IntensityProperty(radius=2)
property_calc.run(particles, image)
...

To create a new property developers just need to inherit SProperty:

from stracking.properties import SProperty

class MyProperty(SProperty):
    def __init__(self, radius):
        super().__init__()

    def run(self, sparticles, image):
        # Calculate here some properties and add them to sparticles.properties
        ...
        return sparticles

Features

SFeature based objects are objects with the same interface. They have a run method that takes the tracks (in a STRacks object) and optionally a numpy array (the 2D+t or 3D+t image), and returns the input STracks object where the calculated features have been added to the STracks.features dictionary. Here is an example of the DistanceFeature that calculate the distance a particle moved:

from stracking.filters import DistanceFeature
...
feature_calc = DistanceFeature()
feature_calc.run(tracks)
...

To create a new feature developers just need to inherit SFeature:

from stracking.features import SFeature

class MyFeature(SFeature):
    def __init__(self):
        super().__init__()

    def run(self, stracks, image=None):
        # Calculate here some features and add them to stracks.features
        ...
        return stracks

filters

SFilter based objects are objects with the same interface. The have a Run method that takes the tracks (in a STRacks object) as input and return the same tracks object where filtered tracks have been removed:

from stracking.filters import FeatureFilter
...
filter_calc = FeatureFilter(feature_name='distance', min_val='20', max_val='120')
filter_calc.run(tracks)

To create a new filter developers just need to inherit SFilter:

from stracking.filters import SFilter

class FeatureFilter(STracksFilter):
    def __init__(self):
        super().__init__()

    def run(self, stracks):
        # Implement here the algorithm to select some tracks
        new_stracks = ...
        return new_stracks

Read and Write

The STracking library provides an extra module called io. It allows to read tracks data from many formats (JSON, CSV, Icy xml, ISBI xml, TrackMate xml…) and write the tracks in JSON format. To read a file, you can use the convenient method read_tracks that takes the path of an input file and return a STracks object:

from stracking.io import read_tracks
tracks = read_tracks('path/to/the/tracks/file.xml'))

You can also alternatively call the IO class from the dedicated format. Read tracks are then available in the tracks attribute of the IO object.

from stracking.io import TrackMateIO

trackmate_reader = TrackMateIO('path/to/the/trackmate/model/file.xml')
trackmate_reader.read()
print(trackmate_reader.stracks.data)

To write STracks into a file, the current version of STracking only support the JSON format from the native stracking IO class:

from stracking.io import StIO
...
writer = StIO('path/to/the/tracks/file.json')
writer.write(mytracks)
...

a more convenient function is the write_tracks function:

from stracking.io import write_tracks
...
write_tracks('path/to/the/tracks/file.json', mytracks)
...

It is also possible to save the particles in a file. The supported format is a CSV file where each columns is a particle property. Mandatory properties are ‘T’, ‘Y’, ‘X’ coordinates for 2D+t particles and ‘T’, ‘Z’, ‘Y’, ‘X’ coordinates for 3D+t particles. To write particles to file you can use the write_particles function: .. code-block:: python

from stracking.io import write_particles … write_particles(‘path/to/the/tracks/file.csv’, particles) …

And to read particles, the read_particles function:

from stracking.io import read_particles
...
particles read_particles('path/to/the/tracks/file.csv')
...

Pipeline

Writing a tracking pipeline with STracking is straightforward. You just need to call the different modules in a sequence:

from stracking.data import fake_tracks1
from stracking.detectors import DoGDetector
from stracking.linkers import SPLinker, EuclideanCost
from stracking.features import DistanceFeature
from stracking.filters import FeatureFilter
from stracking.io import write_tracks
import napari

# Load data
image = fake_tracks1()

# Open napari
viewer = napari.Viewer(axis_labels='tyx')
viewer.add_image(image, contrast_limits=[0, 300])

# Detection
detector = DoGDetector(min_sigma=3, max_sigma=5, threshold=0.2)
particles = detector.run(image)

# Display spots
viewer.add_points(particles.data, size=4, face_color="red", edge_color="red", blending='opaque')

# Linking
euclidean_cost = EuclideanCost(max_cost=3000)
my_tracker = SPLinker(cost=euclidean_cost, gap=1)
tracks = my_tracker.run(particles)

# Display tracks
viewer.add_tracks(tracks.data, name='Tracks', colormap="hsv")

# Calculate distance feature
feature_calc = DistanceFeature()
feature_calc.run(tracks)

# Keep only tracks that moves less than 60 pixels
filter_calc = FeatureFilter(feature_name='distance', min_val=20, max_val=60)
filter_calc.run(tracks)

# Display filtered tracks
viewer.add_tracks(tracks.data, name='Filtered Tracks',colormap="hsv")
napari.run()

# Save the tracks
write_tracks('path/to/the/tracks/file.json', tracks)

The STracking library also provides a STrackingPipeline class that allows to run a tracking pipeline from a pipeline description file (JSON format):

{
  "name": "pipeline1",
  "author": "Sylvain Prigent",
  "date": "2022-04-13",
  "stracking_version": "0.1.8",
  "steps": {
    "detector": {
      "name": "DoGDetector",
      "parameters": {
        "min_sigma": 4,
        "max_sigma": 5,
        "sigma_ratio": 1.1,
        "threshold": 0.15,
        "overlap": 0
      }
    },
    "linker": {
      "name": "SPLinker",
      "cost": {
          "name": "EuclideanCost",
          "parameters": {}
      },
      "parameters": {
        "gap": 1,
        "min_track_length": 2
      }
    },
    "properties": [
      {
        "name": "IntensityProperty",
        "parameters": {
          "radius": 2.5
        }
      }
    ],
    "features": [
      {
        "name": "LengthFeature"
      },
      {
        "name": "DistanceFeature"
      },
      {
        "name": "DisplacementFeature"
      }
    ],
    "filters": [
      {
        "name": "FeatureFilter",
        "parameters": {
          "feature_name": "distance",
          "min_val": 20,
          "max_val": 60
        }
      }
    ]
  }
}

Then, a pipeline can be run with the STrackingPipeline class

from stracking.data import fake_tracks1
from stracking.io import write_tracks
from stracking.pipelines import STrackingPipeline
import napari

# Load data
image = fake_tracks1()

# Run pipeline
pipeline = STrackingPipeline()
pipeline.load('path/to/the/pipeline.json')
tracks = pipeline.run(image)

# display
viewer = napari.Viewer(axis_labels='tyx')
viewer.add_image(image, contrast_limits=[0, 300])
viewer.add_tracks(tracks.data, name='Pipeline Tracks',colormap="hsv")
napari.run()

# save
write_tracks('pipeline_tracks.csv', tracks, format_='csv')