instance
sleap_io.model.instance
¶
Data structures for data associated with a single instance such as an animal.
The Instance class is a SLEAP data structure that contains a collection of points that
correspond to landmarks within a Skeleton.
PredictedInstance additionally contains metadata associated with how the instance was
estimated, such as confidence scores.
Classes:
| Name | Description |
|---|---|
Instance |
This class represents a ground truth instance such as an animal. |
Node |
A landmark type within a |
PointsArray |
A specialized array for storing instance points data. |
PredictedInstance |
A |
PredictedPointsArray |
A specialized array for storing predicted instance points data with scores. |
Skeleton |
A description of a set of landmark types and connections between them. |
Track |
An object that represents the same animal/object across multiple detections. |
Attributes:
| Name | Type | Description |
|---|---|---|
__cached__ |
str(object='') -> str |
|
__doc__ |
str(object='') -> str |
|
__file__ |
str(object='') -> str |
|
__name__ |
str(object='') -> str |
|
__package__ |
str(object='') -> str |
__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/__pycache__/instance.cpython-313.pyc'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__ = 'Data structures for data associated with a single instance such as an animal.\n\nThe `Instance` class is a SLEAP data structure that contains a collection of points that\ncorrespond to landmarks within a `Skeleton`.\n\n`PredictedInstance` additionally contains metadata associated with how the instance was\nestimated, such as confidence scores.\n'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/instance.py'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__ = 'sleap_io.model.instance'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__ = 'sleap_io.model'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
Instance
¶
This class represents a ground truth instance such as an animal.
An Instance has a set of landmarks (points) that correspond to a Skeleton. Each
point is associated with a Node in the skeleton. The points are stored in a
structured numpy array with columns for x, y, visible, complete and name.
The Instance may also be associated with a Track which links multiple instances
together across frames or videos.
Attributes:
| Name | Type | Description |
|---|---|---|
points |
A numpy structured array with columns for xy, visible and complete. The
array should have shape |
|
skeleton |
The |
|
track |
An optional |
|
tracking_score |
The score associated with the |
|
from_predicted |
The |
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Convert the points array after initialization. |
__getitem__ |
Return the point associated with a node. |
__init__ |
Method generated by attrs for class Instance. |
__len__ |
Return the number of points in the instance. |
__replace__ |
Method generated by attrs for class Instance. |
__repr__ |
Return a readable representation of the instance. |
__setitem__ |
Set the point associated with a node. |
bounding_box |
Get the bounding box of visible points. |
empty |
Create an empty instance with no points. |
from_numpy |
Create an instance object from a numpy array. |
numpy |
Return the instance points as a |
overlaps_with |
Check if this instance overlaps with another based on bounding box IoU. |
replace_skeleton |
Replace the skeleton associated with the instance. |
same_identity_as |
Check if this instance has the same identity (track) as another instance. |
same_pose_as |
Check if this instance has the same pose as another instance. |
update_skeleton |
Update or replace the skeleton associated with the instance. |
Source code in sleap_io/model/instance.py
@attrs.define(auto_attribs=True, slots=True, eq=False)
class Instance:
"""This class represents a ground truth instance such as an animal.
An `Instance` has a set of landmarks (points) that correspond to a `Skeleton`. Each
point is associated with a `Node` in the skeleton. The points are stored in a
structured numpy array with columns for x, y, visible, complete and name.
The `Instance` may also be associated with a `Track` which links multiple instances
together across frames or videos.
Attributes:
points: A numpy structured array with columns for xy, visible and complete. The
array should have shape `(n_nodes,)`. This representation is useful for
performance efficiency when working with large datasets.
skeleton: The `Skeleton` that describes the `Node`s and `Edge`s associated with
this instance.
track: An optional `Track` associated with a unique animal/object across frames
or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity assignment.
This is `None` if the instance is not associated with a track or if the
track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
"""
points: PointsArray = attrs.field(eq=attrs.cmp_using(eq=np.array_equal))
skeleton: Skeleton
track: Track | None = None
tracking_score: float | None = None
from_predicted: "PredictedInstance | None" = None
@classmethod
def empty(
cls,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an empty instance with no points.
Args:
skeleton: The `Skeleton` that this `Instance` is associated with.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` with an empty numpy array of shape `(n_nodes,)`.
"""
points = PointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
@classmethod
def _convert_points(
cls, points_data: np.ndarray | dict | list, skeleton: Skeleton
) -> PointsArray:
"""Convert points to a structured numpy array if needed."""
if isinstance(points_data, dict):
return PointsArray.from_dict(points_data, skeleton)
elif isinstance(points_data, (list, np.ndarray)):
if isinstance(points_data, list):
points_data = np.array(points_data)
points = PointsArray.from_array(points_data)
points["name"] = skeleton.node_names
return points
else:
raise ValueError("points must be a numpy array or dictionary.")
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an instance object from a numpy array.
Args:
points_data: A numpy array of shape `(n_nodes, D)` corresponding to the
points of the skeleton. Values of `np.nan` indicate "missing" nodes and
will be reflected in the "visible" field.
If `D == 2`, the array should have columns for x and y.
If `D == 3`, the array should have columns for x, y and visible.
If `D == 4`, the array should have columns for x, y, visible and
complete.
If this is provided as a structured array, it will be used without copy
if it has the correct dtype. Otherwise, a new structured array will be
created reusing the provided data.
skeleton: The `Skeleton` that this `Instance` is associated with. It should
have `n_nodes` nodes.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` object with the specified points.
"""
return cls(
points=points_data,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
def __attrs_post_init__(self):
"""Convert the points array after initialization."""
if not isinstance(self.points, PointsArray):
self.points = self._convert_points(self.points, self.skeleton)
# Ensure points have node names
if "name" in self.points.dtype.names and not all(self.points["name"]):
self.points["name"] = self.skeleton.node_names
def numpy(
self,
invisible_as_nan: bool = True,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `Instance.points["xy"]` is.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the `Instance.points["xy"]`
attribute directly. This will not replace invisible points with `np.nan`.
"""
if invisible_as_nan:
return np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
return self.points["xy"].copy()
def __getitem__(self, node: int | str | Node) -> np.ndarray:
"""Return the point associated with a node."""
if type(node) is not int:
node = self.skeleton.index(node)
return self.points[node]
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 containing (x, y) coordinates.
Notes:
This sets the point coordinates and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
self.points[node]["visible"] = True
def __len__(self) -> int:
"""Return the number of points in the instance."""
return len(self.points)
def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track
return f"Instance(points={pts}, track={track})"
@property
def n_visible(self) -> int:
"""Return the number of visible points in the instance."""
return sum(self.points["visible"])
@property
def is_empty(self) -> bool:
"""Return `True` if no points are visible on the instance."""
return ~(self.points["visible"].any())
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `Instance.skeleton` attribute and the
`Instance.points` attribute in place (a copy is made of the points array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
# old_skeleton = self.skeleton
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
# old_node_names = old_skeleton.node_names
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# old_node_inds = np.array(old_node_inds).reshape(-1, 1)
# new_node_inds = np.array(new_node_inds).reshape(-1, 1)
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
def same_pose_as(self, other: "Instance", tolerance: float = None) -> bool:
"""Check if this instance has the same pose as another instance.
Args:
other: Another instance to compare with.
tolerance: Maximum distance (in pixels) between corresponding points
for them to be considered the same. If None (default), uses exact
comparison including proper NaN handling.
Returns:
True if the instances have the same pose within tolerance, False otherwise.
Notes:
Two instances are considered to have the same pose if:
- They have the same skeleton structure
- When tolerance is None: All coordinates match exactly (including NaN)
- When tolerance is specified: All visible points are within tolerance
distance and NaN patterns match exactly
"""
# Check skeleton compatibility
if not self.skeleton.matches(other.skeleton):
return False
if tolerance is None:
# Exact comparison using numpy arrays with proper NaN handling
return np.array_equal(self.numpy(), other.numpy(), equal_nan=True)
else:
# Tolerance-based comparison with proper NaN handling
self_array = self.numpy()
other_array = other.numpy()
# First, check if NaN patterns match exactly
self_nan_mask = np.isnan(self_array)
other_nan_mask = np.isnan(other_array)
if not np.array_equal(self_nan_mask, other_nan_mask):
return False
# Get mask for non-NaN values
non_nan_mask = ~self_nan_mask
# If all values are NaN, they're considered equal
if not non_nan_mask.any():
return True
# Calculate distances only for non-NaN points
self_pts = self_array[non_nan_mask]
other_pts = other_array[non_nan_mask]
# Reshape to handle the coordinate pairs properly
self_pts = self_pts.reshape(-1, 2)
other_pts = other_pts.reshape(-1, 2)
distances = np.linalg.norm(self_pts - other_pts, axis=1)
return np.all(distances <= tolerance)
def same_identity_as(self, other: "Instance") -> bool:
"""Check if this instance has the same identity (track) as another instance.
Args:
other: Another instance to compare with.
Returns:
True if both instances have the same track identity, False otherwise.
Notes:
Instances have the same identity if they share the same Track object
(by identity, not just by name).
"""
if self.track is None or other.track is None:
return False
return self.track is other.track
def overlaps_with(self, other: "Instance", iou_threshold: float = 0.5) -> bool:
"""Check if this instance overlaps with another based on bounding box IoU.
Args:
other: Another instance to compare with.
iou_threshold: Minimum IoU (Intersection over Union) value to consider
the instances as overlapping.
Returns:
True if the instances overlap above the threshold, False otherwise.
Notes:
Overlap is computed using the bounding boxes of visible points.
If either instance has no visible points, they don't overlap.
"""
# Get visible points for both instances
self_visible = self.points["visible"]
other_visible = other.points["visible"]
if not self_visible.any() or not other_visible.any():
return False
# Calculate bounding boxes
self_pts = self.points["xy"][self_visible]
other_pts = other.points["xy"][other_visible]
self_bbox = np.array(
[
[np.min(self_pts[:, 0]), np.min(self_pts[:, 1])], # min x, y
[np.max(self_pts[:, 0]), np.max(self_pts[:, 1])], # max x, y
]
)
other_bbox = np.array(
[
[np.min(other_pts[:, 0]), np.min(other_pts[:, 1])],
[np.max(other_pts[:, 0]), np.max(other_pts[:, 1])],
]
)
# Calculate intersection
intersection_min = np.maximum(self_bbox[0], other_bbox[0])
intersection_max = np.minimum(self_bbox[1], other_bbox[1])
if np.any(intersection_min >= intersection_max):
# No intersection
return False
intersection_area = np.prod(intersection_max - intersection_min)
# Calculate union
self_area = np.prod(self_bbox[1] - self_bbox[0])
other_area = np.prod(other_bbox[1] - other_bbox[0])
union_area = self_area + other_area - intersection_area
# Calculate IoU
iou = intersection_area / union_area if union_area > 0 else 0
return iou >= iou_threshold
def bounding_box(self) -> np.ndarray | None:
"""Get the bounding box of visible points.
Returns:
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]],
or None if there are no visible points.
"""
visible = self.points["visible"]
if not visible.any():
return None
pts = self.points["xy"][visible]
return np.array(
[
[np.min(pts[:, 0]), np.min(pts[:, 1])],
[np.max(pts[:, 0]), np.max(pts[:, 1])],
]
)
__annotations__ = {'points': 'PointsArray', 'skeleton': 'Skeleton', 'track': 'Track | None', 'tracking_score': 'float | None', 'from_predicted': "'PredictedInstance | None'"}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=False, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'This class represents a ground truth instance such as an animal.\n\nAn `Instance` has a set of landmarks (points) that correspond to a `Skeleton`. Each\npoint is associated with a `Node` in the skeleton. The points are stored in a\nstructured numpy array with columns for x, y, visible, complete and name.\n\nThe `Instance` may also be associated with a `Track` which links multiple instances\ntogether across frames or videos.\n\nAttributes:\n points: A numpy structured array with columns for xy, visible and complete. The\n array should have shape `(n_nodes,)`. This representation is useful for\n performance efficiency when working with large datasets.\n skeleton: The `Skeleton` that describes the `Node`s and `Edge`s associated with\n this instance.\n track: An optional `Track` associated with a unique animal/object across frames\n or videos.\n tracking_score: The score associated with the `Track` assignment. This is\n typically the value from the score matrix used in an identity assignment.\n This is `None` if the instance is not associated with a track or if the\n track was assigned manually.\n from_predicted: The `PredictedInstance` (if any) that this instance was\n initialized from. This is used with human-in-the-loop workflows.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 386
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__match_args__ = ('points', 'skeleton', 'track', 'tracking_score', 'from_predicted')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.instance'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__slots__ = ('points', 'skeleton', 'track', 'tracking_score', 'from_predicted', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ('points', 'skeleton')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
is_empty
property
¶
Return True if no points are visible on the instance.
n_visible
property
¶
Return the number of visible points in the instance.
__attrs_post_init__()
¶
Convert the points array after initialization.
Source code in sleap_io/model/instance.py
def __attrs_post_init__(self):
"""Convert the points array after initialization."""
if not isinstance(self.points, PointsArray):
self.points = self._convert_points(self.points, self.skeleton)
# Ensure points have node names
if "name" in self.points.dtype.names and not all(self.points["name"]):
self.points["name"] = self.skeleton.node_names
__getitem__(node)
¶
__init__(points, skeleton, track=None, tracking_score=None, from_predicted=None)
¶
Method generated by attrs for class Instance.
Source code in sleap_io/model/instance.py
"""Data structures for data associated with a single instance such as an animal.
The `Instance` class is a SLEAP data structure that contains a collection of points that
correspond to landmarks within a `Skeleton`.
`PredictedInstance` additionally contains metadata associated with how the instance was
estimated, such as confidence scores.
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class Instance.
__repr__()
¶
Return a readable representation of the instance.
__setitem__(node, value)
¶
Set the point associated with a node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
int | str | Node
|
The node to set the point for. Can be an integer index, string name, or Node object. |
required |
value
|
A tuple or array-like of length 2 containing (x, y) coordinates. |
required |
Notes
This sets the point coordinates and marks the point as visible.
Source code in sleap_io/model/instance.py
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 containing (x, y) coordinates.
Notes:
This sets the point coordinates and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
self.points[node]["visible"] = True
bounding_box()
¶
Get the bounding box of visible points.
Returns:
| Type | Description |
|---|---|
ndarray | None
|
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]], or None if there are no visible points. |
Source code in sleap_io/model/instance.py
def bounding_box(self) -> np.ndarray | None:
"""Get the bounding box of visible points.
Returns:
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]],
or None if there are no visible points.
"""
visible = self.points["visible"]
if not visible.any():
return None
pts = self.points["xy"][visible]
return np.array(
[
[np.min(pts[:, 0]), np.min(pts[:, 1])],
[np.max(pts[:, 0]), np.max(pts[:, 1])],
]
)
empty(skeleton, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create an empty instance with no points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeleton
|
Skeleton
|
The |
required |
track
|
Track | None
|
An optional |
None
|
tracking_score
|
float | None
|
The score associated with the |
None
|
from_predicted
|
PredictedInstance | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
Instance
|
An |
Source code in sleap_io/model/instance.py
@classmethod
def empty(
cls,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an empty instance with no points.
Args:
skeleton: The `Skeleton` that this `Instance` is associated with.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` with an empty numpy array of shape `(n_nodes,)`.
"""
points = PointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
from_numpy(points_data, skeleton, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create an instance object from a numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points_data
|
ndarray
|
A numpy array of shape If If this is provided as a structured array, it will be used without copy if it has the correct dtype. Otherwise, a new structured array will be created reusing the provided data. |
required |
skeleton
|
Skeleton
|
The |
required |
track
|
Track | None
|
An optional |
None
|
tracking_score
|
float | None
|
The score associated with the |
None
|
from_predicted
|
PredictedInstance | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
Instance
|
An |
Source code in sleap_io/model/instance.py
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an instance object from a numpy array.
Args:
points_data: A numpy array of shape `(n_nodes, D)` corresponding to the
points of the skeleton. Values of `np.nan` indicate "missing" nodes and
will be reflected in the "visible" field.
If `D == 2`, the array should have columns for x and y.
If `D == 3`, the array should have columns for x, y and visible.
If `D == 4`, the array should have columns for x, y, visible and
complete.
If this is provided as a structured array, it will be used without copy
if it has the correct dtype. Otherwise, a new structured array will be
created reusing the provided data.
skeleton: The `Skeleton` that this `Instance` is associated with. It should
have `n_nodes` nodes.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` object with the specified points.
"""
return cls(
points=points_data,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
numpy(invisible_as_nan=True)
¶
Return the instance points as a (n_nodes, 2) numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
invisible_as_nan
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A numpy array of shape |
Notes
This will always return a copy of the array.
If you need to avoid making a copy, just access the Instance.points["xy"]
attribute directly. This will not replace invisible points with np.nan.
Source code in sleap_io/model/instance.py
def numpy(
self,
invisible_as_nan: bool = True,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `Instance.points["xy"]` is.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the `Instance.points["xy"]`
attribute directly. This will not replace invisible points with `np.nan`.
"""
if invisible_as_nan:
return np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
return self.points["xy"].copy()
overlaps_with(other, iou_threshold=0.5)
¶
Check if this instance overlaps with another based on bounding box IoU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
iou_threshold
|
float
|
Minimum IoU (Intersection over Union) value to consider the instances as overlapping. |
0.5
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the instances overlap above the threshold, False otherwise. |
Notes
Overlap is computed using the bounding boxes of visible points. If either instance has no visible points, they don't overlap.
Source code in sleap_io/model/instance.py
def overlaps_with(self, other: "Instance", iou_threshold: float = 0.5) -> bool:
"""Check if this instance overlaps with another based on bounding box IoU.
Args:
other: Another instance to compare with.
iou_threshold: Minimum IoU (Intersection over Union) value to consider
the instances as overlapping.
Returns:
True if the instances overlap above the threshold, False otherwise.
Notes:
Overlap is computed using the bounding boxes of visible points.
If either instance has no visible points, they don't overlap.
"""
# Get visible points for both instances
self_visible = self.points["visible"]
other_visible = other.points["visible"]
if not self_visible.any() or not other_visible.any():
return False
# Calculate bounding boxes
self_pts = self.points["xy"][self_visible]
other_pts = other.points["xy"][other_visible]
self_bbox = np.array(
[
[np.min(self_pts[:, 0]), np.min(self_pts[:, 1])], # min x, y
[np.max(self_pts[:, 0]), np.max(self_pts[:, 1])], # max x, y
]
)
other_bbox = np.array(
[
[np.min(other_pts[:, 0]), np.min(other_pts[:, 1])],
[np.max(other_pts[:, 0]), np.max(other_pts[:, 1])],
]
)
# Calculate intersection
intersection_min = np.maximum(self_bbox[0], other_bbox[0])
intersection_max = np.minimum(self_bbox[1], other_bbox[1])
if np.any(intersection_min >= intersection_max):
# No intersection
return False
intersection_area = np.prod(intersection_max - intersection_min)
# Calculate union
self_area = np.prod(self_bbox[1] - self_bbox[0])
other_area = np.prod(other_bbox[1] - other_bbox[0])
union_area = self_area + other_area - intersection_area
# Calculate IoU
iou = intersection_area / union_area if union_area > 0 else 0
return iou >= iou_threshold
replace_skeleton(new_skeleton, node_names_map=None)
¶
Replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_skeleton
|
Skeleton
|
The new |
required |
node_names_map
|
dict[str, str] | None
|
Dictionary mapping nodes in the old skeleton to nodes in the new skeleton. Keys and values should be specified as lists of strings. If not provided, only nodes with identical names will be mapped. Points associated with unmapped nodes will be removed. |
None
|
Notes
This method will update the Instance.skeleton attribute and the
Instance.points attribute in place (a copy is made of the points array).
It is recommended to use Labels.replace_skeleton instead of this method if
more flexible node mapping is required.
Source code in sleap_io/model/instance.py
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `Instance.skeleton` attribute and the
`Instance.points` attribute in place (a copy is made of the points array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
# old_skeleton = self.skeleton
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
# old_node_names = old_skeleton.node_names
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# old_node_inds = np.array(old_node_inds).reshape(-1, 1)
# new_node_inds = np.array(new_node_inds).reshape(-1, 1)
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
same_identity_as(other)
¶
Check if this instance has the same identity (track) as another instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if both instances have the same track identity, False otherwise. |
Notes
Instances have the same identity if they share the same Track object (by identity, not just by name).
Source code in sleap_io/model/instance.py
def same_identity_as(self, other: "Instance") -> bool:
"""Check if this instance has the same identity (track) as another instance.
Args:
other: Another instance to compare with.
Returns:
True if both instances have the same track identity, False otherwise.
Notes:
Instances have the same identity if they share the same Track object
(by identity, not just by name).
"""
if self.track is None or other.track is None:
return False
return self.track is other.track
same_pose_as(other, tolerance=None)
¶
Check if this instance has the same pose as another instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
tolerance
|
float
|
Maximum distance (in pixels) between corresponding points for them to be considered the same. If None (default), uses exact comparison including proper NaN handling. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the instances have the same pose within tolerance, False otherwise. |
Notes
Two instances are considered to have the same pose if: - They have the same skeleton structure - When tolerance is None: All coordinates match exactly (including NaN) - When tolerance is specified: All visible points are within tolerance distance and NaN patterns match exactly
Source code in sleap_io/model/instance.py
def same_pose_as(self, other: "Instance", tolerance: float = None) -> bool:
"""Check if this instance has the same pose as another instance.
Args:
other: Another instance to compare with.
tolerance: Maximum distance (in pixels) between corresponding points
for them to be considered the same. If None (default), uses exact
comparison including proper NaN handling.
Returns:
True if the instances have the same pose within tolerance, False otherwise.
Notes:
Two instances are considered to have the same pose if:
- They have the same skeleton structure
- When tolerance is None: All coordinates match exactly (including NaN)
- When tolerance is specified: All visible points are within tolerance
distance and NaN patterns match exactly
"""
# Check skeleton compatibility
if not self.skeleton.matches(other.skeleton):
return False
if tolerance is None:
# Exact comparison using numpy arrays with proper NaN handling
return np.array_equal(self.numpy(), other.numpy(), equal_nan=True)
else:
# Tolerance-based comparison with proper NaN handling
self_array = self.numpy()
other_array = other.numpy()
# First, check if NaN patterns match exactly
self_nan_mask = np.isnan(self_array)
other_nan_mask = np.isnan(other_array)
if not np.array_equal(self_nan_mask, other_nan_mask):
return False
# Get mask for non-NaN values
non_nan_mask = ~self_nan_mask
# If all values are NaN, they're considered equal
if not non_nan_mask.any():
return True
# Calculate distances only for non-NaN points
self_pts = self_array[non_nan_mask]
other_pts = other_array[non_nan_mask]
# Reshape to handle the coordinate pairs properly
self_pts = self_pts.reshape(-1, 2)
other_pts = other_pts.reshape(-1, 2)
distances = np.linalg.norm(self_pts - other_pts, axis=1)
return np.all(distances <= tolerance)
update_skeleton(names_only=False)
¶
Update or replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names_only
|
bool
|
If |
False
|
Source code in sleap_io/model/instance.py
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
Node
¶
A landmark type within a Skeleton.
This typically corresponds to a unique landmark within a skeleton, such as the "left eye".
Attributes:
| Name | Type | Description |
|---|---|---|
name |
Descriptive label for the landmark. |
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class Node. |
__replace__ |
Method generated by attrs for class Node. |
__repr__ |
Method generated by attrs for class Node. |
Source code in sleap_io/model/skeleton.py
__annotations__ = {'name': 'str'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A landmark type within a `Skeleton`.\n\nThis typically corresponds to a unique landmark within a skeleton, such as the "left\neye".\n\nAttributes:\n name: Descriptive label for the landmark.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 17
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__match_args__ = ('name',)
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.skeleton'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__slots__ = ('name', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
__init__(name)
¶
__replace__(**changes)
¶
Method generated by attrs for class Node.
__repr__()
¶
Method generated by attrs for class Node.
Source code in sleap_io/model/skeleton.py
"""Data model for skeletons.
Skeletons are collections of nodes and edges which describe the landmarks associated
with a pose model. The edges represent the connections between them and may be used
differently depending on the underlying pose model.
"""
from __future__ import annotations
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
PointsArray
¶
Bases: numpy.ndarray
A specialized array for storing instance points data.
This class ensures that the array always uses the correct dtype and provides convenience methods for working with point data.
The structured dtype includes the following fields
- xy: A float64 array of shape (2,) containing the x, y coordinates
- visible: A boolean indicating if the point is visible
- complete: A boolean indicating if the point is complete
- name: An object dtype containing the name of the node
Methods:
| Name | Description |
|---|---|
empty |
Create an empty points array with the appropriate dtype. |
from_array |
Convert an existing array to a PointsArray with the appropriate dtype. |
from_dict |
Create a PointsArray from a dictionary of node points. |
Attributes:
| Name | Type | Description |
|---|---|---|
__dict__ |
Read-only proxy of a mapping. |
|
__doc__ |
str(object='') -> str |
|
__firstlineno__ |
int([x]) -> integer |
|
__module__ |
str(object='') -> str |
|
__static_attributes__ |
Built-in immutable sequence. |
Source code in sleap_io/model/instance.py
class PointsArray(np.ndarray):
"""A specialized array for storing instance points data.
This class ensures that the array always uses the correct dtype and provides
convenience methods for working with point data.
The structured dtype includes the following fields:
- xy: A float64 array of shape (2,) containing the x, y coordinates
- visible: A boolean indicating if the point is visible
- complete: A boolean indicating if the point is complete
- name: An object dtype containing the name of the node
"""
@classmethod
def _get_dtype(cls):
"""Get the dtype for points array.
Returns:
np.dtype: A structured numpy dtype with fields for xy coordinates,
visible flag, complete flag, and node names.
"""
# Cache the dtype at the class level for performance
# Use cls.__dict__ to check if defined on this class (not inherited)
if "_cached_dtype" not in cls.__dict__:
cls._cached_dtype = np.dtype(
[
("xy", "<f8", (2,)), # 64-bit (8-byte) little-endian double, ndim=2
("visible", "bool"),
("complete", "bool"),
(
"name",
"O",
), # object dtype to store pointers to python string objects
]
)
return cls._cached_dtype
@classmethod
def empty(cls, length: int) -> "PointsArray":
"""Create an empty points array with the appropriate dtype.
Args:
length: The number of points (nodes) to allocate in the array.
Returns:
PointsArray: An empty array of the specified length with the appropriate
dtype.
"""
dtype = cls._get_dtype()
arr = np.empty(length, dtype=dtype).view(cls)
return arr
@classmethod
def from_array(cls, array: np.ndarray) -> "PointsArray":
"""Convert an existing array to a PointsArray with the appropriate dtype.
Args:
array: A numpy array to convert. Can be a structured array or a regular
array. If a regular array, it is assumed to have columns for x, y
coordinates and optionally visible and complete flags.
Returns:
PointsArray: A structured array view of the input data with the appropriate
dtype.
Notes:
If the input is a structured array with fields matching the target dtype,
those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as visible flag
- Fourth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
dtype = cls._get_dtype()
# If already the right type, just view as PointsArray
if isinstance(array, np.ndarray) and array.dtype == dtype:
return array.view(cls)
# Otherwise, create a new array with the right dtype
new_array = np.empty(len(array), dtype=dtype).view(cls)
# Copy available fields
if isinstance(array, np.ndarray) and array.dtype.fields is not None:
# Structured array, copy matching fields
for field_name in dtype.names:
if field_name in array.dtype.names:
new_array[field_name] = array[field_name]
elif isinstance(array, np.ndarray):
# Regular array, assume x, y coordinates
new_array["xy"] = array[:, 0:2]
# Default visibility based on NaN
new_array["visible"] = ~np.isnan(array[:, 0])
# If there are more columns, assume they are visible and complete
if array.shape[1] >= 3:
new_array["visible"] = array[:, 2].astype(bool)
if array.shape[1] >= 4:
new_array["complete"] = array[:, 3].astype(bool)
return new_array
@classmethod
def from_dict(cls, points_dict: dict, skeleton: Skeleton) -> "PointsArray":
"""Create a PointsArray from a dictionary of node points.
Args:
points_dict: A dictionary mapping nodes (as Node objects, indices, or
strings) to point data. Each point should be an array-like with at least
2 elements for x, y coordinates, and optionally visible and complete
flags.
skeleton: The Skeleton object that defines the nodes.
Returns:
PointsArray: A structured array with the appropriate dtype containing the
point data from the dictionary.
Notes:
For each entry in the points_dict:
- First two values are treated as x, y coordinates
- Third value (if present) is treated as visible flag
- Fourth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
points = cls.empty(len(skeleton))
for node, data in points_dict.items():
if isinstance(node, (Node, str)):
node = skeleton.index(node)
points[node]["xy"] = data[:2]
idx = 2
if len(data) > idx:
points[node]["visible"] = data[idx]
else:
points[node]["visible"] = ~np.isnan(data[0])
idx += 1
if len(data) > idx:
points[node]["complete"] = data[idx]
return points
__dict__ = mappingproxy({'__module__': 'sleap_io.model.instance', '__firstlineno__': 18, '__doc__': 'A specialized array for storing instance points data.\n\nThis class ensures that the array always uses the correct dtype and provides\nconvenience methods for working with point data.\n\nThe structured dtype includes the following fields:\n - xy: A float64 array of shape (2,) containing the x, y coordinates\n - visible: A boolean indicating if the point is visible\n - complete: A boolean indicating if the point is complete\n - name: An object dtype containing the name of the node\n', '_get_dtype': <classmethod(<function PointsArray._get_dtype at 0x7f41d432ede0>)>, 'empty': <classmethod(<function PointsArray.empty at 0x7f41d432ee80>)>, 'from_array': <classmethod(<function PointsArray.from_array at 0x7f41d432ef20>)>, 'from_dict': <classmethod(<function PointsArray.from_dict at 0x7f41d432efc0>)>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'PointsArray' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'A specialized array for storing instance points data.\n\nThis class ensures that the array always uses the correct dtype and provides\nconvenience methods for working with point data.\n\nThe structured dtype includes the following fields:\n - xy: A float64 array of shape (2,) containing the x, y coordinates\n - visible: A boolean indicating if the point is visible\n - complete: A boolean indicating if the point is complete\n - name: An object dtype containing the name of the node\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 18
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__module__ = 'sleap_io.model.instance'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
empty(length)
classmethod
¶
Create an empty points array with the appropriate dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
length
|
int
|
The number of points (nodes) to allocate in the array. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PointsArray |
PointsArray
|
An empty array of the specified length with the appropriate dtype. |
Source code in sleap_io/model/instance.py
@classmethod
def empty(cls, length: int) -> "PointsArray":
"""Create an empty points array with the appropriate dtype.
Args:
length: The number of points (nodes) to allocate in the array.
Returns:
PointsArray: An empty array of the specified length with the appropriate
dtype.
"""
dtype = cls._get_dtype()
arr = np.empty(length, dtype=dtype).view(cls)
return arr
from_array(array)
classmethod
¶
Convert an existing array to a PointsArray with the appropriate dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
array
|
ndarray
|
A numpy array to convert. Can be a structured array or a regular array. If a regular array, it is assumed to have columns for x, y coordinates and optionally visible and complete flags. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PointsArray |
PointsArray
|
A structured array view of the input data with the appropriate dtype. |
Notes
If the input is a structured array with fields matching the target dtype, those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as visible flag
- Fourth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x coordinate.
Source code in sleap_io/model/instance.py
@classmethod
def from_array(cls, array: np.ndarray) -> "PointsArray":
"""Convert an existing array to a PointsArray with the appropriate dtype.
Args:
array: A numpy array to convert. Can be a structured array or a regular
array. If a regular array, it is assumed to have columns for x, y
coordinates and optionally visible and complete flags.
Returns:
PointsArray: A structured array view of the input data with the appropriate
dtype.
Notes:
If the input is a structured array with fields matching the target dtype,
those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as visible flag
- Fourth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
dtype = cls._get_dtype()
# If already the right type, just view as PointsArray
if isinstance(array, np.ndarray) and array.dtype == dtype:
return array.view(cls)
# Otherwise, create a new array with the right dtype
new_array = np.empty(len(array), dtype=dtype).view(cls)
# Copy available fields
if isinstance(array, np.ndarray) and array.dtype.fields is not None:
# Structured array, copy matching fields
for field_name in dtype.names:
if field_name in array.dtype.names:
new_array[field_name] = array[field_name]
elif isinstance(array, np.ndarray):
# Regular array, assume x, y coordinates
new_array["xy"] = array[:, 0:2]
# Default visibility based on NaN
new_array["visible"] = ~np.isnan(array[:, 0])
# If there are more columns, assume they are visible and complete
if array.shape[1] >= 3:
new_array["visible"] = array[:, 2].astype(bool)
if array.shape[1] >= 4:
new_array["complete"] = array[:, 3].astype(bool)
return new_array
from_dict(points_dict, skeleton)
classmethod
¶
Create a PointsArray from a dictionary of node points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points_dict
|
dict
|
A dictionary mapping nodes (as Node objects, indices, or strings) to point data. Each point should be an array-like with at least 2 elements for x, y coordinates, and optionally visible and complete flags. |
required |
skeleton
|
Skeleton
|
The Skeleton object that defines the nodes. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PointsArray |
PointsArray
|
A structured array with the appropriate dtype containing the point data from the dictionary. |
Notes
For each entry in the points_dict: - First two values are treated as x, y coordinates - Third value (if present) is treated as visible flag - Fourth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x coordinate.
Source code in sleap_io/model/instance.py
@classmethod
def from_dict(cls, points_dict: dict, skeleton: Skeleton) -> "PointsArray":
"""Create a PointsArray from a dictionary of node points.
Args:
points_dict: A dictionary mapping nodes (as Node objects, indices, or
strings) to point data. Each point should be an array-like with at least
2 elements for x, y coordinates, and optionally visible and complete
flags.
skeleton: The Skeleton object that defines the nodes.
Returns:
PointsArray: A structured array with the appropriate dtype containing the
point data from the dictionary.
Notes:
For each entry in the points_dict:
- First two values are treated as x, y coordinates
- Third value (if present) is treated as visible flag
- Fourth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
points = cls.empty(len(skeleton))
for node, data in points_dict.items():
if isinstance(node, (Node, str)):
node = skeleton.index(node)
points[node]["xy"] = data[:2]
idx = 2
if len(data) > idx:
points[node]["visible"] = data[idx]
else:
points[node]["visible"] = ~np.isnan(data[0])
idx += 1
if len(data) > idx:
points[node]["complete"] = data[idx]
return points
PredictedInstance
¶
Bases: sleap_io.model.instance.Instance
A PredictedInstance is an Instance that was predicted using a model.
Attributes:
| Name | Type | Description |
|---|---|---|
skeleton |
The |
|
points |
A dictionary where keys are |
|
track |
An optional |
|
from_predicted |
Not applicable in |
|
score |
The instance detection or part grouping prediction score. This is a scalar that represents the confidence with which this entire instance was predicted. This may not always be applicable depending on the model type. |
|
tracking_score |
The score associated with the |
Methods:
| Name | Description |
|---|---|
__getitem__ |
Return the point associated with a node. |
__init__ |
Method generated by attrs for class PredictedInstance. |
__replace__ |
Method generated by attrs for class PredictedInstance. |
__repr__ |
Return a readable representation of the instance. |
__setitem__ |
Set the point associated with a node. |
empty |
Create an empty instance with no points. |
from_numpy |
Create a predicted instance object from a numpy array. |
numpy |
Return the instance points as a |
replace_skeleton |
Replace the skeleton associated with the instance. |
update_skeleton |
Update or replace the skeleton associated with the instance. |
Source code in sleap_io/model/instance.py
@attrs.define(eq=False)
class PredictedInstance(Instance):
"""A `PredictedInstance` is an `Instance` that was predicted using a model.
Attributes:
skeleton: The `Skeleton` that this `Instance` is associated with.
points: A dictionary where keys are `Skeleton` nodes and values are `Point`s.
track: An optional `Track` associated with a unique animal/object across frames
or videos.
from_predicted: Not applicable in `PredictedInstance`s (must be set to `None`).
score: The instance detection or part grouping prediction score. This is a
scalar that represents the confidence with which this entire instance was
predicted. This may not always be applicable depending on the model type.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity assignment.
"""
points: PredictedPointsArray = attrs.field(eq=attrs.cmp_using(eq=np.array_equal))
skeleton: Skeleton
score: float = 0.0
track: Track | None = None
tracking_score: float | None = 0
from_predicted: "PredictedInstance | None" = None
def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track
score = str(self.score) if self.score is None else f"{self.score:.2f}"
tracking_score = (
str(self.tracking_score)
if self.tracking_score is None
else f"{self.tracking_score:.2f}"
)
return (
f"PredictedInstance(points={pts}, track={track}, "
f"score={score}, tracking_score={tracking_score})"
)
@classmethod
def empty(
cls,
skeleton: Skeleton,
score: float = 0.0,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "PredictedInstance":
"""Create an empty instance with no points."""
points = PredictedPointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
score=score,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
@classmethod
def _convert_points(
cls, points_data: np.ndarray | dict | list, skeleton: Skeleton
) -> PredictedPointsArray:
"""Convert points to a structured numpy array if needed."""
if isinstance(points_data, dict):
return PredictedPointsArray.from_dict(points_data, skeleton)
elif isinstance(points_data, (list, np.ndarray)):
if isinstance(points_data, list):
points_data = np.array(points_data)
points = PredictedPointsArray.from_array(points_data)
points["name"] = skeleton.node_names
return points
else:
raise ValueError("points must be a numpy array or dictionary.")
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
point_scores: np.ndarray | None = None,
score: float = 0.0,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "PredictedInstance":
"""Create a predicted instance object from a numpy array."""
points = cls._convert_points(points_data, skeleton)
if point_scores is not None:
points["score"] = point_scores
return cls(
points=points,
skeleton=skeleton,
score=score,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
def numpy(
self,
invisible_as_nan: bool = True,
scores: bool = False,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `PredictedInstance.points["xy"]` is.
scores: If `True`, the score associated with each point will be
included in the output.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
If `scores` is `True`, the array will have shape `(n_nodes, 3)` with the
third column containing the score associated with each point.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the
`PredictedInstance.points["xy"]` attribute directly. This will not replace
invisible points with `np.nan`.
"""
if invisible_as_nan:
pts = np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
pts = self.points["xy"].copy()
if scores:
return np.column_stack((pts, self.points["score"]))
else:
return pts
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PredictedPointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `PredictedInstance.skeleton` attribute and the
`PredictedInstance.points` attribute in place (a copy is made of the points
array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# Update the points.
new_points = PredictedPointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
def __getitem__(self, node: int | str | Node) -> np.ndarray:
"""Return the point associated with a node."""
# Inherit from Instance.__getitem__
return super().__getitem__(node)
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 or 3 containing (x, y) coordinates
and optionally a confidence score. If the score is not provided, it
defaults to 1.0.
Notes:
This sets the point coordinates, score, and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
# Set score if provided, otherwise default to 1.0
if len(value) >= 3:
self.points[node]["score"] = value[2]
else:
self.points[node]["score"] = 1.0
self.points[node]["visible"] = True
__annotations__ = {'points': 'PredictedPointsArray', 'skeleton': 'Skeleton', 'score': 'float', 'track': 'Track | None', 'tracking_score': 'float | None', 'from_predicted': "'PredictedInstance | None'"}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=False, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A `PredictedInstance` is an `Instance` that was predicted using a model.\n\nAttributes:\n skeleton: The `Skeleton` that this `Instance` is associated with.\n points: A dictionary where keys are `Skeleton` nodes and values are `Point`s.\n track: An optional `Track` associated with a unique animal/object across frames\n or videos.\n from_predicted: Not applicable in `PredictedInstance`s (must be set to `None`).\n score: The instance detection or part grouping prediction score. This is a\n scalar that represents the confidence with which this entire instance was\n predicted. This may not always be applicable depending on the model type.\n tracking_score: The score associated with the `Track` assignment. This is\n typically the value from the score matrix used in an identity assignment.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 818
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__match_args__ = ('points', 'skeleton', 'score', 'track', 'tracking_score', 'from_predicted')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.instance'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__slots__ = ('score',)
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ('points', 'skeleton')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__getitem__(node)
¶
__init__(points, skeleton, score=0.0, track=None, tracking_score=0, from_predicted=None)
¶
Method generated by attrs for class PredictedInstance.
Source code in sleap_io/model/instance.py
"""Data structures for data associated with a single instance such as an animal.
The `Instance` class is a SLEAP data structure that contains a collection of points that
correspond to landmarks within a `Skeleton`.
`PredictedInstance` additionally contains metadata associated with how the instance was
estimated, such as confidence scores.
"""
__replace__(**changes)
¶
Method generated by attrs for class PredictedInstance.
__repr__()
¶
Return a readable representation of the instance.
Source code in sleap_io/model/instance.py
def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track
score = str(self.score) if self.score is None else f"{self.score:.2f}"
tracking_score = (
str(self.tracking_score)
if self.tracking_score is None
else f"{self.tracking_score:.2f}"
)
return (
f"PredictedInstance(points={pts}, track={track}, "
f"score={score}, tracking_score={tracking_score})"
)
__setitem__(node, value)
¶
Set the point associated with a node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
int | str | Node
|
The node to set the point for. Can be an integer index, string name, or Node object. |
required |
value
|
A tuple or array-like of length 2 or 3 containing (x, y) coordinates and optionally a confidence score. If the score is not provided, it defaults to 1.0. |
required |
Notes
This sets the point coordinates, score, and marks the point as visible.
Source code in sleap_io/model/instance.py
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 or 3 containing (x, y) coordinates
and optionally a confidence score. If the score is not provided, it
defaults to 1.0.
Notes:
This sets the point coordinates, score, and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
# Set score if provided, otherwise default to 1.0
if len(value) >= 3:
self.points[node]["score"] = value[2]
else:
self.points[node]["score"] = 1.0
self.points[node]["visible"] = True
empty(skeleton, score=0.0, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create an empty instance with no points.
Source code in sleap_io/model/instance.py
@classmethod
def empty(
cls,
skeleton: Skeleton,
score: float = 0.0,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "PredictedInstance":
"""Create an empty instance with no points."""
points = PredictedPointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
score=score,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
from_numpy(points_data, skeleton, point_scores=None, score=0.0, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create a predicted instance object from a numpy array.
Source code in sleap_io/model/instance.py
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
point_scores: np.ndarray | None = None,
score: float = 0.0,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "PredictedInstance":
"""Create a predicted instance object from a numpy array."""
points = cls._convert_points(points_data, skeleton)
if point_scores is not None:
points["score"] = point_scores
return cls(
points=points,
skeleton=skeleton,
score=score,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
numpy(invisible_as_nan=True, scores=False)
¶
Return the instance points as a (n_nodes, 2) numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
invisible_as_nan
|
bool
|
If |
True
|
scores
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A numpy array of shape If |
Notes
This will always return a copy of the array.
If you need to avoid making a copy, just access the
PredictedInstance.points["xy"] attribute directly. This will not replace
invisible points with np.nan.
Source code in sleap_io/model/instance.py
def numpy(
self,
invisible_as_nan: bool = True,
scores: bool = False,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `PredictedInstance.points["xy"]` is.
scores: If `True`, the score associated with each point will be
included in the output.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
If `scores` is `True`, the array will have shape `(n_nodes, 3)` with the
third column containing the score associated with each point.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the
`PredictedInstance.points["xy"]` attribute directly. This will not replace
invisible points with `np.nan`.
"""
if invisible_as_nan:
pts = np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
pts = self.points["xy"].copy()
if scores:
return np.column_stack((pts, self.points["score"]))
else:
return pts
replace_skeleton(new_skeleton, node_names_map=None)
¶
Replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_skeleton
|
Skeleton
|
The new |
required |
node_names_map
|
dict[str, str] | None
|
Dictionary mapping nodes in the old skeleton to nodes in the new skeleton. Keys and values should be specified as lists of strings. If not provided, only nodes with identical names will be mapped. Points associated with unmapped nodes will be removed. |
None
|
Notes
This method will update the PredictedInstance.skeleton attribute and the
PredictedInstance.points attribute in place (a copy is made of the points
array).
It is recommended to use Labels.replace_skeleton instead of this method if
more flexible node mapping is required.
Source code in sleap_io/model/instance.py
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `PredictedInstance.skeleton` attribute and the
`PredictedInstance.points` attribute in place (a copy is made of the points
array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# Update the points.
new_points = PredictedPointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
update_skeleton(names_only=False)
¶
Update or replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names_only
|
bool
|
If |
False
|
Source code in sleap_io/model/instance.py
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PredictedPointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
PredictedPointsArray
¶
Bases: sleap_io.model.instance.PointsArray
A specialized array for storing predicted instance points data with scores.
This extends the PointsArray class to include score information for each point.
The structured dtype includes the following fields
- xy: A float64 array of shape (2,) containing the x, y coordinates
- score: A float64 containing the confidence score for the point
- visible: A boolean indicating if the point is visible
- complete: A boolean indicating if the point is complete
- name: An object dtype containing the name of the node
Methods:
| Name | Description |
|---|---|
from_array |
Convert an existing array to a PredictedPointsArray with appropriate dtype. |
from_dict |
Create a PredictedPointsArray from a dictionary of node points. |
Attributes:
| Name | Type | Description |
|---|---|---|
__doc__ |
str(object='') -> str |
|
__firstlineno__ |
int([x]) -> integer |
|
__module__ |
str(object='') -> str |
|
__static_attributes__ |
Built-in immutable sequence. |
Source code in sleap_io/model/instance.py
class PredictedPointsArray(PointsArray):
"""A specialized array for storing predicted instance points data with scores.
This extends the PointsArray class to include score information for each point.
The structured dtype includes the following fields:
- xy: A float64 array of shape (2,) containing the x, y coordinates
- score: A float64 containing the confidence score for the point
- visible: A boolean indicating if the point is visible
- complete: A boolean indicating if the point is complete
- name: An object dtype containing the name of the node
"""
@classmethod
def _get_dtype(cls):
"""Get the dtype for predicted points array with scores.
Returns:
np.dtype: A structured numpy dtype with fields for xy coordinates,
score, visible flag, complete flag, and node names.
"""
# Cache the dtype at the class level for performance
# Use cls.__dict__ to check if defined on this class (not inherited)
if "_cached_dtype" not in cls.__dict__:
cls._cached_dtype = np.dtype(
[
("xy", "<f8", (2,)), # 64-bit (8-byte) little-endian double, ndim=2
("score", "<f8"), # 64-bit (8-byte) little-endian double
("visible", "bool"),
("complete", "bool"),
(
"name",
"O",
), # object dtype to store pointers to python string objects
]
)
return cls._cached_dtype
@classmethod
def from_array(cls, array: np.ndarray) -> "PredictedPointsArray":
"""Convert an existing array to a PredictedPointsArray with appropriate dtype.
Args:
array: A numpy array to convert. Can be a structured array or a regular
array. If a regular array, it is assumed to have columns for x, y
coordinates, scores, and optionally visible and complete flags.
Returns:
PredictedPointsArray: A structured array view of the input data with the
appropriate dtype.
Notes:
If the input is a structured array with fields matching the target dtype,
those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as the score
- Fourth column (if present) is interpreted as visible flag
- Fifth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
dtype = cls._get_dtype()
# If already the right type, just view as PredictedPointsArray
if isinstance(array, np.ndarray) and array.dtype == dtype:
return array.view(cls)
# Otherwise, create a new array with the right dtype
new_array = np.empty(len(array), dtype=dtype).view(cls)
# Copy available fields
if isinstance(array, np.ndarray) and array.dtype.fields is not None:
# Structured array, copy matching fields
for field_name in dtype.names:
if field_name in array.dtype.names:
new_array[field_name] = array[field_name]
elif isinstance(array, np.ndarray):
# Regular array, assume x, y coordinates
new_array["xy"] = array[:, 0:2]
# Default visibility based on NaN
new_array["visible"] = ~np.isnan(array[:, 0])
# If there's a third column, assume it's the score
if array.shape[1] >= 3:
new_array["score"] = array[:, 2]
# If there are more columns, assume they are visible and complete
if array.shape[1] >= 4:
new_array["visible"] = array[:, 3].astype(bool)
if array.shape[1] >= 5:
new_array["complete"] = array[:, 4].astype(bool)
return new_array
@classmethod
def from_dict(cls, points_dict: dict, skeleton: Skeleton) -> "PredictedPointsArray":
"""Create a PredictedPointsArray from a dictionary of node points.
Args:
points_dict: A dictionary mapping nodes (as Node objects, indices, or
strings) to point data. Each point should be an array-like with at least
2 elements for x, y coordinates, and optionally score, visible, and
complete flags.
skeleton: The Skeleton object that defines the nodes.
Returns:
PredictedPointsArray: A structured array with the appropriate dtype
containing the point data from the dictionary.
Notes:
For each entry in the points_dict:
- First two values are treated as x, y coordinates
- Third value (if present) is treated as score
- Fourth value (if present) is treated as visible flag
- Fifth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
points = cls.empty(len(skeleton))
for node, data in points_dict.items():
if isinstance(node, (Node, str)):
node = skeleton.index(node)
points[node]["xy"] = data[:2]
# Score is the third element
idx = 2
if len(data) > idx:
points[node]["score"] = data[idx]
idx += 1
# Visibility is the fourth element (or third if no score)
if len(data) > idx:
points[node]["visible"] = data[idx]
else:
points[node]["visible"] = ~np.isnan(data[0])
idx += 1
# Completeness is the fifth element (or fourth if no score)
if len(data) > idx:
points[node]["complete"] = data[idx]
return points
__doc__ = 'A specialized array for storing predicted instance points data with scores.\n\nThis extends the PointsArray class to include score information for each point.\n\nThe structured dtype includes the following fields:\n - xy: A float64 array of shape (2,) containing the x, y coordinates\n - score: A float64 containing the confidence score for the point\n - visible: A boolean indicating if the point is visible\n - complete: A boolean indicating if the point is complete\n - name: An object dtype containing the name of the node\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 170
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__module__ = 'sleap_io.model.instance'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
from_array(array)
classmethod
¶
Convert an existing array to a PredictedPointsArray with appropriate dtype.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
array
|
ndarray
|
A numpy array to convert. Can be a structured array or a regular array. If a regular array, it is assumed to have columns for x, y coordinates, scores, and optionally visible and complete flags. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PredictedPointsArray |
PredictedPointsArray
|
A structured array view of the input data with the appropriate dtype. |
Notes
If the input is a structured array with fields matching the target dtype, those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as the score
- Fourth column (if present) is interpreted as visible flag
- Fifth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x coordinate.
Source code in sleap_io/model/instance.py
@classmethod
def from_array(cls, array: np.ndarray) -> "PredictedPointsArray":
"""Convert an existing array to a PredictedPointsArray with appropriate dtype.
Args:
array: A numpy array to convert. Can be a structured array or a regular
array. If a regular array, it is assumed to have columns for x, y
coordinates, scores, and optionally visible and complete flags.
Returns:
PredictedPointsArray: A structured array view of the input data with the
appropriate dtype.
Notes:
If the input is a structured array with fields matching the target dtype,
those fields will be copied. Otherwise, a best-effort conversion is made:
- First two columns (or first 2D element) are interpreted as x, y coords
- Third column (if present) is interpreted as the score
- Fourth column (if present) is interpreted as visible flag
- Fifth column (if present) is interpreted as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
dtype = cls._get_dtype()
# If already the right type, just view as PredictedPointsArray
if isinstance(array, np.ndarray) and array.dtype == dtype:
return array.view(cls)
# Otherwise, create a new array with the right dtype
new_array = np.empty(len(array), dtype=dtype).view(cls)
# Copy available fields
if isinstance(array, np.ndarray) and array.dtype.fields is not None:
# Structured array, copy matching fields
for field_name in dtype.names:
if field_name in array.dtype.names:
new_array[field_name] = array[field_name]
elif isinstance(array, np.ndarray):
# Regular array, assume x, y coordinates
new_array["xy"] = array[:, 0:2]
# Default visibility based on NaN
new_array["visible"] = ~np.isnan(array[:, 0])
# If there's a third column, assume it's the score
if array.shape[1] >= 3:
new_array["score"] = array[:, 2]
# If there are more columns, assume they are visible and complete
if array.shape[1] >= 4:
new_array["visible"] = array[:, 3].astype(bool)
if array.shape[1] >= 5:
new_array["complete"] = array[:, 4].astype(bool)
return new_array
from_dict(points_dict, skeleton)
classmethod
¶
Create a PredictedPointsArray from a dictionary of node points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points_dict
|
dict
|
A dictionary mapping nodes (as Node objects, indices, or strings) to point data. Each point should be an array-like with at least 2 elements for x, y coordinates, and optionally score, visible, and complete flags. |
required |
skeleton
|
Skeleton
|
The Skeleton object that defines the nodes. |
required |
Returns:
| Name | Type | Description |
|---|---|---|
PredictedPointsArray |
PredictedPointsArray
|
A structured array with the appropriate dtype containing the point data from the dictionary. |
Notes
For each entry in the points_dict: - First two values are treated as x, y coordinates - Third value (if present) is treated as score - Fourth value (if present) is treated as visible flag - Fifth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x coordinate.
Source code in sleap_io/model/instance.py
@classmethod
def from_dict(cls, points_dict: dict, skeleton: Skeleton) -> "PredictedPointsArray":
"""Create a PredictedPointsArray from a dictionary of node points.
Args:
points_dict: A dictionary mapping nodes (as Node objects, indices, or
strings) to point data. Each point should be an array-like with at least
2 elements for x, y coordinates, and optionally score, visible, and
complete flags.
skeleton: The Skeleton object that defines the nodes.
Returns:
PredictedPointsArray: A structured array with the appropriate dtype
containing the point data from the dictionary.
Notes:
For each entry in the points_dict:
- First two values are treated as x, y coordinates
- Third value (if present) is treated as score
- Fourth value (if present) is treated as visible flag
- Fifth value (if present) is treated as complete flag
If visibility is not provided, it is inferred from NaN values in the x
coordinate.
"""
points = cls.empty(len(skeleton))
for node, data in points_dict.items():
if isinstance(node, (Node, str)):
node = skeleton.index(node)
points[node]["xy"] = data[:2]
# Score is the third element
idx = 2
if len(data) > idx:
points[node]["score"] = data[idx]
idx += 1
# Visibility is the fourth element (or third if no score)
if len(data) > idx:
points[node]["visible"] = data[idx]
else:
points[node]["visible"] = ~np.isnan(data[0])
idx += 1
# Completeness is the fifth element (or fourth if no score)
if len(data) > idx:
points[node]["complete"] = data[idx]
return points
Skeleton
¶
A description of a set of landmark types and connections between them.
Skeletons are represented by a directed graph composed of a set of Nodes (landmark
types such as body parts) and Edges (connections between parts).
Attributes:
| Name | Type | Description |
|---|---|---|
nodes |
A list of |
|
edges |
A list of |
|
symmetries |
A list of |
|
name |
A descriptive name for the |
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Ensure nodes are |
__contains__ |
Check if a node is in the skeleton. |
__getitem__ |
Return a |
__init__ |
Method generated by attrs for class Skeleton. |
__len__ |
Return the number of nodes in the skeleton. |
__replace__ |
Method generated by attrs for class Skeleton. |
__repr__ |
Return a readable representation of the skeleton. |
__setattr__ |
Method generated by attrs for class Skeleton. |
add_edge |
Add an |
add_edges |
Add multiple |
add_node |
Add a |
add_nodes |
Add multiple |
add_symmetries |
Add multiple |
add_symmetry |
Add a symmetry relationship to the skeleton. |
get_flipped_node_inds |
Returns node indices that should be switched when horizontally flipping. |
index |
Return the index of a node specified as a |
match_nodes |
Return the order of nodes in the skeleton. |
matches |
Check if this skeleton matches another skeleton's structure. |
node_similarities |
Calculate node overlap metrics with another skeleton. |
rebuild_cache |
Rebuild the node name/index to |
remove_node |
Remove a single node from the skeleton. |
remove_nodes |
Remove nodes from the skeleton. |
rename_node |
Rename a single node in the skeleton. |
rename_nodes |
Rename nodes in the skeleton. |
reorder_nodes |
Reorder nodes in the skeleton. |
require_node |
Return a |
Source code in sleap_io/model/skeleton.py
@define(eq=False)
class Skeleton:
"""A description of a set of landmark types and connections between them.
Skeletons are represented by a directed graph composed of a set of `Node`s (landmark
types such as body parts) and `Edge`s (connections between parts).
Attributes:
nodes: A list of `Node`s. May be specified as a list of strings to create new
nodes from their names.
edges: A list of `Edge`s. May be specified as a list of 2-tuples of string names
or integer indices of `nodes`. Each edge corresponds to a pair of source and
destination nodes forming a directed edge.
symmetries: A list of `Symmetry`s. Each symmetry corresponds to symmetric body
parts, such as `"left eye", "right eye"`. This is used when applying flip
(reflection) augmentation to images in order to appropriately swap the
indices of symmetric landmarks.
name: A descriptive name for the `Skeleton`.
"""
def _nodes_on_setattr(self, attr, new_nodes):
"""Callback to update caches when nodes are set."""
self.rebuild_cache(nodes=new_nodes)
return new_nodes
nodes: list[Node] = field(
factory=list,
on_setattr=_nodes_on_setattr,
)
edges: list[Edge] = field(factory=list)
symmetries: list[Symmetry] = field(factory=list)
name: str | None = None
_name_to_node_cache: dict[str, Node] = field(init=False, repr=False, eq=False)
_node_to_ind_cache: dict[Node, int] = field(init=False, repr=False, eq=False)
def __attrs_post_init__(self):
"""Ensure nodes are `Node`s, edges are `Edge`s, and `Node` map is updated."""
self._convert_nodes()
self._convert_edges()
self._convert_symmetries()
self.rebuild_cache()
def _convert_nodes(self):
"""Convert nodes to `Node` objects if needed."""
if isinstance(self.nodes, np.ndarray):
object.__setattr__(self, "nodes", self.nodes.tolist())
for i, node in enumerate(self.nodes):
if type(node) is str:
self.nodes[i] = Node(node)
def _convert_edges(self):
"""Convert list of edge names or integers to `Edge` objects if needed."""
if isinstance(self.edges, np.ndarray):
self.edges = self.edges.tolist()
node_names = self.node_names
for i, edge in enumerate(self.edges):
if type(edge) is Edge:
continue
src, dst = edge
if type(src) is str:
try:
src = node_names.index(src)
except ValueError:
raise ValueError(
f"Node '{src}' specified in the edge list is not in the nodes."
)
if type(src) is int or (
np.isscalar(src) and np.issubdtype(src.dtype, np.integer)
):
src = self.nodes[src]
if type(dst) is str:
try:
dst = node_names.index(dst)
except ValueError:
raise ValueError(
f"Node '{dst}' specified in the edge list is not in the nodes."
)
if type(dst) is int or (
np.isscalar(dst) and np.issubdtype(dst.dtype, np.integer)
):
dst = self.nodes[dst]
self.edges[i] = Edge(src, dst)
def _convert_symmetries(self):
"""Convert list of symmetric node names or integers to `Symmetry` objects."""
if isinstance(self.symmetries, np.ndarray):
self.symmetries = self.symmetries.tolist()
node_names = self.node_names
for i, symmetry in enumerate(self.symmetries):
if type(symmetry) is Symmetry:
continue
node1, node2 = symmetry
if type(node1) is str:
try:
node1 = node_names.index(node1)
except ValueError:
raise ValueError(
f"Node '{node1}' specified in the symmetry list is not in the "
"nodes."
)
if type(node1) is int or (
np.isscalar(node1) and np.issubdtype(node1.dtype, np.integer)
):
node1 = self.nodes[node1]
if type(node2) is str:
try:
node2 = node_names.index(node2)
except ValueError:
raise ValueError(
f"Node '{node2}' specified in the symmetry list is not in the "
"nodes."
)
if type(node2) is int or (
np.isscalar(node2) and np.issubdtype(node2.dtype, np.integer)
):
node2 = self.nodes[node2]
self.symmetries[i] = Symmetry({node1, node2})
def rebuild_cache(self, nodes: list[Node] | None = None):
"""Rebuild the node name/index to `Node` map caches.
Args:
nodes: A list of `Node` objects to update the cache with. If not provided,
the cache will be updated with the current nodes in the skeleton. If
nodes are provided, the cache will be updated with the provided nodes,
but the current nodes in the skeleton will not be updated. Default is
`None`.
Notes:
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or `Node` object.
This is done automatically when nodes are added or removed from the skeleton
using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node
list directly.
"""
if nodes is None:
nodes = self.nodes
self._name_to_node_cache = {node.name: node for node in nodes}
self._node_to_ind_cache = {node: i for i, node in enumerate(nodes)}
@property
def node_names(self) -> list[str]:
"""Names of the nodes associated with this skeleton as a list of strings."""
return [node.name for node in self.nodes]
@property
def edge_inds(self) -> list[tuple[int, int]]:
"""Edges indices as a list of 2-tuples."""
return [
(self.nodes.index(edge.source), self.nodes.index(edge.destination))
for edge in self.edges
]
@property
def edge_names(self) -> list[str, str]:
"""Edge names as a list of 2-tuples with string node names."""
return [(edge.source.name, edge.destination.name) for edge in self.edges]
@property
def symmetry_inds(self) -> list[tuple[int, int]]:
"""Symmetry indices as a list of 2-tuples."""
return [
tuple(sorted((self.index(symmetry[0]), self.index(symmetry[1]))))
for symmetry in self.symmetries
]
@property
def symmetry_names(self) -> list[str, str]:
"""Symmetry names as a list of 2-tuples with string node names."""
return [
(self.nodes[i].name, self.nodes[j].name) for (i, j) in self.symmetry_inds
]
def get_flipped_node_inds(self) -> list[int]:
"""Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when
doing data augmentation.
Example:
>>> skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"])
>>> skel.add_symmetry("B_left", "B_right")
>>> skel.add_symmetry("D_left", "D_right")
>>> skel.flipped_node_inds
[0, 2, 1, 3, 5, 4]
>>> pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> pose[skel.flipped_node_inds]
array([[0, 0],
[2, 2],
[1, 1],
[3, 3],
[5, 5],
[4, 4]])
"""
flip_idx = np.arange(len(self.nodes))
if len(self.symmetries) > 0:
symmetry_inds = np.array(
[(self.index(a), self.index(b)) for a, b in self.symmetries]
)
flip_idx[symmetry_inds[:, 0]] = symmetry_inds[:, 1]
flip_idx[symmetry_inds[:, 1]] = symmetry_inds[:, 0]
flip_idx = flip_idx.tolist()
return flip_idx
def __len__(self) -> int:
"""Return the number of nodes in the skeleton."""
return len(self.nodes)
def __repr__(self) -> str:
"""Return a readable representation of the skeleton."""
nodes = ", ".join([f'"{node}"' for node in self.node_names])
return f"Skeleton(nodes=[{nodes}], edges={self.edge_inds})"
def index(self, node: Node | str) -> int:
"""Return the index of a node specified as a `Node` or string name."""
if type(node) is str:
return self.index(self._name_to_node_cache[node])
elif type(node) is Node:
return self._node_to_ind_cache[node]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {node}")
def __getitem__(self, idx: NodeOrIndex) -> Node:
"""Return a `Node` when indexing by name or integer."""
if type(idx) is int:
return self.nodes[idx]
elif type(idx) is str:
return self._name_to_node_cache[idx]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {idx}")
def __contains__(self, node: NodeOrIndex) -> bool:
"""Check if a node is in the skeleton."""
if type(node) is str:
return node in self._name_to_node_cache
elif type(node) is Node:
return node in self.nodes
elif type(node) is int:
return 0 <= node < len(self.nodes)
else:
raise ValueError(f"Invalid node type for skeleton: {node}")
def add_node(self, node: Node | str):
"""Add a `Node` to the skeleton.
Args:
node: A `Node` object or a string name to create a new node.
Raises:
ValueError: If the node already exists in the skeleton or if the node is
not specified as a `Node` or string.
"""
if node in self:
raise ValueError(f"Node '{node}' already exists in the skeleton.")
if type(node) is str:
node = Node(node)
if type(node) is not Node:
raise ValueError(f"Invalid node type: {node} ({type(node)})")
self.nodes.append(node)
# Atomic update of the cache.
self._name_to_node_cache[node.name] = node
self._node_to_ind_cache[node] = len(self.nodes) - 1
def add_nodes(self, nodes: list[Node | str]):
"""Add multiple `Node`s to the skeleton.
Args:
nodes: A list of `Node` objects or string names to create new nodes.
"""
for node in nodes:
self.add_node(node)
def require_node(self, node: NodeOrIndex, add_missing: bool = True) -> Node:
"""Return a `Node` object, handling indexing and adding missing nodes.
Args:
node: A `Node` object, name or index.
add_missing: If `True`, missing nodes will be added to the skeleton. If
`False`, an error will be raised if the node is not found. Default is
`True`.
Returns:
The `Node` object.
Raises:
IndexError: If the node is not found in the skeleton and `add_missing` is
`False`.
"""
if node not in self:
if add_missing:
self.add_node(node)
else:
raise IndexError(f"Node '{node}' not found in the skeleton.")
if type(node) is Node:
return node
return self[node]
def add_edge(
self,
src: NodeOrIndex | Edge | tuple[NodeOrIndex, NodeOrIndex],
dst: NodeOrIndex | None = None,
):
"""Add an `Edge` to the skeleton.
Args:
src: The source node specified as a `Node`, name or index.
dst: The destination node specified as a `Node`, name or index.
"""
edge = None
if type(src) is tuple:
src, dst = src
if is_node_or_index(src):
if not is_node_or_index(dst):
raise ValueError("Destination node must be specified.")
src = self.require_node(src)
dst = self.require_node(dst)
edge = Edge(src, dst)
if type(src) is Edge:
edge = src
if edge not in self.edges:
self.edges.append(edge)
def add_edges(self, edges: list[Edge | tuple[NodeOrIndex, NodeOrIndex]]):
"""Add multiple `Edge`s to the skeleton.
Args:
edges: A list of `Edge` objects or 2-tuples of source and destination nodes.
"""
for edge in edges:
self.add_edge(edge)
def add_symmetry(
self, node1: Symmetry | NodeOrIndex = None, node2: NodeOrIndex | None = None
):
"""Add a symmetry relationship to the skeleton.
Args:
node1: The first node specified as a `Node`, name or index. If a `Symmetry`
object is provided, it will be added directly to the skeleton.
node2: The second node specified as a `Node`, name or index.
"""
symmetry = None
if type(node1) is Symmetry:
symmetry = node1
node1, node2 = symmetry
node1 = self.require_node(node1)
node2 = self.require_node(node2)
if symmetry is None:
symmetry = Symmetry({node1, node2})
if symmetry not in self.symmetries:
self.symmetries.append(symmetry)
def add_symmetries(
self, symmetries: list[Symmetry | tuple[NodeOrIndex, NodeOrIndex]]
):
"""Add multiple `Symmetry` relationships to the skeleton.
Args:
symmetries: A list of `Symmetry` objects or 2-tuples of symmetric nodes.
"""
for symmetry in symmetries:
self.add_symmetry(*symmetry)
def rename_nodes(self, name_map: dict[NodeOrIndex, str] | list[str]):
"""Rename nodes in the skeleton.
Args:
name_map: A dictionary mapping old node names to new node names. Keys can be
specified as `Node` objects, integer indices, or string names. Values
must be specified as string names.
If a list of strings is provided of the same length as the current
nodes, the nodes will be renamed to the names in the list in order.
Raises:
ValueError: If the new node names exist in the skeleton or if the old node
names are not found in the skeleton.
Notes:
This method should always be used when renaming nodes in the skeleton as it
handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton **do NOT need to be updated**
as the nodes are stored by reference in the skeleton, so changes are
reflected automatically.
Example:
>>> skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")])
>>> skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"})
>>> skel.node_names
["X", "Y", "Z"]
>>> skel.rename_nodes(["a", "b", "c"])
>>> skel.node_names
["a", "b", "c"]
"""
if type(name_map) is list:
if len(name_map) != len(self.nodes):
raise ValueError(
"List of new node names must be the same length as the current "
"nodes."
)
name_map = {node: name for node, name in zip(self.nodes, name_map)}
for old_name, new_name in name_map.items():
if type(old_name) is Node:
old_name = old_name.name
if type(old_name) is int:
old_name = self.nodes[old_name].name
if old_name not in self._name_to_node_cache:
raise ValueError(f"Node '{old_name}' not found in the skeleton.")
if new_name in self._name_to_node_cache:
raise ValueError(f"Node '{new_name}' already exists in the skeleton.")
node = self._name_to_node_cache[old_name]
node.name = new_name
self._name_to_node_cache[new_name] = node
del self._name_to_node_cache[old_name]
def rename_node(self, old_name: NodeOrIndex, new_name: str):
"""Rename a single node in the skeleton.
Args:
old_name: The name of the node to rename. Can also be specified as an
integer index or `Node` object.
new_name: The new name for the node.
"""
self.rename_nodes({old_name: new_name})
def remove_nodes(self, nodes: list[NodeOrIndex]):
"""Remove nodes from the skeleton.
Args:
nodes: A list of node names, indices, or `Node` objects to remove.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed nodes will also
be removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`instance.update_nodes()` on each instance that uses this skeleton.
"""
# Standardize input and make a pre-mutation copy before keys are changed.
rm_node_objs = [self.require_node(node, add_missing=False) for node in nodes]
# Remove nodes from the skeleton.
for node in rm_node_objs:
self.nodes.remove(node)
del self._name_to_node_cache[node.name]
# Remove edges connected to the removed nodes.
self.edges = [
edge
for edge in self.edges
if edge.source not in rm_node_objs and edge.destination not in rm_node_objs
]
# Remove symmetries connected to the removed nodes.
self.symmetries = [
symmetry
for symmetry in self.symmetries
if symmetry.nodes.isdisjoint(rm_node_objs)
]
# Update node index map.
self.rebuild_cache()
def remove_node(self, node: NodeOrIndex):
"""Remove a single node from the skeleton.
Args:
node: The node to remove. Can be specified as a string name, integer index,
or `Node` object.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed node will also be
removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
self.remove_nodes([node])
def reorder_nodes(self, new_order: list[NodeOrIndex]):
"""Reorder nodes in the skeleton.
Args:
new_order: A list of node names, indices, or `Node` objects specifying the
new order of the nodes.
Raises:
ValueError: If the new order of nodes is not the same length as the current
nodes.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Warning:
After reordering, instances using this skeleton do not need to be updated as
the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the `Instance.points` dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
`Labels.reorder_nodes()` method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
if len(new_order) != len(self.nodes):
raise ValueError(
"New order of nodes must be the same length as the current nodes."
)
new_nodes = [self.require_node(node, add_missing=False) for node in new_order]
self.nodes = new_nodes
def match_nodes(self, other_nodes: list[str, Node]) -> tuple[list[int], list[int]]:
"""Return the order of nodes in the skeleton.
Args:
other_nodes: A list of node names or `Node` objects.
Returns:
A tuple of `skeleton_inds, `other_inds`.
`skeleton_inds` contains the indices of the nodes in the skeleton that match
the input nodes.
`other_inds` contains the indices of the input nodes that match the nodes in
the skeleton.
These can be used to reorder point data to match the order of nodes in the
skeleton.
See also: match_nodes_cached
"""
if isinstance(other_nodes, np.ndarray):
other_nodes = other_nodes.tolist()
if type(other_nodes) is not tuple:
other_nodes = [x.name if type(x) is Node else x for x in other_nodes]
skeleton_inds, other_inds = match_nodes_cached(
tuple(self.node_names), tuple(other_nodes)
)
return list(skeleton_inds), list(other_inds)
def matches(self, other: "Skeleton", require_same_order: bool = False) -> bool:
"""Check if this skeleton matches another skeleton's structure.
Args:
other: Another skeleton to compare with.
require_same_order: If True, nodes must be in the same order.
If False, only the node names and edges need to match.
Returns:
True if the skeletons match, False otherwise.
Notes:
Two skeletons match if they have the same nodes (by name) and edges.
If require_same_order is True, the nodes must also be in the same order.
"""
# Check if we have the same number of nodes
if len(self.nodes) != len(other.nodes):
return False
# Check node names
if require_same_order:
if self.node_names != other.node_names:
return False
else:
if set(self.node_names) != set(other.node_names):
return False
# Check edges (considering node name mapping if order differs)
if len(self.edges) != len(other.edges):
return False
# Create edge sets for comparison
self_edge_set = {
(edge.source.name, edge.destination.name) for edge in self.edges
}
other_edge_set = {
(edge.source.name, edge.destination.name) for edge in other.edges
}
if self_edge_set != other_edge_set:
return False
# Check symmetries
if len(self.symmetries) != len(other.symmetries):
return False
self_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in self.symmetries
}
other_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in other.symmetries
}
return self_sym_set == other_sym_set
def node_similarities(self, other: "Skeleton") -> dict[str, float]:
"""Calculate node overlap metrics with another skeleton.
Args:
other: Another skeleton to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_common': Number of nodes in common
- 'n_self_only': Number of nodes only in this skeleton
- 'n_other_only': Number of nodes only in the other skeleton
- 'jaccard': Jaccard similarity (intersection/union)
- 'dice': Dice coefficient (2*intersection/(n_self + n_other))
"""
self_nodes = set(self.node_names)
other_nodes = set(other.node_names)
n_common = len(self_nodes & other_nodes)
n_self_only = len(self_nodes - other_nodes)
n_other_only = len(other_nodes - self_nodes)
n_union = len(self_nodes | other_nodes)
jaccard = n_common / n_union if n_union > 0 else 0
dice = (
2 * n_common / (len(self_nodes) + len(other_nodes))
if (len(self_nodes) + len(other_nodes)) > 0
else 0
)
return {
"n_common": n_common,
"n_self_only": n_self_only,
"n_other_only": n_other_only,
"jaccard": jaccard,
"dice": dice,
}
__annotations__ = {'nodes': 'list[Node]', 'edges': 'list[Edge]', 'symmetries': 'list[Symmetry]', 'name': 'str | None', '_name_to_node_cache': 'dict[str, Node]', '_node_to_ind_cache': 'dict[Node, int]'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = True
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=False, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A description of a set of landmark types and connections between them.\n\nSkeletons are represented by a directed graph composed of a set of `Node`s (landmark\ntypes such as body parts) and `Edge`s (connections between parts).\n\nAttributes:\n nodes: A list of `Node`s. May be specified as a list of strings to create new\n nodes from their names.\n edges: A list of `Edge`s. May be specified as a list of 2-tuples of string names\n or integer indices of `nodes`. Each edge corresponds to a pair of source and\n destination nodes forming a directed edge.\n symmetries: A list of `Symmetry`s. Each symmetry corresponds to symmetric body\n parts, such as `"left eye", "right eye"`. This is used when applying flip\n (reflection) augmentation to images in order to appropriately swap the\n indices of symmetric landmarks.\n name: A descriptive name for the `Skeleton`.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 96
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__match_args__ = ('nodes', 'edges', 'symmetries', 'name')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.skeleton'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__slots__ = ('nodes', 'edges', 'symmetries', 'name', '_name_to_node_cache', '_node_to_ind_cache', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ('_name_to_node_cache', '_node_to_ind_cache', 'edges', 'nodes', 'symmetries')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
edge_inds
property
¶
Edges indices as a list of 2-tuples.
edge_names
property
¶
Edge names as a list of 2-tuples with string node names.
node_names
property
¶
Names of the nodes associated with this skeleton as a list of strings.
symmetry_inds
property
¶
Symmetry indices as a list of 2-tuples.
symmetry_names
property
¶
Symmetry names as a list of 2-tuples with string node names.
__attrs_post_init__()
¶
Ensure nodes are Nodes, edges are Edges, and Node map is updated.
__contains__(node)
¶
Check if a node is in the skeleton.
Source code in sleap_io/model/skeleton.py
def __contains__(self, node: NodeOrIndex) -> bool:
"""Check if a node is in the skeleton."""
if type(node) is str:
return node in self._name_to_node_cache
elif type(node) is Node:
return node in self.nodes
elif type(node) is int:
return 0 <= node < len(self.nodes)
else:
raise ValueError(f"Invalid node type for skeleton: {node}")
__getitem__(idx)
¶
Return a Node when indexing by name or integer.
Source code in sleap_io/model/skeleton.py
__init__(nodes=NOTHING, edges=NOTHING, symmetries=NOTHING, name=None)
¶
Method generated by attrs for class Skeleton.
Source code in sleap_io/model/skeleton.py
"""Data model for skeletons.
Skeletons are collections of nodes and edges which describe the landmarks associated
with a pose model. The edges represent the connections between them and may be used
differently depending on the underlying pose model.
"""
from __future__ import annotations
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class Skeleton.
__repr__()
¶
__setattr__(name, val)
¶
Method generated by attrs for class Skeleton.
add_edge(src, dst=None)
¶
Add an Edge to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src
|
Union | Edge | tuple[Union, Union]
|
The source node specified as a |
required |
dst
|
Union | None
|
The destination node specified as a |
None
|
Source code in sleap_io/model/skeleton.py
def add_edge(
self,
src: NodeOrIndex | Edge | tuple[NodeOrIndex, NodeOrIndex],
dst: NodeOrIndex | None = None,
):
"""Add an `Edge` to the skeleton.
Args:
src: The source node specified as a `Node`, name or index.
dst: The destination node specified as a `Node`, name or index.
"""
edge = None
if type(src) is tuple:
src, dst = src
if is_node_or_index(src):
if not is_node_or_index(dst):
raise ValueError("Destination node must be specified.")
src = self.require_node(src)
dst = self.require_node(dst)
edge = Edge(src, dst)
if type(src) is Edge:
edge = src
if edge not in self.edges:
self.edges.append(edge)
add_edges(edges)
¶
Add multiple Edges to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
edges
|
list[Edge | tuple[Union, Union]]
|
A list of |
required |
add_node(node)
¶
Add a Node to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Node | str
|
A |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the node already exists in the skeleton or if the node is
not specified as a |
Source code in sleap_io/model/skeleton.py
def add_node(self, node: Node | str):
"""Add a `Node` to the skeleton.
Args:
node: A `Node` object or a string name to create a new node.
Raises:
ValueError: If the node already exists in the skeleton or if the node is
not specified as a `Node` or string.
"""
if node in self:
raise ValueError(f"Node '{node}' already exists in the skeleton.")
if type(node) is str:
node = Node(node)
if type(node) is not Node:
raise ValueError(f"Invalid node type: {node} ({type(node)})")
self.nodes.append(node)
# Atomic update of the cache.
self._name_to_node_cache[node.name] = node
self._node_to_ind_cache[node] = len(self.nodes) - 1
add_nodes(nodes)
¶
Add multiple Nodes to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Node | str]
|
A list of |
required |
add_symmetries(symmetries)
¶
Add multiple Symmetry relationships to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symmetries
|
list[Symmetry | tuple[Union, Union]]
|
A list of |
required |
Source code in sleap_io/model/skeleton.py
add_symmetry(node1=None, node2=None)
¶
Add a symmetry relationship to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node1
|
Symmetry | Union
|
The first node specified as a |
None
|
node2
|
Union | None
|
The second node specified as a |
None
|
Source code in sleap_io/model/skeleton.py
def add_symmetry(
self, node1: Symmetry | NodeOrIndex = None, node2: NodeOrIndex | None = None
):
"""Add a symmetry relationship to the skeleton.
Args:
node1: The first node specified as a `Node`, name or index. If a `Symmetry`
object is provided, it will be added directly to the skeleton.
node2: The second node specified as a `Node`, name or index.
"""
symmetry = None
if type(node1) is Symmetry:
symmetry = node1
node1, node2 = symmetry
node1 = self.require_node(node1)
node2 = self.require_node(node2)
if symmetry is None:
symmetry = Symmetry({node1, node2})
if symmetry not in self.symmetries:
self.symmetries.append(symmetry)
get_flipped_node_inds()
¶
Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when doing data augmentation.
Example
skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"]) skel.add_symmetry("B_left", "B_right") skel.add_symmetry("D_left", "D_right") skel.flipped_node_inds [0, 2, 1, 3, 5, 4] pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]) pose[skel.flipped_node_inds] array([[0, 0], [2, 2], [1, 1], [3, 3], [5, 5], [4, 4]])
Source code in sleap_io/model/skeleton.py
def get_flipped_node_inds(self) -> list[int]:
"""Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when
doing data augmentation.
Example:
>>> skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"])
>>> skel.add_symmetry("B_left", "B_right")
>>> skel.add_symmetry("D_left", "D_right")
>>> skel.flipped_node_inds
[0, 2, 1, 3, 5, 4]
>>> pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> pose[skel.flipped_node_inds]
array([[0, 0],
[2, 2],
[1, 1],
[3, 3],
[5, 5],
[4, 4]])
"""
flip_idx = np.arange(len(self.nodes))
if len(self.symmetries) > 0:
symmetry_inds = np.array(
[(self.index(a), self.index(b)) for a, b in self.symmetries]
)
flip_idx[symmetry_inds[:, 0]] = symmetry_inds[:, 1]
flip_idx[symmetry_inds[:, 1]] = symmetry_inds[:, 0]
flip_idx = flip_idx.tolist()
return flip_idx
index(node)
¶
Return the index of a node specified as a Node or string name.
Source code in sleap_io/model/skeleton.py
def index(self, node: Node | str) -> int:
"""Return the index of a node specified as a `Node` or string name."""
if type(node) is str:
return self.index(self._name_to_node_cache[node])
elif type(node) is Node:
return self._node_to_ind_cache[node]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {node}")
match_nodes(other_nodes)
¶
Return the order of nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other_nodes
|
list[str, Node]
|
A list of node names or |
required |
Returns:
| Type | Description |
|---|---|
tuple[list[int], list[int]]
|
A tuple of
These can be used to reorder point data to match the order of nodes in the skeleton. |
See also: match_nodes_cached
Source code in sleap_io/model/skeleton.py
def match_nodes(self, other_nodes: list[str, Node]) -> tuple[list[int], list[int]]:
"""Return the order of nodes in the skeleton.
Args:
other_nodes: A list of node names or `Node` objects.
Returns:
A tuple of `skeleton_inds, `other_inds`.
`skeleton_inds` contains the indices of the nodes in the skeleton that match
the input nodes.
`other_inds` contains the indices of the input nodes that match the nodes in
the skeleton.
These can be used to reorder point data to match the order of nodes in the
skeleton.
See also: match_nodes_cached
"""
if isinstance(other_nodes, np.ndarray):
other_nodes = other_nodes.tolist()
if type(other_nodes) is not tuple:
other_nodes = [x.name if type(x) is Node else x for x in other_nodes]
skeleton_inds, other_inds = match_nodes_cached(
tuple(self.node_names), tuple(other_nodes)
)
return list(skeleton_inds), list(other_inds)
matches(other, require_same_order=False)
¶
Check if this skeleton matches another skeleton's structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Skeleton
|
Another skeleton to compare with. |
required |
require_same_order
|
bool
|
If True, nodes must be in the same order. If False, only the node names and edges need to match. |
False
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the skeletons match, False otherwise. |
Notes
Two skeletons match if they have the same nodes (by name) and edges. If require_same_order is True, the nodes must also be in the same order.
Source code in sleap_io/model/skeleton.py
def matches(self, other: "Skeleton", require_same_order: bool = False) -> bool:
"""Check if this skeleton matches another skeleton's structure.
Args:
other: Another skeleton to compare with.
require_same_order: If True, nodes must be in the same order.
If False, only the node names and edges need to match.
Returns:
True if the skeletons match, False otherwise.
Notes:
Two skeletons match if they have the same nodes (by name) and edges.
If require_same_order is True, the nodes must also be in the same order.
"""
# Check if we have the same number of nodes
if len(self.nodes) != len(other.nodes):
return False
# Check node names
if require_same_order:
if self.node_names != other.node_names:
return False
else:
if set(self.node_names) != set(other.node_names):
return False
# Check edges (considering node name mapping if order differs)
if len(self.edges) != len(other.edges):
return False
# Create edge sets for comparison
self_edge_set = {
(edge.source.name, edge.destination.name) for edge in self.edges
}
other_edge_set = {
(edge.source.name, edge.destination.name) for edge in other.edges
}
if self_edge_set != other_edge_set:
return False
# Check symmetries
if len(self.symmetries) != len(other.symmetries):
return False
self_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in self.symmetries
}
other_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in other.symmetries
}
return self_sym_set == other_sym_set
node_similarities(other)
¶
Calculate node overlap metrics with another skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Skeleton
|
Another skeleton to compare with. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A dictionary with similarity metrics: - 'n_common': Number of nodes in common - 'n_self_only': Number of nodes only in this skeleton - 'n_other_only': Number of nodes only in the other skeleton - 'jaccard': Jaccard similarity (intersection/union) - 'dice': Dice coefficient (2*intersection/(n_self + n_other)) |
Source code in sleap_io/model/skeleton.py
def node_similarities(self, other: "Skeleton") -> dict[str, float]:
"""Calculate node overlap metrics with another skeleton.
Args:
other: Another skeleton to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_common': Number of nodes in common
- 'n_self_only': Number of nodes only in this skeleton
- 'n_other_only': Number of nodes only in the other skeleton
- 'jaccard': Jaccard similarity (intersection/union)
- 'dice': Dice coefficient (2*intersection/(n_self + n_other))
"""
self_nodes = set(self.node_names)
other_nodes = set(other.node_names)
n_common = len(self_nodes & other_nodes)
n_self_only = len(self_nodes - other_nodes)
n_other_only = len(other_nodes - self_nodes)
n_union = len(self_nodes | other_nodes)
jaccard = n_common / n_union if n_union > 0 else 0
dice = (
2 * n_common / (len(self_nodes) + len(other_nodes))
if (len(self_nodes) + len(other_nodes)) > 0
else 0
)
return {
"n_common": n_common,
"n_self_only": n_self_only,
"n_other_only": n_other_only,
"jaccard": jaccard,
"dice": dice,
}
rebuild_cache(nodes=None)
¶
Rebuild the node name/index to Node map caches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Node] | None
|
A list of |
None
|
Notes
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or Node object.
This is done automatically when nodes are added or removed from the skeleton using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node list directly.
Source code in sleap_io/model/skeleton.py
def rebuild_cache(self, nodes: list[Node] | None = None):
"""Rebuild the node name/index to `Node` map caches.
Args:
nodes: A list of `Node` objects to update the cache with. If not provided,
the cache will be updated with the current nodes in the skeleton. If
nodes are provided, the cache will be updated with the provided nodes,
but the current nodes in the skeleton will not be updated. Default is
`None`.
Notes:
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or `Node` object.
This is done automatically when nodes are added or removed from the skeleton
using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node
list directly.
"""
if nodes is None:
nodes = self.nodes
self._name_to_node_cache = {node.name: node for node in nodes}
self._node_to_ind_cache = {node: i for i, node in enumerate(nodes)}
remove_node(node)
¶
Remove a single node from the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Union
|
The node to remove. Can be specified as a string name, integer index,
or |
required |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Any edges and symmetries that are connected to the removed node will also be removed.
Warning
This method does NOT update instances that use this skeleton to reflect changes.
It is recommended to use the Labels.remove_nodes() method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
Instance.update_skeleton() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def remove_node(self, node: NodeOrIndex):
"""Remove a single node from the skeleton.
Args:
node: The node to remove. Can be specified as a string name, integer index,
or `Node` object.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed node will also be
removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
self.remove_nodes([node])
remove_nodes(nodes)
¶
Remove nodes from the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Union]
|
A list of node names, indices, or |
required |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Any edges and symmetries that are connected to the removed nodes will also be removed.
Warning
This method does NOT update instances that use this skeleton to reflect changes.
It is recommended to use the Labels.remove_nodes() method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
instance.update_nodes() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def remove_nodes(self, nodes: list[NodeOrIndex]):
"""Remove nodes from the skeleton.
Args:
nodes: A list of node names, indices, or `Node` objects to remove.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed nodes will also
be removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`instance.update_nodes()` on each instance that uses this skeleton.
"""
# Standardize input and make a pre-mutation copy before keys are changed.
rm_node_objs = [self.require_node(node, add_missing=False) for node in nodes]
# Remove nodes from the skeleton.
for node in rm_node_objs:
self.nodes.remove(node)
del self._name_to_node_cache[node.name]
# Remove edges connected to the removed nodes.
self.edges = [
edge
for edge in self.edges
if edge.source not in rm_node_objs and edge.destination not in rm_node_objs
]
# Remove symmetries connected to the removed nodes.
self.symmetries = [
symmetry
for symmetry in self.symmetries
if symmetry.nodes.isdisjoint(rm_node_objs)
]
# Update node index map.
self.rebuild_cache()
rename_node(old_name, new_name)
¶
Rename a single node in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_name
|
Union
|
The name of the node to rename. Can also be specified as an
integer index or |
required |
new_name
|
str
|
The new name for the node. |
required |
Source code in sleap_io/model/skeleton.py
rename_nodes(name_map)
¶
Rename nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name_map
|
dict[Union, str] | list[str]
|
A dictionary mapping old node names to new node names. Keys can be
specified as If a list of strings is provided of the same length as the current nodes, the nodes will be renamed to the names in the list in order. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the new node names exist in the skeleton or if the old node names are not found in the skeleton. |
Notes
This method should always be used when renaming nodes in the skeleton as it handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton do NOT need to be updated as the nodes are stored by reference in the skeleton, so changes are reflected automatically.
Example
skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")]) skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"}) skel.node_names ["X", "Y", "Z"] skel.rename_nodes(["a", "b", "c"]) skel.node_names ["a", "b", "c"]
Source code in sleap_io/model/skeleton.py
def rename_nodes(self, name_map: dict[NodeOrIndex, str] | list[str]):
"""Rename nodes in the skeleton.
Args:
name_map: A dictionary mapping old node names to new node names. Keys can be
specified as `Node` objects, integer indices, or string names. Values
must be specified as string names.
If a list of strings is provided of the same length as the current
nodes, the nodes will be renamed to the names in the list in order.
Raises:
ValueError: If the new node names exist in the skeleton or if the old node
names are not found in the skeleton.
Notes:
This method should always be used when renaming nodes in the skeleton as it
handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton **do NOT need to be updated**
as the nodes are stored by reference in the skeleton, so changes are
reflected automatically.
Example:
>>> skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")])
>>> skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"})
>>> skel.node_names
["X", "Y", "Z"]
>>> skel.rename_nodes(["a", "b", "c"])
>>> skel.node_names
["a", "b", "c"]
"""
if type(name_map) is list:
if len(name_map) != len(self.nodes):
raise ValueError(
"List of new node names must be the same length as the current "
"nodes."
)
name_map = {node: name for node, name in zip(self.nodes, name_map)}
for old_name, new_name in name_map.items():
if type(old_name) is Node:
old_name = old_name.name
if type(old_name) is int:
old_name = self.nodes[old_name].name
if old_name not in self._name_to_node_cache:
raise ValueError(f"Node '{old_name}' not found in the skeleton.")
if new_name in self._name_to_node_cache:
raise ValueError(f"Node '{new_name}' already exists in the skeleton.")
node = self._name_to_node_cache[old_name]
node.name = new_name
self._name_to_node_cache[new_name] = node
del self._name_to_node_cache[old_name]
reorder_nodes(new_order)
¶
Reorder nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_order
|
list[Union]
|
A list of node names, indices, or |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the new order of nodes is not the same length as the current nodes. |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Warning
After reordering, instances using this skeleton do not need to be updated as the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the Instance.points dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
Labels.reorder_nodes() method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
Instance.update_skeleton() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def reorder_nodes(self, new_order: list[NodeOrIndex]):
"""Reorder nodes in the skeleton.
Args:
new_order: A list of node names, indices, or `Node` objects specifying the
new order of the nodes.
Raises:
ValueError: If the new order of nodes is not the same length as the current
nodes.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Warning:
After reordering, instances using this skeleton do not need to be updated as
the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the `Instance.points` dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
`Labels.reorder_nodes()` method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
if len(new_order) != len(self.nodes):
raise ValueError(
"New order of nodes must be the same length as the current nodes."
)
new_nodes = [self.require_node(node, add_missing=False) for node in new_order]
self.nodes = new_nodes
require_node(node, add_missing=True)
¶
Return a Node object, handling indexing and adding missing nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Union
|
A |
required |
add_missing
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
Node
|
The |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the node is not found in the skeleton and |
Source code in sleap_io/model/skeleton.py
def require_node(self, node: NodeOrIndex, add_missing: bool = True) -> Node:
"""Return a `Node` object, handling indexing and adding missing nodes.
Args:
node: A `Node` object, name or index.
add_missing: If `True`, missing nodes will be added to the skeleton. If
`False`, an error will be raised if the node is not found. Default is
`True`.
Returns:
The `Node` object.
Raises:
IndexError: If the node is not found in the skeleton and `add_missing` is
`False`.
"""
if node not in self:
if add_missing:
self.add_node(node)
else:
raise IndexError(f"Node '{node}' not found in the skeleton.")
if type(node) is Node:
return node
return self[node]
Track
¶
An object that represents the same animal/object across multiple detections.
This allows tracking of unique entities in the video over time and space.
A Track may also be used to refer to unique identity classes that span multiple
videos, such as "female mouse".
Attributes:
| Name | Type | Description |
|---|---|---|
name |
A name given to this track for identification purposes. |
Notes
Tracks are compared by identity. This means that unique track objects with the
same name are considered to be different.
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class Track. |
__replace__ |
Method generated by attrs for class Track. |
__repr__ |
Method generated by attrs for class Track. |
matches |
Check if this track matches another track. |
similarity_to |
Calculate similarity metrics with another track. |
Source code in sleap_io/model/instance.py
@attrs.define(eq=False)
class Track:
"""An object that represents the same animal/object across multiple detections.
This allows tracking of unique entities in the video over time and space.
A `Track` may also be used to refer to unique identity classes that span multiple
videos, such as `"female mouse"`.
Attributes:
name: A name given to this track for identification purposes.
Notes:
`Track`s are compared by identity. This means that unique track objects with the
same name are considered to be different.
"""
name: str = ""
def matches(self, other: "Track", method: str = "name") -> bool:
"""Check if this track matches another track.
Args:
other: Another track to compare with.
method: Matching method - "name" (match by name) or "identity"
(match by object identity).
Returns:
True if the tracks match according to the specified method.
"""
if method == "name":
return self.name == other.name
elif method == "identity":
return self is other
else:
raise ValueError(f"Unknown matching method: {method}")
def similarity_to(self, other: "Track") -> dict[str, any]:
"""Calculate similarity metrics with another track.
Args:
other: Another track to compare with.
Returns:
A dictionary with similarity metrics:
- 'same_name': Whether the tracks have the same name
- 'same_identity': Whether the tracks are the same object
- 'name_similarity': Simple string similarity score (0-1)
"""
# Calculate simple string similarity
if self.name and other.name:
# Simple character overlap similarity
common_chars = set(self.name.lower()) & set(other.name.lower())
all_chars = set(self.name.lower()) | set(other.name.lower())
name_similarity = len(common_chars) / len(all_chars) if all_chars else 0
else:
name_similarity = 1.0 if self.name == other.name else 0.0
return {
"same_name": self.name == other.name,
"same_identity": self is other,
"name_similarity": name_similarity,
}
__annotations__ = {'name': 'str'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'An object that represents the same animal/object across multiple detections.\n\nThis allows tracking of unique entities in the video over time and space.\n\nA `Track` may also be used to refer to unique identity classes that span multiple\nvideos, such as `"female mouse"`.\n\nAttributes:\n name: A name given to this track for identification purposes.\n\nNotes:\n `Track`s are compared by identity. This means that unique track objects with the\n same name are considered to be different.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 321
class-attribute
¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.
int('0b100', base=0) 4
__match_args__ = ('name',)
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.instance'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__slots__ = ('name', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
__init__(name='')
¶
__replace__(**changes)
¶
Method generated by attrs for class Track.
__repr__()
¶
Method generated by attrs for class Track.
Source code in sleap_io/model/instance.py
"""Data structures for data associated with a single instance such as an animal.
The `Instance` class is a SLEAP data structure that contains a collection of points that
correspond to landmarks within a `Skeleton`.
`PredictedInstance` additionally contains metadata associated with how the instance was
estimated, such as confidence scores.
"""
from __future__ import annotations
import attrs
import numpy as np
from sleap_io.model.skeleton import Node, Skeleton
matches(other, method='name')
¶
Check if this track matches another track.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Track
|
Another track to compare with. |
required |
method
|
str
|
Matching method - "name" (match by name) or "identity" (match by object identity). |
'name'
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the tracks match according to the specified method. |
Source code in sleap_io/model/instance.py
def matches(self, other: "Track", method: str = "name") -> bool:
"""Check if this track matches another track.
Args:
other: Another track to compare with.
method: Matching method - "name" (match by name) or "identity"
(match by object identity).
Returns:
True if the tracks match according to the specified method.
"""
if method == "name":
return self.name == other.name
elif method == "identity":
return self is other
else:
raise ValueError(f"Unknown matching method: {method}")
similarity_to(other)
¶
Calculate similarity metrics with another track.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Track
|
Another track to compare with. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, any]
|
A dictionary with similarity metrics: - 'same_name': Whether the tracks have the same name - 'same_identity': Whether the tracks are the same object - 'name_similarity': Simple string similarity score (0-1) |
Source code in sleap_io/model/instance.py
def similarity_to(self, other: "Track") -> dict[str, any]:
"""Calculate similarity metrics with another track.
Args:
other: Another track to compare with.
Returns:
A dictionary with similarity metrics:
- 'same_name': Whether the tracks have the same name
- 'same_identity': Whether the tracks are the same object
- 'name_similarity': Simple string similarity score (0-1)
"""
# Calculate simple string similarity
if self.name and other.name:
# Simple character overlap similarity
common_chars = set(self.name.lower()) & set(other.name.lower())
all_chars = set(self.name.lower()) | set(other.name.lower())
name_similarity = len(common_chars) / len(all_chars) if all_chars else 0
else:
name_similarity = 1.0 if self.name == other.name else 0.0
return {
"same_name": self.name == other.name,
"same_identity": self is other,
"name_similarity": name_similarity,
}