Skip to content

write_tracking_h5

sleap.info.write_tracking_h5

Generate an HDF5 or CSV file with track occupancy and point location data.

Ignores tracks that are entirely empty. By default will also ignore empty frames from the beginning and end of video, although --all-frames argument will make it include empty frames from beginning of video.

The HDF5 file has these datasets:

  • "track_occupancy" (shape: tracks * frames)
  • "tracks" (shape: frames * nodes * 2 * tracks)
  • "track_names" (shape: tracks)
  • "node_names" (shape: nodes)
  • "edge_names" (shape: nodes - 1)
  • "edge_inds" (shape: nodes - 1)
  • "point_scores" (shape: frames * nodes * tracks)
  • "instance_scores" (shape: frames * tracks)
  • "tracking_scores" (shape: frames * tracks)
  • "labels_path": Path to the source .slp file (if available from GUI context)
  • "video_path": Path to the source :py:class:Video.
  • "video_ind": Scalar integer index of the video within the :py:class:Labels.
  • "provenance": Dictionary that denotes the origin of the :py:class:Labels.

Note: the datasets are stored column-major as expected by MATLAB.

Functions:

Name Description
get_edges_as_np_strings

Get list of edge names as bytes.

get_nodes_as_np_strings

Get list of node names as bytes.

get_occupancy_and_points_matrices

Builds numpy matrices with track occupancy and point location data.

get_tracks_as_np_strings

Get list of track names as bytes.

main

Writes HDF5 file with matrices of track occupancy and coordinates.

remove_empty_tracks_from_matrices

Removes matrix rows/columns for unoccupied tracks.

write_csv_file

Write CSV file with data from given dictionary.

write_occupancy_file

Write HDF5 file with data from given dictionary.

get_edges_as_np_strings(labels)

Get list of edge names as bytes.

Source code in sleap/info/write_tracking_h5.py
54
55
56
57
58
59
def get_edges_as_np_strings(labels: Labels) -> List[Tuple[bytes, bytes]]:
    """Get list of edge names as bytes."""
    return [
        (src_name.encode("utf-8"), dst_name.encode("utf-8"))
        for (src_name, dst_name) in labels.skeletons[0].edge_names
    ]

get_nodes_as_np_strings(labels)

Get list of node names as bytes.

Source code in sleap/info/write_tracking_h5.py
49
50
51
def get_nodes_as_np_strings(labels: Labels) -> List[bytes]:
    """Get list of node names as bytes."""
    return [node.name.encode("utf-8") for node in labels.skeletons[0].nodes]

get_occupancy_and_points_matrices(labels, all_frames, video=None)

Builds numpy matrices with track occupancy and point location data.

Note: This function assumes either all instances have tracks or no instances have tracks.

Parameters:

Name Type Description Default
labels Labels

The :py:class:Labels from which to get data.

required
all_frames bool

If True, then includes zeros so that frame index will line up with columns in the output. Otherwise, there will only be columns for the frames between the first and last frames with labeling data.

required
video Video

The :py:class:Video from which to get data. If no video is specified, then the first video in source_object videos list will be used. If there are no labeled frames in the video, then None will be returned.

None

Returns:

Type Description
Tuple[ndarray, ndarray, ndarray, ndarray, ndarray]

tuple of arrays:

  • occupancy matrix with shape (tracks, frames)
  • point location array with shape (frames, nodes, 2, tracks)
  • point scores array with shape (frames, nodes, tracks)
  • instance scores array with shape (frames, tracks)
  • tracking scores array with shape (frames, tracks)
Source code in sleap/info/write_tracking_h5.py
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
def get_occupancy_and_points_matrices(
    labels: Labels, all_frames: bool, video: Video = None
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Builds numpy matrices with track occupancy and point location data.

    Note: This function assumes either all instances have tracks or no instances have
    tracks.

    Args:
        labels: The :py:class:`Labels` from which to get data.
        all_frames: If True, then includes zeros so that frame index
            will line up with columns in the output. Otherwise,
            there will only be columns for the frames between the
            first and last frames with labeling data.
        video: The :py:class:`Video` from which to get data. If no `video` is specified,
            then the first video in `source_object` videos list will be used. If there
            are no labeled frames in the `video`, then None will be returned.

    Returns:
        tuple of arrays:

        * occupancy matrix with shape (tracks, frames)
        * point location array with shape (frames, nodes, 2, tracks)
        * point scores array with shape (frames, nodes, tracks)
        * instance scores array with shape (frames, tracks)
        * tracking scores array with shape (frames, tracks)
    """
    # Assumes either all instances have tracks or no instances have tracks
    track_count = len(labels.tracks) or 1
    node_count = len(labels.skeletons[0].nodes)

    # Retrieve frames from current video only
    try:
        if video is None:
            video = labels.videos[0]
    except IndexError:
        print("There are no videos in this project. No occupancy matrix to return.")
        return
    labeled_frames = labels.find(video)

    frame_idxs = [lf.frame_idx for lf in labeled_frames]
    frame_idxs.sort()

    try:
        first_frame_idx = 0 if all_frames else frame_idxs[0]

        frame_count = (
            frame_idxs[-1] - first_frame_idx + 1
        )  # count should include unlabeled frames
    except IndexError:
        print(f"No labeled frames in {video.filename}. No occupancy matrix to return.")
        return

    # Desired MATLAB format:
    # "track_occupancy"     tracks * frames
    # "tracks"              frames * nodes * 2 * tracks
    # "track_names"         tracks
    # "point_scores"        frames * nodes * tracks
    # "instance_scores"     frames * tracks
    # "tracking_scores"     frames * tracks

    occupancy_matrix = np.zeros((track_count, frame_count), dtype=np.uint8)
    locations_matrix = np.full(
        (frame_count, node_count, 2, track_count), np.nan, dtype=float
    )
    point_scores = np.full((frame_count, node_count, track_count), np.nan, dtype=float)
    instance_scores = np.full((frame_count, track_count), np.nan, dtype=float)
    tracking_scores = np.full((frame_count, track_count), np.nan, dtype=float)

    # Assumes either all instances have tracks or no instances have tracks
    # Prefer user-labeled instances over predicted instances
    tracks = labels.tracks or [None]  # Comparator in case of project with no tracks
    lfs_instances = list()
    warning_flag = False
    for lf in labeled_frames:
        user_instances = lf.user_instances
        predicted_instances = lf.predicted_instances
        for track in tracks:
            track_instances = list()
            # If a user-instance exists for this track, then use user-instance
            user_track_instances = [
                inst for inst in user_instances if inst.track == track
            ]
            if len(user_track_instances) > 0:
                track_instances = user_track_instances
            else:
                # Otherwise, if a predicted instance exists, then use the predicted
                predicted_track_instances = [
                    inst for inst in predicted_instances if inst.track == track
                ]
                if len(predicted_track_instances) > 0:
                    track_instances = predicted_track_instances

            lfs_instances.extend([(lf, inst) for inst in track_instances])

            # Set warning flag if more than one instances on a track in a single frame
            warning_flag = warning_flag or (
                (track is not None) and (len(track_instances) > 1)
            )

    if warning_flag:
        print(
            "\nWarning! There are more than one instances per track on a "
            "single frame.\n"
        )

    for lf, inst in lfs_instances:
        frame_i = lf.frame_idx - first_frame_idx
        # Assumes either all instances have tracks or no instances have tracks
        if inst.track is None:
            # We could use lf.instances.index(inst) but then we'd need
            # to calculate the number of "tracks" based on the max number of
            # instances in any frame, so for now we'll assume that there's
            # a single instance if we aren't using tracks.
            track_i = 0
        else:
            track_i = labels.tracks.index(inst.track)

        occupancy_matrix[track_i, frame_i] = 1

        locations_matrix[frame_i, ..., track_i] = inst.numpy()
        tracking_scores[frame_i, ..., track_i] = inst.tracking_score
        if type(inst) == PredictedInstance:
            from sleap.sleap_io_adaptors.instance_utils import instance_get_scores

            scores = instance_get_scores(inst)
            if scores is not None:
                point_scores[frame_i, ..., track_i] = scores
            else:
                # Fallback to NaN if no scores available
                point_scores[frame_i, ..., track_i] = np.nan
            instance_scores[frame_i, ..., track_i] = inst.score

    return (
        occupancy_matrix,
        locations_matrix,
        point_scores,
        instance_scores,
        tracking_scores,
    )

get_tracks_as_np_strings(labels)

Get list of track names as bytes.

Source code in sleap/info/write_tracking_h5.py
44
45
46
def get_tracks_as_np_strings(labels: Labels) -> List[bytes]:
    """Get list of track names as bytes."""
    return [track.name.encode("utf-8") for track in labels.tracks]

main(labels, output_path, labels_path=None, all_frames=True, video=None, csv=False)

Writes HDF5 file with matrices of track occupancy and coordinates.

Parameters:

Name Type Description Default
labels Labels

The :class:Labels from which to get data.

required
output_path str

Path of HDF5 file to create.

required
labels_path str

Path of labels .slp file.

None
all_frames bool

If True, then includes zeros so that frame index will line up with columns in the output. Otherwise, there will only be columns for the frames between the first and last frames with labeling data.

True
video Video

The :py:class:Video from which to get data. If no video is specified, then the first video in source_object videos list will be used. If there are no labeled frames in the video, then no output file will be written.

None
csv bool

Bool to save the analysis as a csv file if set to True

False

Returns:

Type Description

None

Source code in sleap/info/write_tracking_h5.py
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def main(
    labels: Labels,
    output_path: str,
    labels_path: str = None,
    all_frames: bool = True,
    video: Video = None,
    csv: bool = False,
):
    """Writes HDF5 file with matrices of track occupancy and coordinates.

    Args:
        labels: The :class:`Labels` from which to get data.
        output_path: Path of HDF5 file to create.
        labels_path: Path of `labels` .slp file.
        all_frames: If True, then includes zeros so that frame index
            will line up with columns in the output. Otherwise,
            there will only be columns for the frames between the
            first and last frames with labeling data.
        video: The :py:class:`Video` from which to get data. If no `video` is specified,
            then the first video in `source_object` videos list will be used. If there
            are no labeled frames in the `video`, then no output file will be written.
        csv: Bool to save the analysis as a csv file if set to True

    Returns:
        None
    """
    track_names = get_tracks_as_np_strings(labels)

    # Export analysis of current video only
    try:
        if video is None:
            video = labels.videos[0]
    except IndexError:
        print("There are no videos in this project. Output file will not be written.")
        return

    try:
        (
            occupancy_matrix,
            locations_matrix,
            point_scores,
            instance_scores,
            tracking_scores,
        ) = get_occupancy_and_points_matrices(labels, all_frames, video)
    except TypeError:
        print(
            f"No labeled frames in {video.filename}. Skipping the analysis for "
            "this video."
        )
        return

    (
        track_names,
        occupancy_matrix,
        locations_matrix,
        point_scores,
        instance_scores,
        tracking_scores,
    ) = remove_empty_tracks_from_matrices(
        track_names,
        occupancy_matrix,
        locations_matrix,
        point_scores,
        instance_scores,
        tracking_scores,
    )

    data_dict = dict(
        track_names=track_names,
        node_names=get_nodes_as_np_strings(labels),
        edge_names=get_edges_as_np_strings(labels),
        edge_inds=labels.skeletons[0].edge_inds,
        tracks=locations_matrix,
        track_occupancy=occupancy_matrix,
        point_scores=point_scores,
        instance_scores=instance_scores,
        tracking_scores=tracking_scores,
        labels_path=str(labels_path),  # NoneType cannot be written to hdf5.
        video_path=video.filename,
        video_ind=labels.videos.index(video),
        provenance=json.dumps(labels.provenance),  # dict cannot be written to hdf5.
    )

    if csv:
        write_csv_file(output_path, data_dict)
    else:
        write_occupancy_file(output_path, data_dict, transpose=True)

remove_empty_tracks_from_matrices(track_names, occupancy_matrix, locations_matrix, point_scores, instance_scores, tracking_scores)

Removes matrix rows/columns for unoccupied tracks.

Parameters:

Name Type Description Default
track_names List

List of track names

required
occupancy_matrix ndarray

2d numpy matrix, rows correspond to tracks

required
locations_matrix ndarray

4d numpy matrix, last index is track

required
point_scores ndarray

3d numpy matrix, last index is track

required
instance_scores ndarray

2d numpy matrix, last index is track

required
tracking_scores ndarray

2d numpy matrix, last index is track

required

Returns:

Type Description
Tuple[List, ndarray, ndarray, ndarray, ndarray, ndarray]

track_names, occupancy_matrix, locations_matrix, point_scores, instance_scores tracking_scores but without the rows/columns corresponding to unoccupied tracks.

Source code in sleap/info/write_tracking_h5.py
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
def remove_empty_tracks_from_matrices(
    track_names: List,
    occupancy_matrix: np.ndarray,
    locations_matrix: np.ndarray,
    point_scores: np.ndarray,
    instance_scores: np.ndarray,
    tracking_scores: np.ndarray,
) -> Tuple[List, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
    """Removes matrix rows/columns for unoccupied tracks.

    Args:
        track_names: List of track names
        occupancy_matrix: 2d numpy matrix, rows correspond to tracks
        locations_matrix: 4d numpy matrix, last index is track
        point_scores: 3d numpy matrix, last index is track
        instance_scores: 2d numpy matrix, last index is track
        tracking_scores: 2d numpy matrix, last index is track

    Returns:
        track_names, occupancy_matrix, locations_matrix, point_scores, instance_scores
        tracking_scores but without the rows/columns corresponding to unoccupied tracks.
    """
    # Make mask with only the occupied tracks
    occupied_track_mask = np.sum(occupancy_matrix, axis=1) > 0

    # Ignore unoccupied tracks
    if np.sum(~occupied_track_mask):
        print(f"ignoring {np.sum(~occupied_track_mask)} empty tracks")

        occupancy_matrix = occupancy_matrix[occupied_track_mask]
        locations_matrix = locations_matrix[..., occupied_track_mask]
        point_scores = point_scores[..., occupied_track_mask]
        instance_scores = instance_scores[..., occupied_track_mask]
        tracking_scores = tracking_scores[..., occupied_track_mask]
        track_names = [
            track_names[i] for i in range(len(track_names)) if occupied_track_mask[i]
        ]

    return (
        track_names,
        occupancy_matrix,
        locations_matrix,
        point_scores,
        instance_scores,
        tracking_scores,
    )

write_csv_file(output_path, data_dict)

Write CSV file with data from given dictionary.

Parameters:

Name Type Description Default
output_path

Path of HDF5 file.

required
data_dict

Dictionary with data to save. Keys are dataset names, values are the data.

required

Returns:

Type Description

None

Source code in sleap/info/write_tracking_h5.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
def write_csv_file(output_path, data_dict):
    """Write CSV file with data from given dictionary.

    Args:
        output_path: Path of HDF5 file.
        data_dict: Dictionary with data to save. Keys are dataset names,
            values are the data.

    Returns:
        None
    """

    if data_dict["tracks"].shape[-1] == 0:
        print(f"No tracks to export in {data_dict['video_path']}. Skipping the export")
        return

    data_dict["node_names"] = [s.decode() for s in data_dict["node_names"]]
    data_dict["track_names"] = [s.decode() for s in data_dict["track_names"]]
    data_dict["track_occupancy"] = np.transpose(data_dict["track_occupancy"]).astype(
        bool
    )

    # Find frames with at least one animal tracked.
    valid_frame_idxs = np.argwhere(data_dict["track_occupancy"].any(axis=1)).flatten()

    tracks = []
    for frame_idx in valid_frame_idxs:
        frame_tracks = data_dict["tracks"][frame_idx]

        for i in range(frame_tracks.shape[-1]):
            pts = frame_tracks[..., i]
            conf_scores = data_dict["point_scores"][frame_idx][..., i]

            if np.isnan(pts).all():
                # Skip if animal wasn't detected in the current frame.
                continue
            if data_dict["track_names"]:
                track = data_dict["track_names"][i]
            else:
                track = None

            instance_score = data_dict["instance_scores"][frame_idx][i]

            detection = {
                "track": track,
                "frame_idx": frame_idx,
                "instance.score": instance_score,
            }

            # Coordinates for each body part.
            for node_name, score, (x, y) in zip(
                data_dict["node_names"], conf_scores, pts
            ):
                detection[f"{node_name}.x"] = x
                detection[f"{node_name}.y"] = y
                detection[f"{node_name}.score"] = score

            tracks.append(detection)

    tracks = pd.DataFrame(tracks)
    tracks.to_csv(output_path, index=False)

write_occupancy_file(output_path, data_dict, transpose=True)

Write HDF5 file with data from given dictionary.

Parameters:

Name Type Description Default
output_path str

Path of HDF5 file.

required
data_dict Dict[str, Any]

Dictionary with data to save. Keys are dataset names, values are the data.

required
transpose bool

If True, then any ndarray in data dictionary will be transposed before saving. This is useful for writing files that will be imported into MATLAB, which expects data in column-major format.

True

Returns:

Type Description

None

Source code in sleap/info/write_tracking_h5.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def write_occupancy_file(
    output_path: str, data_dict: Dict[str, Any], transpose: bool = True
):
    """Write HDF5 file with data from given dictionary.

    Args:
        output_path: Path of HDF5 file.
        data_dict: Dictionary with data to save. Keys are dataset names,
            values are the data.
        transpose: If True, then any ndarray in data dictionary will be
            transposed before saving. This is useful for writing files
            that will be imported into MATLAB, which expects data in
            column-major format.

    Returns:
        None
    """

    with h5.File(output_path, "w") as f:
        print("\nExporting to SLEAP Analysis file...")
        for key, val in data_dict.items():
            print(f"\t{key}: ", end="")
            if isinstance(val, np.ndarray):
                print(f"{val.shape}")

                if transpose:
                    # Transpose since MATLAB expects column-major
                    f.create_dataset(
                        key,
                        data=np.transpose(val),
                        compression="gzip",
                        compression_opts=9,
                    )
                else:
                    f.create_dataset(
                        key, data=val, compression="gzip", compression_opts=9
                    )
            else:
                if isinstance(val, (str, int, type(None))):
                    print(f"{val}")
                else:
                    print(f"{len(val)}")
                f.create_dataset(key, data=val)

    print(f"Saved as {output_path}")