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: ExistingSegmentationMaskobjects 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_capacityframes and doubles when full, then is trimmed to the actual count on finalize. - Score maps:
PredictedLabelImagescore 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:
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→ emptyPolygon().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, andget_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 |
|
source |
Optional string indicating the source of this annotation. |
|
track |
Optional |
|
tracking_score |
Confidence of the track identity assignment. |
|
identity |
Optional global, ground-truth |
|
identity_score |
Score associated with the |
|
instance |
Optional |
|
scale |
Resolution ratio |
|
offset |
Origin |
|
identity_embedding |
Optional |
|
category_score |
Score associated with the |
|
category_embedding |
Optional |
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 |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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 |
|---|---|
|
|
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 |
required |
stride
|
float | None
|
Convenience for setting isotropic scale. If provided, sets
|
None
|
**kwargs
|
Additional keyword arguments passed to the constructor
(including |
required |
Returns:
| Type | Description |
|---|---|
SegmentationMask
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
0.0
|
error_on_empty
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
BoundingBox
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the mask is empty and |
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'
|
error_on_empty
|
bool
|
If |
False
|
Returns:
| Type | Description |
|---|---|
Centroid
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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)
sleap_io.UserSegmentationMask
¶
Bases: sleap_io.model.mask.SegmentationMask
Human-annotated segmentation mask.
Attributes:
| Name | Type | Description |
|---|---|---|
from_predicted |
The |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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 |
|
score_map_scale |
Resolution ratio |
|
score_map_offset |
Origin |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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
|
Returns:
| Type | Description |
|---|---|
UserSegmentationMask
|
A new |
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 |
|
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 |
|
offset |
Origin |
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 |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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. |
|
tracking_score |
Confidence of the track identity assignment.
|
|
category |
Semantic class label (e.g., |
|
name |
Human-readable name (e.g., |
|
instance |
Linked pose |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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)
¶
__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)
¶
__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 |
required |
label_ids
|
list[int] | None
|
Optional list of positive integer label IDs, one per
mask. |
None
|
tracks
|
list[Track] | None
|
List of |
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 |
None
|
create_tracks
|
bool
|
If |
False
|
**kwargs
|
Passed to the |
required |
Returns:
| Type | Description |
|---|---|
LabelImage
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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 |
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
|
categories
|
dict[int, str] | list[str] | None
|
Same pattern as tracks, for category strings.
|
None
|
create_tracks
|
bool
|
If |
False
|
**kwargs
|
Passed to the LabelImage constructor ( source). |
required |
Returns:
| Type | Description |
|---|---|
LabelImage
|
A |
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 |
required |
tracks
|
dict[int, Track] | list[Track] | None
|
Maps label IDs to Tracks (shared across all frames).
|
None
|
categories
|
dict[int, str] | list[str] | None
|
Same pattern as tracks, for category strings. |
None
|
create_tracks
|
bool
|
If |
False
|
score
|
float | list[float] | None
|
Confidence score(s) for |
None
|
score_map
|
ndarray | None
|
Optional |
None
|
**kwargs
|
Passed to every frame's constructor ( |
required |
Returns:
| Type | Description |
|---|---|
list[LabelImage]
|
A list of |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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)
¶
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 |
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 |
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 |
|
__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
__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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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 |
|
score_map_scale |
Resolution ratio |
|
score_map_offset |
Origin |
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 |
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
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
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 |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. 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 |
|
tracks |
Optional list of |
|
skeleton |
Optional |
|
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 |
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__()
¶
__exit__(*exc)
¶
__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 |
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
|
frame_idx
|
int
|
Frame index for routing context. Defaults to |
-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 |
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 |
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 |
None
|
Returns:
| Type | Description |
|---|---|
Labels
|
A |
Raises:
| Type | Description |
|---|---|
ValueError
|
If source files have label images with different
|
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'
|
Returns:
| Type | Description |
|---|---|
dict[Track, int] | dict[str, int]
|
Mapping of group key to assigned label ID. Keys are |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
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)