Changelog¶
v0.6.1¶
sleap-io v0.6.1 Release Notes
Summary
This release completes the CLI vision from issue #209 with 8 new commands, bringing the total to 14 CLI commands. Major additions include video transformation with automatic coordinate adjustment (sio transform), flexible label merging (sio merge), and video reencoding for reliable seeking (sio reencode).
Highlights:
- 8 New CLI Commands:
merge,unsplit,fix,embed,unembed,trim,reencode,transform - New I/O Formats: CSV and SLEAP Analysis HDF5 format support
- Video FPS Support: Full round-trip FPS preservation through loading, saving, and reencoding
- Python 3.10+ Required: Minimum version bumped from 3.8 to 3.10
- Performance: 23x faster pkg.slp saves, 2.7x faster embedded video loading
Installation / Upgrade
# One-off CLI usage (no installation needed)
uvx [email protected] show labels.slp
# Install as CLI tool (new install)
uv tool install "sleap-io[all]"
# Upgrade existing CLI tool installation
uv tool upgrade sleap-io
# Add to project (new dependency)
uv add "sleap-io[all]"
# Upgrade existing project dependency
uv lock --upgrade-package sleap-io && uv sync
# Or with pip
pip install --upgrade "sleap-io[all]"See installation docs for more options.
Breaking Changes
Python 3.10+ Required (#322)
The minimum Python version has been bumped from 3.8 to 3.10 to align with optional dependencies (PyAV, Polars) and enable modern type hint syntax.
Action: Upgrade to Python 3.10 or later if you haven't already.
New CLI Commands
sio transform - Coordinate-Aware Video Transformations (#326)
Apply geometric transformations to videos while automatically adjusting landmark coordinates to maintain alignment.
# Crop to region of interest
sio transform labels.slp -o cropped.slp --crop 100,100,500,500
# Scale video and coordinates
sio transform labels.slp -o scaled.slp --scale 0.5
# Multiple transformations
sio transform labels.slp -o output.slp --crop 0,0,512,512 --rotate 90 --flip horizontal
# Per-video parameters via YAML config
sio transform labels.slp -o output.slp --config transforms.yaml
# Preview mode (show transformed dimensions without processing)
sio transform labels.slp --crop 100,100,400,400 --dry-runSupported transformations: --crop, --scale, --rotate, --pad, --flip
sio merge - Flexible Labels Merging (#317)
Merge multiple SLEAP files with full control over matching strategies.
# Basic merge
sio merge base.slp predictions.slp -o merged.slp
# Replace old predictions with new ones (keep manual labels)
sio merge project.slp new_preds.slp -o updated.slp --frame replace_predictions
# Merge with explicit matching strategies
sio merge base.slp other.slp -o out.slp --video path --track namesio unsplit - Merge Split Files (#313)
Reverse sio split by merging train/val/test files back into one.
# Merge from directory
sio unsplit splits/ -o merged.slp
# Merge specific files
sio unsplit train.slp val.slp test.slp -o merged.slpsio fix - Labels File Maintenance (#314)
Detect and repair common issues in SLEAP labels files.
# Show issues without fixing
sio fix labels.slp --dry-run
# Fix with safe defaults
sio fix labels.slp -o fixed.slp
# Fix specific issues
sio fix labels.slp -o fixed.slp --remove-empty-frames --consolidate-skeletons
# Update video paths
sio fix labels.slp -o fixed.slp --prefix /old/path /new/pathDetects: Duplicate videos, unused skeletons, empty frames, path issues.
sio embed / sio unembed - Granular Frame Embedding (#315)
Fine-grained control over frame embedding in package files.
# Embed only user-labeled frames
sio embed labels.slp -o labels.pkg.slp --user
# Embed user + predictions
sio embed labels.slp -o labels.pkg.slp --user --predictions
# Embed everything
sio embed labels.slp -o labels.pkg.slp --all
# Restore external video references
sio unembed labels.pkg.slp -o labels.slpsio trim - Clip Videos + Labels (#316)
Trim videos and labels to specific frame ranges.
# Trim labels and video
sio trim labels.slp -o clipped.slp --start 100 --end 500
# Trim standalone video
sio trim video.mp4 -o clip.mp4 --start 0 --end 1000sio reencode - Reliable Video Seeking (#319)
Reencode videos with frequent keyframes for frame-accurate seeking.
# Reencode video for reliable seeking
sio reencode video.mp4 -o reencoded.mp4
# Reencode all videos in a labels file
sio reencode labels.slp -o fixed_labels.slp
# Custom keyframe interval (default: every frame)
sio reencode video.mp4 -o out.mp4 --keyframe-interval 10Use case: Fixes videos where seeking to frame N returns frame N±1, which corrupts annotations.
New I/O Formats
CSV Format (#308)
import sleap_io as sio
# Load CSV (multiple formats supported)
labels = sio.load_csv("poses.csv", format="sleap")
labels = sio.load_csv("dlc_output.csv", format="dlc")
# Save to CSV
sio.save_csv(labels, "output.csv", format="points")Formats: sleap, dlc, points, instances, frames
CLI:
sio convert labels.slp -o poses.csv
sio convert poses.csv -o labels.slp --from csvSLEAP Analysis HDF5 Format (#309)
Read/write SLEAP's Analysis HDF5 format for MATLAB interoperability.
import sleap_io as sio
# Load analysis file
labels = sio.load_analysis("analysis.h5")
# Save with MATLAB-compatible axis ordering
sio.save_analysis(labels, "analysis.h5", preset="matlab")
# Custom axis ordering
sio.save_analysis(labels, "analysis.h5", axis_order=("time", "nodes", "coordinates", "tracks"))New Features
Video FPS Support (#307)
FPS is now a first-class property on Video objects with automatic extraction and round-trip preservation.
import sleap_io as sio
labels = sio.load_file("labels.slp")
video = labels.videos[0]
# Access FPS
print(video.fps) # e.g., 30.0
# Set FPS (useful for image sequences)
video.fps = 25.0
# Convert frame to timestamp
timestamp = video.frame_to_time(100) # Returns time in secondsEnhanced sio show (#310, #311, #330)
# Clearer instance counts (user vs predicted)
sio show labels.slp
# View video encoding info for standalone videos
sio show video.mp4
# Output includes: codec, pixel format, FPS, bitrate, GOP size
# Inspect video provenance chain
sio filenames labels.slp --all
sio filenames labels.slp --original
sio filenames labels.slp --sourceAutomatic Embedded Video Preservation (#328)
CLI commands now automatically preserve embedded videos when converting between .pkg.slp files:
# Embedded frames are preserved automatically (no --embed needed)
sio convert input.pkg.slp -o output.pkg.slp
sio merge base.pkg.slp other.pkg.slp -o merged.pkg.slp
sio fix input.pkg.slp -o fixed.pkg.slpPerformance Improvements
23x Faster pkg.slp Saves (#327)
Saving embedded videos is now dramatically faster by copying raw encoded bytes directly when formats match.
| Operation | Before | After | Speedup |
|---|---|---|---|
| Save pkg.slp with embedded video | 23s | 1s | 23x |
2.7x Faster Embedded Video Loading (#310)
Loading .pkg.slp files with many embedded videos is now significantly faster by avoiding repeated file opens.
Bug Fixes
Fix Video Matching for Embedded Videos (#323)
Fixed "Frame index out of range" errors when merging .pkg.slp files with sio unsplit --embed. Videos are now compared by HDF5 dataset identity, not just filename.
Fix Rendering Crop Offset (#331)
RenderContext.world_to_canvas() now returns correct coordinates when using the crop parameter.
Fix Embedded Video Data Loss (#332)
Fixed critical bug where loading .pkg.slp files with open_videos=False would lose all embedded frames on save. Embedded videos are now detected and preserved via backend metadata.
Fix x264 Coordinate Alignment (#333)
Frames are now padded on bottom/right edges only (instead of scaling) when encoding x264 videos with dimensions not divisible by 16, preserving keypoint coordinate accuracy.
Improvements
Modernized Type Hints (#325)
All type hints updated to Python 3.10+ syntax:
Union[X, Y]→X | YOptional[X]→X | NoneList,Dict→list,dict
Video.original_video Refactor (#312)
Video.original_video is now a computed property that traverses the source_video chain, eliminating redundant HDF5 storage while maintaining backward compatibility.
Documentation (#318, #320, #321, #324)
- Comprehensive SLP file format reference (~400 lines)
- Per-PR isolated docs previews
- Cleaner CLI docs navigation
Changelog
- #307: feat: Add FPS video property support (@talmo)
- #308: feat: Add formal CSV I/O support (@talmo)
- #309: feat: Add Analysis HDF5 format I/O support (@talmo)
- #310: feat: Enhance sio show command and fix load_slp performance for embedded videos (@talmo)
- #311: feat: Enhance sio filenames command and fix Video.filename consistency (@talmo)
- #312: refactor: Make original_video a computed property from source_video chain (@talmo)
- #313: feat: Add sio unsplit command to merge split labels files (@talmo)
- #314: feat: Add sio fix command for labels file maintenance (@talmo)
- #315: feat: Add sio embed/unembed commands for granular frame embedding (@talmo)
- #316: feat: Add sio trim command for clipping videos + labels (@talmo)
- #317: feat: Add sio merge command for flexible labels merging (@talmo)
- #318: docs: Add comprehensive SLP file format reference (@talmo)
- #319: feat: Add sio reencode command for reliable video seeking (@talmo)
- #320: feat: Add PR-local docs preview deployment (@talmo)
- #321: feat: Enhance docs preview with changed page links and markdown sources (@talmo)
- #322: chore: Bump minimum Python version from 3.8 to 3.10 (@talmo)
- #323: fix: Compare HDF5 datasets in video matching to prevent wrong video assignment (@talmo)
- #324: docs: Improve CLI docs navigation structure (@talmo)
- #325: refactor: Modernize type hints to Python 3.10+ syntax (@talmo)
- #326: feat: Add sio transform CLI command for coordinate-aware video transformations (@talmo)
- #327: perf: Add fast path for embedded video saving in pkg.slp files (@talmo)
- #328: feat(cli): Preserve embedded videos by default in pkg.slp to pkg.slp operations (@talmo)
- #330: feat(cli): Show video encoding info in sio show for standalone videos (@talmo)
- #331: fix(rendering): Pass crop_offset to RenderContext in callbacks (@talmo)
- #332: fix(slp): Preserve embedded videos when loaded with open_videos=False (@talmo)
- #333: fix(video_writing): Pad frames to macro_block_size=16 for x264, bottom/right only (@talmo)
- #334: chore: Bump version to 0.6.1 (@talmo)
Full Changelog: v0.6.0...v0.6.1
v0.6.0¶
Summary
This release transforms sleap-io into a comprehensive pose data toolkit with three major new capabilities: a CLI overhaul with 4 new commands, a high-performance pose rendering module, and an in-memory codecs package for seamless data analysis workflows. Additionally, lazy loading delivers ~90x faster SLP file operations for large prediction files.
Highlights:
- CLI Overhaul: 6 commands, 166 tests, comprehensive documentation - a full-featured command-line tool
- Rendering: Publication-ready pose videos at ~50 FPS with skia-python
- Codecs: Convert Labels to/from Dict, NumPy, and DataFrame (pandas/polars)
- Lazy Loading: ~90x faster loading for large SLP files
Thanks to @tom21100227 for contributing the standard color palette (#301)!
Breaking Changes
Simplified Merge API (#300)
The Labels.merge() API has been redesigned for safety and simplicity.
Parameter names simplified:
| Old (0.5.x) | New (0.6.0) |
|---|---|
skeleton_matcher= |
skeleton= |
video_matcher= |
video= |
track_matcher= |
track= |
frame_strategy= |
frame= |
instance_matcher= |
instance= |
Default frame strategy renamed: "smart" → "auto"
String arguments now accepted: No imports needed for simple cases.
# Old API (0.5.x)
from sleap_io.model.matching import VideoMatcher, VideoMatchMethod
base.merge(predictions, video_matcher=VideoMatcher(method=VideoMatchMethod.PATH), frame_strategy="smart")
# New API (0.6.0) - simple
base.merge(predictions) # uses auto defaults
# New API (0.6.0) - explicit
base.merge(predictions, video="path", frame="auto")Removed Unused APIs (#302)
The following unused APIs were removed during a post-merge audit:
| Removed | Reason |
|---|---|
FrameMatcher class |
Never used - frames are uniquely identified by (video, frame_idx) |
SOURCE_VIDEO_MATCHER constant |
Identical to BASENAME_VIDEO_MATCHER |
VideoNotFoundError exception |
Defined but never raised |
Code importing these will need to remove the imports. These were dead code with no production usage.
Performance Improvements
Lazy Loading for SLP Files (#296)
Load large prediction files almost instantly with the new lazy=True parameter. Object creation is deferred until needed, enabling fast workflows for analysis and CLI operations.
| Scenario | Eager | Lazy | Speedup |
|---|---|---|---|
| Load only | 0.47s | 0.005s | ~90x |
| Load + numpy() | 0.86s | 0.38s | ~2x |
| Load + to_dataframe() | 0.13s | 0.09s | ~1.4x |
sio show CLI |
0.84s | 0.36s | ~2.3x |
Benchmarks on 18,000 frames with ~40,000 instances.
import sleap_io as sio
# Fast loading for analysis workflows
labels = sio.load_slp("predictions.slp", lazy=True)
print(labels.is_lazy) # True
# Fast stats (O(1) - no iteration needed)
print(labels.n_pred_instances) # Instant count
# Fast numpy/DataFrame export - no Instance objects created
arr = labels.numpy()
df = labels.to_dataframe(format="points")
# Materialization for modification
eager = labels.materialize()
eager.append(new_frame) # Now worksCLI: sio show uses lazy loading by default for SLP files.
sio show predictions.slp # Fast (lazy)
sio show predictions.slp --no-lazy # Force eagerImpact: Enables instant CLI startup and interactive workflows with large prediction files.
New Features
CLI: New Commands (#280, #285, #286, #288)
The sleap-io CLI receives a major upgrade with 4 new commands and comprehensive documentation at io.sleap.ai.
sio convert - Format Conversion
Convert between 9+ pose data formats with automatic format detection.
# Basic conversion (formats inferred from extensions)
sio convert labels.slp -o labels.nwb
# Explicit format for ambiguous inputs
sio convert annotations.json -o labels.slp --from coco
# Embed frames in output
sio convert labels.slp -o labels.pkg.slp --embed userSupported formats: slp, nwb, coco, labelstudio, alphatracker, jabs, dlc, ultralytics, leap
sio split - Dataset Splitting
Create reproducible train/val/test splits for machine learning workflows.
# Default 80/20 train/val split
sio split labels.slp -o splits/
# Three-way split with seed for reproducibility
sio split labels.slp -o splits/ --train 0.7 --val 0.15 --test 0.15 --seed 42
# Embed user-labeled frames for portable training data
sio split labels.slp -o splits/ --embed user --seed 42Output: train.slp, val.slp, test.slp (or .pkg.slp with --embed)
sio filenames - Video Path Management
Inspect and update video paths when moving projects between systems.
# Inspection mode - list all video paths
sio filenames labels.slp
# Update mode - replace prefixes (cross-platform)
sio filenames labels.slp -o fixed.slp --prefix /old/path /new/pathsio render - Pose Visualization
Render publication-ready videos and images with pose overlays.
# Video rendering
sio render predictions.slp -o output.mp4
sio render predictions.slp --preset preview # Fast 0.25x
# Single frame
sio render predictions.slp --frame 42
# Styling
sio render predictions.slp --color-by track --palette tableau10
# Render without source video (solid background)
sio render predictions.slp --background blackCLI: Improvements (#279, #281, #292, #298, #303)
Enhanced sio show:
- Video index parameter:
sio show labels.slp -v 2 - Standalone video file inspection:
sio show recording.mp4 - Full absolute paths for easy copy-paste
- Plugin status in
--versionoutput - Solarized theme for clean appearance
Consistent input handling: All commands now accept input files both as positional arguments AND via -i/--input:
# Both forms work identically for all commands
sio show labels.slp
sio show -i labels.slp
sio convert labels.slp -o out.nwb
sio convert -i labels.slp -o out.nwbAdditional improvements:
-hworks as alias for--helpon all commands- Color/palette discovery:
sio render --list-colors,sio render --list-palettes - Clear error messages for conflicting inputs
- 166 CLI tests for comprehensive coverage
Pose Rendering Module (#288)
New sleap_io.rendering module for high-performance pose visualization using skia-python (~50 FPS for 1024x1024 frames).
import sleap_io as sio
# Render video with pose overlays
sio.render_video(labels, "output.mp4")
# Quick preview at reduced resolution
labels.render("preview.mp4", preset="preview")
# Single frame with custom styling
sio.render_image(
labeled_frame,
"frame.png",
color_by="track",
palette="tableau10",
marker_shape="diamond"
)
# Render to numpy array
img = sio.render_image(labeled_frame)Capabilities:
| Feature | Options |
|---|---|
| Color schemes | track, instance, node, auto |
| Palettes | 9 built-in + 200+ via colorcet; standard default (MATLAB colors) |
| Marker shapes | circle, square, diamond, triangle, cross |
| Quality presets | preview (0.25x), draft (0.5x), final (1.0x) |
| Background | video frame, solid color, or transparent |
Advanced features:
- Cropping with pixel or normalized coordinates
- Custom callbacks for overlays (labels, frame info, etc.)
- Progress tracking and cancellation
Impact: Publication-ready pose videos without requiring the SLEAP GUI.
Codecs Package for In-Memory Serialization (#290)
New sleap_io.codecs package for flexible conversion between Labels and various in-memory representations.
Three codecs:
| Codec | Methods | Use Case |
|---|---|---|
| Dictionary | to_dict(), from_dict() |
JSON serialization, web APIs |
| NumPy | numpy(), from_numpy() |
ML pipelines, signal processing |
| DataFrame | to_dataframe(), from_dataframe() |
Tabular analysis, export |
DataFrame formats:
# One row per point (most normalized)
df = labels.to_dataframe(format="points")
# One row per instance (ML-ready)
df = labels.to_dataframe(format="instances")
# One row per frame (time-series)
df = labels.to_dataframe(format="frames")
# Hierarchical columns (NWB-compatible)
df = labels.to_dataframe(format="multi_index")Backend support:
# Pandas (default)
df = labels.to_dataframe(backend="pandas")
# Native polars (faster for large datasets)
df = labels.to_dataframe(backend="polars")
# Streaming for memory efficiency
for chunk in labels.to_dataframe_iter(chunk_size=10000):
process(chunk)Impact: Seamless integration with pandas/polars/numpy analysis pipelines.
Labels.copy() Method (#289)
Deep copy Labels with control over video backend behavior.
# Default: preserves each video's current open_backend setting
labels_copy = labels.copy()
# Prevent file handles (useful for batch processing)
labels_copy = labels.copy(open_videos=False)
# Force all videos to auto-open
labels_copy = labels.copy(open_videos=True)Non-mutating save: Save operations no longer mutate the original Labels by default.
# Original labels are NOT modified (default, safer)
labels.save("output.pkg.slp", embed="user")
# With embed_inplace=True: original labels ARE modified (faster)
labels.save("output.pkg.slp", embed="user", embed_inplace=True)NWB Multisubjects Support (#273)
Export multi-animal pose data to NWB with proper subject linkage using the ndx-multisubjects extension.
from sleap_io.io.nwb_annotations import save_labels
# Basic multi-subject export
save_labels(labels, "output.nwb", use_multisubjects=True)
# With detailed subject metadata
subjects_metadata = [
{"sex": "M", "species": "Mus musculus", "age": "P30D"},
{"sex": "F", "species": "Mus musculus", "age": "P45D"},
]
save_labels(
labels,
"output.nwb",
use_multisubjects=True,
subjects_metadata=subjects_metadata
)Impact: Proper multi-animal NWB export for neuroscience workflows.
replace_predictions Merge Strategy (#278)
New merge strategy for re-running inference while preserving manual corrections.
# Load project with existing predictions
project = sio.load_file("project.slp")
# Run new inference
new_preds = sio.load_file("new_predictions.slp")
# Replace old predictions, keep all manual labels
project.merge(new_preds, frame="replace_predictions")Behavior:
- Keeps all user instances from base
- Removes all predictions from base
- Adds only predictions from other (ignores user instances from other)
- No spatial matching (clean replacement)
Safe AUTO Video Matching (#300)
Redesigned video matching algorithm for Labels.merge() that prevents silent data corruption. False positives (matching wrong videos) corrupt data irreversibly; false negatives (adding as new) are easily recoverable.
The AUTO cascade:
| Step | Check | Result |
|---|---|---|
| 1-2 | Shape rejection | Different (frames, H, W) → reject |
| 3 | Provenance conflict | Different original_video → reject |
| 4 | Physical file identity | os.path.samefile() → match |
| 5 | Exact path string | Sanitized paths equal → match |
| 6 | Leaf uniqueness | Minimal unique suffixes match → match |
| 7 | Fallback | Add as new video |
Key scenarios:
- PKG.SLP predictions → external video: Works via provenance chain traversal
- Cross-platform paths (Windows ↔ Linux): Works via leaf path uniqueness
- Same basename, different content (fly.mp4 with 1000 vs 500 frames): Rejected by shape mismatch
New helper: Labels.add_video() prevents duplicate video addition.
Progress Callback for Frame Embedding (#283)
Optional callback for GUI applications during frame embedding operations.
def my_progress(current, total):
print(f"Embedding frame {current}/{total}")
return True # Return False to cancel
sio.save_file(
labels,
"output.pkg.slp",
embed="user",
progress_callback=my_progress
)Features:
- 1-based indexing for intuitive display
- Cancellation support via
ExportCancelledexception - Automatic tqdm disabling when callback provided
Impact: GUI integration with progress bars and cancellation support.
Bug Fixes
Fix Empty Embedded Video References (#282)
Videos without labeled frames are now properly converted to embedded references when exporting package files (.pkg.slp).
Problem: Videos with no labels retained external paths, causing "missing files" errors on other machines.
Solution: All videos are converted to embedded references by default. Use embed_all_videos=False for selective embedding.
Impact: Package files work correctly across machines even when some videos have no labeled frames.
Fix Video Deep Copy Losing Provenance (#302)
Video.__deepcopy__() now preserves the original_video attribute, fixing a critical bug where the provenance chain would break during merge operations.
Impact: Merge operations now correctly track video provenance through the entire chain.
Improvements
imageio-ffmpeg as Core Dependency (#287)
imageio-ffmpeg is now a core dependency, so video operations work out of the box.
# Video operations now work immediately
uvx sleap-io convert labels.slp -o out.pkg.slp --embed user
# No more "no video backend" errors
sio show labels.slp -v # Works without extra installsImpact: Zero-config video support for all users.
Enhanced Merge Provenance Tracking (#299)
Merge operations now record additional metadata for better audit trails:
# After merging predictions.slp into labels.slp
labels.provenance["merge_history"][-1]
# {
# "timestamp": "2025-01-07T14:30:00.123456",
# "source_filename": "predictions.slp",
# "target_filename": "labels.slp",
# "sleap_io_version": "0.6.0",
# "source_labels": {"n_frames": 100, ...},
# "result": {"frames_merged": 100, "instances_added": 500}
# }New fields: source_filename, target_filename, sleap_io_version
Impact: Better data lineage tracking for reproducibility and auditing.
Changelog
- #273: Add NWB Multisubjects support (@talmo)
- #278: Add replace_predictions merge strategy and rewrite merging docs (@talmo)
- #279: Add CLI theming and enhanced version info (@talmo)
- #280: Add CLI convert command for format conversion (@talmo)
- #281: Redesign CLI cat video display with defensive metadata handling (@talmo)
- #282: Fix empty embedded video references for package export (@talmo)
- #283: Add progress_callback support for frame embedding (@talmo)
- #284: Add CLI documentation (@talmo)
- #285: Add CLI split command for train/val/test splits (@talmo)
- #286: Add CLI filenames command for inspecting/updating video paths (@talmo)
- #287: Add imageio-ffmpeg as core dependency for video support (@talmo)
- #288: Add skia-python rendering module for pose visualization (@talmo)
- #289: Add Labels.copy() method with open_videos parameter (@talmo)
- #290: Add codecs package for in-memory serialization (@talmo)
- #292: Enhance CLI show command with video index, full paths, and standalone video display (@talmo)
- #293: Add comprehensive installation documentation page (@talmo)
- #294: Bump version to 0.6.0 (@talmo)
- #296: Add lazy loading for SLP files (@talmo)
- #297: Update documentation for v0.6.0 release (@talmo)
- #298: Standardize CLI patterns and add render enhancements (@talmo)
- #299: Add source/target filenames and version to merge provenance (@talmo)
- #300: Implement safe AUTO video matching algorithm for merges (@talmo)
- #301: Add standard palette with MATLAB default colors (@tom21100227)
- #302: Fix Video.deepcopy() and remove dead code from matching module (@talmo)
- #303: Standardize CLI to support both positional and -i flag input (@talmo)
- #304: Add missing documentation for v0.6.0 features (@talmo)
- #305: Add CI summary job to support docs-only PRs (@talmo)
- #306: Update version examples in install.md to 0.6.0 (@talmo)
Full Changelog: v0.5.8...v0.6.0
v0.5.8¶
sleap-io v0.5.8
🎯 Summary
This release delivers a dramatic performance improvement with 2000x faster imports through lazy loading, makes imageio-ffmpeg optional to reduce installation size, and includes multiple critical bug fixes for video indexing and matching in SLP files. The v0.5.8 release focuses on reducing friction for users while improving reliability for complex video handling scenarios.
⚡ Performance Improvements
Implement Lazy Loading for 2000x Faster Imports (#270)
Dramatically reduced import time using the lazy-loader library (SPEC 1 standard used by NumPy, SciPy, scikit-image).
Performance Results:
| Metric | Before | After | Improvement |
|---|---|---|---|
| Import time | 4.38s | 0.0022s | 1991x faster |
| Target | <500ms | 2.2ms | 227x better than target |
What's deferred:
- pandas (2.24s) - loads only when
load_dlc()is called - PyAV (0.67s) - loads only when video is opened
- NWB tools (0.53s) - loads only when
load_nwb()is called - All format modules - load on first use
# Before: 4.38s to import
# After: 0.0022s to import
import sleap_io
# Functions available immediately (lazy loading is transparent)
labels = sleap_io.load_slp("file.slp")
# First call to load_dlc() imports pandas (one-time ~2s cost)
labels = sleap_io.load_dlc("file.csv")
# Subsequent calls are instant (pandas already cached)
labels = sleap_io.load_dlc("file2.csv")Key Features:
- ✅ Zero API changes - Users import and use sleap-io exactly as before
- ✅ Battle-tested - Uses
lazy-loaderlibrary from SPEC 1 - ✅ Type-safe - Works with mypy/pyright
- ✅ Test coverage -
EAGER_IMPORT=1fixture ensures tests catch missing imports - ✅ Lower memory footprint - ~30-40% reduction
Impact: Instant CLI startup and dramatically improved user experience, especially for quick scripts and interactive workflows.
✨ New Features
Make imageio-ffmpeg Optional and Enhance Backend Plugin System (#272)
Made imageio-ffmpeg an optional dependency and added new introspection APIs for better discoverability.
New Optional Dependency Groups:
pip install sleap-io[ffmpeg] # Recommended for video support
pip install sleap-io[all] # All backends
pip install sleap-io # Minimal (no video backends)New Public API Functions:
import sleap_io as sio
# Check what's available
print(sio.get_available_video_backends())
# Output: ['FFMPEG', 'pyav']
print(sio.get_available_image_backends())
# Output: ['opencv', 'imageio']
# Get installation help
print(sio.get_installation_instructions("opencv"))
# Output: pip install sleap-io[opencv]Enhanced Error Messages:
Before:
ImportError: No video plugins found. Install opencv-python, imageio-ffmpeg, or av.
After:
ImportError: No video backend plugins are installed.
Available options:
opencv (fastest): pip install sleap-io[opencv]
FFMPEG (most reliable): pip install sleap-io[ffmpeg]
pyav (balanced): pip install sleap-io[pyav]
all backends: pip install sleap-io[all]
For more information, see: https://io.sleap.ai
Additional Features:
- Smart warnings when preferred backend is not available
- Automatic fallback to auto-detection
- Updated documentation with new installation options
Impact: Reduces installation footprint and provides better guidance for users setting up video backends.
🐛 Bug Fixes
Fix Video Matching to Prioritize source_filename for HDF5 Backends (#275)
Fixed video matching for .pkg.slp files where multiple videos share the same HDF5 file path but reference different source videos.
Problem: When merging Labels with HDF5 video backends (embedded videos), Video.matches_path() would incorrectly match different videos just because they came from the same HDF5 file.
Solution: For HDF5 backends, matching now prioritizes:
source_filename(the original video path before embedding)- Falls back to
datasetname ifsource_filenameisNone - Returns
Falseif neither is available (avoids false positives)
# After fix: Correct matching for embedded videos
labels1 = sio.load_slp("project1.pkg.slp")
labels2 = sio.load_slp("project2.pkg.slp")
# Videos now match by original source filename, not HDF5 path
labels1.merge(labels2) # ✅ Correct video matchingImpact: Critical fix for workflows involving merged predictions with embedded videos.
Fix Video ID Mapping for Sequential IDs with Sparse Dataset Names (#274)
Fixed loading of SLP files exported from larger .pkg.slp files where embedded video datasets have sparse names but sequential frame video IDs.
Problem: When SLP files are exported (e.g., via "Export Labeled Clip..."), the embedded video datasets retain sparse naming (e.g., video51/video, video49/video) but frame video IDs may be sequential (0, 1, 2, 3). This caused incorrect video-frame associations.
Solution: Added detection logic to determine if frame video IDs are sequential list indices or sparse embedded IDs, and apply the appropriate mapping.
Impact: Fixes data integrity issues when working with exported clips from larger projects.
Fix Sparse Video Indexing While Writing SLP Files (#268)
Fixed preservation of sparse video indexing when writing and re-reading SLP files with embedded videos.
Problem: When saving labels with sparse video indices (e.g., videos indexed as 0, 5, 10, 15, 20), the video IDs were incorrectly mapped to sequential indices, causing data loss or misalignment on reload.
Solution: Extract original video IDs from HDF5 dataset names and use them when writing frame data.
Impact: Ensures data integrity for round-trip operations with sparse video indices.
Fix Sparse Video Indexing Bug in read_labels() (#266)
Fixed loading of .slp files with sparse video indices from old SLEAP versions.
Problem: Old SLEAP versions (format_id < 2.0) could create files where video IDs in the frames dataset were sparse (e.g., 0, 15, 29, 47, ...), causing IndexError when loading.
Solution: Build a video_id_to_index mapping from sparse video IDs to sequential list indices when loading.
import sleap_io as sio
# Now works correctly
labels = sio.load_slp("legacy_file.slp") # ✅ No more IndexError
assert len(labels.videos) == 5
assert len(labels) == 10Impact: Restores compatibility with legacy SLEAP files that have non-sequential video IDs.
Fix KeyError When backend_metadata Lacks Filename Key (#267)
Fixed loading of SLP files where backend_metadata is missing the "filename" key, particularly when upgrading from SLEAP v1.4 to v1.5+.
Solution: Added fallback chain to handle legacy files and ensure "filename" is always present when writing.
Impact: Improves compatibility with older SLEAP project files.
💡 Why These Changes Matter
The v0.5.8 release significantly enhances sleap-io's performance, reliability, and ease of use:
- Instant Imports: 2000x faster import times make sleap-io feel snappy for scripts, notebooks, and CLI tools
- Flexible Installation: Optional video backends let users install only what they need, reducing dependencies and installation size
- Better Discoverability: New introspection APIs and improved error messages help users configure their environment
- Data Integrity: Five bug fixes for video indexing and matching ensure reliable handling of complex video scenarios
- Legacy Compatibility: Improved support for older SLEAP file formats and upgrade paths
This release demonstrates sleap-io's commitment to developer experience, reliability, and backwards compatibility for pose tracking research workflows.
📋 Changelog
- #266: Fix sparse video indexing bug in read_labels() (@talmo)
- #267: Fix KeyError When backend_metadata Lacks Filename Key (@alicup29)
- #268: Fix sparse video indexing while writing slp files (@gitttt-1234)
- #269: Bump version from 0.5.7 to 0.5.8 (@talmo)
- #270: Implement lazy loading to improve import performance (@talmo)
- #271: Add investigation skill for empirical experimentation (@talmo)
- #272: Make imageio-ffmpeg optional and enhance backend plugin system (@talmo)
- #274: Fix video ID mapping for sequential IDs with sparse dataset names (@gitttt-1234)
- #275: Fix video matching to prioritize source_filename for HDF5 backends (@gitttt-1234)
Full Changelog: v0.5.7...v0.5.8
v0.5.7¶
sleap-io v0.5.7
🎯 Summary
This release delivers major format compatibility improvements, critical video matching fixes, and enhanced developer tooling. The v0.5.7 release adds COCO format export for seamless integration with mmpose and other COCO-compatible tools, fixes critical video matching bugs that affected multi-video projects, resolves path expansion issues in video existence checking, and modernizes coverage testing to match Codecov's branch detection capabilities.
✨ New Features
Add COCO Format Export Functionality (#260)
Implemented comprehensive COCO format export capabilities, enabling seamless integration with mmpose, CVAT, and other COCO-compatible pose estimation tools.
Key Features:
Export Functions:
encode_keypoints(): Convert numpy points to COCO keypoint formatconvert_labels(): Transform Labels to COCO JSON structurewrite_labels(): Save COCO JSON annotation filessave_coco(): Main API function for easy accesssave_file(): Updated to auto-detect and handle COCO format
COCO Standard Compliance:
- Bounding boxes: Automatically computed from visible keypoints in
[x, y, width, height]format - Area field: Computed from bounding box dimensions
- iscrowd field: Set to 0 for all annotations (standard requirement)
- Keypoints: Flat list format
[x1, y1, v1, x2, y2, v2, ...] - Skeleton edges: 1-based indexing as per COCO spec
- Visibility encoding: Support for both binary (0/1) and ternary (0/1/2)
Advanced Features:
- Multiple skeletons/categories support
- Tracking via
attributes.object_id(CVAT-compatible) - Custom image filename generation
- NaN coordinate handling for unlabeled keypoints
- Roundtrip conversion (read → write → read)
import sleap_io as sio
# Load SLEAP labels
labels = sio.load_slp("annotations.slp")
# Export to COCO format
sio.save_coco(labels, "annotations.json")
# Or use save_file with auto-detection
sio.save_file(labels, "annotations.json") # Auto-detects COCO from .json extension
# COCO files are now compatible with mmpose, CVAT, and other toolsmmpose Compatibility:
This implementation was validated against mmpose's BaseCocoStyleDataset and AP10KDataset to ensure full compatibility:
✅ Required fields: All mmpose-required fields present (bbox, keypoints, area, iscrowd)
✅ Bbox format: Correct COCO format [x, y, width, height] computed from visible keypoints
✅ Skeleton indexing: 1-based edge indices as expected
✅ Validation: Handles edge cases (zero keypoints, all NaN points)
✅ Tested: Comprehensive test suite with 55 tests including mmpose-specific scenarios
Testing:
- 55 total tests in
tests/io/test_coco.py - New tests for bbox, area, and iscrowd fields
- Edge case testing (NaN points, zero keypoints)
- Roundtrip conversion verification
- Integration tests via main API
- All tests pass with no regressions in the full test suite (415 I/O tests)
Files Modified:
sleap_io/io/coco.py: Complete COCO export implementation (+808 lines)sleap_io/io/main.py: Integrate COCO export into save_file()sleap_io/__init__.py: Export save_coco functiontests/io/test_coco.py: Comprehensive test suite with 55 tests
Impact: Enables seamless integration with the broader pose estimation ecosystem, allowing researchers to export SLEAP annotations to mmpose, CVAT, and other COCO-compatible tools. This significantly expands sleap-io's interoperability and supports diverse research workflows.
🐛 Bug Fixes
Fix AUTO Video Matching to Prefer Basename Over Content Matches (#261)
Resolved a critical bug where multiple videos with identical shapes would incorrectly match by content instead of basename during merge operations.
Problem: When merging predictions back into a project with multiple videos that have identical shapes (common in experimental setups), the AUTO video matcher would incorrectly match videos:
- Before: Predictions for
video_b.mp4would match tovideo_a.mp4(first video with same shape) - After: Predictions correctly match to
video_b.mp4(same basename)
Root Cause: The merge loop's "first match wins" behavior combined with AUTO's content fallback would break on the first content match, even when a better basename match existed later in the list.
Solution: Two-part fix:
-
Updated AUTO method (
matching.py) to try matching in order of specificity:- Strict path match (exact resolved paths)
- Lenient path match (basenames)
- Content match (shape + backend) - only as last resort
-
Added smart matching logic (
labels.py) for AUTO method in merge loop:- Collects all potential matches across all videos
- Categorizes by quality (strict path > basename > content-only)
- Picks best match instead of "first match wins"
from sleap_io import Labels
# Project with multiple videos of same shape
labels = Labels.load("project.slp")
predictions = Labels.load("predictions.slp")
# Merge now correctly matches by basename, not just content
labels.merge(predictions) # ✅ Correctly matches video_b.mp4 → video_b.mp4Testing:
- ✅ New regression test added:
test_merge_auto_video_matching_with_identical_shapes - ✅ All 54 matching tests pass (no regressions)
- ✅ All 14 merging integration tests pass (no regressions)
Files Modified:
sleap_io/io/matching.py: Updated AUTO matching prioritysleap_io/model/labels.py: Added smart matching logic for merge looptests/model/test_labels.py: Added regression test
Impact: Critical fix for multi-video projects with identical video dimensions, ensuring predictions are merged with the correct video files. This is essential for experimental setups where multiple videos have the same resolution and frame rate. Fixes issue #255.
Update Video.filename When VideoBackend.filename Is Expanded (#263)
Fixed a bug where Video.exists() would incorrectly return False even when the backend successfully opened a video file with an expanded path.
Problem: When opening a video with a relative path or user path (e.g., ~/video.mp4), the backend would expand it to an absolute path, but Video.filename would retain the original unexpanded path. This caused Video.exists() to check the wrong path and return False even though the video was successfully loaded.
from sleap_io import Video
# Before fix
video = Video.from_filename("~/data/video.mp4")
# Backend opens: /Users/name/data/video.mp4
# Video.filename remains: ~/data/video.mp4
video.exists() # ❌ False (checking wrong path)
# After fix
video = Video.from_filename("~/data/video.mp4")
# Backend opens: /Users/name/data/video.mp4
# Video.filename updated to: /Users/name/data/video.mp4
video.exists() # ✅ True (checking correct path)Solution: Updated Video.from_filename() to synchronize Video.filename with the backend's expanded filename after opening, ensuring consistency between the video object and its backend.
Files Modified:
sleap_io/model/video.py: UpdateVideo.filenameafter backend initializationtests/model/test_video.py: Added test for path expansion consistency
Impact: Ensures Video.exists() and other path-dependent operations work correctly when using relative paths, user paths, or symlinks. Fixes issue #262.
🔧 Improvements
Improve Coverage Testing to Detect Partial Lines Matching Codecov (#264)
Modernized coverage testing workflow to detect both missed and partial lines, matching what Codecov shows in PR reviews.
Problem: The old coverage annotate approach could only detect missed lines, but not partially covered lines (executed code with missing branch coverage). This meant development tools couldn't see the same gaps that Codecov highlights in yellow during PR reviews.
Solution: Switched from coverage annotate parsing to coverage.xml parsing with branch data, enabling detection of both missed and partial coverage.
Key Changes:
1. Updated Coverage Script (scripts/cov_summary.py)
- Before: Parsed
.py,coverfiles fromcoverage annotate(missed lines only) - After: Parses
coverage.xmlwith branch data (missed + partial lines) - Detects partial lines from XML
condition-coverageattribute - Supports multiple output formats: text, markdown, json, gh-annotations
- Can filter to PR-changed lines using
gh pr diff
2. Created Coverage Skill (.claude/skills/coverage/)
- Comprehensive 200+ line guide for coverage analysis and improvement
- Includes bundled copy of
cov_summary.pyscript - Step-by-step workflow with real examples
- Auto-discovered by Claude Code when working on coverage tasks
- Replaces old
.claude/commands/coverage.mdcommand
3. Configuration Updates
- pyproject.toml: Added
relative_files = truefor cross-OS path stability - CI workflow: Added PR summary step showing coverage table for changed files
Example Output:
Before (text only, missed lines):
sleap_io/io/leap.py: 105,109,148
After (markdown table, missed + partial):
| File | Missed | Partial |
| --- | --- | --- |
| io/leap.py | — | 105,109,148 |
| io/coco.py | 122-124,518 | 82,84,87,121,279 |This shows io/leap.py has full line coverage but incomplete branch coverage, while io/coco.py has both gaps.
Why This Matters:
Coverage XML includes branch information (condition-coverage="50% (1/2)"), allowing detection of:
- Missed: Lines with 0 hits
- Partial: Lines with hits > 0 but incomplete branch coverage
Now development tools see exactly what Codecov shows, making targeted test improvement possible.
Design Decisions:
Why XML instead of JSON?
- Codecov definitely supports XML (Cobertura format)
- XML is proven to work with existing CI setup
- Both formats contain the same branch information
Why include script in skill?
- Makes skill self-contained
- Follows Claude Skills pattern for bundling resources
- Agent can reference it without context pollution
Files Modified:
scripts/cov_summary.py: Rewritten to parse XML instead of annotate files (+669, -361).claude/skills/coverage/: New coverage analysis skillpyproject.toml: Addedrelative_files = true.github/workflows/test.yml: Added PR coverage summary step
Impact: Enables more precise test coverage improvement by showing exactly which branches aren't covered, matching Codecov's analysis. This improves development workflow and helps maintain high code quality.
💡 Why These Changes Matter
The v0.5.7 release significantly enhances sleap-io's ecosystem integration, reliability, and developer experience:
- Ecosystem Expansion: COCO format export enables seamless integration with mmpose, CVAT, and the broader pose estimation ecosystem, expanding research workflows and tool compatibility
- Data Integrity: Fixed video matching ensures predictions merge with correct videos in multi-video projects, preventing silent data corruption in experimental pipelines
- Path Reliability: Video filename synchronization fixes path-dependent operations when using relative paths or symlinks
- Developer Tooling: Improved coverage testing matches Codecov's branch detection, enabling more precise test improvement and maintaining code quality
This release demonstrates sleap-io's continued commitment to interoperability, reliability, and developer productivity for pose tracking research workflows.
📋 Changelog
- #260: Add COCO format export functionality (@talmo)
- #261: Fix AUTO video matching to prefer basename over content matches (@talmo)
- #263: Update
Video.filenameaccordingly if the underlyingVideoBackendhas its.filenameexpanded (@sibocw) - #264: Improve coverage testing to detect partial lines matching Codecov (@talmo)
- #265: Bump version from 0.5.6 to 0.5.7 (@talmo)
Closed Issues:
- #255: Video matching in AUTO mode matches by content when basename matches exist
- #262:
Video.exists()returns False even when backend opens video with expanded path
Full Changelog: v0.5.6...v0.5.7
v0.5.6¶
🎯 Summary
This release delivers significant performance improvements, new CLI capabilities, and critical bug fixes. The v0.5.6 release achieves 52.3% faster SLP file loading through strategic optimization, introduces a command-line interface for quick dataset inspection, and fixes a critical skeleton decoding bug that could cause edge/symmetry mismatches in SLP files.
⚡ Performance Improvements
Optimize SLP Loading Performance (#259)
Achieved 52.3% faster SLP file loading through two complementary optimizations: dtype caching and HDF5 direct loading. These changes eliminate redundant array operations that were being repeated millions of times during file loading.
Performance Results on 4.2M Frame File:
- Load time: 142.94s → 68.20s (52.3% faster, 74.75s saved)
- Per-frame: 0.034 ms → 0.016 ms (over 2x faster)
Optimizations Implemented:
1. Dtype Caching (56.61s saved, 39.6% improvement)
Problem: _get_dtype() was creating numpy structured dtype objects from scratch for every single instance, called millions of times during file loading.
Solution: Cache the dtype at the class level using cls.__dict__, handling inheritance correctly for PredictedPointsArray subclasses.
Impact: _get_dtype() time reduced from 30.38s to 2.24s (92.6% reduction)
2. HDF5 Direct Loading (18.13s saved, 12.7% improvement)
Problem: Redundant data transformations:
- Get HDF5 structured array (x, y, score, visible, complete)
- Extract x, y and
column_stackinto (N, 2) array ← 8.5s wasted - Create instance via
from_arrayconversion ← ~10s overhead - Copy score, visible, complete fields back ← redundant
Solution: Build PointsArray structures directly from HDF5 data in one operation using a new _points_from_hdf5_data() helper function.
Impact: Eliminated column_stack overhead (8.5s) and bypassed from_array conversion (~10s)
import sleap_io as sio
# Load large SLP file - now 52% faster!
labels = sio.load_slp("large_file.slp") # 142.94s → 68.20sFiles Modified:
sleap_io/model/instance.py: Added dtype caching toPointsArray._get_dtype()andPredictedPointsArray._get_dtype()sleap_io/io/slp.py: Added_points_from_hdf5_data()helper and updatedread_instances()
API Changes: None - These are internal optimizations with zero API impact and full backward compatibility.
Performance Breakdown:
| Component | Before | After V1 | After V2 | Savings |
|---|---|---|---|---|
| Total Time | 142.94s | 86.33s | 68.20s | -74.75s |
| _get_dtype | 30.38s | 2.24s | 2.24s | -28.14s ✅ |
| column_stack | 8.50s | 8.50s | 0.00s | -8.50s ✅ |
| from_array overhead | ~19.00s | ~19.00s | ~9.00s | -10.00s ✅ |
| Field copying | ~2.00s | ~2.00s | 0.00s | -2.00s ✅ |
Optimization Methodology:
- Initial Profiling - Used pyinstrument to identify
_get_dtype()consuming 30.38s (21.3% of total time) - Root Cause Analysis - Identified dtype recreation and redundant data copying
- Phased Implementation - Applied dtype caching first (39.6% improvement), then direct loading (additional 21.0% improvement)
- Validation - Verified correctness through successful 4.2M frame load with profiling at each stage
Design Decisions:
Why Class-Level Caching?
- Used
cls.__dict__to ensure each class maintains its own cached dtype PredictedPointsArrayinherits fromPointsArraybut has different dtype fields- Thread-safe: worst case is redundant creation of identical dtype objects
Why Direct Loading?
- The
Instance.__attrs_post_init__()method has an optimization check that skips conversion when passed a fully-formedPointsArray - By building
PointsArraydirectly from HDF5, we leverage this existing fast path - Eliminates intermediate array allocations and copying
Remaining Bottlenecks (68.20s):
- Instance object creation (~30s) - attrs overhead, validation
- Array allocations (~20s) - memory operations
- LabeledFrame construction (~7s) - creating frame objects
- Miscellaneous (~11s)
Further optimization would require architectural changes (HDF5 format redesign, lazy loading, alternative object models).
Impact: Dramatically improves user experience when working with large datasets, reducing wait times by over 50% for common loading operations. Essential for interactive workflows and large-scale analyses.
✨ New Features
Initial Command-Line Interface for Dataset Inspection (#256)
Introduced a Click-based CLI command sio cat for quick read-only inspection of SLEAP labels and videos without opening Python. This is an initial implementation providing core inspection capabilities with room for future enhancements.
Key Capabilities:
- Minimal text summary for
.slpfiles (counts of videos, frames, instances, skeletons) - Detailed labeled frame inspection with
--lf Noption - Skeleton structure visualization with
--skeletonoption - Video metadata inspection support
- Rich-click integration for improved help output
# Summary of labels
sio cat tests/data/slp/typical.slp
# Detailed info for labeled frame 0
sio cat --lf 0 tests/data/slp/typical.slp
# Skeleton nodes and edges
sio cat --skeleton tests/data/slp/typical.slp
# From development environment using uv
uv run -m sleap_io.io.cli cat --skeleton tests/data/slp/typical.slp
uvx --from . sio cat --lf 0 tests/data/slp/typical.slpCLI Options:
--lf N: Show details for labeled frame N (0-based index)--skeleton: Show skeleton node names and edges--open-videos/--no-open-videos: Control whether to open video backends (default: no)
Example Output:
$ sio cat data.slp
File: data.slp
Type: labels
Videos: 1
Labeled frames: 150
Instances: 300
Skeletons: 1
$ sio cat --skeleton data.slp
Skeleton: mouse
Nodes: 5
- head
- thorax
- abdomen
- left_ear
- right_ear
Edges: 4
head → thorax
thorax → abdomen
head → left_ear
head → right_earDesign Decisions:
Read-Only, Minimal CLI
- Focused on discoverability and quick inspection
- Supports uv/uvx workflows without heavy dependencies
- No modification operations (write operations remain in Python API)
Default No-Open-Videos
- Avoids opening video backends by default for portability and CI stability
- Users can opt in with
--open-videoswhen video metadata is needed
Rich-Click Integration
- Improves help UX with minimal overhead
- Consistent styling and markdown support in help text
Testing: Comprehensive test suite in tests/io/test_cli.py using click.testing.CliRunner covers:
- Summary output verification
- Labeled frame details
- Out-of-range handling
- Non-label input (video files)
- Skeleton printing
Impact: Enables quick dataset inspection from command line, supporting rapid prototyping and debugging workflows. Particularly useful with uv/uvx for inspecting files without setting up a full Python environment. Future enhancements will expand CLI capabilities based on community feedback.
🐛 Bug Fixes
Fix py/id Resolution Bug in SLP Skeleton Decoder (#257)
Resolved a critical bug where py/id references in edge types were treated as direct edge type values instead of references to previously defined edge types.
Problem: When a symmetry edge (EdgeType=2) was defined before a regular edge (EdgeType=1) in the metadata, edges and symmetries were swapped:
- First
py/reducecreatesEdgeType(2)and assigns itpy/id=1 - Second
py/reducecreatesEdgeType(1)and assigns itpy/id=2 - Buggy behavior:
py/id=1was treated asEdgeType(1)❌ - Correct behavior:
py/id=1should resolve toEdgeType(2)✅
This affected real .slp files where edge types were defined in non-standard order, causing incorrect skeleton structure.
Solution: Implemented single-pass processing in SkeletonSLPDecoder.decode() that:
- Builds a
py/id→edge_type_valuemapping aspy/reduceobjects are encountered - Resolves
py/idreferences by looking up the mapping - Falls back to treating
py/idas direct edge type value for backward compatibility with files that don't usepy/reduce
import sleap_io as sio
# Load .slp file with non-standard edge type ordering
labels = sio.load_slp("data.slp")
# Skeleton edges and symmetries now correctly decoded ✅
skeleton = labels.skeletons[0]
print(f"Edges: {len(skeleton.edges)}") # Correct count
print(f"Symmetries: {len(skeleton.symmetries)}") # Correct countFiles Modified:
sleap_io/io/skeleton.py: Fixed py/id resolution logic (+18, -1)tests/io/test_skeleton_io.py: Added test for bug (+109)
Example Impact on Real File:
Before Fix:
- 2 edges (wrong)
- 3 symmetries (wrong)
After Fix:
- 3 edges:
nose→left,nose→right,nose→tailstart✓ - 1 symmetry:
left↔right✓
Testing:
- ✅ New test:
test_slp_decoder_edge_type_pyid_resolutionpasses - ✅ All 56 skeleton I/O tests pass
- ✅ All 83 SLP tests pass
- ✅ Real .slp file verified to load correctly with fix
Impact: Critical fix ensuring edge types are correctly decoded regardless of definition order in the metadata. Prevents skeleton structure corruption in files with non-standard edge type ordering, which could silently break downstream analysis.
💡 Why These Changes Matter
The v0.5.6 release significantly enhances sleap-io's performance, usability, and data integrity:
- Dramatic Performance Gains: 52.3% faster SLP loading makes working with large datasets substantially more efficient, reducing wait times from minutes to seconds for multi-million frame files
- Improved Developer Experience: New CLI enables quick dataset inspection without Python scripting, supporting rapid prototyping and debugging workflows
- Data Integrity: Skeleton decoder fix prevents silent corruption of edge/symmetry definitions, ensuring accurate skeletal structure for downstream analysis
- Zero Breaking Changes: All improvements maintain full backward compatibility with existing code and file formats
This release demonstrates sleap-io's continued commitment to performance optimization, developer ergonomics, and data reliability for pose tracking research workflows.
📋 Changelog
- #256: A commandline interface for sleap-io (@mshooter)
- #257: Fix py/id resolution bug in SLP skeleton decoder (@talmo)
- #258: Bump version to 0.5.6 (@talmo)
- #259: Optimize SLP loading performance (@talmo)
Full Changelog: v0.5.5...v0.5.6
v0.5.5¶
🎯 Summary
This release focuses on critical data integrity improvements, format compatibility enhancements, and infrastructure robustness. The v0.5.5 release fixes critical bugs in NaN coordinate handling and color channel ordering, improves cross-platform compatibility with case-insensitive file extensions, adds metadata preservation for suggestion frames, and stabilizes the dependency ecosystem with PyAV version pinning.
🐛 Bug Fixes
Fixed NaN Handling in Label Studio Writer (#254)
Resolved a critical bug where NaN coordinates in pose data caused JSON serialization failures when exporting to Label Studio format.
Problem: When writing labels with missing or occluded keypoints (represented as NaN coordinates) to Label Studio JSON format, simplejson.dump() would fail with:
ValueError: Out of range float values are not JSON compliant: np.float64(nan)
This prevented users from exporting datasets with partial annotations to Label Studio for further refinement.
Solution: Modified convert_labels() in sleap_io/io/labelstudio.py to filter out points with NaN coordinates before JSON serialization. This approach:
- Prevents JSON serialization errors
- Aligns with Label Studio's expected format (numeric coordinates only)
- Maintains consistency with the Label Studio reader (which already skips NaN points)
- Matches how other backends (Ultralytics, JABS) handle missing points
import sleap_io as sio
import numpy as np
# Create labels with NaN coordinates (occluded keypoints)
labels = sio.load_slp("data.slp")
# Export to Label Studio now works without errors
sio.save_labelstudio(labels, "output.json") # ✅ Success
# NaN points are omitted from JSON, matching Label Studio's formatImpact: Enables seamless export of datasets with partial annotations to Label Studio, fixing issue #246 and supporting common annotation workflows where not all keypoints are visible in every frame.
Fixed RGB/BGR Color Channel Ordering in .pkg.slp Embedded Frames (#250)
Addressed critical color channel inconsistencies when embedding and decoding frames in .pkg.slp files, ensuring accurate color representation regardless of encoding/decoding plugin combinations.
Problem: When embedding frames into SLP files, the choice of image encoding plugin (OpenCV vs imageio) was hardcoded based on sys.modules, and there was no tracking of which channel order (RGB vs BGR) was used during encoding. This caused color mismatches when:
- Frames were encoded with OpenCV (BGR) but decoded with imageio (RGB)
- Frames were encoded with imageio (RGB) but decoded with OpenCV (BGR)
- Users switched between different video backends
Solution: Implemented a comprehensive fix with multiple components:
- Bumped SLP format version to 1.4 - Added channel order metadata support
- Added dedicated image plugin system - Separate from video plugins, supports only "opencv" and "imageio"
- Store channel order in metadata - Track whether frames were encoded as RGB or BGR
- Automatic channel correction - Auto-flip channels when encoding/decoding plugins differ
- User-controllable defaults - New API functions to set preferred image plugin globally
import sleap_io as sio
# Set global default for all embedding operations
sio.set_default_image_plugin("opencv")
# Or specify per-save
labels = sio.load_slp("data.slp")
sio.save_slp(labels, "output.pkg.slp", embed="all", plugin="imageio")
# Check current default
print(sio.get_default_image_plugin()) # "opencv"
# Embedded frames now maintain accurate colors regardless of:
# - Which plugin was used for encoding
# - Which plugin is used for decoding
# - Legacy files (format < 1.4) default to BGR for safetyKey Features:
- ✅ RGB/BGR consistency - Automatic channel flipping when needed
- ✅ Backwards compatible - Legacy files (format < 1.4) default to BGR
- ✅ User control - Plugin parameter at all API levels + global defaults
- ✅ Clean separation - Image plugins separate from video plugins
- ✅ Well documented - Complete format version history
Design Decisions:
Why separate image plugin system?
- Image encoding/decoding has different requirements than video streaming
- Only OpenCV and imageio support image encode/decode (PyAV doesn't)
- Clearer API semantics and avoids special-casing PyAV mappings
Why store channel order instead of plugin name?
- More explicit and universally understood (RGB/BGR)
- Plugin implementations could change, but color order is fundamental
- Enables automatic correction regardless of plugin details
Why default to BGR for old files?
- Most embedded images before this change used OpenCV (BGR)
- Safest backwards-compatible default
- Minimizes color errors in existing workflows
Impact: Critical fix ensuring color accuracy in embedded frames, essential for computer vision applications that depend on correct color channels. Fixes a long-standing issue that could silently corrupt color information.
Fixed Case-Insensitive File Extension Handling (#247)
Resolved cross-platform compatibility issues where video files with uppercase extensions were rejected.
Problem: Video files with uppercase extensions (e.g., .MP4, .AVI, .MOV) were not recognized by sleap-io, causing ValueError: Unknown video file type errors. This was particularly problematic on Windows systems where uppercase extensions are common.
import sleap_io as sio
video = sio.load_video("video.MP4")
# ❌ ValueError: Unknown video file type: "video.MP4"Solution: Implemented case-insensitive extension checking across the entire codebase by converting filenames to lowercase before matching against supported extensions.
import sleap_io as sio
# Now works with any case combination
video = sio.load_video("video.MP4") # ✅ Works
video = sio.load_video("video.mp4") # ✅ Works
video = sio.load_video("video.Mp4") # ✅ WorksImpact: Improved cross-platform compatibility, especially for Windows users where uppercase extensions are default. Eliminates a common source of confusion and error messages.
Improved Docs Workflow Robustness and Handle Race Conditions (#253)
Fixed critical documentation deployment failures and added PR preview support.
Problem: The documentation deployment workflow was failing with:
error: failed to push some refs to 'https://github.com/talmolab/sleap-io'
This occurred when:
- gh-pages branch had diverged from local state
- Race conditions when PRs were merged (both PR commit and merge commit triggered builds)
- No way to preview docs changes before merging
Solution: Implemented multiple robustness improvements:
- Separate build and push operations - Split
mike deployfromgit pushfor better control - Retry logic with rebase - Up to 3 attempts with 2s delay for gh-pages pushes
- Concurrency groups - Prevent race conditions with queue-based execution
- PR preview support - PRs now deploy to
devversion for preview
concurrency:
group: docs-deployment
cancel-in-progress: falseFeatures Added:
- ✅ Automatic retry on push failures
- ✅ PR documentation previews (deploy to
devversion) - ✅ Race condition prevention
- ✅ Better error recovery
Impact: More reliable CI/CD for documentation, enabling docs changes to be previewed in PRs and preventing deployment failures from blocking releases.
✨ New Features
Add Metadata Support to SuggestionFrame (#251)
Introduced a flexible metadata system for SuggestionFrame objects to preserve arbitrary metadata during I/O operations.
Feature: Added metadata dictionary attribute to SuggestionFrame (similar to Video.backend_metadata) to store metadata that isn't explicitly represented in the data model.
Primary Use Case: Preserves the group key when reading/writing SLP files, which was previously being discarded.
from sleap_io import Video, SuggestionFrame
video = Video.from_filename("video.mp4")
# Create suggestion with group metadata
suggestion = SuggestionFrame(
video=video,
frame_idx=42,
metadata={"group": 1}
)
# Metadata is preserved when saving/loading
labels.suggestions.append(suggestion)
sio.save_slp(labels, "output.slp")
# Load and verify
loaded = sio.load_slp("output.slp")
print(loaded.suggestions[0].metadata["group"]) # 1 ✅Key Capabilities:
- Flexible catch-all pattern for arbitrary metadata
- Follows existing
Video.backend_metadatapattern for consistency - Fully backward compatible (defaults to empty dict)
- Round-trip preservation in SLP format
Backward Compatibility:
- SLP files without
groupmetadata default togroup=0 - Existing code continues to work unchanged
- Factory default (empty dict) for new instances
Benefits:
- 🎯 Preserves metadata that was previously being lost
- 🔧 Extensible pattern for future metadata additions
↔️ Maintains backward compatibility- 📏 Follows established codebase patterns
Impact: Prevents data loss during I/O operations and enables preservation of workflow-specific metadata throughout the annotation pipeline.
🔧 Improvements
Reorganized Examples Documentation (#252)
Restructured the examples.md documentation to improve organization, reduce redundancy, and provide a more logical learning progression.
New Section Organization:
- Basics - Fundamental operations for creating and working with labels
- Format conversion - All format-related operations including NWB and YOLO
- Editing labels data - Modifying existing label data structures
- Exporting labels - Creating derived datasets and files
- Video operations - Supporting video operations
Content Improvements:
- Added "Convert to Ultralytics YOLO format" example
- Consolidated NWB examples into format conversion section
- Reduced redundancy between dataset splits examples
- Fixed broken anchor links
- Improved flow from basics → conversion → editing → exporting
Impact: Better user experience when learning sleap-io, with clearer organization and more discoverable examples.
Pin PyAV to <16.0.0 and Reorganize Dependencies (#248)
Stabilized the dependency ecosystem and modernized the package structure following PEP 621 and PEP 735 standards.
Problem: PyAV 16.0.0 release failed to upload to PyPI due to storage limitations (see PyAV Issue #2028), causing installation failures for users attempting to install the PyAV backend.
Solution:
- Pinned PyAV version - Added
av<16.0.0constraint to avoid broken release - Renamed optional dependency group - Changed from
[av]to[pyav]for clarity - Reorganized dependency groups following modern standards:
- Moved
opencv,pyav,mat, andallto[project.optional-dependencies](PEP 621) - Moved
devto[dependency-groups](PEP 735)
- Moved
Installation Syntax Changes:
# Old syntax (deprecated)
pip install sleap-io[av]
# New syntax (recommended)
pip install sleap-io[pyav] # PyAV backend
pip install sleap-io[opencv] # OpenCV backend
pip install sleap-io[mat] # MATLAB file support
pip install sleap-io[all] # All optional dependencies
# Development
uv sync --all-extras # All optional deps + dev dependencies
uv sync --group dev # Dev dependencies only (PEP 735)Breaking Change (Installation Only):
- Users installing the PyAV backend must now use
sleap-io[pyav]instead ofsleap-io[av] - No changes to actual API or code imports
Design Decisions:
Why rename av to pyav?
- The package name is
av, so having an optional dependency group also called[av]creates ambiguity - Using
[pyav]makes it clear we're referring to the PyAV library/extra
Why move dev to [dependency-groups]?
- PEP 621 (
[project.optional-dependencies]): For end-user installable extras - PEP 735 (
[dependency-groups]): For development-time dependencies - Clearer intent and better tool support (e.g.,
uv)
Impact: Prevents installation failures from broken PyAV release, modernizes package structure, and improves clarity of dependency organization.
💡 Why These Changes Matter
The v0.5.5 release significantly enhances sleap-io's reliability, data integrity, and cross-platform compatibility:
- Data Integrity: NaN handling and color channel fixes prevent silent data corruption and serialization errors, ensuring accurate pose data analysis
- Format Compatibility: SLP format v1.4 with channel order tracking, case-insensitive extensions, and metadata preservation maintain data fidelity across platforms and workflows
- Dependency Stability: PyAV pinning prevents installation issues and ensures consistent user experience
- Cross-Platform Support: Case-insensitive file extensions eliminate Windows-specific errors
- Metadata Preservation: SuggestionFrame metadata prevents information loss during I/O operations
- Infrastructure Robustness: Improved CI/CD ensures reliable documentation and development workflows
This release demonstrates sleap-io's continued commitment to data quality, reliability, and usability across diverse research workflows and computing environments.
📋 Changelog
- #247: Update file extension handling (@gitttt-1234)
- #248: Pin PyAV to <16.0.0 and rename optional dependency group (@talmo)
- #249: Bump version to 0.5.5 (@talmo)
- #250: Fix RGB/BGR color channel ordering in .pkg.slp embedded frames (@talmo)
- #251: Add metadata support to SuggestionFrame for preserving group information (@talmo)
- #252: Reorganize examples documentation for better clarity (@talmo)
- #253: Improve docs workflow robustness and handle race conditions (@talmo)
- #254: Fix NaN handling in Label Studio writer by filtering out invalid points (@talmo)
Closed Issues:
- #246: save_labelstudio does not recognize nan values
Full Changelog: v0.5.4...v0.5.5
v0.5.4¶
🎯 Summary
This release focuses on improving data manipulation workflows with enhanced merging capabilities and critical bug fixes. The v0.5.4 release introduces refined merge strategies for handling predictions, improved skeleton loading for standalone files, and fixes for data extraction and coordinate comparison operations that enhance the reliability of sleap-io for annotation workflows.
✨ New Features
Updated Smart Merge Strategy (#244)
Enhanced the smart merge strategy to better handle prediction conflicts by preferring newer predictions when both instances are predicted.
Previous Behavior: When merging two predicted instances, the instance with the higher score was selected.
New Behavior: When both instances are predictions, the newer prediction is always chosen, ensuring the most recent model outputs are preserved.
# Smart merge now prefers newer predictions
merged = labels1.merge(labels2, strategy="smart")
# If both sources have predictions for the same frame,
# the prediction from labels2 (newer) is keptImpact: This change improves iterative prediction workflows where models are refined over time, ensuring the latest model outputs are retained during merging operations.
New Update Tracks Merge Strategy (#244)
Introduced a new update_tracks merge strategy for LabeledFrame.merge() that allows updating track assignments and tracking scores while preserving the original pose annotations.
Key capabilities:
- Update track assignments without modifying pose coordinates
- Preserve original annotations while updating tracking metadata
- Useful for refining tracking results after manual review
# Update track assignments while keeping pose data
frame_merged = frame1.merge(
frame2,
merge_strategy="update_tracks"
)
# Pose coordinates from frame1 are preserved
# Track IDs and scores are updated from frame2Use Cases:
- Correcting track identity switches without re-annotating poses
- Updating tracking confidence scores after manual verification
- Refining multi-animal tracking results iteratively
🐛 Bug Fixes
Fixed Standalone Skeleton Loading (#238)
Resolved an issue where standalone skeleton JSON files using the nx_graph format would fail to load correctly.
Problem: Standalone skeleton files (like those in tmp/skeletons/) were loading with 0 nodes and 0 edges because the SkeletonDecoder only looked for links and nodes at the top level, missing the nx_graph.links and nx_graph.nodes structure.
Solution: Enhanced SkeletonDecoder to handle both formats:
nx_graphformat (standalone skeleton files)- Direct format (training config embedded skeletons)
# Standalone skeleton files now load correctly
skeleton = sio.load_skeleton("mice_hc.json")[0]
print(len(skeleton.nodes)) # 5 ✅ (was 0 before)
print(len(skeleton.edges)) # 4 ✅ (was 0 before)Impact: All skeleton files in the SLEAP skeleton repository now load correctly, ensuring seamless skeleton sharing and reuse across projects.
Fixed Labels.extract() to Copy Suggestion Frames (#243)
Fixed a bug where Labels.extract() did not copy suggestion frames, leading to incomplete datasets when exporting subsets.
Problem: When extracting a subset of labels, suggestion frames (frames recommended for annotation) were not included in the new Labels object, causing unexpected behavior during export to SLEAP projects.
Solution: Updated Labels.extract() and Labels.__getitem__() to properly copy suggestion frames associated with extracted labeled frames.
# Extract subset now includes suggestion frames
subset = labels.extract([0, 10, 20])
# subset.suggestions now contains relevant suggestion framesImpact: Ensures data integrity when creating training/validation splits or exporting subsets for annotation, fixing issue #242.
Fixed Duplication Handling in Labels.merge() (#240)
Addressed an edge case where frame indices were incorrectly mapped when merging videos with duplicate images present in both datasets.
Problem: When merging ImageVideo objects with overlapping duplicate frames, the frame index mapping could be incorrect, leading to misaligned annotations.
Solution: Improved the deduplication logic to correctly handle frame indices even when an image appears as a duplicate in both source datasets.
Impact: Ensures accurate frame alignment when merging datasets with complex image sequences, particularly for CVAT-exported data with duplicate frames. Fixes issue #239.
Fixed Instance.same_pose_as() with NaN Coordinates (#237)
Resolved a critical bug where identical instances containing NaN coordinates would incorrectly return False when compared.
Problem: The distance-based comparison failed for NaN values because nan <= tolerance is always False, causing identical instances to be considered different.
Solution: Introduced a two-mode comparison system:
- Exact comparison (new default):
tolerance=Noneusesnp.array_equal(equal_nan=True) - Tolerance-based comparison: When
toleranceis specified, validates NaN patterns match before comparing non-NaN values
from sleap_io import Instance, Skeleton
import numpy as np
skeleton = Skeleton(["head", "thorax", "tail"])
# Instances with NaN coordinates
inst1 = Instance.from_numpy([[1, 2], [np.nan, np.nan], [5, 6]], skeleton=skeleton)
inst2 = Instance.from_numpy([[1, 2], [np.nan, np.nan], [5, 6]], skeleton=skeleton)
# Exact comparison (new default behavior)
assert inst1.same_pose_as(inst2) # ✅ True (was False before)
# Tolerance-based comparison still works
assert inst1.same_pose_as(inst2, tolerance=1.0) # ✅ TrueAPI Change: Default tolerance parameter changed from 5.0 to None for more intuitive exact comparison behavior. To maintain previous behavior, explicitly specify tolerance=5.0.
Impact: Ensures correct pose comparison when working with partial annotations or occluded keypoints, critical for deduplication and tracking workflows.
💡 Why These Changes Matter
The v0.5.4 release enhances sleap-io's robustness and usability for real-world annotation workflows:
- Improved Merge Strategies: Better handling of predictions and tracking data enables more sophisticated human-in-the-loop workflows
- Skeleton Compatibility: Fixed standalone skeleton loading ensures seamless skeleton sharing across the SLEAP ecosystem
- Data Integrity: Fixed extraction and deduplication bugs prevent data loss during dataset manipulation
- Reliable Comparisons: Proper NaN handling in pose comparisons ensures accurate deduplication and tracking operations
This release demonstrates sleap-io's continued focus on reliability and usability for diverse pose tracking research workflows.
📋 Changelog
- #237: Fix
Instance.same_pose_as()bug with NaN coordinates (@talmo) - #238: Fix standalone skeleton loading and add mice_hc test (@talmo)
- #240: Improve duplication handling in
Labels.merge()(@talmo) - #243: Fix Extract Function in Labels Object to Copy Suggestion Frames (@talmo)
- #244: Update merge strategy (@talmo)
Full Changelog: v0.5.3...v0.5.4
v0.5.3¶
🎯 Summary
This release focuses on critical bug fixes and compatibility improvements that enhance the reliability and accuracy of sleap-io across different data formats and video backends. The v0.5.3 release addresses several important issues discovered by users, including coordinate system inconsistencies in legacy files, single-node skeleton loading problems, and video color channel bugs that affected data integrity.
🐛 Bug Fixes
Fixed Legacy SLP Coordinate System (#231)
Resolved a critical coordinate system inconsistency in legacy SLP files (FORMAT_ID < 1.1) that caused a 0.5 pixel offset.
Problem: Legacy SLP files used a coordinate system where the top-left corner of pixels was at (0, 0), while modern files (>=1.1) place pixel centers at (0, 0). This difference caused coordinates from legacy files to be incorrectly positioned.
Solution: Added automatic coordinate adjustment during loading for legacy files:
- Detects files with FORMAT_ID < 1.1
- Applies -0.5 pixel offset to convert from corner-based to center-based coordinates
- Ensures consistent coordinate interpretation across all SLP file versions
# Legacy files now load with corrected coordinates
labels = sio.load_file("legacy_file.slp")
# Point that was at pixel corner (0, 0) is now at pixel center (-0.5, -0.5)Impact: Ensures accurate pose data analysis when working with datasets created in older versions of SLEAP.
Fixed Single Node Skeleton Decoding (#233)
Resolved a critical bug where single-node skeletons with no edges would fail to load from training configuration files.
Problem: The SkeletonDecoder only processed nodes that appeared in the links section, but single-node skeletons have no edges/links, causing empty skeletons to be returned instead of the expected single node.
Solution: Enhanced the skeleton decoder to also process nodes directly defined in the nodes array, ensuring single-node skeletons are properly loaded.
Use Cases: This fix enables several important workflows:
- Plant phenotyping with single-point tracking
- Minimal animal tracking for small organisms
- Custom pose models with single keypoints
- Loading skeleton definitions from existing training configs
# Single-node skeletons now load correctly
skeleton = sio.load_skeleton("single_node_training_config.json")[0]
print(len(skeleton.nodes)) # 1 ✅ (was 0 before)Fixed OpenCV BGR to RGB Color Conversion (#230)
Addressed two critical issues in the OpenCV video backend that affected color accuracy and reader initialization.
Issues Fixed:
- Color Channel Bug: BGR to RGB conversion was happening in the wrong place, causing incorrect color representation
- Reader Initialization: When
keep_open=True, the OpenCV reader wasn't properly initialized, causing errors on first frame read
Impact: Ensures consistent and accurate color representation across all video backends (OpenCV, FFMPEG, PyAV), critical for computer vision applications that depend on correct color channels.
# OpenCV backend now returns correct RGB frames
video = MediaVideo("video.mp4", plugin="opencv")
frame = video.get_frame(0) # Returns RGB frame, not BGRFixed NWB Backwards Compatibility and Added Append Mode (#234)
Restored broken imports from the v0.5.2 NWB API reorganization and added append functionality.
Compatibility Fix: Restored these previously broken imports:
from sleap_io.io.nwb import append_nwb_data # ✅ Now works again
sleap_io.io.nwb.append_nwb_data # ✅ Now works againNew Feature: Added append parameter to NWB saving functions:
# Save predictions to new file
sleap_io.save_nwb(labels, "predictions.nwb")
# Append more predictions to existing file
sleap_io.save_nwb(more_labels, "predictions.nwb", append=True)Impact: Fixes user workflows broken in v0.5.2 and enables incremental prediction saving for large datasets.
🔧 Improvements
Enhanced Documentation Navigation (#229, #235)
- Added literate-nav plugin to mkdocs.yml for better API documentation navigation
- Fixed version dropdown ordering in documentation to show versions in proper semantic order (dev → newest → oldest)
- Improved user experience when browsing different versions of the documentation
💡 Why These Changes Matter
The v0.5.3 release significantly improves the reliability and accuracy of sleap-io:
- Data Integrity: Fixed coordinate system and color channel bugs ensure accurate pose data analysis
- Format Compatibility: Legacy SLP support maintains seamless workflows with older datasets
- Minimal Pose Support: Single-node skeleton fixes enable plant phenotyping and simple tracking applications
- API Stability: NWB backwards compatibility fixes prevent user workflow disruptions
- Developer Experience: Better documentation navigation improves usability
This release demonstrates sleap-io's commitment to maintaining high data quality standards and supporting diverse research workflows across the pose tracking community.
📋 Changelog
- #229: Add literate-nav plugin to mkdocs.yml for better API documentation navigation (@talmo)
- #230: Fix OpenCV BGR to RGB conversion and reader initialization (@talmo)
- #231: Fix coordinate system for legacy SLP files (FORMAT_ID < 1.1) (@talmo)
- #233: Fix single node skeleton decoding (@talmo)
- #234: Fix NWB backwards compatibility and add append mode support (@talmo)
- #235: Fix docs version dropdown ordering (@talmo)
- #236: Bump version to 0.5.3 (@talmo)
Full Changelog: v0.5.2...v0.5.3
v0.5.2¶
🎯 Summary
This release significantly expands sleap-io's format compatibility with three major new pose tracking format readers and important bug fixes. The v0.5.2 release adds support for LEAP MATLAB files, AlphaTracker JSON annotations, and introduces a comprehensive NWB training data I/O system with a simplified API. These additions further establish sleap-io as the universal utility for pose tracking data conversion and interoperability.
✨ New Features
LEAP .mat Format Reader (#224)
Added comprehensive support for reading LEAP (LEAP Estimates Animal Pose) MATLAB .mat files, enabling integration with the LEAP deep learning framework.
Key capabilities:
- Automatic skeleton detection from
nodes/jointsfields - Flexible position data parsing (handles multiple array shapes and field names)
- Video path inference when missing from metadata
- Custom skeleton override support
- MATLAB 1-based to Python 0-based index conversion
import sleap_io as sio
# Basic loading
labels = sio.load_leap("path/to/leap_data.mat")
# Auto-detection via load_file
labels = sio.load_file("path/to/leap_data.mat")
# Custom skeleton override
custom_skeleton = sio.Skeleton(
nodes=["head", "tail"],
edges=[("head", "tail")]
)
labels = sio.load_leap("path/to/leap_data.mat", skeleton=custom_skeleton)Installation: Requires the pymatreader package:
# Install with LEAP support
pip install sleap-io[mat]
# or with all extras
uv sync --all-extrasAlphaTracker Format Reader (#227)
Implemented reading support for AlphaTracker JSON annotation format, a multi-animal pose tracking system that exports annotations with bounding boxes and keypoints.
Key features:
- Dynamic skeleton detection by scanning all annotations
- Robust annotation grouping (Face markers + sequential keypoints)
- Handles variable numbers of keypoints per animal
- Graceful handling of extra annotation types
- Automatic ImageVideo construction from referenced images
from sleap_io import load_file
# Auto-detect format
labels = load_file("path/to/alphatracker.json")
# Or specify explicitly
labels = load_file("path/to/alphatracker.json", format="alphatracker")
# Access the data
for lf in labels.labeled_frames:
for instance in lf.instances:
points = instance.points["xy"] # Shape: (n_nodes, 2)Comprehensive NWB Training Data I/O (#228)
Introduced a major enhancement to NWB (Neurodata Without Borders) support with a harmonization layer that unifies reading and writing of both annotations and predictions. See ndx-pose for more information on the specification.
Harmonization Layer:
- Unified API: Single
load_nwb()andsave_nwb()functions with auto-detection - Format flexibility: Supports multiple NWB output formats via
NwbFormatenum - Auto-detection: Intelligently routes to appropriate backends based on data type
Training Data I/O:
- Full roundtrip conversion with complex skeleton hierarchies
- Multi-skeleton support and frame provenance tracking
- Video export with embedded frames using optimized MJPEG writer
- Custom NWB metadata support
from sleap_io import load_nwb, save_nwb
# Universal loading (auto-detects format)
labels = load_nwb("pose_data.nwb")
# Save with auto-detection
save_nwb(labels, "output.nwb") # Auto-detects annotation vs prediction
# Force specific format
save_nwb(labels, "annotations.nwb", nwb_format="annotations")
save_nwb(labels, "predictions.nwb", nwb_format="predictions")
# Export with embedded video frames
from sleap_io.io.nwb_annotations import export_labels
export_labels(
labels,
output_dir="export/",
nwb_filename="training_with_video.nwb",
as_training=True,
include_videos=True
)🐛 Bug Fixes
Fixed SLP Skeleton Format Compatibility (#222)
- Resolved loading issues with newer SLEAP v1.3.2+ files that use the new "nx_graph" skeleton wrapper format
- Maintains backward compatibility with legacy skeleton format
- Added comprehensive test coverage with new format fixtures
Impact: Projects created with SLEAP v1.3.2 and later can now be loaded correctly without skeleton parsing errors.
💡 Why These Changes Matter
The v0.5.2 release significantly strengthens sleap-io's position as a universal pose tracking data utility:
- Ecosystem Integration: Support for LEAP and AlphaTracker expands compatibility with popular pose estimation frameworks
- HITL Workflows: Enhanced NWB training data I/O enables sophisticated human-in-the-loop annotation workflows
- Data Provenance: Frame mapping and metadata preservation ensure traceability in complex analysis pipelines
- Research Flexibility: Researchers can now seamlessly move data between SLEAP, LEAP, AlphaTracker, and NWB ecosystems
- Future-Proofing: Updated format support ensures compatibility with evolving versions of partner tools
📋 Changelog
- #222: Fix for SLP skeleton format variant (@talmo)
- #223: Bump version to 0.5.2 (@talmo)
- #224: Add LEAP .mat format reader (@talmo)
- #227: Add AlphaTracker format reader (@talmo)
- #228: Add NWB training data I/O with simplified API (@talmo)
Full Changelog: v0.5.1...v0.5.2
v0.5.1¶
🎯 Summary
This release introduces powerful merging capabilities for annotation workflows, enhanced image sequence handling, and improved developer tooling. The highlight is the new comprehensive merging system that enables human-in-the-loop (HITL) workflows and smart combination of multiple annotation sources.
✨ New Features
Comprehensive Merging System (#216)
- New
Labels.merge()method with configurable strategies for combining annotations from multiple sources - Smart merge strategies that preserve user labels over predictions
- Instance matching with spatial, identity, and IoU-based methods
- Skeleton harmonization for consistent structure across merged data
- Video path resolution with automatic path repair
- Progress tracking and provenance metadata for merge operations
# Merge annotations with smart conflict resolution
merged = labels1.merge(labels2, strategy="smart")
# Custom merge with specific matchers
merged = labels1.merge(
labels2,
video_matcher="shape", # Match videos by dimensions
instance_matcher="iou", # Match instances by overlap
conflict_strategy="user" # Prefer user annotations
)See the new Merging guide for more information.
Enhanced ImageVideo support for merging (#219)
- Image deduplication with new
IMAGE_DEDUPmatcher - Shape-based matching for merging videos with same dimensions
- CVAT format support with automatic track preservation
- New Video methods:
has_overlapping_images()- Check for duplicate framesmatches_shape()- Compare video dimensionsdeduplicate_with()- Remove duplicate framesmerge_with()- Combine image sequences
# Remove duplicate images from a video
video_dedup = video.deduplicate_with(other_video)
# Check if videos have the same shape
if video1.matches_shape(video2):
merged = video1.merge_with(video2)🐛 Bug Fixes
Fixed Duplicate Skeleton Symmetries (#217)
- Resolved issue where legacy SLEAP files could create duplicate symmetry relationships
- Ensures clean YAML exports without redundant symmetry definitions
🔧 Improvements
Developer Tooling (#218)
- New coverage analysis script (
scripts/cov_summary.py) for PR-aware coverage reporting - GitHub CLI integration for targeted coverage analysis of changed files
- Simplified coverage command with line-by-line annotations
# Quick coverage check with annotations
uv run pytest -q --maxfail=1 --cov --cov-branch && uv run coverage annotate
# Get coverage for PR changes only
uv run python scripts/cov_summary.py📚 Documentation
Improved Documentation Structure (#220)
- Restructured merging documentation with detailed algorithm explanations
- Enhanced examples with visual improvements using MkDocs Material
- Added mermaid flowcharts and behavior matrices for better understanding
- Improved organization with progressive disclosure of complex topics
💡 Why These Changes Matter
The v0.5.1 release significantly enhances sleap-io's capabilities for real-world annotation workflows:
- HITL Workflows: The new merging system enables seamless integration of manual corrections with model predictions
- Data Consolidation: Easily combine annotations from multiple annotators or sessions
- Video Management: Better handling of image sequences with automatic deduplication
- Developer Experience: Improved tooling for maintaining code quality and test coverage
📋 Changelog
- #216: Implement comprehensive merging system for annotation files (@talmo)
- #217: Fix duplicate skeleton symmetries from legacy SLEAP files (@talmo)
- #218: Add coverage summary script and update coverage command (@talmo)
- #219: Deduplicate merging for
ImageVideoand add better CVAT support (@talmo) - #220: Documentation improvements for merging capabilities (@talmo)
Full Changelog: v0.5.0...v0.5.1
v0.5.0¶
Summary
Overview
The v0.5.0 release of sleap-io represents a major leap forward in format compatibility, development tooling, and data management capabilities. This release introduces support for five new pose tracking formats (COCO, DeepLabCut, TIFF stacks, and enhanced Ultralytics), adds powerful multi-dataset management with LabelsSet, and modernizes the development workflow with UV package manager. The release also includes critical bug fixes, performance improvements, and comprehensive documentation updates.
🚀 New Features
Multi-Dataset Management with LabelsSet (#197)
sleap-io now provides a powerful LabelsSet container for managing multiple Labels objects, enabling seamless handling of train/val/test splits and dataset collections.
This feature includes:
- Hybrid dictionary/tuple interface for flexible access patterns
- Automatic split creation from existing datasets
- Batch I/O operations for entire dataset collections
- Backward-compatible API that doesn't break existing code
Usage:
import sleap_io as sio
# Create train/val/test splits
splits = labels.make_training_splits(n_train=0.8, n_test=0.1)
# Access as dictionary
train = splits["train"]
val = splits["val"]
test = splits["test"]
# Or unpack as tuple (backward compatible)
train, val, test = splits
# Save all splits at once
splits.save("splits/", embed=True) # Creates train.pkg.slp, val.pkg.slp, test.pkg.slp
# Load multi-split datasets
labels_set = sio.load_labels_set("path/to/splits/")COCO-Style Dataset Support (#199)
Added comprehensive support for reading COCO pose format, enabling integration with the broader computer vision ecosystem.
Features:
- Automatic skeleton creation from COCO categories
- Multi-species support with different skeletons per category
- Flexible directory structure handling (flat, nested, categorized)
- Binary and ternary visibility encodings
- Memory-efficient shared video objects for images
Usage:
import sleap_io as sio
# Load COCO annotations
labels = sio.load_file("annotations.json", format="coco")
# Load with custom image root
labels = sio.load_file("coco_data.json", dataset_root="/path/to/images")
# Load multi-split COCO dataset
labels_set = sio.load_labels_set("dataset/", format="coco")DeepLabCut Training Data Support (#201)
Implemented complete support for reading DeepLabCut CSV files, supporting all DLC format variations.
Capabilities:
- Single-animal tracking (SADLC)
- Multi-animal tracking (MADLC)
- Multi-animal with identity tracking (MAUDLC)
- Automatic format detection from CSV headers
- Proper video grouping from image directories
Usage:
import sleap_io as sio
# Load DLC annotations
labels = sio.load_file("CollectedData_scorer.csv")
# Access the data
print(f"Found {len(labels.labeled_frames)} labeled frames")
for lf in labels.labeled_frames:
for instance in lf.instances:
if instance.track:
print(f"Individual '{instance.track.name}': {instance.numpy()}")TIFF Stack Support (#195)
Added native support for multi-page TIFF files through a new TiffVideo backend.
Features:
- Automatic detection of single vs multi-page TIFFs
- Frame-by-frame access for TIFF stacks
- Full SLP serialization support
- Backward compatibility with older SLEAP versions
Usage:
import sleap_io as sio
# Load multi-page TIFF
video = sio.Video.from_filename("multipage_stack.tif")
print(f"Stack has {video.shape[0]} frames")
# Use in labels
labels = sio.Labels()
labels.videos.append(video)
labels.save("project.slp") # TiffVideo metadata preservedVideo Plugin Management (#202)
Introduced comprehensive control over video backend selection to handle platform-specific codec issues.
New capabilities:
- Global default plugin setting
- Flexible plugin name aliases (case-insensitive)
- Runtime plugin switching on existing videos
- Batch plugin changes for entire projects
Usage:
import sleap_io as sio
# Set global default
sio.set_default_video_plugin("opencv") # or "cv2", "cv", "ocv"
# Load with specific plugin
video = sio.load_video("video.mp4", plugin="FFMPEG")
# Switch plugin on existing video
video.set_video_plugin("pyav") # or "av", "PyAV"
# Batch change for all videos in project
labels = sio.load_slp("project.slp")
labels.set_video_plugin("opencv")🐛 Bug Fixes
Fixed Skeleton Node Order Decoding (#208)
- Fixed incorrect node ordering when loading skeletons from training configs with non-sequential py/ids
- Resolved edge connection errors that occurred with complex skeletons
- Added comprehensive tests for node and edge order preservation
Impact: Skeletons loaded from training configs now correctly preserve their structure and edge connections.
🔧 Improvements
Performance: Labels.replace_filenames Control (#213)
Added open_videos parameter to Labels.replace_filenames() for better performance when working with network storage:
# Replace paths without opening videos (faster on network storage)
labels.replace_filenames(
prefix_map={"/old/network/path": "/new/network/path"},
open_videos=False # Avoid costly file checks
)Impact: Significantly faster filename replacement operations when dealing with many files on network storage.
Development Workflow: UV Migration (#214)
Migrated CI/CD and development tooling from conda to UV for dramatic performance improvements:
- 10-100x faster dependency resolution and installation
- CI runtime reduced from ~12 minutes to ~2 minutes
- Single tool for all Python/package management tasks
- PyPI trusted publisher for improved security
Developer experience:
# One-line setup
uv sync --all-extras
# All commands now use uv run prefix
uv run pytest tests/
uv run ruff check sleap_io tests
uv buildTooling: Unified Linting with Ruff (#194)
Replaced black and pydocstyle with ruff for unified, faster code quality checks:
- Single tool for both formatting and linting
- Significantly faster CI runs
- Fixed 50+ code quality issues during migration
- Consistent configuration and error reporting
Documentation Enhancements (#203, #215)
Comprehensive documentation improvements for both AI assistants and human contributors:
- Added Claude Code integration with
.claude/commandsdirectory - Visual data model diagram with relationships
- Modernized CONTRIBUTING.md with Quick Start section
- Updated all examples to use UV commands
- Streamlined testing and coverage workflows
📦 Dependencies
- Added
pyyamlfor YAML skeleton support (from v0.4.0) - Made OpenCV and PyAV optional dependencies with modular installation
- Removed hard conda dependencies in favor of pip/UV
🔄 Migration Notes
Optional Video Backend Dependencies
Video backends are now optional to reduce installation size:
# Basic installation (no video backends)
pip install sleap-io
# With specific backends
pip install sleap-io[opencv] # OpenCV only
pip install sleap-io[av] # PyAV only
pip install sleap-io[all] # All backendsDevelopment Setup
For developers, UV is now the recommended tool:
# Install UV
curl -LsSf https://astral.sh/uv/install.sh | sh
# Setup development environment
uv sync --all-extras
# Run commands with uv run prefix
uv run pytest tests/Conda environments still work but use pip under the hood for simplicity.
No Breaking API Changes
All existing APIs remain compatible. New features are additive and do not affect existing functionality.
🎯 Why These Changes Matter
- Format Ecosystem: Support for COCO and DeepLabCut enables integration with the broader pose tracking community
- Dataset Management:
LabelsSetprovides professional-grade tools for managing train/val/test splits - Performance: UV migration and video control features dramatically improve development and runtime speed
- Media Flexibility: TIFF support and plugin management handle diverse video formats and platform requirements
- Developer Experience: Modern tooling and documentation reduce friction for contributors
- Code Quality: Ruff migration ensures consistent, high-quality code across the project
This release significantly expands sleap-io's capabilities as a universal pose tracking data utility, improving compatibility with external tools, performance for large-scale operations, and the overall development experience.
Changelog
- Replace black/pydocstyle with ruff in CI by @talmo in #194
- Add TIFF support for multi-page stacks by @talmo in #195
- Add LabelsSet for multi-label and split handling by @talmo in #197
- Add COCO-style dataset support by @talmo in #199
- Add DLC training data support by @talmo in #201
- Video plugin conveniences by @talmo in #202
- Fix skeleton node order decoding by @talmo in #208
- Migrate CI from conda to UV by @talmo in #214
- Update to 0.5.0 and refresh docs by @talmo in #203
- Add
open_videosparameter toLabels.replace_filenamesmethod by @talmo in #213 - Documentation improvements for Claude Code and contributors by @talmo in #215
Full Changelog: v0.4.1...v0.5.0
v0.4.1¶
Summary
Overview
The v0.4.1 release of sleap-io introduces experimental Ultralytics YOLO pose format support, enhances skeleton loading capabilities, and provides important improvements to video reference handling in saved files. This release focuses on expanding format compatibility and giving users more control over their workflow when working with package files and predictions.
🚀 New Features
Ultralytics YOLO Pose Format Support (#183) [Experimental]
sleap-io now provides experimental support for reading and writing pose annotations in the Ultralytics YOLO pose format, enabling seamless integration with YOLO-based pose estimation workflows.
Note: This is an initial implementation that has not yet been battle-tested in production environments. Please report any issues or edge cases you encounter.
This feature includes:
- Full support for YOLO's normalized coordinate system
- Multi-instance pose annotations
- Automatic skeleton configuration from
data.yamlfiles - Integration with train/val/test splits
Usage:
import sleap_io as sio
# Load YOLO pose annotations
labels = sio.load_ultralytics(
labels_dir="path/to/labels",
image_dir="path/to/images",
data_yaml="path/to/data.yaml"
)
# Save in YOLO format with train/val/test splits
sio.save_ultralytics(
labels,
save_dir="yolo_dataset",
split_fractions=(0.8, 0.1, 0.1) # 80% train, 10% val, 10% test
)
# Also works via the generic API
labels = sio.load_file("path/to/labels/*.txt") # Auto-detects YOLO format
labels.save("output_dir", format="ultralytics")Video Reference Restoration Control (#192)
Added fine-grained control over video references when saving SLEAP files. This is particularly useful when working with predictions made on .pkg.slp files, allowing you to maintain references to the specific package files used for inference.
Usage:
import sleap_io as sio
# Load a package file used for training/inference
labels = sio.load_file("train.pkg.slp")
# Run inference...
# Save predictions while preserving reference to train.pkg.slp
labels.save("predictions.slp", embed=False, restore_original_videos=False)
# Default behavior still restores original video references
labels.save("predictions_default.slp") # Links to original video filesBenefits:
- Track which exact dataset split was used for predictions
- Compare inference results across different models
- Maintain data lineage for reproducible workflows
🐛 Bug Fixes
Critical Fix: Skeleton Decoding for Complex Skeletons (#185) 🚨
- Fixed a critical bug introduced in v0.4.0 where skeletons with 32+ nodes or non-sequential py/id assignments would fail to decode
- Removed hardcoded assumptions about py/id patterns that only worked for simple skeletons
- Now correctly handles arbitrary py/id assignments in skeleton data
Impact: This bug affected v0.4.0 and prevented users with complex skeletons (e.g., full fly body models with 32 nodes) from loading their data. If you use complex skeletons and are on v0.4.0, upgrading to v0.4.1 is strongly recommended.
🔧 Improvements
Enhanced Skeleton Loading API (#187)
The load_skeleton() function now intelligently loads skeletons from multiple sources:
.slpfiles: Extract skeletons directly from SLEAP project files- Training config JSON: Automatically detect and extract embedded skeletons
- Existing formats: Continue to support standalone skeleton JSON/YAML files
Usage:
import sleap_io as sio
# Load from various sources
skeleton = sio.load_skeleton("project.slp") # From SLEAP project
skeleton = sio.load_skeleton("training_config.json") # From training config
skeleton = sio.load_skeleton("skeleton.yaml") # Standalone YAML
skeleton = sio.load_skeleton("skeleton.json") # Standalone JSONStrengthened Test Coverage (#190)
- Enhanced skeleton test assertions with specific expected values
- Improved test clarity and regression detection
- Ensures skeleton loading behavior is consistent and reliable
📦 Dependencies
No new dependencies added in this release.
🔄 Migration Notes
Video Reference Behavior
The new restore_original_videos parameter defaults to True, maintaining existing behavior:
# These are equivalent (default behavior preserved)
labels.save("output.slp")
labels.save("output.slp", restore_original_videos=True)
# New option to preserve package file references
labels.save("output.slp", restore_original_videos=False)No Breaking API Changes
All existing APIs remain compatible. New features are additive and do not affect existing functionality.
🎯 Why These Changes Matter
- Format Interoperability: YOLO pose format support enables integration with a wider ecosystem of pose estimation tools
- Workflow Control: Video reference preservation gives users control over their data lineage and prediction tracking
- Reliability: Bug fixes for complex skeletons ensure sleap-io works with diverse anatomical models
- Developer Experience: Enhanced skeleton loading API reduces code complexity when working with different file formats
- Quality Assurance: Improved test coverage ensures long-term stability and catches regressions early
This release strengthens sleap-io's position as a versatile tool for pose tracking data management, improving both compatibility with external tools and control over complex workflows.
Changelog
- Add Ultralytics YOLO pose format support by @talmo in #183
- Fix skeleton decoding for non-sequential py/id assignments by @talmo in #185
- Skeleton API enhancements by @talmo in #187
- Strengthen skeleton test assertions by @talmo in #190
- Add video reference restoration control by @talmo in #192
- Bump to v0.4.1 by @talmo in #193
Full Changelog: v0.4.0...v0.4.1
v0.4.0¶
Summary
Overview
The v0.4.0 release of sleap-io introduces significant improvements to skeleton file handling, enhances the package file (.pkg.slp) saving performance, and fixes several important bugs. This release focuses on making skeleton data more accessible and improving the user experience when working with large datasets.
🚀 New Features
Standalone Skeleton Serialization (#178)
sleap-io now supports reading and writing skeleton files independently from label files. This feature enables:
- Loading skeleton definitions from
.jsonfiles in SLEAP's jsonpickle format - Saving skeletons for reuse across projects
- Working with multiple skeletons in a single file
Usage:
import sleap_io as sio
# Load a skeleton
skeleton = sio.load_skeleton("skeleton.json")
# Save a skeleton
sio.save_skeleton(skeleton, "output.json")
# Also works with lists of skeletons
skeletons = sio.load_skeleton("multiple_skeletons.json")
sio.save_skeleton(skeletons, "output.json")YAML Skeleton Format Support (#179)
Added support for human-readable YAML format for skeleton files, making it easier to:
- Manually create and edit skeleton definitions
- Version control skeleton configurations
- Share skeleton templates between projects
Usage:
import sleap_io as sio
# Load from YAML
skeleton = sio.load_skeleton("skeleton.yaml")
# Save to YAML
sio.save_skeleton(skeleton, "output.yml")Example YAML format:
Skeleton-0:
nodes:
- name: head
- name: thorax
- name: abdomen
edges:
- source:
name: head
destination:
name: thorax
symmetries:
- - name: eyeL
- name: eyeRFrame Embedding Progress Bar (#174)
Added a progress bar when embedding frames into .pkg.slp files, providing:
- Visual feedback during long embedding operations
- Better user experience when working with large datasets
- Estimate of remaining time for completion
🐛 Bug Fixes
Fixed Package File Saving with Embedded Videos (#177)
- Changed default behavior:
embed=Falseis now the default for fast-saving - Prevented data loss: Added detection for self-referential paths
- Fixed video referencing: Correctly handles external video references
Impact: Users can now quickly save .pkg.slp files without re-embedding frames, significantly improving save performance for large projects.
Usage:
# Fast save (default behavior)
labels.save("file.slp") # embed=False by default
# Explicitly re-embed frames
labels.save("file.slp", embed=True)Fixed ImageVideo Backend Metadata Serialization (#173)
- Fixed compatibility issue with core SLEAP when using
ImageVideobackend - Resolved
cattrsclass inference errors - Maintained backward compatibility with existing files
Impact: Users working with image sequences no longer encounter errors when loading files in core SLEAP.
🔧 Improvements
SLP Format v1.3 Support (#176)
- Added explicit support for SLEAP label format version 1.3
- Enhanced tracking score support on
Instanceobjects - Added comprehensive tests for format compatibility
Impact: Full compatibility with the latest SLEAP format features, including improved tracking metrics.
📦 Dependencies
- Added
pyyamldependency for YAML skeleton format support
🔄 Migration Notes
Default Embedding Behavior Change
The default behavior for saving .pkg.slp files has changed:
- Before v0.4.0:
embed=True(re-embeds all frames) - After v0.4.0:
embed=False(references existing files)
To maintain previous behavior, explicitly set embed=True:
labels.save("output.pkg.slp", embed=True)No Breaking API Changes
All existing APIs remain compatible. New features are additive and do not affect existing functionality.
🎯 Why These Changes Matter
- Skeleton Management: Standalone skeleton files enable better project organization and reusability
- Performance: Fast-saving package files dramatically reduces save times for large datasets
- User Experience: Progress bars and better error messages improve workflow transparency
- Compatibility: Bug fixes ensure smooth interoperability with core SLEAP
- Flexibility: YAML format provides a human-friendly alternative for skeleton configuration
This release enhances sleap-io's capabilities as a standalone utility for pose tracking data management while maintaining full compatibility with the SLEAP ecosystem.
Changelog
- Fix the backend metadata being serialized to SLP for ImageVideo backends by @talmo in #173
- Frame embedding progress bar by @talmo in #174
- Implement SLP format v1.3 by @talmo in #176
- Fix saving package files with embedded videos by @talmo in #177
- Standalone Skeleton serialization/deserialization by @talmo in #178
- Add YAML support for skeleton serialization by @talmo in #179
- Bump to v0.4.0 by @talmo in #180
Full Changelog: v0.3.0...v0.4.0
v0.3.0¶
What's Changed
- Add skeleton symmetry QOL enhancements by @talmo in #144
- Add support for writing to nwb with ndx-pose > 0.2.0 by @h-mayorquin in #143
- Check for existence of source video when creating from pkg.slp by @talmo in #148
- Add
Cameraclass by @roomrys in #145 - Add CameraGroup class by @roomrys in #146
- Minimize NWB testing time by @talmo in #155
- Implement points array backend by @talmo in #154
- Fix saving .pkg.slp with empty videos by @talmo in #156
- Add all MV data structures by @roomrys in #151
- Integrate recording session with labels by @roomrys in #153
- Remove geometric functionality by @roomrys in #158
- Refactor data handling and implement setitem for Instances by @talmo in #161
- Support user instances in
Labels.numpy()by @talmo in #162 - Implement
update_from_numpymethod for instance updating from tracks array by @talmo in #163 - Add
Labels.from_numpyconstructor by @talmo in #166 - Add repr for mv classes by @roomrys in #167
- Add codespell support (config, workflow to detect/not fix) and make it fix some typos by @yarikoptic in #168
- Update ndx-pose dependency to version >=0.2.1 in environment.yml by @lochhh in #169
- Remove OpenCV dependency for Rodrigues transformation by @talmo in #170
- Bump to v0.3.0 by @talmo in #171
New Contributors
- @yarikoptic made their first contribution in #168
Full Changelog: v0.2.0...v0.3.0
v0.2.0¶
What's Changed
- Update backend filename when backend isn't created on replace by @talmo in #127
- Update labels videos list on replace by @talmo in #128
- Add video writing by @talmo in #129
- Add
sio.VideoWriter: basicimageio-ffmpegvideo writer with sensible H264 presets. This can be used as a context manager:with sio.VideoWriter("video.mp4") as vw: for frame in video: vw(frame)
- Add
sio.save_video: high-level video writing. This can be used to quickly write a set of frames or even a wholeVideofor easy (if inefficient) re-encoding:bad_video = sio.load_video("unseekable.avi") sio.save_video(bad_video, "seekable.mp4")
- Added
IndexErrorinVideoBackendto enable sequence protocol for iteration overVideos:for frame in video: pass
- Refactored
sio.io.videotosio.io.video_reading.
- Add
- Fixes to get JABS export to work with new data by @talmo in #132
- Make skeleton nodes mutable by @talmo in #135
- Add skeleton manipulation utilities by @talmo in #136
Skeleton__contains__(node: NodeOrIndex): ReturnsTrueif a node exists in the skeleton.rebuild_cache(): Method allowing explicit regeneration of the caching attributes from the nodes.- Caching attributes are now named
_name_to_node_cacheand_node_to_ind_cache, better reflecting the mapping directionality. require_node(node: NodeOrIndex, add_missing: bool = True): Returns aNodegiven aNode,intorstr. Ifadd_missingisTrue, the node is added or created, otherwise anIndexErroris raised. This is helpful for flexibly converting between node representations with convenient existence handling.add_nodes(list[Node | str]): Convenience method to add a list of nodes.add_edges(edges: list[Edge | tuple[NodeOrIndex, NodeOrIndex]]): Convenience method to add a list of edges.rename_nodes(name_map: dict[NodeOrIndex, str] | list[str]): Method to rename nodes either by specifying a potentially partial mapping from node(s) to new name(s), or a list of new names. Handles updating both theNode.nameattributes and the cache.rename_node(old_name: NodeOrIndex, new_name: str): Shorter syntax for renaming a single node.remove_nodes(nodes: list[NodeOrIndex]): Method for removing nodes from the skeleton and updating caches. Does NOT update corresponding instances.remove_node(node: NodeOrIndex): Shorter syntax for removing a single node.reorder_nodes(new_order: list[NodeOrIndex]): Method for setting the order of the nodes within the skeleton with cache updating. Does NOT update corresponding instances.
Instance/PredictedInstanceupdate_skeleton(): Updates thepointsattribute on the instance to reflect changes in the associated skeleton (removed nodes and reordering). This is called internally after updating the skeleton from theLabelslevel, but also exposed for more complex data manipulation workflows.replace_skeleton(new_skeleton: Skeleton, node_map: dict[NodeOrIndex, NodeOrIndex] | None = None, rev_node_map: dict[NodeOrIndex, NodeOrIndex] | None = None): Method to replace the skeleton on the instance with optional capability to specify a node mapping so that data stored in thepointsattribute is retained and associated with the right nodes in the new skeleton. Mapping is specified innode_mapfrom old to new nodes and defaults to mapping between node objects with the same name.rev_node_mapmaps new nodes to old nodes and is used internally when calling from theLabelslevel as it bypasses validation.
Labelsinstances: Convenience property that returns a generator that loops over all labeled frames and returns all instances. This can be lazily iterated over without having to construct a huge list of all the instances.rename_nodes(name_map: dict[NodeOrIndex, str] | list[str], skeleton: Skeleton | None = None): Method to rename nodes in a specified skeleton within the labels.remove_nodes(nodes: list[NodeOrIndex], skeleton: Skeleton | None = None): Method to remove nodes in a specified skeleton within the labels. This also updates all instances associated with the skeleton, removing point data for the removed nodes.reorder_nodes(new_order: list[NodeOrIndex], skeleton: Skeleton | None = None): Method to reorder nodes in a specified skeleton within the labels. This also updates all instances associated with the skeleton, reordering point data for the nodes.replace_skeleton(new_skeleton: Skeleton, old_skeleton: Skeleton | None = None, node_map: dict[NodeOrIndex, NodeOrIndex] | None = None): Method to replace a skeleton entirely within the labels, updating all instances associated with the old skeleton to use the new skeleton, optionally with node remapping to retain previous point data.
- Add more checks for video seeking/reading failure by @talmo in #138
- Fix
HDF5Videoedge cases by @talmo in #137 - Docs changelog generation by @talmo in #130
- Add
Labels.extract,Labels.trimandVideo.saveby @talmo in #140LabeledFrame.frame_idx: Now always converted tointtype.Video.close(): Now caches backend metadata toVideo.backend_metadatato persist metadata on close.copy.deepcopy()now works onVideoobjects even if backend is open.Video.save(save_path: str | Path, frame_inds: list[int] | np.ndarray | None = None, video_kwargs: dict[str, Any] | None = None): Method to save a video file to an MP4 usingVideoWriterwith an optional subset of frames.Labels.extract(inds: list[int] | list[tuple[Video, int]] | np.ndarray, copy: bool = True): Add method to extract a subset of frames from the labels, optionally making a copy, and return a newLabelsobject.Labels.trim(save_path: str | Path, frame_inds: list[int] | np.ndarray, video: Video | int | None = None, video_kwargs: dict[str, Any] | None = None): Add method to extract a subset of the labels, write a video clip with the extracted friends, and adjust frame indices to match the clip.
- Docs automation by @talmo in #141
- Add more examples to docs by @talmo in #142
Full Changelog: v0.1.10...v0.2.0
v0.1.10¶
What's Changed
- Fix embedded video lookup by @talmo in #122
- Add better support for exporting and loading RGB videos from .pkg.slp files by @talmo in #125
- Fix video indexing when embedding from labels that already have embedded data by @talmo in #126
Full Changelog: v0.1.9...v0.1.10
v0.1.9¶
What's Changed
- Dependency management by @talmo in #118
- Drop
avas a dependency since it's still a little buggy and doesn't have broad enough platform compatibility. - Pin
ndx-pose< 0.2.0 until #104 is merged in. - Remove livecov dev tool as it was interfering with VSCode debugging.
- Drop
- Safer video loading from SLP by @talmo in #119
- Added
sio.io.utils.is_file_accessibleto check for readability by actually reading a byte. This catches permission and other esoteric filesystem errors (addresses #116). - Explicit control over whether video files should be opened when loading labels with:
sio.load_slp(..., open_videos=False) - Explicit control over whether backend is auto-initialized when creating or using
Videoobjects withVideo(..., open_backend=False). - More sanitization of filenames to posix/forward-slash safe forms when reading and writing SLP files.
- Added
- Fix split calculation and allow for not embedding by @talmo in #120
- Fix: The function now correctly splits the labels into training, validation, and test sets based on the specified proportions (fixes #117). Previously, the validation fraction was being computed incorrectly in cases where its relative fraction was
1.0after taking out the train split. - Enhancement:
Labels.make_training_splits(..., embed=False). Previously, the function would always embed the images, which could be slow for large projects. With this change, theembedparameter is introduced, allowing the user to choose whether to embed the images or save the labels with references to the source video files.
- Fix: The function now correctly splits the labels into training, validation, and test sets based on the specified proportions (fixes #117). Previously, the validation fraction was being computed incorrectly in cases where its relative fraction was
Full Changelog: v0.1.8...v0.1.9
v0.1.8¶
What's Changed
New Contributors
Full Changelog: v0.1.7...v0.1.8
v0.1.7¶
What's Changed
Full Changelog: v0.1.6...v0.1.7
v0.1.6¶
What's Changed
Full Changelog: v0.1.5...v0.1.6
v0.1.5¶
What's Changed
Full Changelog: v0.1.4...v0.1.5
v0.1.4¶
What's Changed
- Add support for embedding images in .pkg.slp by @talmo in #91
- Saving SLP files with embedded images will re-save the embedded images.
- Embed images into SLP files with:
labels.save("labels.pkg.slp", embed="user")to embed frames with user-labeled instances (Instance)labels.save("labels.pkg.slp", embed="user+suggestion")to embed frames with user-labeled instances and suggestion frames (useful for inference after training)labels.save("labels.pkg.slp", embed="source")to restore the source video ("unembed")
- Better reprs and QOL by @talmo in #96
- Better
__repr__s forSkeleton,LabeledFrame,Labels,Instance,PredictedInstance Labels.append()andLabels.extend()to addLabeledFrames now will updateLabels.tracks,Labels.skeletonsandLabels.videoswith contents.Labels.update()to manually updateLabels.tracks,Labels.skeletonsandLabels.videoswith contents ofLabels.labeled_framesandLabels.suggestions.Labels.replace_filenames(): multiple methods for replacing all video filenames across the project (#85).Skeleton.edge_namesto return list of edges as tuples of string names- Added docstrings to
sio.load_videoand related high levelVideoAPIs to clarify supported file formats. - Syntactic sugar: try to initialize video backend with
Video(filename)construction (#94)
- Better
Note: This is a re-release of v0.1.3 which had a borked deployment.
Full Changelog: v0.1.2...v0.1.4
v0.1.3¶
What's Changed
- Add support for embedding images in .pkg.slp by @talmo in #91
- Saving SLP files with embedded images will re-save the embedded images.
- Embed images into SLP files with:
labels.save("labels.pkg.slp", embed="user")to embed frames with user-labeled instances (Instance)labels.save("labels.pkg.slp", embed="user+suggestion")to embed frames with user-labeled instances and suggestion frames (useful for inference after training)labels.save("labels.pkg.slp", embed="source")to restore the source video ("unembed")
- Better reprs and QOL by @talmo in #96
- Better
__repr__s forSkeleton,LabeledFrame,Labels,Instance,PredictedInstance Labels.append()andLabels.extend()to addLabeledFrames now will updateLabels.tracks,Labels.skeletonsandLabels.videoswith contents.Labels.update()to manually updateLabels.tracks,Labels.skeletonsandLabels.videoswith contents ofLabels.labeled_framesandLabels.suggestions.Labels.replace_filenames(): multiple methods for replacing all video filenames across the project (#85).Skeleton.edge_namesto return list of edges as tuples of string names- Added docstrings to
sio.load_videoand related high levelVideoAPIs to clarify supported file formats. - Syntactic sugar: try to initialize video backend with
Video(filename)construction (#94)
- Better
Full Changelog: v0.1.2...v0.1.3
v0.1.2¶
What's Changed
Full Changelog: v0.1.1...v0.1.2
v0.1.1¶
What's Changed
- Create docs pages by @talmo in #87
- Add
ImageVideobackend by @talmo in #88 - Add
SuggestionFrameby @talmo in #89 - Implement
ImageVideosupport in SLP by @talmo in #90 - Bump to v0.1.1 by @talmo in #93
Full Changelog: v0.1.0...v0.1.1
v0.1.0¶
What's Changed
-
Add skeleton utilities by @talmo in #76
Skeleton.add_node: Add a node by name or object.Skeleton.add_edge: Add an edge by lists of names or objects.Skeleton.add_symmetry: Add a symmetry edge by lists of names or objects.
-
Update CI and versions by @talmo in #77
- Update dependency ranges (see below)
- Update action workflow versions
- Enable M1 mac runners
- Expand python version range to 3.7-3.12
- Note: Python 3.7 is no longer tested in CI due to lack of conda-forge compatability.
- Enable pure conda-forge dependency setup
-
Fix multi-skeleton loading by @talmo in #79
- Fixes #71.
-
Add high level APIs by @talmo in #80
- Add
load_videoandload_filehigh level APIs (#48)
- Add
-
Labels QOL enhancements by @talmo in #81
LabeledFrame.remove_predictions: Remove predicted instances from a labeled frame.LabeledFrame.remove_empty_instances: Remove instances with no visible points from a labeled frame.Labels.save: Instance-level convenience wrapper forsio.save_file.Labels.clean: Remove unused or empty frames, instances, videos, skeletons and tracks.Labels.remove_predictions: Remove predicted instances from all labeled frames (#69).Labels.__getitem__: Now supports lists, slices, numpy arrays, tuples of(Video, frame_idx)andVideo.
-
Video QOL enhancements by @talmo in #82
Video.is_open: Checks if the video exists and the backend is set.Video.open: Opens or restarts the backend for reading.Video.close: Closes the backend for reading.Video.exists: Check if the filename for the video exists.Video.replace_filename: Replace the filename and restart the backend.
Notes on dependency pins
ffmpeg < 6.1due to imageio/imageio-ffmpeg#99h5py >= 3.8.0due to h5py/h5py#2118python >= 3.8due toh5py >= 3.8.0(we still supportpython==3.7via pip but this is not longer in CI)
Full Changelog: v0.0.14...v0.1.0
v0.0.14¶
What's Changed
Full Changelog: v0.0.13...v0.0.14
v0.0.13¶
What's Changed
Full Changelog: v0.0.12...v0.0.13
v0.0.12¶
What's Changed
- Add support for the Kumar Lab's JABS format by @SkepticRaven in #63
- Fix multi-video serialization in SLP by @talmo in #72
New Contributors
- @SkepticRaven made their first contribution in #63
Full Changelog: v0.0.11...v0.0.12
v0.0.11¶
What's Changed
Full Changelog: v0.0.10...v0.0.11