Codecs¶
Convert pose tracking data to dictionaries, NumPy arrays, and DataFrames for analysis and interoperability.
What are codecs?¶
Codecs perform in-memory format conversion between Labels objects and common data representations:
| Codec | Output | Use Case |
|---|---|---|
| Dictionary | dict |
JSON export, serialization, inspection |
| NumPy | np.ndarray |
Numerical analysis, ML pipelines |
| DataFrame | pd.DataFrame / pl.DataFrame |
Tabular analysis, visualization |
Codecs are distinct from I/O operations: they convert data structures in memory rather than reading/writing files. This separation enables flexible workflows:
# Codec + I/O composition
df = labels.to_dataframe() # Codec: Labels → DataFrame
df.to_csv("poses.csv") # I/O: DataFrame → disk
Quick start¶
import sleap_io as sio
# Load pose tracking data
labels = sio.load_file("predictions.slp")
# Convert to dictionary (JSON-serializable)
data = labels.to_dict()
# Convert to NumPy array
# Shape: (n_frames, n_tracks, n_nodes, 2); n_frames == len(video) in v0.7.0
tracks = labels.numpy()
# Convert to pandas DataFrame
df = labels.to_dataframe()
Each codec also supports decoding (reconstruction from the converted format):
from sleap_io.codecs import from_dict, from_dataframe, from_numpy
# Reconstruct Labels from dictionary
labels = from_dict(data)
# Reconstruct from DataFrame (requires video/skeleton context)
labels = from_dataframe(df, video=video, skeleton=skeleton)
# Reconstruct from NumPy array
labels = from_numpy(tracks, video=video, skeleton=skeleton)
Dictionary codec¶
The dictionary codec converts Labels to a JSON-serializable Python dictionary with complete structural metadata.
Encoding with to_dict()¶
Output structure:
{
"version": "1.0.0",
"skeletons": [
{
"name": "fly",
"nodes": ["head", "thorax", "abdomen"],
"edges": [[0, 1], [1, 2]]
}
],
"videos": [
{"filename": "video.mp4", "shape": [1000, 1024, 1024, 1]}
],
"tracks": [
{"name": "fly_1"}
],
"labeled_frames": [
{
"frame_idx": 0,
"video_idx": 0,
"instances": [
{
"type": "predicted_instance",
"skeleton_idx": 0,
"points": [
{"x": 100.0, "y": 200.0, "visible": true, "complete": false},
{"x": 150.0, "y": 250.0, "visible": true, "complete": false},
{"x": 200.0, "y": 300.0, "visible": true, "complete": false}
],
"track_idx": 0,
"score": 0.91
}
]
},
{
"frame_idx": 12,
"video_idx": 0,
"instances": [],
"is_negative": true
}
],
"suggestions": [],
"provenance": {}
}
The dictionary uses index-based references (skeleton_idx, video_idx, track_idx) for compactness. All values are JSON-serializable primitives.
Frames explicitly marked as containing no instances (negative training examples) are preserved in v0.7.0 via an is_negative: true field on each frame dict — round-tripping through to_dict() / from_dict() no longer drops them (PR #369).
Parameters:
video: Filter to a specific video (by object or index)skip_empty_frames: Exclude frames with no instances
# Export only frames with instances from the first video
data = labels.to_dict(video=0, skip_empty_frames=True)
Decoding with from_dict()¶
The decoder reconstructs the full object graph including skeletons, videos, tracks, and instances.
Limitations
- Video backends are not restored (videos are created with filename only)
- The
from_predictedrelationship is indicated but not fully linked
JSON export workflow¶
import json
# Export to JSON file
data = labels.to_dict()
with open("labels.json", "w") as f:
json.dump(data, f, indent=2)
# Import from JSON file
with open("labels.json", "r") as f:
data = json.load(f)
labels = from_dict(data)
See also: to_dict(), from_dict()
NumPy codec¶
The NumPy codec converts Labels to a 4D array of track coordinates, suitable for numerical analysis and ML pipelines.
Encoding with to_numpy() / Labels.numpy()¶
import sleap_io as sio
labels = sio.load_file("predictions.slp")
tracks = labels.numpy()
print(tracks.shape)
# (n_frames, n_tracks, n_nodes, 2)
In v0.7.0, n_frames == len(video) — the full video duration. Frames past the last labeled frame are NaN-padded along all coordinate axes (PR #368). Code that previously assumed tracks.shape[0] == last_labeled_frame + 1 may need updating.
Output array:
Shape: (2, 2, 2, 2)
(n_frames=2, n_tracks=2, n_nodes=2, coords=2)
Frame 0, Track 0:
[[100. 200.] # head: x=100, y=200
[150. 250.]] # tail: x=150, y=250
Frame 0, Track 1:
[[300. 400.] # head: x=300, y=400
[350. 450.]] # tail: x=350, y=450
Missing points are represented as np.nan.
Include confidence scores:
tracks = labels.numpy(return_confidence=True)
print(tracks.shape)
# (n_frames, n_tracks, n_nodes, 3) # x, y, score
# Frame 0, Track 0 with scores:
# [[100. 200. 0.95] # head: x, y, score
# [150. 250. 0.92]] # tail: x, y, score
Parameters:
| Parameter | Default | Description |
|---|---|---|
video |
None (first) |
Video to convert |
untracked |
False |
Include untracked instances |
return_confidence |
False |
Include point scores (3rd coord) |
user_instances |
True |
Include user-labeled instances |
predicted_instances |
True |
Include predicted instances |
# Get only predicted instances with confidence scores
tracks = labels.numpy(
user_instances=False,
predicted_instances=True,
return_confidence=True
)
Decoding with from_numpy() / Labels.from_numpy()¶
from sleap_io.codecs import from_numpy
import numpy as np
# Create array: 100 frames, 2 tracks, 3 nodes, xy coordinates
tracks = np.random.rand(100, 2, 3, 2) * 500
# Reconstruct Labels (requires skeleton and video)
labels = from_numpy(
tracks,
video=video,
skeleton=skeleton,
track_names=["animal_1", "animal_2"]
)
Parameters:
| Parameter | Description |
|---|---|
tracks_array |
4D array of shape (frames, tracks, nodes, 2 or 3) |
video |
Video object for the labels |
skeleton |
Skeleton defining node structure |
tracks |
List of Track objects (or auto-created) |
track_names |
Names for auto-created tracks |
first_frame |
Starting frame index (default: 0) |
return_confidence |
Whether array contains scores |
See also: to_numpy(), from_numpy()
DataFrame codec¶
The DataFrame codec converts Labels to tabular format with multiple layout options, supporting both pandas and polars backends.
Encoding with to_dataframe() / Labels.to_dataframe()¶
Four output formats are available:
| Format | Structure | Use Case |
|---|---|---|
points |
One row per point | Normalized, round-trip compatible |
instances |
One row per instance | Feature extraction, ML |
frames |
One row per frame | Trajectory analysis |
multi_index |
Hierarchical columns | NWB-style, pivot tables |
Points format (default)¶
One row per (frame, instance, node) combination—the most normalized representation.
frame_idx node x y track track_score instance_score score
0 0 head 100.0 200.0 None NaN NaN NaN
1 0 thorax 150.0 250.0 None NaN NaN NaN
2 0 abdomen 200.0 300.0 None NaN NaN NaN
3 0 head 105.0 205.0 fly_1 0.98 0.91 0.95
4 0 thorax 155.0 255.0 fly_1 0.98 0.91 0.92
5 0 abdomen 205.0 305.0 fly_1 0.98 0.91 0.88
Columns:
frame_idx: Frame numbernode: Body part namex,y: Point coordinatestrack: Track name (orNonefor untracked)track_score: Tracking confidenceinstance_score: Instance-level prediction scorescore: Per-point confidence score
This format supports full round-trip decoding with from_dataframe().
Instances format¶
One row per instance—coordinates spread across columns.
frame_idx track track_score score head.x head.y head.score tail.x tail.y tail.score
0 0 fly_1 0.98 0.91 100.0 200.0 0.95 150.0 250.0 0.92
1 0 fly_2 0.95 0.86 300.0 400.0 0.88 350.0 450.0 0.85
Each node has columns {node}.x, {node}.y, and {node}.score. This format is useful for computing per-instance features.
Frames format¶
One row per frame—all instances multiplexed across columns.
With instance_id="index" (default):
frame_idx inst0.track inst0.score inst0.head.x inst0.head.y ... inst1.track inst1.head.x ...
0 0 fly_1 0.91 100.0 200.0 ... fly_2 300.0 ...
With instance_id="track":
frame_idx fly_1.score fly_1.head.x fly_1.head.y ... fly_2.score fly_2.head.x ...
0 0 0.91 100.0 200.0 ... 0.86 300.0 ...
The instance_id parameter controls column naming:
"index": Use positional prefixes (inst0,inst1, ...)"track": Use track names as prefixes
Track mode requirements
Using instance_id="track" requires all instances to have tracks assigned. Use untracked="ignore" to skip untracked instances, or untracked="error" (default) to raise an error.
Multi-index format¶
Hierarchical column structure with frame as row index.
inst0 inst1
track track_score score head track track_score score head
x y x y
frame_idx
0 fly_1 0.98 0.91 100.0 200.0 fly_2 0.95 0.86 300.0 400.0
This format is compatible with NWB pose estimation conventions and works well with pandas pivot operations.
Decoding with from_dataframe()¶
Reconstruct Labels from a DataFrame:
from sleap_io.codecs import from_dataframe
# Decode points format
labels = from_dataframe(df, video=video, skeleton=skeleton, format="points")
All four formats support decoding:
# Decode instances format
labels = from_dataframe(df_instances, video=video, skeleton=skeleton, format="instances")
# Decode frames format
labels = from_dataframe(df_frames, video=video, skeleton=skeleton, format="frames")
# Decode multi_index format
labels = from_dataframe(df_multi, video=video, skeleton=skeleton, format="multi_index")
Skeleton inference
If not provided, the skeleton is inferred from column names (node names extracted from {node}.x pattern).
Common parameters¶
All to_dataframe() calls support these options:
| Parameter | Default | Description |
|---|---|---|
video |
None |
Filter to specific video |
include_metadata |
True |
Include track/score columns |
include_score |
True |
Include confidence scores |
include_user_instances |
True |
Include user-labeled instances |
include_predicted_instances |
True |
Include predictions |
video_id |
"path" |
How to represent videos |
include_video |
None |
Force video column on/off |
backend |
"pandas" |
Output type ("pandas" or "polars") |
Video representation options (video_id):
| Value | Column | Example |
|---|---|---|
"path" |
video_path |
"/data/video.mp4" |
"index" |
video_idx |
0 |
"name" |
video_path |
"video.mp4" |
"object" |
video |
<Video> |
See also: to_dataframe(), from_dataframe()
Working with large datasets¶
For datasets with millions of frames, use streaming conversion to avoid memory issues.
Chunked iteration with to_dataframe_iter()¶
from sleap_io.codecs import to_dataframe_iter
# Iterate in chunks of 10,000 rows
for chunk in to_dataframe_iter(labels, format="points", chunk_size=10000):
# Process each chunk
process(chunk)
Output:
The chunk_size parameter specifies rows per chunk:
pointsformat: One point per rowinstancesformat: One instance per rowframes/multi_indexformat: One frame per row
Streaming to disk¶
# Stream to Parquet files
for i, chunk in enumerate(to_dataframe_iter(labels, chunk_size=100000)):
chunk.to_parquet(f"poses_part{i:04d}.parquet")
Memory-efficient CSV export¶
first_chunk = True
for chunk in to_dataframe_iter(labels, chunk_size=50000):
chunk.to_csv(
"poses.csv",
mode="w" if first_chunk else "a",
header=first_chunk,
index=False
)
first_chunk = False
Labels wrapper method¶
The iterator is also available on Labels:
See also: to_dataframe_iter()
Choosing a backend¶
The DataFrame codec supports both pandas and polars backends.
Pandas (default)¶
Pandas is the default and works out of the box.
Polars¶
shape: (6, 8)
┌───────────┬─────────┬───────┬───────┬───────┬─────────────┬────────────────┬───────┐
│ frame_idx ┆ node ┆ x ┆ y ┆ track ┆ track_score ┆ instance_score ┆ score │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ str ┆ f64 ┆ f64 ┆ str ┆ f64 ┆ f64 ┆ f64 │
╞═══════════╪═════════╪═══════╪═══════╪═══════╪═════════════╪════════════════╪═══════╡
│ 0 ┆ head ┆ 100.0 ┆ 200.0 ┆ fly_1 ┆ 0.98 ┆ 0.91 ┆ 0.95 │
│ 0 ┆ thorax ┆ 150.0 ┆ 250.0 ┆ fly_1 ┆ 0.98 ┆ 0.91 ┆ 0.92 │
│ ... ┆ ... ┆ ... ┆ ... ┆ ... ┆ ... ┆ ... ┆ ... │
└───────────┴─────────┴───────┴───────┴───────┴─────────────┴────────────────┴───────┘
When to use polars¶
| Scenario | Recommendation |
|---|---|
| Interactive analysis | pandas (broader ecosystem) |
| Large datasets | polars (faster, lower memory) |
| Streaming to Parquet | polars (native Arrow support) |
| Integration with ML libraries | pandas (wider compatibility) |
Performance comparison (10,000 frames, 3 tracks, 10 nodes):
| Format | Native Polars | Pandas→Polars | Speedup |
|---|---|---|---|
| points | 504ms | 544ms | 1.08x |
| instances | 387ms | 438ms | 1.13x |
| frames | 436ms | 477ms | 1.09x |
| multi_index | 426ms | 550ms | 1.29x |
The native polars backend constructs DataFrames directly without pandas conversion overhead.
Common patterns¶
Export to CSV¶
# Simple export
df = labels.to_dataframe(format="points")
df.to_csv("poses.csv", index=False)
# With specific columns
df = labels.to_dataframe(format="instances", include_score=False)
df.to_csv("poses_no_scores.csv", index=False)
Export to Parquet¶
# Pandas
df = labels.to_dataframe()
df.to_parquet("poses.parquet")
# Polars (more efficient for large files)
df = labels.to_dataframe(backend="polars")
df.write_parquet("poses.parquet")
Filter before conversion¶
# Only predicted instances from first video
df = labels.to_dataframe(
video=0,
include_user_instances=False,
include_predicted_instances=True
)
Analyze trajectories¶
df = labels.to_dataframe(format="points")
# Mean position per track
df.groupby("track")[["x", "y"]].mean()
# Velocity calculation
df_sorted = df.sort_values(["track", "node", "frame_idx"])
df_sorted["vx"] = df_sorted.groupby(["track", "node"])["x"].diff()
df_sorted["vy"] = df_sorted.groupby(["track", "node"])["y"].diff()
Round-trip editing¶
# Convert to DataFrame
df = labels.to_dataframe(format="points")
# Edit (e.g., apply calibration)
df["x"] = df["x"] * scale_x + offset_x
df["y"] = df["y"] * scale_y + offset_y
# Convert back
labels_calibrated = from_dataframe(
df,
video=labels.videos[0],
skeleton=labels.skeletons[0],
format="points"
)
Troubleshooting¶
Polars not installed¶
Solution: Install polars with pip install sleap-io[polars] or pip install polars.
Untracked instances in track mode¶
Solution: Either assign tracks to all instances, or use untracked="ignore":
Missing video/skeleton in decoding¶
Solution: Provide the required context:
Array dimension mismatch in from_numpy()¶
Solution: Ensure your array has shape (n_frames, n_tracks, n_nodes, 2) or (n_frames, n_tracks, n_nodes, 3) if including confidence scores.
API reference¶
DataFrameFormat¶
sleap_io.codecs.DataFrameFormat
¶
Bases: builtins.str, enum.Enum
Enumeration of supported DataFrame formats.
Attributes:
| Name | Type | Description |
|---|---|---|
FRAMES |
Enumeration of supported DataFrame formats. |
|
INSTANCES |
Enumeration of supported DataFrame formats. |
|
MULTI_INDEX |
Enumeration of supported DataFrame formats. |
|
POINTS |
Enumeration of supported DataFrame formats. |
|
__doc__ |
str(object='') -> str |
|
__module__ |
str(object='') -> str |
Source code in sleap_io/codecs/dataframe.py
class DataFrameFormat(str, Enum):
"""Enumeration of supported DataFrame formats."""
POINTS = "points"
"""One row per point (frame, instance, node). Most normalized format."""
INSTANCES = "instances"
"""One row per instance. Columns for each node's x/y coordinates."""
FRAMES = "frames"
"""One row per frame-track combination. For trajectory analysis."""
MULTI_INDEX = "multi_index"
"""Hierarchical column structure. Similar to NWB format."""
FRAMES = <DataFrameFormat.FRAMES: 'frames'>
class-attribute
¶
Enumeration of supported DataFrame formats.
INSTANCES = <DataFrameFormat.INSTANCES: 'instances'>
class-attribute
¶
Enumeration of supported DataFrame formats.
MULTI_INDEX = <DataFrameFormat.MULTI_INDEX: 'multi_index'>
class-attribute
¶
Enumeration of supported DataFrame formats.
POINTS = <DataFrameFormat.POINTS: 'points'>
class-attribute
¶
Enumeration of supported DataFrame formats.
__doc__ = 'Enumeration of supported DataFrame formats.'
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'.
__module__ = 'sleap_io.codecs.dataframe'
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'.
to_dict¶
sleap_io.codecs.dictionary.to_dict(labels, *, video=None, skip_empty_frames=False)
¶
Convert Labels to a primitive dictionary (JSON-serializable).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels
|
Labels
|
Labels object to convert. |
required |
video
|
Video | int | None
|
Optional video filter. If specified, only frames from this video are included. Can be a Video object or integer index. |
None
|
skip_empty_frames
|
bool
|
If True, exclude frames with no instances. |
False
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dictionary with structure: { "version": "1.0.0", "skeletons": [...], "videos": [...], "tracks": [...], "labeled_frames": [...], "suggestions": [...], "provenance": {...} } All values are JSON-serializable primitives (str, int, float, bool, None, list, dict). No numpy arrays or custom objects. |
Examples:
>>> labels = load_file("predictions.slp")
>>> d = to_dict(labels)
>>> import json
>>> json.dumps(d) # Fully serializable!
Notes
- Uses index-based references (e.g., skeleton_idx, video_idx) for compactness
- Preserves all metadata including provenance
- Points are stored as list of dicts with x, y, visible, complete fields
- The output is fully compatible with JSON, YAML, or any format that handles Python primitives
Source code in sleap_io/codecs/dictionary.py
def to_dict(
labels: Labels,
*,
video: Video | int | None = None,
skip_empty_frames: bool = False,
) -> dict[str, Any]:
"""Convert Labels to a primitive dictionary (JSON-serializable).
Args:
labels: Labels object to convert.
video: Optional video filter. If specified, only frames from this video
are included. Can be a Video object or integer index.
skip_empty_frames: If True, exclude frames with no instances.
Returns:
Dictionary with structure:
{
"version": "1.0.0",
"skeletons": [...],
"videos": [...],
"tracks": [...],
"labeled_frames": [...],
"suggestions": [...],
"provenance": {...}
}
All values are JSON-serializable primitives (str, int, float, bool, None,
list, dict). No numpy arrays or custom objects.
Examples:
>>> labels = load_file("predictions.slp")
>>> d = to_dict(labels)
>>> import json
>>> json.dumps(d) # Fully serializable!
>>> # Filter to specific video
>>> d = to_dict(labels, video=0)
>>> # Exclude empty frames
>>> d = to_dict(labels, skip_empty_frames=True)
Notes:
- Uses index-based references (e.g., skeleton_idx, video_idx) for compactness
- Preserves all metadata including provenance
- Points are stored as list of dicts with x, y, visible, complete fields
- The output is fully compatible with JSON, YAML, or any format that handles
Python primitives
"""
# Convert video parameter to index for fast path filtering
video_filter_idx: int | None = None
if video is not None:
if isinstance(video, int):
video_filter_idx = video
video = labels.videos[video]
else:
video_filter_idx = labels.videos.index(video)
# Build skeleton list
skeletons_list = []
for skeleton in labels.skeletons:
skeleton_dict = {
"name": skeleton.name,
"nodes": [node.name for node in skeleton.nodes],
"edges": [
[
skeleton.nodes.index(edge.source),
skeleton.nodes.index(edge.destination),
]
for edge in skeleton.edges
],
}
# Add symmetries if present
if skeleton.symmetries:
symmetries_list = []
for symmetry in skeleton.symmetries:
# Convert set to list for indexing and get indices
nodes_list = list(symmetry.nodes)
indices = [
skeleton.nodes.index(nodes_list[0]),
skeleton.nodes.index(nodes_list[1]),
]
# Sort indices for consistent ordering
indices.sort()
symmetries_list.append(indices)
skeleton_dict["symmetries"] = symmetries_list
skeletons_list.append(skeleton_dict)
# Build video list
videos_list = []
for vid in labels.videos:
video_dict = {
"filename": vid.filename,
}
# Add shape if available (Video.shape handles exceptions internally)
if vid.shape is not None:
video_dict["shape"] = list(vid.shape)
# Add backend info if available
if vid.backend is not None:
video_dict["backend"] = {"type": type(vid.backend).__name__}
videos_list.append(video_dict)
# Build track list
tracks_list = [{"name": track.name} for track in labels.tracks]
# Build labeled frames list - use fast path for lazy Labels
if labels.is_lazy:
labeled_frames_list = _build_labeled_frames_lazy(
labels.labeled_frames._store,
video_filter=video_filter_idx,
skip_empty_frames=skip_empty_frames,
)
else:
# Eager path: filter labeled frames
if video is not None:
labeled_frames = [lf for lf in labels.labeled_frames if lf.video == video]
else:
labeled_frames = labels.labeled_frames
# Skip empty frames if requested (preserve negative frames)
if skip_empty_frames:
labeled_frames = [
lf for lf in labeled_frames if len(lf.instances) > 0 or lf.is_negative
]
labeled_frames_list = []
for lf in labeled_frames:
instances_list = []
for instance in lf.instances:
# Determine instance type
is_predicted = isinstance(instance, PredictedInstance)
# Build points list
points_list = []
for point in instance.points:
point_dict = {
"x": float(point["xy"][0]),
"y": float(point["xy"][1]),
"visible": bool(point["visible"]),
"complete": bool(point["complete"]),
}
points_list.append(point_dict)
# Build instance dict
instance_dict = {
"type": "predicted_instance" if is_predicted else "instance",
"skeleton_idx": labels.skeletons.index(instance.skeleton),
"points": points_list,
}
# Add track if present
if instance.track is not None:
instance_dict["track_idx"] = labels.tracks.index(instance.track)
# Add tracking score if present
if (
hasattr(instance, "tracking_score")
and instance.tracking_score is not None
):
instance_dict["tracking_score"] = float(instance.tracking_score)
# Add score for predicted instances
if is_predicted:
instance_dict["score"] = float(instance.score)
# Add from_predicted if present
if (
hasattr(instance, "from_predicted")
and instance.from_predicted is not None
):
# Note: We can't serialize the reference, just indicate it exists
instance_dict["has_from_predicted"] = True
instances_list.append(instance_dict)
frame_dict = {
"frame_idx": int(lf.frame_idx),
"video_idx": labels.videos.index(lf.video),
"instances": instances_list,
}
if lf.is_negative:
frame_dict["is_negative"] = True
labeled_frames_list.append(frame_dict)
# Build suggestions list (if filtering by video, also filter suggestions)
if video is not None:
suggestions_to_include = [sf for sf in labels.suggestions if sf.video == video]
else:
suggestions_to_include = labels.suggestions
suggestions_list = []
for sf in suggestions_to_include:
suggestion_dict = {
"frame_idx": int(sf.frame_idx),
"video_idx": labels.videos.index(sf.video),
}
suggestions_list.append(suggestion_dict)
# Build complete dictionary
result = {
"version": "1.0.0",
"skeletons": skeletons_list,
"videos": videos_list,
"tracks": tracks_list,
"labeled_frames": labeled_frames_list,
"suggestions": suggestions_list,
"provenance": dict(labels.provenance), # Copy to ensure it's a plain dict
}
return result
from_dict¶
sleap_io.codecs.dictionary.from_dict(data)
¶
Create a Labels object from a dictionary.
This is the inverse of to_dict() and reconstructs a Labels object from
its dictionary representation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
dict[str, Any]
|
Dictionary in the format produced by |
required |
Returns:
| Type | Description |
|---|---|
Labels
|
A Labels object reconstructed from the dictionary. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the dictionary format is invalid or missing required keys. |
Examples:
>>> # Round-trip through JSON
>>> import json
>>> json_str = json.dumps(to_dict(labels))
>>> labels_restored = from_dict(json.loads(json_str))
Notes
- The
from_predictedrelationship cannot be fully restored since the dictionary only indicates its presence, not the actual reference. - Video backends are not restored; videos are created with filename only.
Source code in sleap_io/codecs/dictionary.py
def from_dict(data: dict[str, Any]) -> Labels:
"""Create a Labels object from a dictionary.
This is the inverse of `to_dict()` and reconstructs a Labels object from
its dictionary representation.
Args:
data: Dictionary in the format produced by `to_dict()`.
Returns:
A Labels object reconstructed from the dictionary.
Raises:
ValueError: If the dictionary format is invalid or missing required keys.
Examples:
>>> d = to_dict(labels)
>>> labels_restored = from_dict(d)
>>> # Round-trip through JSON
>>> import json
>>> json_str = json.dumps(to_dict(labels))
>>> labels_restored = from_dict(json.loads(json_str))
Notes:
- The `from_predicted` relationship cannot be fully restored since the
dictionary only indicates its presence, not the actual reference.
- Video backends are not restored; videos are created with filename only.
"""
import numpy as np
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.skeleton import Edge, Node, Skeleton, Symmetry
from sleap_io.model.suggestions import SuggestionFrame
# Validate required keys
required_keys = ["skeletons", "videos", "labeled_frames"]
for key in required_keys:
if key not in data:
raise ValueError(f"Missing required key: {key}")
# Build skeletons
skeletons = []
for skel_dict in data["skeletons"]:
# Create nodes
nodes = [Node(name=name) for name in skel_dict["nodes"]]
# Create edges
edges = []
for src_idx, dst_idx in skel_dict.get("edges", []):
edges.append(Edge(source=nodes[src_idx], destination=nodes[dst_idx]))
# Create symmetries
symmetries = []
for sym_indices in skel_dict.get("symmetries", []):
symmetries.append(
Symmetry(nodes={nodes[sym_indices[0]], nodes[sym_indices[1]]})
)
skeleton = Skeleton(
nodes=nodes,
edges=edges,
symmetries=symmetries,
name=skel_dict.get("name", ""),
)
skeletons.append(skeleton)
# Build videos
videos = []
for vid_dict in data["videos"]:
video = Video(filename=vid_dict["filename"])
videos.append(video)
# Build tracks
tracks = []
for track_dict in data.get("tracks", []):
track = Track(name=track_dict["name"])
tracks.append(track)
# Build labeled frames
labeled_frames = []
for lf_dict in data["labeled_frames"]:
video = videos[lf_dict["video_idx"]]
frame_idx = lf_dict["frame_idx"]
instances = []
for inst_dict in lf_dict.get("instances", []):
skeleton = skeletons[inst_dict["skeleton_idx"]]
is_predicted = inst_dict["type"] == "predicted_instance"
# Build points array
points_list = inst_dict["points"]
n_nodes = len(points_list)
points_data = np.full((n_nodes, 2), np.nan, dtype="float64")
for node_idx, pt in enumerate(points_list):
if pt.get("visible", True):
points_data[node_idx, 0] = pt["x"]
points_data[node_idx, 1] = pt["y"]
# Get track if present
track = None
if "track_idx" in inst_dict:
track = tracks[inst_dict["track_idx"]]
# Get tracking score if present
tracking_score = inst_dict.get("tracking_score")
if is_predicted:
# Get instance score
score = inst_dict.get("score", 0.0)
# Create predicted instance
instance = PredictedInstance.from_numpy(
points_data=points_data,
skeleton=skeleton,
score=score,
track=track,
tracking_score=tracking_score,
)
else:
# Create user instance
instance = Instance.from_numpy(
points_data=points_data,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
)
instances.append(instance)
labeled_frame = LabeledFrame(
video=video,
frame_idx=frame_idx,
instances=instances,
is_negative=lf_dict.get("is_negative", False),
)
labeled_frames.append(labeled_frame)
# Build suggestions
suggestions = []
for sug_dict in data.get("suggestions", []):
video = videos[sug_dict["video_idx"]]
suggestion = SuggestionFrame(
video=video,
frame_idx=sug_dict["frame_idx"],
)
suggestions.append(suggestion)
# Build provenance
provenance = dict(data.get("provenance", {}))
# Create Labels object
labels = Labels(
labeled_frames=labeled_frames,
videos=videos,
skeletons=skeletons,
tracks=tracks,
suggestions=suggestions,
provenance=provenance,
)
return labels
to_numpy¶
sleap_io.codecs.numpy.to_numpy(labels, *, video=None, untracked=False, return_confidence=False, user_instances=True, predicted_instances=True)
¶
Convert Labels to a numpy array.
This is a more flexible version of Labels.numpy() with enhanced options.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels
|
Labels
|
Labels object to convert. |
required |
video
|
Video | int | None
|
Video or video index to convert to numpy arrays. If None (the default), uses the first video. |
None
|
untracked
|
bool
|
If False (the default), include only instances that have a track assignment. If True, includes all instances in each frame in arbitrary order. |
False
|
return_confidence
|
bool
|
If False (the default), only return points of nodes. If True, return the points and scores of nodes. |
False
|
user_instances
|
bool
|
If True (the default), include user instances when available, preferring them over predicted instances with the same track. If False, only include predicted instances. |
True
|
predicted_instances
|
bool
|
If True (the default), include predicted instances. If False, only include user instances. |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
An array of tracks of shape Missing data will be replaced with If this is a single instance project, a track does not need to be assigned. When |
Notes
This method assumes that instances have tracks assigned and is intended to function primarily for single-video prediction results.
This function contains the core logic for numpy conversion. The Labels.numpy() method delegates to this function.
Examples:
>>> arr = to_numpy(labels, video=0, return_confidence=True)
>>> arr.shape
(100, 2, 5, 3) # 100 frames, 2 tracks, 5 nodes, (x, y, score)
>>> # Get only user instances
>>> arr = to_numpy(labels, user_instances=True, predicted_instances=False)
Source code in sleap_io/codecs/numpy.py
def to_numpy(
labels: Labels,
*,
video: Video | int | None = None,
untracked: bool = False,
return_confidence: bool = False,
user_instances: bool = True,
predicted_instances: bool = True,
) -> np.ndarray:
"""Convert Labels to a numpy array.
This is a more flexible version of Labels.numpy() with enhanced options.
Args:
labels: Labels object to convert.
video: Video or video index to convert to numpy arrays. If None (the default),
uses the first video.
untracked: If False (the default), include only instances that have a track
assignment. If True, includes all instances in each frame in arbitrary
order.
return_confidence: If False (the default), only return points of nodes. If
True, return the points and scores of nodes.
user_instances: If True (the default), include user instances when available,
preferring them over predicted instances with the same track. If False,
only include predicted instances.
predicted_instances: If True (the default), include predicted instances.
If False, only include user instances.
Returns:
An array of tracks of shape `(n_frames, n_tracks, n_nodes, 2)` if
`return_confidence` is False. Otherwise returned shape is
`(n_frames, n_tracks, n_nodes, 3)` if `return_confidence` is True.
Missing data will be replaced with `np.nan`.
If this is a single instance project, a track does not need to be assigned.
When `user_instances=False`, only predicted instances will be returned.
When `user_instances=True`, user instances will be preferred over predicted
instances with the same track or if linked via `from_predicted`.
Notes:
This method assumes that instances have tracks assigned and is intended to
function primarily for single-video prediction results.
This function contains the core logic for numpy conversion. The
Labels.numpy() method delegates to this function.
Examples:
>>> arr = to_numpy(labels, video=0, return_confidence=True)
>>> arr.shape
(100, 2, 5, 3) # 100 frames, 2 tracks, 5 nodes, (x, y, score)
>>> # Get only user instances
>>> arr = to_numpy(labels, user_instances=True, predicted_instances=False)
>>> # Include untracked instances
>>> arr = to_numpy(labels, untracked=True)
"""
# Convert video parameter to Video object
if video is None:
video = labels.videos[0] if labels.videos else None
elif type(video) is int:
video = labels.videos[video]
# Use lazy fast path when available
if labels.is_lazy:
store = labels.labeled_frames._store
return store.to_numpy(
video=video,
untracked=untracked,
return_confidence=return_confidence,
user_instances=user_instances,
)
# Eager path: filter labeled frames by video
lfs = [lf for lf in labels.labeled_frames if lf.video == video]
# Figure out frame index range.
first_frame, last_frame = 0, 0
for lf in lfs:
first_frame = min(first_frame, lf.frame_idx)
last_frame = max(last_frame, lf.frame_idx)
# Use video length when available so output spans the full video duration.
video_length = len(video) if video is not None else 0
if video_length > 0:
last_frame = max(last_frame, video_length - 1)
# Figure out the number of tracks based on number of instances in each frame.
n_instances = _max_instances_per_frame(
lfs, user_instances=user_instances, predicted_instances=predicted_instances
)
# Case 1: We don't care about order because there's only 1 instance per frame,
# or we're considering untracked instances.
is_single_instance = n_instances == 1
untracked = untracked or is_single_instance
if untracked:
n_tracks = n_instances
else:
# Case 2: We're considering only tracked instances.
n_tracks = len(labels.tracks)
n_frames = int(last_frame - first_frame + 1)
skeleton = labels.skeletons[-1] # Assume project only uses last skeleton
n_nodes = len(skeleton.nodes)
if return_confidence:
tracks = np.full((n_frames, n_tracks, n_nodes, 3), np.nan, dtype="float32")
else:
tracks = np.full((n_frames, n_tracks, n_nodes, 2), np.nan, dtype="float32")
for lf in lfs:
i = int(lf.frame_idx - first_frame)
if untracked:
# For untracked instances, fill them in arbitrary order.
frame_instances = _untracked_frame_instances(
lf,
is_single_instance=is_single_instance,
user_instances=user_instances,
predicted_instances=predicted_instances,
)
for j, inst in enumerate(frame_instances):
if j < n_tracks:
if return_confidence:
if isinstance(inst, PredictedInstance):
tracks[i, j] = inst.numpy(scores=True)
else:
# For user instances, set confidence to 1.0
points_data = inst.numpy()
confidence = np.ones(
(points_data.shape[0], 1), dtype="float32"
)
tracks[i, j] = np.hstack((points_data, confidence))
else:
tracks[i, j] = inst.numpy()
else: # untracked is False
# For tracked instances, organize by track ID.
track_to_instance = _tracked_frame_instances(
lf,
user_instances=user_instances,
predicted_instances=predicted_instances,
)
for track, inst in track_to_instance.items():
j = labels.tracks.index(track)
if type(inst) is PredictedInstance:
tracks[i, j] = inst.numpy(scores=return_confidence)
elif type(inst) is Instance:
tracks[i, j, :, :2] = inst.numpy()
# If return_confidence is True, add dummy confidence scores
if return_confidence:
tracks[i, j, :, 2] = 1.0
return tracks
from_numpy¶
sleap_io.codecs.numpy.from_numpy(tracks_array, *, videos=None, video=None, skeletons=None, skeleton=None, tracks=None, track_names=None, first_frame=0, return_confidence=False)
¶
Create a new Labels object from a numpy array of tracks.
This factory method creates a new Labels object with instances constructed from the provided numpy array. It is a more flexible version of Labels.from_numpy().
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tracks_array
|
ndarray
|
A numpy array of tracks, with shape
|
required |
videos
|
list[Video] | None
|
List of Video objects to associate with the labels. At least one
video is required. Mutually exclusive with |
None
|
video
|
Video | None
|
Single Video object to associate with the labels. Mutually exclusive
with |
None
|
skeletons
|
list[Skeleton] | Skeleton | None
|
Skeleton or list of Skeleton objects to use for the instances.
At least one skeleton is required. Mutually exclusive with |
None
|
skeleton
|
Skeleton | None
|
Single Skeleton object to use. Mutually exclusive with |
None
|
tracks
|
list[Track] | None
|
List of Track objects corresponding to the second dimension of the
array. If not specified, new tracks will be created automatically using
|
None
|
track_names
|
list[str] | None
|
List of track names to use when auto-creating tracks. Only used
if |
None
|
first_frame
|
int
|
Frame index to start the labeled frames from. Default is 0. |
0
|
return_confidence
|
bool
|
Whether the tracks array contains confidence scores in the last dimension. If True, tracks.shape[-1] should be 3. If False or None, will be inferred from array shape. |
False
|
Returns:
| Type | Description |
|---|---|
Labels
|
A new Labels object with instances constructed from the numpy array. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the array dimensions are invalid, or if no videos or
skeletons are provided, or if both |
Examples:
>>> import numpy as np
>>> from sleap_io import Video, Skeleton
>>> from sleap_io.codecs import from_numpy
>>> # Create a simple tracking array for 2 frames, 1 track, 2 nodes
>>> arr = np.zeros((2, 1, 2, 2))
>>> arr[0, 0] = [[10, 20], [30, 40]] # Frame 0
>>> arr[1, 0] = [[15, 25], [35, 45]] # Frame 1
>>> # Create labels from the array
>>> video = Video(filename="example.mp4")
>>> skeleton = Skeleton(["head", "tail"])
>>> labels = from_numpy(arr, video=video, skeleton=skeleton)
>>> # With custom track names
>>> labels = from_numpy(arr, video=video, skeleton=skeleton,
... track_names=["mouse1"])
>>> # With confidence scores
>>> arr_with_conf = np.zeros((2, 1, 2, 3))
>>> arr_with_conf[0, 0] = [[10, 20, 0.95], [30, 40, 0.98]]
>>> labels = from_numpy(arr_with_conf, video=video, skeleton=skeleton,
... return_confidence=True)
Source code in sleap_io/codecs/numpy.py
def from_numpy(
tracks_array: np.ndarray,
*,
videos: list[Video] | None = None,
video: Video | None = None,
skeletons: list[Skeleton] | Skeleton | None = None,
skeleton: Skeleton | None = None,
tracks: list[Track] | None = None,
track_names: list[str] | None = None,
first_frame: int = 0,
return_confidence: bool = False,
) -> Labels:
"""Create a new Labels object from a numpy array of tracks.
This factory method creates a new Labels object with instances constructed from
the provided numpy array. It is a more flexible version of Labels.from_numpy().
Args:
tracks_array: A numpy array of tracks, with shape
`(n_frames, n_tracks, n_nodes, 2)` or `(n_frames, n_tracks, n_nodes, 3)`,
where the last dimension contains the x,y coordinates (and optionally
confidence scores).
videos: List of Video objects to associate with the labels. At least one
video is required. Mutually exclusive with `video`.
video: Single Video object to associate with the labels. Mutually exclusive
with `videos`.
skeletons: Skeleton or list of Skeleton objects to use for the instances.
At least one skeleton is required. Mutually exclusive with `skeleton`.
skeleton: Single Skeleton object to use. Mutually exclusive with `skeletons`.
tracks: List of Track objects corresponding to the second dimension of the
array. If not specified, new tracks will be created automatically using
`track_names` if provided, or default names.
track_names: List of track names to use when auto-creating tracks. Only used
if `tracks` is None.
first_frame: Frame index to start the labeled frames from. Default is 0.
return_confidence: Whether the tracks array contains confidence scores in the
last dimension. If True, tracks.shape[-1] should be 3. If False or None,
will be inferred from array shape.
Returns:
A new Labels object with instances constructed from the numpy array.
Raises:
ValueError: If the array dimensions are invalid, or if no videos or
skeletons are provided, or if both `videos` and `video` are provided.
Examples:
>>> import numpy as np
>>> from sleap_io import Video, Skeleton
>>> from sleap_io.codecs import from_numpy
>>> # Create a simple tracking array for 2 frames, 1 track, 2 nodes
>>> arr = np.zeros((2, 1, 2, 2))
>>> arr[0, 0] = [[10, 20], [30, 40]] # Frame 0
>>> arr[1, 0] = [[15, 25], [35, 45]] # Frame 1
>>> # Create labels from the array
>>> video = Video(filename="example.mp4")
>>> skeleton = Skeleton(["head", "tail"])
>>> labels = from_numpy(arr, video=video, skeleton=skeleton)
>>> # With custom track names
>>> labels = from_numpy(arr, video=video, skeleton=skeleton,
... track_names=["mouse1"])
>>> # With confidence scores
>>> arr_with_conf = np.zeros((2, 1, 2, 3))
>>> arr_with_conf[0, 0] = [[10, 20, 0.95], [30, 40, 0.98]]
>>> labels = from_numpy(arr_with_conf, video=video, skeleton=skeleton,
... return_confidence=True)
"""
# Check dimensions
if len(tracks_array.shape) != 4:
raise ValueError(
f"Array must have 4 dimensions (n_frames, n_tracks, n_nodes, 2 or 3), "
f"but got {tracks_array.shape}"
)
# Handle video/videos parameter
if video is not None and videos is not None:
raise ValueError("Cannot specify both 'video' and 'videos' parameters")
if video is not None:
videos = [video]
elif videos is None:
raise ValueError("At least one video must be provided via 'video' or 'videos'")
if not videos:
raise ValueError("At least one video must be provided")
video = videos[0] # Use the first video for creating labeled frames
# Handle skeleton/skeletons parameter
if skeleton is not None and skeletons is not None:
raise ValueError("Cannot specify both 'skeleton' and 'skeletons' parameters")
if skeleton is not None:
skeletons = [skeleton]
elif skeletons is None:
raise ValueError(
"At least one skeleton must be provided via 'skeleton' or 'skeletons'"
)
elif isinstance(skeletons, Skeleton):
skeletons = [skeletons]
elif not skeletons: # Check for empty list
raise ValueError("At least one skeleton must be provided")
skeleton = skeletons[0] # Use the first skeleton for creating instances
n_nodes = len(skeleton.nodes)
# Check if tracks_array contains confidence scores
has_confidence = tracks_array.shape[-1] == 3 or return_confidence
# Get dimensions
n_frames, n_tracks_arr, _ = tracks_array.shape[:3]
# Create or validate tracks
if tracks is None:
# Auto-create tracks
if track_names is not None:
if len(track_names) < n_tracks_arr:
# Extend with default names if needed
track_names = list(track_names) + [
f"track_{i}" for i in range(len(track_names), n_tracks_arr)
]
tracks = [Track(name=name) for name in track_names[:n_tracks_arr]]
else:
tracks = [Track(f"track_{i}") for i in range(n_tracks_arr)]
elif len(tracks) < n_tracks_arr:
# Add missing tracks if needed
original_len = len(tracks)
for i in range(n_tracks_arr - original_len):
tracks.append(Track(f"track_{i}"))
# Create a new empty Labels object
labels = Labels()
labels.videos = list(videos)
labels.skeletons = list(skeletons)
labels.tracks = list(tracks)
# Create labeled frames and instances from the array data
for i in range(n_frames):
frame_idx = i + first_frame
# Check if this frame has any valid data across all tracks
frame_has_valid_data = False
for j in range(n_tracks_arr):
track_data = tracks_array[i, j]
# Check if at least one node in this track has valid xy coordinates
if np.any(~np.isnan(track_data[:, 0])):
frame_has_valid_data = True
break
# Skip creating a frame if there's no valid data
if not frame_has_valid_data:
continue
# Create a new labeled frame
labeled_frame = LabeledFrame(video=video, frame_idx=frame_idx)
frame_has_valid_instances = False
# Process each track in this frame
for j in range(n_tracks_arr):
track = tracks[j]
track_data = tracks_array[i, j]
# Check if there's any valid data for this track at this frame
valid_points = ~np.isnan(track_data[:, 0])
if not np.any(valid_points):
continue
# Create points from numpy data
points = track_data[:, :2].copy()
# Create new instance
if has_confidence:
# Get confidence scores
if tracks_array.shape[-1] == 3:
scores = track_data[:, 2].copy()
else:
scores = np.ones(n_nodes)
# Fix NaN scores
scores = np.where(np.isnan(scores), 1.0, scores)
# Create instance with confidence scores
new_instance = PredictedInstance.from_numpy(
points_data=points,
skeleton=skeleton,
point_scores=scores,
score=1.0,
track=track,
)
else:
# Create instance with default scores
new_instance = PredictedInstance.from_numpy(
points_data=points,
skeleton=skeleton,
point_scores=np.ones(n_nodes),
score=1.0,
track=track,
)
# Add to frame
labeled_frame.instances.append(new_instance)
frame_has_valid_instances = True
# Only add frames that have instances
if frame_has_valid_instances:
labels.append(labeled_frame, update=False)
# Update internal references
labels.update()
return labels
to_dataframe¶
sleap_io.codecs.dataframe.to_dataframe(labels, format=<DataFrameFormat.POINTS: 'points'>, *, video=None, include_metadata=True, include_score=True, include_user_instances=True, include_predicted_instances=True, video_id='path', include_video=None, instance_id='index', untracked='error', backend='pandas', all_frames=False, start_frame=None, end_frame=None)
¶
Convert Labels to a DataFrame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels
|
Labels
|
Labels object to convert. |
required |
format
|
DataFrameFormat | str
|
Output format. One of "points", "instances", "frames", "multi_index". |
<DataFrameFormat.POINTS: 'points'>
|
video
|
Video | int | None
|
Optional video filter. If specified, only frames from this video are included. Can be a Video object or integer index. |
None
|
include_metadata
|
bool
|
Include track, video information in columns. |
True
|
include_score
|
bool
|
Include confidence scores for predicted instances. |
True
|
include_user_instances
|
bool
|
Include user-labeled instances. |
True
|
include_predicted_instances
|
bool
|
Include predicted instances. |
True
|
video_id
|
Literal['path', 'index', 'name', 'object']
|
How to represent videos in the DataFrame. Options: - "path": Full filename/path (default). Works for all video types. - "index": Integer video index. Compact, requires video list for decoding. - "name": Just the video filename (no directory). May not be unique. - "object": Store Video object directly. Not serializable but preserves all video metadata (dataset for HDF5, frame paths for ImageVideo). |
'path'
|
include_video
|
bool | None
|
Whether to include video information. If None (default), automatically includes video info if there are multiple videos or if video metadata is needed. Set False to always omit, True to always include. |
None
|
instance_id
|
Literal['index', 'track']
|
How to name instance columns in "frames" and "multi_index" formats. - "index": Use inst0, inst1, inst2, etc. (default). - "track": Use track names as column prefixes (e.g., mouse1, mouse2). |
'index'
|
untracked
|
Literal['error', 'ignore']
|
Behavior for untracked instances with instance_id="track". - "error": Raise error if any instance lacks a track (default). - "ignore": Skip untracked instances silently. |
'error'
|
backend
|
Literal['pandas', 'polars']
|
"pandas" or "polars". Polars requires the polars package. When using polars, DataFrames are constructed natively without going through pandas, providing better performance for large datasets. |
'pandas'
|
all_frames
|
bool
|
If True, include rows for frames without instances (filled with NaN values). If False (default), only include frames that have instances. Only applies to "frames" and "instances" formats. |
False
|
start_frame
|
int | None
|
Start frame index (inclusive) for frame padding. If None, starts from 0 when all_frames=True, or from first labeled frame otherwise. |
None
|
end_frame
|
int | None
|
End frame index (exclusive) for frame padding. If None, ends at the full video length when known, otherwise at last labeled frame + 1. |
None
|
Returns:
| Type | Description |
|---|---|
DataFrame | DataFrame
|
DataFrame in the specified format. Type depends on backend parameter. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an invalid format is specified or polars is requested but not installed. |
Examples:
Basic usage:
>>> labels = load_file("predictions.slp")
>>> df = to_dataframe(labels, format="points")
>>> df.head()
frame_idx video_path track node x y score
0 0 video.mp4 track0 nose 10.0 20.0 0.95
1 0 video.mp4 track0 tail 5.0 8.0 0.92
Wide format with instances multiplexed per frame:
>>> df = to_dataframe(labels, format="frames")
>>> df.columns # inst0.track, inst0.nose.x, inst0.nose.y, ...
Track-named columns (requires tracked instances):
>>> df = to_dataframe(labels, format="frames", instance_id="track")
>>> df.columns # mouse1.nose.x, mouse1.nose.y, mouse2.nose.x, ...
Native polars backend for better performance:
>>> df = to_dataframe(labels, format="points", backend="polars")
>>> type(df)
<class 'polars.dataframe.frame.DataFrame'>
Notes
The specific columns and structure depend on the format parameter. See the DataFrameFormat enum documentation for details on each format.
Column naming conventions: - Points: frame_idx, node, x, y, track, track_score, instance_score - Instances: frame_idx, track, track_score, score, {node}.x/y/score - Frames: frame_idx, {inst}.track, {inst}.track_score, {inst}.score, {inst}.{node}.x, {inst}.{node}.y, {inst}.{node}.score - Multi-index: Hierarchical columns (inst, node, coord) with frame idx For polars backend, multi-index columns are flattened to dot-separated names (e.g., "inst0.nose.x").
Source code in sleap_io/codecs/dataframe.py
def to_dataframe(
labels: Labels,
format: DataFrameFormat | str = DataFrameFormat.POINTS,
*,
video: Video | int | None = None,
include_metadata: bool = True,
include_score: bool = True,
include_user_instances: bool = True,
include_predicted_instances: bool = True,
video_id: Literal["path", "index", "name", "object"] = "path",
include_video: bool | None = None,
instance_id: Literal["index", "track"] = "index",
untracked: Literal["error", "ignore"] = "error",
backend: Literal["pandas", "polars"] = "pandas",
all_frames: bool = False,
start_frame: int | None = None,
end_frame: int | None = None,
) -> pd.DataFrame | "pl.DataFrame":
"""Convert Labels to a DataFrame.
Args:
labels: Labels object to convert.
format: Output format. One of "points", "instances", "frames", "multi_index".
video: Optional video filter. If specified, only frames from this video
are included. Can be a Video object or integer index.
include_metadata: Include track, video information in columns.
include_score: Include confidence scores for predicted instances.
include_user_instances: Include user-labeled instances.
include_predicted_instances: Include predicted instances.
video_id: How to represent videos in the DataFrame. Options:
- "path": Full filename/path (default). Works for all video types.
- "index": Integer video index. Compact, requires video list for decoding.
- "name": Just the video filename (no directory). May not be unique.
- "object": Store Video object directly. Not serializable but preserves
all video metadata (dataset for HDF5, frame paths for ImageVideo).
include_video: Whether to include video information. If None (default),
automatically includes video info if there are multiple videos or if
video metadata is needed. Set False to always omit, True to always include.
instance_id: How to name instance columns in "frames" and "multi_index" formats.
- "index": Use inst0, inst1, inst2, etc. (default).
- "track": Use track names as column prefixes (e.g., mouse1, mouse2).
untracked: Behavior for untracked instances with instance_id="track".
- "error": Raise error if any instance lacks a track (default).
- "ignore": Skip untracked instances silently.
backend: "pandas" or "polars". Polars requires the polars package.
When using polars, DataFrames are constructed natively without
going through pandas, providing better performance for large datasets.
all_frames: If True, include rows for frames without instances (filled with
NaN values). If False (default), only include frames that have instances.
Only applies to "frames" and "instances" formats.
start_frame: Start frame index (inclusive) for frame padding. If None, starts
from 0 when all_frames=True, or from first labeled frame otherwise.
end_frame: End frame index (exclusive) for frame padding. If None, ends at
the full video length when known, otherwise at last labeled frame + 1.
Returns:
DataFrame in the specified format. Type depends on backend parameter.
Raises:
ValueError: If an invalid format is specified or polars is requested but
not installed.
Examples:
Basic usage:
>>> labels = load_file("predictions.slp")
>>> df = to_dataframe(labels, format="points")
>>> df.head()
frame_idx video_path track node x y score
0 0 video.mp4 track0 nose 10.0 20.0 0.95
1 0 video.mp4 track0 tail 5.0 8.0 0.92
Wide format with instances multiplexed per frame:
>>> df = to_dataframe(labels, format="frames")
>>> df.columns # inst0.track, inst0.nose.x, inst0.nose.y, ...
Track-named columns (requires tracked instances):
>>> df = to_dataframe(labels, format="frames", instance_id="track")
>>> df.columns # mouse1.nose.x, mouse1.nose.y, mouse2.nose.x, ...
Native polars backend for better performance:
>>> df = to_dataframe(labels, format="points", backend="polars")
>>> type(df)
<class 'polars.dataframe.frame.DataFrame'>
Notes:
The specific columns and structure depend on the format parameter.
See the DataFrameFormat enum documentation for details on each format.
Column naming conventions:
- Points: frame_idx, node, x, y, track, track_score, instance_score
- Instances: frame_idx, track, track_score, score, {node}.x/y/score
- Frames: frame_idx, {inst}.track, {inst}.track_score, {inst}.score,
{inst}.{node}.x, {inst}.{node}.y, {inst}.{node}.score
- Multi-index: Hierarchical columns (inst, node, coord) with frame idx
For polars backend, multi-index columns are flattened to dot-separated
names (e.g., "inst0.nose.x").
"""
# Validate backend
if backend == "polars" and not HAS_POLARS:
raise ValueError(
"Polars backend requested but polars is not installed. "
"Install with: pip install polars"
)
# Normalize format parameter
if isinstance(format, str):
try:
format = DataFrameFormat(format.lower())
except ValueError:
valid_formats = ", ".join([f.value for f in DataFrameFormat])
raise ValueError(
f"Invalid format '{format}'. Must be one of: {valid_formats}"
)
# Convert video parameter to index for fast path filtering
video_filter_idx: int | None = None
if video is not None:
if isinstance(video, int):
video_filter_idx = video
video = labels.videos[video]
else:
video_filter_idx = labels.videos.index(video)
# Determine whether to include video info
if include_video is None:
# Auto-detect: include if multiple videos, unless explicitly omitted
include_video = len(labels.videos) > 1
# Use lazy fast path when available (for POINTS and INSTANCES formats)
if labels.is_lazy and format in (DataFrameFormat.POINTS, DataFrameFormat.INSTANCES):
store = labels.labeled_frames._store
if format == DataFrameFormat.POINTS:
return _to_points_df_lazy(
store,
labels,
video_filter=video_filter_idx,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
backend=backend,
)
else: # INSTANCES
return _to_instances_df_lazy(
store,
labels,
video_filter=video_filter_idx,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
backend=backend,
)
# Eager path: filter labeled frames
if video is not None:
labeled_frames = [lf for lf in labels.labeled_frames if lf.video == video]
else:
labeled_frames = labels.labeled_frames
# Route to appropriate converter based on format
if format == DataFrameFormat.POINTS:
df = _to_points_df(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
backend=backend,
)
elif format == DataFrameFormat.INSTANCES:
df = _to_instances_df(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
backend=backend,
video_filter_idx=video_filter_idx,
all_frames=all_frames,
start_frame=start_frame,
end_frame=end_frame,
)
elif format == DataFrameFormat.FRAMES:
df = _to_frames_df(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
instance_id=instance_id,
untracked=untracked,
backend=backend,
video_filter_idx=video_filter_idx,
all_frames=all_frames,
start_frame=start_frame,
end_frame=end_frame,
)
elif format == DataFrameFormat.MULTI_INDEX:
df = _to_multi_index_df(
labels,
labeled_frames,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
instance_id=instance_id,
untracked=untracked,
backend=backend,
)
else:
raise ValueError(f"Unknown format: {format}")
return df
from_dataframe¶
sleap_io.codecs.dataframe.from_dataframe(df, *, video=None, skeleton=None, format=<DataFrameFormat.POINTS: 'points'>)
¶
Create a Labels object from a DataFrame.
This function reconstructs a Labels object from a DataFrame created by
to_dataframe(). Supports all formats: points, instances, frames, multi_index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
DataFrame created by to_dataframe() or compatible structure. |
required |
video
|
Video | None
|
Video object to associate with all frames. Required if the DataFrame does not have video information. |
None
|
skeleton
|
Skeleton | None
|
Skeleton object to use. Required if the DataFrame does not have skeleton information or if the skeleton needs to be provided explicitly. |
None
|
format
|
DataFrameFormat | str
|
The format of the input DataFrame. One of "points", "instances", "frames", "multi_index". |
<DataFrameFormat.POINTS: 'points'>
|
Returns:
| Type | Description |
|---|---|
Labels
|
A Labels object reconstructed from the DataFrame. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If required columns are missing or format is invalid. |
Examples:
>>> df = to_dataframe(labels, format="points")
>>> labels_restored = from_dataframe(df, video=video, skeleton=skeleton)
>>> df = to_dataframe(labels, format="instances")
>>> labels_restored = from_dataframe(df, format="instances", skeleton=skeleton)
Notes
- The DataFrame must have the expected structure for the specified format.
- If video information is not in the DataFrame, a Video must be provided.
- If skeleton is not provided, it will be inferred from column names where possible.
- Tracks are reconstructed from track/track_name columns if present.
Source code in sleap_io/codecs/dataframe.py
def from_dataframe(
df: pd.DataFrame,
*,
video: Video | None = None,
skeleton: "Skeleton | None" = None, # noqa: F821
format: DataFrameFormat | str = DataFrameFormat.POINTS,
) -> Labels:
"""Create a Labels object from a DataFrame.
This function reconstructs a Labels object from a DataFrame created by
`to_dataframe()`. Supports all formats: points, instances, frames, multi_index.
Args:
df: DataFrame created by to_dataframe() or compatible structure.
video: Video object to associate with all frames. Required if the DataFrame
does not have video information.
skeleton: Skeleton object to use. Required if the DataFrame does not have
skeleton information or if the skeleton needs to be provided explicitly.
format: The format of the input DataFrame. One of "points", "instances",
"frames", "multi_index".
Returns:
A Labels object reconstructed from the DataFrame.
Raises:
ValueError: If required columns are missing or format is invalid.
Examples:
>>> df = to_dataframe(labels, format="points")
>>> labels_restored = from_dataframe(df, video=video, skeleton=skeleton)
>>> df = to_dataframe(labels, format="instances")
>>> labels_restored = from_dataframe(df, format="instances", skeleton=skeleton)
Notes:
- The DataFrame must have the expected structure for the specified format.
- If video information is not in the DataFrame, a Video must be provided.
- If skeleton is not provided, it will be inferred from column names where
possible.
- Tracks are reconstructed from track/track_name columns if present.
"""
# Normalize format parameter
if isinstance(format, str):
try:
format = DataFrameFormat(format.lower())
except ValueError:
valid_formats = ", ".join([f.value for f in DataFrameFormat])
raise ValueError(
f"Invalid format '{format}'. Must be one of: {valid_formats}"
)
if format == DataFrameFormat.POINTS:
return _from_points_df(df, video=video, skeleton=skeleton)
elif format == DataFrameFormat.INSTANCES:
return _from_instances_df(df, video=video, skeleton=skeleton)
elif format == DataFrameFormat.FRAMES:
return _from_frames_df(df, video=video, skeleton=skeleton)
elif format == DataFrameFormat.MULTI_INDEX:
return _from_multi_index_df(df, video=video, skeleton=skeleton)
else:
raise ValueError(f"Unknown format: {format}")
to_dataframe_iter¶
sleap_io.codecs.dataframe.to_dataframe_iter(labels, format=<DataFrameFormat.POINTS: 'points'>, *, chunk_size=None, video=None, include_metadata=True, include_score=True, include_user_instances=True, include_predicted_instances=True, video_id='path', include_video=None, instance_id='index', untracked='error', backend='pandas')
¶
Iterate over Labels data, yielding DataFrames in chunks.
This is a memory-efficient alternative to to_dataframe() for large datasets.
Instead of materializing the entire DataFrame at once, it yields smaller
DataFrames (chunks) that can be processed incrementally.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
labels
|
Labels
|
Labels object to convert. |
required |
format
|
DataFrameFormat | str
|
Output format. One of "points", "instances", "frames", "multi_index". |
<DataFrameFormat.POINTS: 'points'>
|
chunk_size
|
int | None
|
Number of rows per chunk. If None (default), yields the entire
DataFrame in a single chunk (equivalent to |
None
|
video
|
Video | int | None
|
Optional video filter. If specified, only frames from this video are included. Can be a Video object or integer index. |
None
|
include_metadata
|
bool
|
Include track, video information in columns. |
True
|
include_score
|
bool
|
Include confidence scores for predicted instances. |
True
|
include_user_instances
|
bool
|
Include user-labeled instances. |
True
|
include_predicted_instances
|
bool
|
Include predicted instances. |
True
|
video_id
|
Literal['path', 'index', 'name', 'object']
|
How to represent videos in the DataFrame. Options: - "path": Full filename/path (default). - "index": Integer video index. - "name": Just the video filename. - "object": Store Video object directly. |
'path'
|
include_video
|
bool | None
|
Whether to include video information. |
None
|
instance_id
|
Literal['index', 'track']
|
How to name instance columns in "frames" and "multi_index" formats. - "index": Use inst0, inst1, inst2, etc. (default). - "track": Use track names as column prefixes. |
'index'
|
untracked
|
Literal['error', 'ignore']
|
Behavior for untracked instances with instance_id="track". - "error": Raise error if any instance lacks a track (default). - "ignore": Skip untracked instances silently. |
'error'
|
backend
|
Literal['pandas', 'polars']
|
"pandas" or "polars". Polars requires the polars package. When using polars, DataFrames are constructed natively without going through pandas, providing better performance for large datasets. |
'pandas'
|
Yields:
| Type | Description |
|---|---|
DataFrame | DataFrame
|
DataFrames, each containing up to |
Examples:
Process large datasets in chunks:
>>> for df_chunk in to_dataframe_iter(labels, chunk_size=10000):
... df_chunk.to_parquet("output.parquet", append=True)
Concatenate chunks to get full DataFrame (equivalent to to_dataframe):
Memory-efficient per-video processing:
>>> for video in labels.videos:
... for chunk in to_dataframe_iter(labels, video=video, chunk_size=5000):
... process_chunk(chunk)
Source code in sleap_io/codecs/dataframe.py
def to_dataframe_iter(
labels: Labels,
format: DataFrameFormat | str = DataFrameFormat.POINTS,
*,
chunk_size: int | None = None,
video: Video | int | None = None,
include_metadata: bool = True,
include_score: bool = True,
include_user_instances: bool = True,
include_predicted_instances: bool = True,
video_id: Literal["path", "index", "name", "object"] = "path",
include_video: bool | None = None,
instance_id: Literal["index", "track"] = "index",
untracked: Literal["error", "ignore"] = "error",
backend: Literal["pandas", "polars"] = "pandas",
) -> Iterator[pd.DataFrame | "pl.DataFrame"]:
"""Iterate over Labels data, yielding DataFrames in chunks.
This is a memory-efficient alternative to `to_dataframe()` for large datasets.
Instead of materializing the entire DataFrame at once, it yields smaller
DataFrames (chunks) that can be processed incrementally.
Args:
labels: Labels object to convert.
format: Output format. One of "points", "instances", "frames", "multi_index".
chunk_size: Number of rows per chunk. If None (default), yields the entire
DataFrame in a single chunk (equivalent to `to_dataframe()`).
The meaning of "row" depends on the format:
- points: One point (node) per row
- instances: One instance per row
- frames: One frame per row
- multi_index: One frame per row
video: Optional video filter. If specified, only frames from this video
are included. Can be a Video object or integer index.
include_metadata: Include track, video information in columns.
include_score: Include confidence scores for predicted instances.
include_user_instances: Include user-labeled instances.
include_predicted_instances: Include predicted instances.
video_id: How to represent videos in the DataFrame. Options:
- "path": Full filename/path (default).
- "index": Integer video index.
- "name": Just the video filename.
- "object": Store Video object directly.
include_video: Whether to include video information.
instance_id: How to name instance columns in "frames" and "multi_index" formats.
- "index": Use inst0, inst1, inst2, etc. (default).
- "track": Use track names as column prefixes.
untracked: Behavior for untracked instances with instance_id="track".
- "error": Raise error if any instance lacks a track (default).
- "ignore": Skip untracked instances silently.
backend: "pandas" or "polars". Polars requires the polars package.
When using polars, DataFrames are constructed natively without
going through pandas, providing better performance for large datasets.
Yields:
DataFrames, each containing up to `chunk_size` rows.
Examples:
Process large datasets in chunks:
>>> for df_chunk in to_dataframe_iter(labels, chunk_size=10000):
... df_chunk.to_parquet("output.parquet", append=True)
Concatenate chunks to get full DataFrame (equivalent to to_dataframe):
>>> import pandas as pd
>>> df = pd.concat(list(to_dataframe_iter(labels, chunk_size=1000)))
Memory-efficient per-video processing:
>>> for video in labels.videos:
... for chunk in to_dataframe_iter(labels, video=video, chunk_size=5000):
... process_chunk(chunk)
"""
# Validate backend
if backend == "polars" and not HAS_POLARS:
raise ValueError(
"Polars backend requested but polars is not installed. "
"Install with: pip install polars"
)
# Normalize format parameter
if isinstance(format, str):
try:
format = DataFrameFormat(format.lower())
except ValueError:
valid_formats = ", ".join([f.value for f in DataFrameFormat])
raise ValueError(
f"Invalid format '{format}'. Must be one of: {valid_formats}"
)
# Filter to specific video if requested
if video is not None:
if isinstance(video, int):
video = labels.videos[video]
labeled_frames = [lf for lf in labels.labeled_frames if lf.video == video]
else:
labeled_frames = labels.labeled_frames
# Determine whether to include video info
if include_video is None:
include_video = len(labels.videos) > 1
# If no chunk_size specified, yield entire DataFrame at once
if chunk_size is None:
df = to_dataframe(
labels,
format=format,
video=video,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
video_id=video_id,
include_video=include_video,
instance_id=instance_id,
untracked=untracked,
backend=backend,
)
yield df
return
# Get the appropriate row iterator and DataFrame builder
if format == DataFrameFormat.POINTS:
row_iter = _iter_points_rows(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
)
elif format == DataFrameFormat.INSTANCES:
row_iter = _iter_instances_rows(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
)
elif format == DataFrameFormat.FRAMES:
# For frames format, we need to pre-scan for max_instances and tracks
max_instances, all_tracks, skeleton = _prescan_for_frames(
labels,
labeled_frames,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
instance_id=instance_id,
untracked=untracked,
)
row_iter = _iter_frames_rows(
labels,
labeled_frames,
include_metadata=include_metadata,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
instance_id=instance_id,
untracked=untracked,
max_instances=max_instances,
all_tracks=all_tracks,
skeleton=skeleton,
)
elif format == DataFrameFormat.MULTI_INDEX:
# For multi_index format, we also need to pre-scan
max_instances, all_tracks, skeleton = _prescan_for_frames(
labels,
labeled_frames,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
instance_id=instance_id,
untracked=untracked,
)
row_iter = _iter_multi_index_rows(
labels,
labeled_frames,
include_score=include_score,
include_user_instances=include_user_instances,
include_predicted_instances=include_predicted_instances,
include_video=include_video,
video_id=video_id,
instance_id=instance_id,
untracked=untracked,
max_instances=max_instances,
all_tracks=all_tracks,
skeleton=skeleton,
)
else:
raise ValueError(f"Unknown format: {format}")
# Buffer rows and yield DataFrames
buffer: list[dict] = []
yielded_any = False
for row in row_iter:
buffer.append(row)
if len(buffer) >= chunk_size:
# For multi_index with polars, flatten tuple keys
if format == DataFrameFormat.MULTI_INDEX and backend == "polars":
buffer = _flatten_tuple_keys(buffer)
df = _create_dataframe_from_rows(buffer, backend)
yield df
yielded_any = True
buffer = []
# Yield remaining rows (or empty DataFrame if no data)
if buffer or not yielded_any:
# For multi_index with polars, flatten tuple keys
if format == DataFrameFormat.MULTI_INDEX and backend == "polars":
buffer = _flatten_tuple_keys(buffer)
df = _create_dataframe_from_rows(buffer, backend)
yield df