skeleton
sleap_io.io.skeleton
¶
This module handles I/O operations for standalone skeleton JSON files.
Classes:
| Name | Description |
|---|---|
Edge |
A connection between two |
Node |
A landmark type within a |
Skeleton |
A description of a set of landmark types and connections between them. |
SkeletonDecoder |
Decode skeleton data from jsonpickle-encoded format. |
SkeletonEncoder |
Encode skeleton data to jsonpickle format. |
SkeletonSLPDecoder |
Decode skeleton data from SLP format. |
SkeletonSLPEncoder |
Encode skeleton data to SLP format. |
SkeletonYAMLDecoder |
Decode skeleton data from simplified YAML format. |
SkeletonYAMLEncoder |
Encode skeleton data to simplified YAML format. |
Symmetry |
A relationship between a pair of nodes denoting their left/right pairing. |
Functions:
| Name | Description |
|---|---|
decode_skeleton |
Decode skeleton(s) from JSON data using the default decoder. |
decode_training_config |
Decode skeleton(s) from training config data. |
decode_yaml_skeleton |
Decode skeleton(s) from YAML data. |
encode_skeleton |
Encode skeleton(s) to JSON string using the default encoder. |
encode_yaml_skeleton |
Encode skeleton(s) to YAML string. |
load_skeleton_from_json |
Load skeleton(s) from JSON data, with automatic training config detection. |
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/io/__pycache__/skeleton.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__ = 'This module handles I/O operations for standalone skeleton JSON files.'
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/io/skeleton.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.io.skeleton'
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.io'
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'.
Edge
¶
A connection between two Node objects within a Skeleton.
This is a directed edge, representing the ordering of Nodes in the Skeleton
tree.
Attributes:
| Name | Type | Description |
|---|---|---|
source |
The origin |
|
destination |
The destination |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class Edge. |
__getitem__ |
Return the source |
__hash__ |
Method generated by attrs for class Edge. |
__init__ |
Method generated by attrs for class Edge. |
__repr__ |
Method generated by attrs for class Edge. |
Source code in sleap_io/model/skeleton.py
@define(frozen=True)
class Edge:
"""A connection between two `Node` objects within a `Skeleton`.
This is a directed edge, representing the ordering of `Node`s in the `Skeleton`
tree.
Attributes:
source: The origin `Node`.
destination: The destination `Node`.
"""
source: Node
destination: Node
def __getitem__(self, idx) -> Node:
"""Return the source `Node` (`idx` is 0) or destination `Node` (`idx` is 1)."""
if idx == 0:
return self.source
elif idx == 1:
return self.destination
else:
raise IndexError("Edge only has 2 nodes (source and destination).")
__annotations__ = {'source': 'Node', 'destination': 'Node'}
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_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=True, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.HASHABLE: 'hashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=None, 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 connection between two `Node` objects within a `Skeleton`.\n\nThis is a directed edge, representing the ordering of `Node`s in the `Skeleton`\ntree.\n\nAttributes:\n source: The origin `Node`.\n destination: The destination `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__ = 32
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 iteral.
int('0b100', base=0) 4
__match_args__ = ('source', 'destination')
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__ = ('source', 'destination', '__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
__eq__(other)
¶
__getitem__(idx)
¶
Return the source Node (idx is 0) or destination Node (idx is 1).
Source code in sleap_io/model/skeleton.py
__hash__()
¶
__init__(source, destination)
¶
__repr__()
¶
Method generated by attrs for class Edge.
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 re
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
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. |
__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 0x7f08471ce840>, 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__ = 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 iteral.
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)
¶
__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 re
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
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. |
__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 |
infer_symmetries_by_name |
Infer left/right symmetric node pairs from node names. |
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 infer_symmetries_by_name(
self,
token_pairs: list[tuple[str, str]] | None = None,
) -> list[tuple[int, int]]:
"""Infer left/right symmetric node pairs from node names.
Useful when a skeleton has no symmetries defined (e.g. imported from a
format that does not carry symmetry metadata) but its node names encode
laterality, so that flip-dependent tooling (augmentation, QC) still
works. Names are matched by splitting on separators (`_`, `-`, `.`,
space), camelCase boundaries, and letter/digit boundaries, then pairing
nodes that share a stem but differ by a single left/right token. For
example, `Ear_L`/`Ear_R`, `left_eye`/`right_eye`, `LeftPaw`/`RightPaw`,
and `L1`/`R1` all pair up.
This is intentionally **non-mutating** and conservative: it returns
suggested pairs rather than writing them onto the skeleton, since a wrong
guess would silently corrupt flip augmentation. Apply the result
explicitly if desired, e.g.
`skel.add_symmetries(skel.infer_symmetries_by_name())`. Node names
without a delimited or camelCase/digit token boundary (e.g. `larm`) and
truly non-semantic pairings (e.g. `L1`/`L2`) cannot be inferred and must
be declared with `add_symmetry`.
Args:
token_pairs: List of `(left_token, right_token)` string pairs used to
recognize laterality, matched case-insensitively against whole
name segments. Defaults to `[("left", "right"), ("l", "r")]`.
Returns:
A list of `(left_index, right_index)` node-index pairs, ordered by
left index. Each node appears in at most one pair, and only stems
with exactly one left and one right member are paired (ambiguous
groups are skipped).
Example:
>>> skel = Skeleton(["nose", "eye_L", "eye_R", "ear_L", "ear_R"])
>>> skel.infer_symmetries_by_name()
[(1, 2), (3, 4)]
>>> skel.add_symmetries(skel.infer_symmetries_by_name())
>>> skel.symmetry_names
[('eye_L', 'eye_R'), ('ear_L', 'ear_R')]
"""
return infer_symmetry_pairs_by_name(self.node_names, token_pairs=token_pairs)
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 0x7f08471ce840>, 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__ = 97
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 iteral.
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 re
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
__len__()
¶
__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}")
infer_symmetries_by_name(token_pairs=None)
¶
Infer left/right symmetric node pairs from node names.
Useful when a skeleton has no symmetries defined (e.g. imported from a
format that does not carry symmetry metadata) but its node names encode
laterality, so that flip-dependent tooling (augmentation, QC) still
works. Names are matched by splitting on separators (_, -, .,
space), camelCase boundaries, and letter/digit boundaries, then pairing
nodes that share a stem but differ by a single left/right token. For
example, Ear_L/Ear_R, left_eye/right_eye, LeftPaw/RightPaw,
and L1/R1 all pair up.
This is intentionally non-mutating and conservative: it returns
suggested pairs rather than writing them onto the skeleton, since a wrong
guess would silently corrupt flip augmentation. Apply the result
explicitly if desired, e.g.
skel.add_symmetries(skel.infer_symmetries_by_name()). Node names
without a delimited or camelCase/digit token boundary (e.g. larm) and
truly non-semantic pairings (e.g. L1/L2) cannot be inferred and must
be declared with add_symmetry.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token_pairs
|
list[tuple[str, str]] | None
|
List of |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple[int, int]]
|
A list of |
Example
skel = Skeleton(["nose", "eye_L", "eye_R", "ear_L", "ear_R"]) skel.infer_symmetries_by_name() [(1, 2), (3, 4)] skel.add_symmetries(skel.infer_symmetries_by_name()) skel.symmetry_names [('eye_L', 'eye_R'), ('ear_L', 'ear_R')]
Source code in sleap_io/model/skeleton.py
def infer_symmetries_by_name(
self,
token_pairs: list[tuple[str, str]] | None = None,
) -> list[tuple[int, int]]:
"""Infer left/right symmetric node pairs from node names.
Useful when a skeleton has no symmetries defined (e.g. imported from a
format that does not carry symmetry metadata) but its node names encode
laterality, so that flip-dependent tooling (augmentation, QC) still
works. Names are matched by splitting on separators (`_`, `-`, `.`,
space), camelCase boundaries, and letter/digit boundaries, then pairing
nodes that share a stem but differ by a single left/right token. For
example, `Ear_L`/`Ear_R`, `left_eye`/`right_eye`, `LeftPaw`/`RightPaw`,
and `L1`/`R1` all pair up.
This is intentionally **non-mutating** and conservative: it returns
suggested pairs rather than writing them onto the skeleton, since a wrong
guess would silently corrupt flip augmentation. Apply the result
explicitly if desired, e.g.
`skel.add_symmetries(skel.infer_symmetries_by_name())`. Node names
without a delimited or camelCase/digit token boundary (e.g. `larm`) and
truly non-semantic pairings (e.g. `L1`/`L2`) cannot be inferred and must
be declared with `add_symmetry`.
Args:
token_pairs: List of `(left_token, right_token)` string pairs used to
recognize laterality, matched case-insensitively against whole
name segments. Defaults to `[("left", "right"), ("l", "r")]`.
Returns:
A list of `(left_index, right_index)` node-index pairs, ordered by
left index. Each node appears in at most one pair, and only stems
with exactly one left and one right member are paired (ambiguous
groups are skipped).
Example:
>>> skel = Skeleton(["nose", "eye_L", "eye_R", "ear_L", "ear_R"])
>>> skel.infer_symmetries_by_name()
[(1, 2), (3, 4)]
>>> skel.add_symmetries(skel.infer_symmetries_by_name())
>>> skel.symmetry_names
[('eye_L', 'eye_R'), ('ear_L', 'ear_R')]
"""
return infer_symmetry_pairs_by_name(self.node_names, token_pairs=token_pairs)
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]
SkeletonDecoder
¶
Decode skeleton data from jsonpickle-encoded format.
This decoder handles the custom jsonpickle format used by SLEAP for standalone skeleton JSON files, which differs from the format used within .slp files.
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize the decoder. |
decode |
Decode skeleton(s) from JSON data. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonDecoder:
"""Decode skeleton data from jsonpickle-encoded format.
This decoder handles the custom jsonpickle format used by SLEAP for
standalone skeleton JSON files, which differs from the format used
within .slp files.
"""
def __init__(self):
"""Initialize the decoder."""
self.decoded_objects: list[
Any
] = [] # List of decoded objects indexed by py/id - 1
def decode(self, data: str | dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from JSON data.
Args:
data: JSON string or pre-parsed dictionary containing skeleton data.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
if isinstance(data, str):
data = json.loads(data)
# Reset decoded objects list for each decode operation
self.decoded_objects = []
# Check if this is a list of skeletons or a single skeleton
if isinstance(data, list):
return [self._decode_skeleton(skel_data) for skel_data in data]
else:
return self._decode_skeleton(data)
def _decode_skeleton(self, data: dict) -> Skeleton:
"""Decode a single skeleton from dictionary data.
Args:
data: Dictionary containing skeleton data in jsonpickle format.
Returns:
A Skeleton object.
"""
# Validate input data
if data is None:
raise ValueError("Skeleton data cannot be None")
if not isinstance(data, dict):
raise TypeError(f"Skeleton data must be a dictionary, got {type(data)}")
# Reset decoded objects list for this skeleton
self.decoded_objects = []
# Track edge types separately for formats that use separate py/id spaces
edge_type_ids = {} # edge_type_value -> py/id
next_edge_type_id = 1
# First pass: decode all objects in order of appearance
seen_nodes = set() # Track node names we've already seen
# Handle both direct format and nx_graph format
if "nx_graph" in data:
# nx_graph format (standalone skeleton files)
links_data = data["nx_graph"].get("links", [])
nodes_data = data["nx_graph"].get("nodes", [])
graph_data = data["nx_graph"].get("graph", {})
else:
# Direct format (training config embedded skeletons)
links_data = data.get("links", [])
nodes_data = data.get("nodes", [])
graph_data = data.get("graph", {})
for link in links_data:
# Check each component of the link for new objects
for key in ["source", "target", "type"]:
value = link.get(key, {})
if isinstance(value, dict):
if "py/object" in value:
# New node object
node = self._decode_node(value)
if node.name not in seen_nodes:
self.decoded_objects.append(node)
seen_nodes.add(node.name)
elif "py/reduce" in value:
# New edge type
edge_type_val = value["py/reduce"][1]["py/tuple"][0]
self.decoded_objects.append(edge_type_val)
# Also track edge type IDs separately
if edge_type_val not in edge_type_ids:
edge_type_ids[edge_type_val] = next_edge_type_id
next_edge_type_id += 1
# py/id references are handled in second pass
# Also process nodes that are directly defined in the nodes array
# This is crucial for single-node skeletons with no edges
for node_ref in nodes_data:
if isinstance(node_ref.get("id"), dict) and "py/object" in node_ref["id"]:
# New node object directly in nodes array
node = self._decode_node(node_ref["id"])
if node.name not in seen_nodes:
self.decoded_objects.append(node)
seen_nodes.add(node.name)
# Store edge type mappings for second pass
self._edge_type_ids = edge_type_ids
# Second pass: build edges using the decoded objects
edges = []
symmetries = []
seen_symmetries = set()
for link in links_data:
# Resolve references to build the edge
source_node = self._resolve_link_ref(link["source"])
target_node = self._resolve_link_ref(link["target"])
edge_type_val = self._resolve_edge_type_ref(link.get("type", {}))
if edge_type_val == 1: # Regular edge
edges.append(Edge(source=source_node, destination=target_node))
elif edge_type_val == 2: # Symmetry edge
# Create a unique key for this symmetry pair (order-independent)
sym_key = tuple(sorted([source_node.name, target_node.name]))
if sym_key not in seen_symmetries:
symmetries.append(Symmetry([source_node, target_node]))
seen_symmetries.add(sym_key)
# Build nodes list in the order declared by the nodes section. Map decoded
# Node objects by name so each nodes-array entry resolves back to the SAME
# object the edges reference — whether it is a ``py/id`` back-reference
# (link-connected node) or an inline ``py/object`` (an isolated/edge-less
# node, which was otherwise missed here and appended to the tail, dropping
# its declared position; see #438).
all_nodes = [obj for obj in self.decoded_objects if isinstance(obj, Node)]
node_by_name = {n.name: n for n in all_nodes}
nodes_from_refs = []
for node_ref in nodes_data:
ref_id = node_ref.get("id")
if not isinstance(ref_id, dict):
continue
if "py/id" in ref_id:
py_id = ref_id["py/id"]
# py/id is 1-indexed into the decoded objects.
if py_id <= len(self.decoded_objects):
obj = self.decoded_objects[py_id - 1]
if isinstance(obj, Node):
nodes_from_refs.append(obj)
elif "py/object" in ref_id:
# Isolated node declared inline; resolve to its canonical object.
name = self._decode_node(ref_id).name
if name in node_by_name:
nodes_from_refs.append(node_by_name[name])
if len(nodes_from_refs) < len(all_nodes):
# Incomplete/malformed nodes array: fall back to natural order.
nodes = all_nodes
else:
# Use the declared nodes-array order (isolated nodes kept in slot).
nodes = nodes_from_refs
# Get skeleton name
name = graph_data.get("name", "Skeleton")
return Skeleton(nodes=nodes, edges=edges, symmetries=symmetries, name=name)
def _resolve_link_ref(self, node_ref: dict | int) -> Node:
"""Resolve a node reference.
Args:
node_ref: Node reference (can be embedded object or py/id reference).
Returns:
The resolved Node object.
"""
if isinstance(node_ref, dict):
if "py/object" in node_ref:
# Find the node in decoded objects by name
node = self._decode_node(node_ref)
for obj in self.decoded_objects:
if isinstance(obj, Node) and obj.name == node.name:
return obj
raise ValueError(f"Node {node.name} not found in decoded objects")
elif "py/id" in node_ref:
# Reference to existing object
py_id = node_ref["py/id"]
if py_id <= len(self.decoded_objects):
obj = self.decoded_objects[py_id - 1]
if isinstance(obj, Node):
return obj
raise ValueError(f"py/id {py_id} is not a Node")
raise ValueError(f"py/id {py_id} not found")
elif isinstance(node_ref, int):
# Direct index (used in SLP format, shouldn't happen in standalone)
raise ValueError(f"Direct index reference not supported: {node_ref}")
raise ValueError(f"Unknown node reference format: {node_ref}")
def _resolve_edge_type_ref(self, type_data: dict) -> int:
"""Resolve edge type reference.
Args:
type_data: Dictionary containing edge type data.
Returns:
Integer edge type (1 for regular edge, 2 for symmetry).
"""
if "py/reduce" in type_data:
# Return the value directly (already decoded in first pass)
return type_data["py/reduce"][1]["py/tuple"][0]
elif "py/id" in type_data:
# Reference to existing edge type
py_id = type_data["py/id"]
# First try to find in decoded objects (training config format)
if py_id <= len(self.decoded_objects):
obj = self.decoded_objects[py_id - 1]
if isinstance(obj, int):
return obj
# If not found, check if this is a separate edge type ID space
# (standalone skeleton format)
for edge_val, edge_id in self._edge_type_ids.items():
if edge_id == py_id:
return edge_val
raise ValueError(f"py/id {py_id} not found as edge type")
else:
# Default to regular edge
return 1
def _decode_node(self, data: dict) -> Node:
"""Decode a node from jsonpickle format.
Args:
data: Dictionary containing node data.
Returns:
A Node object.
"""
if "py/state" in data:
state = data["py/state"]
# Handle both tuple and dict formats
if "py/tuple" in state:
# Tuple format: [name, weight]
name = state["py/tuple"][0]
# Note: weight is stored but not used in sleap-io Node objects
else:
# Dict format
name = state.get("name", "")
else:
# Direct format
name = data.get("name", "")
return Node(name=name)
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 113, '__doc__': 'Decode skeleton data from jsonpickle-encoded format.\n\nThis decoder handles the custom jsonpickle format used by SLEAP for\nstandalone skeleton JSON files, which differs from the format used\nwithin .slp files.\n', '__init__': <function SkeletonDecoder.__init__ at 0x7f0836928180>, 'decode': <function SkeletonDecoder.decode at 0x7f0836937ec0>, '_decode_skeleton': <function SkeletonDecoder._decode_skeleton at 0x7f08369371a0>, '_resolve_link_ref': <function SkeletonDecoder._resolve_link_ref at 0x7f0836937100>, '_resolve_edge_type_ref': <function SkeletonDecoder._resolve_edge_type_ref at 0x7f0836937060>, '_decode_node': <function SkeletonDecoder._decode_node at 0x7f0836936fc0>, '__static_attributes__': ('_edge_type_ids', 'decoded_objects'), '__dict__': <attribute '__dict__' of 'SkeletonDecoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonDecoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Decode skeleton data from jsonpickle-encoded format.\n\nThis decoder handles the custom jsonpickle format used by SLEAP for\nstandalone skeleton JSON files, which differs from the format used\nwithin .slp files.\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__ = 113
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__static_attributes__ = ('_edge_type_ids', 'decoded_objects')
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__()
¶
decode(data)
¶
Decode skeleton(s) from JSON data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str | dict
|
JSON string or pre-parsed dictionary containing skeleton data. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons depending on input format. |
Source code in sleap_io/io/skeleton.py
def decode(self, data: str | dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from JSON data.
Args:
data: JSON string or pre-parsed dictionary containing skeleton data.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
if isinstance(data, str):
data = json.loads(data)
# Reset decoded objects list for each decode operation
self.decoded_objects = []
# Check if this is a list of skeletons or a single skeleton
if isinstance(data, list):
return [self._decode_skeleton(skel_data) for skel_data in data]
else:
return self._decode_skeleton(data)
SkeletonEncoder
¶
Encode skeleton data to jsonpickle format.
This encoder produces the jsonpickle format used by SLEAP for standalone skeleton JSON files, ensuring backward compatibility with existing files.
Methods:
| Name | Description |
|---|---|
__init__ |
Initialize the encoder. |
encode |
Encode skeleton(s) to JSON string. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonEncoder:
"""Encode skeleton data to jsonpickle format.
This encoder produces the jsonpickle format used by SLEAP for standalone
skeleton JSON files, ensuring backward compatibility with existing files.
"""
def __init__(self):
"""Initialize the encoder."""
self._object_to_id: dict[int, int] = {} # id(object) -> py/id
self._next_id = 1
def encode(self, skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to JSON string.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
JSON string in jsonpickle format.
"""
# Reset state for each encode operation
self._object_to_id = {}
self._next_id = 1
# Handle single skeleton or list
if isinstance(skeletons, Skeleton):
data = self._encode_skeleton(skeletons)
else:
data = [self._encode_skeleton(skel) for skel in skeletons]
# Sort dictionaries recursively for consistency
data = self._recursively_sort_dict(data)
return json.dumps(data, separators=(", ", ": "))
def _encode_skeleton(self, skeleton: Skeleton) -> dict:
"""Encode a single skeleton to dictionary format.
Args:
skeleton: Skeleton object to encode.
Returns:
Dictionary in jsonpickle format.
"""
# Track nodes and their py/ids
node_to_py_id = {}
# Encode links (edges and symmetries)
links = []
# First, process edges to establish node references
for i, edge in enumerate(skeleton.edges):
# Encode edge
edge_dict = self._encode_edge(edge, i, edge_type=1)
links.append(edge_dict)
# Track node py/ids
if edge.source not in node_to_py_id:
node_to_py_id[edge.source] = self._get_or_create_py_id(edge.source)
if edge.destination not in node_to_py_id:
node_to_py_id[edge.destination] = self._get_or_create_py_id(
edge.destination
)
# Then process symmetries
for i, symmetry in enumerate(skeleton.symmetries):
# Encode symmetry
sym_dict = self._encode_symmetry(symmetry, edge_type=2)
links.append(sym_dict)
# Track node py/ids
for node in symmetry.nodes:
if node not in node_to_py_id:
node_to_py_id[node] = self._get_or_create_py_id(node)
# Ensure all skeleton nodes have py/ids
for node in skeleton.nodes:
if node not in node_to_py_id:
node_to_py_id[node] = self._get_or_create_py_id(node)
# Determine which nodes were serialized with full object state inside
# `links`. Nodes that appear in no edge or symmetry (e.g. the lone node
# of a single-node skeleton, or any isolated node) are never emitted in
# `links`, so referencing them here by py/id alone would dangle and the
# decoder would silently drop them. Emit their full object state in the
# nodes section instead.
nodes_in_links = set()
for edge in skeleton.edges:
nodes_in_links.add(edge.source)
nodes_in_links.add(edge.destination)
for symmetry in skeleton.symmetries:
for node in symmetry.nodes:
nodes_in_links.add(node)
# Create nodes section: py/id references for nodes already serialized in
# `links`, full object state for isolated nodes. Fully-connected
# skeletons are unaffected (every node is in a link), so existing output
# is unchanged.
nodes = []
for node in skeleton.nodes:
if node in nodes_in_links:
nodes.append({"id": {"py/id": node_to_py_id[node]}})
else:
nodes.append({"id": self._encode_node(node)})
# Build final skeleton dict
return {
"directed": True,
"graph": {"name": skeleton.name, "num_edges_inserted": len(skeleton.edges)},
"links": links,
"multigraph": True,
"nodes": nodes,
}
def _encode_edge(self, edge: Edge, edge_idx: int, edge_type: int) -> dict:
"""Encode an edge to jsonpickle format.
Args:
edge: Edge object to encode.
edge_idx: Index of this edge.
edge_type: Type of edge (1 for regular, 2 for symmetry).
Returns:
Dictionary representing the edge.
"""
# Encode edge type - first occurrence uses py/reduce, subsequent use py/id
# For backward compatibility, always use type 1 for regular edges
if edge_type == 1:
if not hasattr(self, "_edge_type_1_encoded"):
type_dict = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [1]},
]
}
self._edge_type_1_encoded = True
else:
type_dict = {"py/id": 1}
else:
if not hasattr(self, "_edge_type_2_encoded"):
type_dict = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [2]},
]
}
self._edge_type_2_encoded = True
else:
type_dict = {"py/id": 2}
return {
"edge_insert_idx": edge_idx,
"key": 0,
"source": self._encode_node(edge.source),
"target": self._encode_node(edge.destination),
"type": type_dict,
}
def _encode_symmetry(self, symmetry: Symmetry, edge_type: int) -> dict:
"""Encode a symmetry to jsonpickle format.
Args:
symmetry: Symmetry object to encode.
edge_type: Type of edge (should be 2 for symmetry).
Returns:
Dictionary representing the symmetry.
"""
# Get source and target nodes (convert set to list for ordering)
nodes_list = list(symmetry.nodes)
source, target = nodes_list[0], nodes_list[1]
# Encode edge type
if not hasattr(self, "_edge_type_2_encoded"):
type_dict = {
"py/reduce": [{"py/type": "sleap.skeleton.EdgeType"}, {"py/tuple": [2]}]
}
self._edge_type_2_encoded = True
else:
type_dict = {"py/id": 2}
return {
"key": 0,
"source": self._encode_node(source),
"target": self._encode_node(target),
"type": type_dict,
}
def _encode_node(self, node: Node) -> dict:
"""Encode a node to jsonpickle format.
Args:
node: Node object to encode.
Returns:
Dictionary with py/object and py/state.
"""
return {
"py/object": "sleap.skeleton.Node",
"py/state": {"py/tuple": [node.name, 1.0]}, # name, weight (always 1.0)
}
def _get_or_create_py_id(self, obj: Any) -> int:
"""Get or create a py/id for an object.
Args:
obj: Object to get/create ID for.
Returns:
The py/id integer.
"""
obj_id = id(obj)
if obj_id not in self._object_to_id:
self._object_to_id[obj_id] = self._next_id
self._next_id += 1
return self._object_to_id[obj_id]
def _recursively_sort_dict(self, obj: Any) -> Any:
"""Recursively sort dictionary keys for consistent output.
Args:
obj: Object to sort (dict, list, or other).
Returns:
Sorted version of the object.
"""
if isinstance(obj, dict):
# Sort keys and recursively sort values
return {k: self._recursively_sort_dict(v) for k, v in sorted(obj.items())}
elif isinstance(obj, list):
# Recursively sort list elements
return [self._recursively_sort_dict(item) for item in obj]
else:
# Return as-is for non-dict/list types
return obj
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 369, '__doc__': 'Encode skeleton data to jsonpickle format.\n\nThis encoder produces the jsonpickle format used by SLEAP for standalone\nskeleton JSON files, ensuring backward compatibility with existing files.\n', '__init__': <function SkeletonEncoder.__init__ at 0x7f0836936f20>, 'encode': <function SkeletonEncoder.encode at 0x7f0836936e80>, '_encode_skeleton': <function SkeletonEncoder._encode_skeleton at 0x7f0836936de0>, '_encode_edge': <function SkeletonEncoder._encode_edge at 0x7f0836936d40>, '_encode_symmetry': <function SkeletonEncoder._encode_symmetry at 0x7f0836936ca0>, '_encode_node': <function SkeletonEncoder._encode_node at 0x7f0836936980>, '_get_or_create_py_id': <function SkeletonEncoder._get_or_create_py_id at 0x7f0836936340>, '_recursively_sort_dict': <function SkeletonEncoder._recursively_sort_dict at 0x7f08369362a0>, '__static_attributes__': ('_edge_type_1_encoded', '_edge_type_2_encoded', '_next_id', '_object_to_id'), '__dict__': <attribute '__dict__' of 'SkeletonEncoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonEncoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Encode skeleton data to jsonpickle format.\n\nThis encoder produces the jsonpickle format used by SLEAP for standalone\nskeleton JSON files, ensuring backward compatibility with existing files.\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__ = 369
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__static_attributes__ = ('_edge_type_1_encoded', '_edge_type_2_encoded', '_next_id', '_object_to_id')
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__()
¶
encode(skeletons)
¶
Encode skeleton(s) to JSON string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeletons
|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons to encode. |
required |
Returns:
| Type | Description |
|---|---|
str
|
JSON string in jsonpickle format. |
Source code in sleap_io/io/skeleton.py
def encode(self, skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to JSON string.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
JSON string in jsonpickle format.
"""
# Reset state for each encode operation
self._object_to_id = {}
self._next_id = 1
# Handle single skeleton or list
if isinstance(skeletons, Skeleton):
data = self._encode_skeleton(skeletons)
else:
data = [self._encode_skeleton(skel) for skel in skeletons]
# Sort dictionaries recursively for consistency
data = self._recursively_sort_dict(data)
return json.dumps(data, separators=(", ", ": "))
SkeletonSLPDecoder
¶
Decode skeleton data from SLP format.
This decoder handles the SLP format used within .slp files, which uses integer indices for node references instead of embedded node objects.
Methods:
| Name | Description |
|---|---|
decode |
Decode skeletons from SLP metadata format. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonSLPDecoder:
"""Decode skeleton data from SLP format.
This decoder handles the SLP format used within .slp files, which uses
integer indices for node references instead of embedded node objects.
"""
def decode(self, metadata: dict, node_names: list[str]) -> list[Skeleton]:
"""Decode skeletons from SLP metadata format.
Args:
metadata: The metadata dict from an SLP file containing skeletons.
node_names: Global list of node names from the SLP file.
Returns:
List of Skeleton objects.
"""
skeleton_objects = []
for skel in metadata["skeletons"]:
# Parse out the cattr-based serialization stuff from the skeleton links.
if "nx_graph" in skel:
# New format introduced in SLEAP v1.3.2
# TODO: Do something with the "description" and "preview_image" keys?
skel = skel["nx_graph"]
# Process links with proper py/id resolution.
# In jsonpickle format, py/reduce creates a new object and assigns it
# an implicit py/id (1, 2, 3...). We track which py/id maps to which
# edge type value as we encounter them.
edge_type_map = {} # py/id -> edge_type_value
next_py_id = 1
edge_inds, symmetry_inds = [], []
for link in skel["links"]:
if "py/reduce" in link["type"]:
# New edge type definition - extract value and assign py/id
edge_type = link["type"]["py/reduce"][1]["py/tuple"][0]
edge_type_map[next_py_id] = edge_type
next_py_id += 1
elif "py/id" in link["type"]:
# Reference to previously defined edge type - look up the value
py_id = link["type"]["py/id"]
# Fallback to py_id value if not in map (for files where edge types
# are defined in a separate scope or use implicit numbering)
edge_type = edge_type_map.get(py_id, py_id)
if edge_type == 1: # 1 -> real edge, 2 -> symmetry edge
edge_inds.append((link["source"], link["target"]))
elif edge_type == 2:
symmetry_inds.append((link["source"], link["target"]))
# Re-index correctly.
skeleton_node_inds = [node["id"] for node in skel["nodes"]]
sorted_node_names = [node_names[i] for i in skeleton_node_inds]
# Create nodes.
nodes = []
for name in sorted_node_names:
nodes.append(Node(name=name))
# Create edges.
edge_inds = [
(skeleton_node_inds.index(s), skeleton_node_inds.index(d))
for s, d in edge_inds
]
edges = []
for edge in edge_inds:
edges.append(Edge(source=nodes[edge[0]], destination=nodes[edge[1]]))
# Create symmetries.
symmetry_inds = [
(skeleton_node_inds.index(s), skeleton_node_inds.index(d))
for s, d in symmetry_inds
]
# Deduplicate symmetries - legacy files may have duplicates
# (one for each direction)
seen_symmetries = set()
symmetries = []
for symmetry in symmetry_inds:
# Create a unique key for this symmetry pair (order-independent)
sym_key = tuple(sorted([symmetry[0], symmetry[1]]))
if sym_key not in seen_symmetries:
symmetries.append(
Symmetry([nodes[symmetry[0]], nodes[symmetry[1]]])
)
seen_symmetries.add(sym_key)
# Create the full skeleton.
skel = Skeleton(
nodes=nodes,
edges=edges,
symmetries=symmetries,
name=skel["graph"]["name"],
)
skeleton_objects.append(skel)
return skeleton_objects
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 607, '__doc__': 'Decode skeleton data from SLP format.\n\nThis decoder handles the SLP format used within .slp files, which uses\ninteger indices for node references instead of embedded node objects.\n', 'decode': <function SkeletonSLPDecoder.decode at 0x7f0836936200>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'SkeletonSLPDecoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonSLPDecoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Decode skeleton data from SLP format.\n\nThis decoder handles the SLP format used within .slp files, which uses\ninteger indices for node references instead of embedded node objects.\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__ = 607
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__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
decode(metadata, node_names)
¶
Decode skeletons from SLP metadata format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
metadata
|
dict
|
The metadata dict from an SLP file containing skeletons. |
required |
node_names
|
list[str]
|
Global list of node names from the SLP file. |
required |
Returns:
| Type | Description |
|---|---|
list[Skeleton]
|
List of Skeleton objects. |
Source code in sleap_io/io/skeleton.py
def decode(self, metadata: dict, node_names: list[str]) -> list[Skeleton]:
"""Decode skeletons from SLP metadata format.
Args:
metadata: The metadata dict from an SLP file containing skeletons.
node_names: Global list of node names from the SLP file.
Returns:
List of Skeleton objects.
"""
skeleton_objects = []
for skel in metadata["skeletons"]:
# Parse out the cattr-based serialization stuff from the skeleton links.
if "nx_graph" in skel:
# New format introduced in SLEAP v1.3.2
# TODO: Do something with the "description" and "preview_image" keys?
skel = skel["nx_graph"]
# Process links with proper py/id resolution.
# In jsonpickle format, py/reduce creates a new object and assigns it
# an implicit py/id (1, 2, 3...). We track which py/id maps to which
# edge type value as we encounter them.
edge_type_map = {} # py/id -> edge_type_value
next_py_id = 1
edge_inds, symmetry_inds = [], []
for link in skel["links"]:
if "py/reduce" in link["type"]:
# New edge type definition - extract value and assign py/id
edge_type = link["type"]["py/reduce"][1]["py/tuple"][0]
edge_type_map[next_py_id] = edge_type
next_py_id += 1
elif "py/id" in link["type"]:
# Reference to previously defined edge type - look up the value
py_id = link["type"]["py/id"]
# Fallback to py_id value if not in map (for files where edge types
# are defined in a separate scope or use implicit numbering)
edge_type = edge_type_map.get(py_id, py_id)
if edge_type == 1: # 1 -> real edge, 2 -> symmetry edge
edge_inds.append((link["source"], link["target"]))
elif edge_type == 2:
symmetry_inds.append((link["source"], link["target"]))
# Re-index correctly.
skeleton_node_inds = [node["id"] for node in skel["nodes"]]
sorted_node_names = [node_names[i] for i in skeleton_node_inds]
# Create nodes.
nodes = []
for name in sorted_node_names:
nodes.append(Node(name=name))
# Create edges.
edge_inds = [
(skeleton_node_inds.index(s), skeleton_node_inds.index(d))
for s, d in edge_inds
]
edges = []
for edge in edge_inds:
edges.append(Edge(source=nodes[edge[0]], destination=nodes[edge[1]]))
# Create symmetries.
symmetry_inds = [
(skeleton_node_inds.index(s), skeleton_node_inds.index(d))
for s, d in symmetry_inds
]
# Deduplicate symmetries - legacy files may have duplicates
# (one for each direction)
seen_symmetries = set()
symmetries = []
for symmetry in symmetry_inds:
# Create a unique key for this symmetry pair (order-independent)
sym_key = tuple(sorted([symmetry[0], symmetry[1]]))
if sym_key not in seen_symmetries:
symmetries.append(
Symmetry([nodes[symmetry[0]], nodes[symmetry[1]]])
)
seen_symmetries.add(sym_key)
# Create the full skeleton.
skel = Skeleton(
nodes=nodes,
edges=edges,
symmetries=symmetries,
name=skel["graph"]["name"],
)
skeleton_objects.append(skel)
return skeleton_objects
SkeletonSLPEncoder
¶
Encode skeleton data to SLP format.
This encoder produces the SLP format used within .slp files, which uses integer indices for node references instead of embedded node objects.
Methods:
| Name | Description |
|---|---|
encode_skeletons |
Serialize a list of Skeleton objects to SLP format. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonSLPEncoder:
"""Encode skeleton data to SLP format.
This encoder produces the SLP format used within .slp files, which uses
integer indices for node references instead of embedded node objects.
"""
def encode_skeletons(
self, skeletons: list[Skeleton]
) -> tuple[list[dict], list[dict]]:
"""Serialize a list of Skeleton objects to SLP format.
Args:
skeletons: A list of Skeleton objects.
Returns:
A tuple of (skeletons_dicts, nodes_dicts).
nodes_dicts is a list of dicts containing the nodes in all the skeletons.
skeletons_dicts is a list of dicts containing the skeletons.
"""
# Create global list of nodes with all nodes from all skeletons.
nodes_dicts = []
node_to_id = {}
for skeleton in skeletons:
for node in skeleton.nodes:
if node not in node_to_id:
node_to_id[node] = len(node_to_id)
nodes_dicts.append({"name": node.name, "weight": 1.0})
skeletons_dicts = []
for skeleton in skeletons:
# Build links dicts for normal edges.
edges_dicts = []
for edge_ind, edge in enumerate(skeleton.edges):
if edge_ind == 0:
edge_type = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [1]}, # 1 = real edge, 2 = symmetry edge
]
}
else:
edge_type = {"py/id": 1}
edges_dicts.append(
{
"edge_insert_idx": edge_ind,
"key": 0, # Always 0.
"source": node_to_id[edge.source],
"target": node_to_id[edge.destination],
"type": edge_type,
}
)
# Build links dicts for symmetry edges.
for symmetry_ind, symmetry in enumerate(skeleton.symmetries):
if symmetry_ind == 0:
edge_type = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [2]}, # 1 = real edge, 2 = symmetry edge
]
}
else:
edge_type = {"py/id": 2}
src, dst = tuple(symmetry.nodes)
edges_dicts.append(
{
"key": 0,
"source": node_to_id[src],
"target": node_to_id[dst],
"type": edge_type,
}
)
# Create skeleton dict.
skeletons_dicts.append(
{
"directed": True,
"graph": {
"name": skeleton.name,
"num_edges_inserted": len(skeleton.edges),
},
"links": edges_dicts,
"multigraph": True,
"nodes": [{"id": node_to_id[node]} for node in skeleton.nodes],
}
)
return skeletons_dicts, nodes_dicts
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 707, '__doc__': 'Encode skeleton data to SLP format.\n\nThis encoder produces the SLP format used within .slp files, which uses\ninteger indices for node references instead of embedded node objects.\n', 'encode_skeletons': <function SkeletonSLPEncoder.encode_skeletons at 0x7f0836935ee0>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'SkeletonSLPEncoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonSLPEncoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Encode skeleton data to SLP format.\n\nThis encoder produces the SLP format used within .slp files, which uses\ninteger indices for node references instead of embedded node objects.\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__ = 707
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__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
encode_skeletons(skeletons)
¶
Serialize a list of Skeleton objects to SLP format.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeletons
|
list[Skeleton]
|
A list of Skeleton objects. |
required |
Returns:
| Type | Description |
|---|---|
tuple[list[dict], list[dict]]
|
A tuple of (skeletons_dicts, nodes_dicts). nodes_dicts is a list of dicts containing the nodes in all the skeletons. skeletons_dicts is a list of dicts containing the skeletons. |
Source code in sleap_io/io/skeleton.py
def encode_skeletons(
self, skeletons: list[Skeleton]
) -> tuple[list[dict], list[dict]]:
"""Serialize a list of Skeleton objects to SLP format.
Args:
skeletons: A list of Skeleton objects.
Returns:
A tuple of (skeletons_dicts, nodes_dicts).
nodes_dicts is a list of dicts containing the nodes in all the skeletons.
skeletons_dicts is a list of dicts containing the skeletons.
"""
# Create global list of nodes with all nodes from all skeletons.
nodes_dicts = []
node_to_id = {}
for skeleton in skeletons:
for node in skeleton.nodes:
if node not in node_to_id:
node_to_id[node] = len(node_to_id)
nodes_dicts.append({"name": node.name, "weight": 1.0})
skeletons_dicts = []
for skeleton in skeletons:
# Build links dicts for normal edges.
edges_dicts = []
for edge_ind, edge in enumerate(skeleton.edges):
if edge_ind == 0:
edge_type = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [1]}, # 1 = real edge, 2 = symmetry edge
]
}
else:
edge_type = {"py/id": 1}
edges_dicts.append(
{
"edge_insert_idx": edge_ind,
"key": 0, # Always 0.
"source": node_to_id[edge.source],
"target": node_to_id[edge.destination],
"type": edge_type,
}
)
# Build links dicts for symmetry edges.
for symmetry_ind, symmetry in enumerate(skeleton.symmetries):
if symmetry_ind == 0:
edge_type = {
"py/reduce": [
{"py/type": "sleap.skeleton.EdgeType"},
{"py/tuple": [2]}, # 1 = real edge, 2 = symmetry edge
]
}
else:
edge_type = {"py/id": 2}
src, dst = tuple(symmetry.nodes)
edges_dicts.append(
{
"key": 0,
"source": node_to_id[src],
"target": node_to_id[dst],
"type": edge_type,
}
)
# Create skeleton dict.
skeletons_dicts.append(
{
"directed": True,
"graph": {
"name": skeleton.name,
"num_edges_inserted": len(skeleton.edges),
},
"links": edges_dicts,
"multigraph": True,
"nodes": [{"id": node_to_id[node]} for node in skeleton.nodes],
}
)
return skeletons_dicts, nodes_dicts
SkeletonYAMLDecoder
¶
Decode skeleton data from simplified YAML format.
This decoder handles a simplified YAML format that is more human-readable than the jsonpickle format.
Methods:
| Name | Description |
|---|---|
decode |
Decode skeleton(s) from YAML data. |
decode_dict |
Decode a single skeleton from a dictionary. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonYAMLDecoder:
"""Decode skeleton data from simplified YAML format.
This decoder handles a simplified YAML format that is more human-readable
than the jsonpickle format.
"""
def decode(self, data: str | dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from YAML data.
Args:
data: YAML string or pre-parsed dictionary containing skeleton data.
If a dict is provided with skeleton names as keys, returns list.
If a dict is provided with nodes/edges/symmetries, returns single
skeleton.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
if isinstance(data, str):
import yaml
data = yaml.safe_load(data)
# Check if this is a single skeleton dict or multiple skeletons
if isinstance(data, dict):
# If it has nodes/edges keys, it's a single skeleton
if "nodes" in data:
return self._decode_skeleton(data)
else:
# Multiple skeletons with names as keys
skeletons = []
for name, skeleton_data in data.items():
skeleton = self._decode_skeleton(skeleton_data, name)
skeletons.append(skeleton)
return skeletons
raise ValueError(f"Unexpected data format: {type(data)}")
def decode_dict(self, skeleton_data: dict, name: str = "Skeleton") -> Skeleton:
"""Decode a single skeleton from a dictionary.
This is useful when the skeleton data is embedded in a larger YAML structure.
Args:
skeleton_data: Dictionary containing nodes, edges, and symmetries.
name: Name for the skeleton (default: "Skeleton").
Returns:
A Skeleton object.
"""
return self._decode_skeleton(skeleton_data, name)
def _decode_skeleton(self, data: dict, name: str | None = None) -> Skeleton:
"""Decode a single skeleton from dictionary data.
Args:
data: Dictionary containing skeleton data in simplified format.
name: Optional name override for the skeleton.
Returns:
A Skeleton object.
"""
# Create nodes
nodes = []
node_map = {}
for node_data in data.get("nodes", []):
node = Node(name=node_data["name"])
nodes.append(node)
node_map[node.name] = node
# Create edges
edges = []
for edge_data in data.get("edges", []):
source_name = edge_data["source"]["name"]
dest_name = edge_data["destination"]["name"]
edge = Edge(source=node_map[source_name], destination=node_map[dest_name])
edges.append(edge)
# Create symmetries
symmetries = []
for sym_data in data.get("symmetries", []):
# Each symmetry is a list of 2 node specifications
node1_name = sym_data[0]["name"]
node2_name = sym_data[1]["name"]
symmetry = Symmetry([node_map[node1_name], node_map[node2_name]])
symmetries.append(symmetry)
# Use provided name or get from data
if name is None:
name = data.get("name", "Skeleton")
return Skeleton(nodes=nodes, edges=edges, symmetries=symmetries, name=name)
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 801, '__doc__': 'Decode skeleton data from simplified YAML format.\n\nThis decoder handles a simplified YAML format that is more human-readable\nthan the jsonpickle format.\n', 'decode': <function SkeletonYAMLDecoder.decode at 0x7f0836935da0>, 'decode_dict': <function SkeletonYAMLDecoder.decode_dict at 0x7f0836935f80>, '_decode_skeleton': <function SkeletonYAMLDecoder._decode_skeleton at 0x7f0836934900>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'SkeletonYAMLDecoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonYAMLDecoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Decode skeleton data from simplified YAML format.\n\nThis decoder handles a simplified YAML format that is more human-readable\nthan the jsonpickle format.\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__ = 801
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__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
decode(data)
¶
Decode skeleton(s) from YAML data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str | dict
|
YAML string or pre-parsed dictionary containing skeleton data. If a dict is provided with skeleton names as keys, returns list. If a dict is provided with nodes/edges/symmetries, returns single skeleton. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons depending on input format. |
Source code in sleap_io/io/skeleton.py
def decode(self, data: str | dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from YAML data.
Args:
data: YAML string or pre-parsed dictionary containing skeleton data.
If a dict is provided with skeleton names as keys, returns list.
If a dict is provided with nodes/edges/symmetries, returns single
skeleton.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
if isinstance(data, str):
import yaml
data = yaml.safe_load(data)
# Check if this is a single skeleton dict or multiple skeletons
if isinstance(data, dict):
# If it has nodes/edges keys, it's a single skeleton
if "nodes" in data:
return self._decode_skeleton(data)
else:
# Multiple skeletons with names as keys
skeletons = []
for name, skeleton_data in data.items():
skeleton = self._decode_skeleton(skeleton_data, name)
skeletons.append(skeleton)
return skeletons
raise ValueError(f"Unexpected data format: {type(data)}")
decode_dict(skeleton_data, name='Skeleton')
¶
Decode a single skeleton from a dictionary.
This is useful when the skeleton data is embedded in a larger YAML structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeleton_data
|
dict
|
Dictionary containing nodes, edges, and symmetries. |
required |
name
|
str
|
Name for the skeleton (default: "Skeleton"). |
'Skeleton'
|
Returns:
| Type | Description |
|---|---|
Skeleton
|
A Skeleton object. |
Source code in sleap_io/io/skeleton.py
def decode_dict(self, skeleton_data: dict, name: str = "Skeleton") -> Skeleton:
"""Decode a single skeleton from a dictionary.
This is useful when the skeleton data is embedded in a larger YAML structure.
Args:
skeleton_data: Dictionary containing nodes, edges, and symmetries.
name: Name for the skeleton (default: "Skeleton").
Returns:
A Skeleton object.
"""
return self._decode_skeleton(skeleton_data, name)
SkeletonYAMLEncoder
¶
Encode skeleton data to simplified YAML format.
This encoder produces a human-readable YAML format that is easier to edit manually than the jsonpickle format.
Methods:
| Name | Description |
|---|---|
encode |
Encode skeleton(s) to YAML string. |
encode_dict |
Encode a single skeleton to a dictionary. |
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. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/io/skeleton.py
class SkeletonYAMLEncoder:
"""Encode skeleton data to simplified YAML format.
This encoder produces a human-readable YAML format that is easier to
edit manually than the jsonpickle format.
"""
def encode(self, skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to YAML string.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
YAML string with skeleton names as top-level keys.
"""
import yaml
if isinstance(skeletons, Skeleton):
skeletons = [skeletons]
data = {}
for skeleton in skeletons:
skeleton_data = self.encode_dict(skeleton)
data[skeleton.name] = skeleton_data
return yaml.dump(data, default_flow_style=False, sort_keys=False)
def encode_dict(self, skeleton: Skeleton) -> dict:
"""Encode a single skeleton to a dictionary.
This is useful when embedding skeleton data in a larger YAML structure.
Args:
skeleton: Skeleton object to encode.
Returns:
Dictionary with nodes, edges, and symmetries.
"""
# Encode nodes
nodes = []
for node in skeleton.nodes:
nodes.append({"name": node.name})
# Encode edges
edges = []
for edge in skeleton.edges:
edges.append(
{
"source": {"name": edge.source.name},
"destination": {"name": edge.destination.name},
}
)
# Encode symmetries
symmetries = []
for symmetry in skeleton.symmetries:
# Convert set to list and encode as pairs
node_list = list(symmetry.nodes)
symmetries.append(
[{"name": node_list[0].name}, {"name": node_list[1].name}]
)
return {"nodes": nodes, "edges": edges, "symmetries": symmetries}
__dict__ = mappingproxy({'__module__': 'sleap_io.io.skeleton', '__firstlineno__': 896, '__doc__': 'Encode skeleton data to simplified YAML format.\n\nThis encoder produces a human-readable YAML format that is easier to\nedit manually than the jsonpickle format.\n', 'encode': <function SkeletonYAMLEncoder.encode at 0x7f08369347c0>, 'encode_dict': <function SkeletonYAMLEncoder.encode_dict at 0x7f0836934720>, '__static_attributes__': (), '__dict__': <attribute '__dict__' of 'SkeletonYAMLEncoder' objects>, '__weakref__': <attribute '__weakref__' of 'SkeletonYAMLEncoder' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Encode skeleton data to simplified YAML format.\n\nThis encoder produces a human-readable YAML format that is easier to\nedit manually than the jsonpickle format.\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__ = 896
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 iteral.
int('0b100', base=0) 4
__module__ = 'sleap_io.io.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'.
__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
encode(skeletons)
¶
Encode skeleton(s) to YAML string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeletons
|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons to encode. |
required |
Returns:
| Type | Description |
|---|---|
str
|
YAML string with skeleton names as top-level keys. |
Source code in sleap_io/io/skeleton.py
def encode(self, skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to YAML string.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
YAML string with skeleton names as top-level keys.
"""
import yaml
if isinstance(skeletons, Skeleton):
skeletons = [skeletons]
data = {}
for skeleton in skeletons:
skeleton_data = self.encode_dict(skeleton)
data[skeleton.name] = skeleton_data
return yaml.dump(data, default_flow_style=False, sort_keys=False)
encode_dict(skeleton)
¶
Encode a single skeleton to a dictionary.
This is useful when embedding skeleton data in a larger YAML structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeleton
|
Skeleton
|
Skeleton object to encode. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Dictionary with nodes, edges, and symmetries. |
Source code in sleap_io/io/skeleton.py
def encode_dict(self, skeleton: Skeleton) -> dict:
"""Encode a single skeleton to a dictionary.
This is useful when embedding skeleton data in a larger YAML structure.
Args:
skeleton: Skeleton object to encode.
Returns:
Dictionary with nodes, edges, and symmetries.
"""
# Encode nodes
nodes = []
for node in skeleton.nodes:
nodes.append({"name": node.name})
# Encode edges
edges = []
for edge in skeleton.edges:
edges.append(
{
"source": {"name": edge.source.name},
"destination": {"name": edge.destination.name},
}
)
# Encode symmetries
symmetries = []
for symmetry in skeleton.symmetries:
# Convert set to list and encode as pairs
node_list = list(symmetry.nodes)
symmetries.append(
[{"name": node_list[0].name}, {"name": node_list[1].name}]
)
return {"nodes": nodes, "edges": edges, "symmetries": symmetries}
Symmetry
¶
A relationship between a pair of nodes denoting their left/right pairing.
Attributes:
| Name | Type | Description |
|---|---|---|
nodes |
A set of two |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class Symmetry. |
__getitem__ |
Return the first node. |
__init__ |
Method generated by attrs for class Symmetry. |
__iter__ |
Iterate over the symmetric nodes. |
__repr__ |
Method generated by attrs for class Symmetry. |
__setattr__ |
Method generated by attrs for class Symmetry. |
Source code in sleap_io/model/skeleton.py
@define
class Symmetry:
"""A relationship between a pair of nodes denoting their left/right pairing.
Attributes:
nodes: A set of two `Node`s.
"""
nodes: set[Node] = field(converter=set, validator=lambda _, __, val: len(val) == 2)
def __iter__(self):
"""Iterate over the symmetric nodes."""
return iter(self.nodes)
def __getitem__(self, idx) -> Node:
"""Return the first node."""
for i, node in enumerate(self.nodes):
if i == idx:
return node
__annotations__ = {'nodes': 'set[Node]'}
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=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, 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 relationship between a pair of nodes denoting their left/right pairing.\n\nAttributes:\n nodes: A set of two `Node`s.\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__ = 57
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 iteral.
int('0b100', base=0) 4
__match_args__ = ('nodes',)
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', '__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
__eq__(other)
¶
__getitem__(idx)
¶
__init__(nodes)
¶
__iter__()
¶
__repr__()
¶
Method generated by attrs for class Symmetry.
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 re
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
__setattr__(name, val)
¶
Method generated by attrs for class Symmetry.
decode_skeleton(data)
¶
Decode skeleton(s) from JSON data using the default decoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
str | dict
|
JSON string or pre-parsed dictionary containing skeleton data. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons depending on input format. |
Source code in sleap_io/io/skeleton.py
def decode_skeleton(data: str | dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from JSON data using the default decoder.
Args:
data: JSON string or pre-parsed dictionary containing skeleton data.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
decoder = SkeletonDecoder()
return decoder.decode(data)
decode_training_config(data)
¶
Decode skeleton(s) from training config data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict
|
Dictionary containing training config with embedded skeletons. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons from the training config. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the data is not a valid training config format. |
Source code in sleap_io/io/skeleton.py
def decode_training_config(data: dict) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from training config data.
Args:
data: Dictionary containing training config with embedded skeletons.
Returns:
A single Skeleton or list of Skeletons from the training config.
Raises:
ValueError: If the data is not a valid training config format.
"""
if isinstance(data, dict) and "data" in data:
if "labels" in data["data"] and "skeletons" in data["data"]["labels"]:
# This is a training config file with embedded skeletons
decoder = SkeletonDecoder()
return decoder.decode(data["data"]["labels"]["skeletons"])
# If not a valid training config, raise an exception
raise ValueError(
"Invalid training config format. Expected dictionary with "
"'data.labels.skeletons' structure."
)
decode_yaml_skeleton(yaml_data)
¶
Decode skeleton(s) from YAML data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yaml_data
|
str
|
YAML string containing skeleton data. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons depending on input format. |
Source code in sleap_io/io/skeleton.py
def decode_yaml_skeleton(yaml_data: str) -> Skeleton | list[Skeleton]:
"""Decode skeleton(s) from YAML data.
Args:
yaml_data: YAML string containing skeleton data.
Returns:
A single Skeleton or list of Skeletons depending on input format.
"""
decoder = SkeletonYAMLDecoder()
return decoder.decode(yaml_data)
encode_skeleton(skeletons)
¶
Encode skeleton(s) to JSON string using the default encoder.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeletons
|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons to encode. |
required |
Returns:
| Type | Description |
|---|---|
str
|
JSON string in jsonpickle format. |
Source code in sleap_io/io/skeleton.py
def encode_skeleton(skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to JSON string using the default encoder.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
JSON string in jsonpickle format.
"""
encoder = SkeletonEncoder()
return encoder.encode(skeletons)
encode_yaml_skeleton(skeletons)
¶
Encode skeleton(s) to YAML string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeletons
|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons to encode. |
required |
Returns:
| Type | Description |
|---|---|
str
|
YAML string with skeleton names as top-level keys. |
Source code in sleap_io/io/skeleton.py
def encode_yaml_skeleton(skeletons: Skeleton | list[Skeleton]) -> str:
"""Encode skeleton(s) to YAML string.
Args:
skeletons: A single Skeleton or list of Skeletons to encode.
Returns:
YAML string with skeleton names as top-level keys.
"""
encoder = SkeletonYAMLEncoder()
return encoder.encode(skeletons)
load_skeleton_from_json(json_data)
¶
Load skeleton(s) from JSON data, with automatic training config detection.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
json_data
|
str
|
JSON string that could be standalone skeleton or training config. |
required |
Returns:
| Type | Description |
|---|---|
Skeleton | list[Skeleton]
|
A single Skeleton or list of Skeletons. |
Source code in sleap_io/io/skeleton.py
def load_skeleton_from_json(json_data: str) -> Skeleton | list[Skeleton]:
"""Load skeleton(s) from JSON data, with automatic training config detection.
Args:
json_data: JSON string that could be standalone skeleton or training config.
Returns:
A single Skeleton or list of Skeletons.
"""
# Try to detect if this is a training config file
try:
data = json.loads(json_data)
if isinstance(data, dict) and "data" in data:
if "labels" in data["data"] and "skeletons" in data["data"]["labels"]:
# This is a training config file with embedded skeletons
return decode_training_config(data)
except (json.JSONDecodeError, KeyError, TypeError):
# Not a training config or invalid JSON structure
pass
# Fall back to regular skeleton JSON decoding
return decode_skeleton(json_data)