Funtracks has a feature computation system that manages attributes (features) from tracking graphs with optional segmentation data. The architecture separates:
- Feature metadata (what features exist and their properties) -
Featureclass - Feature computation (how to calculate feature values) -
GraphAnnotatorclass - Feature storage (where feature values live on the graph) - attributes on the graph nodes/edges
- Feature lifecycle (when to compute, activate, or update features) - computation called by
Tracks, updates triggered byBasicActions
A Feature is a TypedDict that stores metadata about a graph feature.
??? "Show API documentation"
::: funtracks.features.Feature
options:
heading_level: 4
show_root_heading: false
show_source: false
A FeatureDict is a dictionary (dict[str, Feature]) with special tracking for important feature keys:
??? "Show API documentation"
::: funtracks.features.FeatureDict
options:
heading_level: 4
show_root_heading: false
show_source: false
An abstract base class for components that compute and update features on a graph.
??? "Show API documentation"
::: funtracks.annotators.GraphAnnotator
options:
heading_level: 4
show_root_heading: false
show_source: false
| Annotator | Purpose | Requirements | Features Computed | API Reference |
|---|---|---|---|---|
| RegionpropsAnnotator | Extracts node features from segmentation using scikit-image's regionprops |
segmentation must not be None |
pos, area, ellipse_axis_radii, circularity, perimeter |
📚 API |
| EdgeAnnotator | Computes edge features based on segmentation overlap between consecutive time frames | segmentation must not be None |
iou (Intersection over Union) |
📚 API |
| TrackAnnotator | Computes tracklet and lineage IDs for SolutionTracks | Must be used with SolutionTracks (binary tree structure) |
tracklet_id, lineage_id |
📚 API |
A registry that manages multiple GraphAnnotator instances with a unified interface. Extends list[GraphAnnotator].
??? "Show API documentation"
::: funtracks.annotators.AnnotatorRegistry
options:
heading_level: 4
show_root_heading: false
show_source: false
The main class representing a set of tracks: a graph + optional segmentation + features.
??? "Show API documentation"
::: funtracks.data_model.Tracks
options:
heading_level: 4
show_root_heading: false
show_source: false
classDiagram
class Feature {
<<TypedDict>>
+feature_type: Literal
+value_type: Literal
+num_values: int
+display_name: str|Sequence
+default_value: Any
}
class FeatureDict {
+time_key: str
+position_key: str|list|None
+tracklet_key: str|None
+node_features: dict
+edge_features: dict
+register_position_feature()
+register_tracklet_feature()
}
class GraphAnnotator {
<<abstract>>
+tracks: Tracks
+all_features: dict
+features: dict
+can_annotate()*
+activate_features()
+deactivate_features()
+compute()*
+update()*
}
class RegionpropsAnnotator {
+pos_key: str
+area_key: str
+compute()
+update()
}
class EdgeAnnotator {
+iou_key: str
+compute()
+update()
}
class TrackAnnotator {
+tracklet_key: str
+lineage_key: str
+tracklet_id_to_nodes: dict
+lineage_id_to_nodes: dict
+compute()
+update()
}
class AnnotatorRegistry {
+all_features: dict
+features: dict
+activate_features()
+deactivate_features()
+compute()
+update()
}
class Tracks {
+graph: td.graph.GraphView
+segmentation: ndarray|None
+features: FeatureDict
+annotators: AnnotatorRegistry
+scale: list|None
+ndim: int
+enable_features()
+disable_features()
+get_available_features()
+notify_annotators()
}
FeatureDict *-- Feature : stores many
GraphAnnotator <|-- RegionpropsAnnotator : implements
GraphAnnotator <|-- EdgeAnnotator : implements
GraphAnnotator <|-- TrackAnnotator : implements
AnnotatorRegistry o-- GraphAnnotator : aggregates many
Tracks *-- FeatureDict : has one
Tracks *-- AnnotatorRegistry : has one
GraphAnnotator --> Tracks : references
Here's what happens when you create a Tracks instance.
tracks = Tracks(graph, segmentation, ndim=3)- Basic Attribute Setup - save graph, segmentation, scale, etc. as instance variables
- FeatureDict Creation - If features parameter is provided, use the provided FeatureDict and assume all features already exist on the graph. If features=None, create a FeatureDict with static features (time) and provided keys.
- AnnotatorRegistry Creation - build an
AnnotatorRegistrycontaining any Annotators that work on the provided tracks - Core Computed Features Setup - If features parameter provided, activate all computed features with keys in the features dictionary, so that updates will be computed. Does not compute any features from scratch. Otherwise, try to detect which core features are already present, activate those, and compute any missing ones from scratch.
These features are automatically checked during initialization:
pos(position): Always auto-detected for RegionpropsAnnotatorarea: Always auto-detected (for backward compatibility)track_id(tracklet_id): Always auto-detected for TrackAnnotator
Scenario 1: Loading tracks from CSV with pre-computed features
from funtracks.import_export import tracks_from_df
# CSV/DataFrame has columns: id, time, y, x, area, track_id, parent_id
tracks = tracks_from_df(df, segmentation=seg)
# Auto-detection: pos, area, track_id exist → activate without recomputingScenario 2: Creating tracks from raw segmentation
from funtracks.utils import create_empty_graphview_graph
from funtracks.data_model import Tracks
# Create empty graph and add nodes
graph = create_empty_graphview_graph()
graph.add_node(index=1, attrs={"t": 0})
tracks = Tracks(graph, segmentation=seg)
# Auto-detection: pos, area don't exist → compute them from segmentationScenario 3: Explicit feature control with FeatureDict
from funtracks.features import FeatureDict, Time, Position, Area
from funtracks.data_model import Tracks
# Bypass auto-detection entirely
feature_dict = FeatureDict({"t": Time(), "pos": Position(), "area": Area()})
tracks = Tracks(graph, segmentation=seg, features=feature_dict)
# All features in feature_dict are activated, none are computedScenario 4: Enable a new feature
from funtracks.data_model import Tracks
tracks = Tracks(graph, segmentation)
# Initially has: time, pos, area (auto-detected or computed)
tracks.enable_features(["iou", "circularity"])
# Now has: time, pos, area, iou, circularity
# Check active features
print(tracks.features.keys()) # All features in FeatureDict (including static)
print(tracks.annotators.features.keys()) # Only active computed featuresScenario 5: Disable a feature
tracks.disable_features(["area"])
# Removes from FeatureDict, deactivates in annotators
# Note: Doesn't delete values from graph, just stops computing/updating-
Subclass GraphAnnotator:
from funtracks.annotators import GraphAnnotator class MyCustomAnnotator(GraphAnnotator): @classmethod def can_annotate(cls, tracks): # Check if this annotator can handle these tracks return tracks.some_condition def __init__(self, tracks, custom_key="custom"): super().__init__(tracks) self.custom_key = custom_key # Register features self.all_features[custom_key] = (CustomFeature(), False) def compute(self, feature_keys=None): # Compute feature values in bulk if "custom" in self.features: for node in self.tracks.graph.node_ids(): value = self._compute_custom(node) self.tracks[node]["custom"] = value def update(self, action): # Incremental update when graph changes if "custom" in self.features: if isinstance(action, SomeActionType): # Recompute only for affected nodes pass
-
Register in Tracks._get_annotators():
if MyCustomAnnotator.can_annotate(tracks): ann = MyCustomAnnotator(tracks) tracks.annotators.append(ann) tracks.enable_features([key for key in ann.all_features()])