Skip to content

rendering

sleap_io.rendering

Rendering module for visualizing pose data using skia-python.

This module provides high-performance pose rendering with: - Multiple color schemes (by track, instance, or node type) - Various marker shapes (circle, square, diamond, triangle, cross) - Configurable aesthetics (size, width, alpha) - Custom rendering callbacks for overlays - Video export with optional scaling

Example

import sleap_io as sio labels = sio.load_slp("predictions.slp") sio.render_video(labels, "output.mp4") sio.render_image(labels.labeled_frames[0], "frame.png")

Modules:

Name Description
callbacks

Callback context classes for custom rendering.

colors

Color palette utilities for pose rendering.

core

Core rendering functions for pose visualization.

shapes

Marker shape drawing functions for pose rendering.

Classes:

Name Description
InstanceContext

Context passed to per-instance callbacks.

RenderContext

Context passed to pre/post render callbacks.

Functions:

Name Description
get_palette

Get n colors from a named palette as RGB tuples.

render_image

Render single frame with pose overlays.

render_video

Render video with pose overlays.

resolve_color

Resolve a flexible color specification to an RGB tuple.

Attributes:

Name Type Description
ColorSpec

Represent a PEP 604 union type

NAMED_COLORS

dict() -> new empty dictionary

ColorSpec = tuple[int, int, int] | tuple[float, float, float] | int | float | str module-attribute

Represent a PEP 604 union type

E.g. for int | str

NAMED_COLORS = {'black': (0, 0, 0), 'white': (255, 255, 255), 'red': (255, 0, 0), 'green': (0, 255, 0), 'blue': (0, 0, 255), 'yellow': (255, 255, 0), 'cyan': (0, 255, 255), 'magenta': (255, 0, 255), 'gray': (128, 128, 128), 'grey': (128, 128, 128), 'orange': (255, 165, 0), 'purple': (128, 0, 128), 'pink': (255, 192, 203), 'brown': (139, 69, 19)} module-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)

__all__ = ['render_video', 'render_image', 'get_palette', 'resolve_color', 'ColorSpec', 'NAMED_COLORS', 'RenderContext', 'InstanceContext'] module-attribute

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/rendering/__pycache__/__init__.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__ = 'Rendering module for visualizing pose data using skia-python.\n\nThis module provides high-performance pose rendering with:\n- Multiple color schemes (by track, instance, or node type)\n- Various marker shapes (circle, square, diamond, triangle, cross)\n- Configurable aesthetics (size, width, alpha)\n- Custom rendering callbacks for overlays\n- Video export with optional scaling\n\nExample:\n >>> import sleap_io as sio\n >>> labels = sio.load_slp("predictions.slp")\n >>> sio.render_video(labels, "output.mp4")\n >>> sio.render_image(labels.labeled_frames[0], "frame.png")\n' module-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/rendering/__init__.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.rendering' 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.rendering' 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'.

__path__ = ['/home/runner/work/sleap-io/sleap-io/sleap_io/rendering'] module-attribute

Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list. The argument must be an iterable if specified.

InstanceContext

Context passed to per-instance callbacks.

This context provides access to the Skia canvas and instance-level metadata for drawing custom overlays after each instance is rendered.

Attributes:

Name Type Description
canvas

Skia canvas for drawing.

instance_idx

Index of this instance within the frame.

points

(n_nodes, 2) array of keypoint coordinates.

track_id

Track ID if assigned, else None.

track_name

Track name string if available.

confidence

Instance confidence score if available.

skeleton_edges

Edge connectivity as list of (src, dst) tuples.

node_names

List of node name strings.

scale

Current scale factor for rendering.

offset

Current offset (x, y) for cropped/zoomed views.

Methods:

Name Description
__eq__

Method generated by attrs for class InstanceContext.

__init__

Method generated by attrs for class InstanceContext.

__replace__

Method generated by attrs for class InstanceContext.

__repr__

Method generated by attrs for class InstanceContext.

get_bbox

Get bounding box of valid points.

get_centroid

Get centroid of valid points.

world_to_canvas

Transform world coordinates to canvas coordinates.

Source code in sleap_io/rendering/callbacks.py
@define
class InstanceContext:
    """Context passed to per-instance callbacks.

    This context provides access to the Skia canvas and instance-level metadata
    for drawing custom overlays after each instance is rendered.

    Attributes:
        canvas: Skia canvas for drawing.
        instance_idx: Index of this instance within the frame.
        points: (n_nodes, 2) array of keypoint coordinates.
        track_id: Track ID if assigned, else None.
        track_name: Track name string if available.
        confidence: Instance confidence score if available.
        skeleton_edges: Edge connectivity as list of (src, dst) tuples.
        node_names: List of node name strings.
        scale: Current scale factor for rendering.
        offset: Current offset (x, y) for cropped/zoomed views.
    """

    canvas: "skia.Canvas"
    instance_idx: int
    points: np.ndarray
    skeleton_edges: list[tuple[int, int]]
    node_names: list[str]
    track_id: int | None = None
    track_name: str | None = None
    confidence: float | None = None
    scale: float = 1.0
    offset: tuple[float, float] = (0.0, 0.0)

    def world_to_canvas(self, x: float, y: float) -> tuple[float, float]:
        """Transform world coordinates to canvas coordinates.

        Args:
            x: X coordinate in world/frame space.
            y: Y coordinate in world/frame space.

        Returns:
            (x, y) coordinates in canvas space.
        """
        return (
            (x - self.offset[0]) * self.scale,
            (y - self.offset[1]) * self.scale,
        )

    def get_centroid(self) -> tuple[float, float] | None:
        """Get centroid of valid points.

        Returns:
            (x, y) mean of valid (non-NaN) points, or None if all invalid.
        """
        valid_mask = np.isfinite(self.points).all(axis=1)
        valid_points = self.points[valid_mask]
        if len(valid_points) == 0:
            return None
        mean_pt = valid_points.mean(axis=0)
        return (float(mean_pt[0]), float(mean_pt[1]))

    def get_bbox(self) -> tuple[float, float, float, float] | None:
        """Get bounding box of valid points.

        Returns:
            (x1, y1, x2, y2) bounding box, or None if no valid points.
        """
        valid_mask = np.isfinite(self.points).all(axis=1)
        valid_points = self.points[valid_mask]
        if len(valid_points) == 0:
            return None
        return (
            float(valid_points[:, 0].min()),
            float(valid_points[:, 1].min()),
            float(valid_points[:, 0].max()),
            float(valid_points[:, 1].max()),
        )

__annotations__ = {'canvas': "'skia.Canvas'", 'instance_idx': 'int', 'points': 'np.ndarray', 'skeleton_edges': 'list[tuple[int, int]]', 'node_names': 'list[str]', 'track_id': 'int | None', 'track_name': 'str | None', 'confidence': 'float | None', 'scale': 'float', 'offset': 'tuple[float, float]'} 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=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 0x7f41e68fca40>, field_transformer=None) class-attribute

Effective class properties as derived from parameters to attr.s() or define() decorators.

This is the same data structure that attrs uses internally to decide how to construct the final class.

Warning:

This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is 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__ = 'Context passed to per-instance callbacks.\n\nThis context provides access to the Skia canvas and instance-level metadata\nfor drawing custom overlays after each instance is rendered.\n\nAttributes:\n canvas: Skia canvas for drawing.\n instance_idx: Index of this instance within the frame.\n points: (n_nodes, 2) array of keypoint coordinates.\n track_id: Track ID if assigned, else None.\n track_name: Track name string if available.\n confidence: Instance confidence score if available.\n skeleton_edges: Edge connectivity as list of (src, dst) tuples.\n node_names: List of node name strings.\n scale: Current scale factor for rendering.\n offset: Current offset (x, y) for cropped/zoomed views.\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__ = 61 class-attribute

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

int('0b100', base=0) 4

__match_args__ = ('canvas', 'instance_idx', 'points', 'skeleton_edges', 'node_names', 'track_id', 'track_name', 'confidence', 'scale', 'offset') 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.rendering.callbacks' 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__ = ('canvas', 'instance_idx', 'points', 'skeleton_edges', 'node_names', 'track_id', 'track_name', 'confidence', 'scale', 'offset', '__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 InstanceContext.

Source code in sleap_io/rendering/callbacks.py
@define
class RenderContext:
    """Context passed to pre/post render callbacks.

    This context provides access to the Skia canvas and frame-level metadata
    for drawing custom overlays before or after pose rendering.

    Attributes:
        canvas: Skia canvas for drawing.
        frame_idx: Current frame index.
        frame_size: (width, height) tuple of original frame dimensions.
        instances: List of instances in this frame.
        skeleton_edges: Edge connectivity as list of (src, dst) tuples.

__init__(canvas, instance_idx, points, skeleton_edges, node_names, track_id=None, track_name=None, confidence=None, scale=1.0, offset=(0.0, 0.0))

Method generated by attrs for class InstanceContext.

Source code in sleap_io/rendering/callbacks.py
    node_names: List of node name strings.
    scale: Current scale factor for rendering.
    offset: Current offset (x, y) for cropped/zoomed views.
"""

canvas: "skia.Canvas"
frame_idx: int
frame_size: tuple[int, int]
instances: list
skeleton_edges: list[tuple[int, int]]
node_names: list[str]

__replace__(**changes)

Method generated by attrs for class InstanceContext.

__repr__()

Method generated by attrs for class InstanceContext.

Source code in sleap_io/rendering/callbacks.py
"""Callback context classes for custom rendering.

This module provides context objects that are passed to user-defined callbacks
during rendering, giving access to the Skia canvas and rendering metadata.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from attrs import define

if TYPE_CHECKING:
    import skia

get_bbox()

Get bounding box of valid points.

Returns:

Type Description
tuple[float, float, float, float] | None

(x1, y1, x2, y2) bounding box, or None if no valid points.

Source code in sleap_io/rendering/callbacks.py
def get_bbox(self) -> tuple[float, float, float, float] | None:
    """Get bounding box of valid points.

    Returns:
        (x1, y1, x2, y2) bounding box, or None if no valid points.
    """
    valid_mask = np.isfinite(self.points).all(axis=1)
    valid_points = self.points[valid_mask]
    if len(valid_points) == 0:
        return None
    return (
        float(valid_points[:, 0].min()),
        float(valid_points[:, 1].min()),
        float(valid_points[:, 0].max()),
        float(valid_points[:, 1].max()),
    )

get_centroid()

Get centroid of valid points.

Returns:

Type Description
tuple[float, float] | None

(x, y) mean of valid (non-NaN) points, or None if all invalid.

Source code in sleap_io/rendering/callbacks.py
def get_centroid(self) -> tuple[float, float] | None:
    """Get centroid of valid points.

    Returns:
        (x, y) mean of valid (non-NaN) points, or None if all invalid.
    """
    valid_mask = np.isfinite(self.points).all(axis=1)
    valid_points = self.points[valid_mask]
    if len(valid_points) == 0:
        return None
    mean_pt = valid_points.mean(axis=0)
    return (float(mean_pt[0]), float(mean_pt[1]))

world_to_canvas(x, y)

Transform world coordinates to canvas coordinates.

Parameters:

Name Type Description Default
x float

X coordinate in world/frame space.

required
y float

Y coordinate in world/frame space.

required

Returns:

Type Description
tuple[float, float]

(x, y) coordinates in canvas space.

Source code in sleap_io/rendering/callbacks.py
def world_to_canvas(self, x: float, y: float) -> tuple[float, float]:
    """Transform world coordinates to canvas coordinates.

    Args:
        x: X coordinate in world/frame space.
        y: Y coordinate in world/frame space.

    Returns:
        (x, y) coordinates in canvas space.
    """
    return (
        (x - self.offset[0]) * self.scale,
        (y - self.offset[1]) * self.scale,
    )

RenderContext

Context passed to pre/post render callbacks.

This context provides access to the Skia canvas and frame-level metadata for drawing custom overlays before or after pose rendering.

Attributes:

Name Type Description
canvas

Skia canvas for drawing.

frame_idx

Current frame index.

frame_size

(width, height) tuple of original frame dimensions.

instances

List of instances in this frame.

skeleton_edges

Edge connectivity as list of (src, dst) tuples.

node_names

List of node name strings.

scale

Current scale factor for rendering.

offset

Current offset (x, y) for cropped/zoomed views.

Methods:

Name Description
__eq__

Method generated by attrs for class RenderContext.

__init__

Method generated by attrs for class RenderContext.

__replace__

Method generated by attrs for class RenderContext.

__repr__

Method generated by attrs for class RenderContext.

world_to_canvas

Transform world coordinates to canvas coordinates.

Source code in sleap_io/rendering/callbacks.py
@define
class RenderContext:
    """Context passed to pre/post render callbacks.

    This context provides access to the Skia canvas and frame-level metadata
    for drawing custom overlays before or after pose rendering.

    Attributes:
        canvas: Skia canvas for drawing.
        frame_idx: Current frame index.
        frame_size: (width, height) tuple of original frame dimensions.
        instances: List of instances in this frame.
        skeleton_edges: Edge connectivity as list of (src, dst) tuples.
        node_names: List of node name strings.
        scale: Current scale factor for rendering.
        offset: Current offset (x, y) for cropped/zoomed views.
    """

    canvas: "skia.Canvas"
    frame_idx: int
    frame_size: tuple[int, int]
    instances: list
    skeleton_edges: list[tuple[int, int]]
    node_names: list[str]
    scale: float = 1.0
    offset: tuple[float, float] = (0.0, 0.0)

    def world_to_canvas(self, x: float, y: float) -> tuple[float, float]:
        """Transform world coordinates to canvas coordinates.

        Args:
            x: X coordinate in world/frame space.
            y: Y coordinate in world/frame space.

        Returns:
            (x, y) coordinates in canvas space.
        """
        return (
            (x - self.offset[0]) * self.scale,
            (y - self.offset[1]) * self.scale,
        )

__annotations__ = {'canvas': "'skia.Canvas'", 'frame_idx': 'int', 'frame_size': 'tuple[int, int]', 'instances': 'list', 'skeleton_edges': 'list[tuple[int, int]]', 'node_names': 'list[str]', 'scale': 'float', 'offset': 'tuple[float, float]'} 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=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 0x7f41e68fca40>, field_transformer=None) class-attribute

Effective class properties as derived from parameters to attr.s() or define() decorators.

This is the same data structure that attrs uses internally to decide how to construct the final class.

Warning:

This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is 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__ = 'Context passed to pre/post render callbacks.\n\nThis context provides access to the Skia canvas and frame-level metadata\nfor drawing custom overlays before or after pose rendering.\n\nAttributes:\n canvas: Skia canvas for drawing.\n frame_idx: Current frame index.\n frame_size: (width, height) tuple of original frame dimensions.\n instances: List of instances in this frame.\n skeleton_edges: Edge connectivity as list of (src, dst) tuples.\n node_names: List of node name strings.\n scale: Current scale factor for rendering.\n offset: Current offset (x, y) for cropped/zoomed views.\n' class-attribute

str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str

Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.

__firstlineno__ = 18 class-attribute

int([x]) -> integer int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.int(). For floating-point numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by '+' or '-' and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal.

int('0b100', base=0) 4

__match_args__ = ('canvas', 'frame_idx', 'frame_size', 'instances', 'skeleton_edges', 'node_names', 'scale', 'offset') 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.rendering.callbacks' 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__ = ('canvas', 'frame_idx', 'frame_size', 'instances', 'skeleton_edges', 'node_names', 'scale', 'offset', '__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 RenderContext.

Source code in sleap_io/rendering/callbacks.py
@define
class RenderContext:
    """Context passed to pre/post render callbacks.

    This context provides access to the Skia canvas and frame-level metadata
    for drawing custom overlays before or after pose rendering.

    Attributes:
        canvas: Skia canvas for drawing.
        frame_idx: Current frame index.
        frame_size: (width, height) tuple of original frame dimensions.

__init__(canvas, frame_idx, frame_size, instances, skeleton_edges, node_names, scale=1.0, offset=(0.0, 0.0))

Method generated by attrs for class RenderContext.

Source code in sleap_io/rendering/callbacks.py
    instances: List of instances in this frame.
    skeleton_edges: Edge connectivity as list of (src, dst) tuples.
    node_names: List of node name strings.
    scale: Current scale factor for rendering.
    offset: Current offset (x, y) for cropped/zoomed views.
"""

canvas: "skia.Canvas"
frame_idx: int

__replace__(**changes)

Method generated by attrs for class RenderContext.

__repr__()

Method generated by attrs for class RenderContext.

Source code in sleap_io/rendering/callbacks.py
"""Callback context classes for custom rendering.

This module provides context objects that are passed to user-defined callbacks
during rendering, giving access to the Skia canvas and rendering metadata.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import numpy as np
from attrs import define

if TYPE_CHECKING:
    import skia

world_to_canvas(x, y)

Transform world coordinates to canvas coordinates.

Parameters:

Name Type Description Default
x float

X coordinate in world/frame space.

required
y float

Y coordinate in world/frame space.

required

Returns:

Type Description
tuple[float, float]

(x, y) coordinates in canvas space.

Source code in sleap_io/rendering/callbacks.py
def world_to_canvas(self, x: float, y: float) -> tuple[float, float]:
    """Transform world coordinates to canvas coordinates.

    Args:
        x: X coordinate in world/frame space.
        y: Y coordinate in world/frame space.

    Returns:
        (x, y) coordinates in canvas space.
    """
    return (
        (x - self.offset[0]) * self.scale,
        (y - self.offset[1]) * self.scale,
    )

get_palette(name, n_colors)

Get n colors from a named palette as RGB tuples.

Parameters:

Name Type Description Default
name Literal | str

Palette name. Built-in options: 'standard', 'distinct', 'rainbow', 'warm', 'cool', 'pastel', 'seaborn', 'tableau10', 'viridis'. With colorcet installed: 'glasbey', 'glasbey_hv', 'glasbey_cool', 'glasbey_warm'.

required
n_colors int

Number of colors needed.

required

Returns:

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

List of (R, G, B) tuples.

Raises:

Type Description
ValueError

If palette name is not recognized.

Source code in sleap_io/rendering/colors.py
def get_palette(name: PaletteName | str, n_colors: int) -> list[tuple[int, int, int]]:
    """Get n colors from a named palette as RGB tuples.

    Args:
        name: Palette name. Built-in options: 'standard', 'distinct', 'rainbow',
            'warm', 'cool', 'pastel', 'seaborn', 'tableau10', 'viridis'.
            With colorcet installed: 'glasbey', 'glasbey_hv', 'glasbey_cool',
            'glasbey_warm'.
        n_colors: Number of colors needed.

    Returns:
        List of (R, G, B) tuples.

    Raises:
        ValueError: If palette name is not recognized.
    """
    # Try built-in palettes first
    if name in PALETTES:
        palette = PALETTES[name]
        return _extend_palette(palette, n_colors)

    # Try colorcet palettes
    import colorcet as cc

    if name in cc.palette:
        hex_colors = cc.palette[name]
        rgb_colors = [_hex_to_rgb(c) for c in hex_colors]
        return _extend_palette(rgb_colors, n_colors)

    # Unknown palette - raise error with available options
    raise ValueError(
        f"Unknown palette: {name}. "
        f"Available: {list(PALETTES.keys())} (built-in), "
        "or any colorcet palette (e.g., glasbey, glasbey_hv, fire, rainbow4)"
    )

render_image(source, save_path=None, *, lf_ind=None, video=None, frame_idx=None, image=None, crop=None, color_by='auto', palette='standard', marker_shape='circle', marker_size=4.0, line_width=2.0, alpha=1.0, show_nodes=True, show_edges=True, scale=1.0, background='video', pre_render_callback=None, post_render_callback=None, per_instance_callback=None)

Render single frame with pose overlays.

Parameters:

Name Type Description Default
source Labels | LabeledFrame | list[Instance | PredictedInstance]

LabeledFrame, Labels (with frame specifier), or list of instances.

required
save_path str | Path | None

Output image path (PNG/JPEG). If None, only returns array.

None
lf_ind int | None

LabeledFrame index within Labels.labeled_frames (when source is Labels).

None
video Video | int | None

Video object or video index (used with frame_idx when source is Labels).

None
frame_idx int | None

Video frame index (0-based, used with video when source is Labels).

None
image ndarray | None

Override image array (H, W) or (H, W, C) uint8. Fetched from LabeledFrame if not provided.

None
crop CropSpec

Crop specification. Bounds are (x1, y1, x2, y2) where (x1, y1) is the top-left corner and (x2, y2) is the bottom-right (exclusive). Origin (0, 0) is at the image top-left. Can be:

  • Pixel coordinates (int tuple): (100, 100, 300, 300) crops from pixel (100, 100) to (300, 300).
  • Normalized coordinates (float tuple in [0.0, 1.0]): (0.25, 0.25, 0.75, 0.75) crops the center 50% of the frame. Detection is type-based: all values must be float and in range.
  • None: No cropping (default).
None
color_by Literal

Color scheme - 'track', 'instance', 'node', or 'auto'.

'auto'
palette Literal | str

Color palette name.

'standard'
marker_shape Literal

Node marker shape.

'circle'
marker_size float

Node marker radius in pixels.

4.0
line_width float

Edge line width in pixels.

2.0
alpha float

Global transparency (0.0-1.0).

1.0
show_nodes bool

Whether to draw node markers.

True
show_edges bool

Whether to draw skeleton edges.

True
scale float

Output scale factor. Applied after cropping.

1.0
background Literal['video'] | ColorSpec

Background control. Can be: - "video": Load video frame (default). Raises error if unavailable. - Any color spec: Use solid color background, skip video loading entirely. Supports RGB tuples (255, 128, 0), float tuples (1.0, 0.5, 0.0), grayscale 128 or 0.5, named colors "black", hex "#ff8000", or palette index "tableau10[2]".

'video'
pre_render_callback Callable[[RenderContext], None] | None

Called before poses are drawn.

None
post_render_callback Callable[[RenderContext], None] | None

Called after poses are drawn.

None
per_instance_callback Callable[[InstanceContext], None] | None

Called after each instance is drawn.

None

Returns:

Type Description
ndarray

Rendered numpy array (H, W, 3) uint8.

Raises:

Type Description
ValueError

If background="video" and video unavailable.

Examples:

Render a single labeled frame:

>>> import sleap_io as sio
>>> labels = sio.load_slp("predictions.slp")
>>> lf = labels.labeled_frames[0]
>>> img = sio.render_image(lf)

Render with solid color background (no video required):

>>> img = sio.render_image(lf, background="black")
>>> img = sio.render_image(lf, background=(40, 40, 40))
>>> img = sio.render_image(lf, background="#404040")
>>> img = sio.render_image(lf, background=0.25)

Crop to a region (pixel coordinates):

>>> img = sio.render_image(lf, crop=(100, 100, 300, 300))

Normalized crop (center 50% of frame):

>>> img = sio.render_image(lf, crop=(0.25, 0.25, 0.75, 0.75))

Render and save to file:

>>> sio.render_image(labels, lf_ind=0, save_path="frame.png")
>>> sio.render_image(labels, video=0, frame_idx=42, save_path="frame.png")
Source code in sleap_io/rendering/core.py
def render_image(
    source: "Labels | LabeledFrame | list[Instance | PredictedInstance]",
    save_path: str | Path | None = None,
    *,
    # Frame specification (for Labels input)
    lf_ind: int | None = None,
    video: "Video | int | None" = None,
    frame_idx: int | None = None,
    # Image override
    image: np.ndarray | None = None,
    # Cropping
    crop: CropSpec = None,
    # Appearance
    color_by: ColorScheme = "auto",
    palette: PaletteName | str = "standard",
    marker_shape: MarkerShape = "circle",
    marker_size: float = 4.0,
    line_width: float = 2.0,
    alpha: float = 1.0,
    show_nodes: bool = True,
    show_edges: bool = True,
    scale: float = 1.0,
    # Background control
    background: Literal["video"] | ColorSpec = "video",
    # Callbacks
    pre_render_callback: Callable[[RenderContext], None] | None = None,
    post_render_callback: Callable[[RenderContext], None] | None = None,
    per_instance_callback: Callable[[InstanceContext], None] | None = None,
) -> np.ndarray:
    """Render single frame with pose overlays.

    Args:
        source: LabeledFrame, Labels (with frame specifier), or list of instances.
        save_path: Output image path (PNG/JPEG). If None, only returns array.
        lf_ind: LabeledFrame index within Labels.labeled_frames (when source is Labels).
        video: Video object or video index (used with frame_idx when source is Labels).
        frame_idx: Video frame index (0-based, used with video when source is Labels).
        image: Override image array (H, W) or (H, W, C) uint8. Fetched from
            LabeledFrame if not provided.
        crop: Crop specification. Bounds are (x1, y1, x2, y2) where (x1, y1) is
            the top-left corner and (x2, y2) is the bottom-right (exclusive).
            Origin (0, 0) is at the image top-left. Can be:

            - **Pixel coordinates** (int tuple): ``(100, 100, 300, 300)`` crops
              from pixel (100, 100) to (300, 300).
            - **Normalized coordinates** (float tuple in [0.0, 1.0]):
              ``(0.25, 0.25, 0.75, 0.75)`` crops the center 50% of the frame.
              Detection is type-based: all values must be ``float`` and in range.
            - ``None``: No cropping (default).
        color_by: Color scheme - 'track', 'instance', 'node', or 'auto'.
        palette: Color palette name.
        marker_shape: Node marker shape.
        marker_size: Node marker radius in pixels.
        line_width: Edge line width in pixels.
        alpha: Global transparency (0.0-1.0).
        show_nodes: Whether to draw node markers.
        show_edges: Whether to draw skeleton edges.
        scale: Output scale factor. Applied after cropping.
        background: Background control. Can be:
            - ``"video"``: Load video frame (default). Raises error if unavailable.
            - Any color spec: Use solid color background, skip video loading entirely.
              Supports RGB tuples ``(255, 128, 0)``, float tuples ``(1.0, 0.5, 0.0)``,
              grayscale ``128`` or ``0.5``, named colors ``"black"``, hex ``"#ff8000"``,
              or palette index ``"tableau10[2]"``.
        pre_render_callback: Called before poses are drawn.
        post_render_callback: Called after poses are drawn.
        per_instance_callback: Called after each instance is drawn.

    Returns:
        Rendered numpy array (H, W, 3) uint8.

    Raises:
        ValueError: If background="video" and video unavailable.

    Examples:
        Render a single labeled frame:

        >>> import sleap_io as sio
        >>> labels = sio.load_slp("predictions.slp")
        >>> lf = labels.labeled_frames[0]
        >>> img = sio.render_image(lf)

        Render with solid color background (no video required):

        >>> img = sio.render_image(lf, background="black")
        >>> img = sio.render_image(lf, background=(40, 40, 40))
        >>> img = sio.render_image(lf, background="#404040")
        >>> img = sio.render_image(lf, background=0.25)

        Crop to a region (pixel coordinates):

        >>> img = sio.render_image(lf, crop=(100, 100, 300, 300))

        Normalized crop (center 50% of frame):

        >>> img = sio.render_image(lf, crop=(0.25, 0.25, 0.75, 0.75))

        Render and save to file:

        >>> sio.render_image(labels, lf_ind=0, save_path="frame.png")
        >>> sio.render_image(labels, video=0, frame_idx=42, save_path="frame.png")
    """
    import skia  # noqa: F401

    from sleap_io.model.instance import Instance, PredictedInstance
    from sleap_io.model.labeled_frame import LabeledFrame
    from sleap_io.model.labels import Labels

    # Handle background parameter
    use_video = background == "video"
    background_color: tuple[int, int, int] | None = None
    if not use_video:
        background_color = resolve_color(background)

    # Resolve source to LabeledFrame or instances
    if isinstance(source, Labels):
        if video is not None and frame_idx is not None:
            # Render by video + frame_idx
            target_video = source.videos[video] if isinstance(video, int) else video
            lf_list = source.find(target_video, frame_idx)
            if not lf_list:
                raise ValueError(
                    f"No labeled frame found for video {target_video} "
                    f"at frame {frame_idx}"
                )
            lf = lf_list[0]
        elif lf_ind is not None:
            # Render by labeled frame index
            lf = source.labeled_frames[lf_ind]
        else:
            # Default to first labeled frame
            lf = source.labeled_frames[0]

        instances = list(lf.instances)
        skeleton = instances[0].skeleton if instances else source.skeletons[0]
        edge_inds = skeleton.edge_inds
        node_names = [n.name for n in skeleton.nodes]
        fidx_for_callback = lf.frame_idx

        # Get track info
        track_indices = []
        n_tracks = len(source.tracks)
        for inst in instances:
            if inst.track is not None and inst.track in source.tracks:
                track_indices.append(source.tracks.index(inst.track))
            else:
                track_indices.append(0)

        has_tracks = n_tracks > 0

        # Convert instances to point arrays (needed for both image size and rendering)
        instances_points = [inst.numpy() for inst in instances]

        # Get image if not provided
        if image is None:
            if background_color is not None:
                # Solid color background - skip video loading entirely
                video_obj = lf.video
                if hasattr(video_obj, "shape") and video_obj.shape is not None:
                    h, w = video_obj.shape[1:3]
                else:
                    # Estimate from points
                    h, w = _estimate_frame_size(instances_points)
                image = _create_blank_frame(h, w, background_color)[:, :, :3]
            else:
                # Load video frame
                try:
                    image = lf.image
                    if image is None:
                        raise ValueError("No image available")
                except Exception:
                    raise ValueError(
                        "Video unavailable. Specify a background color to render "
                        "without video, e.g., background='black' or "
                        "background=(40, 40, 40)."
                    )

    elif isinstance(source, LabeledFrame):
        lf = source
        instances = list(lf.instances)
        skeleton = instances[0].skeleton if instances else None
        if skeleton is None:
            raise ValueError("LabeledFrame has no instances with skeleton")
        edge_inds = skeleton.edge_inds
        node_names = [n.name for n in skeleton.nodes]
        fidx_for_callback = lf.frame_idx
        track_indices = None
        n_tracks = 0
        has_tracks = False

        # Convert instances to point arrays (needed for both image size and rendering)
        instances_points = [inst.numpy() for inst in instances]

        # Get image if not provided
        if image is None:
            if background_color is not None:
                # Solid color background - skip video loading entirely
                video_obj = lf.video
                if hasattr(video_obj, "shape") and video_obj.shape is not None:
                    h, w = video_obj.shape[1:3]
                else:
                    # Estimate from points
                    h, w = _estimate_frame_size(instances_points)
                image = _create_blank_frame(h, w, background_color)[:, :, :3]
            else:
                # Load video frame
                try:
                    image = lf.image
                    if image is None:
                        raise ValueError("No image available")
                except Exception:
                    raise ValueError(
                        "Video unavailable. Specify a background color to render "
                        "without video, e.g., background='black' or "
                        "background=(40, 40, 40)."
                    )

    elif isinstance(source, list) and all(
        isinstance(x, (Instance, PredictedInstance)) for x in source
    ):
        instances = source
        if not instances:
            raise ValueError("Empty instances list")
        skeleton = instances[0].skeleton
        edge_inds = skeleton.edge_inds
        node_names = [n.name for n in skeleton.nodes]
        fidx_for_callback = 0
        track_indices = None
        n_tracks = 0
        has_tracks = False

        # Convert instances to point arrays
        instances_points = [inst.numpy() for inst in instances]

        if image is None:
            raise ValueError(
                "image parameter required when source is list of instances"
            )

    else:
        raise TypeError(
            f"source must be Labels, LabeledFrame, or list of instances, "
            f"got {type(source)}"
        )

    # Apply cropping if specified
    render_image_data = image
    render_points = instances_points
    crop_offset: tuple[float, float] = (0.0, 0.0)
    if crop is not None:
        h, w = image.shape[:2]
        # Resolve normalized or pixel coordinates
        crop_bounds = _resolve_crop(crop, (h, w))
        crop_offset = (float(crop_bounds[0]), float(crop_bounds[1]))

        render_image_data, render_points, _ = _apply_crop(
            image, instances_points, crop_bounds
        )

    # Build instance metadata for callbacks
    instance_metadata = []
    for inst in instances:
        meta = {}
        if hasattr(inst, "track") and inst.track is not None:
            meta["track_name"] = inst.track.name
        if hasattr(inst, "score"):
            meta["confidence"] = inst.score
        instance_metadata.append(meta)

    # Determine color scheme
    resolved_scheme = determine_color_scheme(
        has_tracks=has_tracks,
        is_single_image=True,
        scheme=color_by,
    )

    # Render
    rendered = render_frame(
        frame=render_image_data,
        instances_points=render_points,
        edge_inds=edge_inds,
        node_names=node_names,
        color_by=resolved_scheme,
        palette=palette,
        marker_shape=marker_shape,
        marker_size=marker_size,
        line_width=line_width,
        alpha=alpha,
        show_nodes=show_nodes,
        show_edges=show_edges,
        scale=scale,
        track_indices=track_indices,
        n_tracks=n_tracks,
        pre_render_callback=pre_render_callback,
        post_render_callback=post_render_callback,
        per_instance_callback=per_instance_callback,
        frame_idx=fidx_for_callback,
        instance_metadata=instance_metadata,
        crop_offset=crop_offset,
    )

    # Save if save_path provided
    if save_path is not None:
        from PIL import Image

        save_path_ = Path(save_path)
        save_path_.parent.mkdir(parents=True, exist_ok=True)
        Image.fromarray(rendered).save(save_path_)

    return rendered

render_video(source, save_path=None, *, video=None, frame_inds=None, start=None, end=None, include_unlabeled=False, crop=None, preset=None, scale=1.0, color_by='auto', palette='standard', marker_shape='circle', marker_size=4.0, line_width=2.0, alpha=1.0, show_nodes=True, show_edges=True, fps=None, codec='libx264', crf=25, x264_preset='superfast', background='video', pre_render_callback=None, post_render_callback=None, per_instance_callback=None, progress_callback=None, show_progress=True)

Render video with pose overlays.

Parameters:

Name Type Description Default
source Labels | list[LabeledFrame]

Labels object or list of LabeledFrames to render.

required
save_path str | Path | None

Output video path. If None, returns list of rendered arrays.

None
video Video | int | None

Video to render from (default: first video in Labels).

None
frame_inds list[int] | None

Specific frame indices to render.

None
start int | None

Start frame index (inclusive).

None
end int | None

End frame index (exclusive).

None
include_unlabeled bool

If True, render all frames in range even if they have no LabeledFrame (just shows video frame without poses). Default False.

False
crop CropSpec

Static crop applied uniformly to all frames. Bounds are (x1, y1, x2, y2) where (x1, y1) is the top-left corner and (x2, y2) is the bottom-right (exclusive). Supports:

  • Pixel coordinates (int tuple): (100, 100, 300, 300)
  • Normalized coordinates (float tuple in [0.0, 1.0]): (0.25, 0.25, 0.75, 0.75) crops the center 50%.
  • None: No cropping (default).
None
preset Literal['preview', 'draft', 'final'] | None

Quality preset ('preview'=0.25x, 'draft'=0.5x, 'final'=1.0x).

None
scale float

Scale factor (overrides preset if both provided).

1.0
color_by Literal

Color scheme - 'track', 'instance', 'node', or 'auto'.

'auto'
palette Literal | str

Color palette name.

'standard'
marker_shape Literal

Node marker shape.

'circle'
marker_size float

Node marker radius in pixels.

4.0
line_width float

Edge line width in pixels.

2.0
alpha float

Global transparency (0.0-1.0).

1.0
show_nodes bool

Whether to draw node markers.

True
show_edges bool

Whether to draw skeleton edges.

True
fps float | None

Output frame rate (default: source video fps).

None
codec str

Video codec for encoding.

'libx264'
crf int

Constant rate factor for quality (2-32, lower=better). Default 25.

25
x264_preset str

H.264 encoding preset (ultrafast, superfast, fast, medium, slow).

'superfast'
background Literal['video'] | ColorSpec

Background control. Can be: - "video": Load video frame (default). Raises error if unavailable. - Any color spec: Use solid color background, skip video loading entirely. Supports RGB tuples (255, 128, 0), float tuples (1.0, 0.5, 0.0), grayscale 128 or 0.5, named colors "black", hex "#ff8000", or palette index "tableau10[2]".

'video'
pre_render_callback Callable[[RenderContext], None] | None

Called before each frame's poses are drawn.

None
post_render_callback Callable[[RenderContext], None] | None

Called after each frame's poses are drawn.

None
per_instance_callback Callable[[InstanceContext], None] | None

Called after each instance is drawn.

None
progress_callback Callable[[int, int], bool] | None

Called with (current, total), return False to cancel.

None
show_progress bool

Show tqdm progress bar.

True

Returns:

Type Description
Video | list[ndarray]

If save_path provided: Video object pointing to output file. If save_path is None: List of rendered numpy arrays (H, W, 3) uint8.

Raises:

Type Description
ValueError

If background="video" and video unavailable.

Examples:

Render full video with pose overlays:

>>> import sleap_io as sio
>>> labels = sio.load_slp("predictions.slp")
>>> sio.render_video(labels, "output.mp4")

Fast preview at reduced resolution:

>>> sio.render_video(labels, "preview.mp4", preset="preview")

Get rendered frames as numpy arrays:

>>> frames = sio.render_video(labels)
Source code in sleap_io/rendering/core.py
def render_video(
    source: "Labels | list[LabeledFrame]",
    save_path: str | Path | None = None,
    *,
    # Video selection
    video: "Video | int | None" = None,
    # Frame selection
    frame_inds: list[int] | None = None,
    start: int | None = None,
    end: int | None = None,
    include_unlabeled: bool = False,
    # Cropping
    crop: CropSpec = None,
    # Quality/scale
    preset: Literal["preview", "draft", "final"] | None = None,
    scale: float = 1.0,
    # Appearance
    color_by: ColorScheme = "auto",
    palette: PaletteName | str = "standard",
    marker_shape: MarkerShape = "circle",
    marker_size: float = 4.0,
    line_width: float = 2.0,
    alpha: float = 1.0,
    show_nodes: bool = True,
    show_edges: bool = True,
    # Video encoding
    fps: float | None = None,
    codec: str = "libx264",
    crf: int = 25,
    x264_preset: str = "superfast",
    # Background control
    background: Literal["video"] | ColorSpec = "video",
    # Callbacks
    pre_render_callback: Callable[[RenderContext], None] | None = None,
    post_render_callback: Callable[[RenderContext], None] | None = None,
    per_instance_callback: Callable[[InstanceContext], None] | None = None,
    # Progress
    progress_callback: Callable[[int, int], bool] | None = None,
    show_progress: bool = True,
) -> "Video | list[np.ndarray]":
    """Render video with pose overlays.

    Args:
        source: Labels object or list of LabeledFrames to render.
        save_path: Output video path. If None, returns list of rendered arrays.
        video: Video to render from (default: first video in Labels).
        frame_inds: Specific frame indices to render.
        start: Start frame index (inclusive).
        end: End frame index (exclusive).
        include_unlabeled: If True, render all frames in range even if they have
            no LabeledFrame (just shows video frame without poses). Default False.
        crop: Static crop applied uniformly to all frames. Bounds are
            (x1, y1, x2, y2) where (x1, y1) is the top-left corner and (x2, y2)
            is the bottom-right (exclusive). Supports:

            - **Pixel coordinates** (int tuple): ``(100, 100, 300, 300)``
            - **Normalized coordinates** (float tuple in [0.0, 1.0]):
              ``(0.25, 0.25, 0.75, 0.75)`` crops the center 50%.
            - ``None``: No cropping (default).
        preset: Quality preset ('preview'=0.25x, 'draft'=0.5x, 'final'=1.0x).
        scale: Scale factor (overrides preset if both provided).
        color_by: Color scheme - 'track', 'instance', 'node', or 'auto'.
        palette: Color palette name.
        marker_shape: Node marker shape.
        marker_size: Node marker radius in pixels.
        line_width: Edge line width in pixels.
        alpha: Global transparency (0.0-1.0).
        show_nodes: Whether to draw node markers.
        show_edges: Whether to draw skeleton edges.
        fps: Output frame rate (default: source video fps).
        codec: Video codec for encoding.
        crf: Constant rate factor for quality (2-32, lower=better). Default 25.
        x264_preset: H.264 encoding preset (ultrafast, superfast, fast, medium, slow).
        background: Background control. Can be:
            - ``"video"``: Load video frame (default). Raises error if unavailable.
            - Any color spec: Use solid color background, skip video loading entirely.
              Supports RGB tuples ``(255, 128, 0)``, float tuples ``(1.0, 0.5, 0.0)``,
              grayscale ``128`` or ``0.5``, named colors ``"black"``, hex ``"#ff8000"``,
              or palette index ``"tableau10[2]"``.
        pre_render_callback: Called before each frame's poses are drawn.
        post_render_callback: Called after each frame's poses are drawn.
        per_instance_callback: Called after each instance is drawn.
        progress_callback: Called with (current, total), return False to cancel.
        show_progress: Show tqdm progress bar.

    Returns:
        If save_path provided: Video object pointing to output file.
        If save_path is None: List of rendered numpy arrays (H, W, 3) uint8.

    Raises:
        ValueError: If background="video" and video unavailable.

    Examples:
        Render full video with pose overlays:

        >>> import sleap_io as sio
        >>> labels = sio.load_slp("predictions.slp")
        >>> sio.render_video(labels, "output.mp4")

        Fast preview at reduced resolution:

        >>> sio.render_video(labels, "preview.mp4", preset="preview")

        Get rendered frames as numpy arrays:

        >>> frames = sio.render_video(labels)
    """
    import skia  # noqa: F401

    from sleap_io.model.labeled_frame import LabeledFrame
    from sleap_io.model.labels import Labels
    from sleap_io.model.video import Video as VideoModel

    # Handle background parameter
    use_video = background == "video"
    background_color: tuple[int, int, int] | None = None
    if not use_video:
        background_color = resolve_color(background)

    # Handle preset
    if preset is not None and preset in PRESETS:
        scale = PRESETS[preset]["scale"]

    # Resolve source
    if isinstance(source, Labels):
        labels = source

        # Resolve video
        if video is None:
            if not labels.videos:
                raise ValueError("Labels has no videos")
            target_video = labels.videos[0]
        elif isinstance(video, int):
            target_video = labels.videos[video]
        else:
            target_video = video

        # Get labeled frames for this video
        labeled_frames = labels.find(target_video)
        if not labeled_frames:
            raise ValueError(f"No labeled frames found for video {target_video}")

        # Sort by frame index
        labeled_frames = sorted(labeled_frames, key=lambda lf: lf.frame_idx)

        # Get skeleton info
        skeleton = labels.skeletons[0] if labels.skeletons else None
        if skeleton is None and labeled_frames:
            for lf in labeled_frames:
                for inst in lf.instances:
                    skeleton = inst.skeleton
                    break
                if skeleton:
                    break

        if skeleton is None:
            raise ValueError("No skeleton found in labels")

        edge_inds = skeleton.edge_inds
        node_names = [n.name for n in skeleton.nodes]
        n_tracks = len(labels.tracks)
        has_tracks = n_tracks > 0

    elif isinstance(source, list) and all(isinstance(x, LabeledFrame) for x in source):
        labeled_frames = source
        if not labeled_frames:
            raise ValueError("Empty labeled frames list")

        target_video = labeled_frames[0].video
        skeleton = None
        for lf in labeled_frames:
            for inst in lf.instances:
                skeleton = inst.skeleton
                break
            if skeleton:
                break

        if skeleton is None:
            raise ValueError("No skeleton found in labeled frames")

        edge_inds = skeleton.edge_inds
        node_names = [n.name for n in skeleton.nodes]
        n_tracks = 0
        has_tracks = False
        labels = None

    else:
        raise TypeError(
            f"source must be Labels or list of LabeledFrame, got {type(source)}"
        )

    # Create frame index mapping
    frame_idx_to_lf = {lf.frame_idx: lf for lf in labeled_frames}

    # Get video frame count for include_unlabeled mode
    n_video_frames = None
    if include_unlabeled:
        if hasattr(target_video, "shape") and target_video.shape is not None:
            n_video_frames = target_video.shape[0]

    # Determine frame indices to render
    if frame_inds is not None:
        render_indices = frame_inds
    elif start is not None or end is not None:
        labeled_indices = sorted(frame_idx_to_lf.keys())
        if include_unlabeled and n_video_frames is not None:
            # Render all frames in range, not just labeled ones
            start_idx = start if start is not None else 0
            end_idx = end if end is not None else n_video_frames
            render_indices = list(range(start_idx, end_idx))
        else:
            # Only render labeled frames in range
            start_idx = start if start is not None else min(labeled_indices, default=0)
            end_idx = end if end is not None else max(labeled_indices, default=0) + 1
            render_indices = [i for i in labeled_indices if start_idx <= i < end_idx]
    else:
        if include_unlabeled and n_video_frames is not None:
            # Render entire video
            render_indices = list(range(n_video_frames))
        else:
            # Only render labeled frames
            render_indices = sorted(frame_idx_to_lf.keys())

    if not render_indices:
        raise ValueError("No frames to render")

    # Determine FPS
    if fps is None:
        # Try to get from video
        if hasattr(target_video, "backend") and target_video.backend is not None:
            try:
                fps = target_video.backend.fps
            except Exception:
                fps = 30.0
        else:
            fps = 30.0

    # Determine color scheme
    resolved_scheme = determine_color_scheme(
        has_tracks=has_tracks,
        is_single_image=False,
        scheme=color_by,
    )

    # Resolve crop bounds once (before the loop)
    # We need the video shape to resolve normalized coordinates
    crop_bounds: tuple[int, int, int, int] | None = None
    crop_offset: tuple[float, float] = (0.0, 0.0)
    if crop is not None:
        if hasattr(target_video, "shape") and target_video.shape is not None:
            h, w = target_video.shape[1:3]
        else:
            # Fallback: try to get from first frame
            h, w = 480, 640  # reasonable default
        crop_bounds = _resolve_crop(crop, (h, w))
        crop_offset = (float(crop_bounds[0]), float(crop_bounds[1]))

    # Setup progress
    if show_progress:
        try:
            from tqdm import tqdm

            iterator = tqdm(render_indices, desc="Rendering", unit="frame")
        except ImportError:
            iterator = render_indices
    else:
        iterator = render_indices

    # Render frames
    rendered_frames = []
    total_frames = len(render_indices)

    for i, fidx in enumerate(iterator):
        # Check for cancellation
        if progress_callback is not None:
            if progress_callback(i, total_frames) is False:
                break

        lf = frame_idx_to_lf.get(fidx)

        # Handle frames without LabeledFrame
        if lf is None:
            if not include_unlabeled:
                continue
            # Render just the video frame without poses
            if background_color is not None:
                # Solid color background - skip video loading entirely
                if hasattr(target_video, "shape") and target_video.shape is not None:
                    h, w = target_video.shape[1:3]
                else:
                    # No video metadata and no points - use minimum default
                    h, w = 64, 64
                image = _create_blank_frame(h, w, background_color)[:, :, :3]
            else:
                try:
                    image = target_video[fidx]
                    if image is None:
                        raise ValueError("No image")
                except Exception:
                    raise ValueError(
                        f"Video unavailable at frame {fidx}. "
                        "Specify a background color to render without video."
                    )

            # Apply cropping if specified
            render_image_data = image
            if crop_bounds is not None:
                render_image_data, _, _ = _apply_crop(image, [], crop_bounds)

            # Render frame without poses
            rendered = render_frame(
                frame=render_image_data,
                instances_points=[],
                edge_inds=edge_inds,
                node_names=node_names,
                color_by=resolved_scheme,
                palette=palette,
                marker_shape=marker_shape,
                marker_size=marker_size,
                line_width=line_width,
                alpha=alpha,
                show_nodes=show_nodes,
                show_edges=show_edges,
                scale=scale,
                track_indices=None,
                n_tracks=n_tracks,
                pre_render_callback=pre_render_callback,
                post_render_callback=post_render_callback,
                per_instance_callback=None,
                frame_idx=fidx,
                instance_metadata=[],
                crop_offset=crop_offset,
            )
            rendered_frames.append(rendered)
            continue

        instances = list(lf.instances)
        instances_points = [inst.numpy() for inst in instances]

        # Get track indices
        track_indices = None
        if labels is not None and has_tracks:
            track_indices = []
            for inst in instances:
                if inst.track is not None and inst.track in labels.tracks:
                    track_indices.append(labels.tracks.index(inst.track))
                else:
                    track_indices.append(0)

        # Build instance metadata
        instance_metadata = []
        for inst in instances:
            meta = {}
            if hasattr(inst, "track") and inst.track is not None:
                meta["track_name"] = inst.track.name
            if hasattr(inst, "score"):
                meta["confidence"] = inst.score
            instance_metadata.append(meta)

        # Get image
        if background_color is not None:
            # Solid color background - skip video loading entirely
            if hasattr(target_video, "shape") and target_video.shape is not None:
                h, w = target_video.shape[1:3]
            else:
                # Estimate from points
                h, w = _estimate_frame_size(instances_points)
            image = _create_blank_frame(h, w, background_color)[:, :, :3]
        else:
            try:
                image = lf.image
                if image is None:
                    raise ValueError("No image")
            except Exception:
                raise ValueError(
                    f"Video unavailable at frame {fidx}. "
                    "Specify a background color to render without video."
                )

        # Apply cropping if specified
        render_image_data = image
        render_points = instances_points
        if crop_bounds is not None:
            render_image_data, render_points, _ = _apply_crop(
                image, instances_points, crop_bounds
            )

        # Render frame
        rendered = render_frame(
            frame=render_image_data,
            instances_points=render_points,
            edge_inds=edge_inds,
            node_names=node_names,
            color_by=resolved_scheme,
            palette=palette,
            marker_shape=marker_shape,
            marker_size=marker_size,
            line_width=line_width,
            alpha=alpha,
            show_nodes=show_nodes,
            show_edges=show_edges,
            scale=scale,
            track_indices=track_indices,
            n_tracks=n_tracks,
            pre_render_callback=pre_render_callback,
            post_render_callback=post_render_callback,
            per_instance_callback=per_instance_callback,
            frame_idx=fidx,
            instance_metadata=instance_metadata,
            crop_offset=crop_offset,
        )

        rendered_frames.append(rendered)

    # Write video or return frames
    if save_path is not None:
        from sleap_io.io.video_writing import VideoWriter

        save_path_ = Path(save_path)
        save_path_.parent.mkdir(parents=True, exist_ok=True)

        with VideoWriter(
            filename=save_path_,
            fps=fps,
            codec=codec,
            crf=crf,
            preset=x264_preset,
        ) as writer:
            for frame in rendered_frames:
                writer(frame)

        # Return Video object pointing to output
        return VideoModel.from_filename(str(save_path_))

    return rendered_frames

resolve_color(color)

Resolve a flexible color specification to an RGB tuple.

Parameters:

Name Type Description Default
color ColorSpec

Color specification in various formats: - RGB int tuple: (255, 128, 0) - RGB float tuple: (1.0, 0.5, 0.0) - values in 0.0-1.0 range - Grayscale int: 128 → (128, 128, 128) - Grayscale float: 0.5 → (127, 127, 127) - Named color: "black", "white", "red", etc. - Hex color: "#ff8000" or "#f80" - Palette index: "tableau10[2]", "glasbey[5]"

required

Returns:

Type Description
tuple[int, int, int]

RGB tuple of integers in 0-255 range.

Raises:

Type Description
ValueError

If color specification is invalid.

Examples:

>>> resolve_color((255, 128, 0))
(255, 128, 0)
>>> resolve_color((1.0, 0.5, 0.0))
(255, 127, 0)
>>> resolve_color("red")
(255, 0, 0)
>>> resolve_color("#ff8000")
(255, 128, 0)
>>> resolve_color("#f80")
(255, 136, 0)
>>> resolve_color("tableau10[2]")
(44, 160, 44)
>>> resolve_color(128)
(128, 128, 128)
>>> resolve_color(0.5)
(127, 127, 127)
Source code in sleap_io/rendering/colors.py
def resolve_color(color: ColorSpec) -> tuple[int, int, int]:
    """Resolve a flexible color specification to an RGB tuple.

    Args:
        color: Color specification in various formats:
            - RGB int tuple: ``(255, 128, 0)``
            - RGB float tuple: ``(1.0, 0.5, 0.0)`` - values in 0.0-1.0 range
            - Grayscale int: ``128`` → ``(128, 128, 128)``
            - Grayscale float: ``0.5`` → ``(127, 127, 127)``
            - Named color: ``"black"``, ``"white"``, ``"red"``, etc.
            - Hex color: ``"#ff8000"`` or ``"#f80"``
            - Palette index: ``"tableau10[2]"``, ``"glasbey[5]"``

    Returns:
        RGB tuple of integers in 0-255 range.

    Raises:
        ValueError: If color specification is invalid.

    Examples:
        >>> resolve_color((255, 128, 0))
        (255, 128, 0)

        >>> resolve_color((1.0, 0.5, 0.0))
        (255, 127, 0)

        >>> resolve_color("red")
        (255, 0, 0)

        >>> resolve_color("#ff8000")
        (255, 128, 0)

        >>> resolve_color("#f80")
        (255, 136, 0)

        >>> resolve_color("tableau10[2]")
        (44, 160, 44)

        >>> resolve_color(128)
        (128, 128, 128)

        >>> resolve_color(0.5)
        (127, 127, 127)
    """
    # Grayscale int
    if isinstance(color, int):
        value = max(0, min(255, color))
        return (value, value, value)

    # Grayscale float (0.0-1.0 range)
    if isinstance(color, float):
        value = int(max(0.0, min(1.0, color)) * 255)
        return (value, value, value)

    # Tuple (RGB)
    if isinstance(color, tuple):
        if len(color) != 3:
            raise ValueError(f"RGB tuple must have 3 elements, got {len(color)}")

        r, g, b = color

        # Detect float vs int by Python type
        if isinstance(r, float) or isinstance(g, float) or isinstance(b, float):
            # Float tuple: 0.0-1.0 range
            r_int = int(max(0.0, min(1.0, float(r))) * 255)
            g_int = int(max(0.0, min(1.0, float(g))) * 255)
            b_int = int(max(0.0, min(1.0, float(b))) * 255)
            return (r_int, g_int, b_int)
        else:
            # Int tuple: 0-255 range
            r_int = max(0, min(255, int(r)))
            g_int = max(0, min(255, int(g)))
            b_int = max(0, min(255, int(b)))
            return (r_int, g_int, b_int)

    # String
    if isinstance(color, str):
        color_lower = color.lower().strip()

        # Named color
        if color_lower in NAMED_COLORS:
            return NAMED_COLORS[color_lower]

        # Hex color
        if color.startswith("#"):
            hex_part = color[1:]
            if len(hex_part) == 3 or len(hex_part) == 6:
                return _hex_to_rgb(color)
            raise ValueError(f"Invalid hex color: {color}")

        # Palette index: "palette_name[index]"
        palette_match = re.match(r"^(\w+)\[(\d+)\]$", color)
        if palette_match:
            palette_name = palette_match.group(1)
            index = int(palette_match.group(2))

            # Get palette colors
            try:
                palette_colors = get_palette(palette_name, index + 1)
                if index < len(palette_colors):
                    return palette_colors[index]
            except ValueError:
                pass

            raise ValueError(f"Invalid palette index: {color}")

        raise ValueError(
            f"Unknown color: {color!r}. Valid named colors: {list(NAMED_COLORS.keys())}"
        )

    raise TypeError(f"Invalid color type: {type(color).__name__}")