Skip to content

video_reading

sleap_io.io.video_reading

Backends for reading videos.

Classes:

Name Description
CropVideoBackend

Virtual, axis-aligned, on-read crop of an inner :class:VideoBackend.

HDF5Video

Video backend for reading videos stored in HDF5 files.

ImageVideo

Video backend for reading videos stored as image files.

MediaVideo

Video backend for reading videos stored as common media files.

TiffVideo

Video backend for reading multi-page TIFF stacks.

VideoBackend

Base class for video backends.

Functions:

Name Description
crop_frame

Crop a frame to the specified region.

crop_points

Adjust point coordinates for a crop transformation.

get_available_image_backends

Get list of available image backend plugins.

get_available_video_backends

Get list of available video backend plugins.

get_default_image_plugin

Get the current default image plugin.

get_default_video_plugin

Get the current default video plugin.

get_installation_instructions

Get installation instructions for backend plugins.

normalize_image_plugin_name

Normalize image plugin names to standard format.

normalize_plugin_name

Normalize plugin names to standard format.

set_default_image_plugin

Set the default image plugin for encoding/decoding embedded images.

set_default_video_plugin

Set the default video plugin for all subsequently loaded videos.

uncrop_points

Map crop-local point coordinates back to source coordinates.

Attributes:

Name Type Description
__annotations__

dict() -> new empty dictionary

__cached__

str(object='') -> str

__doc__

str(object='') -> str

__file__

str(object='') -> str

__name__

str(object='') -> str

__package__

str(object='') -> str

__annotations__ = {'_default_video_plugin': 'str | None', '_default_image_plugin': 'str | None'} module-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)

__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/io/__pycache__/video_reading.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__ = 'Backends for reading videos.' 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/io/video_reading.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.io.video_reading' 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.io' 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'.

CropVideoBackend

Bases: sleap_io.io.video_reading.VideoBackend

Virtual, axis-aligned, on-read crop of an inner :class:VideoBackend.

Wraps an inner backend and reports a cropped (F, h, w, c) view. Frames are decoded by the inner backend, then cropped/padded by :func:sleap_io.transform.frame.crop_frame (byte-identical), so no pixels are copied or re-encoded on disk. Frame count is unchanged (a crop is spatial, not temporal). Reports the cropped shape/grayscale and is a drop-in substitute anywhere a VideoBackend is accepted.

Equality is by object identity (eq=False): inherited cache fields (_cached_shape, _open_reader, ...) would otherwise pollute value equality, and dedup never relies on backend == (it uses an explicit crop key). Always construct via :meth:wrap (never the raw constructor) so the "inner is never a crop" invariant and fill-aware flatten hold by construction.

Attributes:

Name Type Description
inner

The wrapped source backend. Decodes full frames; this wrapper crops its output. Invariant: inner is never itself a CropVideoBackend (enforced by :meth:wrap).

crop

Crop region (x1, y1, x2, y2), x2/y2 exclusive. May be negative or exceed source bounds; out-of-bounds regions are pad-filled. Stored as a tuple of ints.

fill

Fill value for out-of-bounds regions, forwarded to crop_frame.

owns_inner

Whether this wrapper owns the inner backend's decode handle. If True (the default), :meth:close cascades to inner.close(); if False (a shared-decode mosaic tile), it does not, so closing one tile does not tear down siblings sharing the inner.

filename

Derived from inner.filename in post-init; NOT a constructor argument.

Methods:

Name Description
__attrs_post_init__

Derive filename from the inner; inherit resolved grayscale/fps lazily.

__init__

Method generated by attrs for class CropVideoBackend.

__repr__

Method generated by attrs for class CropVideoBackend.

__setattr__

Method generated by attrs for class CropVideoBackend.

close

Release this wrapper's handle and the inner's, if owned.

detect_grayscale

Resolve grayscale from the inner, ignoring any passed cropped image.

has_frame

Check if a frame index is contained in the video (delegates to inner).

read_test_frame

Read the inner backend's UNCROPPED test frame (frame_map-safe).

to_crop_coords

Map source-frame (x, y) coordinates into the cropped frame.

to_source_coords

Map cropped-frame (x, y) coordinates back to source coordinates.

wrap

Wrap inner in a crop view, flattening crop-of-crop when safe.

Source code in sleap_io/io/video_reading.py
@attrs.define(eq=False)
class CropVideoBackend(VideoBackend):
    """Virtual, axis-aligned, on-read crop of an inner :class:`VideoBackend`.

    Wraps an inner backend and reports a cropped ``(F, h, w, c)`` view. Frames are
    decoded by the inner backend, then cropped/padded by
    :func:`sleap_io.transform.frame.crop_frame` (byte-identical), so no pixels are
    copied or re-encoded on disk. Frame count is unchanged (a crop is spatial, not
    temporal). Reports the cropped shape/grayscale and is a drop-in substitute
    anywhere a ``VideoBackend`` is accepted.

    Equality is by object identity (``eq=False``): inherited cache fields
    (``_cached_shape``, ``_open_reader``, ...) would otherwise pollute value
    equality, and dedup never relies on backend ``==`` (it uses an explicit crop
    key). Always construct via :meth:`wrap` (never the raw constructor) so the
    "inner is never a crop" invariant and fill-aware flatten hold by construction.

    Attributes:
        inner: The wrapped source backend. Decodes full frames; this wrapper crops
            its output. Invariant: ``inner`` is never itself a ``CropVideoBackend``
            (enforced by :meth:`wrap`).
        crop: Crop region ``(x1, y1, x2, y2)``, ``x2``/``y2`` exclusive. May be
            negative or exceed source bounds; out-of-bounds regions are pad-filled.
            Stored as a tuple of ints.
        fill: Fill value for out-of-bounds regions, forwarded to ``crop_frame``.
        owns_inner: Whether this wrapper owns the inner backend's decode handle. If
            ``True`` (the default), :meth:`close` cascades to ``inner.close()``; if
            ``False`` (a shared-decode mosaic tile), it does not, so closing one
            tile does not tear down siblings sharing the inner.
        filename: Derived from ``inner.filename`` in post-init; NOT a constructor
            argument.
    """

    filename: str | Path | list[str] | list[Path] = attrs.field(
        init=False, default=None
    )
    inner: VideoBackend = attrs.field(kw_only=True)
    crop: tuple[int, int, int, int] = attrs.field(
        kw_only=True, converter=lambda c: tuple(int(v) for v in c)
    )
    fill: int | tuple[int, ...] = attrs.field(
        default=0,
        kw_only=True,
        converter=lambda f: (
            tuple(int(v) for v in f) if isinstance(f, (list, tuple)) else f
        ),
    )
    owns_inner: bool = attrs.field(default=True, kw_only=True)

    def __attrs_post_init__(self) -> None:
        """Derive ``filename`` from the inner; inherit resolved grayscale/fps lazily."""
        object.__setattr__(self, "filename", getattr(self.inner, "filename", None))
        # Inherit only an ALREADY-resolved inner grayscale; never force a decode at
        # construction (keeps crop construction lazy). When unresolved, grayscale is
        # resolved on first real access via the overridden ``detect_grayscale`` /
        # ``img_shape``, always on the inner's FULL frame (so a degenerate crop never
        # skews detection).
        if self.grayscale is None and self.inner.grayscale is not None:
            object.__setattr__(self, "grayscale", self.inner.grayscale)
        # Carry fps so a closed/non-MediaVideo inner still reports fps.
        if self._fps is None:
            object.__setattr__(self, "_fps", getattr(self.inner, "_fps", None))

    @classmethod
    def wrap(
        cls,
        inner: VideoBackend,
        crop: tuple[int, int, int, int],
        fill: int | tuple[int, ...] = 0,
        owns_inner: bool = True,
    ) -> "CropVideoBackend":
        """Wrap ``inner`` in a crop view, flattening crop-of-crop when safe.

        Flattens (composes into a single wrapper) only when ``inner`` is itself a
        ``CropVideoBackend``, the fills agree, AND the outer crop lies fully within
        the inner cropped frame ``[0, iw] x [0, ih]`` (``iw = ix2 - ix1``,
        ``ih = iy2 - iy1``). Otherwise it nests, preserving byte-parity:

        - Different fills: the inner crop's materialized pad of value ``inner.fill``
          would be silently replaced after a flatten.
        - Outer crop exceeds the inner frame: a flatten would read real source
          pixels where the nested view pads with ``fill``.

        The flatten composition law expresses the outer rect in source coordinates:
        ``(ix1 + ox1, iy1 + oy1, ix1 + ox2, iy1 + oy2)``. A flattened ``inner`` is
        always unwrapped to ``inner.inner`` so the "inner is never a crop"
        invariant holds.

        Args:
            inner: The backend to wrap (may itself be a ``CropVideoBackend``).
            crop: Outer crop region ``(x1, y1, x2, y2)``, expressed in the inner
                (possibly already-cropped) frame.
            fill: Fill value for out-of-bounds regions.
            owns_inner: Whether the returned wrapper owns the inner's decode handle
                (see the class ``owns_inner`` attribute).

        Returns:
            A ``CropVideoBackend`` whose ``inner`` is never a crop.
        """

        # Normalize fills (a list and the equivalent tuple must compare equal so a
        # crop reloaded from /video_crops still flattens against its tuple fill).
        def _norm(f):
            return tuple(int(v) for v in f) if isinstance(f, (list, tuple)) else f

        if isinstance(inner, CropVideoBackend) and _norm(inner.fill) == _norm(fill):
            ix1, iy1, ix2, iy2 = inner.crop
            ox1, oy1, ox2, oy2 = (int(v) for v in crop)
            iw, ih = ix2 - ix1, iy2 - iy1
            if 0 <= ox1 and 0 <= oy1 and ox2 <= iw and oy2 <= ih:
                crop = (ix1 + ox1, iy1 + oy1, ix1 + ox2, iy1 + oy2)
                inner = inner.inner  # invariant: inner.inner is never a crop
        return cls(inner=inner, crop=crop, fill=fill, owns_inner=owns_inner)

    @property
    def num_frames(self) -> int:
        """Number of frames in the video (identity: a crop is spatial)."""
        return self.inner.num_frames

    @property
    def img_shape(self) -> tuple[int, int, int]:
        """Shape of a single cropped frame as ``(height, width, channels)``.

        Mirrors the inner's channel policy without forcing a decode here: when the
        wrapper's grayscale is unresolved (``None``), inherit the inner's channel
        count (``inner.img_shape`` resolves it from HDF5 attrs or a single detect),
        so the cropped channels equal the source and stay stable across an SLP
        round-trip. An explicit ``grayscale=True`` still collapses to 1 channel.
        """
        x1, y1, x2, y2 = self.crop
        c = 1 if self.grayscale else self.inner.img_shape[2]
        return int(y2 - y1), int(x2 - x1), int(c)

    @property
    def dataset(self) -> object | None:
        """Inner backend's dataset name (delegated; ``None`` if absent)."""
        return getattr(self.inner, "dataset", None)

    @property
    def input_format(self) -> object | None:
        """Inner backend's input format (delegated; ``None`` if absent)."""
        return getattr(self.inner, "input_format", None)

    def has_frame(self, frame_idx: int) -> bool:
        """Check if a frame index is contained in the video (delegates to inner).

        Args:
            frame_idx: Index of frame to check.

        Returns:
            ``True`` if the inner backend contains the index, else ``False``.
        """
        return self.inner.has_frame(frame_idx)

    def read_test_frame(self) -> np.ndarray:
        """Read the inner backend's UNCROPPED test frame (frame_map-safe)."""
        return self.inner.read_test_frame()

    def detect_grayscale(self, test_img: np.ndarray | None = None) -> bool:
        """Resolve grayscale from the inner, ignoring any passed cropped image.

        Args:
            test_img: Ignored; grayscale is always resolved on the inner's full
                frame so a degenerate (e.g. 1px-wide) crop never breaks detection.

        Returns:
            Whether the video is grayscale (also cached on ``self.grayscale``).
        """
        gs = self.inner.grayscale
        if gs is None:
            gs = self.inner.detect_grayscale()
        self.grayscale = gs
        self._cached_shape = None
        return gs

    def close(self) -> None:
        """Release this wrapper's handle and the inner's, if owned.

        Always releases the wrapper's own (unused) cached reader via
        :meth:`VideoBackend.close`, then cascades to ``inner.close()`` only when
        ``owns_inner`` (a shared-decode mosaic tile leaves the shared inner open).
        """
        super().close()
        if self.owns_inner:
            self.inner.close()

    def _source_frame(self, frame_idx: int) -> tuple[np.ndarray, bool]:
        """Return ``(frame, already_cropped)`` for a single frame.

        Tries the inner's HDF5 crop pushdown first (returns the final cropped
        frame); otherwise delegates to ``inner._read_frame`` (NOT ``get_frame``, so
        the base ``get_frame`` applies the grayscale slice exactly once to our
        cropped result).

        Args:
            frame_idx: Index of frame to read.

        Returns:
            A tuple of the frame and a flag indicating whether it is already
            cropped (pushdown) or still a full source frame.
        """
        read_crop = getattr(self.inner, "read_crop", None)
        if read_crop is not None:
            out = read_crop(frame_idx, self.crop, self.fill)
            if out is not None:
                return out, True
        return self.inner._read_frame(frame_idx), False

    def _apply_view(self, frame: np.ndarray, frame_idx: int) -> np.ndarray:
        """Crop/pad a full source frame into the cropped view.

        Args:
            frame: A full source frame ``(H, W, C)``.
            frame_idx: Reserved for a future per-frame-varying window; unused.

        Returns:
            The cropped frame, materialized as a contiguous owned array.
        """
        return np.ascontiguousarray(crop_frame(frame, self.crop, fill=self.fill))

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single cropped frame.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            The cropped frame ``(crop_h, crop_w, C)``.
        """
        src, already_cropped = self._source_frame(frame_idx)
        return src if already_cropped else self._apply_view(src, frame_idx)

    def _read_frames(self, frame_inds: list) -> np.ndarray:
        """Read a list of cropped frames.

        Tries the inner's batched HDF5 crop pushdown first; otherwise reads full
        frames from the inner and crops them. The fully-in-bounds case uses a basic
        slice then ``.copy()`` to defuse aliasing into the inner's reused decode
        buffer; otherwise it pads per-frame via ``crop_frame``.

        Args:
            frame_inds: List of frame indices to read.

        Returns:
            Cropped frames ``(N, crop_h, crop_w, C)``.
        """
        read_crops = getattr(self.inner, "read_crops", None)
        if read_crops is not None:
            out = read_crops(frame_inds, self.crop, self.fill)
            if out is not None:
                return out
        imgs = self.inner._read_frames(frame_inds)
        x1, y1, x2, y2 = self.crop
        h, w = imgs.shape[1], imgs.shape[2]
        if 0 <= x1 and 0 <= y1 and x2 <= w and y2 <= h:
            return imgs[:, y1:y2, x1:x2].copy()
        return np.stack(
            [crop_frame(im, self.crop, fill=self.fill) for im in imgs], axis=0
        )

    def to_crop_coords(self, points: np.ndarray) -> np.ndarray:
        """Map source-frame ``(x, y)`` coordinates into the cropped frame.

        Args:
            points: Coordinate array of shape ``(..., 2)``. NaN values are
                preserved.

        Returns:
            Coordinates translated by ``-(x1, y1)`` (copy-based, NaN-preserving).
        """
        return crop_points(points, self.crop)

    def to_source_coords(self, points: np.ndarray) -> np.ndarray:
        """Map cropped-frame ``(x, y)`` coordinates back to source coordinates.

        Inverse of :meth:`to_crop_coords`.

        Args:
            points: Coordinate array of shape ``(..., 2)``. NaN values are
                preserved.

        Returns:
            Coordinates translated by ``+(x1, y1)`` (copy-based, NaN-preserving).
        """
        return uncrop_points(points, self.crop)

__annotations__ = {'filename': 'str | Path | list[str] | list[Path]', 'inner': 'VideoBackend', 'crop': 'tuple[int, int, int, int]', 'fill': 'int | tuple[int, ...]', 'owns_inner': 'bool'} 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__ = 'Virtual, axis-aligned, on-read crop of an inner :class:`VideoBackend`.\n\nWraps an inner backend and reports a cropped ``(F, h, w, c)`` view. Frames are\ndecoded by the inner backend, then cropped/padded by\n:func:`sleap_io.transform.frame.crop_frame` (byte-identical), so no pixels are\ncopied or re-encoded on disk. Frame count is unchanged (a crop is spatial, not\ntemporal). Reports the cropped shape/grayscale and is a drop-in substitute\nanywhere a ``VideoBackend`` is accepted.\n\nEquality is by object identity (``eq=False``): inherited cache fields\n(``_cached_shape``, ``_open_reader``, ...) would otherwise pollute value\nequality, and dedup never relies on backend ``==`` (it uses an explicit crop\nkey). Always construct via :meth:`wrap` (never the raw constructor) so the\n"inner is never a crop" invariant and fill-aware flatten hold by construction.\n\nAttributes:\n inner: The wrapped source backend. Decodes full frames; this wrapper crops\n its output. Invariant: ``inner`` is never itself a ``CropVideoBackend``\n (enforced by :meth:`wrap`).\n crop: Crop region ``(x1, y1, x2, y2)``, ``x2``/``y2`` exclusive. May be\n negative or exceed source bounds; out-of-bounds regions are pad-filled.\n Stored as a tuple of ints.\n fill: Fill value for out-of-bounds regions, forwarded to ``crop_frame``.\n owns_inner: Whether this wrapper owns the inner backend\'s decode handle. If\n ``True`` (the default), :meth:`close` cascades to ``inner.close()``; if\n ``False`` (a shared-decode mosaic tile), it does not, so closing one\n tile does not tear down siblings sharing the inner.\n filename: Derived from ``inner.filename`` in post-init; NOT a constructor\n argument.\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__ = 2282 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__ = ('grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps') 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.io.video_reading' 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__ = ('inner', 'crop', 'fill', 'owns_inner') 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__ = ('_cached_shape', 'grayscale') 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.

dataset property

Inner backend's dataset name (delegated; None if absent).

img_shape property

Shape of a single cropped frame as (height, width, channels).

Mirrors the inner's channel policy without forcing a decode here: when the wrapper's grayscale is unresolved (None), inherit the inner's channel count (inner.img_shape resolves it from HDF5 attrs or a single detect), so the cropped channels equal the source and stay stable across an SLP round-trip. An explicit grayscale=True still collapses to 1 channel.

input_format property

Inner backend's input format (delegated; None if absent).

num_frames property

Number of frames in the video (identity: a crop is spatial).

__attrs_post_init__()

Derive filename from the inner; inherit resolved grayscale/fps lazily.

Source code in sleap_io/io/video_reading.py
def __attrs_post_init__(self) -> None:
    """Derive ``filename`` from the inner; inherit resolved grayscale/fps lazily."""
    object.__setattr__(self, "filename", getattr(self.inner, "filename", None))
    # Inherit only an ALREADY-resolved inner grayscale; never force a decode at
    # construction (keeps crop construction lazy). When unresolved, grayscale is
    # resolved on first real access via the overridden ``detect_grayscale`` /
    # ``img_shape``, always on the inner's FULL frame (so a degenerate crop never
    # skews detection).
    if self.grayscale is None and self.inner.grayscale is not None:
        object.__setattr__(self, "grayscale", self.inner.grayscale)
    # Carry fps so a closed/non-MediaVideo inner still reports fps.
    if self._fps is None:
        object.__setattr__(self, "_fps", getattr(self.inner, "_fps", None))

__init__(grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None, *, inner, crop, fill=0, owns_inner=True)

Method generated by attrs for class CropVideoBackend.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401
except ImportError:
    pass

__repr__()

Method generated by attrs for class CropVideoBackend.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

__setattr__(name, val)

Method generated by attrs for class CropVideoBackend.

Source code in sleap_io/io/video_reading.py
    multiple threads are safe: although all reads share one cached fsspec
    file-like (a single byte position), h5py serializes every HDF5 C-library
    call under a global recursive lock (`h5py._objects.phil`), so the
    seek+read pair a frame read performs is never interleaved across threads.
    For true read *parallelism* (rather than just safety), construct
    independent `Video`/`HDF5Video` instances per worker; each gets its own
    fsspec file and block cache.
"""

close()

Release this wrapper's handle and the inner's, if owned.

Always releases the wrapper's own (unused) cached reader via :meth:VideoBackend.close, then cascades to inner.close() only when owns_inner (a shared-decode mosaic tile leaves the shared inner open).

Source code in sleap_io/io/video_reading.py
def close(self) -> None:
    """Release this wrapper's handle and the inner's, if owned.

    Always releases the wrapper's own (unused) cached reader via
    :meth:`VideoBackend.close`, then cascades to ``inner.close()`` only when
    ``owns_inner`` (a shared-decode mosaic tile leaves the shared inner open).
    """
    super().close()
    if self.owns_inner:
        self.inner.close()

detect_grayscale(test_img=None)

Resolve grayscale from the inner, ignoring any passed cropped image.

Parameters:

Name Type Description Default
test_img ndarray | None

Ignored; grayscale is always resolved on the inner's full frame so a degenerate (e.g. 1px-wide) crop never breaks detection.

None

Returns:

Type Description
bool

Whether the video is grayscale (also cached on self.grayscale).

Source code in sleap_io/io/video_reading.py
def detect_grayscale(self, test_img: np.ndarray | None = None) -> bool:
    """Resolve grayscale from the inner, ignoring any passed cropped image.

    Args:
        test_img: Ignored; grayscale is always resolved on the inner's full
            frame so a degenerate (e.g. 1px-wide) crop never breaks detection.

    Returns:
        Whether the video is grayscale (also cached on ``self.grayscale``).
    """
    gs = self.inner.grayscale
    if gs is None:
        gs = self.inner.detect_grayscale()
    self.grayscale = gs
    self._cached_shape = None
    return gs

has_frame(frame_idx)

Check if a frame index is contained in the video (delegates to inner).

Parameters:

Name Type Description Default
frame_idx int

Index of frame to check.

required

Returns:

Type Description
bool

True if the inner backend contains the index, else False.

Source code in sleap_io/io/video_reading.py
def has_frame(self, frame_idx: int) -> bool:
    """Check if a frame index is contained in the video (delegates to inner).

    Args:
        frame_idx: Index of frame to check.

    Returns:
        ``True`` if the inner backend contains the index, else ``False``.
    """
    return self.inner.has_frame(frame_idx)

read_test_frame()

Read the inner backend's UNCROPPED test frame (frame_map-safe).

Source code in sleap_io/io/video_reading.py
def read_test_frame(self) -> np.ndarray:
    """Read the inner backend's UNCROPPED test frame (frame_map-safe)."""
    return self.inner.read_test_frame()

to_crop_coords(points)

Map source-frame (x, y) coordinates into the cropped frame.

Parameters:

Name Type Description Default
points ndarray

Coordinate array of shape (..., 2). NaN values are preserved.

required

Returns:

Type Description
ndarray

Coordinates translated by -(x1, y1) (copy-based, NaN-preserving).

Source code in sleap_io/io/video_reading.py
def to_crop_coords(self, points: np.ndarray) -> np.ndarray:
    """Map source-frame ``(x, y)`` coordinates into the cropped frame.

    Args:
        points: Coordinate array of shape ``(..., 2)``. NaN values are
            preserved.

    Returns:
        Coordinates translated by ``-(x1, y1)`` (copy-based, NaN-preserving).
    """
    return crop_points(points, self.crop)

to_source_coords(points)

Map cropped-frame (x, y) coordinates back to source coordinates.

Inverse of :meth:to_crop_coords.

Parameters:

Name Type Description Default
points ndarray

Coordinate array of shape (..., 2). NaN values are preserved.

required

Returns:

Type Description
ndarray

Coordinates translated by +(x1, y1) (copy-based, NaN-preserving).

Source code in sleap_io/io/video_reading.py
def to_source_coords(self, points: np.ndarray) -> np.ndarray:
    """Map cropped-frame ``(x, y)`` coordinates back to source coordinates.

    Inverse of :meth:`to_crop_coords`.

    Args:
        points: Coordinate array of shape ``(..., 2)``. NaN values are
            preserved.

    Returns:
        Coordinates translated by ``+(x1, y1)`` (copy-based, NaN-preserving).
    """
    return uncrop_points(points, self.crop)

wrap(inner, crop, fill=0, owns_inner=True) classmethod

Wrap inner in a crop view, flattening crop-of-crop when safe.

Flattens (composes into a single wrapper) only when inner is itself a CropVideoBackend, the fills agree, AND the outer crop lies fully within the inner cropped frame [0, iw] x [0, ih] (iw = ix2 - ix1, ih = iy2 - iy1). Otherwise it nests, preserving byte-parity:

  • Different fills: the inner crop's materialized pad of value inner.fill would be silently replaced after a flatten.
  • Outer crop exceeds the inner frame: a flatten would read real source pixels where the nested view pads with fill.

The flatten composition law expresses the outer rect in source coordinates: (ix1 + ox1, iy1 + oy1, ix1 + ox2, iy1 + oy2). A flattened inner is always unwrapped to inner.inner so the "inner is never a crop" invariant holds.

Parameters:

Name Type Description Default
inner VideoBackend

The backend to wrap (may itself be a CropVideoBackend).

required
crop tuple[int, int, int, int]

Outer crop region (x1, y1, x2, y2), expressed in the inner (possibly already-cropped) frame.

required
fill int | tuple[int, ...]

Fill value for out-of-bounds regions.

0
owns_inner bool

Whether the returned wrapper owns the inner's decode handle (see the class owns_inner attribute).

True

Returns:

Type Description
CropVideoBackend

A CropVideoBackend whose inner is never a crop.

Source code in sleap_io/io/video_reading.py
@classmethod
def wrap(
    cls,
    inner: VideoBackend,
    crop: tuple[int, int, int, int],
    fill: int | tuple[int, ...] = 0,
    owns_inner: bool = True,
) -> "CropVideoBackend":
    """Wrap ``inner`` in a crop view, flattening crop-of-crop when safe.

    Flattens (composes into a single wrapper) only when ``inner`` is itself a
    ``CropVideoBackend``, the fills agree, AND the outer crop lies fully within
    the inner cropped frame ``[0, iw] x [0, ih]`` (``iw = ix2 - ix1``,
    ``ih = iy2 - iy1``). Otherwise it nests, preserving byte-parity:

    - Different fills: the inner crop's materialized pad of value ``inner.fill``
      would be silently replaced after a flatten.
    - Outer crop exceeds the inner frame: a flatten would read real source
      pixels where the nested view pads with ``fill``.

    The flatten composition law expresses the outer rect in source coordinates:
    ``(ix1 + ox1, iy1 + oy1, ix1 + ox2, iy1 + oy2)``. A flattened ``inner`` is
    always unwrapped to ``inner.inner`` so the "inner is never a crop"
    invariant holds.

    Args:
        inner: The backend to wrap (may itself be a ``CropVideoBackend``).
        crop: Outer crop region ``(x1, y1, x2, y2)``, expressed in the inner
            (possibly already-cropped) frame.
        fill: Fill value for out-of-bounds regions.
        owns_inner: Whether the returned wrapper owns the inner's decode handle
            (see the class ``owns_inner`` attribute).

    Returns:
        A ``CropVideoBackend`` whose ``inner`` is never a crop.
    """

    # Normalize fills (a list and the equivalent tuple must compare equal so a
    # crop reloaded from /video_crops still flattens against its tuple fill).
    def _norm(f):
        return tuple(int(v) for v in f) if isinstance(f, (list, tuple)) else f

    if isinstance(inner, CropVideoBackend) and _norm(inner.fill) == _norm(fill):
        ix1, iy1, ix2, iy2 = inner.crop
        ox1, oy1, ox2, oy2 = (int(v) for v in crop)
        iw, ih = ix2 - ix1, iy2 - iy1
        if 0 <= ox1 and 0 <= oy1 and ox2 <= iw and oy2 <= ih:
            crop = (ix1 + ox1, iy1 + oy1, ix1 + ox2, iy1 + oy2)
            inner = inner.inner  # invariant: inner.inner is never a crop
    return cls(inner=inner, crop=crop, fill=fill, owns_inner=owns_inner)

HDF5Video

Bases: sleap_io.io.video_reading.VideoBackend

Video backend for reading videos stored in HDF5 files.

This backend supports reading videos stored in HDF5 files, both in rank-4 datasets as well as in datasets with lists of binary-encoded images.

Embedded image datasets are used in SLEAP when exporting package files (.pkg.slp) with videos embedded in them. This is useful for bundling training or inference data without having to worry about the videos (or frame images) being moved or deleted. It is expected that these types of datasets will be in a Group with a int8 variable length dataset called "video". This dataset must also contain an attribute called "format" with a string describing the image format (e.g., "png" or "jpg") which will be used to decode it appropriately.

If a frame_numbers dataset is present in the group, it will be used to map from source video frames to the frames in the dataset. This is useful to preserve frame indexing when exporting a subset of frames in the video. It will also be used to populate frame_map and source_inds attributes.

Attributes:

Name Type Description
filename

Path to HDF5 file (.h5, .hdf5 or .slp).

grayscale

Whether to force grayscale. If None, autodetect on first frame load.

keep_open

Whether to keep the video reader open between calls to read frames. If False, will close the reader after each call. If True (the default), it will keep the reader open and cache it for subsequent calls which may enhance the performance of reading multiple frames.

dataset

Name of dataset to read from. If None, will try to find a rank-4 dataset by iterating through datasets in the file. If specifying an embedded dataset, this can be the group containing a "video" dataset or the dataset itself (e.g., "video0" or "video0/video").

input_format

Format of the data in the dataset. One of "channels_last" (the default) in (frames, height, width, channels) order or "channels_first" in (frames, channels, width, height) order. Embedded datasets should use the "channels_last" format.

frame_map

Mapping from frame indices to indices in the dataset. This is used to translate between the frame indices of the images within their source video and the indices of the images in the dataset. This is only used when reading embedded image datasets.

source_filename

Path to the source video file. This is metadata and only used when reading embedded image datasets.

source_inds

Indices of the frames in the source video file. This is metadata and only used when reading embedded image datasets.

image_format

Format of the images in the embedded dataset. This is metadata and only used when reading embedded image datasets.

channel_order

Channel order of embedded images, either "RGB" or "BGR". This is used to ensure consistent color channel ordering when decoding embedded images. If the encoding and decoding plugins have different channel orders, the channels will be automatically flipped during decoding.

plugin

Plugin to use for decoding embedded images. One of "opencv" or "FFMPEG". If None, uses the global default or auto-detects based on available packages. Note that "pyav" is automatically mapped to "FFMPEG" since PyAV doesn't support image decoding.

Notes

Concurrent reads of a single remote (URL-backed) HDF5Video from multiple threads are safe: although all reads share one cached fsspec file-like (a single byte position), h5py serializes every HDF5 C-library call under a global recursive lock (h5py._objects.phil), so the seek+read pair a frame read performs is never interleaved across threads. For true read parallelism (rather than just safety), construct independent Video/HDF5Video instances per worker; each gets its own fsspec file and block cache.

Methods:

Name Description
__attrs_post_init__

Auto-detect dataset and frame map heuristically.

__eq__

Method generated by attrs for class HDF5Video.

__getstate__

Return state for pickling/deepcopy, dropping unpicklable handles.

__init__

Method generated by attrs for class HDF5Video.

__repr__

Method generated by attrs for class HDF5Video.

__setattr__

Method generated by attrs for class HDF5Video.

close

Release the cached HDF5 reader and the cached fsspec URL file-like.

decode_embedded

Decode an embedded image string into a numpy array.

get_frame_raw_bytes

Get raw encoded bytes for a frame without decoding.

has_frame

Check if a frame index is contained in the video.

read_crop

Read a spatial hyperslab of a frame, padded to the crop shape.

read_crops

Batched :meth:read_crop.

read_test_frame

Read a single frame from the video to test for grayscale.

Source code in sleap_io/io/video_reading.py
@attrs.define
class HDF5Video(VideoBackend):
    """Video backend for reading videos stored in HDF5 files.

    This backend supports reading videos stored in HDF5 files, both in rank-4 datasets
    as well as in datasets with lists of binary-encoded images.

    Embedded image datasets are used in SLEAP when exporting package files (`.pkg.slp`)
    with videos embedded in them. This is useful for bundling training or inference data
    without having to worry about the videos (or frame images) being moved or deleted.
    It is expected that these types of datasets will be in a `Group` with a `int8`
    variable length dataset called `"video"`. This dataset must also contain an
    attribute called "format" with a string describing the image format (e.g., "png" or
    "jpg") which will be used to decode it appropriately.

    If a `frame_numbers` dataset is present in the group, it will be used to map from
    source video frames to the frames in the dataset. This is useful to preserve frame
    indexing when exporting a subset of frames in the video. It will also be used to
    populate `frame_map` and `source_inds` attributes.

    Attributes:
        filename: Path to HDF5 file (.h5, .hdf5 or .slp).
        grayscale: Whether to force grayscale. If None, autodetect on first frame load.
        keep_open: Whether to keep the video reader open between calls to read frames.
            If False, will close the reader after each call. If True (the default), it
            will keep the reader open and cache it for subsequent calls which may
            enhance the performance of reading multiple frames.
        dataset: Name of dataset to read from. If `None`, will try to find a rank-4
            dataset by iterating through datasets in the file. If specifying an embedded
            dataset, this can be the group containing a "video" dataset or the dataset
            itself (e.g., "video0" or "video0/video").
        input_format: Format of the data in the dataset. One of "channels_last" (the
            default) in `(frames, height, width, channels)` order or "channels_first" in
            `(frames, channels, width, height)` order. Embedded datasets should use the
            "channels_last" format.
        frame_map: Mapping from frame indices to indices in the dataset. This is used to
            translate between the frame indices of the images within their source video
            and the indices of the images in the dataset. This is only used when reading
            embedded image datasets.
        source_filename: Path to the source video file. This is metadata and only used
            when reading embedded image datasets.
        source_inds: Indices of the frames in the source video file. This is metadata
            and only used when reading embedded image datasets.
        image_format: Format of the images in the embedded dataset. This is metadata and
            only used when reading embedded image datasets.
        channel_order: Channel order of embedded images, either "RGB" or "BGR". This is
            used to ensure consistent color channel ordering when decoding embedded
            images. If the encoding and decoding plugins have different channel orders,
            the channels will be automatically flipped during decoding.
        plugin: Plugin to use for decoding embedded images. One of "opencv" or
            "FFMPEG". If None, uses the global default or auto-detects based on
            available packages. Note that "pyav" is automatically mapped to "FFMPEG"
            since PyAV doesn't support image decoding.

    Notes:
        Concurrent reads of a single remote (URL-backed) `HDF5Video` from
        multiple threads are safe: although all reads share one cached fsspec
        file-like (a single byte position), h5py serializes every HDF5 C-library
        call under a global recursive lock (`h5py._objects.phil`), so the
        seek+read pair a frame read performs is never interleaved across threads.
        For true read *parallelism* (rather than just safety), construct
        independent `Video`/`HDF5Video` instances per worker; each gets its own
        fsspec file and block cache.
    """

    dataset: str | None = None
    input_format: str = attrs.field(
        default="channels_last",
        validator=attrs.validators.in_(["channels_last", "channels_first"]),
    )
    frame_map: dict[int, int] = attrs.field(init=False, default=attrs.Factory(dict))
    _can_push_crop_cached: bool | None = attrs.field(
        init=False, default=None, repr=False, eq=False
    )
    source_filename: str | None = None
    source_inds: np.ndarray | None = None
    image_format: str = "hdf5"
    channel_order: str = "RGB"
    plugin: str | None = None
    _url_file: object | None = attrs.field(
        init=False, default=None, repr=False, eq=False
    )
    # ``_url_headers`` / ``_url_stream_mode`` are ``init=True`` (attrs derives the
    # constructor aliases ``url_headers`` / ``url_stream_mode`` by stripping the
    # leading underscore) so the metadata probe in ``__attrs_post_init__`` runs
    # *authenticated*: an embedded ``pkg.slp`` over an auth-gated URL would
    # otherwise probe with no headers and silently lose the embedded-image
    # metadata. They remain ``repr=False, eq=False`` and, because the attribute
    # names keep the leading underscore, the name-based ``__getstate__`` pickle
    # contract is unchanged.
    _url_headers: dict[str, str] | None = attrs.field(
        default=None, repr=False, eq=False
    )
    _url_stream_mode: str = attrs.field(default="blockcache", repr=False, eq=False)

    EXTS = ("h5", "hdf5", "slp")

    def _open_h5(self) -> h5py.File:
        """Open the backing HDF5 file as an ``h5py.File`` in read mode.

        For local paths this opens ``self.filename`` directly. For URLs it lazily
        opens (and caches on ``self._url_file``) an fsspec-backed file-like object
        via :func:`sleap_io.io._remote.open_url` and wraps it with ``h5py``.

        Returns:
            An open ``h5py.File`` handle. The caller owns closing the returned
            handle; the cached ``self._url_file`` is reused across reads and
            dropped on pickling.
        """
        from sleap_io.io import _remote

        if _remote._is_url(self.filename):
            if self._url_file is None:
                self._url_file = _remote.open_url(
                    self.filename,
                    headers=self._url_headers,
                    stream_mode=self._url_stream_mode,
                )
            return h5py.File(self._url_file, "r")
        return h5py.File(self.filename, "r")

    def _close_url_file(self) -> None:
        """Close and drop the cached fsspec URL file-like, if any (idempotent).

        A no-op for local files (where ``_url_file`` is never set) and when it
        has already been closed/dropped.
        """
        if self._url_file is None:
            return
        try:
            self._url_file.close()
        except Exception:  # pragma: no cover - defensive: close() should not raise
            pass
        self._url_file = None

    def _release_probe_url_file(self, preexisting: bool) -> None:
        """Close and drop ``self._url_file`` if a probe opened it.

        Used by :meth:`__attrs_post_init__` so that a remote handle opened just
        to sniff the dataset/frame-map does not leak and is not reused by later
        (possibly authenticated) reads. A no-op for local files and when the
        cached file-like already existed before the probe.

        Args:
            preexisting: Whether ``self._url_file`` was already set before the
                probe opened the file (in which case it is left untouched).
        """
        if preexisting:
            return
        self._close_url_file()

    def close(self) -> None:
        """Release the cached HDF5 reader and the cached fsspec URL file-like.

        Extends :meth:`VideoBackend.close` (which drops the cached ``h5py.File``
        reader) by also closing the fsspec-backed ``_url_file`` shared across
        reads, which ``h5py.File.close()`` does not close on its own. Both are
        lazily reopened on the next read, so this is safe to call between reads.
        """
        super().close()
        self._close_url_file()

    def __getstate__(self) -> dict:
        """Return state for pickling/deepcopy, dropping unpicklable handles.

        Extends :meth:`VideoBackend.__getstate__` to also drop the cached
        fsspec-backed ``_url_file`` (reopened lazily by :meth:`_open_h5`).
        """
        state = super().__getstate__()
        state["_url_file"] = None
        return state

    def __attrs_post_init__(self):
        """Auto-detect dataset and frame map heuristically."""
        # Check if the file accessible before applying heuristics.
        # For URLs, track whether this probe opened the cached fsspec file-like
        # so it can be released afterwards (it would otherwise leak the handle on
        # an early return / exception, and a probe-time open may predate the
        # final auth headers being applied).
        url_file_preexisting = self._url_file is not None
        try:
            f = self._open_h5()
        except OSError:
            self._release_probe_url_file(url_file_preexisting)
            return

        try:
            if self.dataset is None:
                # Iterate through datasets to find a rank 4 array.
                def find_movies(name, obj):
                    if isinstance(obj, h5py.Dataset) and obj.ndim == 4:
                        self.dataset = name
                        return True

                f.visititems(find_movies)

            if self.dataset is None:
                # Iterate through datasets to find an embedded video dataset.
                def find_embedded(name, obj):
                    if isinstance(obj, h5py.Dataset) and name.endswith("/video"):
                        self.dataset = name
                        return True

                f.visititems(find_embedded)

            if self.dataset is None:
                # Couldn't find video datasets.
                return

            if isinstance(f[self.dataset], h5py.Group):
                # If this is a group, assume it's an embedded video dataset.
                if "video" in f[self.dataset]:
                    self.dataset = f"{self.dataset}/video"

            if self.dataset.split("/")[-1] == "video":
                # This may be an embedded video dataset. Check for frame map.
                ds = f[self.dataset]

                if "format" in ds.attrs:
                    self.image_format = ds.attrs["format"]

                # Read channel_order, with backwards compatibility
                if "channel_order" in ds.attrs:
                    self.channel_order = ds.attrs["channel_order"]
                else:
                    # Backwards compatibility: Check format_id for older files
                    # Prior to format 1.4, embedded images were primarily encoded
                    # with OpenCV which uses BGR, so default to BGR for older
                    # formats
                    if "metadata" in f and "format_id" in f["metadata"].attrs:
                        format_id = f["metadata"].attrs["format_id"]
                        if format_id < 1.4:
                            self.channel_order = "BGR"  # Legacy default
                    # If no format_id found, assume BGR (safest legacy default)
                    # since most embedded images before this change used OpenCV

                if "frame_numbers" in ds.parent:
                    frame_numbers = ds.parent["frame_numbers"][:].astype(int)
                    self.frame_map = {
                        frame: idx for idx, frame in enumerate(frame_numbers)
                    }
                    self.source_inds = frame_numbers

                if "source_video" in ds.parent:
                    source_grp = ds.parent["source_video"]
                    # Source metadata is normally in the "json" attribute, but
                    # oversized metadata (e.g. an image-sequence source with many
                    # thousands of filenames, exceeding HDF5's 64 KB attribute limit)
                    # is stored in a "json" *dataset* instead (see
                    # ``slp._write_source_video_json``). Read whichever is present so
                    # such packages remain openable -- otherwise the backend fails to
                    # open, ``Video.backend`` is left ``None``, and embedded frames
                    # cannot be read.
                    if "json" in source_grp:
                        source_json = source_grp["json"][()]
                    else:
                        source_json = source_grp.attrs["json"]
                    self.source_filename = json.loads(source_json)["backend"][
                        "filename"
                    ]

                # Read FPS from attributes if present
                if "fps" in ds.attrs:
                    self._fps = float(ds.attrs["fps"])
                elif "fps" in ds.parent.attrs:
                    self._fps = float(ds.parent.attrs["fps"])
        finally:
            f.close()
            self._release_probe_url_file(url_file_preexisting)

        # Set default plugin if not specified (use image plugin, not video plugin)
        if self.plugin is None:
            # Check image plugin default first (for embedded images)
            if _default_image_plugin is not None:
                self.plugin = _default_image_plugin
            # Otherwise auto-detect (for embedded image decoding)
            elif "cv2" in sys.modules:
                self.plugin = "opencv"
            else:
                self.plugin = "imageio"  # imageio fallback

    @property
    def num_frames(self) -> int:
        """Number of frames in the video."""
        with self._open_h5() as f:
            return f[self.dataset].shape[0]

    @property
    def img_shape(self) -> tuple[int, int, int]:
        """Shape of a single frame in the video as `(height, width, channels)`."""
        with self._open_h5() as f:
            ds = f[self.dataset]

            img_shape = None
            if "height" in ds.attrs:
                # Try to get shape from the attributes.
                img_shape = (
                    ds.attrs["height"],
                    ds.attrs["width"],
                    ds.attrs["channels"],
                )

                if img_shape[0] == 0 or img_shape[1] == 0:
                    # Invalidate the shape if the attributes are zero.
                    img_shape = None

            if img_shape is None and self.image_format == "hdf5" and ds.ndim == 4:
                # Use the dataset shape if just stored as a rank-4 array.
                img_shape = ds.shape[1:]

                if self.input_format == "channels_first":
                    img_shape = img_shape[::-1]

        if img_shape is None:
            # Fall back to reading a test frame.
            return super().img_shape

        return int(img_shape[0]), int(img_shape[1]), int(img_shape[2])

    def read_test_frame(self) -> np.ndarray:
        """Read a single frame from the video to test for grayscale."""
        if self.frame_map:
            frame_idx = list(self.frame_map.keys())[0]
        else:
            frame_idx = 0
        return self._read_frame(frame_idx)

    @property
    def has_embedded_images(self) -> bool:
        """Return True if the dataset contains embedded images."""
        return self.image_format is not None and self.image_format != "hdf5"

    @property
    def embedded_frame_inds(self) -> list[int]:
        """Return the frame indices of the embedded images."""
        return list(self.frame_map.keys())

    def decode_embedded(self, img_string: np.ndarray) -> np.ndarray:
        """Decode an embedded image string into a numpy array.

        Args:
            img_string: Binary string of the image as a `int8` numpy vector with the
                bytes as values corresponding to the format-encoded image.

        Returns:
            The decoded image as a numpy array of shape `(height, width, channels)`. If
            a rank-2 image is decoded, it will be expanded such that channels will be 1.

            This method does not apply grayscale conversion as per the `grayscale`
            attribute. Use the `get_frame` or `get_frames` methods of the `VideoBackend`
            to apply grayscale conversion rather than calling this function directly.
        """
        # Decode based on plugin
        if self.plugin == "opencv":
            img = cv2.imdecode(img_string, cv2.IMREAD_UNCHANGED)
            decoder_order = "BGR"  # OpenCV decodes to BGR
        else:
            # Use imageio for FFMPEG or any other plugin
            img = iio.imread(BytesIO(img_string), extension=f".{self.image_format}")
            decoder_order = "RGB"  # imageio decodes to RGB

        if img.ndim == 2:
            img = np.expand_dims(img, axis=-1)

        # Convert channel order if needed
        # If the stored order doesn't match the decoder order, flip channels
        if img.shape[-1] == 3 and self.channel_order != decoder_order:
            img = img[..., ::-1]  # Flip RGB <-> BGR

        return img

    def has_frame(self, frame_idx: int) -> bool:
        """Check if a frame index is contained in the video.

        Args:
            frame_idx: Index of frame to check.

        Returns:
            `True` if the index is contained in the video, otherwise `False`.
        """
        if self.frame_map:
            return frame_idx in self.frame_map
        else:
            return frame_idx < len(self)

    def get_frame_raw_bytes(self, frame_idx: int) -> np.ndarray | None:
        """Get raw encoded bytes for a frame without decoding.

        This method reads the raw compressed image data (PNG/JPEG bytes) directly
        from the HDF5 dataset without decoding it. This is useful for fast copying
        of embedded images when the target format matches the source format.

        Args:
            frame_idx: Index of the frame to read.

        Returns:
            Raw encoded bytes as int8 numpy array, or None if:
            - The backend doesn't have embedded images (including "hdf5" format which
              stores raw numpy arrays, not encoded images)
            - The frame index is not available

        Notes:
            For variable-length datasets, returns the raw bytes directly.
            For fixed-length datasets, returns bytes with trailing zeros stripped.
        """
        if not self.has_embedded_images:
            return None

        if not self.has_frame(frame_idx):
            return None

        # Get the internal index (handle frame_map)
        internal_idx = (
            self.frame_map.get(frame_idx, frame_idx) if self.frame_map else frame_idx
        )

        # Read directly from dataset
        if self.keep_open:
            if self._open_reader is None:
                self._open_reader = self._open_h5()
            f = self._open_reader
        else:
            f = self._open_h5()

        ds = f[self.dataset]
        raw_bytes = ds[internal_idx]

        # Handle fixed-length padding (strip trailing zeros)
        is_vlen = h5py.check_vlen_dtype(ds.dtype) is not None
        if not is_vlen:
            # Find last non-zero byte
            non_zero_mask = raw_bytes != 0
            if non_zero_mask.any():
                last_non_zero = np.where(non_zero_mask)[0][-1]
                raw_bytes = raw_bytes[: last_non_zero + 1]

        if not self.keep_open:
            f.close()

        return raw_bytes

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the video.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            The frame as a numpy array of shape `(height, width, channels)`.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frame` method of the `VideoBackend` class instead.
        """
        if self.keep_open:
            if self._open_reader is None:
                self._open_reader = self._open_h5()
            f = self._open_reader
        else:
            f = self._open_h5()

        ds = f[self.dataset]

        if self.frame_map:
            frame_idx = self.frame_map[frame_idx]

        img = ds[frame_idx]

        if self.has_embedded_images:
            img = self.decode_embedded(img)

        if self.input_format == "channels_first":
            img = np.transpose(img, (2, 1, 0))

        if not self.keep_open:
            f.close()
        return img

    def _read_frames(self, frame_inds: list) -> np.ndarray:
        """Read a list of frames from the video.

        Args:
            frame_inds: List of indices of frames to read.

        Returns:
            The frame as a numpy array of shape `(frames, height, width, channels)`.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frames` method of the `VideoBackend` class instead.
        """
        if self.keep_open:
            if self._open_reader is None:
                self._open_reader = self._open_h5()
            f = self._open_reader
        else:
            f = self._open_h5()

        if self.frame_map:
            frame_inds = [self.frame_map[idx] for idx in frame_inds]

        ds = f[self.dataset]
        imgs = ds[frame_inds]

        if "format" in ds.attrs:
            imgs = np.stack(
                [self.decode_embedded(img) for img in imgs],
                axis=0,
            )

        if self.input_format == "channels_first":
            imgs = np.transpose(imgs, (0, 3, 2, 1))

        if not self.keep_open:
            f.close()

        return imgs

    @property
    def _can_push_crop(self) -> bool:
        """Whether this dataset supports HDF5 crop pushdown (dataset-level gate).

        Pushdown reads only a spatial hyperslab of a frame instead of decoding the
        whole frame, but it is only valid (and beneficial) for raw rank-4 chunked
        datasets with sub-frame spatial chunking and no embedded/frame-mapped
        subset. This is the dataset-level gate only (the per-call "crop smaller than
        the chunk span" predicate is evaluated in :meth:`read_crop`/:meth:`read_crops`).
        The probe reflects the immutable on-disk layout, so the result is cached
        after the first call (no file open per read).

        Returns:
            ``True`` if the dataset is a raw (``image_format == "hdf5"``) rank-4
            chunked array with sub-frame spatial chunking and an empty
            ``frame_map``; ``False`` otherwise (including any error while probing,
            so a non-applicable dataset never raises).
        """
        # Cheap short-circuit (no file open) for embedded/frame-mapped datasets.
        if self.image_format != "hdf5" or self.frame_map:
            return False
        if self._can_push_crop_cached is None:
            self._can_push_crop_cached = self._probe_can_push_crop()
        return self._can_push_crop_cached

    def _probe_can_push_crop(self) -> bool:
        """Probe the on-disk layout for pushdown eligibility (opens the file once)."""
        try:
            with self._open_h5() as f:
                ds = f[self.dataset]
                if ds.ndim != 4 or ds.chunks is None:
                    return False
                chunks = ds.chunks
                if self.input_format == "channels_first":
                    # On-disk layout is (F, C, W, H).
                    disk_w, disk_h = ds.shape[2], ds.shape[3]
                    return chunks[2] < disk_w or chunks[3] < disk_h
                # channels_last on-disk layout is (F, H, W, C).
                height, width = ds.shape[1], ds.shape[2]
                return chunks[1] < height or chunks[2] < width
        except (OSError, KeyError, TypeError):  # pragma: no cover - defensive
            return False

    def read_crop(
        self,
        frame_idx: int,
        crop: tuple[int, int, int, int],
        fill: int | tuple[int, ...] = 0,
    ) -> np.ndarray | None:
        """Read a spatial hyperslab of a frame, padded to the crop shape.

        This is the single-frame HDF5 crop pushdown hook consumed by
        :class:`CropVideoBackend`. When applicable, it reads only the spatial
        region of the frame that overlaps ``crop`` directly from the chunked
        dataset (avoiding a full-frame decode) and pads out-of-bounds regions
        exactly as :func:`sleap_io.transform.frame.crop_frame` would.

        Args:
            frame_idx: Index of the frame to read (source-video index; mapped
                through ``frame_map`` if present, though pushdown is gated off when
                a ``frame_map`` exists).
            crop: Crop region ``(x1, y1, x2, y2)`` with ``x2``/``y2`` exclusive.
                May be negative or exceed the frame bounds (padded with ``fill``).
            fill: Fill value for out-of-bounds regions.

        Returns:
            A ``(y2 - y1, x2 - x1, C)`` array (pre-grayscale, ``dtype == ds.dtype``)
            byte-identical to ``crop_frame(self._read_frame(frame_idx), crop,
            fill)`` when pushdown is applicable; otherwise ``None`` to signal the
            caller should fall back to a full-frame decode plus ``crop_frame``.
            Never raises for out-of-bounds crops.
        """
        if not self._can_push_crop:
            return None
        try:
            if self.keep_open:
                if self._open_reader is None:
                    self._open_reader = self._open_h5()
                f = self._open_reader
                ds = f[self.dataset]
                return self._read_crop_from_ds(ds, frame_idx, crop, fill)
            else:
                with self._open_h5() as f:
                    ds = f[self.dataset]
                    return self._read_crop_from_ds(ds, frame_idx, crop, fill)
        except (OSError, KeyError, IndexError):  # pragma: no cover - defensive
            return None

    def read_crops(
        self,
        frame_inds: list,
        crop: tuple[int, int, int, int],
        fill: int | tuple[int, ...] = 0,
    ) -> np.ndarray | None:
        """Batched :meth:`read_crop`.

        Args:
            frame_inds: List of source-video frame indices to read.
            crop: Crop region ``(x1, y1, x2, y2)`` with ``x2``/``y2`` exclusive.
            fill: Fill value for out-of-bounds regions.

        Returns:
            A ``(N, y2 - y1, x2 - x1, C)`` array byte-identical to stacking
            per-frame ``crop_frame`` results, or ``None`` to fall back to a
            full-frame decode plus ``crop_frame``.
        """
        if not self._can_push_crop:
            return None
        try:
            if self.keep_open:
                if self._open_reader is None:
                    self._open_reader = self._open_h5()
                f = self._open_reader
                ds = f[self.dataset]
                return self._stack_crops(ds, frame_inds, crop, fill)
            else:
                with self._open_h5() as f:
                    ds = f[self.dataset]
                    return self._stack_crops(ds, frame_inds, crop, fill)
        except (OSError, KeyError, IndexError):  # pragma: no cover - defensive
            return None

    def _stack_crops(
        self,
        ds: h5py.Dataset,
        frame_inds: list,
        crop: tuple[int, int, int, int],
        fill: int | tuple[int, ...],
    ) -> np.ndarray | None:
        """Stack per-frame crop reads, falling back to ``None`` if the gate declines.

        The per-call gate in :meth:`_read_crop_from_ds` is frame-index independent, so
        a batch is uniformly all-arrays or all-``None``; returning ``None`` on any
        ``None`` keeps batched reads byte-for-byte consistent with the scalar path
        (the caller then decodes the full frames and crops them).

        Args:
            ds: The open ``h5py.Dataset`` (raw rank-4).
            frame_inds: Frame indices to read.
            crop: Crop region ``(x1, y1, x2, y2)``.
            fill: Fill value for out-of-bounds regions.

        Returns:
            A ``(N, y2 - y1, x2 - x1, C)`` array, or ``None`` to signal fallback.
        """
        parts = [self._read_crop_from_ds(ds, i, crop, fill) for i in frame_inds]
        if any(p is None for p in parts):
            return None
        return np.stack(parts, axis=0)

    def _read_crop_from_ds(
        self,
        ds: h5py.Dataset,
        frame_idx: int,
        crop: tuple[int, int, int, int],
        fill: int | tuple[int, ...],
    ) -> np.ndarray | None:
        """Read and pad one frame's crop region from an open dataset.

        Performs the per-call gate (crop must be smaller than the chunk span on at
        least one spatial axis) and the clamp+pad hyperslab read. Axis ordering is
        derived from ``ds.shape`` rather than assumed. Pushdown is structurally gated
        off for frame-mapped/embedded datasets (see :attr:`_can_push_crop`), so
        ``frame_idx`` is always a raw source index here.

        Args:
            ds: The open ``h5py.Dataset`` (raw rank-4).
            frame_idx: Frame index (raw source index; no ``frame_map`` remap needed).
            crop: Crop region ``(x1, y1, x2, y2)``.
            fill: Fill value for out-of-bounds regions.

        Returns:
            The ``(y2 - y1, x2 - x1, C)`` cropped/padded frame, or ``None`` if the
            per-call gate decides a full read is at least as good.
        """
        x1, y1, x2, y2 = crop
        chunks = ds.chunks

        if self.input_format == "channels_first":
            # On-disk layout (F, C, W, H): x maps to axis 2 (W), y to axis 3 (H).
            channels = ds.shape[1]
            disk_w, disk_h = ds.shape[2], ds.shape[3]
            width, height = disk_w, disk_h
            chunk_w, chunk_h = chunks[2], chunks[3]
        else:
            # channels_last (F, H, W, C).
            height, width, channels = ds.shape[1], ds.shape[2], ds.shape[3]
            chunk_h, chunk_w = chunks[1], chunks[2]

        # Per-call gate: only push down when the crop touches fewer spatial chunks
        # than the full frame does on at least one axis. If the (in-bounds) crop
        # already touches every chunk on both spatial axes, a hyperslab read buys
        # nothing over a full read, so fall back.
        crop_w, crop_h = x2 - x1, y2 - y1
        in_sx1, in_sy1 = max(0, x1), max(0, y1)
        in_sx2, in_sy2 = min(width, x2), min(height, y2)
        if in_sx2 <= in_sx1 or in_sy2 <= in_sy1:
            # Fully outside on at least one axis: no valid source pixels to read,
            # so the hyperslab touches no chunks; pushdown is trivially beneficial.
            n_chunks_w = n_chunks_h = 0
        else:
            n_chunks_w = (in_sx2 - 1) // chunk_w - in_sx1 // chunk_w + 1
            n_chunks_h = (in_sy2 - 1) // chunk_h - in_sy1 // chunk_h + 1
        frame_chunks_w = -(-width // chunk_w)
        frame_chunks_h = -(-height // chunk_h)
        if n_chunks_w >= frame_chunks_w and n_chunks_h >= frame_chunks_h:
            return None

        # frame_map is always empty here: _can_push_crop gates pushdown off for
        # frame-mapped/embedded datasets, so frame_idx is a raw source index.
        out = np.full((crop_h, crop_w, channels), fill, dtype=ds.dtype)

        # Clamp the requested rect to the valid frame bounds.
        sx1, sy1 = max(0, x1), max(0, y1)
        sx2, sy2 = min(width, x2), min(height, y2)
        if sx2 > sx1 and sy2 > sy1:
            if self.input_format == "channels_first":
                region = np.transpose(ds[frame_idx, :, sx1:sx2, sy1:sy2], (2, 1, 0))
            else:
                region = ds[frame_idx, sy1:sy2, sx1:sx2, :]
            out[
                sy1 - y1 : sy1 - y1 + (sy2 - sy1),
                sx1 - x1 : sx1 - x1 + (sx2 - sx1),
            ] = region
        return out

EXTS = ('h5', 'hdf5', 'slp') 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.

__annotations__ = {'dataset': 'str | None', 'input_format': 'str', 'frame_map': 'dict[int, int]', '_can_push_crop_cached': 'bool | None', 'source_filename': 'str | None', 'source_inds': 'np.ndarray | None', 'image_format': 'str', 'channel_order': 'str', 'plugin': 'str | None', '_url_file': 'object | None', '_url_headers': 'dict[str, str] | None', '_url_stream_mode': 'str'} 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=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=False, 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__ = 'Video backend for reading videos stored in HDF5 files.\n\nThis backend supports reading videos stored in HDF5 files, both in rank-4 datasets\nas well as in datasets with lists of binary-encoded images.\n\nEmbedded image datasets are used in SLEAP when exporting package files (`.pkg.slp`)\nwith videos embedded in them. This is useful for bundling training or inference data\nwithout having to worry about the videos (or frame images) being moved or deleted.\nIt is expected that these types of datasets will be in a `Group` with a `int8`\nvariable length dataset called `"video"`. This dataset must also contain an\nattribute called "format" with a string describing the image format (e.g., "png" or\n"jpg") which will be used to decode it appropriately.\n\nIf a `frame_numbers` dataset is present in the group, it will be used to map from\nsource video frames to the frames in the dataset. This is useful to preserve frame\nindexing when exporting a subset of frames in the video. It will also be used to\npopulate `frame_map` and `source_inds` attributes.\n\nAttributes:\n filename: Path to HDF5 file (.h5, .hdf5 or .slp).\n grayscale: Whether to force grayscale. If None, autodetect on first frame load.\n keep_open: Whether to keep the video reader open between calls to read frames.\n If False, will close the reader after each call. If True (the default), it\n will keep the reader open and cache it for subsequent calls which may\n enhance the performance of reading multiple frames.\n dataset: Name of dataset to read from. If `None`, will try to find a rank-4\n dataset by iterating through datasets in the file. If specifying an embedded\n dataset, this can be the group containing a "video" dataset or the dataset\n itself (e.g., "video0" or "video0/video").\n input_format: Format of the data in the dataset. One of "channels_last" (the\n default) in `(frames, height, width, channels)` order or "channels_first" in\n `(frames, channels, width, height)` order. Embedded datasets should use the\n "channels_last" format.\n frame_map: Mapping from frame indices to indices in the dataset. This is used to\n translate between the frame indices of the images within their source video\n and the indices of the images in the dataset. This is only used when reading\n embedded image datasets.\n source_filename: Path to the source video file. This is metadata and only used\n when reading embedded image datasets.\n source_inds: Indices of the frames in the source video file. This is metadata\n and only used when reading embedded image datasets.\n image_format: Format of the images in the embedded dataset. This is metadata and\n only used when reading embedded image datasets.\n channel_order: Channel order of embedded images, either "RGB" or "BGR". This is\n used to ensure consistent color channel ordering when decoding embedded\n images. If the encoding and decoding plugins have different channel orders,\n the channels will be automatically flipped during decoding.\n plugin: Plugin to use for decoding embedded images. One of "opencv" or\n "FFMPEG". If None, uses the global default or auto-detects based on\n available packages. Note that "pyav" is automatically mapped to "FFMPEG"\n since PyAV doesn\'t support image decoding.\n\nNotes:\n Concurrent reads of a single remote (URL-backed) `HDF5Video` from\n multiple threads are safe: although all reads share one cached fsspec\n file-like (a single byte position), h5py serializes every HDF5 C-library\n call under a global recursive lock (`h5py._objects.phil`), so the\n seek+read pair a frame read performs is never interleaved across threads.\n For true read *parallelism* (rather than just safety), construct\n independent `Video`/`HDF5Video` instances per worker; each gets its own\n fsspec file and block cache.\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__ = 1126 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps', 'dataset', 'input_format', 'source_filename', 'source_inds', 'image_format', 'channel_order', 'plugin', '_url_headers', '_url_stream_mode') 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.io.video_reading' 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__ = ('dataset', 'input_format', 'frame_map', '_can_push_crop_cached', 'source_filename', 'source_inds', 'image_format', 'channel_order', 'plugin', '_url_file', '_url_headers', '_url_stream_mode') 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__ = ('_can_push_crop_cached', '_fps', '_open_reader', '_url_file', 'channel_order', 'dataset', 'frame_map', 'image_format', 'plugin', 'source_filename', 'source_inds') 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.

embedded_frame_inds property

Return the frame indices of the embedded images.

has_embedded_images property

Return True if the dataset contains embedded images.

img_shape property

Shape of a single frame in the video as (height, width, channels).

num_frames property

Number of frames in the video.

__attrs_post_init__()

Auto-detect dataset and frame map heuristically.

Source code in sleap_io/io/video_reading.py
def __attrs_post_init__(self):
    """Auto-detect dataset and frame map heuristically."""
    # Check if the file accessible before applying heuristics.
    # For URLs, track whether this probe opened the cached fsspec file-like
    # so it can be released afterwards (it would otherwise leak the handle on
    # an early return / exception, and a probe-time open may predate the
    # final auth headers being applied).
    url_file_preexisting = self._url_file is not None
    try:
        f = self._open_h5()
    except OSError:
        self._release_probe_url_file(url_file_preexisting)
        return

    try:
        if self.dataset is None:
            # Iterate through datasets to find a rank 4 array.
            def find_movies(name, obj):
                if isinstance(obj, h5py.Dataset) and obj.ndim == 4:
                    self.dataset = name
                    return True

            f.visititems(find_movies)

        if self.dataset is None:
            # Iterate through datasets to find an embedded video dataset.
            def find_embedded(name, obj):
                if isinstance(obj, h5py.Dataset) and name.endswith("/video"):
                    self.dataset = name
                    return True

            f.visititems(find_embedded)

        if self.dataset is None:
            # Couldn't find video datasets.
            return

        if isinstance(f[self.dataset], h5py.Group):
            # If this is a group, assume it's an embedded video dataset.
            if "video" in f[self.dataset]:
                self.dataset = f"{self.dataset}/video"

        if self.dataset.split("/")[-1] == "video":
            # This may be an embedded video dataset. Check for frame map.
            ds = f[self.dataset]

            if "format" in ds.attrs:
                self.image_format = ds.attrs["format"]

            # Read channel_order, with backwards compatibility
            if "channel_order" in ds.attrs:
                self.channel_order = ds.attrs["channel_order"]
            else:
                # Backwards compatibility: Check format_id for older files
                # Prior to format 1.4, embedded images were primarily encoded
                # with OpenCV which uses BGR, so default to BGR for older
                # formats
                if "metadata" in f and "format_id" in f["metadata"].attrs:
                    format_id = f["metadata"].attrs["format_id"]
                    if format_id < 1.4:
                        self.channel_order = "BGR"  # Legacy default
                # If no format_id found, assume BGR (safest legacy default)
                # since most embedded images before this change used OpenCV

            if "frame_numbers" in ds.parent:
                frame_numbers = ds.parent["frame_numbers"][:].astype(int)
                self.frame_map = {
                    frame: idx for idx, frame in enumerate(frame_numbers)
                }
                self.source_inds = frame_numbers

            if "source_video" in ds.parent:
                source_grp = ds.parent["source_video"]
                # Source metadata is normally in the "json" attribute, but
                # oversized metadata (e.g. an image-sequence source with many
                # thousands of filenames, exceeding HDF5's 64 KB attribute limit)
                # is stored in a "json" *dataset* instead (see
                # ``slp._write_source_video_json``). Read whichever is present so
                # such packages remain openable -- otherwise the backend fails to
                # open, ``Video.backend`` is left ``None``, and embedded frames
                # cannot be read.
                if "json" in source_grp:
                    source_json = source_grp["json"][()]
                else:
                    source_json = source_grp.attrs["json"]
                self.source_filename = json.loads(source_json)["backend"][
                    "filename"
                ]

            # Read FPS from attributes if present
            if "fps" in ds.attrs:
                self._fps = float(ds.attrs["fps"])
            elif "fps" in ds.parent.attrs:
                self._fps = float(ds.parent.attrs["fps"])
    finally:
        f.close()
        self._release_probe_url_file(url_file_preexisting)

    # Set default plugin if not specified (use image plugin, not video plugin)
    if self.plugin is None:
        # Check image plugin default first (for embedded images)
        if _default_image_plugin is not None:
            self.plugin = _default_image_plugin
        # Otherwise auto-detect (for embedded image decoding)
        elif "cv2" in sys.modules:
            self.plugin = "opencv"
        else:
            self.plugin = "imageio"  # imageio fallback

__eq__(other)

Method generated by attrs for class HDF5Video.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401
except ImportError:
    pass

try:
    import av  # noqa: F401
except ImportError:
    pass

__getstate__()

Return state for pickling/deepcopy, dropping unpicklable handles.

Extends :meth:VideoBackend.__getstate__ to also drop the cached fsspec-backed _url_file (reopened lazily by :meth:_open_h5).

Source code in sleap_io/io/video_reading.py
def __getstate__(self) -> dict:
    """Return state for pickling/deepcopy, dropping unpicklable handles.

    Extends :meth:`VideoBackend.__getstate__` to also drop the cached
    fsspec-backed ``_url_file`` (reopened lazily by :meth:`_open_h5`).
    """
    state = super().__getstate__()
    state["_url_file"] = None
    return state

__init__(filename, grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None, dataset=None, input_format='channels_last', source_filename=None, source_inds=None, image_format='hdf5', channel_order='RGB', plugin=None, url_headers=None, url_stream_mode='blockcache')

Method generated by attrs for class HDF5Video.

Source code in sleap_io/io/video_reading.py
# Track available backends (populated on module import)
_AVAILABLE_VIDEO_BACKENDS = {
    "opencv": "cv2" in sys.modules,
    "FFMPEG": "imageio_ffmpeg" in sys.modules,
    "pyav": "av" in sys.modules,
}

_AVAILABLE_IMAGE_BACKENDS = {
    "opencv": "cv2" in sys.modules,
    "imageio": True,  # Always available (core dependency)
}


# Global default video plugin
_default_video_plugin: str | None = None


def normalize_plugin_name(plugin: str) -> str:
    """Normalize plugin names to standard format.

    Args:
        plugin: Plugin name or alias (case-insensitive).

__repr__()

Method generated by attrs for class HDF5Video.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

__setattr__(name, val)

Method generated by attrs for class HDF5Video.

Source code in sleap_io/io/video_reading.py
    multiple threads are safe: although all reads share one cached fsspec
    file-like (a single byte position), h5py serializes every HDF5 C-library
    call under a global recursive lock (`h5py._objects.phil`), so the
    seek+read pair a frame read performs is never interleaved across threads.
    For true read *parallelism* (rather than just safety), construct
    independent `Video`/`HDF5Video` instances per worker; each gets its own
    fsspec file and block cache.
"""

close()

Release the cached HDF5 reader and the cached fsspec URL file-like.

Extends :meth:VideoBackend.close (which drops the cached h5py.File reader) by also closing the fsspec-backed _url_file shared across reads, which h5py.File.close() does not close on its own. Both are lazily reopened on the next read, so this is safe to call between reads.

Source code in sleap_io/io/video_reading.py
def close(self) -> None:
    """Release the cached HDF5 reader and the cached fsspec URL file-like.

    Extends :meth:`VideoBackend.close` (which drops the cached ``h5py.File``
    reader) by also closing the fsspec-backed ``_url_file`` shared across
    reads, which ``h5py.File.close()`` does not close on its own. Both are
    lazily reopened on the next read, so this is safe to call between reads.
    """
    super().close()
    self._close_url_file()

decode_embedded(img_string)

Decode an embedded image string into a numpy array.

Parameters:

Name Type Description Default
img_string ndarray

Binary string of the image as a int8 numpy vector with the bytes as values corresponding to the format-encoded image.

required

Returns:

Type Description
ndarray

The decoded image as a numpy array of shape (height, width, channels). If a rank-2 image is decoded, it will be expanded such that channels will be 1.

This method does not apply grayscale conversion as per the grayscale attribute. Use the get_frame or get_frames methods of the VideoBackend to apply grayscale conversion rather than calling this function directly.

Source code in sleap_io/io/video_reading.py
def decode_embedded(self, img_string: np.ndarray) -> np.ndarray:
    """Decode an embedded image string into a numpy array.

    Args:
        img_string: Binary string of the image as a `int8` numpy vector with the
            bytes as values corresponding to the format-encoded image.

    Returns:
        The decoded image as a numpy array of shape `(height, width, channels)`. If
        a rank-2 image is decoded, it will be expanded such that channels will be 1.

        This method does not apply grayscale conversion as per the `grayscale`
        attribute. Use the `get_frame` or `get_frames` methods of the `VideoBackend`
        to apply grayscale conversion rather than calling this function directly.
    """
    # Decode based on plugin
    if self.plugin == "opencv":
        img = cv2.imdecode(img_string, cv2.IMREAD_UNCHANGED)
        decoder_order = "BGR"  # OpenCV decodes to BGR
    else:
        # Use imageio for FFMPEG or any other plugin
        img = iio.imread(BytesIO(img_string), extension=f".{self.image_format}")
        decoder_order = "RGB"  # imageio decodes to RGB

    if img.ndim == 2:
        img = np.expand_dims(img, axis=-1)

    # Convert channel order if needed
    # If the stored order doesn't match the decoder order, flip channels
    if img.shape[-1] == 3 and self.channel_order != decoder_order:
        img = img[..., ::-1]  # Flip RGB <-> BGR

    return img

get_frame_raw_bytes(frame_idx)

Get raw encoded bytes for a frame without decoding.

This method reads the raw compressed image data (PNG/JPEG bytes) directly from the HDF5 dataset without decoding it. This is useful for fast copying of embedded images when the target format matches the source format.

Parameters:

Name Type Description Default
frame_idx int

Index of the frame to read.

required

Returns:

Type Description
ndarray | None

Raw encoded bytes as int8 numpy array, or None if: - The backend doesn't have embedded images (including "hdf5" format which stores raw numpy arrays, not encoded images) - The frame index is not available

Notes

For variable-length datasets, returns the raw bytes directly. For fixed-length datasets, returns bytes with trailing zeros stripped.

Source code in sleap_io/io/video_reading.py
def get_frame_raw_bytes(self, frame_idx: int) -> np.ndarray | None:
    """Get raw encoded bytes for a frame without decoding.

    This method reads the raw compressed image data (PNG/JPEG bytes) directly
    from the HDF5 dataset without decoding it. This is useful for fast copying
    of embedded images when the target format matches the source format.

    Args:
        frame_idx: Index of the frame to read.

    Returns:
        Raw encoded bytes as int8 numpy array, or None if:
        - The backend doesn't have embedded images (including "hdf5" format which
          stores raw numpy arrays, not encoded images)
        - The frame index is not available

    Notes:
        For variable-length datasets, returns the raw bytes directly.
        For fixed-length datasets, returns bytes with trailing zeros stripped.
    """
    if not self.has_embedded_images:
        return None

    if not self.has_frame(frame_idx):
        return None

    # Get the internal index (handle frame_map)
    internal_idx = (
        self.frame_map.get(frame_idx, frame_idx) if self.frame_map else frame_idx
    )

    # Read directly from dataset
    if self.keep_open:
        if self._open_reader is None:
            self._open_reader = self._open_h5()
        f = self._open_reader
    else:
        f = self._open_h5()

    ds = f[self.dataset]
    raw_bytes = ds[internal_idx]

    # Handle fixed-length padding (strip trailing zeros)
    is_vlen = h5py.check_vlen_dtype(ds.dtype) is not None
    if not is_vlen:
        # Find last non-zero byte
        non_zero_mask = raw_bytes != 0
        if non_zero_mask.any():
            last_non_zero = np.where(non_zero_mask)[0][-1]
            raw_bytes = raw_bytes[: last_non_zero + 1]

    if not self.keep_open:
        f.close()

    return raw_bytes

has_frame(frame_idx)

Check if a frame index is contained in the video.

Parameters:

Name Type Description Default
frame_idx int

Index of frame to check.

required

Returns:

Type Description
bool

True if the index is contained in the video, otherwise False.

Source code in sleap_io/io/video_reading.py
def has_frame(self, frame_idx: int) -> bool:
    """Check if a frame index is contained in the video.

    Args:
        frame_idx: Index of frame to check.

    Returns:
        `True` if the index is contained in the video, otherwise `False`.
    """
    if self.frame_map:
        return frame_idx in self.frame_map
    else:
        return frame_idx < len(self)

read_crop(frame_idx, crop, fill=0)

Read a spatial hyperslab of a frame, padded to the crop shape.

This is the single-frame HDF5 crop pushdown hook consumed by :class:CropVideoBackend. When applicable, it reads only the spatial region of the frame that overlaps crop directly from the chunked dataset (avoiding a full-frame decode) and pads out-of-bounds regions exactly as :func:sleap_io.transform.frame.crop_frame would.

Parameters:

Name Type Description Default
frame_idx int

Index of the frame to read (source-video index; mapped through frame_map if present, though pushdown is gated off when a frame_map exists).

required
crop tuple[int, int, int, int]

Crop region (x1, y1, x2, y2) with x2/y2 exclusive. May be negative or exceed the frame bounds (padded with fill).

required
fill int | tuple[int, ...]

Fill value for out-of-bounds regions.

0

Returns:

Type Description
ndarray | None

A (y2 - y1, x2 - x1, C) array (pre-grayscale, dtype == ds.dtype) byte-identical to crop_frame(self._read_frame(frame_idx), crop, fill) when pushdown is applicable; otherwise None to signal the caller should fall back to a full-frame decode plus crop_frame. Never raises for out-of-bounds crops.

Source code in sleap_io/io/video_reading.py
def read_crop(
    self,
    frame_idx: int,
    crop: tuple[int, int, int, int],
    fill: int | tuple[int, ...] = 0,
) -> np.ndarray | None:
    """Read a spatial hyperslab of a frame, padded to the crop shape.

    This is the single-frame HDF5 crop pushdown hook consumed by
    :class:`CropVideoBackend`. When applicable, it reads only the spatial
    region of the frame that overlaps ``crop`` directly from the chunked
    dataset (avoiding a full-frame decode) and pads out-of-bounds regions
    exactly as :func:`sleap_io.transform.frame.crop_frame` would.

    Args:
        frame_idx: Index of the frame to read (source-video index; mapped
            through ``frame_map`` if present, though pushdown is gated off when
            a ``frame_map`` exists).
        crop: Crop region ``(x1, y1, x2, y2)`` with ``x2``/``y2`` exclusive.
            May be negative or exceed the frame bounds (padded with ``fill``).
        fill: Fill value for out-of-bounds regions.

    Returns:
        A ``(y2 - y1, x2 - x1, C)`` array (pre-grayscale, ``dtype == ds.dtype``)
        byte-identical to ``crop_frame(self._read_frame(frame_idx), crop,
        fill)`` when pushdown is applicable; otherwise ``None`` to signal the
        caller should fall back to a full-frame decode plus ``crop_frame``.
        Never raises for out-of-bounds crops.
    """
    if not self._can_push_crop:
        return None
    try:
        if self.keep_open:
            if self._open_reader is None:
                self._open_reader = self._open_h5()
            f = self._open_reader
            ds = f[self.dataset]
            return self._read_crop_from_ds(ds, frame_idx, crop, fill)
        else:
            with self._open_h5() as f:
                ds = f[self.dataset]
                return self._read_crop_from_ds(ds, frame_idx, crop, fill)
    except (OSError, KeyError, IndexError):  # pragma: no cover - defensive
        return None

read_crops(frame_inds, crop, fill=0)

Batched :meth:read_crop.

Parameters:

Name Type Description Default
frame_inds list

List of source-video frame indices to read.

required
crop tuple[int, int, int, int]

Crop region (x1, y1, x2, y2) with x2/y2 exclusive.

required
fill int | tuple[int, ...]

Fill value for out-of-bounds regions.

0

Returns:

Type Description
ndarray | None

A (N, y2 - y1, x2 - x1, C) array byte-identical to stacking per-frame crop_frame results, or None to fall back to a full-frame decode plus crop_frame.

Source code in sleap_io/io/video_reading.py
def read_crops(
    self,
    frame_inds: list,
    crop: tuple[int, int, int, int],
    fill: int | tuple[int, ...] = 0,
) -> np.ndarray | None:
    """Batched :meth:`read_crop`.

    Args:
        frame_inds: List of source-video frame indices to read.
        crop: Crop region ``(x1, y1, x2, y2)`` with ``x2``/``y2`` exclusive.
        fill: Fill value for out-of-bounds regions.

    Returns:
        A ``(N, y2 - y1, x2 - x1, C)`` array byte-identical to stacking
        per-frame ``crop_frame`` results, or ``None`` to fall back to a
        full-frame decode plus ``crop_frame``.
    """
    if not self._can_push_crop:
        return None
    try:
        if self.keep_open:
            if self._open_reader is None:
                self._open_reader = self._open_h5()
            f = self._open_reader
            ds = f[self.dataset]
            return self._stack_crops(ds, frame_inds, crop, fill)
        else:
            with self._open_h5() as f:
                ds = f[self.dataset]
                return self._stack_crops(ds, frame_inds, crop, fill)
    except (OSError, KeyError, IndexError):  # pragma: no cover - defensive
        return None

read_test_frame()

Read a single frame from the video to test for grayscale.

Source code in sleap_io/io/video_reading.py
def read_test_frame(self) -> np.ndarray:
    """Read a single frame from the video to test for grayscale."""
    if self.frame_map:
        frame_idx = list(self.frame_map.keys())[0]
    else:
        frame_idx = 0
    return self._read_frame(frame_idx)

ImageVideo

Bases: sleap_io.io.video_reading.VideoBackend

Video backend for reading videos stored as image files.

This backend supports reading videos stored as a list of images.

Attributes:

Name Type Description
filename

Path to image files.

grayscale

Whether to force grayscale. If None, autodetect on first frame load.

plugin

Image plugin to use for reading. One of "opencv" or "imageio". If None, uses global default from get_default_image_plugin(), or auto-detects.

Methods:

Name Description
__eq__

Method generated by attrs for class ImageVideo.

__init__

Method generated by attrs for class ImageVideo.

__repr__

Method generated by attrs for class ImageVideo.

__setattr__

Method generated by attrs for class ImageVideo.

find_images

Find images in a folder and return a list of filenames.

get_frame_raw_bytes

Return the raw encoded bytes of the source image file for a frame.

Source code in sleap_io/io/video_reading.py
@attrs.define
class ImageVideo(VideoBackend):
    """Video backend for reading videos stored as image files.

    This backend supports reading videos stored as a list of images.

    Attributes:
        filename: Path to image files.
        grayscale: Whether to force grayscale. If None, autodetect on first frame load.
        plugin: Image plugin to use for reading. One of "opencv" or "imageio".
            If None, uses global default from get_default_image_plugin(), or
            auto-detects.
    """

    EXTS = ("png", "jpg", "jpeg", "tif", "tiff", "bmp")

    plugin: str = attrs.field()

    @plugin.validator
    def _validate_plugin(self, attribute, value):
        """Validate and normalize plugin name."""
        normalized = normalize_image_plugin_name(value)
        object.__setattr__(self, attribute.name, normalized)

    @plugin.default
    def _default_plugin(self) -> str:
        """Get default plugin, checking global default first."""
        # Check global default first
        if _default_image_plugin is not None:
            # Warn if preferred plugin not available
            if not _AVAILABLE_IMAGE_BACKENDS.get(_default_image_plugin, False):
                import warnings

                available = get_available_image_backends()
                install_cmd = get_installation_instructions(
                    _default_image_plugin, "image"
                )
                warnings.warn(
                    f"Preferred image plugin '{_default_image_plugin}' is not "
                    f"available. Available plugins: {available}\n"
                    f"Install with: {install_cmd}"
                )
                # Fall through to auto-detection
            else:
                return _default_image_plugin

        # Otherwise auto-detect
        if "cv2" in sys.modules:
            return "opencv"
        else:
            return "imageio"

    @staticmethod
    def find_images(folder: str) -> list[str]:
        """Find images in a folder and return a list of filenames."""
        folder = Path(folder)
        return sorted(
            [f.as_posix() for f in folder.glob("*") if f.suffix[1:] in ImageVideo.EXTS]
        )

    @property
    def num_frames(self) -> int:
        """Number of frames in the video."""
        return len(self.filename)

    def get_frame_raw_bytes(self, frame_idx: int) -> np.ndarray | None:
        """Return the raw encoded bytes of the source image file for a frame.

        Reads the on-disk image file verbatim (no decode/re-encode), enabling a
        direct byte-for-byte embed of already-compressed sources. This avoids both
        the cost of a decode/re-encode cycle and any additional compression
        artifacts (important for lossy JPEG sources).

        Only PNG/JPEG sources are supported here -- these are already entropy-coded
        and are decodable by the embedded-image reader (`HDF5Video.decode_embedded`).
        Other extensions (e.g. TIFF/BMP) return `None` so the caller falls back to
        decoding and re-encoding to the requested format.

        Args:
            frame_idx: Index of the frame to read.

        Returns:
            The raw file bytes as an `int8` numpy vector, or `None` if the source
            file is not a directly-storable compressed image (PNG/JPEG) or cannot
            be read.

        Notes:
            Bytes copied this way decode back to RGB (matching `_read_frame`), so
            the embedded dataset should record `channel_order="RGB"`.
        """
        filename = self.filename[frame_idx]
        ext = Path(filename).suffix.lower().lstrip(".")
        if ext not in ("png", "jpg", "jpeg"):
            return None
        try:
            with open(filename, "rb") as f:
                data = f.read()
        except OSError:
            return None
        return np.frombuffer(data, dtype="int8")

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the video.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            The frame as a numpy array of shape `(height, width, channels)` in RGB
            order.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frame` method of the `VideoBackend` class instead.

            Images are always returned in RGB order regardless of plugin:
            - imageio: Returns RGB natively
            - opencv: Returns BGR, automatically flipped to RGB
        """
        if self.plugin == "opencv":
            # OpenCV reads as BGR, flip to RGB
            img = cv2.imread(self.filename[frame_idx], cv2.IMREAD_UNCHANGED)
            if img is None:
                raise ValueError(f"Failed to read image: {self.filename[frame_idx]}")
            if img.ndim == 3 and img.shape[-1] == 3:
                img = img[..., ::-1]  # BGR -> RGB
        else:  # imageio
            # imageio reads as RGB natively
            img = iio.imread(self.filename[frame_idx])

        if img.ndim == 2:
            img = np.expand_dims(img, axis=-1)

        return img

EXTS = ('png', 'jpg', 'jpeg', 'tif', 'tiff', 'bmp') 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.

__annotations__ = {'plugin': 'str'} 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=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

__doc__ = 'Video backend for reading videos stored as image files.\n\nThis backend supports reading videos stored as a list of images.\n\nAttributes:\n filename: Path to image files.\n grayscale: Whether to force grayscale. If None, autodetect on first frame load.\n plugin: Image plugin to use for reading. One of "opencv" or "imageio".\n If None, uses global default from get_default_image_plugin(), or\n auto-detects.\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__ = 1871 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps', 'plugin') 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.io.video_reading' 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__ = ('plugin',) 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.

num_frames property

Number of frames in the video.

__eq__(other)

Method generated by attrs for class ImageVideo.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401
except ImportError:

__init__(filename, grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None, plugin=NOTHING)

Method generated by attrs for class ImageVideo.

Source code in sleap_io/io/video_reading.py
    pass

try:
    import av  # noqa: F401
except ImportError:
    pass


# Track available backends (populated on module import)
_AVAILABLE_VIDEO_BACKENDS = {
    "opencv": "cv2" in sys.modules,
    "FFMPEG": "imageio_ffmpeg" in sys.modules,
    "pyav": "av" in sys.modules,
}

__repr__()

Method generated by attrs for class ImageVideo.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

__setattr__(name, val)

Method generated by attrs for class ImageVideo.

Source code in sleap_io/io/video_reading.py
    multiple threads are safe: although all reads share one cached fsspec
    file-like (a single byte position), h5py serializes every HDF5 C-library
    call under a global recursive lock (`h5py._objects.phil`), so the
    seek+read pair a frame read performs is never interleaved across threads.
    For true read *parallelism* (rather than just safety), construct
    independent `Video`/`HDF5Video` instances per worker; each gets its own
    fsspec file and block cache.
"""

find_images(folder) staticmethod

Find images in a folder and return a list of filenames.

Source code in sleap_io/io/video_reading.py
@staticmethod
def find_images(folder: str) -> list[str]:
    """Find images in a folder and return a list of filenames."""
    folder = Path(folder)
    return sorted(
        [f.as_posix() for f in folder.glob("*") if f.suffix[1:] in ImageVideo.EXTS]
    )

get_frame_raw_bytes(frame_idx)

Return the raw encoded bytes of the source image file for a frame.

Reads the on-disk image file verbatim (no decode/re-encode), enabling a direct byte-for-byte embed of already-compressed sources. This avoids both the cost of a decode/re-encode cycle and any additional compression artifacts (important for lossy JPEG sources).

Only PNG/JPEG sources are supported here -- these are already entropy-coded and are decodable by the embedded-image reader (HDF5Video.decode_embedded). Other extensions (e.g. TIFF/BMP) return None so the caller falls back to decoding and re-encoding to the requested format.

Parameters:

Name Type Description Default
frame_idx int

Index of the frame to read.

required

Returns:

Type Description
ndarray | None

The raw file bytes as an int8 numpy vector, or None if the source file is not a directly-storable compressed image (PNG/JPEG) or cannot be read.

Notes

Bytes copied this way decode back to RGB (matching _read_frame), so the embedded dataset should record channel_order="RGB".

Source code in sleap_io/io/video_reading.py
def get_frame_raw_bytes(self, frame_idx: int) -> np.ndarray | None:
    """Return the raw encoded bytes of the source image file for a frame.

    Reads the on-disk image file verbatim (no decode/re-encode), enabling a
    direct byte-for-byte embed of already-compressed sources. This avoids both
    the cost of a decode/re-encode cycle and any additional compression
    artifacts (important for lossy JPEG sources).

    Only PNG/JPEG sources are supported here -- these are already entropy-coded
    and are decodable by the embedded-image reader (`HDF5Video.decode_embedded`).
    Other extensions (e.g. TIFF/BMP) return `None` so the caller falls back to
    decoding and re-encoding to the requested format.

    Args:
        frame_idx: Index of the frame to read.

    Returns:
        The raw file bytes as an `int8` numpy vector, or `None` if the source
        file is not a directly-storable compressed image (PNG/JPEG) or cannot
        be read.

    Notes:
        Bytes copied this way decode back to RGB (matching `_read_frame`), so
        the embedded dataset should record `channel_order="RGB"`.
    """
    filename = self.filename[frame_idx]
    ext = Path(filename).suffix.lower().lstrip(".")
    if ext not in ("png", "jpg", "jpeg"):
        return None
    try:
        with open(filename, "rb") as f:
            data = f.read()
    except OSError:
        return None
    return np.frombuffer(data, dtype="int8")

MediaVideo

Bases: sleap_io.io.video_reading.VideoBackend

Video backend for reading videos stored as common media files.

This backend supports reading through FFMPEG (the default), pyav, or OpenCV. Here are their trade-offs:

- "opencv": Fastest video reader, but only supports a limited number of codecs
    and may not be able to read some videos. It requires `opencv-python` to be
    installed. It is the fastest because it uses the OpenCV C++ library to read
    videos, but is limited by the version of FFMPEG that was linked into it at
    build time as well as the OpenCV version used.
- "FFMPEG": Slowest, but most reliable. This is the default backend. It requires
    `imageio-ffmpeg` and a `ffmpeg` executable on the system path (which can be
    installed via conda). The `imageio` plugin for FFMPEG reads frames into raw
    bytes which are communicated to Python through STDOUT on a subprocess pipe,
    which can be slow. However, it is the most reliable and feature-complete. If
    you install the conda-forge version of ffmpeg, it will be compiled with
    support for many codecs, including GPU-accelerated codecs like NVDEC for
    H264 and others.
- "pyav": Supports most codecs that FFMPEG does, but not as complete or reliable
    of an implementation in `imageio` as FFMPEG for some video types. It is
    faster than FFMPEG because it uses the `av` package to read frames directly
    into numpy arrays in memory without the need for a subprocess pipe. These
    are Python bindings for the C library libav, which is the same library that
    FFMPEG uses under the hood.

Attributes:

Name Type Description
filename

Path to video file.

grayscale

Whether to force grayscale. If None, autodetect on first frame load.

keep_open

Whether to keep the video reader open between calls to read frames. If False, will close the reader after each call. If True (the default), it will keep the reader open and cache it for subsequent calls which may enhance the performance of reading multiple frames.

plugin

Video plugin to use. One of "opencv", "FFMPEG", or "pyav". If None, will use the first available plugin in the order listed above.

Methods:

Name Description
__eq__

Method generated by attrs for class MediaVideo.

__init__

Method generated by attrs for class MediaVideo.

__repr__

Method generated by attrs for class MediaVideo.

__setattr__

Method generated by attrs for class MediaVideo.

Source code in sleap_io/io/video_reading.py
@attrs.define
class MediaVideo(VideoBackend):
    """Video backend for reading videos stored as common media files.

    This backend supports reading through FFMPEG (the default), pyav, or OpenCV. Here
    are their trade-offs:

        - "opencv": Fastest video reader, but only supports a limited number of codecs
            and may not be able to read some videos. It requires `opencv-python` to be
            installed. It is the fastest because it uses the OpenCV C++ library to read
            videos, but is limited by the version of FFMPEG that was linked into it at
            build time as well as the OpenCV version used.
        - "FFMPEG": Slowest, but most reliable. This is the default backend. It requires
            `imageio-ffmpeg` and a `ffmpeg` executable on the system path (which can be
            installed via conda). The `imageio` plugin for FFMPEG reads frames into raw
            bytes which are communicated to Python through STDOUT on a subprocess pipe,
            which can be slow. However, it is the most reliable and feature-complete. If
            you install the conda-forge version of ffmpeg, it will be compiled with
            support for many codecs, including GPU-accelerated codecs like NVDEC for
            H264 and others.
        - "pyav": Supports most codecs that FFMPEG does, but not as complete or reliable
            of an implementation in `imageio` as FFMPEG for some video types. It is
            faster than FFMPEG because it uses the `av` package to read frames directly
            into numpy arrays in memory without the need for a subprocess pipe. These
            are Python bindings for the C library libav, which is the same library that
            FFMPEG uses under the hood.

    Attributes:
        filename: Path to video file.
        grayscale: Whether to force grayscale. If None, autodetect on first frame load.
        keep_open: Whether to keep the video reader open between calls to read frames.
            If False, will close the reader after each call. If True (the default), it
            will keep the reader open and cache it for subsequent calls which may
            enhance the performance of reading multiple frames.
        plugin: Video plugin to use. One of "opencv", "FFMPEG", or "pyav". If `None`,
            will use the first available plugin in the order listed above.
    """

    plugin: str = attrs.field()

    @plugin.validator
    def _validate_plugin(self, attribute, value):
        # Normalize the plugin name
        normalized = normalize_plugin_name(value)
        # Update the actual value to the normalized version
        object.__setattr__(self, attribute.name, normalized)

    EXTS = ("mp4", "avi", "mov", "mj2", "mkv")

    @plugin.default
    def _default_plugin(self) -> str:
        # Check global default first
        if _default_video_plugin is not None:
            # Warn if preferred plugin not available
            if not _AVAILABLE_VIDEO_BACKENDS.get(_default_video_plugin, False):
                import warnings

                available = get_available_video_backends()
                install_cmd = get_installation_instructions(_default_video_plugin)
                warnings.warn(
                    f"Preferred video plugin '{_default_video_plugin}' is not "
                    f"available. Available plugins: {available}\n"
                    f"Install with: {install_cmd}"
                )
                # Fall through to auto-detection
            else:
                return _default_video_plugin

        # Auto-detect based on what's available
        if "cv2" in sys.modules:
            return "opencv"
        elif "imageio_ffmpeg" in sys.modules:
            return "FFMPEG"
        elif "av" in sys.modules:
            return "pyav"
        else:
            # Enhanced error message with installation instructions
            raise ImportError(
                "No video backend plugins are available.\n\n"
                "The bundled imageio-ffmpeg should be available by default.\n"
                "If you see this error, try reinstalling sleap-io:\n"
                "  pip install --force-reinstall sleap-io\n\n"
                "Alternative backends:\n"
                "  opencv (fastest):  pip install sleap-io[opencv]\n"
                "  pyav (balanced):   pip install sleap-io[pyav]\n\n"
                "For more information, see: https://io.sleap.ai"
            )

    @property
    def reader(self) -> object:
        """Return the reader object for the video, caching if necessary."""
        if self.keep_open:
            if self._open_reader is None:
                if self.plugin == "opencv":
                    self._open_reader = cv2.VideoCapture(self.filename)
                elif self.plugin == "pyav" or self.plugin == "FFMPEG":
                    self._open_reader = iio.imopen(
                        self.filename, "r", plugin=self.plugin
                    )
            return self._open_reader
        else:
            if self.plugin == "opencv":
                return cv2.VideoCapture(self.filename)
            elif self.plugin == "pyav" or self.plugin == "FFMPEG":
                return iio.imopen(self.filename, "r", plugin=self.plugin)

    @property
    def num_frames(self) -> int:
        """Number of frames in the video."""
        if self.plugin == "opencv":
            return int(self.reader.get(cv2.CAP_PROP_FRAME_COUNT))
        else:
            props = iio.improps(self.filename, plugin=self.plugin)
            n_frames = props.n_images
            if np.isinf(n_frames):
                legacy_reader = self.reader.legacy_get_reader()
                # Note: This might be super slow for some videos, so maybe we should
                # defer evaluation of this or give the user control over it.
                n_frames = legacy_reader.count_frames()
            return n_frames

    @property
    def fps(self) -> float | None:
        """Frames per second from video container metadata.

        Returns:
            The FPS from the video container, or None if it cannot be determined.

        Notes:
            This reads the FPS from the video file metadata using the appropriate
            method for the current plugin:
            - OpenCV: cv2.CAP_PROP_FPS
            - FFMPEG/pyav: imageio metadata

            For remote (URL) filenames the FPS is read directly from the pyav
            container via ``av.open(url)``. imageio's v2 FFMPEG reader (used for
            local files) requires the ``imageio-ffmpeg`` package and an ffmpeg
            executable, which are not guaranteed in a pyav-only install, whereas
            ``av`` is already required for remote loading.
        """
        # Return cached/explicit value if set
        if self._fps is not None:
            return self._fps

        # Read from container metadata and cache the result so repeated access
        # is O(1). This matters most for the remote (URL) path: ``av.open`` over
        # http does not use Range requests, so each uncached read re-streams the
        # entire video. Public helpers such as ``Video.frame_to_seconds`` read
        # ``fps`` multiple times per call, which would otherwise re-download the
        # whole video each time. Container fps is immutable for a given file, and
        # the cache slot is cleared when the filename changes (see
        # ``Video.replace_filename``), so caching is safe.
        try:
            if self.plugin == "opencv":
                fps = self.reader.get(cv2.CAP_PROP_FPS)
                rate = fps if fps > 0 else None
            elif _remote._is_url(self.filename):
                rate = _fps_from_av_container(self.filename)
            else:
                # Use imageio v2 API to get metadata (v3 improps doesn't include fps)
                import imageio.v2 as iio_v2

                reader = iio_v2.get_reader(self.filename, format="FFMPEG")
                meta = reader.get_meta_data()
                reader.close()
                fps = meta.get("fps")
                rate = float(fps) if fps is not None else None
        except Exception:
            return None
        self._fps = rate
        return rate

    @fps.setter
    def fps(self, value: float | None) -> None:
        """Set an explicit FPS override.

        Args:
            value: Frames per second. Must be positive if not None.

        Raises:
            ValueError: If value is not positive.

        Notes:
            Setting FPS on MediaVideo overrides the value from container metadata.
            This can be useful when the container metadata is incorrect or missing.
        """
        if value is not None and value <= 0:
            raise ValueError(f"FPS must be positive, got {value}")
        self._fps = value

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the video.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            The frame as a numpy array of shape `(height, width, channels)`.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frame` method of the `VideoBackend` class instead.
        """
        if self.plugin == "opencv":
            if self.keep_open:
                if self._open_reader is None:
                    self._open_reader = cv2.VideoCapture(self.filename)
                reader = self._open_reader
            else:
                reader = cv2.VideoCapture(self.filename)

            if reader.get(cv2.CAP_PROP_POS_FRAMES) != frame_idx:
                reader.set(cv2.CAP_PROP_POS_FRAMES, frame_idx)
            success, img = reader.read()

            if success:
                img = img[..., ::-1]  # BGR -> RGB

        elif self.plugin == "pyav" or self.plugin == "FFMPEG":
            if self.keep_open:
                img = self.reader.read(index=frame_idx)
            else:
                with iio.imopen(self.filename, "r", plugin=self.plugin) as reader:
                    img = reader.read(index=frame_idx)
            success = img is not None

        if not success:
            raise IndexError(f"Failed to read frame index {frame_idx}.")

        return img

    def _read_frames(self, frame_inds: list) -> np.ndarray:
        """Read a list of frames from the video.

        Args:
            frame_inds: List of indices of frames to read.

        Returns:
            The frame as a numpy array of shape `(frames, height, width, channels)`.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frames` method of the `VideoBackend` class instead.
        """
        if self.plugin == "opencv":
            if self.keep_open:
                if self._open_reader is None:
                    self._open_reader = cv2.VideoCapture(self.filename)
                reader = self._open_reader
            else:
                reader = cv2.VideoCapture(self.filename)

            reader.set(cv2.CAP_PROP_POS_FRAMES, frame_inds[0])
            imgs = []
            for idx in frame_inds:
                if reader.get(cv2.CAP_PROP_POS_FRAMES) != idx:
                    reader.set(cv2.CAP_PROP_POS_FRAMES, idx)
                _, img = reader.read()
                imgs.append(img)
            imgs = np.stack(imgs, axis=0)

            imgs = imgs[..., ::-1]  # BGR -> RGB

        elif self.plugin == "pyav" or self.plugin == "FFMPEG":
            if self.keep_open:
                if self._open_reader is None:
                    self._open_reader = iio.imopen(
                        self.filename, "r", plugin=self.plugin
                    )
                reader = self._open_reader
                imgs = np.stack([reader.read(index=idx) for idx in frame_inds], axis=0)
            else:
                with iio.imopen(self.filename, "r", plugin=self.plugin) as reader:
                    imgs = np.stack(
                        [reader.read(index=idx) for idx in frame_inds], axis=0
                    )
        return imgs

EXTS = ('mp4', 'avi', 'mov', 'mj2', 'mkv') 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.

__annotations__ = {'plugin': 'str'} 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=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

__doc__ = 'Video backend for reading videos stored as common media files.\n\nThis backend supports reading through FFMPEG (the default), pyav, or OpenCV. Here\nare their trade-offs:\n\n - "opencv": Fastest video reader, but only supports a limited number of codecs\n and may not be able to read some videos. It requires `opencv-python` to be\n installed. It is the fastest because it uses the OpenCV C++ library to read\n videos, but is limited by the version of FFMPEG that was linked into it at\n build time as well as the OpenCV version used.\n - "FFMPEG": Slowest, but most reliable. This is the default backend. It requires\n `imageio-ffmpeg` and a `ffmpeg` executable on the system path (which can be\n installed via conda). The `imageio` plugin for FFMPEG reads frames into raw\n bytes which are communicated to Python through STDOUT on a subprocess pipe,\n which can be slow. However, it is the most reliable and feature-complete. If\n you install the conda-forge version of ffmpeg, it will be compiled with\n support for many codecs, including GPU-accelerated codecs like NVDEC for\n H264 and others.\n - "pyav": Supports most codecs that FFMPEG does, but not as complete or reliable\n of an implementation in `imageio` as FFMPEG for some video types. It is\n faster than FFMPEG because it uses the `av` package to read frames directly\n into numpy arrays in memory without the need for a subprocess pipe. These\n are Python bindings for the C library libav, which is the same library that\n FFMPEG uses under the hood.\n\nAttributes:\n filename: Path to video file.\n grayscale: Whether to force grayscale. If None, autodetect on first frame load.\n keep_open: Whether to keep the video reader open between calls to read frames.\n If False, will close the reader after each call. If True (the default), it\n will keep the reader open and cache it for subsequent calls which may\n enhance the performance of reading multiple frames.\n plugin: Video plugin to use. One of "opencv", "FFMPEG", or "pyav". If `None`,\n will use the first available plugin in the order listed above.\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__ = 847 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps', 'plugin') 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.io.video_reading' 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__ = ('plugin',) 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__ = ('_fps', '_open_reader') 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.

fps property

Frames per second from video container metadata.

Returns:

Type Description

The FPS from the video container, or None if it cannot be determined.

Notes

This reads the FPS from the video file metadata using the appropriate method for the current plugin: - OpenCV: cv2.CAP_PROP_FPS - FFMPEG/pyav: imageio metadata

For remote (URL) filenames the FPS is read directly from the pyav container via av.open(url). imageio's v2 FFMPEG reader (used for local files) requires the imageio-ffmpeg package and an ffmpeg executable, which are not guaranteed in a pyav-only install, whereas av is already required for remote loading.

num_frames property

Number of frames in the video.

reader property

Return the reader object for the video, caching if necessary.

__eq__(other)

Method generated by attrs for class MediaVideo.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401
except ImportError:

__init__(filename, grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None, plugin=NOTHING)

Method generated by attrs for class MediaVideo.

Source code in sleap_io/io/video_reading.py
    pass

try:
    import av  # noqa: F401
except ImportError:
    pass


# Track available backends (populated on module import)
_AVAILABLE_VIDEO_BACKENDS = {
    "opencv": "cv2" in sys.modules,
    "FFMPEG": "imageio_ffmpeg" in sys.modules,
    "pyav": "av" in sys.modules,
}

__repr__()

Method generated by attrs for class MediaVideo.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

__setattr__(name, val)

Method generated by attrs for class MediaVideo.

Source code in sleap_io/io/video_reading.py
    multiple threads are safe: although all reads share one cached fsspec
    file-like (a single byte position), h5py serializes every HDF5 C-library
    call under a global recursive lock (`h5py._objects.phil`), so the
    seek+read pair a frame read performs is never interleaved across threads.
    For true read *parallelism* (rather than just safety), construct
    independent `Video`/`HDF5Video` instances per worker; each gets its own
    fsspec file and block cache.
"""

TiffVideo

Bases: sleap_io.io.video_reading.VideoBackend

Video backend for reading multi-page TIFF stacks.

This backend supports reading multi-page TIFF files as video sequences. Each page in the TIFF is treated as a frame.

Attributes:

Name Type Description
filename

Path to the multi-page TIFF file.

grayscale

Whether to force grayscale. If None, autodetect on first frame load.

keep_open

Whether to keep the reader open between calls to read frames.

format

Format of the TIFF file ("multi_page", "THW", "HWT", "THWC", "CHWT").

Methods:

Name Description
__attrs_post_init__

Initialize format if not provided.

__eq__

Method generated by attrs for class TiffVideo.

__init__

Method generated by attrs for class TiffVideo.

__repr__

Method generated by attrs for class TiffVideo.

detect_format

Detect TIFF format and shape for single files.

is_multipage

Check if a TIFF file contains multiple pages.

Source code in sleap_io/io/video_reading.py
@attrs.define
class TiffVideo(VideoBackend):
    """Video backend for reading multi-page TIFF stacks.

    This backend supports reading multi-page TIFF files as video sequences.
    Each page in the TIFF is treated as a frame.

    Attributes:
        filename: Path to the multi-page TIFF file.
        grayscale: Whether to force grayscale. If None, autodetect on first frame load.
        keep_open: Whether to keep the reader open between calls to read frames.
        format: Format of the TIFF file ("multi_page", "THW", "HWT", "THWC", "CHWT").
    """

    EXTS = ("tif", "tiff")
    format: str | None = None

    @staticmethod
    def is_multipage(filename: str) -> bool:
        """Check if a TIFF file contains multiple pages.

        Args:
            filename: Path to the TIFF file.

        Returns:
            True if the TIFF contains multiple pages, False otherwise.
        """
        try:
            # Try to read the second frame
            iio.imread(filename, index=1)
            return True
        except (IndexError, ValueError):
            return False
        except Exception:
            # For any other error, assume it's not multi-page
            return False

    @staticmethod
    def detect_format(filename: str) -> tuple[str, dict]:
        """Detect TIFF format and shape for single files.

        Args:
            filename: Path to the TIFF file.

        Returns:
            Tuple of (format_type, metadata) where:
            - format_type: "single_frame", "multi_page", "rank3_video", or "rank4_video"
            - metadata: dict with shape info and inferred format
        """
        try:
            # Read first frame to check shape
            img = iio.imread(filename, index=0)
            shape = img.shape

            # Check if multi-page first
            is_multi = TiffVideo.is_multipage(filename)

            if is_multi:
                return "multi_page", {"shape": shape}

            # Single page cases
            if img.ndim == 2:
                # Rank-2: single channel image
                return "single_frame", {"shape": shape}
            elif img.ndim == 3:
                # Rank-3: could be HWC (single frame) or THW/HWT (video)
                return TiffVideo._detect_rank3_format(shape)
            elif img.ndim == 4:
                # Rank-4: video with channels
                return TiffVideo._detect_rank4_format(shape)
            else:
                return "single_frame", {"shape": shape}

        except Exception:
            return "single_frame", {"shape": None}

    @staticmethod
    def _detect_rank3_format(shape: tuple) -> tuple[str, dict]:
        """Detect format for rank-3 TIFF files.

        Args:
            shape: Shape tuple (dim1, dim2, dim3)

        Returns:
            Tuple of (format_type, metadata)
        """
        dim1, dim2, dim3 = shape

        # If last dimension is 1 or 3, likely HWC (single frame)
        if dim3 in (1, 3):
            return "single_frame", {"shape": shape, "format": "HWC"}

        # If first two dims are equal, it's likely HWT format
        # (most common case for square frames stored as H x W x T)
        if dim1 == dim2:
            # Default to HWT format for square frames
            return "rank3_video", {
                "shape": shape,
                "format": "HWT",
                "height": dim1,
                "width": dim2,
                "n_frames": dim3,
            }
        else:
            # For non-square frames, check if it could be THW
            # This is less common but possible
            if dim2 == dim3:
                # Could be THW format
                return "rank3_video", {
                    "shape": shape,
                    "format": "THW",
                    "n_frames": dim1,
                    "height": dim2,
                    "width": dim3,
                }
            else:
                # Default to HWT format
                return "rank3_video", {
                    "shape": shape,
                    "format": "HWT",
                    "height": dim1,
                    "width": dim2,
                    "n_frames": dim3,
                }

    @staticmethod
    def _detect_rank4_format(shape: tuple) -> tuple[str, dict]:
        """Detect format for rank-4 TIFF files.

        Args:
            shape: Shape tuple (dim1, dim2, dim3, dim4)

        Returns:
            Tuple of (format_type, metadata)
        """
        dim1, dim2, dim3, dim4 = shape

        # Check if first or last dimension is 1 or 3 (channels)
        if dim1 in (1, 3):
            # CHWT format
            return "rank4_video", {
                "shape": shape,
                "format": "CHWT",
                "channels": dim1,
                "height": dim2,
                "width": dim3,
                "n_frames": dim4,
            }
        elif dim4 in (1, 3):
            # THWC format
            return "rank4_video", {
                "shape": shape,
                "format": "THWC",
                "n_frames": dim1,
                "height": dim2,
                "width": dim3,
                "channels": dim4,
            }
        else:
            # Default to THWC
            return "rank4_video", {
                "shape": shape,
                "format": "THWC",
                "n_frames": dim1,
                "height": dim2,
                "width": dim3,
                "channels": dim4,
            }

    def __attrs_post_init__(self):
        """Initialize format if not provided."""
        if self.format is None:
            # Auto-detect format
            format_type, metadata = TiffVideo.detect_format(self.filename)
            if format_type == "multi_page":
                self.format = "multi_page"
            elif format_type in ("rank3_video", "rank4_video"):
                self.format = metadata.get("format", "multi_page")
            else:
                self.format = "multi_page"

    @property
    def num_frames(self) -> int:
        """Number of frames in the TIFF stack."""
        if self.format == "multi_page":
            # Count frames by trying to read each one until we get an error
            frame_count = 0
            while True:
                try:
                    iio.imread(self.filename, index=frame_count)
                    frame_count += 1
                except (IndexError, ValueError):
                    break
            return frame_count
        else:
            # For rank3/rank4 formats, detect from shape
            format_type, metadata = TiffVideo.detect_format(self.filename)
            return metadata.get("n_frames", 1)

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the TIFF stack.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            The frame as a numpy array of shape `(height, width, channels)`.

        Notes:
            This does not apply grayscale conversion. It is recommended to use the
            `get_frame` method of the `VideoBackend` class instead.
        """
        if self.format == "multi_page":
            img = iio.imread(self.filename, index=frame_idx)
            if img.ndim == 2:
                img = np.expand_dims(img, axis=-1)
            return img
        else:
            # Read entire array for rank3/rank4 formats
            img = iio.imread(self.filename)

            if self.format == "THW":
                # Extract frame from THW format
                frame = img[frame_idx, :, :]
                return np.expand_dims(frame, axis=-1)
            elif self.format == "HWT":
                # Extract frame from HWT format
                frame = img[:, :, frame_idx]
                return np.expand_dims(frame, axis=-1)
            elif self.format == "THWC":
                # Extract frame from THWC format
                return img[frame_idx, :, :, :]
            elif self.format == "CHWT":
                # Extract frame from CHWT format
                frame = img[:, :, :, frame_idx]
                return np.moveaxis(frame, 0, -1)  # CHW -> HWC
            else:
                raise ValueError(f"Unknown format: {self.format}")

    def _read_frames(self, frame_inds: list) -> np.ndarray:
        """Read multiple frames from the TIFF stack.

        Args:
            frame_inds: List of frame indices to read.

        Returns:
            Frames as a numpy array of shape `(frames, height, width, channels)`.
        """
        if self.format == "multi_page":
            imgs = []
            for idx in frame_inds:
                imgs.append(self._read_frame(idx))
            return np.stack(imgs, axis=0)
        else:
            # For rank3/rank4, read all at once and extract
            img = iio.imread(self.filename)

            if self.format == "THW":
                frames = img[frame_inds, :, :]
                return np.expand_dims(frames, axis=-1)
            elif self.format == "HWT":
                frames = img[:, :, frame_inds]
                frames = np.moveaxis(frames, -1, 0)  # HWT -> THW
                return np.expand_dims(frames, axis=-1)
            elif self.format == "THWC":
                return img[frame_inds, :, :, :]
            elif self.format == "CHWT":
                frames = img[:, :, :, frame_inds]
                frames = np.moveaxis(frames, -1, 0)  # CHWT -> TCHW
                frames = np.moveaxis(frames, 1, -1)  # TCHW -> THWC
                return frames
            else:
                raise ValueError(f"Unknown format: {self.format}")

EXTS = ('tif', 'tiff') 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.

__annotations__ = {'format': 'str | 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=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 0x7f08471ce840>, field_transformer=None) class-attribute

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

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

Warning:

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

Attributes:

Name Type Description
is_exception bool

Whether the class is treated as an exception class.

is_slotted bool

Whether the class is slotted <slotted classes>.

has_weakref_slot bool

Whether the class has a slot for weak references.

is_frozen bool

Whether the class is frozen.

kw_only KeywordOnly

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

collected_fields_by_mro bool

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

added_init bool

Whether the class has an attrs-generated __init__ method.

added_repr bool

Whether the class has an attrs-generated __repr__ method.

added_eq bool

Whether the class has attrs-generated equality methods.

added_ordering bool

Whether the class has attrs-generated ordering methods.

hashability Hashability

How hashable <hashing> the class is.

added_match_args bool

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

added_str bool

Whether the class has an attrs-generated __str__ method.

added_pickling bool

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

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

The class's __setattr__ hook.

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

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

.. versionadded:: 25.4.0

__doc__ = 'Video backend for reading multi-page TIFF stacks.\n\nThis backend supports reading multi-page TIFF files as video sequences.\nEach page in the TIFF is treated as a frame.\n\nAttributes:\n filename: Path to the multi-page TIFF file.\n grayscale: Whether to force grayscale. If None, autodetect on first frame load.\n keep_open: Whether to keep the reader open between calls to read frames.\n format: Format of the TIFF file ("multi_page", "THW", "HWT", "THWC", "CHWT").\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__ = 2007 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps', 'format') 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.io.video_reading' 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__ = ('format',) 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__ = ('format',) 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.

num_frames property

Number of frames in the TIFF stack.

__attrs_post_init__()

Initialize format if not provided.

Source code in sleap_io/io/video_reading.py
def __attrs_post_init__(self):
    """Initialize format if not provided."""
    if self.format is None:
        # Auto-detect format
        format_type, metadata = TiffVideo.detect_format(self.filename)
        if format_type == "multi_page":
            self.format = "multi_page"
        elif format_type in ("rank3_video", "rank4_video"):
            self.format = metadata.get("format", "multi_page")
        else:
            self.format = "multi_page"

__eq__(other)

Method generated by attrs for class TiffVideo.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401
except ImportError:

__init__(filename, grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None, format=None)

Method generated by attrs for class TiffVideo.

Source code in sleap_io/io/video_reading.py
    pass

try:
    import av  # noqa: F401
except ImportError:
    pass


# Track available backends (populated on module import)

__repr__()

Method generated by attrs for class TiffVideo.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

detect_format(filename) staticmethod

Detect TIFF format and shape for single files.

Parameters:

Name Type Description Default
filename str

Path to the TIFF file.

required

Returns:

Type Description
tuple[str, dict]

Tuple of (format_type, metadata) where: - format_type: "single_frame", "multi_page", "rank3_video", or "rank4_video" - metadata: dict with shape info and inferred format

Source code in sleap_io/io/video_reading.py
@staticmethod
def detect_format(filename: str) -> tuple[str, dict]:
    """Detect TIFF format and shape for single files.

    Args:
        filename: Path to the TIFF file.

    Returns:
        Tuple of (format_type, metadata) where:
        - format_type: "single_frame", "multi_page", "rank3_video", or "rank4_video"
        - metadata: dict with shape info and inferred format
    """
    try:
        # Read first frame to check shape
        img = iio.imread(filename, index=0)
        shape = img.shape

        # Check if multi-page first
        is_multi = TiffVideo.is_multipage(filename)

        if is_multi:
            return "multi_page", {"shape": shape}

        # Single page cases
        if img.ndim == 2:
            # Rank-2: single channel image
            return "single_frame", {"shape": shape}
        elif img.ndim == 3:
            # Rank-3: could be HWC (single frame) or THW/HWT (video)
            return TiffVideo._detect_rank3_format(shape)
        elif img.ndim == 4:
            # Rank-4: video with channels
            return TiffVideo._detect_rank4_format(shape)
        else:
            return "single_frame", {"shape": shape}

    except Exception:
        return "single_frame", {"shape": None}

is_multipage(filename) staticmethod

Check if a TIFF file contains multiple pages.

Parameters:

Name Type Description Default
filename str

Path to the TIFF file.

required

Returns:

Type Description
bool

True if the TIFF contains multiple pages, False otherwise.

Source code in sleap_io/io/video_reading.py
@staticmethod
def is_multipage(filename: str) -> bool:
    """Check if a TIFF file contains multiple pages.

    Args:
        filename: Path to the TIFF file.

    Returns:
        True if the TIFF contains multiple pages, False otherwise.
    """
    try:
        # Try to read the second frame
        iio.imread(filename, index=1)
        return True
    except (IndexError, ValueError):
        return False
    except Exception:
        # For any other error, assume it's not multi-page
        return False

VideoBackend

Base class for video backends.

This class is not meant to be used directly. Instead, use the from_filename constructor to create a backend instance.

Attributes:

Name Type Description
filename

Path to video file(s).

grayscale

Whether to force grayscale. If None, autodetect on first frame load.

keep_open

Whether to keep the video reader open between calls to read frames. If False, will close the reader after each call. If True (the default), it will keep the reader open and cache it for subsequent calls which may enhance the performance of reading multiple frames.

fps

Frames per second of the video. For MediaVideo, this is read from container metadata. For other backends (ImageVideo, HDF5Video, TiffVideo), this must be set explicitly or will be None.

Methods:

Name Description
__eq__

Method generated by attrs for class VideoBackend.

__getitem__

Return a single frame or a list of frames from the video.

__getstate__

Return state for pickling/deepcopy, dropping the open reader handle.

__init__

Method generated by attrs for class VideoBackend.

__len__

Return number of frames in the video.

__repr__

Method generated by attrs for class VideoBackend.

__setstate__

Restore state from pickling/deepcopy.

close

Release the cached open reader handle, if any.

detect_grayscale

Detect whether the video is grayscale.

from_filename

Create a VideoBackend from a filename.

get_frame

Read a single frame from the video.

get_frames

Read a list of frames from the video.

has_frame

Check if a frame index is contained in the video.

read_test_frame

Read a single frame from the video to test for grayscale.

Source code in sleap_io/io/video_reading.py
@attrs.define
class VideoBackend:
    """Base class for video backends.

    This class is not meant to be used directly. Instead, use the `from_filename`
    constructor to create a backend instance.

    Attributes:
        filename: Path to video file(s).
        grayscale: Whether to force grayscale. If None, autodetect on first frame load.
        keep_open: Whether to keep the video reader open between calls to read frames.
            If False, will close the reader after each call. If True (the default), it
            will keep the reader open and cache it for subsequent calls which may
            enhance the performance of reading multiple frames.
        fps: Frames per second of the video. For MediaVideo, this is read from container
            metadata. For other backends (ImageVideo, HDF5Video, TiffVideo), this must
            be set explicitly or will be None.
    """

    filename: str | Path | list[str] | list[Path]
    grayscale: bool | None = None
    keep_open: bool = True
    _cached_shape: tuple[int, int, int, int] | None = None
    _open_reader: object | None = None
    _fps: float | None = None

    @property
    def fps(self) -> float | None:
        """Frames per second of the video.

        Returns:
            The FPS if known, or None if unavailable/unknown.

        Notes:
            For MediaVideo, this is read from container metadata.
            For ImageVideo, HDF5Video, and TiffVideo, this must be set explicitly
            or inherited from source_video.
        """
        return self._fps

    @fps.setter
    def fps(self, value: float | None) -> None:
        """Set the FPS.

        Args:
            value: Frames per second. Must be positive if not None.

        Raises:
            ValueError: If value is not positive.
        """
        if value is not None and value <= 0:
            raise ValueError(f"FPS must be positive, got {value}")
        self._fps = value

    def __getstate__(self) -> dict:
        """Return state for pickling/deepcopy, dropping the open reader handle.

        The cached ``_open_reader`` (e.g. an ``h5py.File`` or video container) is
        not picklable and is reopened lazily on next access, so it is excluded.
        """
        import attr

        state = {a.name: getattr(self, a.name) for a in attr.fields(type(self))}
        state["_open_reader"] = None
        return state

    def __setstate__(self, state: dict) -> None:
        """Restore state from pickling/deepcopy.

        attrs slotted classes need ``object.__setattr__`` to set slots directly.
        Validators are skipped, which is safe since state came from a valid object.
        """
        for key, value in state.items():
            object.__setattr__(self, key, value)

    def close(self) -> None:
        """Release the cached open reader handle, if any.

        Closes (``.close()``) or releases (``.release()`` for an OpenCV
        ``VideoCapture``) the cached ``_open_reader`` and drops the reference so
        a long-lived backend does not leak the underlying file/container handle.
        The reader is lazily reopened on the next read, so this is safe to call
        between reads. A no-op when nothing is cached. Subclasses that hold
        additional handles (e.g. :class:`HDF5Video`'s URL file-like) override
        this and call ``super().close()``.
        """
        reader = self._open_reader
        self._open_reader = None
        if reader is None:
            return
        # Every real reader is an h5py.File / imageio reader (``.close()``) or an
        # OpenCV VideoCapture (``.release()``); the None case is purely defensive.
        closer = getattr(reader, "close", None) or getattr(reader, "release", None)
        if closer is None:  # pragma: no cover - defensive: reader always closeable
            return
        try:
            closer()
        except Exception:  # pragma: no cover - defensive: close should not raise
            pass

    @classmethod
    def from_filename(
        cls,
        filename: str | list[str],
        dataset: str | None = None,
        grayscale: bool | None = None,
        keep_open: bool = True,
        url_headers: dict[str, str] | None = None,
        url_stream_mode: str = "blockcache",
        **kwargs,
    ) -> "VideoBackend":
        """Create a VideoBackend from a filename.

        Args:
            filename: Path to video file(s).
            dataset: Name of dataset in HDF5 file.
            grayscale: Whether to force grayscale. If None, autodetect on first frame
                load.
            keep_open: Whether to keep the video reader open between calls to read
                frames. If False, will close the reader after each call. If True (the
                default), it will keep the reader open and cache it for subsequent calls
                which may enhance the performance of reading multiple frames.
            url_headers: HTTP headers forwarded to the remote backend when
                ``filename`` is a URL (HDF5Video only). Set at construction so the
                metadata probe is authenticated; ignored for local files and other
                backends.
            url_stream_mode: Remote streaming strategy for a URL-backed HDF5Video
                (one of ``"blockcache"``/``"cache"``/``"filecache"``/``"download"``).
                Ignored for local files and other backends.
            **kwargs: Additional backend-specific arguments. These are filtered to only
                include parameters that are valid for the specific backend being
                created:
                - For ImageVideo: plugin (str): Image plugin to use. One of "opencv"
                  or "imageio". Also accepts aliases (case-insensitive).
                  If None, uses global default if set, otherwise auto-detects.
                - For MediaVideo: plugin (str): Video plugin to use. One of "opencv",
                  "FFMPEG", or "pyav". Also accepts aliases (case-insensitive).
                  If None, uses global default if set, otherwise auto-detects.
                - For HDF5Video: input_format (str), frame_map (dict),
                  source_filename (str),
                  source_inds (np.ndarray), image_format (str). See HDF5Video for
                  details.

        Returns:
            VideoBackend subclass instance.
        """
        if isinstance(filename, Path):
            filename = filename.as_posix()

        is_url = type(filename) is str and _remote._is_url(filename)

        if is_url:
            from sleap_io.io._gdrive import _is_gdrive_url

            if _is_gdrive_url(filename):
                # Drive download URLs carry no extension and Drive rejects the
                # range/HEAD requests video decoding relies on, so streaming a
                # Drive video is not supported. Drive *labels* (.slp) loading is
                # supported via load_slp/load_file.
                raise NotImplementedError(
                    "Loading videos directly from Google Drive URLs is not "
                    "supported (Drive download links carry no file extension and "
                    "reject the range requests video decoding needs). Download "
                    "the video file first, or load Drive .slp label files with "
                    f"load_slp/load_file. (URL: {_remote._redact_url(filename)})"
                )

        # Skip local-filesystem dir detection for URLs (``Path.is_dir`` on a URL
        # is meaningless and would just return False, but avoid the syscall).
        if type(filename) is str and not is_url and Path(filename).is_dir():
            filename = ImageVideo.find_images(filename)

        # Match extensions against the URL *path* (query/fragment stripped) for
        # URLs, and the lowercased filename otherwise.
        ext_token = _extension_token(filename) if type(filename) is str else ""

        if type(filename) is list:
            filename = [Path(f).as_posix() for f in filename]
            return ImageVideo(
                filename, grayscale=grayscale, **_get_valid_kwargs(ImageVideo, kwargs)
            )
        elif ext_token.endswith(("tif", "tiff")):
            # Detect TIFF format
            format_type, metadata = TiffVideo.detect_format(filename)

            if format_type in ("multi_page", "rank3_video", "rank4_video"):
                # Use TiffVideo for multi-page or multi-dimensional TIFFs
                tiff_kwargs = _get_valid_kwargs(TiffVideo, kwargs)
                # Add format if detected
                if format_type in ("rank3_video", "rank4_video"):
                    tiff_kwargs["format"] = metadata.get("format")
                return TiffVideo(
                    filename,
                    grayscale=grayscale,
                    keep_open=keep_open,
                    **tiff_kwargs,
                )
            else:
                # Single-page TIFF, treat as regular image
                return ImageVideo(
                    [filename],
                    grayscale=grayscale,
                    **_get_valid_kwargs(ImageVideo, kwargs),
                )
        elif ext_token.endswith(tuple(ext.lower() for ext in ImageVideo.EXTS)):
            return ImageVideo(
                [filename], grayscale=grayscale, **_get_valid_kwargs(ImageVideo, kwargs)
            )
        elif ext_token.endswith(".seq"):
            from sleap_io.io.seq import SeqVideo

            return SeqVideo(
                filename,
                grayscale=grayscale,
                keep_open=keep_open,
                **_get_valid_kwargs(SeqVideo, kwargs),
            )
        elif ext_token.endswith(tuple(ext.lower() for ext in MediaVideo.EXTS)):
            media_kwargs = _get_valid_kwargs(MediaVideo, kwargs)
            if is_url:
                # Remote media videos are read via pyav (imageio's pyav plugin
                # forwards http(s) URIs to ``av.open`` natively). Enforce the
                # documented contract that only http/https URLs are supported:
                # cloud schemes (s3/gs/gcs/az/abfs) are recognized as remote by
                # ``_is_url`` but are not safe to hand to ``av.open``, so reject
                # them cleanly here rather than letting the raw URL reach the
                # decoder.
                scheme = urllib.parse.urlparse(filename).scheme.lower()
                if scheme not in ("http", "https"):
                    raise NotImplementedError(
                        "Remote video loading only supports http/https URLs; "
                        f"got scheme '{scheme}' for "
                        f"{_remote._redact_url(filename)}. Download the file "
                        "locally first."
                    )
                # Remote media video is decoded by handing the raw URL to
                # ``av.open`` (via imageio's pyav plugin), which has no hook for
                # forwarding HTTP request headers or selecting an fsspec stream
                # mode. Auth/streaming kwargs that work for remote .slp/.pkg.slp
                # (HDF5Video) therefore cannot be honored here. Rather than
                # silently drop them and return an unauthenticated backend,
                # reject them with an actionable error. ``url_headers`` /
                # ``url_stream_mode`` are the explicit ``from_filename``
                # parameters; ``headers`` / ``stream_mode`` arrive via
                # ``**kwargs`` (e.g. from ``load_video(url, headers=...)``).
                if (
                    url_headers is not None
                    or url_stream_mode != "blockcache"
                    or kwargs.get("headers") is not None
                    or kwargs.get("stream_mode") not in (None, "auto")
                ):
                    raise ValueError(
                        "Remote media video cannot be authenticated with "
                        "'headers'/'url_headers' or configured with a stream "
                        "mode: it is decoded by handing the URL directly to "
                        "FFmpeg (via pyav), which does not support custom HTTP "
                        "headers or fsspec streaming. Use a pre-signed URL that "
                        "embeds credentials in the query string, or download "
                        "the file locally first. (These options do work for "
                        "remote .slp/.pkg.slp labels.) (URL: "
                        f"{_remote._redact_url(filename)})"
                    )
                # Default to pyav when the caller did not request a specific
                # plugin, and require the ``av`` package up front for a clear
                # error.
                if media_kwargs.get("plugin") is None:
                    media_kwargs["plugin"] = "pyav"
                if (
                    normalize_plugin_name(media_kwargs["plugin"]) == "pyav"
                    and not _is_pyav_available()
                ):
                    # Defensive: ``av`` is required for remote loading and is
                    # always present in the test/CI environment, so this guard
                    # only fires for an install lacking the ``[pyav]`` extra.
                    raise ImportError(  # pragma: no cover
                        "Loading videos from URLs requires the 'av' package "
                        "(pyav). Install with: pip install 'sleap-io[pyav]'. "
                        f"(URL: {_remote._redact_url(filename)})"
                    )
            return MediaVideo(
                filename,
                grayscale=grayscale,
                keep_open=keep_open,
                **media_kwargs,
            )
        elif ext_token.endswith(tuple(ext.lower() for ext in HDF5Video.EXTS)):
            # Pass ``url_headers`` / ``url_stream_mode`` explicitly (not via
            # ``_get_valid_kwargs``, which keys on the underscored field *name*
            # and would drop the alias) so the construction-time probe in
            # ``HDF5Video.__attrs_post_init__`` is authenticated for remote URLs.
            return HDF5Video(
                filename,
                dataset=dataset,
                grayscale=grayscale,
                keep_open=keep_open,
                url_headers=url_headers,
                url_stream_mode=url_stream_mode,
                **_get_valid_kwargs(HDF5Video, kwargs),
            )
        else:
            raise ValueError(f"Unknown video file type: {filename}")

    def _read_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the video. Must be implemented in subclasses."""
        raise NotImplementedError

    def _read_frames(self, frame_inds: list) -> np.ndarray:
        """Read a list of frames from the video."""
        return np.stack([self.get_frame(i) for i in frame_inds], axis=0)

    def read_test_frame(self) -> np.ndarray:
        """Read a single frame from the video to test for grayscale.

        Note:
            This reads the frame at index 0. This may not be appropriate if the first
            frame is not available in a given backend.
        """
        return self._read_frame(0)

    def detect_grayscale(self, test_img: np.ndarray | None = None) -> bool:
        """Detect whether the video is grayscale.

        This works by reading in a test frame and comparing the first and last channel
        for equality. It may fail in cases where, due to compression, the first and
        last channels are not exactly the same.

        Args:
            test_img: Optional test image to use. If not provided, a test image will be
                loaded via the `read_test_frame` method.

        Returns:
            Whether the video is grayscale. This value is also cached in the `grayscale`
            attribute of the class.
        """
        if test_img is None:
            test_img = self.read_test_frame()
        is_grayscale = np.array_equal(test_img[..., 0], test_img[..., -1])
        self.grayscale = is_grayscale
        return is_grayscale

    @property
    def num_frames(self) -> int:
        """Number of frames in the video. Must be implemented in subclasses."""
        raise NotImplementedError

    @property
    def img_shape(self) -> tuple[int, int, int]:
        """Shape of a single frame in the video."""
        height, width, channels = self.read_test_frame().shape
        if self.grayscale is None:
            self.detect_grayscale()
        if self.grayscale is False:
            channels = 3
        elif self.grayscale is True:
            channels = 1
        return int(height), int(width), int(channels)

    @property
    def shape(self) -> tuple[int, int, int, int]:
        """Shape of the video as a tuple of `(frames, height, width, channels)`.

        On first call, this will defer to `num_frames` and `img_shape` to determine the
        full shape. This call may be expensive for some subclasses, so the result is
        cached and returned on subsequent calls.
        """
        if self._cached_shape is not None:
            return self._cached_shape
        else:
            shape = (self.num_frames,) + self.img_shape
            self._cached_shape = shape
            return shape

    @property
    def frames(self) -> int:
        """Number of frames in the video."""
        return self.shape[0]

    def __len__(self) -> int:
        """Return number of frames in the video."""
        return self.shape[0]

    def has_frame(self, frame_idx: int) -> bool:
        """Check if a frame index is contained in the video.

        Args:
            frame_idx: Index of frame to check.

        Returns:
            `True` if the index is contained in the video, otherwise `False`.
        """
        return frame_idx < len(self)

    def get_frame(self, frame_idx: int) -> np.ndarray:
        """Read a single frame from the video.

        Args:
            frame_idx: Index of frame to read.

        Returns:
            Frame as a numpy array of shape `(height, width, channels)` where the
            `channels` dimension is 1 for grayscale videos and 3 for color videos.

        Notes:
            If the `grayscale` attribute is set to `True`, the `channels` dimension will
            be reduced to 1 if an RGB frame is loaded from the backend.

            If the `grayscale` attribute is set to `None`, the `grayscale` attribute
            will be automatically set based on the first frame read.

        See also: `get_frames`
        """
        if not self.has_frame(frame_idx):
            raise IndexError(f"Frame index {frame_idx} out of range.")

        img = self._read_frame(frame_idx)

        if self.grayscale is None:
            self.detect_grayscale(img)

        if self.grayscale:
            img = img[..., [0]]

        return img

    def get_frames(self, frame_inds: list[int]) -> np.ndarray:
        """Read a list of frames from the video.

        Depending on the backend implementation, this may be faster than reading frames
        individually using `get_frame`.

        Args:
            frame_inds: List of frame indices to read.

        Returns:
            Frames as a numpy array of shape `(frames, height, width, channels)` where
            `channels` dimension is 1 for grayscale videos and 3 for color videos.

        Notes:
            If the `grayscale` attribute is set to `True`, the `channels` dimension will
            be reduced to 1 if an RGB frame is loaded from the backend.

            If the `grayscale` attribute is set to `None`, the `grayscale` attribute
            will be automatically set based on the first frame read.

        See also: `get_frame`
        """
        imgs = self._read_frames(frame_inds)

        if self.grayscale is None:
            self.detect_grayscale(imgs[0])

        if self.grayscale:
            imgs = imgs[..., [0]]

        return imgs

    def __getitem__(self, ind: int | list[int] | slice) -> np.ndarray:
        """Return a single frame or a list of frames from the video.

        Args:
            ind: Index or list of indices of frames to read.

        Returns:
            Frame or frames as a numpy array of shape `(height, width, channels)` if a
            scalar index is provided, or `(frames, height, width, channels)` if a list
            of indices is provided.

        See also: get_frame, get_frames
        """
        if np.isscalar(ind):
            return self.get_frame(ind)
        else:
            if type(ind) is slice:
                start = (ind.start or 0) % len(self)
                stop = ind.stop or len(self)
                if stop < 0:
                    stop = len(self) + stop
                step = ind.step or 1
                ind = range(start, stop, step)
            return self.get_frames(ind)

__annotations__ = {'filename': 'str | Path | list[str] | list[Path]', 'grayscale': 'bool | None', 'keep_open': 'bool', '_cached_shape': 'tuple[int, int, int, int] | None', '_open_reader': 'object | None', '_fps': 'float | 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=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=False, 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__ = 'Base class for video backends.\n\nThis class is not meant to be used directly. Instead, use the `from_filename`\nconstructor to create a backend instance.\n\nAttributes:\n filename: Path to video file(s).\n grayscale: Whether to force grayscale. If None, autodetect on first frame load.\n keep_open: Whether to keep the video reader open between calls to read frames.\n If False, will close the reader after each call. If True (the default), it\n will keep the reader open and cache it for subsequent calls which may\n enhance the performance of reading multiple frames.\n fps: Frames per second of the video. For MediaVideo, this is read from container\n metadata. For other backends (ImageVideo, HDF5Video, TiffVideo), this must\n be set explicitly or will be 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__ = 365 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps') 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.io.video_reading' 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__ = ('filename', 'grayscale', 'keep_open', '_cached_shape', '_open_reader', '_fps', '__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__ = ('_cached_shape', '_fps', '_open_reader', 'grayscale') 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

fps property

Frames per second of the video.

Returns:

Type Description

The FPS if known, or None if unavailable/unknown.

Notes

For MediaVideo, this is read from container metadata. For ImageVideo, HDF5Video, and TiffVideo, this must be set explicitly or inherited from source_video.

frames property

Number of frames in the video.

img_shape property

Shape of a single frame in the video.

num_frames property

Number of frames in the video. Must be implemented in subclasses.

shape property

Shape of the video as a tuple of (frames, height, width, channels).

On first call, this will defer to num_frames and img_shape to determine the full shape. This call may be expensive for some subclasses, so the result is cached and returned on subsequent calls.

__eq__(other)

Method generated by attrs for class VideoBackend.

Source code in sleap_io/io/video_reading.py
from sleap_io.io import _remote
from sleap_io.transform.frame import crop_frame
from sleap_io.transform.points import crop_points, uncrop_points

try:
    import cv2
except ImportError:
    pass

try:
    import imageio_ffmpeg  # noqa: F401

__getitem__(ind)

Return a single frame or a list of frames from the video.

Parameters:

Name Type Description Default
ind int | list[int] | slice

Index or list of indices of frames to read.

required

Returns:

Type Description
ndarray

Frame or frames as a numpy array of shape (height, width, channels) if a scalar index is provided, or (frames, height, width, channels) if a list of indices is provided.

See also: get_frame, get_frames

Source code in sleap_io/io/video_reading.py
def __getitem__(self, ind: int | list[int] | slice) -> np.ndarray:
    """Return a single frame or a list of frames from the video.

    Args:
        ind: Index or list of indices of frames to read.

    Returns:
        Frame or frames as a numpy array of shape `(height, width, channels)` if a
        scalar index is provided, or `(frames, height, width, channels)` if a list
        of indices is provided.

    See also: get_frame, get_frames
    """
    if np.isscalar(ind):
        return self.get_frame(ind)
    else:
        if type(ind) is slice:
            start = (ind.start or 0) % len(self)
            stop = ind.stop or len(self)
            if stop < 0:
                stop = len(self) + stop
            step = ind.step or 1
            ind = range(start, stop, step)
        return self.get_frames(ind)

__getstate__()

Return state for pickling/deepcopy, dropping the open reader handle.

The cached _open_reader (e.g. an h5py.File or video container) is not picklable and is reopened lazily on next access, so it is excluded.

Source code in sleap_io/io/video_reading.py
def __getstate__(self) -> dict:
    """Return state for pickling/deepcopy, dropping the open reader handle.

    The cached ``_open_reader`` (e.g. an ``h5py.File`` or video container) is
    not picklable and is reopened lazily on next access, so it is excluded.
    """
    import attr

    state = {a.name: getattr(self, a.name) for a in attr.fields(type(self))}
    state["_open_reader"] = None
    return state

__init__(filename, grayscale=None, keep_open=True, cached_shape=None, open_reader=None, fps=None)

Method generated by attrs for class VideoBackend.

Source code in sleap_io/io/video_reading.py
except ImportError:
    pass

try:
    import av  # noqa: F401
except ImportError:
    pass

__len__()

Return number of frames in the video.

Source code in sleap_io/io/video_reading.py
def __len__(self) -> int:
    """Return number of frames in the video."""
    return self.shape[0]

__repr__()

Method generated by attrs for class VideoBackend.

Source code in sleap_io/io/video_reading.py
"""Backends for reading videos."""

from __future__ import annotations

import sys
import urllib.parse
from io import BytesIO
from pathlib import Path

import attrs
import h5py
import imageio.v3 as iio
import numpy as np
import simplejson as json

__setstate__(state)

Restore state from pickling/deepcopy.

attrs slotted classes need object.__setattr__ to set slots directly. Validators are skipped, which is safe since state came from a valid object.

Source code in sleap_io/io/video_reading.py
def __setstate__(self, state: dict) -> None:
    """Restore state from pickling/deepcopy.

    attrs slotted classes need ``object.__setattr__`` to set slots directly.
    Validators are skipped, which is safe since state came from a valid object.
    """
    for key, value in state.items():
        object.__setattr__(self, key, value)

close()

Release the cached open reader handle, if any.

Closes (.close()) or releases (.release() for an OpenCV VideoCapture) the cached _open_reader and drops the reference so a long-lived backend does not leak the underlying file/container handle. The reader is lazily reopened on the next read, so this is safe to call between reads. A no-op when nothing is cached. Subclasses that hold additional handles (e.g. :class:HDF5Video's URL file-like) override this and call super().close().

Source code in sleap_io/io/video_reading.py
def close(self) -> None:
    """Release the cached open reader handle, if any.

    Closes (``.close()``) or releases (``.release()`` for an OpenCV
    ``VideoCapture``) the cached ``_open_reader`` and drops the reference so
    a long-lived backend does not leak the underlying file/container handle.
    The reader is lazily reopened on the next read, so this is safe to call
    between reads. A no-op when nothing is cached. Subclasses that hold
    additional handles (e.g. :class:`HDF5Video`'s URL file-like) override
    this and call ``super().close()``.
    """
    reader = self._open_reader
    self._open_reader = None
    if reader is None:
        return
    # Every real reader is an h5py.File / imageio reader (``.close()``) or an
    # OpenCV VideoCapture (``.release()``); the None case is purely defensive.
    closer = getattr(reader, "close", None) or getattr(reader, "release", None)
    if closer is None:  # pragma: no cover - defensive: reader always closeable
        return
    try:
        closer()
    except Exception:  # pragma: no cover - defensive: close should not raise
        pass

detect_grayscale(test_img=None)

Detect whether the video is grayscale.

This works by reading in a test frame and comparing the first and last channel for equality. It may fail in cases where, due to compression, the first and last channels are not exactly the same.

Parameters:

Name Type Description Default
test_img ndarray | None

Optional test image to use. If not provided, a test image will be loaded via the read_test_frame method.

None

Returns:

Type Description
bool

Whether the video is grayscale. This value is also cached in the grayscale attribute of the class.

Source code in sleap_io/io/video_reading.py
def detect_grayscale(self, test_img: np.ndarray | None = None) -> bool:
    """Detect whether the video is grayscale.

    This works by reading in a test frame and comparing the first and last channel
    for equality. It may fail in cases where, due to compression, the first and
    last channels are not exactly the same.

    Args:
        test_img: Optional test image to use. If not provided, a test image will be
            loaded via the `read_test_frame` method.

    Returns:
        Whether the video is grayscale. This value is also cached in the `grayscale`
        attribute of the class.
    """
    if test_img is None:
        test_img = self.read_test_frame()
    is_grayscale = np.array_equal(test_img[..., 0], test_img[..., -1])
    self.grayscale = is_grayscale
    return is_grayscale

from_filename(filename, dataset=None, grayscale=None, keep_open=True, url_headers=None, url_stream_mode='blockcache', **kwargs) classmethod

Create a VideoBackend from a filename.

Parameters:

Name Type Description Default
filename str | list[str]

Path to video file(s).

required
dataset str | None

Name of dataset in HDF5 file.

None
grayscale bool | None

Whether to force grayscale. If None, autodetect on first frame load.

None
keep_open bool

Whether to keep the video reader open between calls to read frames. If False, will close the reader after each call. If True (the default), it will keep the reader open and cache it for subsequent calls which may enhance the performance of reading multiple frames.

True
url_headers dict[str, str] | None

HTTP headers forwarded to the remote backend when filename is a URL (HDF5Video only). Set at construction so the metadata probe is authenticated; ignored for local files and other backends.

None
url_stream_mode str

Remote streaming strategy for a URL-backed HDF5Video (one of "blockcache"/"cache"/"filecache"/"download"). Ignored for local files and other backends.

'blockcache'
**kwargs

Additional backend-specific arguments. These are filtered to only include parameters that are valid for the specific backend being created: - For ImageVideo: plugin (str): Image plugin to use. One of "opencv" or "imageio". Also accepts aliases (case-insensitive). If None, uses global default if set, otherwise auto-detects. - For MediaVideo: plugin (str): Video plugin to use. One of "opencv", "FFMPEG", or "pyav". Also accepts aliases (case-insensitive). If None, uses global default if set, otherwise auto-detects. - For HDF5Video: input_format (str), frame_map (dict), source_filename (str), source_inds (np.ndarray), image_format (str). See HDF5Video for details.

required

Returns:

Type Description
VideoBackend

VideoBackend subclass instance.

Source code in sleap_io/io/video_reading.py
@classmethod
def from_filename(
    cls,
    filename: str | list[str],
    dataset: str | None = None,
    grayscale: bool | None = None,
    keep_open: bool = True,
    url_headers: dict[str, str] | None = None,
    url_stream_mode: str = "blockcache",
    **kwargs,
) -> "VideoBackend":
    """Create a VideoBackend from a filename.

    Args:
        filename: Path to video file(s).
        dataset: Name of dataset in HDF5 file.
        grayscale: Whether to force grayscale. If None, autodetect on first frame
            load.
        keep_open: Whether to keep the video reader open between calls to read
            frames. If False, will close the reader after each call. If True (the
            default), it will keep the reader open and cache it for subsequent calls
            which may enhance the performance of reading multiple frames.
        url_headers: HTTP headers forwarded to the remote backend when
            ``filename`` is a URL (HDF5Video only). Set at construction so the
            metadata probe is authenticated; ignored for local files and other
            backends.
        url_stream_mode: Remote streaming strategy for a URL-backed HDF5Video
            (one of ``"blockcache"``/``"cache"``/``"filecache"``/``"download"``).
            Ignored for local files and other backends.
        **kwargs: Additional backend-specific arguments. These are filtered to only
            include parameters that are valid for the specific backend being
            created:
            - For ImageVideo: plugin (str): Image plugin to use. One of "opencv"
              or "imageio". Also accepts aliases (case-insensitive).
              If None, uses global default if set, otherwise auto-detects.
            - For MediaVideo: plugin (str): Video plugin to use. One of "opencv",
              "FFMPEG", or "pyav". Also accepts aliases (case-insensitive).
              If None, uses global default if set, otherwise auto-detects.
            - For HDF5Video: input_format (str), frame_map (dict),
              source_filename (str),
              source_inds (np.ndarray), image_format (str). See HDF5Video for
              details.

    Returns:
        VideoBackend subclass instance.
    """
    if isinstance(filename, Path):
        filename = filename.as_posix()

    is_url = type(filename) is str and _remote._is_url(filename)

    if is_url:
        from sleap_io.io._gdrive import _is_gdrive_url

        if _is_gdrive_url(filename):
            # Drive download URLs carry no extension and Drive rejects the
            # range/HEAD requests video decoding relies on, so streaming a
            # Drive video is not supported. Drive *labels* (.slp) loading is
            # supported via load_slp/load_file.
            raise NotImplementedError(
                "Loading videos directly from Google Drive URLs is not "
                "supported (Drive download links carry no file extension and "
                "reject the range requests video decoding needs). Download "
                "the video file first, or load Drive .slp label files with "
                f"load_slp/load_file. (URL: {_remote._redact_url(filename)})"
            )

    # Skip local-filesystem dir detection for URLs (``Path.is_dir`` on a URL
    # is meaningless and would just return False, but avoid the syscall).
    if type(filename) is str and not is_url and Path(filename).is_dir():
        filename = ImageVideo.find_images(filename)

    # Match extensions against the URL *path* (query/fragment stripped) for
    # URLs, and the lowercased filename otherwise.
    ext_token = _extension_token(filename) if type(filename) is str else ""

    if type(filename) is list:
        filename = [Path(f).as_posix() for f in filename]
        return ImageVideo(
            filename, grayscale=grayscale, **_get_valid_kwargs(ImageVideo, kwargs)
        )
    elif ext_token.endswith(("tif", "tiff")):
        # Detect TIFF format
        format_type, metadata = TiffVideo.detect_format(filename)

        if format_type in ("multi_page", "rank3_video", "rank4_video"):
            # Use TiffVideo for multi-page or multi-dimensional TIFFs
            tiff_kwargs = _get_valid_kwargs(TiffVideo, kwargs)
            # Add format if detected
            if format_type in ("rank3_video", "rank4_video"):
                tiff_kwargs["format"] = metadata.get("format")
            return TiffVideo(
                filename,
                grayscale=grayscale,
                keep_open=keep_open,
                **tiff_kwargs,
            )
        else:
            # Single-page TIFF, treat as regular image
            return ImageVideo(
                [filename],
                grayscale=grayscale,
                **_get_valid_kwargs(ImageVideo, kwargs),
            )
    elif ext_token.endswith(tuple(ext.lower() for ext in ImageVideo.EXTS)):
        return ImageVideo(
            [filename], grayscale=grayscale, **_get_valid_kwargs(ImageVideo, kwargs)
        )
    elif ext_token.endswith(".seq"):
        from sleap_io.io.seq import SeqVideo

        return SeqVideo(
            filename,
            grayscale=grayscale,
            keep_open=keep_open,
            **_get_valid_kwargs(SeqVideo, kwargs),
        )
    elif ext_token.endswith(tuple(ext.lower() for ext in MediaVideo.EXTS)):
        media_kwargs = _get_valid_kwargs(MediaVideo, kwargs)
        if is_url:
            # Remote media videos are read via pyav (imageio's pyav plugin
            # forwards http(s) URIs to ``av.open`` natively). Enforce the
            # documented contract that only http/https URLs are supported:
            # cloud schemes (s3/gs/gcs/az/abfs) are recognized as remote by
            # ``_is_url`` but are not safe to hand to ``av.open``, so reject
            # them cleanly here rather than letting the raw URL reach the
            # decoder.
            scheme = urllib.parse.urlparse(filename).scheme.lower()
            if scheme not in ("http", "https"):
                raise NotImplementedError(
                    "Remote video loading only supports http/https URLs; "
                    f"got scheme '{scheme}' for "
                    f"{_remote._redact_url(filename)}. Download the file "
                    "locally first."
                )
            # Remote media video is decoded by handing the raw URL to
            # ``av.open`` (via imageio's pyav plugin), which has no hook for
            # forwarding HTTP request headers or selecting an fsspec stream
            # mode. Auth/streaming kwargs that work for remote .slp/.pkg.slp
            # (HDF5Video) therefore cannot be honored here. Rather than
            # silently drop them and return an unauthenticated backend,
            # reject them with an actionable error. ``url_headers`` /
            # ``url_stream_mode`` are the explicit ``from_filename``
            # parameters; ``headers`` / ``stream_mode`` arrive via
            # ``**kwargs`` (e.g. from ``load_video(url, headers=...)``).
            if (
                url_headers is not None
                or url_stream_mode != "blockcache"
                or kwargs.get("headers") is not None
                or kwargs.get("stream_mode") not in (None, "auto")
            ):
                raise ValueError(
                    "Remote media video cannot be authenticated with "
                    "'headers'/'url_headers' or configured with a stream "
                    "mode: it is decoded by handing the URL directly to "
                    "FFmpeg (via pyav), which does not support custom HTTP "
                    "headers or fsspec streaming. Use a pre-signed URL that "
                    "embeds credentials in the query string, or download "
                    "the file locally first. (These options do work for "
                    "remote .slp/.pkg.slp labels.) (URL: "
                    f"{_remote._redact_url(filename)})"
                )
            # Default to pyav when the caller did not request a specific
            # plugin, and require the ``av`` package up front for a clear
            # error.
            if media_kwargs.get("plugin") is None:
                media_kwargs["plugin"] = "pyav"
            if (
                normalize_plugin_name(media_kwargs["plugin"]) == "pyav"
                and not _is_pyav_available()
            ):
                # Defensive: ``av`` is required for remote loading and is
                # always present in the test/CI environment, so this guard
                # only fires for an install lacking the ``[pyav]`` extra.
                raise ImportError(  # pragma: no cover
                    "Loading videos from URLs requires the 'av' package "
                    "(pyav). Install with: pip install 'sleap-io[pyav]'. "
                    f"(URL: {_remote._redact_url(filename)})"
                )
        return MediaVideo(
            filename,
            grayscale=grayscale,
            keep_open=keep_open,
            **media_kwargs,
        )
    elif ext_token.endswith(tuple(ext.lower() for ext in HDF5Video.EXTS)):
        # Pass ``url_headers`` / ``url_stream_mode`` explicitly (not via
        # ``_get_valid_kwargs``, which keys on the underscored field *name*
        # and would drop the alias) so the construction-time probe in
        # ``HDF5Video.__attrs_post_init__`` is authenticated for remote URLs.
        return HDF5Video(
            filename,
            dataset=dataset,
            grayscale=grayscale,
            keep_open=keep_open,
            url_headers=url_headers,
            url_stream_mode=url_stream_mode,
            **_get_valid_kwargs(HDF5Video, kwargs),
        )
    else:
        raise ValueError(f"Unknown video file type: {filename}")

get_frame(frame_idx)

Read a single frame from the video.

Parameters:

Name Type Description Default
frame_idx int

Index of frame to read.

required

Returns:

Type Description
ndarray

Frame as a numpy array of shape (height, width, channels) where the channels dimension is 1 for grayscale videos and 3 for color videos.

Notes

If the grayscale attribute is set to True, the channels dimension will be reduced to 1 if an RGB frame is loaded from the backend.

If the grayscale attribute is set to None, the grayscale attribute will be automatically set based on the first frame read.

See also: get_frames

Source code in sleap_io/io/video_reading.py
def get_frame(self, frame_idx: int) -> np.ndarray:
    """Read a single frame from the video.

    Args:
        frame_idx: Index of frame to read.

    Returns:
        Frame as a numpy array of shape `(height, width, channels)` where the
        `channels` dimension is 1 for grayscale videos and 3 for color videos.

    Notes:
        If the `grayscale` attribute is set to `True`, the `channels` dimension will
        be reduced to 1 if an RGB frame is loaded from the backend.

        If the `grayscale` attribute is set to `None`, the `grayscale` attribute
        will be automatically set based on the first frame read.

    See also: `get_frames`
    """
    if not self.has_frame(frame_idx):
        raise IndexError(f"Frame index {frame_idx} out of range.")

    img = self._read_frame(frame_idx)

    if self.grayscale is None:
        self.detect_grayscale(img)

    if self.grayscale:
        img = img[..., [0]]

    return img

get_frames(frame_inds)

Read a list of frames from the video.

Depending on the backend implementation, this may be faster than reading frames individually using get_frame.

Parameters:

Name Type Description Default
frame_inds list[int]

List of frame indices to read.

required

Returns:

Type Description
ndarray

Frames as a numpy array of shape (frames, height, width, channels) where channels dimension is 1 for grayscale videos and 3 for color videos.

Notes

If the grayscale attribute is set to True, the channels dimension will be reduced to 1 if an RGB frame is loaded from the backend.

If the grayscale attribute is set to None, the grayscale attribute will be automatically set based on the first frame read.

See also: get_frame

Source code in sleap_io/io/video_reading.py
def get_frames(self, frame_inds: list[int]) -> np.ndarray:
    """Read a list of frames from the video.

    Depending on the backend implementation, this may be faster than reading frames
    individually using `get_frame`.

    Args:
        frame_inds: List of frame indices to read.

    Returns:
        Frames as a numpy array of shape `(frames, height, width, channels)` where
        `channels` dimension is 1 for grayscale videos and 3 for color videos.

    Notes:
        If the `grayscale` attribute is set to `True`, the `channels` dimension will
        be reduced to 1 if an RGB frame is loaded from the backend.

        If the `grayscale` attribute is set to `None`, the `grayscale` attribute
        will be automatically set based on the first frame read.

    See also: `get_frame`
    """
    imgs = self._read_frames(frame_inds)

    if self.grayscale is None:
        self.detect_grayscale(imgs[0])

    if self.grayscale:
        imgs = imgs[..., [0]]

    return imgs

has_frame(frame_idx)

Check if a frame index is contained in the video.

Parameters:

Name Type Description Default
frame_idx int

Index of frame to check.

required

Returns:

Type Description
bool

True if the index is contained in the video, otherwise False.

Source code in sleap_io/io/video_reading.py
def has_frame(self, frame_idx: int) -> bool:
    """Check if a frame index is contained in the video.

    Args:
        frame_idx: Index of frame to check.

    Returns:
        `True` if the index is contained in the video, otherwise `False`.
    """
    return frame_idx < len(self)

read_test_frame()

Read a single frame from the video to test for grayscale.

Note

This reads the frame at index 0. This may not be appropriate if the first frame is not available in a given backend.

Source code in sleap_io/io/video_reading.py
def read_test_frame(self) -> np.ndarray:
    """Read a single frame from the video to test for grayscale.

    Note:
        This reads the frame at index 0. This may not be appropriate if the first
        frame is not available in a given backend.
    """
    return self._read_frame(0)

crop_frame(frame, crop, fill=0)

Crop a frame to the specified region.

If the crop region extends beyond the frame bounds, the out-of-bounds area is filled with the fill value.

Parameters:

Name Type Description Default
frame ndarray

Input frame as numpy array with shape (H, W) or (H, W, C).

required
crop tuple[int, int, int, int]

Crop region as (x1, y1, x2, y2) pixel coordinates.

required
fill tuple[int, ...] | int

Fill value for out-of-bounds regions.

0

Returns:

Type Description
ndarray

Cropped frame as numpy array.

Source code in sleap_io/transform/frame.py
def crop_frame(
    frame: np.ndarray,
    crop: tuple[int, int, int, int],
    fill: tuple[int, ...] | int = 0,
) -> np.ndarray:
    """Crop a frame to the specified region.

    If the crop region extends beyond the frame bounds, the out-of-bounds area
    is filled with the fill value.

    Args:
        frame: Input frame as numpy array with shape (H, W) or (H, W, C).
        crop: Crop region as (x1, y1, x2, y2) pixel coordinates.
        fill: Fill value for out-of-bounds regions.

    Returns:
        Cropped frame as numpy array.
    """
    x1, y1, x2, y2 = crop
    h, w = frame.shape[:2]
    crop_w, crop_h = x2 - x1, y2 - y1

    # Compute valid source region. Clamp the upper bounds to the lower bounds so a
    # crop that lies wholly beyond the frame on an axis yields an empty (not
    # negative-extent) source slice, which pastes cleanly into an all-fill output
    # instead of raising a broadcast error.
    src_x1 = max(0, x1)
    src_y1 = max(0, y1)
    src_x2 = max(src_x1, min(w, x2))
    src_y2 = max(src_y1, min(h, y2))

    # Extract source region
    cropped = frame[src_y1:src_y2, src_x1:src_x2]

    # Check if padding is needed
    if x1 < 0 or y1 < 0 or x2 > w or y2 > h:
        # Create output array with fill value
        if frame.ndim == 3:
            output_shape = (crop_h, crop_w, frame.shape[2])
        else:
            output_shape = (crop_h, crop_w)

        output = np.full(output_shape, fill, dtype=frame.dtype)

        # Compute paste region
        paste_x1 = src_x1 - x1
        paste_y1 = src_y1 - y1
        paste_x2 = paste_x1 + (src_x2 - src_x1)
        paste_y2 = paste_y1 + (src_y2 - src_y1)

        output[paste_y1:paste_y2, paste_x1:paste_x2] = cropped
        return output

    return cropped

crop_points(points, crop)

Adjust point coordinates for a crop transformation.

Parameters:

Name Type Description Default
points ndarray

Coordinate array of shape (..., 2) where the last dimension contains (x, y) coordinates. NaN values are preserved.

required
crop tuple[int, int, int, int]

Crop region as (x1, y1, x2, y2) pixel coordinates.

required

Returns:

Type Description
ndarray

Adjusted coordinates with same shape as input.

Source code in sleap_io/transform/points.py
def crop_points(
    points: np.ndarray,
    crop: tuple[int, int, int, int],
) -> np.ndarray:
    """Adjust point coordinates for a crop transformation.

    Args:
        points: Coordinate array of shape (..., 2) where the last dimension
            contains (x, y) coordinates. NaN values are preserved.
        crop: Crop region as (x1, y1, x2, y2) pixel coordinates.

    Returns:
        Adjusted coordinates with same shape as input.
    """
    x1, y1, x2, y2 = crop
    result = points.copy()
    result[..., 0] = points[..., 0] - x1
    result[..., 1] = points[..., 1] - y1
    return result

get_available_image_backends()

Get list of available image backend plugins.

Returns:

Type Description
list[str]

List of plugin names that are currently available. Will always include "imageio" (core dependency), and may include "opencv" if installed.

Examples:

>>> import sleap_io as sio
>>> sio.get_available_image_backends()
['imageio']
>>> 'opencv' in sio.get_available_image_backends()
False
Source code in sleap_io/io/video_reading.py
def get_available_image_backends() -> list[str]:
    """Get list of available image backend plugins.

    Returns:
        List of plugin names that are currently available. Will always include
        "imageio" (core dependency), and may include "opencv" if installed.

    Examples:
        >>> import sleap_io as sio
        >>> sio.get_available_image_backends()
        ['imageio']
        >>> 'opencv' in sio.get_available_image_backends()
        False
    """
    return [k for k, v in _AVAILABLE_IMAGE_BACKENDS.items() if v]

get_available_video_backends()

Get list of available video backend plugins.

Returns:

Type Description
list[str]

List of plugin names that are currently available. Possible values include "opencv", "FFMPEG", and "pyav".

Examples:

>>> import sleap_io as sio
>>> sio.get_available_video_backends()
['FFMPEG', 'pyav']
>>> 'opencv' in sio.get_available_video_backends()
False
Source code in sleap_io/io/video_reading.py
def get_available_video_backends() -> list[str]:
    """Get list of available video backend plugins.

    Returns:
        List of plugin names that are currently available. Possible values include
        "opencv", "FFMPEG", and "pyav".

    Examples:
        >>> import sleap_io as sio
        >>> sio.get_available_video_backends()
        ['FFMPEG', 'pyav']
        >>> 'opencv' in sio.get_available_video_backends()
        False
    """
    return [k for k, v in _AVAILABLE_VIDEO_BACKENDS.items() if v]

get_default_image_plugin()

Get the current default image plugin.

Returns:

Type Description
str | None

The current default image plugin name ("opencv" or "imageio"), or None.

Examples:

>>> import sleap_io as sio
>>> sio.get_default_image_plugin()
None
>>> sio.set_default_image_plugin("opencv")
>>> sio.get_default_image_plugin()
'opencv'
Source code in sleap_io/io/video_reading.py
def get_default_image_plugin() -> str | None:
    """Get the current default image plugin.

    Returns:
        The current default image plugin name ("opencv" or "imageio"), or None.

    Examples:
        >>> import sleap_io as sio
        >>> sio.get_default_image_plugin()
        None
        >>> sio.set_default_image_plugin("opencv")
        >>> sio.get_default_image_plugin()
        'opencv'
    """
    return _default_image_plugin

get_default_video_plugin()

Get the current default video plugin.

Returns:

Type Description
str | None

The current default video plugin name, or None if not set.

Examples:

>>> import sleap_io as sio
>>> sio.get_default_video_plugin()
None
>>> sio.set_default_video_plugin("opencv")
>>> sio.get_default_video_plugin()
'opencv'
Source code in sleap_io/io/video_reading.py
def get_default_video_plugin() -> str | None:
    """Get the current default video plugin.

    Returns:
        The current default video plugin name, or None if not set.

    Examples:
        >>> import sleap_io as sio
        >>> sio.get_default_video_plugin()
        None
        >>> sio.set_default_video_plugin("opencv")
        >>> sio.get_default_video_plugin()
        'opencv'
    """
    return _default_video_plugin

get_installation_instructions(plugin=None, backend_type='video')

Get installation instructions for backend plugins.

Parameters:

Name Type Description Default
plugin str | None

Specific plugin name (e.g., "opencv", "FFMPEG", "pyav"), or None to get instructions for all plugins. Case-insensitive, accepts aliases.

None
backend_type str

Either "video" or "image". Determines which backend type to provide instructions for.

'video'

Returns:

Type Description
str

Installation instructions as a formatted string.

Examples:

>>> import sleap_io as sio
>>> print(sio.get_installation_instructions("opencv"))
pip install sleap-io[opencv]
>>> print(sio.get_installation_instructions())
Video backend installation options:
  FFMPEG (bundled):        Included by default
  opencv (fastest):        pip install sleap-io[opencv]
  pyav (balanced):         pip install sleap-io[pyav]
Source code in sleap_io/io/video_reading.py
def get_installation_instructions(
    plugin: str | None = None, backend_type: str = "video"
) -> str:
    """Get installation instructions for backend plugins.

    Args:
        plugin: Specific plugin name (e.g., "opencv", "FFMPEG", "pyav"), or None to
            get instructions for all plugins. Case-insensitive, accepts aliases.
        backend_type: Either "video" or "image". Determines which backend type to
            provide instructions for.

    Returns:
        Installation instructions as a formatted string.

    Examples:
        >>> import sleap_io as sio
        >>> print(sio.get_installation_instructions("opencv"))
        pip install sleap-io[opencv]

        >>> print(sio.get_installation_instructions())
        Video backend installation options:
          FFMPEG (bundled):        Included by default
          opencv (fastest):        pip install sleap-io[opencv]
          pyav (balanced):         pip install sleap-io[pyav]
    """
    if backend_type == "video":
        instructions = {
            "opencv": "pip install sleap-io[opencv]",
            "FFMPEG": "Included by default (imageio-ffmpeg)",
            "pyav": "pip install sleap-io[pyav]",
        }

        if plugin is not None:
            plugin = normalize_plugin_name(plugin)
            return instructions.get(plugin, "pip install sleap-io[all]")
        else:
            return (
                "Video backend installation options:\n"
                "  FFMPEG (bundled):        Included by default\n"
                "  opencv (fastest):        pip install sleap-io[opencv]\n"
                "  pyav (balanced):         pip install sleap-io[pyav]"
            )
    else:
        instructions = {
            "opencv": "pip install sleap-io[opencv]",
            "imageio": "Already installed (core dependency)",
        }

        if plugin is not None:
            plugin = normalize_image_plugin_name(plugin)
            return instructions.get(plugin, "pip install sleap-io[all]")
        else:
            return (
                "Image backend installation options:\n"
                "  opencv: pip install sleap-io[opencv]\n"
                "  imageio: Already installed (core dependency)"
            )

normalize_image_plugin_name(plugin)

Normalize image plugin names to standard format.

Parameters:

Name Type Description Default
plugin str

Plugin name or alias (case-insensitive).

required

Returns:

Type Description
str

Normalized plugin name ("opencv" or "imageio").

Raises:

Type Description
ValueError

If plugin name is not recognized.

Source code in sleap_io/io/video_reading.py
def normalize_image_plugin_name(plugin: str) -> str:
    """Normalize image plugin names to standard format.

    Args:
        plugin: Plugin name or alias (case-insensitive).

    Returns:
        Normalized plugin name ("opencv" or "imageio").

    Raises:
        ValueError: If plugin name is not recognized.
    """
    plugin_lower = plugin.lower()

    # Map aliases to standard names (only opencv and imageio for images)
    aliases = {
        "cv": "opencv",
        "cv2": "opencv",
        "opencv": "opencv",
        "ocv": "opencv",
        "imageio": "imageio",
        "iio": "imageio",
    }

    if plugin_lower not in aliases:
        raise ValueError(
            f"Unknown image plugin: {plugin}. Valid options: opencv, imageio"
        )

    return aliases[plugin_lower]

normalize_plugin_name(plugin)

Normalize plugin names to standard format.

Parameters:

Name Type Description Default
plugin str

Plugin name or alias (case-insensitive).

required

Returns:

Type Description
str

Normalized plugin name ("opencv", "FFMPEG", or "pyav").

Raises:

Type Description
ValueError

If plugin name is not recognized.

Source code in sleap_io/io/video_reading.py
def normalize_plugin_name(plugin: str) -> str:
    """Normalize plugin names to standard format.

    Args:
        plugin: Plugin name or alias (case-insensitive).

    Returns:
        Normalized plugin name ("opencv", "FFMPEG", or "pyav").

    Raises:
        ValueError: If plugin name is not recognized.
    """
    plugin_lower = plugin.lower()

    # Map aliases to standard names
    aliases = {
        "cv": "opencv",
        "cv2": "opencv",
        "opencv": "opencv",
        "ocv": "opencv",
        "ffmpeg": "FFMPEG",
        "imageio-ffmpeg": "FFMPEG",
        "imageio_ffmpeg": "FFMPEG",
        "pyav": "pyav",
        "av": "pyav",
    }

    if plugin_lower not in aliases:
        raise ValueError(
            f"Unknown plugin: {plugin}. Valid options: opencv, FFMPEG, pyav"
        )

    return aliases[plugin_lower]

set_default_image_plugin(plugin)

Set the default image plugin for encoding/decoding embedded images.

Parameters:

Name Type Description Default
plugin str | None

Image plugin name. One of "opencv" or "imageio". Also accepts aliases: "cv", "cv2", "ocv" for opencv; "iio" for imageio. Case-insensitive. If None, clears the default preference.

required

Examples:

>>> import sleap_io as sio
>>> sio.set_default_image_plugin("opencv")
>>> sio.set_default_image_plugin("imageio")
>>> sio.set_default_image_plugin(None)  # Clear preference
Source code in sleap_io/io/video_reading.py
def set_default_image_plugin(plugin: str | None) -> None:
    """Set the default image plugin for encoding/decoding embedded images.

    Args:
        plugin: Image plugin name. One of "opencv" or "imageio".
            Also accepts aliases: "cv", "cv2", "ocv" for opencv;
            "iio" for imageio. Case-insensitive.
            If None, clears the default preference.

    Examples:
        >>> import sleap_io as sio
        >>> sio.set_default_image_plugin("opencv")
        >>> sio.set_default_image_plugin("imageio")
        >>> sio.set_default_image_plugin(None)  # Clear preference
    """
    global _default_image_plugin
    if plugin is not None:
        plugin = normalize_image_plugin_name(plugin)
    _default_image_plugin = plugin

set_default_video_plugin(plugin)

Set the default video plugin for all subsequently loaded videos.

Parameters:

Name Type Description Default
plugin str | None

Video plugin name. One of "opencv", "FFMPEG", or "pyav". Also accepts aliases: "cv", "cv2", "ocv" for opencv; "imageio-ffmpeg", "imageio_ffmpeg" for FFMPEG; "av" for pyav. Case-insensitive. If None, clears the default preference.

required

Examples:

>>> import sleap_io as sio
>>> sio.set_default_video_plugin("opencv")
>>> sio.set_default_video_plugin("cv2")  # Same as "opencv"
>>> sio.set_default_video_plugin(None)  # Clear preference
Source code in sleap_io/io/video_reading.py
def set_default_video_plugin(plugin: str | None) -> None:
    """Set the default video plugin for all subsequently loaded videos.

    Args:
        plugin: Video plugin name. One of "opencv", "FFMPEG", or "pyav".
            Also accepts aliases: "cv", "cv2", "ocv" for opencv;
            "imageio-ffmpeg", "imageio_ffmpeg" for FFMPEG; "av" for pyav.
            Case-insensitive. If None, clears the default preference.

    Examples:
        >>> import sleap_io as sio
        >>> sio.set_default_video_plugin("opencv")
        >>> sio.set_default_video_plugin("cv2")  # Same as "opencv"
        >>> sio.set_default_video_plugin(None)  # Clear preference
    """
    global _default_video_plugin
    if plugin is not None:
        plugin = normalize_plugin_name(plugin)
    _default_video_plugin = plugin

uncrop_points(points, crop)

Map crop-local point coordinates back to source coordinates.

Inverse of :func:crop_points: maps crop-local (x, y) coordinates back to source coordinates by adding the crop origin (x1, y1).

Parameters:

Name Type Description Default
points ndarray

Coordinate array of shape (..., 2) where the last dimension contains (x, y) coordinates. NaN values are preserved.

required
crop tuple[int, int, int, int]

Crop region as (x1, y1, x2, y2) pixel coordinates.

required

Returns:

Type Description
ndarray

Adjusted coordinates with same shape as input.

Source code in sleap_io/transform/points.py
def uncrop_points(
    points: np.ndarray,
    crop: tuple[int, int, int, int],
) -> np.ndarray:
    """Map crop-local point coordinates back to source coordinates.

    Inverse of :func:`crop_points`: maps crop-local (x, y) coordinates back to
    source coordinates by adding the crop origin (x1, y1).

    Args:
        points: Coordinate array of shape (..., 2) where the last dimension
            contains (x, y) coordinates. NaN values are preserved.
        crop: Crop region as (x1, y1, x2, y2) pixel coordinates.

    Returns:
        Adjusted coordinates with same shape as input.
    """
    x1, y1, x2, y2 = crop
    result = points.copy()
    result[..., 0] = points[..., 0] + x1
    result[..., 1] = points[..., 1] + y1
    return result