Source code for ddd_pylib.tracking._tracking_base

from ._base_tracking_strategy import BaseTrackingStrategy


[docs] class TrackingBase: """ Tracking in ddd-pylib ===================== This block of documentation covers the tracking architecture in ddd-pylib. It is intended for developers who want to understand how tracking is implemented and how to implement new tracking strategies. You are not supposed to use this class directly if you are a user of ddd-pylib. Instead, you should use the `ObjectsTrackingOperator` or `PointsTrackingOperator` classes, which provide a higher-level interface for tracking objects or points in images. .. uml:: /_diagrams/tracking.puml :align: center :caption: Architecture of the tracking module I. What to track? ----------------- There are two things that we possibly need to track: objects (labels) and points (coordinates): - When we track **points**, the input should be a DataFrame of coordinates with a potential original identity and the output will be a DataFrame of the same shape but with a new identity (track_id) for each point. - When tracking **objects** (labels), the input should be an nD+t labeled image and the output should be the exact same image but with consistent values for labels at each time point. Both these tasks are very close, and it would be an implementation flaw to have them as totally independent classes. II. Interfacing with Napari --------------------------- The point of :code:`ddd-pylib` is to be a stand-alone library to be used to build new operators and to be easily interfaced through Napari. To do this, we need to conciliate the way Napari handles images and points with the way tracking libraries expect data to be formatted. On one hand: Napari ~~~~~~~~~~~~~~~~~~~ In Napari, when images and points are used at the same time on two layers from the same viewer, points data is expressed in pixel-coordinates and the scale of the layer puts them in the correct location over the image. We end up with integer coordinates for time and floating coordinates (sub-pixel resolution) for space. On the other hand: tracking libraries ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The tracking libraries force us to use integer coordinates for time, and the **memory** is expected to be a number of frames (integer) instead of a physical time unit (e.g. seconds). When it comes to space, parameters such as the searching distance are expressed in physical units (e.g. micrometers), but the coordinates of points need to be expressed in the same unit, contrary to what we get from a typical points layer in Napari. Resolution ~~~~~~~~~~ In the :code:`PointsTrackingOperator` coordinates are received in pixels and converted to physical units using the calibration of the image. The memory is also converted to frames using the calibration of the image. So the user can express the memory in physical units and the searching distance in physical units, and the operator will convert them to the correct units for the tracking library. The opposite conversion is done when the result is returned, so the user gets back a DataFrame with coordinates in pixels and time in frames. III. Strategy pattern --------------------- There is not a single way to track objects or points, and different tracking methods available through different libraries have different strengths and weaknesses. The common point shared by most of them is that they all end up taking a DataFrame of coordinates and returning a DataFrame of linked coordinates with a new identity (track_id) for each point. We rely on that and the **strategy** pattern to implement new tracking methods. A strategy is a class that implements the :code:`BaseTrackingStrategy` interface and can be used by the :code:`PointsTrackingOperator` or :code:`ObjectsTrackingOperator` classes to perform tracking. Objects tracking relies on the points tracking and is simply some kind of wrapper. It starts by converting the labels to points, then it uses the points tracking strategy to link the points, and finally it relabels the original image with the new track_id. Tracking strategies don't own the data (the operator does) but are able to interrogate the operator through accessors. Each strategy is supposed to only know the settings required for its own algorithm. The goal is to avoid having a god class that knows every possible parameters for every possible tracking algorithm. IV. Implementation ------------------ The :code:`TrackingBase` class is a mixin that provides the common interface for tracking operators. Basically, it only contains the storage of the strategy and the accessors to get and set it. The :code:`ObjectsTrackingOperator` takes a labeled image and produces a lebeled image so it inherits from :code:`ImageOperator` and :code:`TrackingBase`. The :code:`PointsTrackingOperator` takes a DataFrame of coordinates and produces a DataFrame of linked coordinates so it inherits from :code:`PointsOperator` and :code:`TrackingBase`. To add a new tracking algorithm, you simply need to make a new class that inherits from :code:`BaseTrackingStrategy` and implements the :code:`_linkTracks` method. """ def __init__(self): self._strategy = None
[docs] def getStrategy(self) -> BaseTrackingStrategy: if self._strategy is None: raise ValueError("Tracking strategy has not been set.") return self._strategy
[docs] def setStrategy(self, s: BaseTrackingStrategy): self._strategy = s