Features
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
Core Components
1. Feature (TypedDict)
A Feature is a TypedDict that stores metadata about a graph feature.
Show API documentation
Bases: TypedDict
TypedDict for storing metadata associated with a graph feature.
Use factory functions like Time(), Position(), Area() etc. to create features with standard defaults.
The key is stored separately in the FeatureDict mapping (not in the Feature itself).
Attributes:
| Name | Type | Description |
|---|---|---|
feature_type |
FeatureType
|
Which graph elements the feature applies to. A single string ("node" or "edge") or a list of them (e.g. ["node", "edge"]). |
value_type |
ValueType
|
The data type of the feature values. |
num_values |
int
|
The number of values expected for this feature. |
display_name |
str
|
Optional. A display name for the feature. If not provided, the feature key is used. |
value_names |
Sequence[str]
|
Optional. Individual display names for each value when num_values > 1. Length should match num_values. |
default_value |
Any
|
This value is returned whenever the feature value is missing on the graph. |
spatial_dims |
bool
|
Optional. If True, num_values must match the number of spatial dimensions (e.g., 2 for 2D, 3 for 3D). Used for features like Position and EllipsoidAxes. |
derived_features |
list[str]
|
Optional. Feature keys that are derived from this feature and should be cascade-deleted when this feature is removed. For example, SegMask lists the bbox key here so that deleting the mask also deletes the bounding box. |
2. FeatureDict
A FeatureDict is a dictionary (dict[str, Feature]) with special tracking for important feature keys:
Show API documentation
Bases: dict[str, Feature]
A dictionary mapping keys to Features, with special tracking for time/position.
Inherits from dict[str, Feature], so can be used directly as a dictionary. Provides convenient access to time and position features through properties.
Attributes:
| Name | Type | Description |
|---|---|---|
time_key |
str
|
The key used for the time feature |
position_key |
str | list[str] | None
|
The key(s) used for position feature(s) |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
features
|
dict[str, Feature]
|
Mapping from feature keys to Features |
required |
time_key
|
str
|
The key for the time feature (must be in features) |
required |
position_key
|
str | list[str] | None
|
The key(s) for position feature(s) |
required |
tracklet_key
|
str | None
|
The key for the tracklet feature |
required |
lineage_key
|
str | None
|
The key for the lineage feature |
None
|
edge_features
edge_features: dict[str, Feature]
A dict of all features that apply to edges
node_features
node_features: dict[str, Feature]
A dict of all features that apply to nodes
dump_json
dump_json() -> dict
Dump this FeatureDict to a json compatible dictionary
Returns:
| Name | Type | Description |
|---|---|---|
dict |
dict
|
A map from the key "FeatureDict" containing features, time_key, position_key, tracklet_key, and lineage_key |
from_json
from_json(json_dict: dict) -> FeatureDict
Generate a FeatureDict from a json dict such as one generated by dump_json
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
json_dict
|
dict
|
A dictionary with the key "FeatureDict" containing features, time_key, position_key, tracklet_key, and lineage_key |
required |
Returns:
| Name | Type | Description |
|---|---|---|
FeatureDict |
FeatureDict
|
A FeatureDict object containing the features from the dictionary |
register_lineage_feature
register_lineage_feature(
key: str, feature: Feature
) -> None
Register the lineage feature and set the lineage_key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to use for the lineage feature |
required |
feature
|
Feature
|
The Feature to register |
required |
register_position_feature
register_position_feature(
key: str, feature: Feature
) -> None
Register the position feature and set the position_key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to use for the position feature |
required |
feature
|
Feature
|
The Feature to register |
required |
register_tracklet_feature
register_tracklet_feature(
key: str, feature: Feature
) -> None
Register the tracklet feature and set the tracklet_key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key to use for the tracklet feature |
required |
feature
|
Feature
|
The Feature to register |
required |
3. GraphAnnotator (Base Class)
An abstract base class for components that compute and update features on a graph.
Show API documentation
A base class for adding and updating graph features.
This class holds a set of features that it is responsible for. The annotator will compute these features and add them to the Tracks initially, and update them when necessary. The set of features will all be computed and updated together, although individual ones can be removed for efficiency.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tracks
|
Tracks
|
The tracks to manage features for. |
required |
features
|
dict[str, Feature]
|
A dict mapping keys to features that this annotator is capable of computing and updating. |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
all_features |
dict[str, tuple[Feature, bool]]
|
Maps feature keys to (feature, is_included) tuples. Tracks both what can be computed and what is currently being computed. Defaults to computing nothing. |
features
features: dict[str, Feature]
The dict of features that this annotator currently manages.
Filtered from all_features based on inclusion flags.
graph
graph
The graph this annotator iterates over and reads topology/masks from.
Defaults to the full graph. Detection features (pos, area, iou, ...) are
intrinsic to a node/edge — independent of solution membership — so they are
computed for every node/edge, including soft-deleted (solution=False)
candidates, keeping them valid for revive and ready for re-solving.
TrackAnnotator overrides this to graph_solution, since track ids
(tracklet_id, lineage_id) are derived from the solution topology. Root and
view share attribute storage, so writes made here are seen through the solution
view automatically.
KNOWN COST: computing over graph_full means compute()/update() run
regionprops / mask.iou for every candidate (solution=False) node/edge too. On
candidate graphs with many unselected detections (often 10-100x the solution
size) this is O(candidates) work that is mostly never read, since revive is rare.
This is a deliberate eagerness-for-readiness trade; if it becomes a bottleneck,
the lever is lazy compute-on-revive (default to graph_solution and compute a
candidate's intrinsic features only when it enters the solution).
activate_features
activate_features(keys: list[str]) -> None
Activate computation of the given features in the annotation process.
Filters the list to only features this annotator owns, ignoring others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[str]
|
List of feature keys to activate. Only keys in all_features are activated. |
required |
can_annotate
can_annotate(tracks: Tracks) -> bool
Check if this annotator can annotate the given tracks.
Subclasses should override this method to specify their requirements (e.g., segmentation, Tracks, etc.).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tracks
|
Tracks
|
The tracks to check compatibility with |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the annotator can annotate these tracks, False otherwise |
change_key
change_key(old_key: str, new_key: str) -> None
Rename a feature key in this annotator.
Base implementation updates the all_features dictionary. Subclasses should override this method to update any additional internal mappings they maintain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_key
|
str
|
Existing key to rename. |
required |
new_key
|
str
|
New key to replace it with. |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If old_key does not exist in all_features. |
compute
compute(feature_keys: list[str] | None = None) -> None
Compute a set of features and add them to the tracks.
This involves both updating the node/edge attributes on self.graph
and adding the features to the FeatureDict, if necessary. This is distinct
from update to allow more efficient bulk computation of features.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_keys
|
list[str] | None
|
Optional list of specific feature keys to compute. If None, computes all currently active features. Any provided keys not in the currently active set are ignored. |
None
|
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If not implemented in subclass. |
deactivate_features
deactivate_features(keys: list[str]) -> None
Deactivate computation of the given features in the annotation process.
Filters the list to only features this annotator owns, ignoring others.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[str]
|
List of feature keys to deactivate. Only keys in all_features are deactivated. |
required |
update
update(action: BasicAction) -> None
Update a set of features based on the given action.
This involves both updating the node or edge attributes on self.graph
and adding the features to the FeatureDict, if
necessary. This is distinct
from compute to allow more efficient computation of features for single
elements.
The action contains all necessary information about which elements to update (e.g., AddNode.node, AddEdge.edge, UpdateNodeSeg.node).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
BasicAction
|
The action that triggered this update |
required |
Raises:
| Type | Description |
|---|---|
NotImplementedError
|
If not implemented in subclass. |
4. GraphAnnotator Implementations
| 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 | Requires tracks.features.tracklet_key to be set (i.e. a tracking solution with a binary-tree structure) |
tracklet_id, lineage_id |
📚 API |
5. AnnotatorRegistry
A registry that manages multiple GraphAnnotator instances with a unified interface. Extends list[GraphAnnotator].
Show API documentation
Bases: list[GraphAnnotator]
A list of annotators with coordinated operations.
Inherits from list[GraphAnnotator], so can be used directly as a list. Provides coordinated compute/update/enable/disable operations across all annotators.
Example
annotators = AnnotatorRegistry([ RegionpropsAnnotator(tracks, pos_key="centroid"), EdgeAnnotator(tracks), TrackAnnotator(tracks, tracklet_key="tracklet_id"), ])
Can use as a list
annotators.append(MyCustomAnnotator(tracks))
Coordinated operations
annotators.activate_features(["area", "iou"]) annotators.compute()
Initialize with a list of annotators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
annotators
|
list[GraphAnnotator]
|
List of instantiated annotator objects |
required |
all_features
all_features: dict[str, tuple[Feature, bool]]
Dynamically aggregate all_features from all annotators.
Returns:
| Type | Description |
|---|---|
dict[str, tuple[Feature, bool]]
|
Dictionary mapping feature keys to (Feature, is_enabled) tuples |
features
features: dict[str, Feature]
Get all currently active features from all annotators.
Returns:
| Type | Description |
|---|---|
dict[str, Feature]
|
Dictionary mapping feature keys to Feature definitions (only active features) |
activate_features
activate_features(keys: list[str]) -> None
Activate features across all annotators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[str]
|
List of feature keys to activate |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If any feature keys are not available |
change_key
change_key(old_key: str, new_key: str) -> None
Rename a feature key across all annotators.
Finds the annotator that owns the feature and calls its change_key method. This allows the Tracks user to rename features without needing to find the specific annotator that manages it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_key
|
str
|
Existing key to rename. |
required |
new_key
|
str
|
New key to replace it with. |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If old_key does not exist in any annotator. |
compute
compute(feature_keys: list[str] | None = None) -> None
Compute features across all annotators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_keys
|
list[str] | None
|
Optional list of specific feature keys to compute. If None, computes all currently active features. |
None
|
deactivate_features
deactivate_features(keys: list[str]) -> None
Deactivate features across all annotators.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[str]
|
List of feature keys to deactivate |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If any feature keys are not available |
update
update(action: BasicAction) -> None
Update features across all annotators based on the action.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
BasicAction
|
The action that triggered this update |
required |
6. Tracks
The main class representing a set of tracks: a graph + optional segmentation + features.
Show API documentation
A set of tracks consisting of a graph and an optional segmentation.
The graph nodes represent detections and must have a time attribute and position attribute. Edges in the graph represent links across time.
Attributes:
| Name | Type | Description |
|---|---|---|
graph_full |
BaseGraph
|
The full graph (first-class): every node/edge ever known, including soft-deleted (solution=False) candidates. Nodes represent detections, edges represent links across time. |
graph_solution |
GraphView
|
A solution==True view derived from graph_full; the user-visible tracking solution. |
features |
FeatureDict
|
Dictionary of features tracked on graph nodes/edges. |
annotators |
AnnotatorRegistry
|
List of annotators that compute features. |
scale |
list[float] | None
|
How much to scale each dimension by, including time. |
ndim |
int
|
Number of dimensions (3 for 2D+time, 4 for 3D+time). |
Initialize a Tracks object.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
graph
|
BaseGraph
|
the full base graph (graph_full) with nodes as detections and edges as links. Must be a base graph, not a view; graph_solution is built internally as its solution==True view. |
required |
time_attr
|
str | None
|
Graph attribute name for time. Defaults to "time" if None. |
None
|
pos_attr
|
str | tuple[str, ...] | list[str] | None
|
Graph attribute name(s) for position. Can be: - Single string for one attribute containing position array - List/tuple of strings for multi-axis (one attribute per axis) Defaults to "pos" if None. |
None
|
tracklet_attr
|
str | None
|
Graph attribute name for tracklet/track IDs. Defaults to "tracklet_id" if None. Every Tracks gets a TrackAnnotator and track ids (no "plain" vs "solution" distinction); existing ids on the graph are reused, otherwise they are computed. |
None
|
lineage_attr
|
str | None
|
Graph attribute name for lineage IDs. Defaults to "lineage_id" if None. |
None
|
scale
|
list[float] | None
|
Scaling factors for each dimension (including time). If None, all dimensions scaled by 1.0. |
None
|
ndim
|
int | None
|
Number of dimensions (3 for 2D+time, 4 for 3D+time). If None, inferred from segmentation or scale. |
None
|
features
|
FeatureDict | None
|
Pre-built FeatureDict with feature definitions. If provided, time_attr/pos_attr/tracklet_attr are ignored. Assumes that all features in the dict already exist on the graph (will be activated but not recomputed). If None, core computed features (pos, tracklet_id) are auto-detected by checking if they exist on the graph. |
None
|
_segmentation
|
GraphArrayView | None
|
Internal parameter for reusing an existing GraphArrayView instance. Not intended for public use. |
None
|
graph
graph: GraphView
The solution graph view (graph_solution).
.. deprecated::
Use graph_solution directly. graph is kept only for
backward compatibility with code written against funtracks v2.
max_track_id
max_track_id: int
The maximum tracklet id currently in use.
track_annotator
track_annotator: TrackAnnotator
The registered TrackAnnotator. Always present, since track ids are a core feature of every Tracks (_get_annotators registers one unconditionally).
track_id_to_node
track_id_to_node: dict[int, list[int]]
Mapping from tracklet id to the list of node ids with that track id.
add_feature
add_feature(key: str, feature: Feature) -> None
Add a feature to the features dictionary and perform graph operations.
This is the preferred way to add new features as it ensures both the features dictionary is updated and any necessary graph operations are performed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key for the new feature |
required |
feature
|
Feature
|
The Feature object to add |
required |
delete_feature
delete_feature(key: str) -> None
Delete a feature from the features dictionary and perform graph operations.
This is the preferred way to delete features as it ensures both the features dictionary is updated and any necessary graph operations are performed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
str
|
The key for the feature to delete |
required |
disable_features
disable_features(feature_keys: list[str]) -> None
Disable multiple features from computation.
Removes features from annotators and FeatureDict.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_keys
|
list[str]
|
List of feature keys to disable |
required |
Raises:
| Type | Description |
|---|---|
KeyError
|
If any feature is not available (raised by annotators) |
edges
edges()
Return the edge ids of the solution graph as a numpy array.
enable_features
enable_features(
feature_keys: list[str], recompute: bool = True
) -> None
Enable multiple features for computation efficiently.
Adds features to annotators and FeatureDict, optionally computes their values.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
feature_keys
|
list[str]
|
List of feature keys to enable |
required |
recompute
|
bool
|
If True, compute feature values. If False, assume values already exist in graph and just register the feature. |
True
|
Raises:
| Type | Description |
|---|---|
KeyError
|
If any feature is not available (raised by annotators) |
get_available_features
get_available_features() -> dict[str, Feature]
Get all features that can be computed across all annotators.
Returns:
| Type | Description |
|---|---|
dict[str, Feature]
|
Dictionary mapping feature keys to Feature definitions |
get_edge_attr
get_edge_attr(edge: Edge, attr: str)
Get an attribute value for a single edge (resolved on graph_full).
Returns None if the attribute is not registered on the graph.
get_edges_attr
get_edges_attr(edges: Iterable[Edge], attr: str)
Get an attribute value for each of the given edges.
get_lineage_id
get_lineage_id(node) -> int
Get the lineage ID for a node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
The node to get lineage ID for |
required |
Returns:
| Type | Description |
|---|---|
int
|
The lineage ID. |
get_mask
get_mask(
node: Node, mask_key: str = td.DEFAULT_ATTR_KEYS.MASK
) -> Mask | None
Get the segmentation mask associated with a given node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Node
|
The node to get the mask for. |
required |
mask_key
|
str
|
The feature key for the mask column. Defaults to the standard mask key. |
MASK
|
Returns:
| Type | Description |
|---|---|
Mask | None
|
Mask | None: The segmentation mask for the node, or None if no |
Mask | None
|
segmentation is available. |
get_next_lineage_id
get_next_lineage_id() -> int
Return the next available lineage_id.
The max_lineage_id in TrackAnnotator is updated automatically when a node is added or lineage IDs are updated via UpdateTrackIDs.
get_next_track_id
get_next_track_id() -> int
Return the next available track_id.
The max_tracklet_id in TrackAnnotator is updated automatically when a node is added or track IDs are updated via UpdateTrackIDs.
get_node_attr
get_node_attr(node: Node, attr: str)
Get an attribute value for a single node (resolved on graph_full).
get_nodes_attr
get_nodes_attr(nodes: Iterable[Node], attr: str)
Get an attribute value for each of the given nodes.
get_position
get_position(node: Node, incl_time=False) -> list
Get position of a single node. Uses a direct per-node query — do not use in a loop over many nodes; use get_positions() instead.
get_positions
get_positions(
nodes: Iterable[Node], incl_time: bool = False
) -> np.ndarray
Get the positions of nodes in the graph. Optionally include the time frame as the first dimension. Raises an error if any of the nodes are not in the graph.
NOTE: fetches all nodes in the graph internally. Optimised for bulk use. For a single node use get_position() instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
Iterable[Node]
|
The node ids in the graph to get the positions of |
required |
incl_time
|
bool
|
If true, include the time as the first element of each position array. Defaults to False. |
False
|
Returns:
| Type | Description |
|---|---|
ndarray
|
np.ndarray: A N x ndim numpy array holding the positions, where N is the number of nodes passed in |
get_time
get_time(node: Node) -> int
Get the time frame of a given node. Raises an error if the node is not in the graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Any
|
The node id to get the time frame for |
required |
Returns:
| Name | Type | Description |
|---|---|---|
int |
int
|
The time frame that the node is in |
get_times
get_times(nodes: Iterable[Node]) -> Sequence[int]
Batch fetch times for many nodes in one query. NOTE: fetches all nodes in the graph internally. Optimised for bulk use. For a single node use get_time() instead.
get_track_id
get_track_id(node) -> int
Get the tracklet id of a single node.
get_track_ids
get_track_ids(nodes) -> list[int]
Batch version of get_track_id — one query fetching all nodes in the graph. NOTE: always fetches the entire graph internally. Optimised for bulk (all-node) calls. For small subsets or single nodes use get_track_id() instead.
get_track_neighbors
get_track_neighbors(
track_id: int, time: int
) -> tuple[Node | None, Node | None]
Get the last node with the given track id before time, and the first node with the track id after time, if any. Does not assume that a node with the given track_id and time is already in tracks, but it can be.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
track_id
|
int
|
The track id to search for |
required |
time
|
int
|
The time point to find the immediate predecessor and successor for |
required |
Returns:
| Type | Description |
|---|---|
Node | None
|
tuple[Node | None, Node | None]: The last node before time with the given |
Node | None
|
track id, and the first node after time with the given track id, |
tuple[Node | None, Node | None]
|
or Nones if there are no such nodes. |
has_track_id_at_time
has_track_id_at_time(track_id: int, time: int) -> bool
Function to check if a node with given track id exists at given time point.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
track_id
|
int
|
The track id to search for. |
required |
time
|
int
|
The time point to check. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if a node with given track id exists at given time point. |
nodes
nodes()
Return the node ids of the solution graph as a numpy array.
notify_annotators
notify_annotators(action: BasicAction) -> None
Notify annotators about an action so they can recompute affected features.
Delegates to the annotator registry which broadcasts to all annotators. The action contains all necessary information about which elements to update.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
action
|
BasicAction
|
The action that triggered this notification |
required |
predecessors
predecessors(node: int) -> list[int]
Return the predecessors of a node in the solution graph.
redo
redo() -> bool
Redo the last undone action from the action history.
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if an action was redone, False if there were no actions to redo |
set_position
set_position(node: Node, position: list | ndarray)
Set the position of a single node.
set_positions
set_positions(nodes: Iterable[Node], positions: ndarray)
Set the location of nodes in the graph. Optionally include the time frame as the first dimension. Raises an error if any of the nodes are not in the graph.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
Iterable[node]
|
The node ids in the graph to set the location of. |
required |
positions
|
ndarray
|
An (ndim, num_nodes) shape array of positions to set. |
required |
incl_time
|
bool
|
If true, time is the first column and is included in ndim. Defaults to False. |
required |
successors
successors(node: int) -> list[int]
Return the successors of a node in the solution graph.
undo
undo() -> bool
Undo the last performed action from the action history.
Returns:
| Name | Type | Description |
|---|---|---|
bool |
bool
|
True if an action was undone, False if there were no actions to undo |
update_mask
update_mask(
node: Node,
mask: Mask,
mask_key: str = td.DEFAULT_ATTR_KEYS.MASK,
) -> None
Update the segmentation mask for an existing node, auto-syncing the bbox.
Writes both the mask and its bbox to the graph. The bbox key is looked up from the mask Feature's derived_features list.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Node
|
The node to update the mask for. |
required |
mask
|
Mask
|
The Mask object (carries .bbox). |
required |
mask_key
|
str
|
The feature key for the mask column. Defaults to the standard mask key. |
MASK
|
Architecture Diagrams
Class Diagram
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_full: td.graph.BaseGraph
+graph_solution: 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
Feature Keys and Graph Attributes
Two Layers: Metadata vs. Semantics
The Feature TypedDict is pure metadata — it describes how to handle a feature's
data (value type, number of values, display name, spatial dimensions). It does not
describe what the feature means.
The semantic meaning of a feature — "this attribute stores tracklet IDs" or "this
attribute stores positions" — is tracked by special key pointers on FeatureDict:
FeatureDict.time_key— which graph attribute stores timeFeatureDict.position_key— which graph attribute stores positionFeatureDict.tracklet_key— which graph attribute stores tracklet IDsFeatureDict.lineage_key— which graph attribute stores lineage IDs
These pointers are the source of truth for where core semantic concepts live on the graph. The rest of funtracks (export, validation, annotators) reads these pointers rather than assuming hardcoded attribute names.
Default vs. Custom Attribute Names
Each core semantic feature has a default attribute name (e.g., "tracklet_id" for
tracklet IDs, "pos" for position). These defaults are used when no explicit name is
provided:
# Uses default: tracklet_key="tracklet_id"
tracks = Tracks(graph=graph, tracklet_attr="tracklet_id")
# Uses custom attribute name
tracks = Tracks(graph=graph, tracklet_attr="my_track_col")
Custom attribute names are also supported through FeatureDict:
fd = FeatureDict(
features={"my_track_col": TrackletID(), "my_lineage": LineageID()},
tracklet_key="my_track_col",
lineage_key="my_lineage",
)
tracks = Tracks(graph=graph, features=fd)
When a FeatureDict is provided, the tracklet_attr/pos_attr/time_attr arguments
are ignored — the FeatureDict is the sole authority.
Computed Features and Key Names
Annotators (e.g., TrackAnnotator, RegionpropsAnnotator) accept custom key names
in their constructors:
TrackAnnotator(tracks, tracklet_key="my_track", lineage_key="my_lineage")
RegionpropsAnnotator(tracks, pos_key="coordinates")
When constructing Tracks directly, you have full control over
which attribute names are used.
Through the import path (tracks_from_df, import_from_geff), computed features
always use their default key names. The node_name_map controls what the source
column is called, but the builder always renames it to the standard key before adding
it to the graph. For example, {"tracklet_id": "Track ID"} loads the "Track ID"
column and stores it as "tracklet_id" on the graph.
Why Custom Keys Are Useful
Renaming attributes on a graph is O(n) — it requires iterating all nodes. If an
external data source uses "track" as the tracklet column name, a user can construct
a Tracks object that uses "track" directly, avoiding the cost of a rename. The
FeatureDict key pointer tells funtracks where to find the data regardless of the
attribute name.
Initialization Lifecycle
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.
Core Features
These features are automatically checked during initialization:
pos(position): Always auto-detected for RegionpropsAnnotatorarea: Always auto-detected (for backward compatibility)tracklet_id: Always auto-detected for TrackAnnotator
Example Scenarios
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 recomputing
Scenario 2: Creating tracks from raw segmentation
from funtracks.utils import create_empty_graph
from funtracks.data_model import Tracks
# Create empty graph and add nodes
graph = create_empty_graph()
graph.add_node(index=1, attrs={"t": 0})
tracks = Tracks(graph, segmentation=seg)
# Auto-detection: pos, area don't exist → compute them from segmentation
Scenario 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 computed
Scenario 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 features
Scenario 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
Extending the System
Creating a New Annotator
-
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_solution.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()])