Skip to content

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 objects within a Skeleton.

Node

A landmark type within a Skeleton.

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 Node.

destination

The destination Node.

Methods:

Name Description
__eq__

Method generated by attrs for class Edge.

__getitem__

Return the source Node (idx is 0) or destination Node (idx is 1).

__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 slotted <slotted classes>.

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 __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

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 hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. 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)

Method generated by attrs for class Edge.

Source code in sleap_io/model/skeleton.py
@define(eq=False)
class Node:
    """A landmark type within a `Skeleton`.

    This typically corresponds to a unique landmark within a skeleton, such as the "left

__getitem__(idx)

Return the source Node (idx is 0) or destination Node (idx is 1).

Source code in sleap_io/model/skeleton.py
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).")

__hash__()

Method generated by attrs for class Edge.

Source code in sleap_io/model/skeleton.py
eye".

Attributes:
    name: Descriptive label for the landmark.
"""

__init__(source, destination)

Method generated by attrs for class Edge.

Source code in sleap_io/model/skeleton.py
    name: str


@define(frozen=True)

__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
@define(eq=False)
class Node:
    """A landmark type within a `Skeleton`.

    This typically corresponds to a unique landmark within a skeleton, such as the "left
    eye".

    Attributes:
        name: Descriptive label for the landmark.
    """

    name: str

__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 slotted <slotted classes>.

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 __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

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 hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. 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)

Method generated by attrs for class Node.

Source code in sleap_io/model/skeleton.py

__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 Nodes. May be specified as a list of strings to create new nodes from their names.

edges

A list of Edges. 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 Symmetrys. 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.

Methods:

Name Description
__attrs_post_init__

Ensure nodes are Nodes, edges are Edges, and Node map is updated.

__contains__

Check if a node is in the skeleton.

__getitem__

Return a Node when indexing by name or integer.

__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 Edge to the skeleton.

add_edges

Add multiple Edges to the skeleton.

add_node

Add a Node to the skeleton.

add_nodes

Add multiple Nodes to the skeleton.

add_symmetries

Add multiple Symmetry relationships to the skeleton.

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 Node or string name.

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 Node map caches.

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 Node object, handling indexing and adding missing nodes.

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 slotted <slotted classes>.

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 __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

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 hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. 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.

Source code in sleap_io/model/skeleton.py
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()

__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
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}")

__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__()

Return the number of nodes in the skeleton.

Source code in sleap_io/model/skeleton.py
def __len__(self) -> int:
    """Return the number of nodes in the skeleton."""
    return len(self.nodes)

__repr__()

Return a readable representation of the skeleton.

Source code in sleap_io/model/skeleton.py
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})"

__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 Node, name or index.

required
dst Union | None

The destination node specified as a Node, name or index.

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 Edge objects or 2-tuples of source and destination nodes.

required
Source code in sleap_io/model/skeleton.py
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)

add_node(node)

Add a Node to the skeleton.

Parameters:

Name Type Description Default
node Node | str

A Node object or a string name to create a new node.

required

Raises:

Type Description
ValueError

If the node already exists in the skeleton or if the node is not specified as a Node or string.

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 Node objects or string names to create new nodes.

required
Source code in sleap_io/model/skeleton.py
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)

add_symmetries(symmetries)

Add multiple Symmetry relationships to the skeleton.

Parameters:

Name Type Description Default
symmetries list[Symmetry | tuple[Union, Union]]

A list of Symmetry objects or 2-tuples of symmetric nodes.

required
Source code in sleap_io/model/skeleton.py
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)

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 Node, name or index. If a Symmetry object is provided, it will be added directly to the skeleton.

None
node2 Union | None

The second node specified as a Node, name or index.

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 (left_token, right_token) string pairs used to recognize laterality, matched case-insensitively against whole name segments. Defaults to [("left", "right"), ("l", "r")].

None

Returns:

Type Description
list[tuple[int, int]]

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')]

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 Node objects.

required

Returns:

Type Description
tuple[list[int], list[int]]

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

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 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.

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 Node object.

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 Node objects to remove.

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 Node object.

required
new_name str

The new name for the node.

required
Source code in sleap_io/model/skeleton.py
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})

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 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.

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 Node objects specifying the new order of the nodes.

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 Node object, name or index.

required
add_missing bool

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.

True

Returns:

Type Description
Node

The Node object.

Raises:

Type Description
IndexError

If the node is not found in the skeleton and add_missing is False.

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__()

Initialize the decoder.

Source code in sleap_io/io/skeleton.py
def __init__(self):
    """Initialize the decoder."""
    self.decoded_objects: list[
        Any
    ] = []  # List of decoded objects indexed by py/id - 1

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__()

Initialize the encoder.

Source code in sleap_io/io/skeleton.py
def __init__(self):
    """Initialize the encoder."""
    self._object_to_id: dict[int, int] = {}  # id(object) -> py/id
    self._next_id = 1

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 Nodes.

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 slotted <slotted classes>.

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 __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

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 hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. 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)

Method generated by attrs for class Symmetry.

Source code in sleap_io/model/skeleton.py
@define(eq=False)
class Node:
    """A landmark type within a `Skeleton`.

__getitem__(idx)

Return the first node.

Source code in sleap_io/model/skeleton.py
def __getitem__(self, idx) -> Node:
    """Return the first node."""
    for i, node in enumerate(self.nodes):
        if i == idx:
            return node

__init__(nodes)

Method generated by attrs for class Symmetry.

Source code in sleap_io/model/skeleton.py
This typically corresponds to a unique landmark within a skeleton, such as the "left
eye".

Attributes:
    name: Descriptive label for the landmark.

__iter__()

Iterate over the symmetric nodes.

Source code in sleap_io/model/skeleton.py
def __iter__(self):
    """Iterate over the symmetric nodes."""
    return iter(self.nodes)

__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)