Skip to content

funtracks.data_model.tracks

Classes:

  • Tracks

    A set of tracks consisting of a graph and an optional segmentation.

Tracks

Tracks(graph: nx.DiGraph, segmentation: np.ndarray | None = None, time_attr: str = NodeAttr.TIME.value, pos_attr: str | tuple[str] | list[str] = NodeAttr.POS.value, scale: list[float] | None = None, ndim: int | None = None)

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:

  • graph (DiGraph) –

    A graph with nodes representing detections and and edges representing links across time.

  • segmentation (Optional(np.ndarray) –

    An optional segmentation that accompanies the tracking graph. If a segmentation is provided, the node ids in the graph must match the segmentation labels. Defaults to None.

  • time_attr (str) –

    The attribute in the graph that specifies the time frame each node is in.

  • pos_attr (str | tuple[str] | list[str]) –

    The attribute in the graph that specifies the position of each node. Can be a single attribute that holds a list, or a list of attribute keys.

For bulk operations on attributes, a KeyError will be raised if a node or edge in the input set is not in the graph. All operations before the error node will be performed, and those after will not.

Methods:

  • add_node

    Add a node to the graph. Will update the internal mappings and generate the

  • add_nodes

    Add a set of nodes to the tracks object. Includes computing node attributes

  • get_area

    Get the area/volume of a given node. Raises a KeyError if the node

  • get_areas

    Get the area/volume of a given node. Raises a KeyError if the node

  • get_pixels

    Get the pixels corresponding to each node in the nodes list.

  • get_positions

    Get the positions of nodes in the graph. Optionally include the

  • get_time

    Get the time frame of a given node. Raises an error if the node

  • load

    Load a Tracks object from the given directory. Looks for files

  • remove_node

    Remove the node from the graph.

  • save

    Save the tracks to the given directory.

  • set_pixels

    Set the given pixels in the segmentation to the given value.

  • set_positions

    Set the location of nodes in the graph. Optionally include the

  • set_time

    Set the time frame of a given node. Raises an error if the node

  • update_segmentations

    Updates the segmentation of the given nodes. Also updates the

Source code in src/funtracks/data_model/tracks.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(
    self,
    graph: nx.DiGraph,
    segmentation: np.ndarray | None = None,
    time_attr: str = NodeAttr.TIME.value,
    pos_attr: str | tuple[str] | list[str] = NodeAttr.POS.value,
    scale: list[float] | None = None,
    ndim: int | None = None,
):
    self.graph = graph
    self.segmentation = segmentation
    self.time_attr = time_attr
    self.pos_attr = pos_attr
    self.scale = scale
    self.ndim = self._compute_ndim(segmentation, scale, ndim)

add_node

add_node(node: Node, time: int, position: Sequence | None = None, attrs: Attrs | None = None)

Add a node to the graph. Will update the internal mappings and generate the segmentation-controlled attributes if there is a segmentation present. The segmentation should have been previously updated, otherwise the attributes will not update properly.

Parameters:

  • node

    (Node) –

    The node id to add

  • time

    (int) –

    the time frame of the node to add

  • position

    (Sequence | None, default: None ) –

    The spatial position of the node (excluding time). Can be None if it should be automatically detected from the segmentation. Either segmentation or position must be provided. Defaults to None.

  • attrs

    (Attrs | None, default: None ) –

    The additional attributes to add to node. Defaults to None.

Source code in src/funtracks/data_model/tracks.py
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def add_node(
    self,
    node: Node,
    time: int,
    position: Sequence | None = None,
    attrs: Attrs | None = None,
):
    """Add a node to the graph. Will update the internal mappings and generate the
    segmentation-controlled attributes if there is a segmentation present.
    The segmentation should have been previously updated, otherwise the
    attributes will not update properly.

    Args:
        node (Node): The node id to add
        time (int): the time frame of the node to add
        position (Sequence | None): The spatial position of the node (excluding time).
            Can be None if it should be automatically detected from the segmentation.
            Either segmentation or position must be provided. Defaults to None.
        attrs (Attrs | None, optional): The additional attributes to add to node.
            Defaults to None.
    """
    pos = np.expand_dims(position, axis=0) if position is not None else None
    attributes: dict[str, list[Any]] | None = (
        {key: [val] for key, val in attrs.items()} if attrs is not None else None
    )
    self.add_nodes([node], [time], positions=pos, attrs=attributes)

add_nodes

add_nodes(nodes: Iterable[Node], times: Iterable[int], positions: np.ndarray | None = None, attrs: Attrs | None = None)

Add a set of nodes to the tracks object. Includes computing node attributes (position, area) from the segmentation if there is one. Does not include setting the segmentation pixels - assumes this is already done.

Parameters:

  • nodes

    (Iterable[Node]) –

    node ids to add

  • times

    (Iterable[int]) –

    times of nodes to add

  • positions

    (ndarray | None, default: None ) –

    The positions to set for each node, if no segmentation is present. If segmentation is present, these provided values will take precedence over the computed centroids. Defaults to None.

  • attrs

    (Attrs | None, default: None ) –

    The additional attributes to add to each node. Defaults to None.

Raises:

  • ValueError

    If neither positions nor segmentations are provided

Source code in src/funtracks/data_model/tracks.py
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
def add_nodes(
    self,
    nodes: Iterable[Node],
    times: Iterable[int],
    positions: np.ndarray | None = None,
    attrs: Attrs | None = None,
):
    """Add a set of nodes to the tracks object. Includes computing node attributes
    (position, area) from the segmentation if there is one. Does not include setting
    the segmentation pixels - assumes this is already done.

    Args:
        nodes (Iterable[Node]): node ids to add
        times (Iterable[int]): times of nodes to add
        positions (np.ndarray | None, optional): The positions to set for each node,
            if no segmentation is present. If segmentation is present, these provided
            values will take precedence over the computed centroids. Defaults to None.
        attrs (Attrs | None, optional): The additional attributes to add to each node.
            Defaults to None.

    Raises:
        ValueError: If neither positions nor segmentations are provided
    """
    if attrs is None:
        attrs = {}
    self.graph.add_nodes_from(nodes)
    self.set_times(nodes, times)
    final_pos: np.ndarray
    if self.segmentation is not None:
        computed_attrs = self._compute_node_attrs(nodes, times)
        if positions is None:
            final_pos = np.array(computed_attrs[NodeAttr.POS.value])
        else:
            final_pos = positions
        attrs[NodeAttr.AREA.value] = computed_attrs[NodeAttr.AREA.value]
    elif positions is None:
        raise ValueError("Must provide positions or segmentation and ids")
    else:
        final_pos = positions

    self.set_positions(nodes, final_pos)
    for attr, values in attrs.items():
        self._set_nodes_attr(nodes, attr, values)

get_area

get_area(node: Node) -> int | None

Get the area/volume of a given node. Raises a KeyError if the node is not in the graph. Returns None if the given node does not have an Area attribute.

Parameters:

  • node

    (Node) –

    The node id to get the area/volume for

Returns:

  • int ( int | None ) –

    The area/volume of the node

Source code in src/funtracks/data_model/tracks.py
298
299
300
301
302
303
304
305
306
307
308
309
def get_area(self, node: Node) -> int | None:
    """Get the area/volume of a given node. Raises a KeyError if the node
    is not in the graph. Returns None if the given node does not have an Area
    attribute.

    Args:
        node (Node): The node id to get the area/volume for

    Returns:
        int: The area/volume of the node
    """
    return self.get_areas([node])[0]

get_areas

get_areas(nodes: Iterable[Node]) -> Sequence[int | None]

Get the area/volume of a given node. Raises a KeyError if the node is not in the graph. Returns None if the given node does not have an Area attribute.

Parameters:

  • node

    (Node) –

    The node id to get the area/volume for

Returns:

  • int ( Sequence[int | None] ) –

    The area/volume of the node

Source code in src/funtracks/data_model/tracks.py
285
286
287
288
289
290
291
292
293
294
295
296
def get_areas(self, nodes: Iterable[Node]) -> Sequence[int | None]:
    """Get the area/volume of a given node. Raises a KeyError if the node
    is not in the graph. Returns None if the given node does not have an Area
    attribute.

    Args:
        node (Node): The node id to get the area/volume for

    Returns:
        int: The area/volume of the node
    """
    return self._get_nodes_attr(nodes, NodeAttr.AREA.value)

get_pixels

get_pixels(nodes: Iterable[Node]) -> list[tuple[np.ndarray, ...]] | None

Get the pixels corresponding to each node in the nodes list.

Parameters:

  • nodes

    (list[Node]) –

    A list of node to get the values for.

Returns:

  • list[tuple[ndarray, ...]] | None

    list[tuple[np.ndarray, ...]] | None: A list of tuples, where each tuple

  • list[tuple[ndarray, ...]] | None

    represents the pixels for one of the input nodes, or None if the segmentation

  • list[tuple[ndarray, ...]] | None

    is None. The tuple will have length equal to the number of segmentation

  • list[tuple[ndarray, ...]] | None

    dimensions, and can be used to index the segmentation.

Source code in src/funtracks/data_model/tracks.py
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
def get_pixels(self, nodes: Iterable[Node]) -> list[tuple[np.ndarray, ...]] | None:
    """Get the pixels corresponding to each node in the nodes list.

    Args:
        nodes (list[Node]): A list of node to get the values for.

    Returns:
        list[tuple[np.ndarray, ...]] | None: A list of tuples, where each tuple
        represents the pixels for one of the input nodes, or None if the segmentation
        is None. The tuple will have length equal to the number of segmentation
        dimensions, and can be used to index the segmentation.
    """
    if self.segmentation is None:
        return None
    pix_list = []
    for node in nodes:
        time = self.get_time(node)
        loc_pixels = np.nonzero(self.segmentation[time] == node)
        time_array = np.ones_like(loc_pixels[0]) * time
        pix_list.append((time_array, *loc_pixels))
    return pix_list

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.

Parameters:

  • node

    (Iterable[Node]) –

    The node ids in the graph to get the positions of

  • incl_time

    (bool, default: False ) –

    If true, include the time as the first element of each position array. Defaults to False.

Returns:

  • ndarray

    np.ndarray: A N x ndim numpy array holding the positions, where N is the number of nodes passed in

Source code in src/funtracks/data_model/tracks.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
def get_positions(
    self, 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.

    Args:
        node (Iterable[Node]): The node ids in the graph to get the positions of
        incl_time (bool, optional): If true, include the time as the
            first element of each position array. Defaults to False.

    Returns:
        np.ndarray: A N x ndim numpy array holding the positions, where N is the
            number of nodes passed in
    """
    if isinstance(self.pos_attr, tuple | list):
        positions = np.stack(
            [
                self._get_nodes_attr(nodes, dim, required=True)
                for dim in self.pos_attr
            ],
            axis=1,
        )
    else:
        positions = np.array(
            self._get_nodes_attr(nodes, self.pos_attr, required=True)
        )

    if incl_time:
        times = np.array(self._get_nodes_attr(nodes, self.time_attr, required=True))
        positions = np.c_[times, positions]

    return positions

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:

  • node

    (Any) –

    The node id to get the time frame for

Returns:

  • int ( int ) –

    The time frame that the node is in

Source code in src/funtracks/data_model/tracks.py
149
150
151
152
153
154
155
156
157
158
159
def get_time(self, node: Node) -> int:
    """Get the time frame of a given node. Raises an error if the node
    is not in the graph.

    Args:
        node (Any): The node id to get the time frame for

    Returns:
        int: The time frame that the node is in
    """
    return int(self.get_times([node])[0])

load classmethod

load(directory: Path, seg_required=False) -> Tracks

Load a Tracks object from the given directory. Looks for files in the format generated by Tracks.save.

Parameters:

  • directory

    (Path) –

    The directory containing tracks to load

  • seg_required

    (bool, default: False ) –

    If true, raises a FileNotFoundError if the segmentation file is not present in the directory. Defaults to False.

Returns:

  • Tracks ( Tracks ) –

    A tracks object loaded from the given directory

Source code in src/funtracks/data_model/tracks.py
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
@classmethod
def load(cls, directory: Path, seg_required=False) -> Tracks:
    """Load a Tracks object from the given directory. Looks for files
    in the format generated by Tracks.save.

    Args:
        directory (Path): The directory containing tracks to load
        seg_required (bool, optional): If true, raises a FileNotFoundError if the
            segmentation file is not present in the directory. Defaults to False.

    Returns:
        Tracks: A tracks object loaded from the given directory
    """
    graph_file = directory / cls.GRAPH_FILE
    graph = cls._load_graph(graph_file)

    seg_file = directory / cls.SEG_FILE
    seg = cls._load_seg(seg_file, seg_required=seg_required)

    attrs_file = directory / cls.ATTRS_FILE
    attrs = cls._load_attrs(attrs_file)

    return cls(graph, seg, **attrs)

remove_node

remove_node(node: Node)

Remove the node from the graph. Does not update the segmentation if present.

Parameters:

  • node

    (Node) –

    The node to remove from the graph

Source code in src/funtracks/data_model/tracks.py
250
251
252
253
254
255
256
257
def remove_node(self, node: Node):
    """Remove the node from the graph.
    Does not update the segmentation if present.

    Args:
        node (Node): The node to remove from the graph
    """
    self.remove_nodes([node])

save

save(directory: Path)

Save the tracks to the given directory. Currently, saves the graph as a json file in networkx node link data format, saves the segmentation as a numpy npz file, and saves the time and position attributes and scale information in an attributes json file.

Parameters:

  • directory

    (Path) –

    The directory to save the tracks in.

Source code in src/funtracks/data_model/tracks.py
410
411
412
413
414
415
416
417
418
419
420
421
def save(self, directory: Path):
    """Save the tracks to the given directory.
    Currently, saves the graph as a json file in networkx node link data format,
    saves the segmentation as a numpy npz file, and saves the time and position
    attributes and scale information in an attributes json file.

    Args:
        directory (Path): The directory to save the tracks in.
    """
    self._save_graph(directory)
    self._save_seg(directory)
    self._save_attrs(directory)

set_pixels

set_pixels(pixels: Iterable[tuple[np.ndarray, ...]], values: Iterable[int | None])

Set the given pixels in the segmentation to the given value.

Parameters:

  • pixels

    (Iterable[tuple[ndarray]]) –

    The pixels that should be set, formatted like the output of np.nonzero (each element of the tuple represents one dimension, containing an array of indices in that dimension). Can be used to directly index the segmentation.

  • value

    (Iterable[int | None]) –

    The value to set each pixel to

Source code in src/funtracks/data_model/tracks.py
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
def set_pixels(
    self, pixels: Iterable[tuple[np.ndarray, ...]], values: Iterable[int | None]
):
    """Set the given pixels in the segmentation to the given value.

    Args:
        pixels (Iterable[tuple[np.ndarray]]): The pixels that should be set,
            formatted like the output of np.nonzero (each element of the tuple
            represents one dimension, containing an array of indices in that dimension).
            Can be used to directly index the segmentation.
        value (Iterable[int | None]): The value to set each pixel to
    """
    if self.segmentation is None:
        raise ValueError("Cannot set pixels when segmentation is None")
    for pix, val in zip(pixels, values, strict=False):
        if val is None:
            raise ValueError("Cannot set pixels to None value")
        self.segmentation[pix] = val

set_positions

set_positions(nodes: Iterable[Node], positions: np.ndarray, incl_time: bool = False)

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:

  • nodes

    (Iterable[node]) –

    The node ids in the graph to set the location of.

  • positions

    (ndarray) –

    An (ndim, num_nodes) shape array of positions to set.

  • incl_time

    (bool, default: False ) –

    If true, include the time as the first column of the position array. Defaults to False.

Source code in src/funtracks/data_model/tracks.py
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
def set_positions(
    self,
    nodes: Iterable[Node],
    positions: np.ndarray,
    incl_time: bool = False,
):
    """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.

    Args:
        nodes (Iterable[node]): The node ids in the graph to set the location of.
        positions (np.ndarray): An (ndim, num_nodes) shape array of positions to set.
        f incl_time is true, time is the first column and is included in ndim.
        incl_time (bool, optional): If true, include the time as the
            first column of the position array. Defaults to False.
    """
    if not isinstance(positions, np.ndarray):
        positions = np.array(positions)
    if incl_time:
        self.set_times(nodes, positions[:, 0].tolist())
        positions = positions[:, 1:]

    if isinstance(self.pos_attr, tuple | list):
        for idx, attr in enumerate(self.pos_attr):
            self._set_nodes_attr(nodes, attr, positions[:, idx].tolist())
    else:
        self._set_nodes_attr(nodes, self.pos_attr, positions.tolist())

set_time

set_time(node: Any, time: int)

Set the time frame of a given node. Raises an error if the node is not in the graph.

Parameters:

  • node

    (Any) –

    The node id to set the time frame for

  • time

    (int) –

    The time to set

Source code in src/funtracks/data_model/tracks.py
165
166
167
168
169
170
171
172
173
174
def set_time(self, node: Any, time: int):
    """Set the time frame of a given node. Raises an error if the node
    is not in the graph.

    Args:
        node (Any): The node id to set the time frame for
        time (int): The time to set

    """
    self.set_times([node], [int(time)])

update_segmentations

update_segmentations(nodes: Iterable[Node], pixels: Iterable[SegMask], added: bool = True) -> None

Updates the segmentation of the given nodes. Also updates the auto-computed attributes of the nodes and incident edges.

Source code in src/funtracks/data_model/tracks.py
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
def update_segmentations(
    self, nodes: Iterable[Node], pixels: Iterable[SegMask], added: bool = True
) -> None:
    """Updates the segmentation of the given nodes. Also updates the
    auto-computed attributes of the nodes and incident edges.
    """
    times = self.get_times(nodes)
    values = nodes if added else [0 for _ in nodes]
    self.set_pixels(pixels, values)
    computed_attrs = self._compute_node_attrs(nodes, times)
    positions = np.array(computed_attrs[NodeAttr.POS.value])
    self.set_positions(nodes, positions)
    self._set_nodes_attr(
        nodes, NodeAttr.AREA.value, computed_attrs[NodeAttr.AREA.value]
    )

    incident_edges = list(self.graph.in_edges(nodes)) + list(
        self.graph.out_edges(nodes)
    )
    for edge in incident_edges:
        new_edge_attrs = self._compute_edge_attrs([edge])
        self._set_edge_attributes([edge], new_edge_attrs)