Skip to content

mask

sleap_io.model.mask

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 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.

Classes:

Name Description
PredictedSegmentationMask

Model-predicted segmentation mask with confidence score.

SegmentationMask

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

UserSegmentationMask

Human-annotated segmentation mask.

Functions:

Name Description
to_category

Coerce a category-like value to a Category (or None).

Attributes:

Name Type Description
TYPE_CHECKING

Returns True when the argument is true, False otherwise.

__cached__

str(object='') -> str

__doc__

str(object='') -> str

__file__

str(object='') -> str

__name__

str(object='') -> str

__package__

str(object='') -> str

TYPE_CHECKING = False module-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.

__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/__pycache__/mask.cpython-313.pyc' module-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'.

__doc__ = 'Data structures for segmentation mask annotations.\n\nSegmentation masks represent raster (per-pixel) annotations stored in\nrun-length encoded (RLE) format for compact storage. They can be converted\nto and from numpy arrays and polygon representations.\n\nEach ``SegmentationMask`` stores a single binary mask for one object. For\ndense per-pixel segmentation where all objects are stored in one integer\narray, see ``LabelImage`` in ``sleap_io.model.label_image``.\n\n**When to use SegmentationMask vs LabelImage:**\n\n- Use ``SegmentationMask`` when you have individual binary masks per object\n (e.g., from Mask R-CNN, manual annotation, or ROI-based workflows).\n- Use ``LabelImage`` when you have a dense integer array from an instance\n segmentation tool (Cellpose, StarDist) where each pixel value identifies\n an object.\n- To convert: ``LabelImage.to_masks()`` decomposes into per-object masks,\n and ``LabelImage.from_masks(masks)`` composes masks into a label image.\n\nSee Also:\n ``sleap_io.model.label_image``: Dense integer label images.\n' module-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'.

__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/mask.py' module-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'.

__name__ = 'sleap_io.model.mask' module-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'.

__package__ = 'sleap_io.model' module-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'.

PredictedSegmentationMask

Bases: sleap_io.model.mask.SegmentationMask

Model-predicted segmentation mask with confidence score.

Attributes:

Name Type Description
score

Object-level confidence score (0-1).

score_map

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

score_map_scale

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

score_map_offset

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

Methods:

Name Description
__init__

Method generated by attrs for class PredictedSegmentationMask.

__repr__

Method generated by attrs for class PredictedSegmentationMask.

__setattr__

Method generated by attrs for class PredictedSegmentationMask.

to_user

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

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

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

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

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

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

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

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

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

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

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

__attrs_own_setattr__ = True class-attribute

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

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

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

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

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

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

__firstlineno__ = 600 class-attribute

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

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

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

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

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

Built-in immutable sequence.

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

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

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

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

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

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

Built-in immutable sequence.

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

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

__static_attributes__ = () class-attribute

Built-in immutable sequence.

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

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

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

Method generated by attrs for class PredictedSegmentationMask.

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

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

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

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

__repr__()

Method generated by attrs for class PredictedSegmentationMask.

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

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

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

**When to use SegmentationMask vs LabelImage:**

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

__setattr__(name, val)

Method generated by attrs for class PredictedSegmentationMask.

to_user(link=True)

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

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

Parameters:

Name Type Description Default
link bool

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

True

Returns:

Type Description
UserSegmentationMask

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

Notes

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

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

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

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

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

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

SegmentationMask

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

Attributes:

Name Type Description
rle_counts

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

height

Height of the mask in pixels.

width

Width of the mask in pixels.

name

Optional human-readable name for this mask.

category

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

source

Optional string indicating the source of this annotation.

track

Optional Track this mask is associated with.

tracking_score

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

identity

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

identity_score

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

instance

Optional Instance this mask is associated with.

scale

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

offset

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

identity_embedding

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

category_score

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

category_embedding

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

Notes

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

See Also

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

Methods:

Name Description
__attrs_post_init__

Validate that this class is not instantiated directly.

__init__

Method generated by attrs for class SegmentationMask.

__repr__

Method generated by attrs for class SegmentationMask.

__setattr__

Method generated by attrs for class SegmentationMask.

from_numpy

Create a SegmentationMask from a 2D numpy array.

resampled

Return a new mask resampled to the target dimensions.

to_bbox

Convert to a BoundingBox object.

to_centroid

Convert the mask to a centroid point.

to_polygon

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

to_roi

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        from sleap_io.model.roi import _apply_padding

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

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

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

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

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

        from sleap_io.model.roi import PredictedROI, UserROI

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

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

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

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

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

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

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

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

__attrs_own_setattr__ = True class-attribute

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

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

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

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

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

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

__firstlineno__ = 121 class-attribute

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

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

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

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

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

Built-in immutable sequence.

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

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

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

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

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

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

Built-in immutable sequence.

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

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

__static_attributes__ = () class-attribute

Built-in immutable sequence.

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

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

__weakref__ property

list of weak references to the object

area property

Number of foreground (True) pixels in the mask.

bbox property

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

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

Returns:

Type Description

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

data property

Decode the mask to a 2D boolean numpy array.

Returns:

Type Description

A boolean array of shape (height, width).

has_spatial_transform property

Whether this mask has non-default scale or offset.

image_extent property

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

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

is_empty property

Whether the mask has no foreground pixels.

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

Returns:

Type Description

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

is_predicted property

Whether this mask is a model prediction.

__attrs_post_init__()

Validate that this class is not instantiated directly.

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

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

Method generated by attrs for class SegmentationMask.

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

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

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

if TYPE_CHECKING:

__repr__()

Method generated by attrs for class SegmentationMask.

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

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

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

**When to use SegmentationMask vs LabelImage:**

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

__setattr__(name, val)

Method generated by attrs for class SegmentationMask.

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

Create a SegmentationMask from a 2D numpy array.

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

Parameters:

Name Type Description Default
mask ndarray

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

required
stride float | None

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

None
**kwargs

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

required

Returns:

Type Description
SegmentationMask

A SegmentationMask with RLE-encoded data.

Raises:

Type Description
ValueError

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

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

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

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

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

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

resampled(target_height, target_width)

Return a new mask resampled to the target dimensions.

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

Parameters:

Name Type Description Default
target_height int

Target height in pixels.

required
target_width int

Target width in pixels.

required

Returns:

Type Description
Self

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

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

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

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

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

to_bbox(padding=0.0, error_on_empty=False)

Convert to a BoundingBox object.

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

Parameters:

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

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

0.0
error_on_empty bool

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

False

Returns:

Type Description
BoundingBox

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

Raises:

Type Description
ValueError

If the mask is empty and error_on_empty is True.

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

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

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

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

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

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

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

    from sleap_io.model.roi import _apply_padding

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

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

Convert the mask to a centroid point.

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

Parameters:

Name Type Description Default
method str

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

'center_of_mass'
error_on_empty bool

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

False

Returns:

Type Description
Centroid

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

Raises:

Type Description
ValueError

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

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

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

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

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

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

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

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

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

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

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

to_polygon()

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

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

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

Returns:

Type Description
ROI

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

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

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

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

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

    from sleap_io.model.roi import PredictedROI, UserROI

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

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

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

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

to_roi()

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

Returns:

Type Description
ROI

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

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

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

UserSegmentationMask

Bases: sleap_io.model.mask.SegmentationMask

Human-annotated segmentation mask.

Attributes:

Name Type Description
from_predicted

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

Methods:

Name Description
__init__

Method generated by attrs for class UserSegmentationMask.

__repr__

Method generated by attrs for class UserSegmentationMask.

__setattr__

Method generated by attrs for class UserSegmentationMask.

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

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

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

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

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

__attrs_own_setattr__ = True class-attribute

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

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

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

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

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

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

__firstlineno__ = 580 class-attribute

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

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

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

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

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

Built-in immutable sequence.

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

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

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

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

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

__slots__ = ('from_predicted',) class-attribute

Built-in immutable sequence.

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

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

__static_attributes__ = () class-attribute

Built-in immutable sequence.

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

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

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

Method generated by attrs for class UserSegmentationMask.

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

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

from __future__ import annotations

import sys
from typing import TYPE_CHECKING

import attrs
import numpy as np

from sleap_io.model.category import to_category

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

__repr__()

Method generated by attrs for class UserSegmentationMask.

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

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

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

**When to use SegmentationMask vs LabelImage:**

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

__setattr__(name, val)

Method generated by attrs for class UserSegmentationMask.

to_category(value)

Coerce a category-like value to a Category (or None).

Promotes the legacy free-form category: str field (the object-detection class label) to a first-class Category, keeping existing category="mouse" call sites working:

  • None or the empty string "" (the old "unset" sentinel) -> None.
  • a non-empty str -> Category(name=value).
  • an existing Category -> returned unchanged.

Parameters:

Name Type Description Default
value Category | str | None

A Category, a class-label string, "", or None.

required

Returns:

Type Description
Category | None

A Category, or None if the input was None / "".

Raises:

Type Description
TypeError

If value is not a Category, str, or None.

Source code in sleap_io/model/category.py
def to_category(value: "Category | str | None") -> "Category | None":
    """Coerce a category-like value to a `Category` (or ``None``).

    Promotes the legacy free-form ``category: str`` field (the object-detection
    class label) to a first-class `Category`, keeping existing ``category="mouse"``
    call sites working:

    - ``None`` or the empty string ``""`` (the old "unset" sentinel) -> ``None``.
    - a non-empty ``str`` -> ``Category(name=value)``.
    - an existing `Category` -> returned unchanged.

    Args:
        value: A `Category`, a class-label string, ``""``, or ``None``.

    Returns:
        A `Category`, or ``None`` if the input was ``None`` / ``""``.

    Raises:
        TypeError: If `value` is not a `Category`, `str`, or ``None``.
    """
    if value is None:
        return None
    if isinstance(value, Category):
        return value
    if isinstance(value, str):
        if value == "":
            return None
        return Category(name=value)
    raise TypeError(
        f"category must be a Category, str, or None, got {type(value).__name__}."
    )