Skip to content

Commit 2200190

Browse files
gitttt-1234claude
andauthored
refactor: Use sleap-io for analysis HDF5 and CSV exports (#2649)
* refactor: Use sleap-io for analysis HDF5 and CSV exports Replace SLEAP's internal write_tracking_h5.py with sleap-io's save_analysis_h5() and save_csv() functions for exporting analysis files. Benefits: - Single source of truth for analysis export logic (sleap-io) - Consistent format between SLEAP GUI and sleap-io CLI exports - Additional metadata in output files (dimension labels, skeleton info) - Backwards compatible with existing analysis file readers Changes: - sleap/io/format/sleap_analysis.py: Use sio.load_analysis_h5() and sio.save_analysis_h5() instead of custom implementation - sleap/io/format/csv.py: Use sio.save_csv() instead of write_tracking_h5 - sleap/io/convert.py: Use sleap-io functions for analysis export Related to #1651 (analysis export enhancements) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Handle videos with no labeled frames gracefully sleap-io raises ValueError when a video has no labeled frames, but the old SLEAP behavior was to silently skip such videos. Add try-except to maintain backwards compatibility. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: Check for labeled frames before CSV export sleap-io's save_csv doesn't raise ValueError when there are no labeled frames - it writes an empty file. Check for labeled frames BEFORE calling the export function to maintain old behavior of skipping empty videos. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 960fd9f commit 2200190

File tree

3 files changed

+93
-113
lines changed

3 files changed

+93
-113
lines changed

sleap/io/convert.py

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -158,29 +158,43 @@ def main(args: list = None):
158158
outnames.append(dflt_name)
159159

160160
if "csv" in args.format:
161-
from sleap.info.write_tracking_h5 import main as write_analysis
161+
import sleap_io as sio
162162

163163
for video, output_path in zip(vids, outnames):
164-
write_analysis(
164+
# Check for labeled frames before exporting
165+
labeled_frames = labels.find(video)
166+
if not labeled_frames:
167+
print(f"No labeled frames in {video.filename}. Skipping.")
168+
continue
169+
170+
sio.save_csv(
165171
labels,
166-
output_path=output_path,
167-
labels_path=args.input_path,
168-
all_frames=True,
172+
output_path,
173+
format="sleap",
169174
video=video,
170-
csv=True,
175+
include_score=True,
176+
include_empty=True,
177+
save_metadata=True,
171178
)
172179

173180
else:
174-
from sleap.info.write_tracking_h5 import main as write_analysis
181+
import sleap_io as sio
175182

176183
for video, output_path in zip(vids, outnames):
177-
write_analysis(
178-
labels,
179-
output_path=output_path,
180-
labels_path=args.input_path,
181-
all_frames=True,
182-
video=video,
183-
)
184+
try:
185+
sio.save_analysis_h5(
186+
labels,
187+
output_path,
188+
video=video,
189+
labels_path=args.input_path,
190+
all_frames=True,
191+
preset="matlab",
192+
)
193+
except ValueError as e:
194+
if "No labeled frames" in str(e):
195+
print(f"No labeled frames in {video.filename}. Skipping.")
196+
else:
197+
raise
184198

185199
elif len(args.outputs) > 0:
186200
print(f"Output SLEAP dataset: {args.outputs[0]}")

sleap/io/format/csv.py

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1-
"""Adaptor for writing SLEAP analysis as csv."""
1+
"""Adaptor for writing SLEAP analysis as CSV.
2+
3+
This adaptor uses sleap-io for CSV export, providing a consistent format
4+
with the analysis HDF5 export.
5+
"""
26

37
from sleap.io import format
48

59
from sleap_io import Labels, Video
10+
import sleap_io as sio
611

712

813
class CSVAdaptor(format.adaptor.Adaptor):
@@ -46,24 +51,34 @@ def write(
4651
source_path: str = None,
4752
video: Video = None,
4853
):
49-
"""Writes csv file for :py:class:`Labels` `source_object`.
54+
"""Writes CSV file for :py:class:`Labels` `source_object`.
5055
5156
Args:
5257
filename: The filename for the output file.
5358
source_object: The :py:class:`Labels` from which to get data from.
54-
source_path: Path for the labels object
55-
video: The :py:class:`Video` from which toget data from. If no `video` is
59+
source_path: Path for the labels object (stored as metadata).
60+
video: The :py:class:`Video` from which to get data from. If no `video` is
5661
specified, then the first video in `source_object` videos list will be
57-
used. If there are no :py:class:`Labeled Frame`s in the `video`,
62+
used. If there are no :py:class:`LabeledFrame`s in the `video`,
5863
then no analysis file will be written.
5964
"""
60-
from sleap.info.write_tracking_h5 import main as write_analysis
61-
62-
write_analysis(
63-
labels=source_object,
64-
output_path=filename,
65-
labels_path=source_path,
66-
all_frames=True,
65+
# Resolve video
66+
if video is None:
67+
video = source_object.videos[0] if source_object.videos else None
68+
69+
# Check for labeled frames before exporting (sleap-io may not raise error)
70+
if video is not None:
71+
labeled_frames = source_object.find(video)
72+
if not labeled_frames:
73+
print("No labeled frames in video. Skipping CSV export.")
74+
return
75+
76+
sio.save_csv(
77+
source_object,
78+
filename,
79+
format="sleap",
6780
video=video,
68-
csv=True,
81+
include_score=True,
82+
include_empty=True, # Include all frames from 0 to last labeled
83+
save_metadata=True,
6984
)

sleap/io/format/sleap_analysis.py

Lines changed: 37 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,20 @@
1-
"""
2-
Adaptor to read and write analysis HDF5 files.
1+
"""Adaptor to read and write analysis HDF5 files.
32
43
These contain location and track data, but lack other metadata included in a
54
full SLEAP dataset file.
65
7-
Note that this adaptor will use default track names and skeleton node names
8-
if these cannot be read from the HDF5 (some files have these, some don't).
6+
This adaptor uses sleap-io for both reading and writing analysis HDF5 files,
7+
providing a consistent format with additional metadata like dimension labels
8+
and skeleton symmetries.
99
1010
To determine whether this adaptor can read a file, we check it's an HDF5 file
1111
with a `track_occupancy` dataset.
1212
"""
1313

14-
import numpy as np
15-
1614
from typing import Union
1715

18-
from sleap_io import Labels, Video, LabeledFrame
19-
from sleap_io import Skeleton
20-
from sleap_io.model.instance import Track
16+
from sleap_io import Labels, Video
17+
import sleap_io as sio
2118

2219
from .adaptor import Adaptor, SleapObjectType
2320
from .filehandle import FileHandle
@@ -66,72 +63,18 @@ def read(
6663
*args,
6764
**kwargs,
6865
) -> Labels:
69-
connect_adj_nodes = False
70-
71-
if video is None:
72-
raise ValueError("Cannot read analysis hdf5 if no video specified.")
73-
74-
if not isinstance(video, Video):
75-
video = Video.from_filename(video)
76-
77-
f = file.file
78-
tracks_matrix = f["tracks"][:].T
79-
80-
# shape: frames * nodes * 2 * tracks
81-
frame_count, node_count, _, track_count = tracks_matrix.shape
82-
83-
if "track_names" in f and len(f["track_names"]):
84-
track_names_list = f["track_names"][:].T
85-
tracks = [
86-
Track(name=track_name.decode()) for track_name in track_names_list
87-
]
88-
else:
89-
tracks = [Track(name=f"track_{i}") for i in range(track_count)]
90-
91-
if "node_names" in f:
92-
node_names_dset = f["node_names"][:].T
93-
node_names = [name.decode() for name in node_names_dset]
94-
else:
95-
node_names = [f"node {i}" for i in range(node_count)]
96-
97-
skeleton = Skeleton()
98-
last_node_name = None
99-
for node_name in node_names:
100-
skeleton.add_node(node_name)
101-
if connect_adj_nodes and last_node_name:
102-
skeleton.add_edge(last_node_name, node_name)
103-
last_node_name = node_name
104-
105-
frames = []
106-
for frame_idx in range(frame_count):
107-
instances = []
108-
for track_idx in range(track_count):
109-
points = tracks_matrix[frame_idx, ..., track_idx]
110-
if not np.all(np.isnan(points)):
111-
point_scores = np.ones(len(points))
112-
# make everything a PredictedInstance since the usual use
113-
# case is to export predictions for analysis
114-
from sleap.sleap_io_adaptors.instance_utils import (
115-
predicted_instance_from_numpy_compat,
116-
)
117-
118-
instances.append(
119-
predicted_instance_from_numpy_compat(
120-
points=points,
121-
point_confidences=point_scores,
122-
skeleton=skeleton,
123-
track=tracks[track_idx],
124-
instance_score=1,
125-
)
126-
)
127-
if instances:
128-
frames.append(
129-
LabeledFrame(video=video, frame_idx=frame_idx, instances=instances)
130-
)
131-
132-
labels = Labels(labeled_frames=frames)
133-
labels.update()
134-
return labels
66+
"""Reads analysis HDF5 file using sleap-io.
67+
68+
Args:
69+
file: The file handle for the HDF5 file.
70+
video: The video to associate with the data. Can be a Video object
71+
or a path string.
72+
73+
Returns:
74+
Labels object with loaded pose data.
75+
"""
76+
# Use sleap-io's load_analysis_h5 which handles all format variants
77+
return sio.load_analysis_h5(file.filename, video=video)
13578

13679
@classmethod
13780
def write(
@@ -146,17 +89,25 @@ def write(
14689
Args:
14790
filename: The filename for the output file.
14891
source_object: The :py:class:`Labels` from which to get data from.
149-
video: The :py:class:`Video` from which toget data from. If no `video` is
92+
source_path: Path to the source labels file (stored as metadata).
93+
video: The :py:class:`Video` from which to get data from. If no `video` is
15094
specified, then the first video in `source_object` videos list will be
151-
used. If there are no :py:class:`Labeled Frame`s in the `video`,
95+
used. If there are no :py:class:`LabeledFrame`s in the `video`,
15296
then no analysis file will be written.
15397
"""
154-
from sleap.info.write_tracking_h5 import main as write_analysis
155-
156-
write_analysis(
157-
labels=source_object,
158-
output_path=filename,
159-
labels_path=source_path,
160-
all_frames=True,
161-
video=video,
162-
)
98+
try:
99+
sio.save_analysis_h5(
100+
source_object,
101+
filename,
102+
video=video,
103+
labels_path=source_path,
104+
all_frames=True,
105+
preset="matlab", # SLEAP-compatible format
106+
)
107+
except ValueError as e:
108+
# Handle case where video has no labeled frames
109+
# sleap-io raises ValueError, but we silently skip like old behavior
110+
if "No labeled frames" in str(e):
111+
print("No labeled frames in video. Skipping analysis export.")
112+
else:
113+
raise

0 commit comments

Comments
 (0)