Skip to content

Segmentation

Spatial annotations: Centroids · Boxes · ROIs · Segmentation. These types nest per-frame on LabeledFrame — see Working with annotations in frames.

sleap-io represents pixel-level segmentation with two complementary types: SegmentationMask for per-object binary masks (run-length encoded), and LabelImage for a dense integer label image holding all objects of a frame in one array — the standard output of instance-segmentation tools like Cellpose and StarDist.


Segmentation masks

A SegmentationMask stores per-pixel binary annotations in a compact run-length encoded (RLE) format. RLE avoids storing the full raster array, making masks efficient for storage and serialization while still supporting fast conversion to and from numpy arrays. SegmentationMask is abstract — use UserSegmentationMask or PredictedSegmentationMask.

From a numpy array

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((480, 640), dtype=bool)
>>> mask_data[100:200, 150:300] = True  # rectangular region
>>> mask = sio.UserSegmentationMask.from_numpy(
...     mask_data,
... )
>>> print(mask.area)
15000
>>> print(mask.height)
480
>>> print(mask.width)
640

Decoding back to numpy

The .data property decodes the RLE back to a full boolean array:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> mask = sio.UserSegmentationMask.from_numpy(mask_data)
>>> decoded = mask.data
>>> print(decoded.shape)
(100, 100)
>>> print(decoded.dtype)
bool
>>> print(decoded.sum())
600

Bounding box of a mask

The .bbox property returns the tightest axis-aligned bounding box containing all foreground pixels as (x, y, width, height):

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> mask = sio.UserSegmentationMask.from_numpy(mask_data)
>>> print(mask.bbox)
(30.0, 20.0, 30.0, 20.0)

To get a full BoundingBox object (with metadata) instead of a raw tuple, use .to_bbox():

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> mask = sio.UserSegmentationMask.from_numpy(
...     mask_data, track=sio.Track(name="cell_A"), category="neuron",
... )
>>> bb = mask.to_bbox()
>>> print(bb.xyxy)
(30.0, 20.0, 60.0, 40.0)
>>> print(bb.track.name)
cell_A
>>> print(bb.centroid_xy)
(45.0, 30.0)

The returned BoundingBox inherits track, category, name, instance, and source from the mask. PredictedSegmentationMask produces a PredictedBoundingBox with the mask's score.

User vs. predicted segmentation masks

UserSegmentationMask and PredictedSegmentationMask distinguish human annotations from model predictions. PredictedSegmentationMask adds score and optional score_map fields:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> user_mask = sio.UserSegmentationMask.from_numpy(mask_data)
>>> print(user_mask.is_predicted)
False
>>> pred_mask = sio.PredictedSegmentationMask.from_numpy(
...     mask_data, score=0.87,
... )
>>> print(pred_mask.score)
0.87
>>> print(pred_mask.score_map)  # None unless explicitly set
None

The score_map field is an optional dense float32 array of shape (H, W) providing pixel-level confidence. It is stored separately in the SLP format using zlib compression to avoid bloating files.

Adopting predictions (human-in-the-loop)

PredictedSegmentationMask.to_user() converts a prediction into a UserSegmentationMask, the predicted -> human-correct -> retrain round-trip for masks (mirroring Instance.from_predicted for poses). It copies the mask raster and shared metadata (track, instance, name, category, source, tracking_score, scale, offset), drops the prediction-only fields (score, score_map), and records the source prediction on from_predicted:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> pred_mask = sio.PredictedSegmentationMask.from_numpy(mask_data, score=0.87)
>>> user_mask = pred_mask.to_user()
>>> print(user_mask.is_predicted)
False
>>> print(user_mask.from_predicted is pred_mask)
True

Pass to_user(link=False) for an unlinked copy. The from_predicted link is persisted to the SLP format as an index into the saved mask list (mirroring instance from_predicted), so it survives a save/load round-trip as long as the source prediction is also saved. Files written before this column existed load it as None.

This mirrors the pose flow, where a user Instance is created from a PredictedInstance with from_predicted= set. To adopt a prediction within a frame, append the user mask to frame.masks; the source prediction stays in the frame, exactly as predicted poses do, so the "predicted + user → replace" resolution can happen later at merge time (see Merging):

>>> import numpy as np
>>> import sleap_io as sio
>>> video = sio.Video(filename="example.mp4", open_backend=False)
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> pred_mask = sio.PredictedSegmentationMask.from_numpy(mask_data, score=0.87)
>>> frame = sio.LabeledFrame(video=video, frame_idx=0, masks=[pred_mask])
>>> frame.masks.append(pred_mask.to_user())
>>> print(len(frame.masks))  # prediction stays, user mask appended alongside
2

To find predicted masks that have not yet been corrected — the segmentation analogue of LabeledFrame.unused_predictions for poses — use LabeledFrame.unused_predicted_masks. A PredictedSegmentationMask is treated as adopted (and excluded) when a UserSegmentationMask in the same frame links to it via from_predicted (checked first), or, lacking a link, spatially overlaps it (bbox-centroid within 5 px). This drives the "retrain only what a human corrected" workflow:

>>> import numpy as np
>>> import sleap_io as sio
>>> video = sio.Video(filename="example.mp4", open_backend=False)
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> pred_a = sio.PredictedSegmentationMask.from_numpy(mask_data, score=0.87)
>>> pred_b = sio.PredictedSegmentationMask.from_numpy(mask_data, score=0.62)
>>> pred_b.offset = (500.0, 500.0)  # a separate prediction elsewhere in the frame
>>> frame = sio.LabeledFrame(video=video, frame_idx=0, masks=[pred_a, pred_b])
>>> frame.masks.append(pred_a.to_user())  # adopt pred_a, leave pred_b
>>> unused = frame.unused_predicted_masks  # only the uncorrected prediction
>>> len(unused), unused[0] is pred_b

Multi-resolution masks

Segmentation masks stored at lower resolution — e.g., from a model that downsamples inputs, or a cropped tile extracted from a larger image — can carry spatial metadata so they round-trip losslessly into image-pixel space.

SegmentationMask.scale is a (sx, sy) scale factor and offset is an (x, y) pixel shift. Together they define the transform image_coord = mask_coord / scale + offset. Use the stride= convenience argument to set an isotropic downsample ratio, or pass offset= directly:

>>> import numpy as np
>>> import sleap_io as sio
>>> half_res = np.zeros((240, 320), dtype=bool)
>>> half_res[60:120, 80:180] = True
>>> mask = sio.UserSegmentationMask.from_numpy(half_res, stride=2)
>>> print(mask.scale)  # (0.5, 0.5) — equivalent to stride=2
(0.5, 0.5)
>>> print(mask.bbox)   # bbox is returned in full-image coordinates
(160.0, 120.0, 200.0, 120.0)

Crop-space masks (e.g., from a detector that processes (H, W) tiles) use the offset= kwarg — mask.bbox then maps back into image-pixel space without any extra math on your side:

>>> import numpy as np
>>> import sleap_io as sio
>>> crop = np.zeros((50, 50), dtype=bool)
>>> crop[10:30, 15:40] = True
>>> mask = sio.UserSegmentationMask.from_numpy(crop, offset=(100.0, 200.0))
>>> print(mask.offset)
(100.0, 200.0)
>>> print(mask.bbox)  # offset + extent → image coordinates
(115.0, 210.0, 25.0, 20.0)

Call mask.resampled(target_height, target_width) to materialize the mask at a new resolution while preserving its spatial metadata for further transforms.

Also on LabelImage

The same scale / offset / resampled() convention applies to LabelImage — a single LabelImage can carry a whole frame's worth of objects at a downsampled resolution, with the object-id metadata mapping back to full-image pixel space via the same transform.

Metadata fields

Every segmentation mask can carry optional metadata:

Field Type Description
track Track \| None Tracking identity across frames
tracking_score float \| None Confidence of track identity assignment
identity Identity \| None Global cross-video re-ID identity (mirrors Instance.identity); persists via /identity_links (owner_type=3)
identity_score float \| None Confidence of the identity assignment
instance Instance \| None Linked pose instance
scale tuple[float, float] (sx, sy) spatial scale (default (1, 1))
offset tuple[float, float] (x, y) pixel offset (default (0, 0))
category str Class label (e.g., "neuron")
name str Human-readable name
source str Annotation source identifier

Rendering

Use sio.draw_masks to composite a sequence of segmentation masks onto an image, or include them in sio.render_image / sio.render_video overlays. See Rendering → Segmentation Overlays.

Label images

A LabelImage stores dense per-pixel instance segmentation for a single video frame, where each pixel value encodes which object occupies that pixel. Unlike a SegmentationMask — which is a binary mask for a single object — a LabelImage stores all objects in one integer array. Background pixels are 0, and each positive integer identifies a distinct object. The objects dict maps these IDs to metadata (track, category, name). LabelImage is abstract — use UserLabelImage or PredictedLabelImage.

From a numpy array

The from_numpy factory method is the easiest way to create a LabelImage. Pass an integer array where each unique positive value is an object:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((128, 128), dtype=np.int32)
>>> data[10:40, 10:40] = 1   # object 1
>>> data[60:90, 60:90] = 2   # object 2
>>> li = sio.UserLabelImage.from_numpy(data)
>>> print(li.n_objects)
2
>>> print(li.label_ids)
[1 2]

By default, from_numpy does not create tracks. Set create_tracks=True to auto-create one Track per unique label ID, or supply explicit tracks and categories:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((128, 128), dtype=np.int32)
>>> data[10:40, 10:40] = 1
>>> data[60:90, 60:90] = 2
>>> tracks = [sio.Track(name="cell_A"), sio.Track(name="cell_B")]
>>> li = sio.UserLabelImage.from_numpy(
...     data,
...     tracks=tracks,
...     categories=["neuron", "glia"],
... )
>>> print(li.tracks)
[Track(name='cell_A'), Track(name='cell_B')]
>>> print(li.categories)
{'neuron', 'glia'}

Lists are positional (tracks[0] maps to label 1, etc.). Dict mappings give explicit control:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((128, 128), dtype=np.int32)
>>> data[10:40, 10:40] = 5
>>> data[60:90, 60:90] = 10
>>> li = sio.UserLabelImage.from_numpy(
...     data,
...     tracks={5: sio.Track(name="A"), 10: sio.Track(name="B")},
...     categories={5: "neuron", 10: "glia"},
... )
>>> print(li.n_objects)
2

From segmentation masks

Compose a LabelImage from existing SegmentationMask objects. Each mask becomes one object with a unique label ID. Track, category, and name are inherited from each mask's metadata:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask1 = sio.UserSegmentationMask.from_numpy(
...     np.ones((64, 64), dtype=bool), track=sio.Track(name="A"),
... )
>>> mask2 = sio.UserSegmentationMask.from_numpy(
...     np.ones((64, 64), dtype=bool), track=sio.Track(name="B"),
... )
>>> li = sio.UserLabelImage.from_masks([mask1, mask2])
>>> print(li.n_objects)
1
>>> print(li.tracks)
[Track(name='A'), Track(name='B')]

From binary masks

When you have per-object binary masks — e.g., from SAM, Mask R-CNN, or similar instance segmentation tools — use from_binary_masks to composite them into a single LabelImage without constructing SegmentationMask objects first:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_a = np.zeros((64, 64), dtype=bool)
>>> mask_b = np.zeros((64, 64), dtype=bool)
>>> mask_a[10:30, 10:30] = True
>>> mask_b[40:60, 40:60] = True
>>> li = sio.PredictedLabelImage.from_binary_masks(
...     [mask_a, mask_b],
...     tracks=[sio.Track(name="cell_A"), sio.Track(name="cell_B")],
...     categories=["neuron", "glia"],
...     scores=[0.95, 0.87],
...     score=0.9,
... )
>>> print(li.n_objects)
2
>>> print(li.objects[1].track.name, li.objects[1].score)
cell_A 0.95

Accepts a list of (H, W) arrays, a stacked (N, H, W) array, or a single (H, W) array. Values are cast to bool (nonzero = True). Overlapping pixels are assigned to the last mask. Use create_tracks=True to auto-create tracks instead of providing them explicitly.

Use label_ids to control pixel values explicitly — useful when objects appear/disappear across frames and you need consistent values per track:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_a = np.zeros((64, 64), dtype=bool)
>>> mask_b = np.zeros((64, 64), dtype=bool)
>>> mask_a[10:30, 10:30] = True
>>> mask_b[40:60, 40:60] = True
>>> li = sio.PredictedLabelImage.from_binary_masks(
...     [mask_a, mask_b],
...     label_ids=[5, 10],
...     scores=[0.95, 0.87],
... )
>>> print(li.label_ids)
[ 5 10]

When to use which factory method

  • from_binary_masks: Per-object binary masks from SAM, Mask R-CNN, etc.
  • from_numpy: Pre-composited integer array from Cellpose, StarDist, etc.
  • from_masks: Existing SegmentationMask objects with rich metadata.
  • from_stack: (T, H, W) integer array for multiple frames at once.

Direct construction

For full control, construct directly with the data array and objects dict:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.array([[0, 1], [2, 0]], dtype=np.int32)
>>> li = sio.UserLabelImage(
...     data=data,
...     objects={
...         1: sio.LabelImage.Info(
...             track=sio.Track(name="cell_1"), category="neuron",
...         ),
...         2: sio.LabelImage.Info(
...             track=sio.Track(name="cell_2"), category="glia",
...         ),
...     },
... )
>>> print(li.n_objects)
2

Object metadata

Each non-zero label ID can have a LabelImage.Info entry in the objects dict:

Field Type Description
track Track \| None Cross-frame identity
category str Semantic class label (e.g., "neuron")
name str Human-readable name (e.g., "cell_042")
instance Instance \| None Linked pose instance
score float \| None Per-object confidence score

Label IDs not present in objects are treated as having default (empty) metadata.

Querying objects

Index with a Track to get the binary mask for that object:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((64, 64), dtype=np.int32)
>>> data[10:30, 10:30] = 1
>>> track = sio.Track(name="cell_A")
>>> li = sio.UserLabelImage.from_numpy(data, tracks={1: track})
>>> mask = li[track]
>>> print(mask.dtype)
bool
>>> print(mask.sum())
400

Test containment, iterate objects, or get a union mask by category:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((64, 64), dtype=np.int32)
>>> data[10:30, 10:30] = 1
>>> track = sio.Track(name="cell_A")
>>> li = sio.UserLabelImage.from_numpy(data, tracks={1: track})
>>> print(track in li)
True
>>> for track, category, mask in li.items():
...     print(f"{track.name}: {category}, {mask.sum()} px")
cell_A: , 400 px
Property Type Description
n_objects int Number of unique non-zero labels
label_ids np.ndarray Sorted array of non-zero label values
tracks list[Track] Tracks with non-None track in objects
categories set[str] Unique non-empty category strings
height int Image height in pixels
width int Image width in pixels

Decomposing to SegmentationMasks

A LabelImage can be decomposed into per-object binary SegmentationMask objects and reconstructed from them:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((64, 64), dtype=np.int32)
>>> data[10:30, 10:30] = 1
>>> data[40:60, 40:60] = 2
>>> li = sio.UserLabelImage.from_numpy(data)
>>> masks = li.to_masks()
>>> print(len(masks))
2
>>> print(masks[0].area)
400
>>> li2 = sio.UserLabelImage.from_masks(masks)
>>> print(li2.n_objects)
2

Extracting bounding boxes

Extract per-object bounding boxes directly from a LabelImage with .to_bboxes(). This is more efficient than decomposing to masks first, since it only needs the pixel extents (no full mask decode):

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((64, 64), dtype=np.int32)
>>> data[10:30, 10:30] = 1
>>> data[40:60, 40:60] = 2
>>> li = sio.UserLabelImage.from_numpy(
...     data,
...     tracks=[sio.Track(name="cell_A"), sio.Track(name="cell_B")],
...     categories=["neuron", "glia"],
... )
>>> bboxes = li.to_bboxes()
>>> print(len(bboxes))
2
>>> for bb in bboxes:
...     print(bb.track.name, bb.category, bb.xyxy, bb.centroid_xy)
cell_A Category(name="neuron") (10.0, 10.0, 30.0, 30.0) (20.0, 20.0)
cell_B Category(name="glia") (40.0, 40.0, 60.0, 60.0) (50.0, 50.0)

Each BoundingBox inherits track, category, name, instance, and source from the corresponding object. PredictedLabelImage produces PredictedBoundingBox objects with per-object scores (falling back to the image-level score when a per-object score is None). Bounding boxes are in image coordinates, respecting the label image's scale and offset.

User vs. predicted label images

UserLabelImage and PredictedLabelImage distinguish human annotations from model predictions. PredictedLabelImage adds score and optional score_map fields, and per-object scores can be set via LabelImage.Info.score:

>>> import numpy as np
>>> import sleap_io as sio
>>> data = np.zeros((128, 128), dtype=np.int32)
>>> data[10:40, 10:40] = 1
>>> data[60:90, 60:90] = 2
>>> pred_li = sio.PredictedLabelImage(
...     data=data,
...     objects={
...         1: sio.LabelImage.Info(category="neuron", score=0.92),
...         2: sio.LabelImage.Info(category="glia", score=0.78),
...     },
...     score=0.88,
... )
>>> print(pred_li.is_predicted)
True
>>> print(pred_li.score)
0.88
>>> print(pred_li.objects[1].score)
0.92

From a stack of frames

The from_stack factory method converts a (T, H, W) array (e.g., direct Cellpose output) into a list of LabelImage objects with consistent Track objects shared across frames:

>>> import numpy as np
>>> import sleap_io as sio
>>> masks = np.zeros((3, 64, 64), dtype=np.int32)
>>> masks[0, 10:30, 10:30] = 1
>>> masks[1, 10:30, 10:30] = 1
>>> masks[2, 20:40, 20:40] = 2
>>> label_images = sio.PredictedLabelImage.from_stack(
...     masks, create_tracks=True, score=1.0,
... )
>>> print(len(label_images))
3
>>> print(label_images[0].objects[1].track is label_images[1].objects[1].track)
True

TIFF I/O

Label images can be saved and loaded as TIFF files using the top-level functions sio.load_label_images() and sio.save_label_images(). See the TIFF Format Reference for details on file structures and sidecar metadata.

Streaming writes

For large datasets where holding all frames in memory is impractical, LabelImageWriter writes label images one at a time to an SLP file with constant memory usage:

import sleap_io as sio

video = sio.load_video("microscopy.tif")
with sio.LabelImageWriter("output.slp", video=video) as writer:
    for frame_idx, mask in enumerate(segmentation_results):
        li = sio.PredictedLabelImage.from_numpy(
            mask,
            source="cellpose:nuclei", create_tracks=True, score=1.0,
        )
        writer.add(li)
# File is finalized on context exit

The writer uses the chunked (T, H, W) format (v2.2) with write_direct_chunk for maximum throughput. The HDF5 file and pixel dataset are created lazily on the first add() call. All frames must have the same (H, W) dimensions.

Key features:

  • Constant memory: Only one frame's compressed data is in memory at a time.
  • Exponential growth: The dataset starts at initial_capacity frames and doubles when full, then is trimmed to the actual count on finalize.
  • Score maps: PredictedLabelImage score maps are supported.
  • Batch convenience: writer.add_batch(list_of_label_images) writes multiple frames in one call.

Merging label images

merge_label_images() combines label images from multiple SLP files into one, copying compressed chunks directly without decompression when possible:

import sleap_io as sio

merged = sio.merge_label_images(
    ["chunk_0.slp", "chunk_1.slp", "chunk_2.slp"],
    "merged.slp",
)
print(len(merged.label_images))  # Total across all sources

This is useful for parallelized segmentation workflows where each chunk of frames is processed independently and the results need to be combined. Videos are deduplicated by filename and tracks by name. All source files must have the same frame dimensions (H, W).

Normalizing label IDs

When label images come from different sources or segmentation runs, the same Track may have different pixel values in different frames. normalize_label_ids() rewrites pixel values so each Track gets a globally consistent label ID (1, 2, 3, ...) assigned in order of first appearance:

import sleap_io as sio

labels = sio.load_slp("segmented.slp")
track_map = sio.normalize_label_ids(labels.label_images, by="track")
# Now the same Track always has the same pixel value in every frame.

# Safe to stack into a (T, H, W) array:
import numpy as np

stack = np.stack([li.data for li in labels.label_images])

For semantic segmentation, group by category instead — all objects with the same category string merge into one pixel value per frame:

sio.normalize_label_ids(labels.label_images, by="category")

Rendering

Use sio.draw_label_image to composite a single LabelImage onto an arbitrary image, or pass overlay=label_stack_or_image to sio.render_image / sio.render_video. See Rendering → Segmentation Overlays for the full overlay pipeline.

Lazy loading

When loading SLP files, label image pixel data is loaded lazily — metadata (tracks, frame indices, categories) is available immediately, and the actual pixel array is decompressed only on first .data access:

labels = sio.load_slp("large_dataset.slp")

# Metadata queries — no decompression
li = labels.get_label_images(frame_idx=42)[0]
print(li.tracks)      # free
print(li.height)      # free (cached from metadata)

# Pixel data decompressed on first access, then cached
mask = li.data  # decompresses this frame only

This keeps memory usage proportional to the number of frames actually accessed rather than the total dataset size. The underlying HDF5 file handle is managed by the Labels object and can be explicitly closed with labels.close().

Converting between annotation types

sleap-io models five spatial detection modalities — pose (Instance), Centroid, BoundingBox, SegmentationMask, and ROI (polygon) — and every modality exposes the same verb set (.to_centroid(), .to_bbox(), .to_mask(), .to_roi()) so you can move freely between them. Geometry conversions are centralized on the two hubs that already exist — ROI (Shapely) for vector ops and SegmentationMask for raster — so obj.to_mask(height, width) is always obj.to_roi(...).to_mask(height, width).

The conversion matrix

from ↓ to → Centroid BoundingBox SegmentationMask ROI (polygon)
Pose (Instance) to_centroid(method=, node=, fallback=) to_bbox(mode=, size=, padding=, rotated=) to_mask(height, width, **roi_kwargs) to_roi(method=, node_radius=, edge_radius=, radius=)
Centroid to_bbox(size, padding=) to_mask(height, width, radius) to_roi(radius)
BoundingBox to_centroid() pad(padding) to_mask(height, width) to_roi()
SegmentationMask to_centroid(method=) to_bbox(padding=) to_polygon() / to_roi()
ROI to_centroid(representative=) to_bbox(padding=, rotated=) to_mask(height, width)

The reverse direction back to pose is Centroid.to_pose(skeleton=None), which reconstructs a single-node Instance (the only modality→pose conversion that is geometrically well-defined). bbox→pose and mask→pose are out of scope — they would require a target skeleton.

Key kwargs by verb:

Verb Key kwargs
Instance.to_centroid method (center_of_mass | bbox_center | anchor | geometric_median), node, fallback
Instance.to_bbox mode (tight | centered), size, padding, node, center_method, rotated
Instance.to_roi method (shapes | convex_hull), node_radius, edge_radius, radius, quad_segs
Centroid.to_bbox size (required), padding
Centroid.to_roi / to_mask radius (required)
*.to_bbox padding (scalar or (px, py)); rotated on ROI
SegmentationMask.to_centroid method (center_of_mass | bbox_center)
ROI.to_centroid representative

Predicted variants, metadata, and degenerate inputs

Every conversion returns the User*/Predicted* variant matching the source: a Predicted* input produces a Predicted* output carrying its score. The identifying metadata (track, category, name, source, and the instance= backref) is propagated where present; the geometry conversions drop tracking_score (the shape, not the track assignment, is being reinterpreted). to_user() is the separate predicted → user adoption path (see below), which faithfully carries the full annotation — including tracking_score, scale, and offset — dropping only the prediction-only fields (score, score_map).

The to_X() verbs are return-type stable: they always return the target class, never None. When the input is degenerate (no visible points, an empty mask, or an occluded anchor whose fallback is exhausted), the result is an empty target instance:

  • Centroid / BoundingBox → NaN coordinates (x = y = nan, all corners NaN).
  • ROI → empty Polygon().
  • SegmentationMask → all-background mask (zero foreground pixels).

Each verb accepts error_on_empty=False; pass error_on_empty=True to raise a ValueError instead of returning an empty result. A companion is_empty property is available on Centroid, BoundingBox, ROI, and SegmentationMask (mirroring Instance.is_empty) so an empty result is cheap to detect:

>>> import sleap_io as sio
>>> empty_box = sio.UserBoundingBox(
...     x1=float("nan"), y1=float("nan"), x2=float("nan"), y2=float("nan"),
... )
>>> print(empty_box.is_empty)
True

One case is not degenerate input: Instance.to_roi(method="shapes") with both node_radius == 0 and edge_radius == 0 is a misconfiguration and always raises ValueError, regardless of error_on_empty.

The LabelImage decompositions round out the table:

From To Method
PredictedSegmentationMask UserSegmentationMask pred_mask.to_user()
LabelImage list[SegmentationMask] li.to_masks()
LabelImage list[BoundingBox] li.to_bboxes()
list[SegmentationMask] LabelImage UserLabelImage.from_masks(masks)
>>> import sleap_io as sio
>>> # BoundingBox -> ROI -> SegmentationMask -> polygon ROI
>>> bbox = sio.UserBoundingBox(
...     x1=40, y1=35, x2=60, y2=65,
... )
>>> roi = bbox.to_roi()
>>> print(roi.area)
600.0
>>> mask = roi.to_mask(100, 100)
>>> print(mask.area)
600
>>> polygon_roi = mask.to_polygon()
>>> print(polygon_roi.area)
600.0

mask.to_polygon() (and its to_roi() alias) preserves prediction semantics: a PredictedSegmentationMask produces a PredictedROI carrying the mask's score (earlier versions always returned a UserROI, silently dropping the predicted variant):

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> pred_mask = sio.PredictedSegmentationMask.from_numpy(mask_data, score=0.9)
>>> pred_roi = pred_mask.to_roi()
>>> print(type(pred_roi).__name__)
PredictedROI
>>> print(pred_roi.score)
0.9

Reducing a mask to a point or box

SegmentationMask.to_centroid() reduces a mask to a single Centroid. method="center_of_mass" (default) takes the mean of the foreground pixels; method="bbox_center" takes the midpoint of the pixel bounding box (concave-robust). Both map back to image space via the mask's scale/offset. to_bbox(padding=) adds optional padding to the tight box:

>>> import numpy as np
>>> import sleap_io as sio
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> mask = sio.UserSegmentationMask.from_numpy(mask_data)
>>> print(mask.to_centroid().xy)
(44.5, 29.5)
>>> print(mask.to_centroid(method="bbox_center").xy)
(45.0, 30.0)
>>> print(mask.to_bbox(padding=4).xyxy)
(26.0, 16.0, 64.0, 44.0)

Batch conversion: LabeledFrame.convert / Labels.convert

Rather than a combinatorial set of helpers, LabeledFrame.convert (and Labels.convert) names the source and to modalities and forwards all remaining kwargs to the matching per-object to_<target>(). This reaches every cell of the matrix through one entry point. source reads from the matching per-frame list (instances, centroids, bboxes, masks, rois); inplace=True also appends the results to the frame:

>>> import numpy as np
>>> import sleap_io as sio
>>> video = sio.Video("test.mp4", open_backend=False)
>>> skeleton = sio.Skeleton(["head", "thorax", "abdomen"])
>>> inst = sio.Instance.from_numpy(
...     np.array([[10, 20], [30, 40], [50, 60]]), skeleton=skeleton,
... )
>>> lf = sio.LabeledFrame(video=video, frame_idx=0, instances=[inst])
>>> # pose -> centroid with an anchor node + fallback
>>> centroids = lf.convert(
...     to="centroid", method="anchor", node="thorax", fallback="center_of_mass",
... )
>>> print(centroids[0].xy, centroids[0].source)
(30.0, 40.0) anchor:thorax
>>> # pose -> mask (burns nodes + edges into a raster)
>>> masks = lf.convert(
...     to="mask", source="pose", method="shapes",
...     node_radius=6, edge_radius=3, height=80, width=80,
... )
>>> print(masks[0].area > 0)
True

Labels.convert maps LabeledFrame.convert over every frame and returns a single flat list of all produced annotations:

>>> import numpy as np
>>> import sleap_io as sio
>>> video = sio.Video("test.mp4", open_backend=False)
>>> mask_data = np.zeros((100, 100), dtype=bool)
>>> mask_data[20:40, 30:60] = True
>>> mask = sio.UserSegmentationMask.from_numpy(mask_data)
>>> lf = sio.LabeledFrame(video=video, frame_idx=0, masks=[mask])
>>> labels = sio.Labels([lf])
>>> boxes = labels.convert(to="bbox", source="mask", padding=4, inplace=True)
>>> print(boxes[0].xyxy)
(26.0, 16.0, 64.0, 44.0)
>>> print(len(lf.bboxes))  # appended in place
1

Converting to="pose" is only defined from a centroid source (it calls Centroid.to_pose); any other source raises a clear ValueError. Unknown modality strings also raise.


See also

  • Centroids, Boxes, ROIs — the other spatial annotation types.
  • Labels & Frames: Accessing masks/label images via labels.masks, labels.label_images, and get_masks() / get_label_images().
  • Rendering: Visualizing segmentation overlays on video frames.
  • TIFF Format: Reading and writing label images as TIFF with sidecar metadata.
  • SLP Format: HDF5 storage layout (blob and chunked formats).
  • Merging: Combining label images from multiple SLP files.

API reference

sleap_io.SegmentationMask

A segmentation mask stored as run-length encoded (RLE) data.

Attributes:

Name Type Description
rle_counts

Run-length encoded counts as a uint32 array. Alternating runs of 0s and 1s, starting with 0s.

height

Height of the mask in pixels.

width

Width of the mask in pixels.

name

Optional human-readable name for this mask.

category

Optional Category (class label, e.g. class name for detection) for this mask. Promoted from the legacy free-form string; None if unset. Mirrors Instance.category.

source

Optional string indicating the source of this annotation.

track

Optional Track this mask is associated with.

tracking_score

Confidence of the track identity assignment. None if unassigned or manually assigned.

identity

Optional global, ground-truth Identity for this mask -- the persistent cross-video animal identity / re-identification key. None if no global identity is assigned. Mirrors Instance.identity.

identity_score

Score associated with the identity assignment (e.g. the re-ID match similarity). None if unassigned or assigned manually. Kept separate from tracking_score (short-term tracklet vs long-term identity).

instance

Optional Instance this mask is associated with.

scale

Resolution ratio (sx, sy) where sx = mask_width / image_width and sy = mask_height / image_height. (1.0, 1.0) means full resolution. (0.5, 0.5) means half resolution (each mask pixel covers 2x2 image pixels). Coordinate mapping: image_coord = mask_coord / scale + offset.

offset

Origin (x, y) of the mask in image pixel coordinates.

identity_embedding

Optional Embedding describing this detection's appearance for re-identification. None by default.

category_score

Score associated with the category assignment (e.g. the classifier confidence). None if unassigned or assigned manually.

category_embedding

Optional Embedding describing this detection's appearance for classification. None by default.

Notes

Masks use identity-based equality (two mask objects are only equal if they are the same object in memory).

See Also

LabelImage: Dense integer label images (all objects in one array).

Methods:

Name Description
__attrs_post_init__

Validate that this class is not instantiated directly.

__init__

Method generated by attrs for class SegmentationMask.

__repr__

Method generated by attrs for class SegmentationMask.

__setattr__

Method generated by attrs for class SegmentationMask.

from_numpy

Create a SegmentationMask from a 2D numpy array.

resampled

Return a new mask resampled to the target dimensions.

to_bbox

Convert to a BoundingBox object.

to_centroid

Convert the mask to a centroid point.

to_polygon

Convert the mask to a polygon ROI via row-rectangle union.

to_roi

Convert the mask to an ROI (alias for to_polygon).

Source code in sleap_io/model/mask.py
@attrs.define(eq=False)
class SegmentationMask:
    """A segmentation mask stored as run-length encoded (RLE) data.

    Attributes:
        rle_counts: Run-length encoded counts as a uint32 array. Alternating runs
            of 0s and 1s, starting with 0s.
        height: Height of the mask in pixels.
        width: Width of the mask in pixels.
        name: Optional human-readable name for this mask.
        category: Optional `Category` (class label, e.g. class name for
            detection) for this mask. Promoted from the legacy free-form string;
            ``None`` if unset. Mirrors `Instance.category`.
        source: Optional string indicating the source of this annotation.
        track: Optional `Track` this mask is associated with.
        tracking_score: Confidence of the track identity assignment. ``None``
            if unassigned or manually assigned.
        identity: Optional global, ground-truth `Identity` for this mask -- the
            persistent cross-video animal identity / re-identification key. ``None``
            if no global identity is assigned. Mirrors `Instance.identity`.
        identity_score: Score associated with the `identity` assignment (e.g. the
            re-ID match similarity). ``None`` if unassigned or assigned manually.
            Kept separate from `tracking_score` (short-term tracklet vs long-term
            identity).
        instance: Optional `Instance` this mask is associated with.
        scale: Resolution ratio ``(sx, sy)`` where ``sx = mask_width / image_width``
            and ``sy = mask_height / image_height``. ``(1.0, 1.0)`` means full
            resolution. ``(0.5, 0.5)`` means half resolution (each mask pixel
            covers 2x2 image pixels). Coordinate mapping:
            ``image_coord = mask_coord / scale + offset``.
        offset: Origin ``(x, y)`` of the mask in image pixel coordinates.
        identity_embedding: Optional `Embedding` describing this detection's
            appearance for re-identification. ``None`` by default.
        category_score: Score associated with the `category` assignment (e.g. the
            classifier confidence). ``None`` if unassigned or assigned manually.
        category_embedding: Optional `Embedding` describing this detection's
            appearance for classification. ``None`` by default.

    Notes:
        Masks use identity-based equality (two mask objects are only equal if they
        are the same object in memory).

    See Also:
        ``LabelImage``: Dense integer label images (all objects in one array).
    """

    rle_counts: np.ndarray = attrs.field()
    height: int = attrs.field()
    width: int = attrs.field()
    name: str = attrs.field(default="")
    category: "Category | None" = attrs.field(default=None, converter=to_category)
    source: str = attrs.field(default="")
    track: "Track | None" = attrs.field(default=None)
    tracking_score: float | None = attrs.field(default=None)
    identity: "Identity | None" = attrs.field(default=None)
    identity_score: float | None = attrs.field(default=None)
    instance: "Instance | None" = attrs.field(default=None)
    _instance_idx: int = attrs.field(default=-1, repr=False, eq=False, init=False)
    scale: tuple[float, float] = attrs.field(default=(1.0, 1.0))
    offset: tuple[float, float] = attrs.field(default=(0.0, 0.0))
    identity_embedding: "Embedding | None" = attrs.field(default=None, repr=False)
    category_score: float | None = attrs.field(default=None)
    category_embedding: "Embedding | None" = attrs.field(default=None, repr=False)

    def __attrs_post_init__(self):
        """Validate that this class is not instantiated directly."""
        if type(self) is SegmentationMask:
            raise TypeError(
                "SegmentationMask is abstract. "
                "Use UserSegmentationMask or PredictedSegmentationMask."
            )
        if self.scale[0] <= 0 or self.scale[1] <= 0:
            raise ValueError(f"Scale values must be positive, got {self.scale}.")

    @property
    def is_predicted(self) -> bool:
        """Whether this mask is a model prediction."""
        return isinstance(self, PredictedSegmentationMask)

    @property
    def has_spatial_transform(self) -> bool:
        """Whether this mask has non-default scale or offset."""
        return self.scale != (1.0, 1.0) or self.offset != (0.0, 0.0)

    @property
    def image_extent(self) -> tuple[int, int]:
        """Image-space ``(height, width)`` this mask covers (excluding offset).

        Computed as ``(int(height / scale_y), int(width / scale_x))``.
        """
        return (
            int(self.height / self.scale[1]),
            int(self.width / self.scale[0]),
        )

    def resampled(self, target_height: int, target_width: int) -> Self:
        """Return a new mask resampled to the target dimensions.

        The returned mask has ``scale=(1.0, 1.0)`` and ``offset=(0.0, 0.0)``
        with the mask data resized using nearest-neighbor interpolation.

        Args:
            target_height: Target height in pixels.
            target_width: Target width in pixels.

        Returns:
            A new mask of the same concrete type with resampled data.
        """
        resized = _resize_nearest(self.data, target_height, target_width)
        kwargs: dict = dict(
            name=self.name,
            category=self.category,
            category_score=self.category_score,
            category_embedding=self.category_embedding,
            source=self.source,
            track=self.track,
            tracking_score=self.tracking_score,
            identity=self.identity,
            identity_score=self.identity_score,
            identity_embedding=self.identity_embedding,
            instance=self.instance,
            scale=(1.0, 1.0),
            offset=(0.0, 0.0),
        )
        if isinstance(self, UserSegmentationMask):
            # Preserve the provenance link so resampling a corrected mask keeps
            # its source prediction (mirrors track/instance preservation above).
            kwargs["from_predicted"] = self.from_predicted
        if isinstance(self, PredictedSegmentationMask):
            kwargs["score"] = self.score
            if self.score_map is not None:
                kwargs["score_map"] = _resize_nearest(
                    self.score_map, target_height, target_width
                )
            kwargs["score_map_scale"] = (1.0, 1.0)
            kwargs["score_map_offset"] = (0.0, 0.0)
        resampled = type(self).from_numpy(resized, **kwargs)
        # Carry the deferred instance index through (init=False, so set after
        # construction; mirrors to_user() preserving the lazy association).
        resampled._instance_idx = self._instance_idx
        return resampled

    @classmethod
    def from_numpy(
        cls,
        mask: np.ndarray,
        stride: float | None = None,
        **kwargs,
    ) -> "SegmentationMask":
        """Create a SegmentationMask from a 2D numpy array.

        A ``SegmentationMask`` is binary by design (one object per mask). If a
        multi-class or multi-instance integer array is passed in, the internal
        RLE cast would silently drop all class/instance distinctions. This
        method rejects such inputs with a pointed error instead.

        Args:
            mask: A 2D boolean or ``{0, 1}`` integer array of shape
                ``(height, width)``. Inputs with more than one distinct
                non-zero value are rejected.
            stride: Convenience for setting isotropic scale. If provided, sets
                ``scale = (1/stride, 1/stride)``. Overrides ``scale`` in kwargs.
            **kwargs: Additional keyword arguments passed to the constructor
                (including ``scale``, ``offset``, ``name``, ``category``, etc.).

        Returns:
            A `SegmentationMask` with RLE-encoded data.

        Raises:
            ValueError: If ``mask`` contains more than one distinct non-zero
                value. Use ``LabelImage.from_numpy`` (to keep all classes in
                one dense array) or ``LabelImage.from_binary_masks`` (to
                split per-class binaries) for multi-class inputs.
        """
        arr = np.asarray(mask)
        if arr.dtype != bool:
            nonzero = arr[arr != 0]
            if nonzero.size > 0:
                uniques = np.unique(nonzero)
                if uniques.size > 1:
                    preview = sorted(uniques.tolist())[:5]
                    raise ValueError(
                        f"SegmentationMask is binary (one object per mask) but "
                        f"got an array with {uniques.size} distinct non-zero "
                        f"values (e.g. {preview}). Use "
                        f"sleap_io.UserLabelImage.from_numpy(array) to keep all "
                        f"classes in one dense array, or "
                        f"sleap_io.UserLabelImage.from_binary_masks([...]) to "
                        f"split per-class binaries. To opt in to binarization "
                        f"explicitly, pass array.astype(bool)."
                    )
        if stride is not None:
            kwargs["scale"] = (1.0 / stride, 1.0 / stride)
        height, width = arr.shape
        rle_counts = _encode_rle(arr)
        return cls(rle_counts=rle_counts, height=height, width=width, **kwargs)

    @property
    def data(self) -> np.ndarray:
        """Decode the mask to a 2D boolean numpy array.

        Returns:
            A boolean array of shape (height, width).
        """
        return _decode_rle(self.rle_counts, self.height, self.width)

    @property
    def area(self) -> int:
        """Number of foreground (True) pixels in the mask."""
        # Sum the odd-indexed runs (1-runs)
        return int(sum(self.rle_counts[1::2]))

    @property
    def is_empty(self) -> bool:
        """Whether the mask has no foreground pixels.

        Mirrors `Instance.is_empty`. ``True`` when the mask area is zero.

        Returns:
            ``True`` if there are no foreground (True) pixels, else ``False``.
        """
        return self.area == 0

    @property
    def bbox(self) -> tuple[float, float, float, float]:
        """Bounding box of the mask as (x, y, width, height) in image coordinates.

        When ``scale`` or ``offset`` are non-default, the bounding box is
        transformed from mask-pixel space to image-pixel space using
        ``image_coord = mask_coord / scale + offset``.

        Returns:
            A tuple of (x, y, width, height) for the tightest axis-aligned
            bounding box containing all foreground pixels. Returns (0, 0, 0, 0)
            if the mask is empty.
        """
        mask = self.data
        rows = np.any(mask, axis=1)
        cols = np.any(mask, axis=0)

        if not np.any(rows):
            return (0.0, 0.0, 0.0, 0.0)

        rmin, rmax = np.where(rows)[0][[0, -1]]
        cmin, cmax = np.where(cols)[0][[0, -1]]

        sx, sy = self.scale
        ox, oy = self.offset
        return (
            float(cmin / sx + ox),
            float(rmin / sy + oy),
            float((cmax - cmin + 1) / sx),
            float((rmax - rmin + 1) / sy),
        )

    def to_centroid(
        self,
        method: str = "center_of_mass",
        error_on_empty: bool = False,
    ) -> "Centroid":
        """Convert the mask to a centroid point.

        Returns a ``UserCentroid`` or ``PredictedCentroid`` with metadata
        (track, tracking_score, identity, identity_score, category, name,
        source, instance) inherited from this mask. Coordinates are in image
        space (respecting
        ``scale``/``offset``).

        Args:
            method: How to compute the centroid. ``"center_of_mass"`` (default)
                uses the mean of foreground pixel coordinates mapped to image
                space. ``"bbox_center"`` uses the midpoint of the mask's tight
                bounding box (concave-robust).
            error_on_empty: If ``True``, raise ``ValueError`` when the mask has no
                foreground pixels instead of returning a degenerate (NaN)
                centroid.

        Returns:
            A ``Centroid`` at the computed location. For an empty mask, returns a
            degenerate centroid with ``x = y = nan`` (unless ``error_on_empty``).

        Raises:
            ValueError: If ``method`` is not recognized, or if the mask is empty
                and ``error_on_empty`` is ``True``.
        """
        from sleap_io.model.centroid import PredictedCentroid, UserCentroid

        if method not in ("center_of_mass", "bbox_center"):
            raise ValueError(
                f"Unknown method {method!r}. Expected 'center_of_mass' or "
                f"'bbox_center'."
            )

        cls = PredictedCentroid if self.is_predicted else UserCentroid
        kwargs: dict = dict(
            track=self.track,
            tracking_score=self.tracking_score,
            identity=self.identity,
            identity_score=self.identity_score,
            identity_embedding=self.identity_embedding,
            instance=self.instance,
            category=self.category,
            category_score=self.category_score,
            category_embedding=self.category_embedding,
            name=self.name,
            source=self.source,
        )
        if self.is_predicted:
            kwargs["score"] = self.score

        if self.is_empty:
            if error_on_empty:
                raise ValueError(
                    "Cannot compute centroid of an empty mask (no foreground pixels)."
                )
            return cls(x=float("nan"), y=float("nan"), **kwargs)

        if method == "center_of_mass":
            sx, sy = self.scale
            ox, oy = self.offset
            rows, cols = np.nonzero(self.data)
            x = float(cols.mean() / sx + ox)
            y = float(rows.mean() / sy + oy)
        else:  # bbox_center
            bx, by, bw, bh = self.bbox
            x = bx + bw / 2.0
            y = by + bh / 2.0

        return cls(x=x, y=y, **kwargs)

    def to_bbox(
        self,
        padding: float | tuple[float, float] = 0.0,
        error_on_empty: bool = False,
    ) -> "BoundingBox":
        """Convert to a BoundingBox object.

        Returns a ``UserBoundingBox`` or ``PredictedBoundingBox`` with metadata
        (track, tracking_score, identity, identity_score, category, name,
        source, instance) inherited from this mask. Coordinates are in image
        space (respecting scale/offset).

        Args:
            padding: Amount to inflate the tight bounding box, as a scalar (applied
                to both axes) or ``(px, py)``. Positive values expand the box,
                negative values shrink it. Defaults to ``0.0`` (no padding).
            error_on_empty: If ``True``, raise ``ValueError`` when the mask has no
                foreground pixels instead of returning a degenerate (NaN) box.

        Returns:
            A ``BoundingBox`` matching this mask's tight bounding box (with
            optional padding). For an empty mask, returns a degenerate box with
            all corners ``nan`` (unless ``error_on_empty``).

        Raises:
            ValueError: If the mask is empty and ``error_on_empty`` is ``True``.
        """
        from sleap_io.model.bbox import PredictedBoundingBox, UserBoundingBox

        cls = PredictedBoundingBox if self.is_predicted else UserBoundingBox
        kwargs: dict = dict(
            track=self.track,
            tracking_score=self.tracking_score,
            identity=self.identity,
            identity_score=self.identity_score,
            identity_embedding=self.identity_embedding,
            instance=self.instance,
            category=self.category,
            category_score=self.category_score,
            category_embedding=self.category_embedding,
            name=self.name,
            source=self.source,
        )
        if self.is_predicted:
            kwargs["score"] = self.score

        if self.is_empty:
            if error_on_empty:
                raise ValueError(
                    "Cannot compute bounding box of an empty mask (no foreground "
                    "pixels)."
                )
            nan = float("nan")
            return cls(x1=nan, y1=nan, x2=nan, y2=nan, angle=0.0, **kwargs)

        from sleap_io.model.roi import _apply_padding

        x, y, w, h = self.bbox
        x1, y1, x2, y2 = _apply_padding(x, y, x + w, y + h, padding)
        return cls(x1=x1, y1=y1, x2=x2, y2=y2, angle=0.0, **kwargs)

    def to_polygon(self) -> "ROI":
        """Convert the mask to a polygon ROI via row-rectangle union.

        Builds pixel-aligned rectangles for each horizontal run of foreground
        pixels, then merges them with Shapely's ``unary_union`` to produce an
        exact polygon boundary. Handles non-convex shapes and holes correctly.

        When ``scale`` or ``offset`` are non-default, the polygon coordinates
        are transformed from mask-pixel space to image-pixel space.

        Returns:
            An `ROI` with polygon geometry derived from the mask. Returns an
            ROI with an empty polygon if the mask has no foreground pixels.
        """
        from shapely.geometry import Polygon, box
        from shapely.ops import unary_union

        from sleap_io.model.roi import PredictedROI, UserROI

        sx, sy = self.scale
        ox, oy = self.offset

        mask = self.data
        rectangles = []
        for y in range(self.height):
            row = mask[y].astype(np.uint8)
            diff = np.diff(np.concatenate([[0], row, [0]]))
            starts = np.where(diff == 1)[0]
            ends = np.where(diff == -1)[0]
            for s, e in zip(starts, ends):
                rectangles.append(
                    box(s / sx + ox, y / sy + oy, e / sx + ox, (y + 1) / sy + oy)
                )

        if not rectangles:
            geometry = Polygon()
        else:
            geometry = unary_union(rectangles)

        cls = PredictedROI if self.is_predicted else UserROI
        kwargs: dict = dict(
            geometry=geometry,
            name=self.name,
            category=self.category,
            category_score=self.category_score,
            category_embedding=self.category_embedding,
            source=self.source,
            track=self.track,
            tracking_score=self.tracking_score,
            identity=self.identity,
            identity_score=self.identity_score,
            identity_embedding=self.identity_embedding,
            instance=self.instance,
        )
        if self.is_predicted:
            kwargs["score"] = self.score
        return cls(**kwargs)

    def to_roi(self) -> "ROI":
        """Convert the mask to an ROI (alias for `to_polygon`).

        Returns:
            An `ROI` with polygon geometry derived from the mask. See
            `to_polygon` for details.
        """
        return self.to_polygon()

__annotations__ = {'rle_counts': 'np.ndarray', 'height': 'int', 'width': 'int', 'name': 'str', 'category': "'Category | None'", 'source': 'str', 'track': "'Track | None'", 'tracking_score': 'float | None', 'identity': "'Identity | None'", 'identity_score': 'float | None', 'instance': "'Instance | None'", '_instance_idx': 'int', 'scale': 'tuple[float, float]', 'offset': 'tuple[float, float]', 'identity_embedding': "'Embedding | None'", 'category_score': 'float | None', 'category_embedding': "'Embedding | None'"} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = True class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = "A segmentation mask stored as run-length encoded (RLE) data.\n\nAttributes:\n rle_counts: Run-length encoded counts as a uint32 array. Alternating runs\n of 0s and 1s, starting with 0s.\n height: Height of the mask in pixels.\n width: Width of the mask in pixels.\n name: Optional human-readable name for this mask.\n category: Optional `Category` (class label, e.g. class name for\n detection) for this mask. Promoted from the legacy free-form string;\n ``None`` if unset. Mirrors `Instance.category`.\n source: Optional string indicating the source of this annotation.\n track: Optional `Track` this mask is associated with.\n tracking_score: Confidence of the track identity assignment. ``None``\n if unassigned or manually assigned.\n identity: Optional global, ground-truth `Identity` for this mask -- the\n persistent cross-video animal identity / re-identification key. ``None``\n if no global identity is assigned. Mirrors `Instance.identity`.\n identity_score: Score associated with the `identity` assignment (e.g. the\n re-ID match similarity). ``None`` if unassigned or assigned manually.\n Kept separate from `tracking_score` (short-term tracklet vs long-term\n identity).\n instance: Optional `Instance` this mask is associated with.\n scale: Resolution ratio ``(sx, sy)`` where ``sx = mask_width / image_width``\n and ``sy = mask_height / image_height``. ``(1.0, 1.0)`` means full\n resolution. ``(0.5, 0.5)`` means half resolution (each mask pixel\n covers 2x2 image pixels). Coordinate mapping:\n ``image_coord = mask_coord / scale + offset``.\n offset: Origin ``(x, y)`` of the mask in image pixel coordinates.\n identity_embedding: Optional `Embedding` describing this detection's\n appearance for re-identification. ``None`` by default.\n category_score: Score associated with the `category` assignment (e.g. the\n classifier confidence). ``None`` if unassigned or assigned manually.\n category_embedding: Optional `Embedding` describing this detection's\n appearance for classification. ``None`` by default.\n\nNotes:\n Masks use identity-based equality (two mask objects are only equal if they\n are the same object in memory).\n\nSee Also:\n ``LabelImage``: Dense integer label images (all objects in one array).\n" class-attribute

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

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

__firstlineno__ = 121 class-attribute

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

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

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

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

__match_args__ = ('rle_counts', 'height', 'width', 'name', 'category', 'source', 'track', 'tracking_score', 'identity', 'identity_score', 'instance', 'scale', 'offset', 'identity_embedding', 'category_score', 'category_embedding') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.mask' class-attribute

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

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

__slots__ = ('rle_counts', 'height', 'width', 'name', 'category', 'source', 'track', 'tracking_score', 'identity', 'identity_score', 'instance', '_instance_idx', 'scale', 'offset', 'identity_embedding', 'category_score', 'category_embedding', '__weakref__') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__weakref__ property

list of weak references to the object

area property

Number of foreground (True) pixels in the mask.

bbox property

Bounding box of the mask as (x, y, width, height) in image coordinates.

When scale or offset are non-default, the bounding box is transformed from mask-pixel space to image-pixel space using image_coord = mask_coord / scale + offset.

Returns:

Type Description

A tuple of (x, y, width, height) for the tightest axis-aligned bounding box containing all foreground pixels. Returns (0, 0, 0, 0) if the mask is empty.

data property

Decode the mask to a 2D boolean numpy array.

Returns:

Type Description

A boolean array of shape (height, width).

has_spatial_transform property

Whether this mask has non-default scale or offset.

image_extent property

Image-space (height, width) this mask covers (excluding offset).

Computed as (int(height / scale_y), int(width / scale_x)).

is_empty property

Whether the mask has no foreground pixels.

Mirrors Instance.is_empty. True when the mask area is zero.

Returns:

Type Description

True if there are no foreground (True) pixels, else False.

is_predicted property

Whether this mask is a model prediction.

__attrs_post_init__()

Validate that this class is not instantiated directly.

Source code in sleap_io/model/mask.py
def __attrs_post_init__(self):
    """Validate that this class is not instantiated directly."""
    if type(self) is SegmentationMask:
        raise TypeError(
            "SegmentationMask is abstract. "
            "Use UserSegmentationMask or PredictedSegmentationMask."
        )
    if self.scale[0] <= 0 or self.scale[1] <= 0:
        raise ValueError(f"Scale values must be positive, got {self.scale}.")

__init__(rle_counts, height, width, name='', category=None, source='', track=None, tracking_score=None, identity=None, identity_score=None, instance=None, scale=(1.0, 1.0), offset=(0.0, 0.0), identity_embedding=None, category_score=None, category_embedding=None)

Method generated by attrs for class SegmentationMask.

Source code in sleap_io/model/mask.py
  segmentation tool (Cellpose, StarDist) where each pixel value identifies
  an object.
- To convert: ``LabelImage.to_masks()`` decomposes into per-object masks,
  and ``LabelImage.from_masks(masks)`` composes masks into a label image.

See Also:
    ``sleap_io.model.label_image``: Dense integer label images.
"""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

if TYPE_CHECKING:

__repr__()

Method generated by attrs for class SegmentationMask.

Source code in sleap_io/model/mask.py
"""Data structures for segmentation mask annotations.

Segmentation masks represent raster (per-pixel) annotations stored in
run-length encoded (RLE) format for compact storage. They can be converted
to and from numpy arrays and polygon representations.

Each ``SegmentationMask`` stores a single binary mask for one object. For
dense per-pixel segmentation where all objects are stored in one integer
array, see ``LabelImage`` in ``sleap_io.model.label_image``.

**When to use SegmentationMask vs LabelImage:**

- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows).
- Use ``LabelImage`` when you have a dense integer array from an instance

__setattr__(name, val)

Method generated by attrs for class SegmentationMask.

from_numpy(mask, stride=None, **kwargs) classmethod

Create a SegmentationMask from a 2D numpy array.

A SegmentationMask is binary by design (one object per mask). If a multi-class or multi-instance integer array is passed in, the internal RLE cast would silently drop all class/instance distinctions. This method rejects such inputs with a pointed error instead.

Parameters:

Name Type Description Default
mask ndarray

A 2D boolean or {0, 1} integer array of shape (height, width). Inputs with more than one distinct non-zero value are rejected.

required
stride float | None

Convenience for setting isotropic scale. If provided, sets scale = (1/stride, 1/stride). Overrides scale in kwargs.

None
**kwargs

Additional keyword arguments passed to the constructor (including scale, offset, name, category, etc.).

required

Returns:

Type Description
SegmentationMask

A SegmentationMask with RLE-encoded data.

Raises:

Type Description
ValueError

If mask contains more than one distinct non-zero value. Use LabelImage.from_numpy (to keep all classes in one dense array) or LabelImage.from_binary_masks (to split per-class binaries) for multi-class inputs.

Source code in sleap_io/model/mask.py
@classmethod
def from_numpy(
    cls,
    mask: np.ndarray,
    stride: float | None = None,
    **kwargs,
) -> "SegmentationMask":
    """Create a SegmentationMask from a 2D numpy array.

    A ``SegmentationMask`` is binary by design (one object per mask). If a
    multi-class or multi-instance integer array is passed in, the internal
    RLE cast would silently drop all class/instance distinctions. This
    method rejects such inputs with a pointed error instead.

    Args:
        mask: A 2D boolean or ``{0, 1}`` integer array of shape
            ``(height, width)``. Inputs with more than one distinct
            non-zero value are rejected.
        stride: Convenience for setting isotropic scale. If provided, sets
            ``scale = (1/stride, 1/stride)``. Overrides ``scale`` in kwargs.
        **kwargs: Additional keyword arguments passed to the constructor
            (including ``scale``, ``offset``, ``name``, ``category``, etc.).

    Returns:
        A `SegmentationMask` with RLE-encoded data.

    Raises:
        ValueError: If ``mask`` contains more than one distinct non-zero
            value. Use ``LabelImage.from_numpy`` (to keep all classes in
            one dense array) or ``LabelImage.from_binary_masks`` (to
            split per-class binaries) for multi-class inputs.
    """
    arr = np.asarray(mask)
    if arr.dtype != bool:
        nonzero = arr[arr != 0]
        if nonzero.size > 0:
            uniques = np.unique(nonzero)
            if uniques.size > 1:
                preview = sorted(uniques.tolist())[:5]
                raise ValueError(
                    f"SegmentationMask is binary (one object per mask) but "
                    f"got an array with {uniques.size} distinct non-zero "
                    f"values (e.g. {preview}). Use "
                    f"sleap_io.UserLabelImage.from_numpy(array) to keep all "
                    f"classes in one dense array, or "
                    f"sleap_io.UserLabelImage.from_binary_masks([...]) to "
                    f"split per-class binaries. To opt in to binarization "
                    f"explicitly, pass array.astype(bool)."
                )
    if stride is not None:
        kwargs["scale"] = (1.0 / stride, 1.0 / stride)
    height, width = arr.shape
    rle_counts = _encode_rle(arr)
    return cls(rle_counts=rle_counts, height=height, width=width, **kwargs)

resampled(target_height, target_width)

Return a new mask resampled to the target dimensions.

The returned mask has scale=(1.0, 1.0) and offset=(0.0, 0.0) with the mask data resized using nearest-neighbor interpolation.

Parameters:

Name Type Description Default
target_height int

Target height in pixels.

required
target_width int

Target width in pixels.

required

Returns:

Type Description
Self

A new mask of the same concrete type with resampled data.

Source code in sleap_io/model/mask.py
def resampled(self, target_height: int, target_width: int) -> Self:
    """Return a new mask resampled to the target dimensions.

    The returned mask has ``scale=(1.0, 1.0)`` and ``offset=(0.0, 0.0)``
    with the mask data resized using nearest-neighbor interpolation.

    Args:
        target_height: Target height in pixels.
        target_width: Target width in pixels.

    Returns:
        A new mask of the same concrete type with resampled data.
    """
    resized = _resize_nearest(self.data, target_height, target_width)
    kwargs: dict = dict(
        name=self.name,
        category=self.category,
        category_score=self.category_score,
        category_embedding=self.category_embedding,
        source=self.source,
        track=self.track,
        tracking_score=self.tracking_score,
        identity=self.identity,
        identity_score=self.identity_score,
        identity_embedding=self.identity_embedding,
        instance=self.instance,
        scale=(1.0, 1.0),
        offset=(0.0, 0.0),
    )
    if isinstance(self, UserSegmentationMask):
        # Preserve the provenance link so resampling a corrected mask keeps
        # its source prediction (mirrors track/instance preservation above).
        kwargs["from_predicted"] = self.from_predicted
    if isinstance(self, PredictedSegmentationMask):
        kwargs["score"] = self.score
        if self.score_map is not None:
            kwargs["score_map"] = _resize_nearest(
                self.score_map, target_height, target_width
            )
        kwargs["score_map_scale"] = (1.0, 1.0)
        kwargs["score_map_offset"] = (0.0, 0.0)
    resampled = type(self).from_numpy(resized, **kwargs)
    # Carry the deferred instance index through (init=False, so set after
    # construction; mirrors to_user() preserving the lazy association).
    resampled._instance_idx = self._instance_idx
    return resampled

to_bbox(padding=0.0, error_on_empty=False)

Convert to a BoundingBox object.

Returns a UserBoundingBox or PredictedBoundingBox with metadata (track, tracking_score, identity, identity_score, category, name, source, instance) inherited from this mask. Coordinates are in image space (respecting scale/offset).

Parameters:

Name Type Description Default
padding float | tuple[float, float]

Amount to inflate the tight bounding box, as a scalar (applied to both axes) or (px, py). Positive values expand the box, negative values shrink it. Defaults to 0.0 (no padding).

0.0
error_on_empty bool

If True, raise ValueError when the mask has no foreground pixels instead of returning a degenerate (NaN) box.

False

Returns:

Type Description
BoundingBox

A BoundingBox matching this mask's tight bounding box (with optional padding). For an empty mask, returns a degenerate box with all corners nan (unless error_on_empty).

Raises:

Type Description
ValueError

If the mask is empty and error_on_empty is True.

Source code in sleap_io/model/mask.py
def to_bbox(
    self,
    padding: float | tuple[float, float] = 0.0,
    error_on_empty: bool = False,
) -> "BoundingBox":
    """Convert to a BoundingBox object.

    Returns a ``UserBoundingBox`` or ``PredictedBoundingBox`` with metadata
    (track, tracking_score, identity, identity_score, category, name,
    source, instance) inherited from this mask. Coordinates are in image
    space (respecting scale/offset).

    Args:
        padding: Amount to inflate the tight bounding box, as a scalar (applied
            to both axes) or ``(px, py)``. Positive values expand the box,
            negative values shrink it. Defaults to ``0.0`` (no padding).
        error_on_empty: If ``True``, raise ``ValueError`` when the mask has no
            foreground pixels instead of returning a degenerate (NaN) box.

    Returns:
        A ``BoundingBox`` matching this mask's tight bounding box (with
        optional padding). For an empty mask, returns a degenerate box with
        all corners ``nan`` (unless ``error_on_empty``).

    Raises:
        ValueError: If the mask is empty and ``error_on_empty`` is ``True``.
    """
    from sleap_io.model.bbox import PredictedBoundingBox, UserBoundingBox

    cls = PredictedBoundingBox if self.is_predicted else UserBoundingBox
    kwargs: dict = dict(
        track=self.track,
        tracking_score=self.tracking_score,
        identity=self.identity,
        identity_score=self.identity_score,
        identity_embedding=self.identity_embedding,
        instance=self.instance,
        category=self.category,
        category_score=self.category_score,
        category_embedding=self.category_embedding,
        name=self.name,
        source=self.source,
    )
    if self.is_predicted:
        kwargs["score"] = self.score

    if self.is_empty:
        if error_on_empty:
            raise ValueError(
                "Cannot compute bounding box of an empty mask (no foreground "
                "pixels)."
            )
        nan = float("nan")
        return cls(x1=nan, y1=nan, x2=nan, y2=nan, angle=0.0, **kwargs)

    from sleap_io.model.roi import _apply_padding

    x, y, w, h = self.bbox
    x1, y1, x2, y2 = _apply_padding(x, y, x + w, y + h, padding)
    return cls(x1=x1, y1=y1, x2=x2, y2=y2, angle=0.0, **kwargs)

to_centroid(method='center_of_mass', error_on_empty=False)

Convert the mask to a centroid point.

Returns a UserCentroid or PredictedCentroid with metadata (track, tracking_score, identity, identity_score, category, name, source, instance) inherited from this mask. Coordinates are in image space (respecting scale/offset).

Parameters:

Name Type Description Default
method str

How to compute the centroid. "center_of_mass" (default) uses the mean of foreground pixel coordinates mapped to image space. "bbox_center" uses the midpoint of the mask's tight bounding box (concave-robust).

'center_of_mass'
error_on_empty bool

If True, raise ValueError when the mask has no foreground pixels instead of returning a degenerate (NaN) centroid.

False

Returns:

Type Description
Centroid

A Centroid at the computed location. For an empty mask, returns a degenerate centroid with x = y = nan (unless error_on_empty).

Raises:

Type Description
ValueError

If method is not recognized, or if the mask is empty and error_on_empty is True.

Source code in sleap_io/model/mask.py
def to_centroid(
    self,
    method: str = "center_of_mass",
    error_on_empty: bool = False,
) -> "Centroid":
    """Convert the mask to a centroid point.

    Returns a ``UserCentroid`` or ``PredictedCentroid`` with metadata
    (track, tracking_score, identity, identity_score, category, name,
    source, instance) inherited from this mask. Coordinates are in image
    space (respecting
    ``scale``/``offset``).

    Args:
        method: How to compute the centroid. ``"center_of_mass"`` (default)
            uses the mean of foreground pixel coordinates mapped to image
            space. ``"bbox_center"`` uses the midpoint of the mask's tight
            bounding box (concave-robust).
        error_on_empty: If ``True``, raise ``ValueError`` when the mask has no
            foreground pixels instead of returning a degenerate (NaN)
            centroid.

    Returns:
        A ``Centroid`` at the computed location. For an empty mask, returns a
        degenerate centroid with ``x = y = nan`` (unless ``error_on_empty``).

    Raises:
        ValueError: If ``method`` is not recognized, or if the mask is empty
            and ``error_on_empty`` is ``True``.
    """
    from sleap_io.model.centroid import PredictedCentroid, UserCentroid

    if method not in ("center_of_mass", "bbox_center"):
        raise ValueError(
            f"Unknown method {method!r}. Expected 'center_of_mass' or "
            f"'bbox_center'."
        )

    cls = PredictedCentroid if self.is_predicted else UserCentroid
    kwargs: dict = dict(
        track=self.track,
        tracking_score=self.tracking_score,
        identity=self.identity,
        identity_score=self.identity_score,
        identity_embedding=self.identity_embedding,
        instance=self.instance,
        category=self.category,
        category_score=self.category_score,
        category_embedding=self.category_embedding,
        name=self.name,
        source=self.source,
    )
    if self.is_predicted:
        kwargs["score"] = self.score

    if self.is_empty:
        if error_on_empty:
            raise ValueError(
                "Cannot compute centroid of an empty mask (no foreground pixels)."
            )
        return cls(x=float("nan"), y=float("nan"), **kwargs)

    if method == "center_of_mass":
        sx, sy = self.scale
        ox, oy = self.offset
        rows, cols = np.nonzero(self.data)
        x = float(cols.mean() / sx + ox)
        y = float(rows.mean() / sy + oy)
    else:  # bbox_center
        bx, by, bw, bh = self.bbox
        x = bx + bw / 2.0
        y = by + bh / 2.0

    return cls(x=x, y=y, **kwargs)

to_polygon()

Convert the mask to a polygon ROI via row-rectangle union.

Builds pixel-aligned rectangles for each horizontal run of foreground pixels, then merges them with Shapely's unary_union to produce an exact polygon boundary. Handles non-convex shapes and holes correctly.

When scale or offset are non-default, the polygon coordinates are transformed from mask-pixel space to image-pixel space.

Returns:

Type Description
ROI

An ROI with polygon geometry derived from the mask. Returns an ROI with an empty polygon if the mask has no foreground pixels.

Source code in sleap_io/model/mask.py
def to_polygon(self) -> "ROI":
    """Convert the mask to a polygon ROI via row-rectangle union.

    Builds pixel-aligned rectangles for each horizontal run of foreground
    pixels, then merges them with Shapely's ``unary_union`` to produce an
    exact polygon boundary. Handles non-convex shapes and holes correctly.

    When ``scale`` or ``offset`` are non-default, the polygon coordinates
    are transformed from mask-pixel space to image-pixel space.

    Returns:
        An `ROI` with polygon geometry derived from the mask. Returns an
        ROI with an empty polygon if the mask has no foreground pixels.
    """
    from shapely.geometry import Polygon, box
    from shapely.ops import unary_union

    from sleap_io.model.roi import PredictedROI, UserROI

    sx, sy = self.scale
    ox, oy = self.offset

    mask = self.data
    rectangles = []
    for y in range(self.height):
        row = mask[y].astype(np.uint8)
        diff = np.diff(np.concatenate([[0], row, [0]]))
        starts = np.where(diff == 1)[0]
        ends = np.where(diff == -1)[0]
        for s, e in zip(starts, ends):
            rectangles.append(
                box(s / sx + ox, y / sy + oy, e / sx + ox, (y + 1) / sy + oy)
            )

    if not rectangles:
        geometry = Polygon()
    else:
        geometry = unary_union(rectangles)

    cls = PredictedROI if self.is_predicted else UserROI
    kwargs: dict = dict(
        geometry=geometry,
        name=self.name,
        category=self.category,
        category_score=self.category_score,
        category_embedding=self.category_embedding,
        source=self.source,
        track=self.track,
        tracking_score=self.tracking_score,
        identity=self.identity,
        identity_score=self.identity_score,
        identity_embedding=self.identity_embedding,
        instance=self.instance,
    )
    if self.is_predicted:
        kwargs["score"] = self.score
    return cls(**kwargs)

to_roi()

Convert the mask to an ROI (alias for to_polygon).

Returns:

Type Description
ROI

An ROI with polygon geometry derived from the mask. See to_polygon for details.

Source code in sleap_io/model/mask.py
def to_roi(self) -> "ROI":
    """Convert the mask to an ROI (alias for `to_polygon`).

    Returns:
        An `ROI` with polygon geometry derived from the mask. See
        `to_polygon` for details.
    """
    return self.to_polygon()

sleap_io.UserSegmentationMask

Bases: sleap_io.model.mask.SegmentationMask

Human-annotated segmentation mask.

Attributes:

Name Type Description
from_predicted

The PredictedSegmentationMask (if any) that this user mask was initialized from, recorded by PredictedSegmentationMask.to_user() for human-in-the-loop correction workflows. None if the mask was created directly. This provenance link is persisted to the SLP format as an index into the saved mask list (mirroring instance from_predicted), so it survives a save/load round-trip as long as the source prediction is also saved. Files written before this column existed load it as None.

Methods:

Name Description
__init__

Method generated by attrs for class UserSegmentationMask.

__repr__

Method generated by attrs for class UserSegmentationMask.

__setattr__

Method generated by attrs for class UserSegmentationMask.

Source code in sleap_io/model/mask.py
@attrs.define(eq=False)
class UserSegmentationMask(SegmentationMask):
    """Human-annotated segmentation mask.

    Attributes:
        from_predicted: The `PredictedSegmentationMask` (if any) that this user
            mask was initialized from, recorded by
            `PredictedSegmentationMask.to_user()` for human-in-the-loop
            correction workflows. `None` if the mask was created directly. This
            provenance link is persisted to the SLP format as an index into the
            saved mask list (mirroring instance `from_predicted`), so it survives
            a save/load round-trip as long as the source prediction is also
            saved. Files written before this column existed load it as `None`.
    """

    from_predicted: "PredictedSegmentationMask | None" = attrs.field(
        default=None, repr=False
    )

__annotations__ = {'from_predicted': "'PredictedSegmentationMask | None'"} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = True class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = 'Human-annotated segmentation mask.\n\nAttributes:\n from_predicted: The `PredictedSegmentationMask` (if any) that this user\n mask was initialized from, recorded by\n `PredictedSegmentationMask.to_user()` for human-in-the-loop\n correction workflows. `None` if the mask was created directly. This\n provenance link is persisted to the SLP format as an index into the\n saved mask list (mirroring instance `from_predicted`), so it survives\n a save/load round-trip as long as the source prediction is also\n saved. Files written before this column existed load it as `None`.\n' class-attribute

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

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

__firstlineno__ = 580 class-attribute

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

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

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

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

__match_args__ = ('rle_counts', 'height', 'width', 'name', 'category', 'source', 'track', 'tracking_score', 'identity', 'identity_score', 'instance', 'scale', 'offset', 'identity_embedding', 'category_score', 'category_embedding', 'from_predicted') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.mask' class-attribute

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

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

__slots__ = ('from_predicted',) class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__init__(rle_counts, height, width, name='', category=None, source='', track=None, tracking_score=None, identity=None, identity_score=None, instance=None, scale=(1.0, 1.0), offset=(0.0, 0.0), identity_embedding=None, category_score=None, category_embedding=None, from_predicted=None)

Method generated by attrs for class UserSegmentationMask.

Source code in sleap_io/model/mask.py
  segmentation tool (Cellpose, StarDist) where each pixel value identifies
  an object.
- To convert: ``LabelImage.to_masks()`` decomposes into per-object masks,
  and ``LabelImage.from_masks(masks)`` composes masks into a label image.

See Also:
    ``sleap_io.model.label_image``: Dense integer label images.
"""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

if TYPE_CHECKING:
    if sys.version_info >= (3, 11):

__repr__()

Method generated by attrs for class UserSegmentationMask.

Source code in sleap_io/model/mask.py
"""Data structures for segmentation mask annotations.

Segmentation masks represent raster (per-pixel) annotations stored in
run-length encoded (RLE) format for compact storage. They can be converted
to and from numpy arrays and polygon representations.

Each ``SegmentationMask`` stores a single binary mask for one object. For
dense per-pixel segmentation where all objects are stored in one integer
array, see ``LabelImage`` in ``sleap_io.model.label_image``.

**When to use SegmentationMask vs LabelImage:**

- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows).
- Use ``LabelImage`` when you have a dense integer array from an instance

__setattr__(name, val)

Method generated by attrs for class UserSegmentationMask.

sleap_io.PredictedSegmentationMask

Bases: sleap_io.model.mask.SegmentationMask

Model-predicted segmentation mask with confidence score.

Attributes:

Name Type Description
score

Object-level confidence score (0-1).

score_map

Optional dense pixel-level confidence map of shape (H, W) as float32. This can be large and is stored separately in the SLP format. If None, only the object-level score is available.

score_map_scale

Resolution ratio (sx, sy) for the score map, independent of the mask's own scale. Defaults to (1.0, 1.0).

score_map_offset

Origin (x, y) of the score map in image pixel coordinates. Defaults to (0.0, 0.0).

Methods:

Name Description
__init__

Method generated by attrs for class PredictedSegmentationMask.

__repr__

Method generated by attrs for class PredictedSegmentationMask.

__setattr__

Method generated by attrs for class PredictedSegmentationMask.

to_user

Convert this predicted mask to a user mask, recording provenance.

Source code in sleap_io/model/mask.py
@attrs.define(eq=False)
class PredictedSegmentationMask(SegmentationMask):
    """Model-predicted segmentation mask with confidence score.

    Attributes:
        score: Object-level confidence score (0-1).
        score_map: Optional dense pixel-level confidence map of shape (H, W)
            as float32. This can be large and is stored separately in the SLP
            format. If ``None``, only the object-level score is available.
        score_map_scale: Resolution ratio ``(sx, sy)`` for the score map,
            independent of the mask's own ``scale``. Defaults to ``(1.0, 1.0)``.
        score_map_offset: Origin ``(x, y)`` of the score map in image pixel
            coordinates. Defaults to ``(0.0, 0.0)``.
    """

    score: float = attrs.field(default=0.0)
    score_map: np.ndarray | None = attrs.field(default=None)
    score_map_scale: tuple[float, float] = attrs.field(default=(1.0, 1.0))
    score_map_offset: tuple[float, float] = attrs.field(default=(0.0, 0.0))

    def to_user(self, link: bool = True) -> "UserSegmentationMask":
        """Convert this predicted mask to a user mask, recording provenance.

        Returns a new `UserSegmentationMask` carrying a copy of the RLE raster
        and all shared metadata (`name`, `category`, `source`, `track`,
        `tracking_score`, `identity`, `identity_score`, `instance`, `scale`,
        `offset`). The prediction-only
        fields (`score`, `score_map`, `score_map_scale`, `score_map_offset`)
        are dropped. This is the predicted -> user adoption path for the
        inference -> human-correct -> retrain loop, mirroring
        `Instance.from_predicted` for poses.

        Args:
            link: If `True` (the default), set `from_predicted` on the returned
                mask to this prediction, recording that the user annotation
                originated from it. Pass `False` for an unlinked copy.

        Returns:
            A new `UserSegmentationMask` with an independent RLE buffer and the
            shared metadata above. `from_predicted` points back at this mask
            when `link` is `True`, otherwise `None`.

        Notes:
            The `track` and `instance` references are shared (not copied), so
            mutating them affects both masks. The `from_predicted` link is
            persisted to the SLP format (as an index into the saved mask list);
            it survives a save/load round-trip as long as this source prediction
            is also saved.
        """
        user = UserSegmentationMask(
            rle_counts=self.rle_counts.copy(),
            height=self.height,
            width=self.width,
            name=self.name,
            category=self.category,
            category_score=self.category_score,
            category_embedding=self.category_embedding,
            source=self.source,
            track=self.track,
            tracking_score=self.tracking_score,
            identity=self.identity,
            identity_score=self.identity_score,
            identity_embedding=self.identity_embedding,
            instance=self.instance,
            scale=self.scale,
            offset=self.offset,
            from_predicted=self if link else None,
        )
        user._instance_idx = self._instance_idx
        return user

__annotations__ = {'score': 'float', 'score_map': 'np.ndarray | None', 'score_map_scale': 'tuple[float, float]', 'score_map_offset': 'tuple[float, float]'} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = True class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = "Model-predicted segmentation mask with confidence score.\n\nAttributes:\n score: Object-level confidence score (0-1).\n score_map: Optional dense pixel-level confidence map of shape (H, W)\n as float32. This can be large and is stored separately in the SLP\n format. If ``None``, only the object-level score is available.\n score_map_scale: Resolution ratio ``(sx, sy)`` for the score map,\n independent of the mask's own ``scale``. Defaults to ``(1.0, 1.0)``.\n score_map_offset: Origin ``(x, y)`` of the score map in image pixel\n coordinates. Defaults to ``(0.0, 0.0)``.\n" class-attribute

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

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

__firstlineno__ = 600 class-attribute

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

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

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

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

__match_args__ = ('rle_counts', 'height', 'width', 'name', 'category', 'source', 'track', 'tracking_score', 'identity', 'identity_score', 'instance', 'scale', 'offset', 'identity_embedding', 'category_score', 'category_embedding', 'score', 'score_map', 'score_map_scale', 'score_map_offset') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.mask' class-attribute

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

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

__slots__ = ('score', 'score_map', 'score_map_scale', 'score_map_offset') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__init__(rle_counts, height, width, name='', category=None, source='', track=None, tracking_score=None, identity=None, identity_score=None, instance=None, scale=(1.0, 1.0), offset=(0.0, 0.0), identity_embedding=None, category_score=None, category_embedding=None, score=0.0, score_map=None, score_map_scale=(1.0, 1.0), score_map_offset=(0.0, 0.0))

Method generated by attrs for class PredictedSegmentationMask.

Source code in sleap_io/model/mask.py
  segmentation tool (Cellpose, StarDist) where each pixel value identifies
  an object.
- To convert: ``LabelImage.to_masks()`` decomposes into per-object masks,
  and ``LabelImage.from_masks(masks)`` composes masks into a label image.

See Also:
    ``sleap_io.model.label_image``: Dense integer label images.
"""

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

if TYPE_CHECKING:
    if sys.version_info >= (3, 11):
        from typing import Self
    else:
        from typing_extensions import Self

__repr__()

Method generated by attrs for class PredictedSegmentationMask.

Source code in sleap_io/model/mask.py
"""Data structures for segmentation mask annotations.

Segmentation masks represent raster (per-pixel) annotations stored in
run-length encoded (RLE) format for compact storage. They can be converted
to and from numpy arrays and polygon representations.

Each ``SegmentationMask`` stores a single binary mask for one object. For
dense per-pixel segmentation where all objects are stored in one integer
array, see ``LabelImage`` in ``sleap_io.model.label_image``.

**When to use SegmentationMask vs LabelImage:**

- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows).
- Use ``LabelImage`` when you have a dense integer array from an instance

__setattr__(name, val)

Method generated by attrs for class PredictedSegmentationMask.

to_user(link=True)

Convert this predicted mask to a user mask, recording provenance.

Returns a new UserSegmentationMask carrying a copy of the RLE raster and all shared metadata (name, category, source, track, tracking_score, identity, identity_score, instance, scale, offset). The prediction-only fields (score, score_map, score_map_scale, score_map_offset) are dropped. This is the predicted -> user adoption path for the inference -> human-correct -> retrain loop, mirroring Instance.from_predicted for poses.

Parameters:

Name Type Description Default
link bool

If True (the default), set from_predicted on the returned mask to this prediction, recording that the user annotation originated from it. Pass False for an unlinked copy.

True

Returns:

Type Description
UserSegmentationMask

A new UserSegmentationMask with an independent RLE buffer and the shared metadata above. from_predicted points back at this mask when link is True, otherwise None.

Notes

The track and instance references are shared (not copied), so mutating them affects both masks. The from_predicted link is persisted to the SLP format (as an index into the saved mask list); it survives a save/load round-trip as long as this source prediction is also saved.

Source code in sleap_io/model/mask.py
def to_user(self, link: bool = True) -> "UserSegmentationMask":
    """Convert this predicted mask to a user mask, recording provenance.

    Returns a new `UserSegmentationMask` carrying a copy of the RLE raster
    and all shared metadata (`name`, `category`, `source`, `track`,
    `tracking_score`, `identity`, `identity_score`, `instance`, `scale`,
    `offset`). The prediction-only
    fields (`score`, `score_map`, `score_map_scale`, `score_map_offset`)
    are dropped. This is the predicted -> user adoption path for the
    inference -> human-correct -> retrain loop, mirroring
    `Instance.from_predicted` for poses.

    Args:
        link: If `True` (the default), set `from_predicted` on the returned
            mask to this prediction, recording that the user annotation
            originated from it. Pass `False` for an unlinked copy.

    Returns:
        A new `UserSegmentationMask` with an independent RLE buffer and the
        shared metadata above. `from_predicted` points back at this mask
        when `link` is `True`, otherwise `None`.

    Notes:
        The `track` and `instance` references are shared (not copied), so
        mutating them affects both masks. The `from_predicted` link is
        persisted to the SLP format (as an index into the saved mask list);
        it survives a save/load round-trip as long as this source prediction
        is also saved.
    """
    user = UserSegmentationMask(
        rle_counts=self.rle_counts.copy(),
        height=self.height,
        width=self.width,
        name=self.name,
        category=self.category,
        category_score=self.category_score,
        category_embedding=self.category_embedding,
        source=self.source,
        track=self.track,
        tracking_score=self.tracking_score,
        identity=self.identity,
        identity_score=self.identity_score,
        identity_embedding=self.identity_embedding,
        instance=self.instance,
        scale=self.scale,
        offset=self.offset,
        from_predicted=self if link else None,
    )
    user._instance_idx = self._instance_idx
    return user

sleap_io.LabelImage

Per-pixel object segmentation for a single video frame.

Each pixel is either background (0) or belongs to a tracked, categorized object. The integer values in data are an internal encoding. Use the track-centric API (__getitem__, tracks, items) to query objects without dealing with raw label IDs.

Attributes:

Name Type Description
data

Integer array of shape (H, W) with dtype int32. 0 is background, positive values are object IDs.

objects

Mapping from label ID to object metadata. Defines what each non-zero pixel value represents. Label IDs not in this dict are treated as having default (empty) metadata.

source

Source identifier string.

scale

Resolution ratio (sx, sy) where sx = label_width / image_width and sy = label_height / image_height. (1.0, 1.0) means full resolution. (0.5, 0.5) means half resolution. Coordinate mapping: image_coord = label_coord / scale + offset.

offset

Origin (x, y) of the label image in image pixel coordinates.

See Also

SegmentationMask: Per-object binary masks (one mask per object). sleap_io.load_label_images: Load label images from TIFF files. sleap_io.save_label_images: Save label images to TIFF files.

Classes:

Name Description
Info

Metadata for one segmented object within a LabelImage.

Methods:

Name Description
__attrs_post_init__

Validate and normalize data array on construction.

__contains__

Whether this track has pixels in this frame.

__deepcopy__

Deep copy that materializes lazy data before copying.

__getitem__

Get binary (H, W) mask for a tracked object.

__init__

Method generated by attrs for class LabelImage.

__repr__

Method generated by attrs for class LabelImage.

from_binary_masks

Create a LabelImage from per-object binary mask arrays.

from_masks

Compose from binary SegmentationMasks.

from_numpy

Create from an integer array.

from_stack

Create label images from a stack of frames.

get_category_mask

Union mask of all objects matching a category.

get_track_mask

Get binary (H, W) mask for a tracked object. Same as getitem.

items

Iterate over objects as (track, category, mask) tuples.

resampled

Return a new label image resampled to the target dimensions.

to_bboxes

Extract tight bounding boxes for each object in the label image.

to_masks

Decompose into per-object binary SegmentationMasks.

Source code in sleap_io/model/label_image.py
@attrs.define(eq=False)
class LabelImage:
    """Per-pixel object segmentation for a single video frame.

    Each pixel is either background (0) or belongs to a tracked, categorized
    object. The integer values in ``data`` are an internal encoding. Use the
    track-centric API (``__getitem__``, ``tracks``, ``items``) to query objects
    without dealing with raw label IDs.

    Attributes:
        data: Integer array of shape ``(H, W)`` with dtype int32. ``0`` is
            background, positive values are object IDs.
        objects: Mapping from label ID to object metadata. Defines what each
            non-zero pixel value represents. Label IDs not in this dict are
            treated as having default (empty) metadata.
        source: Source identifier string.
        scale: Resolution ratio ``(sx, sy)`` where ``sx = label_width / image_width``
            and ``sy = label_height / image_height``. ``(1.0, 1.0)`` means full
            resolution. ``(0.5, 0.5)`` means half resolution. Coordinate mapping:
            ``image_coord = label_coord / scale + offset``.
        offset: Origin ``(x, y)`` of the label image in image pixel coordinates.

    See Also:
        ``SegmentationMask``: Per-object binary masks (one mask per object).
        ``sleap_io.load_label_images``: Load label images from TIFF files.
        ``sleap_io.save_label_images``: Save label images to TIFF files.
    """

    @attrs.define
    class Info:
        """Metadata for one segmented object within a ``LabelImage``.

        Attributes:
            track: Track identity for cross-frame association. ``None`` if
                untracked.
            tracking_score: Confidence of the track identity assignment.
                ``None`` if unassigned or manually assigned.
            category: Semantic class label (e.g., ``"neuron"``, ``"glia"``).
            name: Human-readable name (e.g., ``"cell_042"``).
            instance: Linked pose ``Instance``, if any.
        """

        track: "Track | None" = None
        tracking_score: float | None = None
        category: str = ""
        name: str = ""
        instance: "Instance | None" = None
        score: float | None = None

        # Private: deferred instance index for lazy loading. When label images
        # are read from a file without materialized instances (e.g., lazy mode),
        # this stores the raw instance_idx so it can be resolved later or
        # written back as-is.
        _instance_idx: int = attrs.field(default=-1, repr=False, eq=False, init=False)

    _data: "np.ndarray | None" = attrs.field(default=None, alias="data")
    objects: dict[int, Info] = Factory(dict)
    source: str = attrs.field(default="")
    scale: tuple[float, float] = attrs.field(default=(1.0, 1.0))
    offset: tuple[float, float] = attrs.field(default=(0.0, 0.0))

    # Private: lazy loading support. When set, data is decompressed on first
    # access via the .data property and cached. The loader is cleared after use.
    _lazy_loader: "Callable[[], np.ndarray] | None" = attrs.field(
        default=None, init=False, repr=False, eq=False
    )
    # Private: cached dimensions from metadata (avoids triggering lazy load for
    # height/width queries). Set by the I/O layer after construction.
    _height: int = attrs.field(default=0, init=False, repr=False, eq=False)
    _width: int = attrs.field(default=0, init=False, repr=False, eq=False)

    @property
    def data(self) -> np.ndarray:
        """Integer array of shape ``(H, W)`` with dtype int32.

        ``0`` is background, positive values are object IDs. When the label
        image was loaded lazily, the pixel data is decompressed on first access
        and cached for subsequent reads.
        """
        if self._data is None:
            if self._lazy_loader is not None:
                self._data = self._lazy_loader()
                self._lazy_loader = None
                self._validate_data()
            else:
                raise ValueError("LabelImage has no data and no lazy loader.")
        return self._data

    @data.setter
    def data(self, value: np.ndarray) -> None:
        self._data = value
        self._lazy_loader = None
        if value is not None:
            self._height = value.shape[0]
            self._width = value.shape[1]

    @property
    def is_predicted(self) -> bool:
        """Whether this label image is a model prediction."""
        return isinstance(self, PredictedLabelImage)

    def _validate_data(self) -> None:
        """Validate and normalize the data array.

        Called eagerly from ``__attrs_post_init__`` when data is provided at
        construction time, or lazily on first ``.data`` access when a
        ``_lazy_loader`` is used.
        """
        if self._data.ndim != 2:
            raise ValueError(
                f"LabelImage data must be 2D, got shape {self._data.shape}"
            )
        if np.any(self._data < 0):
            raise ValueError("LabelImage data must not contain negative values.")
        if self._data.dtype != np.int32:
            self._data = self._data.astype(np.int32)
        self._height = self._data.shape[0]
        self._width = self._data.shape[1]

    def __attrs_post_init__(self):
        """Validate and normalize data array on construction."""
        if type(self) is LabelImage:
            raise TypeError(
                "LabelImage is abstract. Use UserLabelImage or PredictedLabelImage."
            )
        if self._data is not None:
            self._validate_data()
        # When _data is None, validation is deferred to first .data access.
        if self.scale[0] <= 0 or self.scale[1] <= 0:
            raise ValueError(f"Scale values must be positive, got {self.scale}.")

    def __deepcopy__(self, memo: dict) -> "LabelImage":
        """Deep copy that materializes lazy data before copying.

        This is necessary because lazy loaders capture h5py dataset references
        which cannot be pickled/deepcopied.
        """
        # Materialize lazy data before copying (h5py refs can't survive deepcopy).
        if self._data is not None:
            data = self._data.copy()
        elif self._lazy_loader is not None:
            data = self.data.copy()  # Triggers lazy load, then copy
        else:
            data = None
        objects = {lid: copy.deepcopy(info, memo) for lid, info in self.objects.items()}

        kwargs: dict = dict(
            data=data,
            objects=objects,
            source=self.source,
            scale=self.scale,
            offset=self.offset,
        )
        if isinstance(self, PredictedLabelImage):
            sm = self.score_map
            kwargs["score"] = self.score
            kwargs["score_map"] = sm.copy() if sm is not None else None
            kwargs["score_map_scale"] = self.score_map_scale
            kwargs["score_map_offset"] = self.score_map_offset
        result = type(self)(**kwargs)
        memo[id(self)] = result
        return result

    @property
    def height(self) -> int:
        """Height of the label image in pixels."""
        if self._height > 0:
            return self._height
        return self.data.shape[0]

    @property
    def width(self) -> int:
        """Width of the label image in pixels."""
        if self._width > 0:
            return self._width
        return self.data.shape[1]

    @property
    def has_spatial_transform(self) -> bool:
        """Whether this label image has non-default scale or offset."""
        return self.scale != (1.0, 1.0) or self.offset != (0.0, 0.0)

    @property
    def image_extent(self) -> tuple[int, int]:
        """Image-space ``(height, width)`` this label image covers (excluding offset).

        Computed as ``(int(height / scale_y), int(width / scale_x))``.
        """
        return (
            int(self.height / self.scale[1]),
            int(self.width / self.scale[0]),
        )

    def resampled(self, target_height: int, target_width: int) -> Self:
        """Return a new label image resampled to the target dimensions.

        The returned label image has ``scale=(1.0, 1.0)`` and
        ``offset=(0.0, 0.0)`` with the data resized using nearest-neighbor
        interpolation to preserve label IDs.

        Args:
            target_height: Target height in pixels.
            target_width: Target width in pixels.

        Returns:
            A new label image of the same concrete type with resampled data.
        """
        from sleap_io.model.mask import _resize_nearest

        resized = _resize_nearest(self.data, target_height, target_width)
        objects: dict[int, LabelImage.Info] = {}
        for lid, info in self.objects.items():
            new_info = attrs.evolve(info)
            # Carry the deferred instance index through (init=False, so it is not
            # reproduced by attrs.evolve and must be set after construction;
            # mirrors how __deepcopy__ preserves the lazy association).
            new_info._instance_idx = info._instance_idx
            objects[lid] = new_info
        kwargs: dict = dict(
            data=resized,
            objects=objects,
            source=self.source,
            scale=(1.0, 1.0),
            offset=(0.0, 0.0),
        )
        if isinstance(self, PredictedLabelImage):
            kwargs["score"] = self.score
            if self.score_map is not None:
                kwargs["score_map"] = _resize_nearest(
                    self.score_map, target_height, target_width
                )
            kwargs["score_map_scale"] = (1.0, 1.0)
            kwargs["score_map_offset"] = (0.0, 0.0)
        return type(self)(**kwargs)

    @property
    def n_objects(self) -> int:
        """Number of unique non-zero labels present in data."""
        return len(self.label_ids)

    @property
    def label_ids(self) -> np.ndarray:
        """Sorted array of unique non-zero label values in data."""
        ids = np.unique(self.data)
        return ids[ids > 0]

    @property
    def tracks(self) -> list["Track"]:
        """Tracks present in this frame (from objects with non-None track)."""
        return [
            self.objects[lid].track
            for lid in sorted(self.objects)
            if self.objects[lid].track is not None
        ]

    @property
    def categories(self) -> set[str]:
        """Unique non-empty category strings present."""
        return {info.category for info in self.objects.values() if info.category != ""}

    def __getitem__(self, track: "Track") -> np.ndarray:
        """Get binary (H, W) mask for a tracked object.

        Args:
            track: The Track to look up.

        Returns:
            Boolean array of shape (H, W).

        Raises:
            KeyError: If the track is not present in this frame.
        """
        for label_id, info in self.objects.items():
            if info.track is track:
                return self.data == label_id
        raise KeyError(f"Track {track} not found in this LabelImage.")

    def __contains__(self, track: "Track") -> bool:
        """Whether this track has pixels in this frame."""
        return any(info.track is track for info in self.objects.values())

    def get_track_mask(self, track: "Track") -> np.ndarray:
        """Get binary (H, W) mask for a tracked object. Same as __getitem__."""
        return self[track]

    def get_category_mask(self, category: str) -> np.ndarray:
        """Union mask of all objects matching a category.

        Args:
            category: Semantic class label to filter by.

        Returns:
            Boolean array of shape (H, W). All-False if no objects match.
        """
        label_ids = [
            lid for lid, info in self.objects.items() if info.category == category
        ]
        if not label_ids:
            return np.zeros((self.height, self.width), dtype=bool)
        return np.isin(self.data, label_ids)

    def items(self) -> Iterator[tuple["Track | None", str, np.ndarray]]:
        """Iterate over objects as (track, category, mask) tuples.

        Yields one tuple per unique non-zero label ID, in sorted label order.
        """
        for label_id in np.sort(self.label_ids):
            lid = int(label_id)
            info = self.objects.get(lid, LabelImage.Info())
            yield info.track, info.category, self.data == lid

    @classmethod
    def from_numpy(
        cls,
        data: np.ndarray,
        tracks: "dict[int, Track] | list[Track] | None" = None,
        categories: dict[int, str] | list[str] | None = None,
        create_tracks: bool = False,
        **kwargs,
    ) -> "LabelImage":
        """Create from an integer array.

        Args:
            data: (H, W) integer array. Cast to int32.
            tracks: Maps label IDs to Tracks.

                - ``None``: no tracks unless ``create_tracks=True``.
                - ``list``: positional — ``tracks[i]`` maps to label ``i + 1``.
                - ``dict``: explicit ``{label_id: Track}`` mapping. When
                  combined with ``create_tracks=True``, the dict is used as
                  a shared accumulator — existing entries are reused and new
                  entries are added for unseen label IDs (mutated in place).
            categories: Same pattern as tracks, for category strings.

                - ``None``: no categories set.
                - ``list``: positional — ``categories[i]`` maps to label ``i + 1``.
                - ``dict``: explicit ``{label_id: category}`` mapping.
            create_tracks: If ``True`` and ``tracks`` is ``None``, auto-create
                one Track per unique non-zero label with Track.name set to the
                string of the label ID. If ``True`` and ``tracks`` is a dict,
                create new Tracks for any label IDs not already in the dict
                (the dict is mutated in place to accumulate mappings across
                calls). Default is ``False``.
            **kwargs: Passed to the LabelImage constructor (
                source).

        Returns:
            A ``LabelImage`` with populated ``objects`` dict.
        """
        from sleap_io.model.instance import Track

        data = np.asarray(data, dtype=np.int32)
        unique_ids = np.unique(data)
        unique_ids = unique_ids[unique_ids > 0]

        # Build track mapping
        track_map: dict[int, Track] = {}
        if tracks is None:
            if create_tracks:
                for lid in unique_ids:
                    track_map[int(lid)] = Track(name=str(int(lid)))
        elif isinstance(tracks, dict):
            track_map = dict(tracks)
            if create_tracks:
                # Accumulate: create new tracks for unseen IDs, mutate
                # the caller's dict in place so it stays in sync.
                for lid in unique_ids:
                    lid_int = int(lid)
                    if lid_int not in track_map:
                        new_track = Track(name=str(lid_int))
                        track_map[lid_int] = new_track
                        tracks[lid_int] = new_track
        elif isinstance(tracks, list):
            for i, t in enumerate(tracks):
                track_map[i + 1] = t

        # Build category mapping
        cat_map: dict[int, str] = {}
        if categories is None:
            pass  # No categories
        elif isinstance(categories, list):
            for i, c in enumerate(categories):
                cat_map[i + 1] = c
        else:
            cat_map = dict(categories)

        # Build objects dict
        objects: dict[int, LabelImage.Info] = {}
        all_ids = set(int(lid) for lid in unique_ids) | set(track_map) | set(cat_map)
        for lid in sorted(all_ids):
            objects[lid] = LabelImage.Info(
                track=track_map.get(lid),
                category=cat_map.get(lid, ""),
            )

        return cls(data=data, objects=objects, **kwargs)

    @classmethod
    def from_masks(
        cls,
        masks: list["SegmentationMask"],
        **kwargs,
    ) -> "LabelImage":
        """Compose from binary SegmentationMasks.

        Each mask becomes one object with a unique label ID. Track, category,
        and name are inherited from each mask's metadata. Overlapping pixels
        are assigned to the last mask in the list.

        All masks must share the same ``scale`` and ``offset``. The resulting
        ``LabelImage`` inherits the shared spatial metadata (unless overridden
        via ``**kwargs``).

        Args:
            masks: Binary masks. Must all have the same height, width, scale,
                and offset.
            **kwargs: Passed to the LabelImage constructor.

        Returns:
            A ``LabelImage`` composing all masks.

        Raises:
            ValueError: If masks have inconsistent shapes, scale/offset, or the
                list is empty.
        """
        if not masks:
            raise ValueError("Cannot create LabelImage from empty mask list.")

        height, width = masks[0].height, masks[0].width
        for m in masks[1:]:
            if m.height != height or m.width != width:
                raise ValueError(
                    f"All masks must have the same shape. "
                    f"Expected ({height}, {width}), got ({m.height}, {m.width})."
                )

        scales = {m.scale for m in masks}
        offsets = {m.offset for m in masks}
        if len(scales) > 1 or len(offsets) > 1:
            raise ValueError(
                "All masks must share the same scale and offset. "
                "Use mask.resampled() to align them first."
            )

        # Inherit spatial metadata from masks unless explicitly overridden.
        if "scale" not in kwargs:
            kwargs["scale"] = masks[0].scale
        if "offset" not in kwargs:
            kwargs["offset"] = masks[0].offset

        data = np.zeros((height, width), dtype=np.int32)
        objects: dict[int, LabelImage.Info] = {}

        for i, mask in enumerate(masks):
            label_id = i + 1
            data[mask.data] = label_id
            objects[label_id] = LabelImage.Info(
                track=mask.track,
                category=mask.category.name if mask.category else "",
                name=mask.name,
                instance=mask.instance,
            )

        return cls(data=data, objects=objects, **kwargs)

    @classmethod
    def from_binary_masks(
        cls,
        masks: "np.ndarray | list[np.ndarray]",
        label_ids: list[int] | None = None,
        tracks: "list[Track] | None" = None,
        categories: list[str] | None = None,
        names: list[str] | None = None,
        scores: list[float] | None = None,
        create_tracks: bool = False,
        **kwargs,
    ) -> "LabelImage":
        """Create a LabelImage from per-object binary mask arrays.

        This is a convenience constructor for workflows that produce per-object
        binary masks, such as SAM, Mask R-CNN, or other instance segmentation
        tools. Each binary mask becomes one object in the composited label image
        with a unique label ID (1, 2, ..., N, unless ``label_ids`` is provided).
        Overlapping pixels are assigned to the last mask in the list.

        Unlike ``from_masks()``, this takes raw numpy arrays instead of
        ``SegmentationMask`` objects, avoiding RLE encoding overhead.

        Args:
            masks: Per-object binary masks as an ``(N, H, W)`` array or a list
                of ``(H, W)`` arrays. Values are cast to bool (nonzero = True).
            label_ids: Optional list of positive integer label IDs, one per
                mask. ``label_ids[i]`` sets the pixel value for mask ``i``. If
                ``None`` (default), masks are numbered 1, 2, ..., N. All values
                must be positive (0 is background) and unique.
            tracks: List of ``Track`` objects, one per mask. ``tracks[i]`` is
                assigned to mask ``i`` (label ID ``label_ids[i]`` or ``i + 1``
                by default).
            categories: List of category strings, one per mask.
            names: List of human-readable name strings, one per mask.
            scores: List of per-object confidence scores, one per mask. Stored
                in ``Info.score`` for each object.
            create_tracks: If ``True`` and ``tracks`` is ``None``, auto-create
                a ``Track`` per mask with ``name=str(label_id)``.
            **kwargs: Passed to the ``LabelImage`` constructor (e.g.,
                ``source``, ``scale``, ``offset``). For
                ``PredictedLabelImage``, also accepts ``score``, ``score_map``.

        Returns:
            A ``LabelImage`` compositing all masks.

        Raises:
            ValueError: If ``masks`` is empty, shapes are inconsistent, or any
                parallel array has the wrong length.

        Example:
            Create a label image from SAM output::

                li = PredictedLabelImage.from_binary_masks(
                    sam_masks,          # (N, H, W) bool
                    tracks=[t1, t2],    # per-object tracks
                    scores=[0.95, 0.87],# per-object confidence
                    score=0.9,          # image-level confidence
                )

        See Also:
            :meth:`from_masks`: Create from ``SegmentationMask`` objects.
            :meth:`from_numpy`: Create from a pre-composited integer array.
        """
        from sleap_io.model.instance import Track

        # Normalize input to list of 2D arrays.
        if isinstance(masks, np.ndarray):
            if masks.ndim == 3:
                mask_list = [masks[i] for i in range(masks.shape[0])]
            elif masks.ndim == 2:
                mask_list = [masks]
            else:
                raise ValueError(
                    f"Expected 2D or 3D array, got {masks.ndim}D with shape "
                    f"{masks.shape}."
                )
        else:
            mask_list = list(masks)

        if not mask_list:
            raise ValueError("Cannot create LabelImage from empty mask list.")

        # Validate consistent shapes.
        height, width = mask_list[0].shape[0], mask_list[0].shape[1]
        for i, m in enumerate(mask_list[1:], 1):
            if m.shape[0] != height or m.shape[1] != width:
                raise ValueError(
                    f"All masks must have the same shape. "
                    f"Expected ({height}, {width}), got ({m.shape[0]}, {m.shape[1]}) "
                    f"at index {i}."
                )

        n = len(mask_list)

        # Validate parallel array lengths.
        for param_name, param in [
            ("label_ids", label_ids),
            ("tracks", tracks),
            ("categories", categories),
            ("names", names),
            ("scores", scores),
        ]:
            if param is not None and len(param) != n:
                raise ValueError(
                    f"{param_name} length ({len(param)}) must match number of "
                    f"masks ({n})."
                )

        # Validate label_ids semantics.
        if label_ids is not None:
            if any(lid <= 0 for lid in label_ids):
                raise ValueError(
                    "All label_ids must be positive (0 is reserved for background)."
                )
            if len(set(label_ids)) != len(label_ids):
                raise ValueError("label_ids must contain unique values.")

        # Build track list.
        if tracks is not None:
            track_list = tracks
        elif create_tracks:
            track_list = [
                Track(name=str(label_ids[i] if label_ids is not None else i + 1))
                for i in range(n)
            ]
        else:
            track_list = [None] * n

        # Composite masks and build objects dict.
        data = np.zeros((height, width), dtype=np.int32)
        objects: dict[int, LabelImage.Info] = {}

        for i, mask in enumerate(mask_list):
            label_id = label_ids[i] if label_ids is not None else i + 1
            data[np.asarray(mask, dtype=bool)] = label_id
            objects[label_id] = LabelImage.Info(
                track=track_list[i],
                category=categories[i] if categories is not None else "",
                name=names[i] if names is not None else "",
                score=scores[i] if scores is not None else None,
            )

        return cls(data=data, objects=objects, **kwargs)

    @classmethod
    def from_stack(
        cls,
        data: "np.ndarray | list[np.ndarray]",
        tracks: "dict[int, Track] | list[Track] | None" = None,
        categories: dict[int, str] | list[str] | None = None,
        create_tracks: bool = False,
        score: "float | list[float] | None" = None,
        score_map: np.ndarray | None = None,
        **kwargs,
    ) -> "list[LabelImage]":
        """Create label images from a stack of frames.

        This is the batch equivalent of ``from_numpy()``. It accepts a
        ``(T, H, W)`` array (or list of 2D arrays) and returns one
        ``LabelImage`` per frame with consistent ``Track`` objects shared
        across frames.

        Args:
            data: Integer label data as a 3D ``(T, H, W)`` array or a list
                of 2D ``(H, W)`` arrays. Cast to int32.
            tracks: Maps label IDs to Tracks (shared across all frames).

                - ``None``: no tracks unless ``create_tracks=True``.
                - ``list``: positional — ``tracks[i]`` maps to label
                  ``i + 1``.
                - ``dict``: explicit ``{label_id: Track}`` mapping.
            categories: Same pattern as tracks, for category strings.
            create_tracks: If ``True`` and ``tracks`` is ``None``,
                auto-create one ``Track`` per unique non-zero label ID
                found across all frames. The same ``Track`` object is
                shared across frames. Default is ``False``.
            score: Confidence score(s) for ``PredictedLabelImage``. A
                single float is broadcast to all frames; a list must have
                length ``T``. Defaults to ``0.0`` for all frames if
                ``None``. Ignored for ``UserLabelImage``.
            score_map: Optional ``(T, H, W)`` float32 array of per-pixel
                confidence maps. Sliced per frame. Ignored for
                ``UserLabelImage``.
            **kwargs: Passed to every frame's constructor (``source``,
                ``scale``, ``offset``).

        Returns:
            A list of ``LabelImage`` objects, one per frame.

        Raises:
            ValueError: If ``data`` is not 3D (or a list), or if
                ``score`` lengths don't match.

        Note:
            For loading label images from TIFF files (single, multi-page,
            or directory), use ``sleap_io.load_label_images()`` which
            handles file I/O and sidecar metadata. ``from_stack()`` is
            for converting in-memory numpy arrays (e.g., direct Cellpose
            output).

        Example::

            masks = np.stack(cellpose_masks)  # (T, H, W) int32
            label_images = sio.PredictedLabelImage.from_stack(
                masks,
                source="cellpose:nuclei",
                create_tracks=True,
                score=1.0,
            )
        """
        from sleap_io.model.instance import Track

        # Normalize input to list of 2D arrays
        if isinstance(data, np.ndarray):
            if data.ndim != 3:
                raise ValueError(
                    f"from_stack expects a (T, H, W) array, got shape "
                    f"{data.shape}. Use from_numpy() for a single frame."
                )
            frames = [data[t] for t in range(data.shape[0])]
        elif isinstance(data, list):
            frames = data
        else:
            raise ValueError(
                f"data must be a (T, H, W) numpy array or list of 2D "
                f"arrays, got {type(data).__name__}."
            )

        n_frames = len(frames)
        if n_frames == 0:
            return []

        # Collect unique non-zero IDs across all frames
        all_ids: set[int] = set()
        for frame in frames:
            ids = np.unique(frame)
            all_ids.update(int(i) for i in ids if i > 0)

        # Build global track map (shared across frames)
        track_map: dict[int, Track] = {}
        if tracks is None:
            if create_tracks:
                for lid in sorted(all_ids):
                    track_map[lid] = Track(name=str(lid))
        elif isinstance(tracks, list):
            for i, t in enumerate(tracks):
                track_map[i + 1] = t
        else:
            track_map = dict(tracks)

        # Build global category map
        cat_map: dict[int, str] = {}
        if categories is None:
            pass
        elif isinstance(categories, list):
            for i, c in enumerate(categories):
                cat_map[i + 1] = c
        else:
            cat_map = dict(categories)

        # Handle PredictedLabelImage-specific parameters
        is_predicted = issubclass(cls, PredictedLabelImage)
        scores: list[float] = []
        if is_predicted:
            if score is None:
                scores = [0.0] * n_frames
            elif isinstance(score, (int, float)):
                scores = [float(score)] * n_frames
            else:
                if len(score) != n_frames:
                    raise ValueError(
                        f"score list length ({len(score)}) must match "
                        f"number of frames ({n_frames})."
                    )
                scores = [float(s) for s in score]

        score_maps: list[np.ndarray | None] = [None] * n_frames
        if is_predicted and score_map is not None:
            if score_map.ndim == 3 and score_map.shape[0] == n_frames:
                score_maps = [score_map[t] for t in range(n_frames)]
            else:
                raise ValueError(
                    f"score_map must be (T, H, W) with T={n_frames}, "
                    f"got shape {score_map.shape}."
                )

        # Build per-frame LabelImages with shared Track objects
        result: list[LabelImage] = []
        for t, frame in enumerate(frames):
            frame_data = np.asarray(frame, dtype=np.int32)
            frame_ids = np.unique(frame_data)
            frame_ids = frame_ids[frame_ids > 0]

            objects: dict[int, LabelImage.Info] = {}
            for lid in frame_ids:
                lid_int = int(lid)
                objects[lid_int] = LabelImage.Info(
                    track=track_map.get(lid_int),
                    category=cat_map.get(lid_int, ""),
                )

            frame_kwargs = dict(kwargs)
            if is_predicted:
                frame_kwargs["score"] = scores[t]
                frame_kwargs["score_map"] = score_maps[t]

            result.append(cls(data=frame_data, objects=objects, **frame_kwargs))

        return result

    def to_masks(self) -> list["SegmentationMask"]:
        """Decompose into per-object binary SegmentationMasks.

        Returns one SegmentationMask per unique non-zero label. Each mask
        inherits track, category, name, instance, source, scale, and offset
        from the ``LabelImage``.

        Returns:
            A list of ``SegmentationMask`` objects, one per object.
        """
        from sleap_io.model.mask import UserSegmentationMask

        result = []
        for label_id in np.sort(self.label_ids):
            lid = int(label_id)
            info = self.objects.get(lid, LabelImage.Info())
            binary_mask = self.data == lid
            result.append(
                UserSegmentationMask.from_numpy(
                    binary_mask,
                    name=info.name,
                    category=info.category,
                    track=info.track,
                    instance=info.instance,
                    source=self.source,
                    scale=self.scale,
                    offset=self.offset,
                )
            )
        return result

    def to_bboxes(self) -> list["BoundingBox"]:
        """Extract tight bounding boxes for each object in the label image.

        Returns a list of ``BoundingBox`` objects (``UserBoundingBox`` or
        ``PredictedBoundingBox`` depending on whether this label image is
        predicted), one per non-zero label. Each bounding box inherits track,
        category, name, instance, and score from the corresponding
        ``self.objects`` entry.

        Bounding boxes are in image coordinates (respecting scale/offset).
        Label IDs present in ``objects`` but with no pixels in the data are
        skipped.

        Returns:
            A list of ``BoundingBox`` objects, one per object.
        """
        from sleap_io.model.bbox import PredictedBoundingBox, UserBoundingBox

        data = self.data
        cls = PredictedBoundingBox if self.is_predicted else UserBoundingBox
        sx, sy = self.scale
        ox, oy = self.offset

        # Single-pass: find all foreground pixels at once.
        fg_rows, fg_cols = np.where(data > 0)
        if len(fg_rows) == 0:
            return []

        # Map sparse label IDs to dense indices and compute per-label bounds.
        fg_labels = data[fg_rows, fg_cols]
        unique_labels, inverse = np.unique(fg_labels, return_inverse=True)
        n = len(unique_labels)

        row_min = np.full(n, np.iinfo(np.intp).max, dtype=np.intp)
        row_max = np.full(n, np.iinfo(np.intp).min, dtype=np.intp)
        col_min = np.full(n, np.iinfo(np.intp).max, dtype=np.intp)
        col_max = np.full(n, np.iinfo(np.intp).min, dtype=np.intp)

        np.minimum.at(row_min, inverse, fg_rows)
        np.maximum.at(row_max, inverse, fg_rows)
        np.minimum.at(col_min, inverse, fg_cols)
        np.maximum.at(col_max, inverse, fg_cols)

        label_to_idx = {int(lid): i for i, lid in enumerate(unique_labels)}

        # Build BoundingBox objects using precomputed bounds.
        bboxes = []
        for lid, info in self.objects.items():
            idx = label_to_idx.get(lid)
            if idx is None:
                continue

            x1 = float(col_min[idx] / sx + ox)
            y1 = float(row_min[idx] / sy + oy)
            x2 = float((col_max[idx] + 1) / sx + ox)
            y2 = float((row_max[idx] + 1) / sy + oy)

            kwargs: dict = dict(
                track=info.track,
                instance=info.instance,
                category=info.category,
                name=info.name,
                source=self.source,
            )
            if self.is_predicted:
                kwargs["score"] = info.score if info.score is not None else self.score

            bboxes.append(cls.from_xyxy(x1, y1, x2, y2, **kwargs))

        return bboxes

__annotations__ = {'_data': "'np.ndarray | None'", 'objects': 'dict[int, Info]', 'source': 'str', 'scale': 'tuple[float, float]', 'offset': 'tuple[float, float]', '_lazy_loader': "'Callable[[], np.ndarray] | None'", '_height': 'int', '_width': 'int'} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = False class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = 'Per-pixel object segmentation for a single video frame.\n\nEach pixel is either background (0) or belongs to a tracked, categorized\nobject. The integer values in ``data`` are an internal encoding. Use the\ntrack-centric API (``__getitem__``, ``tracks``, ``items``) to query objects\nwithout dealing with raw label IDs.\n\nAttributes:\n data: Integer array of shape ``(H, W)`` with dtype int32. ``0`` is\n background, positive values are object IDs.\n objects: Mapping from label ID to object metadata. Defines what each\n non-zero pixel value represents. Label IDs not in this dict are\n treated as having default (empty) metadata.\n source: Source identifier string.\n scale: Resolution ratio ``(sx, sy)`` where ``sx = label_width / image_width``\n and ``sy = label_height / image_height``. ``(1.0, 1.0)`` means full\n resolution. ``(0.5, 0.5)`` means half resolution. Coordinate mapping:\n ``image_coord = label_coord / scale + offset``.\n offset: Origin ``(x, y)`` of the label image in image pixel coordinates.\n\nSee Also:\n ``SegmentationMask``: Per-object binary masks (one mask per object).\n ``sleap_io.load_label_images``: Load label images from TIFF files.\n ``sleap_io.save_label_images``: Save label images to TIFF files.\n' class-attribute

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

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

__firstlineno__ = 52 class-attribute

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

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

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

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

__match_args__ = ('_data', 'objects', 'source', 'scale', 'offset') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.label_image' class-attribute

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

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

__slots__ = ('_data', 'objects', 'source', 'scale', 'offset', '_lazy_loader', '_height', '_width', '__weakref__') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = ('_data', '_height', '_lazy_loader', '_width') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__weakref__ property

list of weak references to the object

categories property

Unique non-empty category strings present.

data property

Integer array of shape (H, W) with dtype int32.

0 is background, positive values are object IDs. When the label image was loaded lazily, the pixel data is decompressed on first access and cached for subsequent reads.

has_spatial_transform property

Whether this label image has non-default scale or offset.

height property

Height of the label image in pixels.

image_extent property

Image-space (height, width) this label image covers (excluding offset).

Computed as (int(height / scale_y), int(width / scale_x)).

is_predicted property

Whether this label image is a model prediction.

label_ids property

Sorted array of unique non-zero label values in data.

n_objects property

Number of unique non-zero labels present in data.

tracks property

Tracks present in this frame (from objects with non-None track).

width property

Width of the label image in pixels.

Info

Metadata for one segmented object within a LabelImage.

Attributes:

Name Type Description
track

Track identity for cross-frame association. None if untracked.

tracking_score

Confidence of the track identity assignment. None if unassigned or manually assigned.

category

Semantic class label (e.g., "neuron", "glia").

name

Human-readable name (e.g., "cell_042").

instance

Linked pose Instance, if any.

Methods:

Name Description
__eq__

Method generated by attrs for class LabelImage.Info.

__init__

Method generated by attrs for class LabelImage.Info.

__repr__

Method generated by attrs for class LabelImage.Info.

Source code in sleap_io/model/label_image.py
@attrs.define
class Info:
    """Metadata for one segmented object within a ``LabelImage``.

    Attributes:
        track: Track identity for cross-frame association. ``None`` if
            untracked.
        tracking_score: Confidence of the track identity assignment.
            ``None`` if unassigned or manually assigned.
        category: Semantic class label (e.g., ``"neuron"``, ``"glia"``).
        name: Human-readable name (e.g., ``"cell_042"``).
        instance: Linked pose ``Instance``, if any.
    """

    track: "Track | None" = None
    tracking_score: float | None = None
    category: str = ""
    name: str = ""
    instance: "Instance | None" = None
    score: float | None = None

    # Private: deferred instance index for lazy loading. When label images
    # are read from a file without materialized instances (e.g., lazy mode),
    # this stores the raw instance_idx so it can be resolved later or
    # written back as-is.
    _instance_idx: int = attrs.field(default=-1, repr=False, eq=False, init=False)

__annotations__ = {'track': "'Track | None'", 'tracking_score': 'float | None', 'category': 'str', 'name': 'str', 'instance': "'Instance | None'", 'score': 'float | None', '_instance_idx': 'int'} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = False class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = 'Metadata for one segmented object within a ``LabelImage``.\n\nAttributes:\n track: Track identity for cross-frame association. ``None`` if\n untracked.\n tracking_score: Confidence of the track identity assignment.\n ``None`` if unassigned or manually assigned.\n category: Semantic class label (e.g., ``"neuron"``, ``"glia"``).\n name: Human-readable name (e.g., ``"cell_042"``).\n instance: Linked pose ``Instance``, if any.\n' class-attribute

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

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

__firstlineno__ = 80 class-attribute

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

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

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

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

__match_args__ = ('track', 'tracking_score', 'category', 'name', 'instance', 'score') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.label_image' class-attribute

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

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

__slots__ = ('track', 'tracking_score', 'category', 'name', 'instance', 'score', '_instance_idx', '__weakref__') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__weakref__ property

list of weak references to the object

__eq__(other)

Method generated by attrs for class LabelImage.Info.

Source code in sleap_io/model/label_image.py
- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows). Each
  mask is stored separately with RLE compression.
- Use ``LabelImage.from_binary_masks()`` to create a label image directly from
  per-object binary numpy arrays (e.g., from SAM or Mask R-CNN output).
- To convert between them, use ``LabelImage.to_masks()`` and
  ``LabelImage.from_masks()``.

For TIFF I/O of label images, see ``sleap_io.load_label_images()`` and
``sleap_io.save_label_images()``.

__init__(track=None, tracking_score=None, category='', name='', instance=None, score=None)

Method generated by attrs for class LabelImage.Info.

Source code in sleap_io/model/label_image.py
See Also:
    ``sleap_io.model.mask``: Binary segmentation masks (one per object).
"""

from __future__ import annotations

import copy
import sys

__repr__()

Method generated by attrs for class LabelImage.Info.

Source code in sleap_io/model/label_image.py
"""Data structure for integer label image annotations.

Label images represent per-pixel object segmentation for a single video frame,
where each pixel value encodes which object occupies that pixel. This is the
standard output format of instance segmentation tools like Cellpose and StarDist.

Unlike binary ``SegmentationMask`` objects (one mask per object), a single
``LabelImage`` efficiently stores all objects for a frame in one dense integer
array.

**When to use LabelImage vs SegmentationMask:**

- Use ``LabelImage`` when you have a dense integer array from a segmentation
  tool (Cellpose, StarDist, COCO panoptic) where each pixel value identifies
  an object. One ``LabelImage`` per frame stores all objects at once.

__attrs_post_init__()

Validate and normalize data array on construction.

Source code in sleap_io/model/label_image.py
def __attrs_post_init__(self):
    """Validate and normalize data array on construction."""
    if type(self) is LabelImage:
        raise TypeError(
            "LabelImage is abstract. Use UserLabelImage or PredictedLabelImage."
        )
    if self._data is not None:
        self._validate_data()
    # When _data is None, validation is deferred to first .data access.
    if self.scale[0] <= 0 or self.scale[1] <= 0:
        raise ValueError(f"Scale values must be positive, got {self.scale}.")

__contains__(track)

Whether this track has pixels in this frame.

Source code in sleap_io/model/label_image.py
def __contains__(self, track: "Track") -> bool:
    """Whether this track has pixels in this frame."""
    return any(info.track is track for info in self.objects.values())

__deepcopy__(memo)

Deep copy that materializes lazy data before copying.

This is necessary because lazy loaders capture h5py dataset references which cannot be pickled/deepcopied.

Source code in sleap_io/model/label_image.py
def __deepcopy__(self, memo: dict) -> "LabelImage":
    """Deep copy that materializes lazy data before copying.

    This is necessary because lazy loaders capture h5py dataset references
    which cannot be pickled/deepcopied.
    """
    # Materialize lazy data before copying (h5py refs can't survive deepcopy).
    if self._data is not None:
        data = self._data.copy()
    elif self._lazy_loader is not None:
        data = self.data.copy()  # Triggers lazy load, then copy
    else:
        data = None
    objects = {lid: copy.deepcopy(info, memo) for lid, info in self.objects.items()}

    kwargs: dict = dict(
        data=data,
        objects=objects,
        source=self.source,
        scale=self.scale,
        offset=self.offset,
    )
    if isinstance(self, PredictedLabelImage):
        sm = self.score_map
        kwargs["score"] = self.score
        kwargs["score_map"] = sm.copy() if sm is not None else None
        kwargs["score_map_scale"] = self.score_map_scale
        kwargs["score_map_offset"] = self.score_map_offset
    result = type(self)(**kwargs)
    memo[id(self)] = result
    return result

__getitem__(track)

Get binary (H, W) mask for a tracked object.

Parameters:

Name Type Description Default
track Track

The Track to look up.

required

Returns:

Type Description
ndarray

Boolean array of shape (H, W).

Raises:

Type Description
KeyError

If the track is not present in this frame.

Source code in sleap_io/model/label_image.py
def __getitem__(self, track: "Track") -> np.ndarray:
    """Get binary (H, W) mask for a tracked object.

    Args:
        track: The Track to look up.

    Returns:
        Boolean array of shape (H, W).

    Raises:
        KeyError: If the track is not present in this frame.
    """
    for label_id, info in self.objects.items():
        if info.track is track:
            return self.data == label_id
    raise KeyError(f"Track {track} not found in this LabelImage.")

__init__(data=None, objects=NOTHING, source='', scale=(1.0, 1.0), offset=(0.0, 0.0))

Method generated by attrs for class LabelImage.

Source code in sleap_io/model/label_image.py
- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows). Each
  mask is stored separately with RLE compression.
- Use ``LabelImage.from_binary_masks()`` to create a label image directly from
  per-object binary numpy arrays (e.g., from SAM or Mask R-CNN output).
- To convert between them, use ``LabelImage.to_masks()`` and
  ``LabelImage.from_masks()``.

For TIFF I/O of label images, see ``sleap_io.load_label_images()`` and
``sleap_io.save_label_images()``.

See Also:
    ``sleap_io.model.mask``: Binary segmentation masks (one per object).

__repr__()

Method generated by attrs for class LabelImage.

Source code in sleap_io/model/label_image.py
"""Data structure for integer label image annotations.

Label images represent per-pixel object segmentation for a single video frame,
where each pixel value encodes which object occupies that pixel. This is the
standard output format of instance segmentation tools like Cellpose and StarDist.

Unlike binary ``SegmentationMask`` objects (one mask per object), a single
``LabelImage`` efficiently stores all objects for a frame in one dense integer
array.

**When to use LabelImage vs SegmentationMask:**

- Use ``LabelImage`` when you have a dense integer array from a segmentation
  tool (Cellpose, StarDist, COCO panoptic) where each pixel value identifies
  an object. One ``LabelImage`` per frame stores all objects at once.

from_binary_masks(masks, label_ids=None, tracks=None, categories=None, names=None, scores=None, create_tracks=False, **kwargs) classmethod

Create a LabelImage from per-object binary mask arrays.

This is a convenience constructor for workflows that produce per-object binary masks, such as SAM, Mask R-CNN, or other instance segmentation tools. Each binary mask becomes one object in the composited label image with a unique label ID (1, 2, ..., N, unless label_ids is provided). Overlapping pixels are assigned to the last mask in the list.

Unlike from_masks(), this takes raw numpy arrays instead of SegmentationMask objects, avoiding RLE encoding overhead.

Parameters:

Name Type Description Default
masks ndarray | list[ndarray]

Per-object binary masks as an (N, H, W) array or a list of (H, W) arrays. Values are cast to bool (nonzero = True).

required
label_ids list[int] | None

Optional list of positive integer label IDs, one per mask. label_ids[i] sets the pixel value for mask i. If None (default), masks are numbered 1, 2, ..., N. All values must be positive (0 is background) and unique.

None
tracks list[Track] | None

List of Track objects, one per mask. tracks[i] is assigned to mask i (label ID label_ids[i] or i + 1 by default).

None
categories list[str] | None

List of category strings, one per mask.

None
names list[str] | None

List of human-readable name strings, one per mask.

None
scores list[float] | None

List of per-object confidence scores, one per mask. Stored in Info.score for each object.

None
create_tracks bool

If True and tracks is None, auto-create a Track per mask with name=str(label_id).

False
**kwargs

Passed to the LabelImage constructor (e.g., source, scale, offset). For PredictedLabelImage, also accepts score, score_map.

required

Returns:

Type Description
LabelImage

A LabelImage compositing all masks.

Raises:

Type Description
ValueError

If masks is empty, shapes are inconsistent, or any parallel array has the wrong length.

Example

Create a label image from SAM output::

li = PredictedLabelImage.from_binary_masks(
    sam_masks,          # (N, H, W) bool
    tracks=[t1, t2],    # per-object tracks
    scores=[0.95, 0.87],# per-object confidence
    score=0.9,          # image-level confidence
)
See Also

:meth:from_masks: Create from SegmentationMask objects. :meth:from_numpy: Create from a pre-composited integer array.

Source code in sleap_io/model/label_image.py
@classmethod
def from_binary_masks(
    cls,
    masks: "np.ndarray | list[np.ndarray]",
    label_ids: list[int] | None = None,
    tracks: "list[Track] | None" = None,
    categories: list[str] | None = None,
    names: list[str] | None = None,
    scores: list[float] | None = None,
    create_tracks: bool = False,
    **kwargs,
) -> "LabelImage":
    """Create a LabelImage from per-object binary mask arrays.

    This is a convenience constructor for workflows that produce per-object
    binary masks, such as SAM, Mask R-CNN, or other instance segmentation
    tools. Each binary mask becomes one object in the composited label image
    with a unique label ID (1, 2, ..., N, unless ``label_ids`` is provided).
    Overlapping pixels are assigned to the last mask in the list.

    Unlike ``from_masks()``, this takes raw numpy arrays instead of
    ``SegmentationMask`` objects, avoiding RLE encoding overhead.

    Args:
        masks: Per-object binary masks as an ``(N, H, W)`` array or a list
            of ``(H, W)`` arrays. Values are cast to bool (nonzero = True).
        label_ids: Optional list of positive integer label IDs, one per
            mask. ``label_ids[i]`` sets the pixel value for mask ``i``. If
            ``None`` (default), masks are numbered 1, 2, ..., N. All values
            must be positive (0 is background) and unique.
        tracks: List of ``Track`` objects, one per mask. ``tracks[i]`` is
            assigned to mask ``i`` (label ID ``label_ids[i]`` or ``i + 1``
            by default).
        categories: List of category strings, one per mask.
        names: List of human-readable name strings, one per mask.
        scores: List of per-object confidence scores, one per mask. Stored
            in ``Info.score`` for each object.
        create_tracks: If ``True`` and ``tracks`` is ``None``, auto-create
            a ``Track`` per mask with ``name=str(label_id)``.
        **kwargs: Passed to the ``LabelImage`` constructor (e.g.,
            ``source``, ``scale``, ``offset``). For
            ``PredictedLabelImage``, also accepts ``score``, ``score_map``.

    Returns:
        A ``LabelImage`` compositing all masks.

    Raises:
        ValueError: If ``masks`` is empty, shapes are inconsistent, or any
            parallel array has the wrong length.

    Example:
        Create a label image from SAM output::

            li = PredictedLabelImage.from_binary_masks(
                sam_masks,          # (N, H, W) bool
                tracks=[t1, t2],    # per-object tracks
                scores=[0.95, 0.87],# per-object confidence
                score=0.9,          # image-level confidence
            )

    See Also:
        :meth:`from_masks`: Create from ``SegmentationMask`` objects.
        :meth:`from_numpy`: Create from a pre-composited integer array.
    """
    from sleap_io.model.instance import Track

    # Normalize input to list of 2D arrays.
    if isinstance(masks, np.ndarray):
        if masks.ndim == 3:
            mask_list = [masks[i] for i in range(masks.shape[0])]
        elif masks.ndim == 2:
            mask_list = [masks]
        else:
            raise ValueError(
                f"Expected 2D or 3D array, got {masks.ndim}D with shape "
                f"{masks.shape}."
            )
    else:
        mask_list = list(masks)

    if not mask_list:
        raise ValueError("Cannot create LabelImage from empty mask list.")

    # Validate consistent shapes.
    height, width = mask_list[0].shape[0], mask_list[0].shape[1]
    for i, m in enumerate(mask_list[1:], 1):
        if m.shape[0] != height or m.shape[1] != width:
            raise ValueError(
                f"All masks must have the same shape. "
                f"Expected ({height}, {width}), got ({m.shape[0]}, {m.shape[1]}) "
                f"at index {i}."
            )

    n = len(mask_list)

    # Validate parallel array lengths.
    for param_name, param in [
        ("label_ids", label_ids),
        ("tracks", tracks),
        ("categories", categories),
        ("names", names),
        ("scores", scores),
    ]:
        if param is not None and len(param) != n:
            raise ValueError(
                f"{param_name} length ({len(param)}) must match number of "
                f"masks ({n})."
            )

    # Validate label_ids semantics.
    if label_ids is not None:
        if any(lid <= 0 for lid in label_ids):
            raise ValueError(
                "All label_ids must be positive (0 is reserved for background)."
            )
        if len(set(label_ids)) != len(label_ids):
            raise ValueError("label_ids must contain unique values.")

    # Build track list.
    if tracks is not None:
        track_list = tracks
    elif create_tracks:
        track_list = [
            Track(name=str(label_ids[i] if label_ids is not None else i + 1))
            for i in range(n)
        ]
    else:
        track_list = [None] * n

    # Composite masks and build objects dict.
    data = np.zeros((height, width), dtype=np.int32)
    objects: dict[int, LabelImage.Info] = {}

    for i, mask in enumerate(mask_list):
        label_id = label_ids[i] if label_ids is not None else i + 1
        data[np.asarray(mask, dtype=bool)] = label_id
        objects[label_id] = LabelImage.Info(
            track=track_list[i],
            category=categories[i] if categories is not None else "",
            name=names[i] if names is not None else "",
            score=scores[i] if scores is not None else None,
        )

    return cls(data=data, objects=objects, **kwargs)

from_masks(masks, **kwargs) classmethod

Compose from binary SegmentationMasks.

Each mask becomes one object with a unique label ID. Track, category, and name are inherited from each mask's metadata. Overlapping pixels are assigned to the last mask in the list.

All masks must share the same scale and offset. The resulting LabelImage inherits the shared spatial metadata (unless overridden via **kwargs).

Parameters:

Name Type Description Default
masks list[SegmentationMask]

Binary masks. Must all have the same height, width, scale, and offset.

required
**kwargs

Passed to the LabelImage constructor.

required

Returns:

Type Description
LabelImage

A LabelImage composing all masks.

Raises:

Type Description
ValueError

If masks have inconsistent shapes, scale/offset, or the list is empty.

Source code in sleap_io/model/label_image.py
@classmethod
def from_masks(
    cls,
    masks: list["SegmentationMask"],
    **kwargs,
) -> "LabelImage":
    """Compose from binary SegmentationMasks.

    Each mask becomes one object with a unique label ID. Track, category,
    and name are inherited from each mask's metadata. Overlapping pixels
    are assigned to the last mask in the list.

    All masks must share the same ``scale`` and ``offset``. The resulting
    ``LabelImage`` inherits the shared spatial metadata (unless overridden
    via ``**kwargs``).

    Args:
        masks: Binary masks. Must all have the same height, width, scale,
            and offset.
        **kwargs: Passed to the LabelImage constructor.

    Returns:
        A ``LabelImage`` composing all masks.

    Raises:
        ValueError: If masks have inconsistent shapes, scale/offset, or the
            list is empty.
    """
    if not masks:
        raise ValueError("Cannot create LabelImage from empty mask list.")

    height, width = masks[0].height, masks[0].width
    for m in masks[1:]:
        if m.height != height or m.width != width:
            raise ValueError(
                f"All masks must have the same shape. "
                f"Expected ({height}, {width}), got ({m.height}, {m.width})."
            )

    scales = {m.scale for m in masks}
    offsets = {m.offset for m in masks}
    if len(scales) > 1 or len(offsets) > 1:
        raise ValueError(
            "All masks must share the same scale and offset. "
            "Use mask.resampled() to align them first."
        )

    # Inherit spatial metadata from masks unless explicitly overridden.
    if "scale" not in kwargs:
        kwargs["scale"] = masks[0].scale
    if "offset" not in kwargs:
        kwargs["offset"] = masks[0].offset

    data = np.zeros((height, width), dtype=np.int32)
    objects: dict[int, LabelImage.Info] = {}

    for i, mask in enumerate(masks):
        label_id = i + 1
        data[mask.data] = label_id
        objects[label_id] = LabelImage.Info(
            track=mask.track,
            category=mask.category.name if mask.category else "",
            name=mask.name,
            instance=mask.instance,
        )

    return cls(data=data, objects=objects, **kwargs)

from_numpy(data, tracks=None, categories=None, create_tracks=False, **kwargs) classmethod

Create from an integer array.

Parameters:

Name Type Description Default
data ndarray

(H, W) integer array. Cast to int32.

required
tracks dict[int, Track] | list[Track] | None

Maps label IDs to Tracks.

  • None: no tracks unless create_tracks=True.
  • list: positional — tracks[i] maps to label i + 1.
  • dict: explicit {label_id: Track} mapping. When combined with create_tracks=True, the dict is used as a shared accumulator — existing entries are reused and new entries are added for unseen label IDs (mutated in place).
None
categories dict[int, str] | list[str] | None

Same pattern as tracks, for category strings.

  • None: no categories set.
  • list: positional — categories[i] maps to label i + 1.
  • dict: explicit {label_id: category} mapping.
None
create_tracks bool

If True and tracks is None, auto-create one Track per unique non-zero label with Track.name set to the string of the label ID. If True and tracks is a dict, create new Tracks for any label IDs not already in the dict (the dict is mutated in place to accumulate mappings across calls). Default is False.

False
**kwargs

Passed to the LabelImage constructor ( source).

required

Returns:

Type Description
LabelImage

A LabelImage with populated objects dict.

Source code in sleap_io/model/label_image.py
@classmethod
def from_numpy(
    cls,
    data: np.ndarray,
    tracks: "dict[int, Track] | list[Track] | None" = None,
    categories: dict[int, str] | list[str] | None = None,
    create_tracks: bool = False,
    **kwargs,
) -> "LabelImage":
    """Create from an integer array.

    Args:
        data: (H, W) integer array. Cast to int32.
        tracks: Maps label IDs to Tracks.

            - ``None``: no tracks unless ``create_tracks=True``.
            - ``list``: positional — ``tracks[i]`` maps to label ``i + 1``.
            - ``dict``: explicit ``{label_id: Track}`` mapping. When
              combined with ``create_tracks=True``, the dict is used as
              a shared accumulator — existing entries are reused and new
              entries are added for unseen label IDs (mutated in place).
        categories: Same pattern as tracks, for category strings.

            - ``None``: no categories set.
            - ``list``: positional — ``categories[i]`` maps to label ``i + 1``.
            - ``dict``: explicit ``{label_id: category}`` mapping.
        create_tracks: If ``True`` and ``tracks`` is ``None``, auto-create
            one Track per unique non-zero label with Track.name set to the
            string of the label ID. If ``True`` and ``tracks`` is a dict,
            create new Tracks for any label IDs not already in the dict
            (the dict is mutated in place to accumulate mappings across
            calls). Default is ``False``.
        **kwargs: Passed to the LabelImage constructor (
            source).

    Returns:
        A ``LabelImage`` with populated ``objects`` dict.
    """
    from sleap_io.model.instance import Track

    data = np.asarray(data, dtype=np.int32)
    unique_ids = np.unique(data)
    unique_ids = unique_ids[unique_ids > 0]

    # Build track mapping
    track_map: dict[int, Track] = {}
    if tracks is None:
        if create_tracks:
            for lid in unique_ids:
                track_map[int(lid)] = Track(name=str(int(lid)))
    elif isinstance(tracks, dict):
        track_map = dict(tracks)
        if create_tracks:
            # Accumulate: create new tracks for unseen IDs, mutate
            # the caller's dict in place so it stays in sync.
            for lid in unique_ids:
                lid_int = int(lid)
                if lid_int not in track_map:
                    new_track = Track(name=str(lid_int))
                    track_map[lid_int] = new_track
                    tracks[lid_int] = new_track
    elif isinstance(tracks, list):
        for i, t in enumerate(tracks):
            track_map[i + 1] = t

    # Build category mapping
    cat_map: dict[int, str] = {}
    if categories is None:
        pass  # No categories
    elif isinstance(categories, list):
        for i, c in enumerate(categories):
            cat_map[i + 1] = c
    else:
        cat_map = dict(categories)

    # Build objects dict
    objects: dict[int, LabelImage.Info] = {}
    all_ids = set(int(lid) for lid in unique_ids) | set(track_map) | set(cat_map)
    for lid in sorted(all_ids):
        objects[lid] = LabelImage.Info(
            track=track_map.get(lid),
            category=cat_map.get(lid, ""),
        )

    return cls(data=data, objects=objects, **kwargs)

from_stack(data, tracks=None, categories=None, create_tracks=False, score=None, score_map=None, **kwargs) classmethod

Create label images from a stack of frames.

This is the batch equivalent of from_numpy(). It accepts a (T, H, W) array (or list of 2D arrays) and returns one LabelImage per frame with consistent Track objects shared across frames.

Parameters:

Name Type Description Default
data ndarray | list[ndarray]

Integer label data as a 3D (T, H, W) array or a list of 2D (H, W) arrays. Cast to int32.

required
tracks dict[int, Track] | list[Track] | None

Maps label IDs to Tracks (shared across all frames).

  • None: no tracks unless create_tracks=True.
  • list: positional — tracks[i] maps to label i + 1.
  • dict: explicit {label_id: Track} mapping.
None
categories dict[int, str] | list[str] | None

Same pattern as tracks, for category strings.

None
create_tracks bool

If True and tracks is None, auto-create one Track per unique non-zero label ID found across all frames. The same Track object is shared across frames. Default is False.

False
score float | list[float] | None

Confidence score(s) for PredictedLabelImage. A single float is broadcast to all frames; a list must have length T. Defaults to 0.0 for all frames if None. Ignored for UserLabelImage.

None
score_map ndarray | None

Optional (T, H, W) float32 array of per-pixel confidence maps. Sliced per frame. Ignored for UserLabelImage.

None
**kwargs

Passed to every frame's constructor (source, scale, offset).

required

Returns:

Type Description
list[LabelImage]

A list of LabelImage objects, one per frame.

Raises:

Type Description
ValueError

If data is not 3D (or a list), or if score lengths don't match.

Note

For loading label images from TIFF files (single, multi-page, or directory), use sleap_io.load_label_images() which handles file I/O and sidecar metadata. from_stack() is for converting in-memory numpy arrays (e.g., direct Cellpose output).

Example::

masks = np.stack(cellpose_masks)  # (T, H, W) int32
label_images = sio.PredictedLabelImage.from_stack(
    masks,
    source="cellpose:nuclei",
    create_tracks=True,
    score=1.0,
)
Source code in sleap_io/model/label_image.py
@classmethod
def from_stack(
    cls,
    data: "np.ndarray | list[np.ndarray]",
    tracks: "dict[int, Track] | list[Track] | None" = None,
    categories: dict[int, str] | list[str] | None = None,
    create_tracks: bool = False,
    score: "float | list[float] | None" = None,
    score_map: np.ndarray | None = None,
    **kwargs,
) -> "list[LabelImage]":
    """Create label images from a stack of frames.

    This is the batch equivalent of ``from_numpy()``. It accepts a
    ``(T, H, W)`` array (or list of 2D arrays) and returns one
    ``LabelImage`` per frame with consistent ``Track`` objects shared
    across frames.

    Args:
        data: Integer label data as a 3D ``(T, H, W)`` array or a list
            of 2D ``(H, W)`` arrays. Cast to int32.
        tracks: Maps label IDs to Tracks (shared across all frames).

            - ``None``: no tracks unless ``create_tracks=True``.
            - ``list``: positional — ``tracks[i]`` maps to label
              ``i + 1``.
            - ``dict``: explicit ``{label_id: Track}`` mapping.
        categories: Same pattern as tracks, for category strings.
        create_tracks: If ``True`` and ``tracks`` is ``None``,
            auto-create one ``Track`` per unique non-zero label ID
            found across all frames. The same ``Track`` object is
            shared across frames. Default is ``False``.
        score: Confidence score(s) for ``PredictedLabelImage``. A
            single float is broadcast to all frames; a list must have
            length ``T``. Defaults to ``0.0`` for all frames if
            ``None``. Ignored for ``UserLabelImage``.
        score_map: Optional ``(T, H, W)`` float32 array of per-pixel
            confidence maps. Sliced per frame. Ignored for
            ``UserLabelImage``.
        **kwargs: Passed to every frame's constructor (``source``,
            ``scale``, ``offset``).

    Returns:
        A list of ``LabelImage`` objects, one per frame.

    Raises:
        ValueError: If ``data`` is not 3D (or a list), or if
            ``score`` lengths don't match.

    Note:
        For loading label images from TIFF files (single, multi-page,
        or directory), use ``sleap_io.load_label_images()`` which
        handles file I/O and sidecar metadata. ``from_stack()`` is
        for converting in-memory numpy arrays (e.g., direct Cellpose
        output).

    Example::

        masks = np.stack(cellpose_masks)  # (T, H, W) int32
        label_images = sio.PredictedLabelImage.from_stack(
            masks,
            source="cellpose:nuclei",
            create_tracks=True,
            score=1.0,
        )
    """
    from sleap_io.model.instance import Track

    # Normalize input to list of 2D arrays
    if isinstance(data, np.ndarray):
        if data.ndim != 3:
            raise ValueError(
                f"from_stack expects a (T, H, W) array, got shape "
                f"{data.shape}. Use from_numpy() for a single frame."
            )
        frames = [data[t] for t in range(data.shape[0])]
    elif isinstance(data, list):
        frames = data
    else:
        raise ValueError(
            f"data must be a (T, H, W) numpy array or list of 2D "
            f"arrays, got {type(data).__name__}."
        )

    n_frames = len(frames)
    if n_frames == 0:
        return []

    # Collect unique non-zero IDs across all frames
    all_ids: set[int] = set()
    for frame in frames:
        ids = np.unique(frame)
        all_ids.update(int(i) for i in ids if i > 0)

    # Build global track map (shared across frames)
    track_map: dict[int, Track] = {}
    if tracks is None:
        if create_tracks:
            for lid in sorted(all_ids):
                track_map[lid] = Track(name=str(lid))
    elif isinstance(tracks, list):
        for i, t in enumerate(tracks):
            track_map[i + 1] = t
    else:
        track_map = dict(tracks)

    # Build global category map
    cat_map: dict[int, str] = {}
    if categories is None:
        pass
    elif isinstance(categories, list):
        for i, c in enumerate(categories):
            cat_map[i + 1] = c
    else:
        cat_map = dict(categories)

    # Handle PredictedLabelImage-specific parameters
    is_predicted = issubclass(cls, PredictedLabelImage)
    scores: list[float] = []
    if is_predicted:
        if score is None:
            scores = [0.0] * n_frames
        elif isinstance(score, (int, float)):
            scores = [float(score)] * n_frames
        else:
            if len(score) != n_frames:
                raise ValueError(
                    f"score list length ({len(score)}) must match "
                    f"number of frames ({n_frames})."
                )
            scores = [float(s) for s in score]

    score_maps: list[np.ndarray | None] = [None] * n_frames
    if is_predicted and score_map is not None:
        if score_map.ndim == 3 and score_map.shape[0] == n_frames:
            score_maps = [score_map[t] for t in range(n_frames)]
        else:
            raise ValueError(
                f"score_map must be (T, H, W) with T={n_frames}, "
                f"got shape {score_map.shape}."
            )

    # Build per-frame LabelImages with shared Track objects
    result: list[LabelImage] = []
    for t, frame in enumerate(frames):
        frame_data = np.asarray(frame, dtype=np.int32)
        frame_ids = np.unique(frame_data)
        frame_ids = frame_ids[frame_ids > 0]

        objects: dict[int, LabelImage.Info] = {}
        for lid in frame_ids:
            lid_int = int(lid)
            objects[lid_int] = LabelImage.Info(
                track=track_map.get(lid_int),
                category=cat_map.get(lid_int, ""),
            )

        frame_kwargs = dict(kwargs)
        if is_predicted:
            frame_kwargs["score"] = scores[t]
            frame_kwargs["score_map"] = score_maps[t]

        result.append(cls(data=frame_data, objects=objects, **frame_kwargs))

    return result

get_category_mask(category)

Union mask of all objects matching a category.

Parameters:

Name Type Description Default
category str

Semantic class label to filter by.

required

Returns:

Type Description
ndarray

Boolean array of shape (H, W). All-False if no objects match.

Source code in sleap_io/model/label_image.py
def get_category_mask(self, category: str) -> np.ndarray:
    """Union mask of all objects matching a category.

    Args:
        category: Semantic class label to filter by.

    Returns:
        Boolean array of shape (H, W). All-False if no objects match.
    """
    label_ids = [
        lid for lid, info in self.objects.items() if info.category == category
    ]
    if not label_ids:
        return np.zeros((self.height, self.width), dtype=bool)
    return np.isin(self.data, label_ids)

get_track_mask(track)

Get binary (H, W) mask for a tracked object. Same as getitem.

Source code in sleap_io/model/label_image.py
def get_track_mask(self, track: "Track") -> np.ndarray:
    """Get binary (H, W) mask for a tracked object. Same as __getitem__."""
    return self[track]

items()

Iterate over objects as (track, category, mask) tuples.

Yields one tuple per unique non-zero label ID, in sorted label order.

Source code in sleap_io/model/label_image.py
def items(self) -> Iterator[tuple["Track | None", str, np.ndarray]]:
    """Iterate over objects as (track, category, mask) tuples.

    Yields one tuple per unique non-zero label ID, in sorted label order.
    """
    for label_id in np.sort(self.label_ids):
        lid = int(label_id)
        info = self.objects.get(lid, LabelImage.Info())
        yield info.track, info.category, self.data == lid

resampled(target_height, target_width)

Return a new label image resampled to the target dimensions.

The returned label image has scale=(1.0, 1.0) and offset=(0.0, 0.0) with the data resized using nearest-neighbor interpolation to preserve label IDs.

Parameters:

Name Type Description Default
target_height int

Target height in pixels.

required
target_width int

Target width in pixels.

required

Returns:

Type Description
Self

A new label image of the same concrete type with resampled data.

Source code in sleap_io/model/label_image.py
def resampled(self, target_height: int, target_width: int) -> Self:
    """Return a new label image resampled to the target dimensions.

    The returned label image has ``scale=(1.0, 1.0)`` and
    ``offset=(0.0, 0.0)`` with the data resized using nearest-neighbor
    interpolation to preserve label IDs.

    Args:
        target_height: Target height in pixels.
        target_width: Target width in pixels.

    Returns:
        A new label image of the same concrete type with resampled data.
    """
    from sleap_io.model.mask import _resize_nearest

    resized = _resize_nearest(self.data, target_height, target_width)
    objects: dict[int, LabelImage.Info] = {}
    for lid, info in self.objects.items():
        new_info = attrs.evolve(info)
        # Carry the deferred instance index through (init=False, so it is not
        # reproduced by attrs.evolve and must be set after construction;
        # mirrors how __deepcopy__ preserves the lazy association).
        new_info._instance_idx = info._instance_idx
        objects[lid] = new_info
    kwargs: dict = dict(
        data=resized,
        objects=objects,
        source=self.source,
        scale=(1.0, 1.0),
        offset=(0.0, 0.0),
    )
    if isinstance(self, PredictedLabelImage):
        kwargs["score"] = self.score
        if self.score_map is not None:
            kwargs["score_map"] = _resize_nearest(
                self.score_map, target_height, target_width
            )
        kwargs["score_map_scale"] = (1.0, 1.0)
        kwargs["score_map_offset"] = (0.0, 0.0)
    return type(self)(**kwargs)

to_bboxes()

Extract tight bounding boxes for each object in the label image.

Returns a list of BoundingBox objects (UserBoundingBox or PredictedBoundingBox depending on whether this label image is predicted), one per non-zero label. Each bounding box inherits track, category, name, instance, and score from the corresponding self.objects entry.

Bounding boxes are in image coordinates (respecting scale/offset). Label IDs present in objects but with no pixels in the data are skipped.

Returns:

Type Description
list[BoundingBox]

A list of BoundingBox objects, one per object.

Source code in sleap_io/model/label_image.py
def to_bboxes(self) -> list["BoundingBox"]:
    """Extract tight bounding boxes for each object in the label image.

    Returns a list of ``BoundingBox`` objects (``UserBoundingBox`` or
    ``PredictedBoundingBox`` depending on whether this label image is
    predicted), one per non-zero label. Each bounding box inherits track,
    category, name, instance, and score from the corresponding
    ``self.objects`` entry.

    Bounding boxes are in image coordinates (respecting scale/offset).
    Label IDs present in ``objects`` but with no pixels in the data are
    skipped.

    Returns:
        A list of ``BoundingBox`` objects, one per object.
    """
    from sleap_io.model.bbox import PredictedBoundingBox, UserBoundingBox

    data = self.data
    cls = PredictedBoundingBox if self.is_predicted else UserBoundingBox
    sx, sy = self.scale
    ox, oy = self.offset

    # Single-pass: find all foreground pixels at once.
    fg_rows, fg_cols = np.where(data > 0)
    if len(fg_rows) == 0:
        return []

    # Map sparse label IDs to dense indices and compute per-label bounds.
    fg_labels = data[fg_rows, fg_cols]
    unique_labels, inverse = np.unique(fg_labels, return_inverse=True)
    n = len(unique_labels)

    row_min = np.full(n, np.iinfo(np.intp).max, dtype=np.intp)
    row_max = np.full(n, np.iinfo(np.intp).min, dtype=np.intp)
    col_min = np.full(n, np.iinfo(np.intp).max, dtype=np.intp)
    col_max = np.full(n, np.iinfo(np.intp).min, dtype=np.intp)

    np.minimum.at(row_min, inverse, fg_rows)
    np.maximum.at(row_max, inverse, fg_rows)
    np.minimum.at(col_min, inverse, fg_cols)
    np.maximum.at(col_max, inverse, fg_cols)

    label_to_idx = {int(lid): i for i, lid in enumerate(unique_labels)}

    # Build BoundingBox objects using precomputed bounds.
    bboxes = []
    for lid, info in self.objects.items():
        idx = label_to_idx.get(lid)
        if idx is None:
            continue

        x1 = float(col_min[idx] / sx + ox)
        y1 = float(row_min[idx] / sy + oy)
        x2 = float((col_max[idx] + 1) / sx + ox)
        y2 = float((row_max[idx] + 1) / sy + oy)

        kwargs: dict = dict(
            track=info.track,
            instance=info.instance,
            category=info.category,
            name=info.name,
            source=self.source,
        )
        if self.is_predicted:
            kwargs["score"] = info.score if info.score is not None else self.score

        bboxes.append(cls.from_xyxy(x1, y1, x2, y2, **kwargs))

    return bboxes

to_masks()

Decompose into per-object binary SegmentationMasks.

Returns one SegmentationMask per unique non-zero label. Each mask inherits track, category, name, instance, source, scale, and offset from the LabelImage.

Returns:

Type Description
list[SegmentationMask]

A list of SegmentationMask objects, one per object.

Source code in sleap_io/model/label_image.py
def to_masks(self) -> list["SegmentationMask"]:
    """Decompose into per-object binary SegmentationMasks.

    Returns one SegmentationMask per unique non-zero label. Each mask
    inherits track, category, name, instance, source, scale, and offset
    from the ``LabelImage``.

    Returns:
        A list of ``SegmentationMask`` objects, one per object.
    """
    from sleap_io.model.mask import UserSegmentationMask

    result = []
    for label_id in np.sort(self.label_ids):
        lid = int(label_id)
        info = self.objects.get(lid, LabelImage.Info())
        binary_mask = self.data == lid
        result.append(
            UserSegmentationMask.from_numpy(
                binary_mask,
                name=info.name,
                category=info.category,
                track=info.track,
                instance=info.instance,
                source=self.source,
                scale=self.scale,
                offset=self.offset,
            )
        )
    return result

sleap_io.UserLabelImage

Bases: sleap_io.model.label_image.LabelImage

Human-annotated label image.

Methods:

Name Description
__init__

Method generated by attrs for class UserLabelImage.

__repr__

Method generated by attrs for class UserLabelImage.

Attributes:

Name Type Description
__annotations__

dict() -> new empty dictionary

__attrs_own_setattr__

Returns True when the argument is true, False otherwise.

__attrs_props__

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

__doc__

str(object='') -> str

__firstlineno__

int([x]) -> integer

__match_args__

Built-in immutable sequence.

__module__

str(object='') -> str

__slots__

Built-in immutable sequence.

__static_attributes__

Built-in immutable sequence.

Source code in sleap_io/model/label_image.py
@attrs.define(eq=False)
class UserLabelImage(LabelImage):
    """Human-annotated label image."""

    pass

__annotations__ = {} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = False class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = 'Human-annotated label image.' class-attribute

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

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

__firstlineno__ = 931 class-attribute

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

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

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

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

__match_args__ = ('_data', 'objects', 'source', 'scale', 'offset') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.label_image' class-attribute

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

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

__slots__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = () class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__init__(data=None, objects=NOTHING, source='', scale=(1.0, 1.0), offset=(0.0, 0.0))

Method generated by attrs for class UserLabelImage.

Source code in sleap_io/model/label_image.py
- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows). Each
  mask is stored separately with RLE compression.
- Use ``LabelImage.from_binary_masks()`` to create a label image directly from
  per-object binary numpy arrays (e.g., from SAM or Mask R-CNN output).
- To convert between them, use ``LabelImage.to_masks()`` and
  ``LabelImage.from_masks()``.

For TIFF I/O of label images, see ``sleap_io.load_label_images()`` and
``sleap_io.save_label_images()``.

See Also:
    ``sleap_io.model.mask``: Binary segmentation masks (one per object).

__repr__()

Method generated by attrs for class UserLabelImage.

Source code in sleap_io/model/label_image.py
"""Data structure for integer label image annotations.

Label images represent per-pixel object segmentation for a single video frame,
where each pixel value encodes which object occupies that pixel. This is the
standard output format of instance segmentation tools like Cellpose and StarDist.

Unlike binary ``SegmentationMask`` objects (one mask per object), a single
``LabelImage`` efficiently stores all objects for a frame in one dense integer
array.

**When to use LabelImage vs SegmentationMask:**

- Use ``LabelImage`` when you have a dense integer array from a segmentation
  tool (Cellpose, StarDist, COCO panoptic) where each pixel value identifies
  an object. One ``LabelImage`` per frame stores all objects at once.

sleap_io.PredictedLabelImage

Bases: sleap_io.model.label_image.LabelImage

Model-predicted label image with confidence score.

Attributes:

Name Type Description
score

Image-level confidence score (0-1).

score_map

Optional dense pixel-level confidence map of shape (H, W) as float32. This can be large and is stored separately in the SLP format. If None, only per-object scores in Info are available. When loaded lazily, decompressed on first access and cached.

score_map_scale

Resolution ratio (sx, sy) for the score map, independent of the label image's own scale.

score_map_offset

Origin (x, y) of the score map in image pixel coordinates.

Methods:

Name Description
__init__

Method generated by attrs for class PredictedLabelImage.

__repr__

Method generated by attrs for class PredictedLabelImage.

Source code in sleap_io/model/label_image.py
@attrs.define(eq=False)
class PredictedLabelImage(LabelImage):
    """Model-predicted label image with confidence score.

    Attributes:
        score: Image-level confidence score (0-1).
        score_map: Optional dense pixel-level confidence map of shape (H, W)
            as float32. This can be large and is stored separately in the SLP
            format. If ``None``, only per-object scores in ``Info`` are available.
            When loaded lazily, decompressed on first access and cached.
        score_map_scale: Resolution ratio ``(sx, sy)`` for the score map,
            independent of the label image's own ``scale``.
        score_map_offset: Origin ``(x, y)`` of the score map in image pixel
            coordinates.
    """

    score: float = attrs.field(default=0.0)
    _score_map: "np.ndarray | None" = attrs.field(default=None, alias="score_map")
    score_map_scale: tuple[float, float] = attrs.field(default=(1.0, 1.0))
    score_map_offset: tuple[float, float] = attrs.field(default=(0.0, 0.0))

    # Private: lazy loading support for score maps.
    _score_map_lazy_loader: "Callable[[], np.ndarray] | None" = attrs.field(
        default=None, init=False, repr=False, eq=False
    )

    @property
    def score_map(self) -> np.ndarray | None:
        """Optional dense pixel-level confidence map of shape ``(H, W)``."""
        if self._score_map is None and self._score_map_lazy_loader is not None:
            self._score_map = self._score_map_lazy_loader()
            self._score_map_lazy_loader = None
        return self._score_map

    @score_map.setter
    def score_map(self, value: np.ndarray | None) -> None:
        self._score_map = value
        self._score_map_lazy_loader = None

__annotations__ = {'score': 'float', '_score_map': "'np.ndarray | None'", 'score_map_scale': 'tuple[float, float]', 'score_map_offset': 'tuple[float, float]', '_score_map_lazy_loader': "'Callable[[], np.ndarray] | None'"} class-attribute

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

__attrs_own_setattr__ = False class-attribute

Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.

__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

Whether / how the class enforces keyword-only arguments on the __init__ method.

collected_fields_by_mro bool

Whether the class fields were collected by method resolution order. That is, correctly but unlike dataclasses.

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

Whether the class supports positional match <match> over its fields.

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

Whether the class has attrs-generated __getstate__ and __setstate__ methods for pickle.

on_setattr_hook Callable[[Any, Attribute[Any], Any], Any] | None

The class's __setattr__ hook.

field_transformer Callable[[Attribute[Any]], Attribute[Any]] | None

The class's field transformers <transform-fields>.

.. versionadded:: 25.4.0

__doc__ = "Model-predicted label image with confidence score.\n\nAttributes:\n score: Image-level confidence score (0-1).\n score_map: Optional dense pixel-level confidence map of shape (H, W)\n as float32. This can be large and is stored separately in the SLP\n format. If ``None``, only per-object scores in ``Info`` are available.\n When loaded lazily, decompressed on first access and cached.\n score_map_scale: Resolution ratio ``(sx, sy)`` for the score map,\n independent of the label image's own ``scale``.\n score_map_offset: Origin ``(x, y)`` of the score map in image pixel\n coordinates.\n" class-attribute

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

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

__firstlineno__ = 938 class-attribute

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

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

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

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

__match_args__ = ('_data', 'objects', 'source', 'scale', 'offset', 'score', '_score_map', 'score_map_scale', 'score_map_offset') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__module__ = 'sleap_io.model.label_image' class-attribute

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

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

__slots__ = ('score', '_score_map', 'score_map_scale', 'score_map_offset', '_score_map_lazy_loader') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__static_attributes__ = ('_score_map', '_score_map_lazy_loader') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

score_map property

Optional dense pixel-level confidence map of shape (H, W).

__init__(data=None, objects=NOTHING, source='', scale=(1.0, 1.0), offset=(0.0, 0.0), score=0.0, score_map=None, score_map_scale=(1.0, 1.0), score_map_offset=(0.0, 0.0))

Method generated by attrs for class PredictedLabelImage.

Source code in sleap_io/model/label_image.py
- Use ``SegmentationMask`` when you have individual binary masks per object
  (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows). Each
  mask is stored separately with RLE compression.
- Use ``LabelImage.from_binary_masks()`` to create a label image directly from
  per-object binary numpy arrays (e.g., from SAM or Mask R-CNN output).
- To convert between them, use ``LabelImage.to_masks()`` and
  ``LabelImage.from_masks()``.

For TIFF I/O of label images, see ``sleap_io.load_label_images()`` and
``sleap_io.save_label_images()``.

See Also:
    ``sleap_io.model.mask``: Binary segmentation masks (one per object).
"""

from __future__ import annotations

import copy

__repr__()

Method generated by attrs for class PredictedLabelImage.

Source code in sleap_io/model/label_image.py
"""Data structure for integer label image annotations.

Label images represent per-pixel object segmentation for a single video frame,
where each pixel value encodes which object occupies that pixel. This is the
standard output format of instance segmentation tools like Cellpose and StarDist.

Unlike binary ``SegmentationMask`` objects (one mask per object), a single
``LabelImage`` efficiently stores all objects for a frame in one dense integer
array.

**When to use LabelImage vs SegmentationMask:**

- Use ``LabelImage`` when you have a dense integer array from a segmentation
  tool (Cellpose, StarDist, COCO panoptic) where each pixel value identifies
  an object. One ``LabelImage`` per frame stores all objects at once.

sleap_io.LabelImageWriter

Streaming writer for label image annotations to SLP files.

Writes label images one at a time (or in batches) to an HDF5/SLP file without holding all pixel data in memory simultaneously. Uses the chunked (T, H, W) int32 format with write_direct_chunk for maximum throughput.

The HDF5 file and pixel dataset are created lazily on the first add() call, since the frame dimensions (H, W) are needed to define the dataset shape. All subsequent frames must have the same dimensions.

Attributes:

Name Type Description
path

Path to the output SLP file.

video

Optional Video to associate with all label images.

tracks

Optional list of Track objects.

skeleton

Optional Skeleton for metadata.

initial_capacity

Initial number of frames to allocate in the dataset.

Example::

with LabelImageWriter("output.slp", video=video) as writer:
    for frame_data in segmentation_results:
        li = UserLabelImage(data=frame_data, video=video, frame_idx=i)
        writer.add(li)
    labels = writer.finalize()

Methods:

Name Description
__enter__

Enter context manager.

__exit__

Exit context manager, finalizing if not already done.

__init__

Initialize the streaming label image writer.

add

Add a single label image to the file.

add_batch

Add multiple label images at once.

finalize

Finish writing, close the file, and return a Labels object.

Source code in sleap_io/io/slp.py
class LabelImageWriter:
    """Streaming writer for label image annotations to SLP files.

    Writes label images one at a time (or in batches) to an HDF5/SLP file
    without holding all pixel data in memory simultaneously. Uses the chunked
    ``(T, H, W)`` int32 format with ``write_direct_chunk`` for maximum
    throughput.

    The HDF5 file and pixel dataset are created lazily on the first ``add()``
    call, since the frame dimensions ``(H, W)`` are needed to define the
    dataset shape. All subsequent frames must have the same dimensions.

    Attributes:
        path: Path to the output SLP file.
        video: Optional ``Video`` to associate with all label images.
        tracks: Optional list of ``Track`` objects.
        skeleton: Optional ``Skeleton`` for metadata.
        initial_capacity: Initial number of frames to allocate in the dataset.

    Example::

        with LabelImageWriter("output.slp", video=video) as writer:
            for frame_data in segmentation_results:
                li = UserLabelImage(data=frame_data, video=video, frame_idx=i)
                writer.add(li)
            labels = writer.finalize()
    """

    def __init__(
        self,
        path: str,
        video: Video | None = None,
        tracks: list[Track] | None = None,
        skeleton: Skeleton | None = None,
        initial_capacity: int = 100,
    ):
        """Initialize the streaming label image writer.

        Args:
            path: Path to the output SLP file.
            video: Optional video to associate with all label images.
            tracks: Optional initial list of tracks for object associations.
                New tracks encountered in ``add()`` calls are appended
                automatically.
            skeleton: Optional skeleton for metadata.
            initial_capacity: Initial number of frames to allocate. The dataset
                grows exponentially (doubles) when capacity is exceeded.
        """
        self.path = str(path)
        self.video = video
        self.tracks = tracks or []
        self.skeleton = skeleton
        self.initial_capacity = initial_capacity

        # State
        self._file: h5py.File | None = None
        self._pixel_dset: h5py.Dataset | None = None
        self._frame_h: int = 0
        self._frame_w: int = 0
        self._capacity: int = initial_capacity
        self._count: int = 0

        # Accumulated metadata (kept in memory, ~80 bytes/frame + 16 bytes/obj)
        self._li_rows: list[tuple] = []
        self._obj_rows: list[tuple] = []
        self._obj_offset: int = 0
        self._sources: list[str] = []
        self._categories: list[str] = []
        self._obj_names: list[str] = []

        # Score map data (blob format, accumulated in memory)
        self._sm_indices: list[tuple] = []
        self._sm_chunks: list[np.ndarray] = []
        self._sm_offset: int = 0

        self._finalized: bool = False

    def _ensure_file(self, height: int, width: int) -> None:
        """Create the HDF5 file and pixel dataset on first use.

        Args:
            height: Frame height in pixels.
            width: Frame width in pixels.
        """
        if self._file is not None:
            return

        self._frame_h = height
        self._frame_w = width

        self._file = h5py.File(self.path, "w")
        self._pixel_dset = self._file.create_dataset(
            "label_image_data",
            shape=(self._capacity, height, width),
            maxshape=(None, height, width),
            chunks=(1, height, width),
            dtype=np.int32,
            compression="gzip",
            compression_opts=1,
        )

    def _grow_if_needed(self) -> None:
        """Double the pixel dataset capacity if it's full."""
        if self._count >= self._capacity:
            self._capacity *= 2
            self._pixel_dset.resize(self._capacity, axis=0)

    def add(
        self,
        label_image: LabelImage,
        video_idx: int = -1,
        frame_idx: int = -1,
    ) -> None:
        """Add a single label image to the file.

        The first call creates the HDF5 file and locks the frame dimensions.
        Subsequent calls must provide frames with the same ``(H, W)``.

        Args:
            label_image: The label image to write.
            video_idx: Video index for routing context. Defaults to ``-1``.
            frame_idx: Frame index for routing context. Defaults to ``-1``.

        Raises:
            ValueError: If the frame dimensions don't match the first frame.
            RuntimeError: If the writer has already been finalized.
        """
        if self._finalized:
            raise RuntimeError("Writer has already been finalized.")

        # Auto-resolve video_idx from self.video when not explicitly provided
        if video_idx == -1 and self.video is not None:
            video_idx = 0
        # Auto-resolve frame_idx from write count when not explicitly provided
        if frame_idx == -1:
            frame_idx = self._count

        h, w = label_image.height, label_image.width
        self._ensure_file(h, w)

        if h != self._frame_h or w != self._frame_w:
            raise ValueError(
                f"Frame size ({h}, {w}) does not match expected "
                f"({self._frame_h}, {self._frame_w}). All frames must have "
                f"the same dimensions."
            )

        idx = self._count
        self._grow_if_needed()

        # Write pixel data via write_direct_chunk
        compressed = zlib.compress(label_image.data.astype(np.int32).tobytes(), level=1)
        self._pixel_dset.id.write_direct_chunk((idx, 0, 0), compressed)

        # Use provided routing context (video_idx, frame_idx are parameters)

        # Build object rows (auto-collect new tracks)
        n_objects = len(label_image.objects)
        objects_start = self._obj_offset
        _track_set = set(self.tracks)

        for label_id in sorted(label_image.objects):
            info = label_image.objects[label_id]
            if info.track is not None and info.track not in _track_set:
                self.tracks.append(info.track)
                _track_set.add(info.track)
            track_idx = (
                self.tracks.index(info.track) if info.track in self.tracks else -1
            )
            instance_idx = info._instance_idx
            obj_score = info.score if info.score is not None else float("nan")
            obj_tracking_score = (
                info.tracking_score if info.tracking_score is not None else float("nan")
            )
            self._obj_rows.append(
                (label_id, track_idx, instance_idx, obj_score, obj_tracking_score)
            )
            self._categories.append(info.category)
            self._obj_names.append(info.name)

        self._obj_offset += n_objects

        is_predicted = isinstance(label_image, PredictedLabelImage)
        score = label_image.score if is_predicted else float("nan")

        self._li_rows.append(
            (
                video_idx,
                frame_idx,
                h,
                w,
                n_objects,
                objects_start,
                0,  # data_start (unused for chunked)
                0,  # data_end (unused for chunked)
                int(is_predicted),
                score,
                label_image.scale[0],
                label_image.scale[1],
                label_image.offset[0],
                label_image.offset[1],
            )
        )

        self._sources.append(label_image.source)

        # Score map handling for PredictedLabelImage
        if is_predicted and label_image.score_map is not None:
            sm = label_image.score_map
            compressed_sm = zlib.compress(sm.astype(np.float32).tobytes())
            sm_bytes = np.frombuffer(compressed_sm, dtype=np.uint8)
            sm_h, sm_w = sm.shape[:2]
            self._sm_indices.append(
                (
                    idx,
                    self._sm_offset,
                    self._sm_offset + len(sm_bytes),
                    sm_h,
                    sm_w,
                    label_image.score_map_scale[0],
                    label_image.score_map_scale[1],
                    label_image.score_map_offset[0],
                    label_image.score_map_offset[1],
                )
            )
            self._sm_chunks.append(sm_bytes)
            self._sm_offset += len(sm_bytes)

        self._count += 1

    def add_batch(self, label_images: list[LabelImage]) -> None:
        """Add multiple label images at once.

        Convenience wrapper that calls ``add()`` for each label image.

        Args:
            label_images: List of label images to write.
        """
        for li in label_images:
            self.add(li)

    def finalize(self) -> Labels:
        """Finish writing, close the file, and return a ``Labels`` object.

        Trims the pixel dataset to the actual number of frames written, writes
        all metadata datasets, and closes the HDF5 file. The returned
        ``Labels`` object can be used directly or the file can be re-loaded
        with ``load_slp()``.

        Returns:
            A ``Labels`` object pointing at the written file.

        Raises:
            RuntimeError: If the writer has already been finalized.
        """
        if self._finalized:
            raise RuntimeError("Writer has already been finalized.")
        self._finalized = True

        # Handle empty writer (no frames added)
        if self._file is None:
            # Create minimal empty SLP file
            with h5py.File(self.path, "w"):
                pass
            videos = [self.video] if self.video is not None else []
            skeletons = [self.skeleton] if self.skeleton is not None else []
            write_videos(self.path, videos)
            write_video_crops(self.path, Labels(videos=videos))
            write_tracks(self.path, self.tracks)
            _write_metadata_standalone(self.path, skeletons=skeletons, videos=videos)
            return Labels(
                videos=videos,
                skeletons=skeletons,
                tracks=self.tracks,
            )

        # Trim pixel dataset to actual count
        self._pixel_dset.resize(self._count, axis=0)

        # Write metadata datasets
        li_array = np.array(self._li_rows, dtype=LI_DTYPE)
        obj_array = (
            np.array(self._obj_rows, dtype=OBJ_DTYPE)
            if self._obj_rows
            else np.array([], dtype=OBJ_DTYPE)
        )

        str_dt = h5py.special_dtype(vlen=str)
        f = self._file
        f.create_dataset("label_images", data=li_array, dtype=LI_DTYPE)
        f.create_dataset("label_image_objects", data=obj_array, dtype=OBJ_DTYPE)
        f.create_dataset("label_image_sources", data=self._sources, dtype=str_dt)
        f.create_dataset(
            "label_image_obj_categories", data=self._categories, dtype=str_dt
        )
        f.create_dataset("label_image_obj_names", data=self._obj_names, dtype=str_dt)

        # Write score maps if any
        if self._sm_indices:
            sm_index_array = np.array(self._sm_indices, dtype=LI_SM_INDEX_DTYPE)
            sm_flat = np.concatenate(self._sm_chunks)
            f.create_dataset("label_image_score_map_index", data=sm_index_array)
            f.create_dataset(
                "label_image_score_maps",
                data=sm_flat,
                dtype=np.uint8,
                **({"chunks": True} if len(sm_flat) > 0 else {}),
            )

        # Close HDF5 file before writing video/track/metadata
        f.close()
        self._file = None

        # Write video, track, and metadata info
        videos = [self.video] if self.video is not None else []
        skeletons = [self.skeleton] if self.skeleton is not None else []
        write_videos(self.path, videos)
        write_video_crops(self.path, Labels(videos=videos))
        write_tracks(self.path, self.tracks)
        _write_metadata_standalone(self.path, skeletons=skeletons, videos=videos)

        li_tuples, li_file = read_label_images(self.path, videos, self.tracks, [])
        # Distribute label images to frames
        labeled_frames = []
        frame_lookup: dict[tuple[int, int], LabeledFrame] = {}
        for li, vid_idx, fidx in li_tuples:
            key = (vid_idx, fidx)
            if key not in frame_lookup:
                video = videos[vid_idx] if 0 <= vid_idx < len(videos) else None
                if video is not None:
                    lf = LabeledFrame(video=video, frame_idx=fidx)
                    labeled_frames.append(lf)
                    frame_lookup[key] = lf
            if key in frame_lookup:
                frame_lookup[key].label_images.append(li)
        labels = Labels(
            labeled_frames=labeled_frames,
            videos=videos,
            skeletons=skeletons,
            tracks=self.tracks,
        )
        if li_file is not None:
            labels._label_image_file = li_file
        return labels

    def __enter__(self) -> "LabelImageWriter":
        """Enter context manager."""
        return self

    def __exit__(self, *exc: object) -> None:
        """Exit context manager, finalizing if not already done."""
        if not self._finalized:
            self.finalize()
        elif self._file is not None:
            self._file.close()
            self._file = None

__dict__ = mappingproxy({'__module__': 'sleap_io.io.slp', '__firstlineno__': 7569, '__doc__': 'Streaming writer for label image annotations to SLP files.\n\nWrites label images one at a time (or in batches) to an HDF5/SLP file\nwithout holding all pixel data in memory simultaneously. Uses the chunked\n``(T, H, W)`` int32 format with ``write_direct_chunk`` for maximum\nthroughput.\n\nThe HDF5 file and pixel dataset are created lazily on the first ``add()``\ncall, since the frame dimensions ``(H, W)`` are needed to define the\ndataset shape. All subsequent frames must have the same dimensions.\n\nAttributes:\n path: Path to the output SLP file.\n video: Optional ``Video`` to associate with all label images.\n tracks: Optional list of ``Track`` objects.\n skeleton: Optional ``Skeleton`` for metadata.\n initial_capacity: Initial number of frames to allocate in the dataset.\n\nExample::\n\n with LabelImageWriter("output.slp", video=video) as writer:\n for frame_data in segmentation_results:\n li = UserLabelImage(data=frame_data, video=video, frame_idx=i)\n writer.add(li)\n labels = writer.finalize()\n', '__init__': <function LabelImageWriter.__init__ at 0x7f0827dfae80>, '_ensure_file': <function LabelImageWriter._ensure_file at 0x7f0827dfaf20>, '_grow_if_needed': <function LabelImageWriter._grow_if_needed at 0x7f0827dfafc0>, 'add': <function LabelImageWriter.add at 0x7f0827dfb060>, 'add_batch': <function LabelImageWriter.add_batch at 0x7f0827dfb100>, 'finalize': <function LabelImageWriter.finalize at 0x7f0827dfb1a0>, '__enter__': <function LabelImageWriter.__enter__ at 0x7f0827dfb240>, '__exit__': <function LabelImageWriter.__exit__ at 0x7f0827dfb2e0>, '__static_attributes__': ('_capacity', '_categories', '_count', '_file', '_finalized', '_frame_h', '_frame_w', '_li_rows', '_obj_names', '_obj_offset', '_obj_rows', '_pixel_dset', '_sm_chunks', '_sm_indices', '_sm_offset', '_sources', 'initial_capacity', 'path', 'skeleton', 'tracks', 'video'), '__dict__': <attribute '__dict__' of 'LabelImageWriter' objects>, '__weakref__': <attribute '__weakref__' of 'LabelImageWriter' objects>}) class-attribute

Read-only proxy of a mapping.

__doc__ = 'Streaming writer for label image annotations to SLP files.\n\nWrites label images one at a time (or in batches) to an HDF5/SLP file\nwithout holding all pixel data in memory simultaneously. Uses the chunked\n``(T, H, W)`` int32 format with ``write_direct_chunk`` for maximum\nthroughput.\n\nThe HDF5 file and pixel dataset are created lazily on the first ``add()``\ncall, since the frame dimensions ``(H, W)`` are needed to define the\ndataset shape. All subsequent frames must have the same dimensions.\n\nAttributes:\n path: Path to the output SLP file.\n video: Optional ``Video`` to associate with all label images.\n tracks: Optional list of ``Track`` objects.\n skeleton: Optional ``Skeleton`` for metadata.\n initial_capacity: Initial number of frames to allocate in the dataset.\n\nExample::\n\n with LabelImageWriter("output.slp", video=video) as writer:\n for frame_data in segmentation_results:\n li = UserLabelImage(data=frame_data, video=video, frame_idx=i)\n writer.add(li)\n labels = writer.finalize()\n' class-attribute

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

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

__firstlineno__ = 7569 class-attribute

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

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

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

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

__module__ = 'sleap_io.io.slp' class-attribute

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

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

__static_attributes__ = ('_capacity', '_categories', '_count', '_file', '_finalized', '_frame_h', '_frame_w', '_li_rows', '_obj_names', '_obj_offset', '_obj_rows', '_pixel_dset', '_sm_chunks', '_sm_indices', '_sm_offset', '_sources', 'initial_capacity', 'path', 'skeleton', 'tracks', 'video') class-attribute

Built-in immutable sequence.

If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.

If the argument is a tuple, the return value is the same object.

__weakref__ property

list of weak references to the object

__enter__()

Enter context manager.

Source code in sleap_io/io/slp.py
def __enter__(self) -> "LabelImageWriter":
    """Enter context manager."""
    return self

__exit__(*exc)

Exit context manager, finalizing if not already done.

Source code in sleap_io/io/slp.py
def __exit__(self, *exc: object) -> None:
    """Exit context manager, finalizing if not already done."""
    if not self._finalized:
        self.finalize()
    elif self._file is not None:
        self._file.close()
        self._file = None

__init__(path, video=None, tracks=None, skeleton=None, initial_capacity=100)

Initialize the streaming label image writer.

Parameters:

Name Type Description Default
path str

Path to the output SLP file.

required
video Video | None

Optional video to associate with all label images.

None
tracks list[Track] | None

Optional initial list of tracks for object associations. New tracks encountered in add() calls are appended automatically.

None
skeleton Skeleton | None

Optional skeleton for metadata.

None
initial_capacity int

Initial number of frames to allocate. The dataset grows exponentially (doubles) when capacity is exceeded.

100
Source code in sleap_io/io/slp.py
def __init__(
    self,
    path: str,
    video: Video | None = None,
    tracks: list[Track] | None = None,
    skeleton: Skeleton | None = None,
    initial_capacity: int = 100,
):
    """Initialize the streaming label image writer.

    Args:
        path: Path to the output SLP file.
        video: Optional video to associate with all label images.
        tracks: Optional initial list of tracks for object associations.
            New tracks encountered in ``add()`` calls are appended
            automatically.
        skeleton: Optional skeleton for metadata.
        initial_capacity: Initial number of frames to allocate. The dataset
            grows exponentially (doubles) when capacity is exceeded.
    """
    self.path = str(path)
    self.video = video
    self.tracks = tracks or []
    self.skeleton = skeleton
    self.initial_capacity = initial_capacity

    # State
    self._file: h5py.File | None = None
    self._pixel_dset: h5py.Dataset | None = None
    self._frame_h: int = 0
    self._frame_w: int = 0
    self._capacity: int = initial_capacity
    self._count: int = 0

    # Accumulated metadata (kept in memory, ~80 bytes/frame + 16 bytes/obj)
    self._li_rows: list[tuple] = []
    self._obj_rows: list[tuple] = []
    self._obj_offset: int = 0
    self._sources: list[str] = []
    self._categories: list[str] = []
    self._obj_names: list[str] = []

    # Score map data (blob format, accumulated in memory)
    self._sm_indices: list[tuple] = []
    self._sm_chunks: list[np.ndarray] = []
    self._sm_offset: int = 0

    self._finalized: bool = False

add(label_image, video_idx=-1, frame_idx=-1)

Add a single label image to the file.

The first call creates the HDF5 file and locks the frame dimensions. Subsequent calls must provide frames with the same (H, W).

Parameters:

Name Type Description Default
label_image LabelImage

The label image to write.

required
video_idx int

Video index for routing context. Defaults to -1.

-1
frame_idx int

Frame index for routing context. Defaults to -1.

-1

Raises:

Type Description
ValueError

If the frame dimensions don't match the first frame.

RuntimeError

If the writer has already been finalized.

Source code in sleap_io/io/slp.py
def add(
    self,
    label_image: LabelImage,
    video_idx: int = -1,
    frame_idx: int = -1,
) -> None:
    """Add a single label image to the file.

    The first call creates the HDF5 file and locks the frame dimensions.
    Subsequent calls must provide frames with the same ``(H, W)``.

    Args:
        label_image: The label image to write.
        video_idx: Video index for routing context. Defaults to ``-1``.
        frame_idx: Frame index for routing context. Defaults to ``-1``.

    Raises:
        ValueError: If the frame dimensions don't match the first frame.
        RuntimeError: If the writer has already been finalized.
    """
    if self._finalized:
        raise RuntimeError("Writer has already been finalized.")

    # Auto-resolve video_idx from self.video when not explicitly provided
    if video_idx == -1 and self.video is not None:
        video_idx = 0
    # Auto-resolve frame_idx from write count when not explicitly provided
    if frame_idx == -1:
        frame_idx = self._count

    h, w = label_image.height, label_image.width
    self._ensure_file(h, w)

    if h != self._frame_h or w != self._frame_w:
        raise ValueError(
            f"Frame size ({h}, {w}) does not match expected "
            f"({self._frame_h}, {self._frame_w}). All frames must have "
            f"the same dimensions."
        )

    idx = self._count
    self._grow_if_needed()

    # Write pixel data via write_direct_chunk
    compressed = zlib.compress(label_image.data.astype(np.int32).tobytes(), level=1)
    self._pixel_dset.id.write_direct_chunk((idx, 0, 0), compressed)

    # Use provided routing context (video_idx, frame_idx are parameters)

    # Build object rows (auto-collect new tracks)
    n_objects = len(label_image.objects)
    objects_start = self._obj_offset
    _track_set = set(self.tracks)

    for label_id in sorted(label_image.objects):
        info = label_image.objects[label_id]
        if info.track is not None and info.track not in _track_set:
            self.tracks.append(info.track)
            _track_set.add(info.track)
        track_idx = (
            self.tracks.index(info.track) if info.track in self.tracks else -1
        )
        instance_idx = info._instance_idx
        obj_score = info.score if info.score is not None else float("nan")
        obj_tracking_score = (
            info.tracking_score if info.tracking_score is not None else float("nan")
        )
        self._obj_rows.append(
            (label_id, track_idx, instance_idx, obj_score, obj_tracking_score)
        )
        self._categories.append(info.category)
        self._obj_names.append(info.name)

    self._obj_offset += n_objects

    is_predicted = isinstance(label_image, PredictedLabelImage)
    score = label_image.score if is_predicted else float("nan")

    self._li_rows.append(
        (
            video_idx,
            frame_idx,
            h,
            w,
            n_objects,
            objects_start,
            0,  # data_start (unused for chunked)
            0,  # data_end (unused for chunked)
            int(is_predicted),
            score,
            label_image.scale[0],
            label_image.scale[1],
            label_image.offset[0],
            label_image.offset[1],
        )
    )

    self._sources.append(label_image.source)

    # Score map handling for PredictedLabelImage
    if is_predicted and label_image.score_map is not None:
        sm = label_image.score_map
        compressed_sm = zlib.compress(sm.astype(np.float32).tobytes())
        sm_bytes = np.frombuffer(compressed_sm, dtype=np.uint8)
        sm_h, sm_w = sm.shape[:2]
        self._sm_indices.append(
            (
                idx,
                self._sm_offset,
                self._sm_offset + len(sm_bytes),
                sm_h,
                sm_w,
                label_image.score_map_scale[0],
                label_image.score_map_scale[1],
                label_image.score_map_offset[0],
                label_image.score_map_offset[1],
            )
        )
        self._sm_chunks.append(sm_bytes)
        self._sm_offset += len(sm_bytes)

    self._count += 1

add_batch(label_images)

Add multiple label images at once.

Convenience wrapper that calls add() for each label image.

Parameters:

Name Type Description Default
label_images list[LabelImage]

List of label images to write.

required
Source code in sleap_io/io/slp.py
def add_batch(self, label_images: list[LabelImage]) -> None:
    """Add multiple label images at once.

    Convenience wrapper that calls ``add()`` for each label image.

    Args:
        label_images: List of label images to write.
    """
    for li in label_images:
        self.add(li)

finalize()

Finish writing, close the file, and return a Labels object.

Trims the pixel dataset to the actual number of frames written, writes all metadata datasets, and closes the HDF5 file. The returned Labels object can be used directly or the file can be re-loaded with load_slp().

Returns:

Type Description
Labels

A Labels object pointing at the written file.

Raises:

Type Description
RuntimeError

If the writer has already been finalized.

Source code in sleap_io/io/slp.py
def finalize(self) -> Labels:
    """Finish writing, close the file, and return a ``Labels`` object.

    Trims the pixel dataset to the actual number of frames written, writes
    all metadata datasets, and closes the HDF5 file. The returned
    ``Labels`` object can be used directly or the file can be re-loaded
    with ``load_slp()``.

    Returns:
        A ``Labels`` object pointing at the written file.

    Raises:
        RuntimeError: If the writer has already been finalized.
    """
    if self._finalized:
        raise RuntimeError("Writer has already been finalized.")
    self._finalized = True

    # Handle empty writer (no frames added)
    if self._file is None:
        # Create minimal empty SLP file
        with h5py.File(self.path, "w"):
            pass
        videos = [self.video] if self.video is not None else []
        skeletons = [self.skeleton] if self.skeleton is not None else []
        write_videos(self.path, videos)
        write_video_crops(self.path, Labels(videos=videos))
        write_tracks(self.path, self.tracks)
        _write_metadata_standalone(self.path, skeletons=skeletons, videos=videos)
        return Labels(
            videos=videos,
            skeletons=skeletons,
            tracks=self.tracks,
        )

    # Trim pixel dataset to actual count
    self._pixel_dset.resize(self._count, axis=0)

    # Write metadata datasets
    li_array = np.array(self._li_rows, dtype=LI_DTYPE)
    obj_array = (
        np.array(self._obj_rows, dtype=OBJ_DTYPE)
        if self._obj_rows
        else np.array([], dtype=OBJ_DTYPE)
    )

    str_dt = h5py.special_dtype(vlen=str)
    f = self._file
    f.create_dataset("label_images", data=li_array, dtype=LI_DTYPE)
    f.create_dataset("label_image_objects", data=obj_array, dtype=OBJ_DTYPE)
    f.create_dataset("label_image_sources", data=self._sources, dtype=str_dt)
    f.create_dataset(
        "label_image_obj_categories", data=self._categories, dtype=str_dt
    )
    f.create_dataset("label_image_obj_names", data=self._obj_names, dtype=str_dt)

    # Write score maps if any
    if self._sm_indices:
        sm_index_array = np.array(self._sm_indices, dtype=LI_SM_INDEX_DTYPE)
        sm_flat = np.concatenate(self._sm_chunks)
        f.create_dataset("label_image_score_map_index", data=sm_index_array)
        f.create_dataset(
            "label_image_score_maps",
            data=sm_flat,
            dtype=np.uint8,
            **({"chunks": True} if len(sm_flat) > 0 else {}),
        )

    # Close HDF5 file before writing video/track/metadata
    f.close()
    self._file = None

    # Write video, track, and metadata info
    videos = [self.video] if self.video is not None else []
    skeletons = [self.skeleton] if self.skeleton is not None else []
    write_videos(self.path, videos)
    write_video_crops(self.path, Labels(videos=videos))
    write_tracks(self.path, self.tracks)
    _write_metadata_standalone(self.path, skeletons=skeletons, videos=videos)

    li_tuples, li_file = read_label_images(self.path, videos, self.tracks, [])
    # Distribute label images to frames
    labeled_frames = []
    frame_lookup: dict[tuple[int, int], LabeledFrame] = {}
    for li, vid_idx, fidx in li_tuples:
        key = (vid_idx, fidx)
        if key not in frame_lookup:
            video = videos[vid_idx] if 0 <= vid_idx < len(videos) else None
            if video is not None:
                lf = LabeledFrame(video=video, frame_idx=fidx)
                labeled_frames.append(lf)
                frame_lookup[key] = lf
        if key in frame_lookup:
            frame_lookup[key].label_images.append(li)
    labels = Labels(
        labeled_frames=labeled_frames,
        videos=videos,
        skeletons=skeletons,
        tracks=self.tracks,
    )
    if li_file is not None:
        labels._label_image_file = li_file
    return labels

sleap_io.merge_label_images(source_paths, dest_path, video=None)

Merge label images from multiple SLP files into one.

Copies compressed chunks directly (no decompression) via read_direct_chunk -> write_direct_chunk when possible, falling back to decompress + recompress for legacy blob-format sources.

Parameters:

Name Type Description Default
source_paths list[str | Path]

List of paths to source SLP files containing label images to merge.

required
dest_path str | Path

Path to the destination SLP file to create.

required
video Video | None

Optional Video to associate with all merged label images. If None, videos are deduplicated by filename across sources.

None

Returns:

Type Description
Labels

A Labels object pointing at the merged file.

Raises:

Type Description
ValueError

If source files have label images with different (height, width) dimensions, or if no source files are provided, or if a source contains no label images.

See also: :func:sleap_io.io.slp.merge_label_images

Source code in sleap_io/io/main.py
def merge_label_images(
    source_paths: list[str | Path],
    dest_path: str | Path,
    video: Video | None = None,
) -> Labels:
    """Merge label images from multiple SLP files into one.

    Copies compressed chunks directly (no decompression) via
    ``read_direct_chunk`` -> ``write_direct_chunk`` when possible, falling
    back to decompress + recompress for legacy blob-format sources.

    Args:
        source_paths: List of paths to source SLP files containing label
            images to merge.
        dest_path: Path to the destination SLP file to create.
        video: Optional ``Video`` to associate with all merged label images.
            If ``None``, videos are deduplicated by filename across sources.

    Returns:
        A ``Labels`` object pointing at the merged file.

    Raises:
        ValueError: If source files have label images with different
            ``(height, width)`` dimensions, or if no source files are
            provided, or if a source contains no label images.

    See also: :func:`sleap_io.io.slp.merge_label_images`
    """
    from sleap_io.io.slp import merge_label_images as _merge_label_images

    return _merge_label_images(source_paths, dest_path, video=video)

sleap_io.normalize_label_ids(label_images, by='track')

Remap label IDs so each group gets a globally consistent ID.

Rewrites .data arrays and .objects dicts in place so that the same Track (or category) always maps to the same pixel value across all frames. IDs are assigned 1, 2, 3, ... in order of first appearance.

Parameters:

Name Type Description Default
label_images list[LabelImage]

Label images to normalize. Modified in place.

required
by Literal['track', 'category']

Grouping key.

  • "track": Each unique Track object gets one ID. Identity is by Python object reference (is), not by name — ensure the same Track instance is shared across frames. Objects with track=None each get a unique ID.
  • "category": Each unique category string gets one ID. Within a frame, multiple objects with the same category merge into one pixel value (semantic segmentation).
'track'

Returns:

Type Description
dict[Track, int] | dict[str, int]

Mapping of group key to assigned label ID. Keys are Track objects when by="track" or category strings when by="category". Objects with track=None or empty category are not included.

Raises:

Type Description
ValueError

If by is not "track" or "category".

Source code in sleap_io/model/label_image.py
def normalize_label_ids(
    label_images: list[LabelImage],
    by: "Literal['track', 'category']" = "track",
) -> "dict[Track, int] | dict[str, int]":
    """Remap label IDs so each group gets a globally consistent ID.

    Rewrites ``.data`` arrays and ``.objects`` dicts in place so that the same
    Track (or category) always maps to the same pixel value across all frames.
    IDs are assigned 1, 2, 3, ... in order of first appearance.

    Args:
        label_images: Label images to normalize. Modified in place.
        by: Grouping key.

            - ``"track"``: Each unique ``Track`` object gets one ID.
              Identity is by Python object reference (``is``), not by
              name — ensure the same ``Track`` instance is shared across
              frames. Objects with ``track=None`` each get a unique ID.
            - ``"category"``: Each unique category string gets one ID.
              Within a frame, multiple objects with the same category
              merge into one pixel value (semantic segmentation).

    Returns:
        Mapping of group key to assigned label ID. Keys are ``Track`` objects
        when ``by="track"`` or category strings when ``by="category"``.
        Objects with ``track=None`` or empty category are not included.

    Raises:
        ValueError: If ``by`` is not ``"track"`` or ``"category"``.
    """
    if by not in ("track", "category"):
        raise ValueError(f"by must be 'track' or 'category', got {by!r}.")

    if not label_images:
        return {}

    if by == "track":
        return _normalize_by_track(label_images)
    else:
        return _normalize_by_category(label_images)