camera
sleap_io.model.camera
¶
Data structure for a single camera view in a multi-camera setup.
Classes:
| Name | Description |
|---|---|
Camera |
A camera used to record in a multi-view |
CameraGroup |
A group of cameras used to record a multi-view |
FrameGroup |
Defines a group of |
Instance |
This class represents a ground truth instance such as an animal. |
InstanceGroup |
Defines a group of instances across the same frame index. |
LabeledFrame |
Labeled data for a single frame of a video. |
RecordingSession |
A recording session with multiple cameras. |
Video |
|
Functions:
| Name | Description |
|---|---|
rodrigues_transformation |
Convert between rotation vector and rotation matrix using Rodrigues' formula. |
Attributes:
| Name | Type | Description |
|---|---|---|
__cached__ |
str(object='') -> str |
|
__doc__ |
str(object='') -> str |
|
__file__ |
str(object='') -> str |
|
__name__ |
str(object='') -> str |
|
__package__ |
str(object='') -> str |
__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/__pycache__/camera.cpython-313.pyc'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__doc__ = 'Data structure for a single camera view in a multi-camera setup.'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/camera.py'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__name__ = 'sleap_io.model.camera'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__package__ = 'sleap_io.model'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
Camera
¶
A camera used to record in a multi-view RecordingSession.
Attributes:
| Name | Type | Description |
|---|---|---|
matrix |
Intrinsic camera matrix of size (3, 3) and type float64. |
|
dist |
Radial-tangential distortion coefficients [k_1, k_2, p_1, p_2, k_3] of size (5,) and type float64. |
|
size |
Image size (width, height) of camera in pixels of size (2,) and type int. |
|
rvec |
Rotation vector in unnormalized axis-angle representation of size (3,) and type float64. |
|
tvec |
Translation vector of size (3,) and type float64. |
|
extrinsic_matrix |
Extrinsic matrix of camera of size (4, 4) and type float64. |
|
name |
Camera name. |
|
metadata |
Dictionary of metadata. |
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Initialize extrinsic matrix from rotation and translation vectors. |
__init__ |
Method generated by attrs for class Camera. |
__replace__ |
Method generated by attrs for class Camera. |
__repr__ |
Return a readable representation of the camera. |
__setattr__ |
Method generated by attrs for class Camera. |
get_video |
Get video associated with recording session. |
Source code in sleap_io/model/camera.py
@define(eq=False) # Set eq to false to make class hashable
class Camera:
"""A camera used to record in a multi-view `RecordingSession`.
Attributes:
matrix: Intrinsic camera matrix of size (3, 3) and type float64.
dist: Radial-tangential distortion coefficients [k_1, k_2, p_1, p_2, k_3] of
size (5,) and type float64.
size: Image size (width, height) of camera in pixels of size (2,) and type int.
rvec: Rotation vector in unnormalized axis-angle representation of size (3,) and
type float64.
tvec: Translation vector of size (3,) and type float64.
extrinsic_matrix: Extrinsic matrix of camera of size (4, 4) and type float64.
name: Camera name.
metadata: Dictionary of metadata.
"""
matrix: np.ndarray = field(
default=np.eye(3),
converter=lambda x: np.array(x, dtype="float64"),
)
dist: np.ndarray = field(
default=np.zeros(5), converter=lambda x: np.array(x, dtype="float64").ravel()
)
size: tuple[int, int] = field(
default=None, converter=attrs.converters.optional(tuple)
)
_rvec: np.ndarray = field(
default=np.zeros(3), converter=lambda x: np.array(x, dtype="float64").ravel()
)
_tvec: np.ndarray = field(
default=np.zeros(3), converter=lambda x: np.array(x, dtype="float64").ravel()
)
name: str = field(default=None, converter=attrs.converters.optional(str))
_extrinsic_matrix: np.ndarray = field(init=False)
metadata: dict = field(factory=dict, validator=instance_of(dict))
@matrix.validator
@dist.validator
@size.validator
@_rvec.validator
@_tvec.validator
@_extrinsic_matrix.validator
def _validate_shape(self, attribute: attrs.Attribute, value):
"""Validate shape of attribute based on metadata.
Args:
attribute: Attribute to validate.
value: Value of attribute to validate.
Raises:
ValueError: If attribute shape is not as expected.
"""
# Define metadata for each attribute
attr_metadata = {
"matrix": {"shape": (3, 3), "type": np.ndarray},
"dist": {"shape": (5,), "type": np.ndarray},
"size": {"shape": (2,), "type": tuple},
"_rvec": {"shape": (3,), "type": np.ndarray},
"_tvec": {"shape": (3,), "type": np.ndarray},
"_extrinsic_matrix": {"shape": (4, 4), "type": np.ndarray},
}
optional_attrs = ["size"]
# Skip validation if optional attribute is None
if attribute.name in optional_attrs and value is None:
return
# Validate shape of attribute
expected_shape = attr_metadata[attribute.name]["shape"]
expected_type = attr_metadata[attribute.name]["type"]
if np.shape(value) != expected_shape:
raise ValueError(
f"{attribute.name} must be a {expected_type} of size {expected_shape}, "
f"but received shape: {np.shape(value)} and type: {type(value)} for "
f"value: {value}"
)
def __attrs_post_init__(self):
"""Initialize extrinsic matrix from rotation and translation vectors."""
self._extrinsic_matrix = np.eye(4, dtype="float64")
self._extrinsic_matrix[:3, :3] = rodrigues_transformation(self._rvec)[0]
self._extrinsic_matrix[:3, 3] = self._tvec
@property
def rvec(self) -> np.ndarray:
"""Get rotation vector of camera.
Returns:
Rotation vector of camera of size 3.
"""
return self._rvec
@rvec.setter
def rvec(self, value: np.ndarray):
"""Set rotation vector and update extrinsic matrix.
Args:
value: Rotation vector of size 3.
"""
self._rvec = value
self._extrinsic_matrix[:3, :3] = rodrigues_transformation(self._rvec)[0]
@property
def tvec(self) -> np.ndarray:
"""Get translation vector of camera.
Returns:
Translation vector of camera of size 3.
"""
return self._tvec
@tvec.setter
def tvec(self, value: np.ndarray):
"""Set translation vector and update extrinsic matrix.
Args:
value: Translation vector of size 3.
"""
self._tvec = value
# Update extrinsic matrix
self._extrinsic_matrix[:3, 3] = self._tvec
@property
def extrinsic_matrix(self) -> np.ndarray:
"""Get extrinsic matrix of camera.
Returns:
Extrinsic matrix of camera of size 4 x 4.
"""
return self._extrinsic_matrix
@extrinsic_matrix.setter
def extrinsic_matrix(self, value: np.ndarray):
"""Set extrinsic matrix and update rotation and translation vectors.
Args:
value: Extrinsic matrix of size 4 x 4.
"""
self._extrinsic_matrix = value
# Update rotation and translation vectors
self._rvec = rodrigues_transformation(self._extrinsic_matrix[:3, :3])[0].ravel()
self._tvec = self._extrinsic_matrix[:3, 3]
def get_video(self, session: RecordingSession) -> Video | None:
"""Get video associated with recording session.
Args:
session: Recording session to get video for.
Returns:
Video associated with recording session or None if not found.
"""
return session.get_video(camera=self)
def __repr__(self) -> str:
"""Return a readable representation of the camera."""
matrix_str = (
"identity" if np.array_equal(self.matrix, np.eye(3)) else "non-identity"
)
dist_str = "zero" if np.array_equal(self.dist, np.zeros(5)) else "non-zero"
size_str = "None" if self.size is None else self.size
rvec_str = (
"zero"
if np.array_equal(self.rvec, np.zeros(3))
else np.array2string(self.rvec, precision=2, suppress_small=True)
)
tvec_str = (
"zero"
if np.array_equal(self.tvec, np.zeros(3))
else np.array2string(self.tvec, precision=2, suppress_small=True)
)
name_str = self.name if self.name is not None else "None"
return (
"Camera("
f"matrix={matrix_str}, "
f"dist={dist_str}, "
f"size={size_str}, "
f"rvec={rvec_str}, "
f"tvec={tvec_str}, "
f"name={name_str}"
")"
)
__annotations__ = {'matrix': 'np.ndarray', 'dist': 'np.ndarray', 'size': 'tuple[int, int]', '_rvec': 'np.ndarray', '_tvec': 'np.ndarray', 'name': 'str', '_extrinsic_matrix': 'np.ndarray', 'metadata': 'dict'}
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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A camera used to record in a multi-view `RecordingSession`.\n\nAttributes:\n matrix: Intrinsic camera matrix of size (3, 3) and type float64.\n dist: Radial-tangential distortion coefficients [k_1, k_2, p_1, p_2, k_3] of\n size (5,) and type float64.\n size: Image size (width, height) of camera in pixels of size (2,) and type int.\n rvec: Rotation vector in unnormalized axis-angle representation of size (3,) and\n type float64.\n tvec: Translation vector of size (3,) and type float64.\n extrinsic_matrix: Extrinsic matrix of camera of size (4, 4) and type float64.\n name: Camera name.\n metadata: Dictionary of metadata.\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__ = 255
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 literal.
int('0b100', base=0) 4
__match_args__ = ('matrix', 'dist', 'size', '_rvec', '_tvec', 'name', 'metadata')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.camera'
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__ = ('matrix', 'dist', 'size', '_rvec', '_tvec', 'name', '_extrinsic_matrix', 'metadata', '__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__ = ('_extrinsic_matrix', '_rvec', '_tvec')
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
extrinsic_matrix
property
¶
Get extrinsic matrix of camera.
Returns:
| Type | Description |
|---|---|
|
Extrinsic matrix of camera of size 4 x 4. |
rvec
property
¶
Get rotation vector of camera.
Returns:
| Type | Description |
|---|---|
|
Rotation vector of camera of size 3. |
tvec
property
¶
Get translation vector of camera.
Returns:
| Type | Description |
|---|---|
|
Translation vector of camera of size 3. |
__attrs_post_init__()
¶
Initialize extrinsic matrix from rotation and translation vectors.
Source code in sleap_io/model/camera.py
__init__(matrix=array([[1., 0., 0.],[0., 1., 0.],[0., 0., 1.]]), dist=array([0., 0., 0., 0., 0.]), size=None, rvec=array([0., 0., 0.]), tvec=array([0., 0., 0.]), name=None, metadata=NOTHING)
¶
Method generated by attrs for class Camera.
Source code in sleap_io/model/camera.py
"""Data structure for a single camera view in a multi-camera setup."""
from __future__ import annotations
import attrs
import numpy as np
from attrs import define, field
from attrs.validators import instance_of
from sleap_io.model.instance import Instance
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.video import Video
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between:
1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix
2. A 3x3 rotation matrix to a 3D rotation vector
__replace__(**changes)
¶
Method generated by attrs for class Camera.
__repr__()
¶
Return a readable representation of the camera.
Source code in sleap_io/model/camera.py
def __repr__(self) -> str:
"""Return a readable representation of the camera."""
matrix_str = (
"identity" if np.array_equal(self.matrix, np.eye(3)) else "non-identity"
)
dist_str = "zero" if np.array_equal(self.dist, np.zeros(5)) else "non-zero"
size_str = "None" if self.size is None else self.size
rvec_str = (
"zero"
if np.array_equal(self.rvec, np.zeros(3))
else np.array2string(self.rvec, precision=2, suppress_small=True)
)
tvec_str = (
"zero"
if np.array_equal(self.tvec, np.zeros(3))
else np.array2string(self.tvec, precision=2, suppress_small=True)
)
name_str = self.name if self.name is not None else "None"
return (
"Camera("
f"matrix={matrix_str}, "
f"dist={dist_str}, "
f"size={size_str}, "
f"rvec={rvec_str}, "
f"tvec={tvec_str}, "
f"name={name_str}"
")"
)
__setattr__(name, val)
¶
Method generated by attrs for class Camera.
get_video(session)
¶
Get video associated with recording session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
RecordingSession
|
Recording session to get video for. |
required |
Returns:
| Type | Description |
|---|---|
Video | None
|
Video associated with recording session or None if not found. |
Source code in sleap_io/model/camera.py
CameraGroup
¶
A group of cameras used to record a multi-view RecordingSession.
Attributes:
| Name | Type | Description |
|---|---|---|
cameras |
List of |
|
metadata |
Dictionary of metadata. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class CameraGroup. |
__init__ |
Method generated by attrs for class CameraGroup. |
__replace__ |
Method generated by attrs for class CameraGroup. |
__repr__ |
Return a readable representation of the camera group. |
__setattr__ |
Method generated by attrs for class CameraGroup. |
Source code in sleap_io/model/camera.py
@define
class CameraGroup:
"""A group of cameras used to record a multi-view `RecordingSession`.
Attributes:
cameras: List of `Camera` objects in the group.
metadata: Dictionary of metadata.
"""
cameras: "list[Camera]" = field(factory=list, validator=instance_of(list))
metadata: dict = field(factory=dict, validator=instance_of(dict))
def __repr__(self):
"""Return a readable representation of the camera group."""
camera_names = ", ".join([c.name or "None" for c in self.cameras])
return f"CameraGroup(cameras={len(self.cameras)}:[{camera_names}])"
__annotations__ = {'cameras': "'list[Camera]'", 'metadata': 'dict'}
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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A group of cameras used to record a multi-view `RecordingSession`.\n\nAttributes:\n cameras: List of `Camera` objects in the group.\n metadata: Dictionary of metadata.\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__ = 110
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 literal.
int('0b100', base=0) 4
__match_args__ = ('cameras', 'metadata')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.camera'
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__ = ('cameras', 'metadata', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
__eq__(other)
¶
__init__(cameras=NOTHING, metadata=NOTHING)
¶
Method generated by attrs for class CameraGroup.
Source code in sleap_io/model/camera.py
from attrs.validators import instance_of
from sleap_io.model.instance import Instance
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.video import Video
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between:
1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix
2. A 3x3 rotation matrix to a 3D rotation vector
__replace__(**changes)
¶
Method generated by attrs for class CameraGroup.
__repr__()
¶
Return a readable representation of the camera group.
__setattr__(name, val)
¶
Method generated by attrs for class CameraGroup.
FrameGroup
¶
Defines a group of InstanceGroups across views at the same frame index.
Attributes:
| Name | Type | Description |
|---|---|---|
frame_idx |
Frame index for the |
|
instance_groups |
List of |
|
cameras |
List of |
|
labeled_frames |
List of |
|
metadata |
Metadata for the |
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class FrameGroup. |
__replace__ |
Method generated by attrs for class FrameGroup. |
__repr__ |
Return a readable representation of the frame group. |
__setattr__ |
Method generated by attrs for class FrameGroup. |
get_frame |
Get |
Source code in sleap_io/model/camera.py
@define(eq=False) # Set eq to false to make class hashable
class FrameGroup:
"""Defines a group of `InstanceGroups` across views at the same frame index.
Attributes:
frame_idx: Frame index for the `FrameGroup`.
instance_groups: List of `InstanceGroup`s in the `FrameGroup`.
cameras: List of `Camera` objects linked to `LabeledFrame`s in the `FrameGroup`.
labeled_frames: List of `LabeledFrame`s in the `FrameGroup`.
metadata: Metadata for the `FrameGroup` that is provided but not deserialized.
"""
frame_idx: int = field(converter=int)
_instance_groups: list[InstanceGroup] = field(
factory=list, validator=instance_of(list)
)
_labeled_frame_by_camera: dict[Camera, LabeledFrame] = field(
factory=dict, validator=instance_of(dict)
)
metadata: dict = field(factory=dict, validator=instance_of(dict))
@property
def instance_groups(self) -> list[InstanceGroup]:
"""List of `InstanceGroup`s."""
return self._instance_groups
@property
def cameras(self) -> "list[Camera]":
"""List of `Camera` objects."""
return list(self._labeled_frame_by_camera.keys())
@property
def labeled_frames(self) -> list[LabeledFrame]:
"""List of `LabeledFrame`s."""
return list(self._labeled_frame_by_camera.values())
def get_frame(self, camera: Camera) -> LabeledFrame | None:
"""Get `LabeledFrame` associated with `camera`.
Args:
camera: `Camera` to get `LabeledFrame`.
Returns:
`LabeledFrame` associated with `camera` or None if not found.
"""
return self._labeled_frame_by_camera.get(camera, None)
def __repr__(self) -> str:
"""Return a readable representation of the frame group."""
cameras_str = ", ".join([c.name or "None" for c in self.cameras])
return (
f"FrameGroup("
f"frame_idx={self.frame_idx},"
f"instance_groups={len(self.instance_groups)},"
f"cameras={len(self.cameras)}:[{cameras_str}]"
f")"
)
__annotations__ = {'frame_idx': 'int', '_instance_groups': 'list[InstanceGroup]', '_labeled_frame_by_camera': 'dict[Camera, LabeledFrame]', 'metadata': 'dict'}
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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'Defines a group of `InstanceGroups` across views at the same frame index.\n\nAttributes:\n frame_idx: Frame index for the `FrameGroup`.\n instance_groups: List of `InstanceGroup`s in the `FrameGroup`.\n cameras: List of `Camera` objects linked to `LabeledFrame`s in the `FrameGroup`.\n labeled_frames: List of `LabeledFrame`s in the `FrameGroup`.\n metadata: Metadata for the `FrameGroup` that is provided but not deserialized.\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__ = 511
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 literal.
int('0b100', base=0) 4
__match_args__ = ('frame_idx', '_instance_groups', '_labeled_frame_by_camera', 'metadata')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.camera'
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__ = ('frame_idx', '_instance_groups', '_labeled_frame_by_camera', 'metadata', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
cameras
property
¶
List of Camera objects.
instance_groups
property
¶
List of InstanceGroups.
labeled_frames
property
¶
List of LabeledFrames.
__init__(frame_idx, instance_groups=NOTHING, labeled_frame_by_camera=NOTHING, metadata=NOTHING)
¶
Method generated by attrs for class FrameGroup.
Source code in sleap_io/model/camera.py
"""Data structure for a single camera view in a multi-camera setup."""
from __future__ import annotations
import attrs
import numpy as np
from attrs import define, field
from attrs.validators import instance_of
from sleap_io.model.instance import Instance
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.video import Video
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between:
1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix
__replace__(**changes)
¶
Method generated by attrs for class FrameGroup.
__repr__()
¶
Return a readable representation of the frame group.
Source code in sleap_io/model/camera.py
def __repr__(self) -> str:
"""Return a readable representation of the frame group."""
cameras_str = ", ".join([c.name or "None" for c in self.cameras])
return (
f"FrameGroup("
f"frame_idx={self.frame_idx},"
f"instance_groups={len(self.instance_groups)},"
f"cameras={len(self.cameras)}:[{cameras_str}]"
f")"
)
__setattr__(name, val)
¶
Method generated by attrs for class FrameGroup.
get_frame(camera)
¶
Get LabeledFrame associated with camera.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
camera
|
Camera
|
|
required |
Returns:
| Type | Description |
|---|---|
LabeledFrame | None
|
|
Source code in sleap_io/model/camera.py
Instance
¶
This class represents a ground truth instance such as an animal.
An Instance has a set of landmarks (points) that correspond to a Skeleton. Each
point is associated with a Node in the skeleton. The points are stored in a
structured numpy array with columns for x, y, visible, complete and name.
The Instance may also be associated with a Track which links multiple instances
together across frames or videos.
Attributes:
| Name | Type | Description |
|---|---|---|
points |
A numpy structured array with columns for xy, visible and complete. The
array should have shape |
|
skeleton |
The |
|
track |
An optional |
|
tracking_score |
The score associated with the |
|
from_predicted |
The |
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Convert the points array after initialization. |
__getitem__ |
Return the point associated with a node. |
__init__ |
Method generated by attrs for class Instance. |
__len__ |
Return the number of points in the instance. |
__replace__ |
Method generated by attrs for class Instance. |
__repr__ |
Return a readable representation of the instance. |
__setitem__ |
Set the point associated with a node. |
bounding_box |
Get the bounding box of visible points. |
empty |
Create an empty instance with no points. |
from_numpy |
Create an instance object from a numpy array. |
numpy |
Return the instance points as a |
overlaps_with |
Check if this instance overlaps with another based on bounding box IoU. |
replace_skeleton |
Replace the skeleton associated with the instance. |
same_identity_as |
Check if this instance has the same identity (track) as another instance. |
same_pose_as |
Check if this instance has the same pose as another instance. |
update_skeleton |
Update or replace the skeleton associated with the instance. |
Source code in sleap_io/model/instance.py
@attrs.define(auto_attribs=True, slots=True, eq=False)
class Instance:
"""This class represents a ground truth instance such as an animal.
An `Instance` has a set of landmarks (points) that correspond to a `Skeleton`. Each
point is associated with a `Node` in the skeleton. The points are stored in a
structured numpy array with columns for x, y, visible, complete and name.
The `Instance` may also be associated with a `Track` which links multiple instances
together across frames or videos.
Attributes:
points: A numpy structured array with columns for xy, visible and complete. The
array should have shape `(n_nodes,)`. This representation is useful for
performance efficiency when working with large datasets.
skeleton: The `Skeleton` that describes the `Node`s and `Edge`s associated with
this instance.
track: An optional `Track` associated with a unique animal/object across frames
or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity assignment.
This is `None` if the instance is not associated with a track or if the
track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
"""
points: PointsArray = attrs.field(eq=attrs.cmp_using(eq=np.array_equal))
skeleton: Skeleton
track: Track | None = None
tracking_score: float | None = None
from_predicted: "PredictedInstance | None" = None
@classmethod
def empty(
cls,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an empty instance with no points.
Args:
skeleton: The `Skeleton` that this `Instance` is associated with.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` with an empty numpy array of shape `(n_nodes,)`.
"""
points = PointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
@classmethod
def _convert_points(
cls, points_data: np.ndarray | dict | list, skeleton: Skeleton
) -> PointsArray:
"""Convert points to a structured numpy array if needed."""
if isinstance(points_data, dict):
return PointsArray.from_dict(points_data, skeleton)
elif isinstance(points_data, (list, np.ndarray)):
if isinstance(points_data, list):
points_data = np.array(points_data)
points = PointsArray.from_array(points_data)
points["name"] = skeleton.node_names
return points
else:
raise ValueError("points must be a numpy array or dictionary.")
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an instance object from a numpy array.
Args:
points_data: A numpy array of shape `(n_nodes, D)` corresponding to the
points of the skeleton. Values of `np.nan` indicate "missing" nodes and
will be reflected in the "visible" field.
If `D == 2`, the array should have columns for x and y.
If `D == 3`, the array should have columns for x, y and visible.
If `D == 4`, the array should have columns for x, y, visible and
complete.
If this is provided as a structured array, it will be used without copy
if it has the correct dtype. Otherwise, a new structured array will be
created reusing the provided data.
skeleton: The `Skeleton` that this `Instance` is associated with. It should
have `n_nodes` nodes.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` object with the specified points.
"""
return cls(
points=points_data,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
def __attrs_post_init__(self):
"""Convert the points array after initialization."""
if not isinstance(self.points, PointsArray):
self.points = self._convert_points(self.points, self.skeleton)
# Ensure points have node names
if "name" in self.points.dtype.names and not all(self.points["name"]):
self.points["name"] = self.skeleton.node_names
def numpy(
self,
invisible_as_nan: bool = True,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `Instance.points["xy"]` is.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the `Instance.points["xy"]`
attribute directly. This will not replace invisible points with `np.nan`.
"""
if invisible_as_nan:
return np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
return self.points["xy"].copy()
def __getitem__(self, node: int | str | Node) -> np.ndarray:
"""Return the point associated with a node."""
if type(node) is not int:
node = self.skeleton.index(node)
return self.points[node]
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 containing (x, y) coordinates.
Notes:
This sets the point coordinates and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
self.points[node]["visible"] = True
def __len__(self) -> int:
"""Return the number of points in the instance."""
return len(self.points)
def __repr__(self) -> str:
"""Return a readable representation of the instance."""
pts = self.numpy().tolist()
track = f'"{self.track.name}"' if self.track is not None else self.track
return f"Instance(points={pts}, track={track})"
@property
def n_visible(self) -> int:
"""Return the number of visible points in the instance."""
return sum(self.points["visible"])
@property
def is_empty(self) -> bool:
"""Return `True` if no points are visible on the instance."""
return ~(self.points["visible"].any())
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `Instance.skeleton` attribute and the
`Instance.points` attribute in place (a copy is made of the points array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
# old_skeleton = self.skeleton
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
# old_node_names = old_skeleton.node_names
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# old_node_inds = np.array(old_node_inds).reshape(-1, 1)
# new_node_inds = np.array(new_node_inds).reshape(-1, 1)
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
def same_pose_as(self, other: "Instance", tolerance: float = None) -> bool:
"""Check if this instance has the same pose as another instance.
Args:
other: Another instance to compare with.
tolerance: Maximum distance (in pixels) between corresponding points
for them to be considered the same. If None (default), uses exact
comparison including proper NaN handling.
Returns:
True if the instances have the same pose within tolerance, False otherwise.
Notes:
Two instances are considered to have the same pose if:
- They have the same skeleton structure
- When tolerance is None: All coordinates match exactly (including NaN)
- When tolerance is specified: All visible points are within tolerance
distance and NaN patterns match exactly
"""
# Check skeleton compatibility
if not self.skeleton.matches(other.skeleton):
return False
if tolerance is None:
# Exact comparison using numpy arrays with proper NaN handling
return np.array_equal(self.numpy(), other.numpy(), equal_nan=True)
else:
# Tolerance-based comparison with proper NaN handling
self_array = self.numpy()
other_array = other.numpy()
# First, check if NaN patterns match exactly
self_nan_mask = np.isnan(self_array)
other_nan_mask = np.isnan(other_array)
if not np.array_equal(self_nan_mask, other_nan_mask):
return False
# Get mask for non-NaN values
non_nan_mask = ~self_nan_mask
# If all values are NaN, they're considered equal
if not non_nan_mask.any():
return True
# Calculate distances only for non-NaN points
self_pts = self_array[non_nan_mask]
other_pts = other_array[non_nan_mask]
# Reshape to handle the coordinate pairs properly
self_pts = self_pts.reshape(-1, 2)
other_pts = other_pts.reshape(-1, 2)
distances = np.linalg.norm(self_pts - other_pts, axis=1)
return np.all(distances <= tolerance)
def same_identity_as(self, other: "Instance") -> bool:
"""Check if this instance has the same identity (track) as another instance.
Args:
other: Another instance to compare with.
Returns:
True if both instances have the same track identity, False otherwise.
Notes:
Instances have the same identity if they share the same Track object
(by identity, not just by name).
"""
if self.track is None or other.track is None:
return False
return self.track is other.track
def overlaps_with(self, other: "Instance", iou_threshold: float = 0.5) -> bool:
"""Check if this instance overlaps with another based on bounding box IoU.
Args:
other: Another instance to compare with.
iou_threshold: Minimum IoU (Intersection over Union) value to consider
the instances as overlapping.
Returns:
True if the instances overlap above the threshold, False otherwise.
Notes:
Overlap is computed using the bounding boxes of visible points.
If either instance has no visible points, they don't overlap.
"""
# Get visible points for both instances
self_visible = self.points["visible"]
other_visible = other.points["visible"]
if not self_visible.any() or not other_visible.any():
return False
# Calculate bounding boxes
self_pts = self.points["xy"][self_visible]
other_pts = other.points["xy"][other_visible]
self_bbox = np.array(
[
[np.min(self_pts[:, 0]), np.min(self_pts[:, 1])], # min x, y
[np.max(self_pts[:, 0]), np.max(self_pts[:, 1])], # max x, y
]
)
other_bbox = np.array(
[
[np.min(other_pts[:, 0]), np.min(other_pts[:, 1])],
[np.max(other_pts[:, 0]), np.max(other_pts[:, 1])],
]
)
# Calculate intersection
intersection_min = np.maximum(self_bbox[0], other_bbox[0])
intersection_max = np.minimum(self_bbox[1], other_bbox[1])
if np.any(intersection_min >= intersection_max):
# No intersection
return False
intersection_area = np.prod(intersection_max - intersection_min)
# Calculate union
self_area = np.prod(self_bbox[1] - self_bbox[0])
other_area = np.prod(other_bbox[1] - other_bbox[0])
union_area = self_area + other_area - intersection_area
# Calculate IoU
iou = intersection_area / union_area if union_area > 0 else 0
return iou >= iou_threshold
def bounding_box(self) -> np.ndarray | None:
"""Get the bounding box of visible points.
Returns:
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]],
or None if there are no visible points.
"""
visible = self.points["visible"]
if not visible.any():
return None
pts = self.points["xy"][visible]
return np.array(
[
[np.min(pts[:, 0]), np.min(pts[:, 1])],
[np.max(pts[:, 0]), np.max(pts[:, 1])],
]
)
__annotations__ = {'points': 'PointsArray', 'skeleton': 'Skeleton', 'track': 'Track | None', 'tracking_score': 'float | None', 'from_predicted': "'PredictedInstance | 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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'This class represents a ground truth instance such as an animal.\n\nAn `Instance` has a set of landmarks (points) that correspond to a `Skeleton`. Each\npoint is associated with a `Node` in the skeleton. The points are stored in a\nstructured numpy array with columns for x, y, visible, complete and name.\n\nThe `Instance` may also be associated with a `Track` which links multiple instances\ntogether across frames or videos.\n\nAttributes:\n points: A numpy structured array with columns for xy, visible and complete. The\n array should have shape `(n_nodes,)`. This representation is useful for\n performance efficiency when working with large datasets.\n skeleton: The `Skeleton` that describes the `Node`s and `Edge`s associated with\n this instance.\n track: An optional `Track` associated with a unique animal/object across frames\n or videos.\n tracking_score: The score associated with the `Track` assignment. This is\n typically the value from the score matrix used in an identity assignment.\n This is `None` if the instance is not associated with a track or if the\n track was assigned manually.\n from_predicted: The `PredictedInstance` (if any) that this instance was\n initialized from. This is used with human-in-the-loop workflows.\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__ = 386
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 literal.
int('0b100', base=0) 4
__match_args__ = ('points', 'skeleton', 'track', 'tracking_score', 'from_predicted')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.instance'
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__ = ('points', 'skeleton', 'track', 'tracking_score', 'from_predicted', '__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__ = ('points', 'skeleton')
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
is_empty
property
¶
Return True if no points are visible on the instance.
n_visible
property
¶
Return the number of visible points in the instance.
__attrs_post_init__()
¶
Convert the points array after initialization.
Source code in sleap_io/model/instance.py
def __attrs_post_init__(self):
"""Convert the points array after initialization."""
if not isinstance(self.points, PointsArray):
self.points = self._convert_points(self.points, self.skeleton)
# Ensure points have node names
if "name" in self.points.dtype.names and not all(self.points["name"]):
self.points["name"] = self.skeleton.node_names
__getitem__(node)
¶
__init__(points, skeleton, track=None, tracking_score=None, from_predicted=None)
¶
Method generated by attrs for class Instance.
Source code in sleap_io/model/instance.py
"""Data structures for data associated with a single instance such as an animal.
The `Instance` class is a SLEAP data structure that contains a collection of points that
correspond to landmarks within a `Skeleton`.
`PredictedInstance` additionally contains metadata associated with how the instance was
estimated, such as confidence scores.
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class Instance.
__repr__()
¶
Return a readable representation of the instance.
__setitem__(node, value)
¶
Set the point associated with a node.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
int | str | Node
|
The node to set the point for. Can be an integer index, string name, or Node object. |
required |
value
|
A tuple or array-like of length 2 containing (x, y) coordinates. |
required |
Notes
This sets the point coordinates and marks the point as visible.
Source code in sleap_io/model/instance.py
def __setitem__(self, node: int | str | Node, value):
"""Set the point associated with a node.
Args:
node: The node to set the point for. Can be an integer index, string name,
or Node object.
value: A tuple or array-like of length 2 containing (x, y) coordinates.
Notes:
This sets the point coordinates and marks the point as visible.
"""
if type(node) is not int:
node = self.skeleton.index(node)
if len(value) < 2:
raise ValueError("Value must have at least 2 elements (x, y)")
self.points[node]["xy"] = value[:2]
self.points[node]["visible"] = True
bounding_box()
¶
Get the bounding box of visible points.
Returns:
| Type | Description |
|---|---|
ndarray | None
|
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]], or None if there are no visible points. |
Source code in sleap_io/model/instance.py
def bounding_box(self) -> np.ndarray | None:
"""Get the bounding box of visible points.
Returns:
A numpy array of shape (2, 2) with [[min_x, min_y], [max_x, max_y]],
or None if there are no visible points.
"""
visible = self.points["visible"]
if not visible.any():
return None
pts = self.points["xy"][visible]
return np.array(
[
[np.min(pts[:, 0]), np.min(pts[:, 1])],
[np.max(pts[:, 0]), np.max(pts[:, 1])],
]
)
empty(skeleton, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create an empty instance with no points.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
skeleton
|
Skeleton
|
The |
required |
track
|
Track | None
|
An optional |
None
|
tracking_score
|
float | None
|
The score associated with the |
None
|
from_predicted
|
PredictedInstance | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
Instance
|
An |
Source code in sleap_io/model/instance.py
@classmethod
def empty(
cls,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an empty instance with no points.
Args:
skeleton: The `Skeleton` that this `Instance` is associated with.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` with an empty numpy array of shape `(n_nodes,)`.
"""
points = PointsArray.empty(len(skeleton))
points["name"] = skeleton.node_names
return cls(
points=points,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
from_numpy(points_data, skeleton, track=None, tracking_score=None, from_predicted=None)
classmethod
¶
Create an instance object from a numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
points_data
|
ndarray
|
A numpy array of shape If If this is provided as a structured array, it will be used without copy if it has the correct dtype. Otherwise, a new structured array will be created reusing the provided data. |
required |
skeleton
|
Skeleton
|
The |
required |
track
|
Track | None
|
An optional |
None
|
tracking_score
|
float | None
|
The score associated with the |
None
|
from_predicted
|
PredictedInstance | None
|
The |
None
|
Returns:
| Type | Description |
|---|---|
Instance
|
An |
Source code in sleap_io/model/instance.py
@classmethod
def from_numpy(
cls,
points_data: np.ndarray,
skeleton: Skeleton,
track: Track | None = None,
tracking_score: float | None = None,
from_predicted: "PredictedInstance | None" = None,
) -> "Instance":
"""Create an instance object from a numpy array.
Args:
points_data: A numpy array of shape `(n_nodes, D)` corresponding to the
points of the skeleton. Values of `np.nan` indicate "missing" nodes and
will be reflected in the "visible" field.
If `D == 2`, the array should have columns for x and y.
If `D == 3`, the array should have columns for x, y and visible.
If `D == 4`, the array should have columns for x, y, visible and
complete.
If this is provided as a structured array, it will be used without copy
if it has the correct dtype. Otherwise, a new structured array will be
created reusing the provided data.
skeleton: The `Skeleton` that this `Instance` is associated with. It should
have `n_nodes` nodes.
track: An optional `Track` associated with a unique animal/object across
frames or videos.
tracking_score: The score associated with the `Track` assignment. This is
typically the value from the score matrix used in an identity
assignment. This is `None` if the instance is not associated with a
track or if the track was assigned manually.
from_predicted: The `PredictedInstance` (if any) that this instance was
initialized from. This is used with human-in-the-loop workflows.
Returns:
An `Instance` object with the specified points.
"""
return cls(
points=points_data,
skeleton=skeleton,
track=track,
tracking_score=tracking_score,
from_predicted=from_predicted,
)
numpy(invisible_as_nan=True)
¶
Return the instance points as a (n_nodes, 2) numpy array.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
invisible_as_nan
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
ndarray
|
A numpy array of shape |
Notes
This will always return a copy of the array.
If you need to avoid making a copy, just access the Instance.points["xy"]
attribute directly. This will not replace invisible points with np.nan.
Source code in sleap_io/model/instance.py
def numpy(
self,
invisible_as_nan: bool = True,
) -> np.ndarray:
"""Return the instance points as a `(n_nodes, 2)` numpy array.
Args:
invisible_as_nan: If `True` (the default), points that are not visible will
be set to `np.nan`. If `False`, they will be whatever the stored value
of `Instance.points["xy"]` is.
Returns:
A numpy array of shape `(n_nodes, 2)` corresponding to the points of the
skeleton. Values of `np.nan` indicate "missing" nodes.
Notes:
This will always return a copy of the array.
If you need to avoid making a copy, just access the `Instance.points["xy"]`
attribute directly. This will not replace invisible points with `np.nan`.
"""
if invisible_as_nan:
return np.where(
self.points["visible"].reshape(-1, 1), self.points["xy"], np.nan
)
else:
return self.points["xy"].copy()
overlaps_with(other, iou_threshold=0.5)
¶
Check if this instance overlaps with another based on bounding box IoU.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
iou_threshold
|
float
|
Minimum IoU (Intersection over Union) value to consider the instances as overlapping. |
0.5
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the instances overlap above the threshold, False otherwise. |
Notes
Overlap is computed using the bounding boxes of visible points. If either instance has no visible points, they don't overlap.
Source code in sleap_io/model/instance.py
def overlaps_with(self, other: "Instance", iou_threshold: float = 0.5) -> bool:
"""Check if this instance overlaps with another based on bounding box IoU.
Args:
other: Another instance to compare with.
iou_threshold: Minimum IoU (Intersection over Union) value to consider
the instances as overlapping.
Returns:
True if the instances overlap above the threshold, False otherwise.
Notes:
Overlap is computed using the bounding boxes of visible points.
If either instance has no visible points, they don't overlap.
"""
# Get visible points for both instances
self_visible = self.points["visible"]
other_visible = other.points["visible"]
if not self_visible.any() or not other_visible.any():
return False
# Calculate bounding boxes
self_pts = self.points["xy"][self_visible]
other_pts = other.points["xy"][other_visible]
self_bbox = np.array(
[
[np.min(self_pts[:, 0]), np.min(self_pts[:, 1])], # min x, y
[np.max(self_pts[:, 0]), np.max(self_pts[:, 1])], # max x, y
]
)
other_bbox = np.array(
[
[np.min(other_pts[:, 0]), np.min(other_pts[:, 1])],
[np.max(other_pts[:, 0]), np.max(other_pts[:, 1])],
]
)
# Calculate intersection
intersection_min = np.maximum(self_bbox[0], other_bbox[0])
intersection_max = np.minimum(self_bbox[1], other_bbox[1])
if np.any(intersection_min >= intersection_max):
# No intersection
return False
intersection_area = np.prod(intersection_max - intersection_min)
# Calculate union
self_area = np.prod(self_bbox[1] - self_bbox[0])
other_area = np.prod(other_bbox[1] - other_bbox[0])
union_area = self_area + other_area - intersection_area
# Calculate IoU
iou = intersection_area / union_area if union_area > 0 else 0
return iou >= iou_threshold
replace_skeleton(new_skeleton, node_names_map=None)
¶
Replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_skeleton
|
Skeleton
|
The new |
required |
node_names_map
|
dict[str, str] | None
|
Dictionary mapping nodes in the old skeleton to nodes in the new skeleton. Keys and values should be specified as lists of strings. If not provided, only nodes with identical names will be mapped. Points associated with unmapped nodes will be removed. |
None
|
Notes
This method will update the Instance.skeleton attribute and the
Instance.points attribute in place (a copy is made of the points array).
It is recommended to use Labels.replace_skeleton instead of this method if
more flexible node mapping is required.
Source code in sleap_io/model/instance.py
def replace_skeleton(
self,
new_skeleton: Skeleton,
node_names_map: dict[str, str] | None = None,
):
"""Replace the skeleton associated with the instance.
Args:
new_skeleton: The new `Skeleton` to associate with the instance.
node_names_map: Dictionary mapping nodes in the old skeleton to nodes in the
new skeleton. Keys and values should be specified as lists of strings.
If not provided, only nodes with identical names will be mapped. Points
associated with unmapped nodes will be removed.
Notes:
This method will update the `Instance.skeleton` attribute and the
`Instance.points` attribute in place (a copy is made of the points array).
It is recommended to use `Labels.replace_skeleton` instead of this method if
more flexible node mapping is required.
"""
# Update skeleton object.
# old_skeleton = self.skeleton
self.skeleton = new_skeleton
# Get node names with replacements from node map if possible.
# old_node_names = old_skeleton.node_names
old_node_names = self.points["name"].tolist()
if node_names_map is not None:
old_node_names = [node_names_map.get(node, node) for node in old_node_names]
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(old_node_names)
# old_node_inds = np.array(old_node_inds).reshape(-1, 1)
# new_node_inds = np.array(new_node_inds).reshape(-1, 1)
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
self.points = new_points
self.points["name"] = self.skeleton.node_names
same_identity_as(other)
¶
Check if this instance has the same identity (track) as another instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if both instances have the same track identity, False otherwise. |
Notes
Instances have the same identity if they share the same Track object (by identity, not just by name).
Source code in sleap_io/model/instance.py
def same_identity_as(self, other: "Instance") -> bool:
"""Check if this instance has the same identity (track) as another instance.
Args:
other: Another instance to compare with.
Returns:
True if both instances have the same track identity, False otherwise.
Notes:
Instances have the same identity if they share the same Track object
(by identity, not just by name).
"""
if self.track is None or other.track is None:
return False
return self.track is other.track
same_pose_as(other, tolerance=None)
¶
Check if this instance has the same pose as another instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Instance
|
Another instance to compare with. |
required |
tolerance
|
float
|
Maximum distance (in pixels) between corresponding points for them to be considered the same. If None (default), uses exact comparison including proper NaN handling. |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the instances have the same pose within tolerance, False otherwise. |
Notes
Two instances are considered to have the same pose if: - They have the same skeleton structure - When tolerance is None: All coordinates match exactly (including NaN) - When tolerance is specified: All visible points are within tolerance distance and NaN patterns match exactly
Source code in sleap_io/model/instance.py
def same_pose_as(self, other: "Instance", tolerance: float = None) -> bool:
"""Check if this instance has the same pose as another instance.
Args:
other: Another instance to compare with.
tolerance: Maximum distance (in pixels) between corresponding points
for them to be considered the same. If None (default), uses exact
comparison including proper NaN handling.
Returns:
True if the instances have the same pose within tolerance, False otherwise.
Notes:
Two instances are considered to have the same pose if:
- They have the same skeleton structure
- When tolerance is None: All coordinates match exactly (including NaN)
- When tolerance is specified: All visible points are within tolerance
distance and NaN patterns match exactly
"""
# Check skeleton compatibility
if not self.skeleton.matches(other.skeleton):
return False
if tolerance is None:
# Exact comparison using numpy arrays with proper NaN handling
return np.array_equal(self.numpy(), other.numpy(), equal_nan=True)
else:
# Tolerance-based comparison with proper NaN handling
self_array = self.numpy()
other_array = other.numpy()
# First, check if NaN patterns match exactly
self_nan_mask = np.isnan(self_array)
other_nan_mask = np.isnan(other_array)
if not np.array_equal(self_nan_mask, other_nan_mask):
return False
# Get mask for non-NaN values
non_nan_mask = ~self_nan_mask
# If all values are NaN, they're considered equal
if not non_nan_mask.any():
return True
# Calculate distances only for non-NaN points
self_pts = self_array[non_nan_mask]
other_pts = other_array[non_nan_mask]
# Reshape to handle the coordinate pairs properly
self_pts = self_pts.reshape(-1, 2)
other_pts = other_pts.reshape(-1, 2)
distances = np.linalg.norm(self_pts - other_pts, axis=1)
return np.all(distances <= tolerance)
update_skeleton(names_only=False)
¶
Update or replace the skeleton associated with the instance.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
names_only
|
bool
|
If |
False
|
Source code in sleap_io/model/instance.py
def update_skeleton(self, names_only: bool = False):
"""Update or replace the skeleton associated with the instance.
Args:
names_only: If `True`, only update the node names in the points array. If
`False`, the points array will be updated to match the new skeleton.
"""
if names_only:
# Update the node names.
self.points["name"] = self.skeleton.node_names
return
# Find correspondences.
new_node_inds, old_node_inds = self.skeleton.match_nodes(self.points["name"])
# Update the points.
new_points = PointsArray.empty(len(self.skeleton))
new_points[new_node_inds] = self.points[old_node_inds]
new_points["name"] = self.skeleton.node_names
self.points = new_points
InstanceGroup
¶
Defines a group of instances across the same frame index.
Attributes:
| Name | Type | Description |
|---|---|---|
instances_by_camera |
Dictionary of |
|
instances |
List of |
|
cameras |
List of |
|
score |
Optional score for the |
|
points |
Optional 3D points for the |
|
metadata |
Dictionary of metadata. |
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class InstanceGroup. |
__replace__ |
Method generated by attrs for class InstanceGroup. |
__repr__ |
Return a readable representation of the instance group. |
__setattr__ |
Method generated by attrs for class InstanceGroup. |
get_instance |
Get |
Source code in sleap_io/model/camera.py
@define(eq=False) # Set eq to false to make class hashable
class InstanceGroup:
"""Defines a group of instances across the same frame index.
Attributes:
instances_by_camera: Dictionary of `Instance` objects by `Camera`.
instances: List of `Instance` objects in the group.
cameras: List of `Camera` objects that have an `Instance` associated.
score: Optional score for the `InstanceGroup`. Setting the score will also
update the score for all `instances` already in the `InstanceGroup`. The
score for `instances` will not be updated upon initialization.
points: Optional 3D points for the `InstanceGroup`.
metadata: Dictionary of metadata.
"""
_instance_by_camera: dict[Camera, Instance] = field(
factory=dict, validator=instance_of(dict)
)
_score: float | None = field(
default=None, converter=attrs.converters.optional(float)
)
_points: np.ndarray | None = field(
default=None,
converter=attrs.converters.optional(lambda x: np.array(x, dtype="float64")),
)
metadata: dict = field(factory=dict, validator=instance_of(dict))
@property
def instance_by_camera(self) -> dict[Camera, Instance]:
"""Get dictionary of `Instance` objects by `Camera`."""
return self._instance_by_camera
@property
def instances(self) -> list[Instance]:
"""List of `Instance` objects."""
return list(self._instance_by_camera.values())
@property
def cameras(self) -> "list[Camera]":
"""List of `Camera` objects."""
return list(self._instance_by_camera.keys())
@property
def score(self) -> float | None:
"""Get score for `InstanceGroup`."""
return self._score
@property
def points(self) -> np.ndarray | None:
"""Get 3D points for `InstanceGroup`."""
return self._points
def get_instance(self, camera: Camera) -> Instance | None:
"""Get `Instance` associated with `camera`.
Args:
camera: `Camera` to get `Instance`.
Returns:
`Instance` associated with `camera` or None if not found.
"""
return self._instance_by_camera.get(camera, None)
def __repr__(self) -> str:
"""Return a readable representation of the instance group."""
cameras_str = ", ".join([c.name or "None" for c in self.cameras])
return f"InstanceGroup(cameras={len(self.cameras)}:[{cameras_str}])"
__annotations__ = {'_instance_by_camera': 'dict[Camera, Instance]', '_score': 'float | None', '_points': 'np.ndarray | None', 'metadata': 'dict'}
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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'Defines a group of instances across the same frame index.\n\nAttributes:\n instances_by_camera: Dictionary of `Instance` objects by `Camera`.\n instances: List of `Instance` objects in the group.\n cameras: List of `Camera` objects that have an `Instance` associated.\n score: Optional score for the `InstanceGroup`. Setting the score will also\n update the score for all `instances` already in the `InstanceGroup`. The\n score for `instances` will not be updated upon initialization.\n points: Optional 3D points for the `InstanceGroup`.\n metadata: Dictionary of metadata.\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__ = 442
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 literal.
int('0b100', base=0) 4
__match_args__ = ('_instance_by_camera', '_score', '_points', 'metadata')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.camera'
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__ = ('_instance_by_camera', '_score', '_points', 'metadata', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
cameras
property
¶
List of Camera objects.
instance_by_camera
property
¶
Get dictionary of Instance objects by Camera.
instances
property
¶
List of Instance objects.
points
property
¶
Get 3D points for InstanceGroup.
score
property
¶
Get score for InstanceGroup.
__init__(instance_by_camera=NOTHING, score=None, points=None, metadata=NOTHING)
¶
Method generated by attrs for class InstanceGroup.
Source code in sleap_io/model/camera.py
"""Data structure for a single camera view in a multi-camera setup."""
from __future__ import annotations
import attrs
import numpy as np
from attrs import define, field
from attrs.validators import instance_of
from sleap_io.model.instance import Instance
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.video import Video
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
__replace__(**changes)
¶
Method generated by attrs for class InstanceGroup.
__repr__()
¶
Return a readable representation of the instance group.
__setattr__(name, val)
¶
Method generated by attrs for class InstanceGroup.
LabeledFrame
¶
Labeled data for a single frame of a video.
Attributes:
| Name | Type | Description |
|---|---|---|
video |
The |
|
frame_idx |
The index of the |
|
instances |
List of |
Notes
Instances of this class are hashed by identity, not by value. This means that
two LabeledFrame instances with the same attributes will NOT be considered
equal in a set or dict.
Methods:
| Name | Description |
|---|---|
__getitem__ |
Return the |
__init__ |
Method generated by attrs for class LabeledFrame. |
__iter__ |
Iterate over |
__len__ |
Return the number of instances in the frame. |
__replace__ |
Method generated by attrs for class LabeledFrame. |
__repr__ |
Method generated by attrs for class LabeledFrame. |
__setattr__ |
Method generated by attrs for class LabeledFrame. |
matches |
Check if this frame matches another frame's identity. |
merge |
Merge instances from another frame into this frame. |
numpy |
Return all instances in the frame as a numpy array. |
remove_empty_instances |
Remove all instances with no visible points. |
remove_predictions |
Remove all |
similarity_to |
Calculate instance overlap metrics with another frame. |
Source code in sleap_io/model/labeled_frame.py
@define(eq=False)
class LabeledFrame:
"""Labeled data for a single frame of a video.
Attributes:
video: The `Video` associated with this `LabeledFrame`.
frame_idx: The index of the `LabeledFrame` in the `Video`.
instances: List of `Instance` objects associated with this `LabeledFrame`.
Notes:
Instances of this class are hashed by identity, not by value. This means that
two `LabeledFrame` instances with the same attributes will NOT be considered
equal in a set or dict.
"""
video: Video
frame_idx: int = field(converter=int)
instances: list[Instance | PredictedInstance] = field(factory=list)
def __len__(self) -> int:
"""Return the number of instances in the frame."""
return len(self.instances)
def __getitem__(self, key: int) -> Instance | PredictedInstance:
"""Return the `Instance` at `key` index in the `instances` list."""
return self.instances[key]
def __iter__(self):
"""Iterate over `Instance`s in `instances` list."""
return iter(self.instances)
@property
def user_instances(self) -> list[Instance]:
"""Frame instances that are user-labeled (`Instance` objects)."""
return [inst for inst in self.instances if type(inst) is Instance]
@property
def has_user_instances(self) -> bool:
"""Return True if the frame has any user-labeled instances."""
for inst in self.instances:
if type(inst) is Instance:
return True
return False
@property
def predicted_instances(self) -> list[Instance]:
"""Frame instances that are predicted by a model (`PredictedInstance`)."""
return [inst for inst in self.instances if type(inst) is PredictedInstance]
@property
def has_predicted_instances(self) -> bool:
"""Return True if the frame has any predicted instances."""
for inst in self.instances:
if type(inst) is PredictedInstance:
return True
return False
def numpy(self) -> np.ndarray:
"""Return all instances in the frame as a numpy array.
Returns:
Points as a numpy array of shape `(n_instances, n_nodes, 2)`.
Note that the order of the instances is arbitrary.
"""
n_instances = len(self.instances)
n_nodes = len(self.instances[0]) if n_instances > 0 else 0
pts = np.full((n_instances, n_nodes, 2), np.nan)
for i, inst in enumerate(self.instances):
pts[i] = inst.numpy()[:, 0:2]
return pts
@property
def image(self) -> np.ndarray:
"""Return the image of the frame as a numpy array."""
return self.video[self.frame_idx]
@property
def unused_predictions(self) -> list[Instance]:
"""Return a list of "unused" `PredictedInstance` objects in frame.
This is all of the `PredictedInstance` objects which do not have a corresponding
`Instance` in the same track in the same frame.
"""
unused_predictions = []
any_tracks = [inst.track for inst in self.instances if inst.track is not None]
if len(any_tracks):
# Use tracks to determine which predicted instances have been used
used_tracks = [
inst.track
for inst in self.instances
if type(inst) is Instance and inst.track is not None
]
unused_predictions = [
inst
for inst in self.instances
if inst.track not in used_tracks and type(inst) is PredictedInstance
]
else:
# Use from_predicted to determine which predicted instances have been used
# TODO: should we always do this instead of using tracks?
used_instances = [
inst.from_predicted
for inst in self.instances
if inst.from_predicted is not None
]
unused_predictions = [
inst
for inst in self.instances
if type(inst) is PredictedInstance and inst not in used_instances
]
return unused_predictions
def remove_predictions(self):
"""Remove all `PredictedInstance` objects from the frame."""
self.instances = [inst for inst in self.instances if type(inst) is Instance]
def remove_empty_instances(self):
"""Remove all instances with no visible points."""
self.instances = [inst for inst in self.instances if not inst.is_empty]
def matches(self, other: "LabeledFrame", video_must_match: bool = True) -> bool:
"""Check if this frame matches another frame's identity.
Args:
other: Another LabeledFrame to compare with.
video_must_match: If True, frames must be from the same video.
If False, only frame index needs to match.
Returns:
True if the frames have the same identity, False otherwise.
Notes:
Frame identity is determined by video and frame index.
This does not compare the instances within the frame.
"""
if self.frame_idx != other.frame_idx:
return False
if video_must_match:
# Check if videos are the same object
if self.video is other.video:
return True
# Check if videos have matching paths
return self.video.matches_path(other.video, strict=False)
return True
def similarity_to(self, other: "LabeledFrame") -> dict[str, any]:
"""Calculate instance overlap metrics with another frame.
Args:
other: Another LabeledFrame to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_user_self': Number of user instances in this frame
- 'n_user_other': Number of user instances in the other frame
- 'n_pred_self': Number of predicted instances in this frame
- 'n_pred_other': Number of predicted instances in the other frame
- 'n_overlapping': Number of instances that overlap (by IoU)
- 'mean_pose_distance': Mean distance between matching poses
"""
metrics = {
"n_user_self": len(self.user_instances),
"n_user_other": len(other.user_instances),
"n_pred_self": len(self.predicted_instances),
"n_pred_other": len(other.predicted_instances),
"n_overlapping": 0,
"mean_pose_distance": None,
}
# Count overlapping instances and compute pose distances
pose_distances = []
for inst1 in self.instances:
for inst2 in other.instances:
# Check if instances overlap
if inst1.overlaps_with(inst2, iou_threshold=0.1):
metrics["n_overlapping"] += 1
# If they have the same skeleton, compute pose distance
if inst1.skeleton.matches(inst2.skeleton):
# Get visible points for both
pts1 = inst1.numpy()
pts2 = inst2.numpy()
# Compute distances for visible points in both
valid = ~(np.isnan(pts1[:, 0]) | np.isnan(pts2[:, 0]))
if valid.any():
distances = np.linalg.norm(
pts1[valid] - pts2[valid], axis=1
)
pose_distances.extend(distances.tolist())
if pose_distances:
metrics["mean_pose_distance"] = np.mean(pose_distances)
return metrics
def merge(
self,
other: "LabeledFrame",
instance: "InstanceMatcher | None" = None,
frame: str = "auto",
) -> tuple[list[Instance], list[tuple[Instance, Instance, str]]]:
"""Merge instances from another frame into this frame.
Args:
other: Another LabeledFrame to merge instances from.
instance: Matcher to use for finding duplicate instances.
If None, uses default spatial matching with 5px tolerance.
frame: Merge strategy:
- "auto": Keep user labels, update predictions only if no user label
- "keep_original": Keep all original instances, ignore new ones
- "keep_new": Replace with new instances
- "keep_both": Keep all instances from both frames
- "update_tracks": Update track and score of the original instances
from the new instances.
- "replace_predictions": Keep all user instances from original frame,
remove all predictions from original frame, add only predictions
from the incoming frame. No spatial matching is performed.
Returns:
A tuple of (merged_instances, conflicts) where:
- merged_instances: List of instances after merging
- conflicts: List of (original, new, resolution) tuples for conflicts
Notes:
This method doesn't modify the frame in place. It returns the merged
instance list which can be assigned back if desired.
"""
from sleap_io.model.matching import InstanceMatcher, InstanceMatchMethod
if instance is None:
instance_matcher = InstanceMatcher(
method=InstanceMatchMethod.SPATIAL, threshold=5.0
)
else:
instance_matcher = instance
conflicts = []
if frame == "keep_original":
return self.instances.copy(), conflicts
elif frame == "keep_new":
return other.instances.copy(), conflicts
elif frame == "keep_both":
return self.instances + other.instances, conflicts
elif frame == "update_tracks":
# match instances and update .track and tracking score of the old instances
matches = instance_matcher.find_matches(self.instances, other.instances)
for self_idx, other_idx, score in matches:
self.instances[self_idx].track = other.instances[other_idx].track
self.instances[self_idx].tracking_score = other.instances[
other_idx
].tracking_score
return self.instances, conflicts
elif frame == "replace_predictions":
# Keep all user instances from original frame
merged = [inst for inst in self.instances if type(inst) is Instance]
# Add only predictions from incoming frame (not user instances)
merged.extend(
inst for inst in other.instances if type(inst) is PredictedInstance
)
# No conflicts to report - this is a clean replacement
return merged, []
# Auto merging strategy
merged_instances = []
used_indices = set()
# First, keep all user instances from self
for inst in self.instances:
if type(inst) is Instance:
merged_instances.append(inst)
# Find matches between instances
matches = instance_matcher.find_matches(self.instances, other.instances)
# Group matches by instance in other frame
other_to_self = {}
for self_idx, other_idx, score in matches:
if other_idx not in other_to_self or score > other_to_self[other_idx][1]:
other_to_self[other_idx] = (self_idx, score)
# Process instances from other frame
for other_idx, other_inst in enumerate(other.instances):
if other_idx in other_to_self:
self_idx, score = other_to_self[other_idx]
self_inst = self.instances[self_idx]
# Check for conflicts
if type(self_inst) is Instance and type(other_inst) is Instance:
# Both are user instances - conflict
conflicts.append((self_inst, other_inst, "kept_original"))
used_indices.add(self_idx)
elif (
type(self_inst) is PredictedInstance
and type(other_inst) is Instance
):
# Replace prediction with user instance
if self_idx not in used_indices:
merged_instances.append(other_inst)
used_indices.add(self_idx)
elif (
type(self_inst) is Instance
and type(other_inst) is PredictedInstance
):
# Keep user instance, ignore prediction
conflicts.append((self_inst, other_inst, "kept_user"))
used_indices.add(self_idx)
else:
# Both are predictions - keep the new one
if self_idx not in used_indices:
merged_instances.append(other_inst)
used_indices.add(self_idx)
else:
# No match found, add new instance
merged_instances.append(other_inst)
# Add remaining instances from self that weren't matched
for self_idx, self_inst in enumerate(self.instances):
if type(self_inst) is PredictedInstance and self_idx not in used_indices:
# Check if this prediction should be kept
# NOTE: This defensive logic should be unreachable under normal
# circumstances since all matched instances should have been added to
# used_indices above. However, we keep this as a safety net for edge
# cases or future changes.
keep = True
for other_idx, (matched_self_idx, _) in other_to_self.items():
if matched_self_idx == self_idx:
keep = False
break
if keep:
merged_instances.append(self_inst)
return merged_instances, conflicts
__annotations__ = {'video': 'Video', 'frame_idx': 'int', 'instances': 'list[Instance | PredictedInstance]'}
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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'Labeled data for a single frame of a video.\n\nAttributes:\n video: The `Video` associated with this `LabeledFrame`.\n frame_idx: The index of the `LabeledFrame` in the `Video`.\n instances: List of `Instance` objects associated with this `LabeledFrame`.\n\nNotes:\n Instances of this class are hashed by identity, not by value. This means that\n two `LabeledFrame` instances with the same attributes will NOT be considered\n equal in a set or dict.\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__ = 21
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 literal.
int('0b100', base=0) 4
__match_args__ = ('video', 'frame_idx', 'instances')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.labeled_frame'
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__ = ('video', 'frame_idx', 'instances', '__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__ = ('instances',)
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
has_predicted_instances
property
¶
Return True if the frame has any predicted instances.
has_user_instances
property
¶
Return True if the frame has any user-labeled instances.
image
property
¶
Return the image of the frame as a numpy array.
predicted_instances
property
¶
Frame instances that are predicted by a model (PredictedInstance).
unused_predictions
property
¶
Return a list of "unused" PredictedInstance objects in frame.
This is all of the PredictedInstance objects which do not have a corresponding
Instance in the same track in the same frame.
user_instances
property
¶
Frame instances that are user-labeled (Instance objects).
__getitem__(key)
¶
__init__(video, frame_idx, instances=NOTHING)
¶
__iter__()
¶
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class LabeledFrame.
__repr__()
¶
Method generated by attrs for class LabeledFrame.
Source code in sleap_io/model/labeled_frame.py
"""Data structures for data contained within a single video frame.
The `LabeledFrame` class is a data structure that contains `Instance`s and
`PredictedInstance`s that are associated with a single frame within a video.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import numpy as np
from attrs import define, field
from sleap_io.model.instance import Instance, PredictedInstance
from sleap_io.model.video import Video
__setattr__(name, val)
¶
Method generated by attrs for class LabeledFrame.
matches(other, video_must_match=True)
¶
Check if this frame matches another frame's identity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
LabeledFrame
|
Another LabeledFrame to compare with. |
required |
video_must_match
|
bool
|
If True, frames must be from the same video. If False, only frame index needs to match. |
True
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the frames have the same identity, False otherwise. |
Notes
Frame identity is determined by video and frame index. This does not compare the instances within the frame.
Source code in sleap_io/model/labeled_frame.py
def matches(self, other: "LabeledFrame", video_must_match: bool = True) -> bool:
"""Check if this frame matches another frame's identity.
Args:
other: Another LabeledFrame to compare with.
video_must_match: If True, frames must be from the same video.
If False, only frame index needs to match.
Returns:
True if the frames have the same identity, False otherwise.
Notes:
Frame identity is determined by video and frame index.
This does not compare the instances within the frame.
"""
if self.frame_idx != other.frame_idx:
return False
if video_must_match:
# Check if videos are the same object
if self.video is other.video:
return True
# Check if videos have matching paths
return self.video.matches_path(other.video, strict=False)
return True
merge(other, instance=None, frame='auto')
¶
Merge instances from another frame into this frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
LabeledFrame
|
Another LabeledFrame to merge instances from. |
required |
instance
|
InstanceMatcher | None
|
Matcher to use for finding duplicate instances. If None, uses default spatial matching with 5px tolerance. |
None
|
frame
|
str
|
Merge strategy: - "auto": Keep user labels, update predictions only if no user label - "keep_original": Keep all original instances, ignore new ones - "keep_new": Replace with new instances - "keep_both": Keep all instances from both frames - "update_tracks": Update track and score of the original instances from the new instances. - "replace_predictions": Keep all user instances from original frame, remove all predictions from original frame, add only predictions from the incoming frame. No spatial matching is performed. |
'auto'
|
Returns:
| Type | Description |
|---|---|
tuple[list[Instance], list[tuple[Instance, Instance, str]]]
|
A tuple of (merged_instances, conflicts) where: - merged_instances: List of instances after merging - conflicts: List of (original, new, resolution) tuples for conflicts |
Notes
This method doesn't modify the frame in place. It returns the merged instance list which can be assigned back if desired.
Source code in sleap_io/model/labeled_frame.py
def merge(
self,
other: "LabeledFrame",
instance: "InstanceMatcher | None" = None,
frame: str = "auto",
) -> tuple[list[Instance], list[tuple[Instance, Instance, str]]]:
"""Merge instances from another frame into this frame.
Args:
other: Another LabeledFrame to merge instances from.
instance: Matcher to use for finding duplicate instances.
If None, uses default spatial matching with 5px tolerance.
frame: Merge strategy:
- "auto": Keep user labels, update predictions only if no user label
- "keep_original": Keep all original instances, ignore new ones
- "keep_new": Replace with new instances
- "keep_both": Keep all instances from both frames
- "update_tracks": Update track and score of the original instances
from the new instances.
- "replace_predictions": Keep all user instances from original frame,
remove all predictions from original frame, add only predictions
from the incoming frame. No spatial matching is performed.
Returns:
A tuple of (merged_instances, conflicts) where:
- merged_instances: List of instances after merging
- conflicts: List of (original, new, resolution) tuples for conflicts
Notes:
This method doesn't modify the frame in place. It returns the merged
instance list which can be assigned back if desired.
"""
from sleap_io.model.matching import InstanceMatcher, InstanceMatchMethod
if instance is None:
instance_matcher = InstanceMatcher(
method=InstanceMatchMethod.SPATIAL, threshold=5.0
)
else:
instance_matcher = instance
conflicts = []
if frame == "keep_original":
return self.instances.copy(), conflicts
elif frame == "keep_new":
return other.instances.copy(), conflicts
elif frame == "keep_both":
return self.instances + other.instances, conflicts
elif frame == "update_tracks":
# match instances and update .track and tracking score of the old instances
matches = instance_matcher.find_matches(self.instances, other.instances)
for self_idx, other_idx, score in matches:
self.instances[self_idx].track = other.instances[other_idx].track
self.instances[self_idx].tracking_score = other.instances[
other_idx
].tracking_score
return self.instances, conflicts
elif frame == "replace_predictions":
# Keep all user instances from original frame
merged = [inst for inst in self.instances if type(inst) is Instance]
# Add only predictions from incoming frame (not user instances)
merged.extend(
inst for inst in other.instances if type(inst) is PredictedInstance
)
# No conflicts to report - this is a clean replacement
return merged, []
# Auto merging strategy
merged_instances = []
used_indices = set()
# First, keep all user instances from self
for inst in self.instances:
if type(inst) is Instance:
merged_instances.append(inst)
# Find matches between instances
matches = instance_matcher.find_matches(self.instances, other.instances)
# Group matches by instance in other frame
other_to_self = {}
for self_idx, other_idx, score in matches:
if other_idx not in other_to_self or score > other_to_self[other_idx][1]:
other_to_self[other_idx] = (self_idx, score)
# Process instances from other frame
for other_idx, other_inst in enumerate(other.instances):
if other_idx in other_to_self:
self_idx, score = other_to_self[other_idx]
self_inst = self.instances[self_idx]
# Check for conflicts
if type(self_inst) is Instance and type(other_inst) is Instance:
# Both are user instances - conflict
conflicts.append((self_inst, other_inst, "kept_original"))
used_indices.add(self_idx)
elif (
type(self_inst) is PredictedInstance
and type(other_inst) is Instance
):
# Replace prediction with user instance
if self_idx not in used_indices:
merged_instances.append(other_inst)
used_indices.add(self_idx)
elif (
type(self_inst) is Instance
and type(other_inst) is PredictedInstance
):
# Keep user instance, ignore prediction
conflicts.append((self_inst, other_inst, "kept_user"))
used_indices.add(self_idx)
else:
# Both are predictions - keep the new one
if self_idx not in used_indices:
merged_instances.append(other_inst)
used_indices.add(self_idx)
else:
# No match found, add new instance
merged_instances.append(other_inst)
# Add remaining instances from self that weren't matched
for self_idx, self_inst in enumerate(self.instances):
if type(self_inst) is PredictedInstance and self_idx not in used_indices:
# Check if this prediction should be kept
# NOTE: This defensive logic should be unreachable under normal
# circumstances since all matched instances should have been added to
# used_indices above. However, we keep this as a safety net for edge
# cases or future changes.
keep = True
for other_idx, (matched_self_idx, _) in other_to_self.items():
if matched_self_idx == self_idx:
keep = False
break
if keep:
merged_instances.append(self_inst)
return merged_instances, conflicts
numpy()
¶
Return all instances in the frame as a numpy array.
Returns:
| Type | Description |
|---|---|
ndarray
|
Points as a numpy array of shape Note that the order of the instances is arbitrary. |
Source code in sleap_io/model/labeled_frame.py
def numpy(self) -> np.ndarray:
"""Return all instances in the frame as a numpy array.
Returns:
Points as a numpy array of shape `(n_instances, n_nodes, 2)`.
Note that the order of the instances is arbitrary.
"""
n_instances = len(self.instances)
n_nodes = len(self.instances[0]) if n_instances > 0 else 0
pts = np.full((n_instances, n_nodes, 2), np.nan)
for i, inst in enumerate(self.instances):
pts[i] = inst.numpy()[:, 0:2]
return pts
remove_empty_instances()
¶
remove_predictions()
¶
similarity_to(other)
¶
Calculate instance overlap metrics with another frame.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
LabeledFrame
|
Another LabeledFrame to compare with. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, any]
|
A dictionary with similarity metrics: - 'n_user_self': Number of user instances in this frame - 'n_user_other': Number of user instances in the other frame - 'n_pred_self': Number of predicted instances in this frame - 'n_pred_other': Number of predicted instances in the other frame - 'n_overlapping': Number of instances that overlap (by IoU) - 'mean_pose_distance': Mean distance between matching poses |
Source code in sleap_io/model/labeled_frame.py
def similarity_to(self, other: "LabeledFrame") -> dict[str, any]:
"""Calculate instance overlap metrics with another frame.
Args:
other: Another LabeledFrame to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_user_self': Number of user instances in this frame
- 'n_user_other': Number of user instances in the other frame
- 'n_pred_self': Number of predicted instances in this frame
- 'n_pred_other': Number of predicted instances in the other frame
- 'n_overlapping': Number of instances that overlap (by IoU)
- 'mean_pose_distance': Mean distance between matching poses
"""
metrics = {
"n_user_self": len(self.user_instances),
"n_user_other": len(other.user_instances),
"n_pred_self": len(self.predicted_instances),
"n_pred_other": len(other.predicted_instances),
"n_overlapping": 0,
"mean_pose_distance": None,
}
# Count overlapping instances and compute pose distances
pose_distances = []
for inst1 in self.instances:
for inst2 in other.instances:
# Check if instances overlap
if inst1.overlaps_with(inst2, iou_threshold=0.1):
metrics["n_overlapping"] += 1
# If they have the same skeleton, compute pose distance
if inst1.skeleton.matches(inst2.skeleton):
# Get visible points for both
pts1 = inst1.numpy()
pts2 = inst2.numpy()
# Compute distances for visible points in both
valid = ~(np.isnan(pts1[:, 0]) | np.isnan(pts2[:, 0]))
if valid.any():
distances = np.linalg.norm(
pts1[valid] - pts2[valid], axis=1
)
pose_distances.extend(distances.tolist())
if pose_distances:
metrics["mean_pose_distance"] = np.mean(pose_distances)
return metrics
RecordingSession
¶
A recording session with multiple cameras.
Attributes:
| Name | Type | Description |
|---|---|---|
camera_group |
|
|
frame_groups |
Dictionary mapping frame index to |
|
videos |
List of |
|
cameras |
List of |
|
metadata |
Dictionary of metadata. |
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class RecordingSession. |
__replace__ |
Method generated by attrs for class RecordingSession. |
__repr__ |
Return a readable representation of the session. |
__setattr__ |
Method generated by attrs for class RecordingSession. |
add_video |
Add |
get_camera |
Get |
get_video |
Get |
remove_video |
Remove |
Source code in sleap_io/model/camera.py
@define(eq=False) # Set eq to false to make class hashable
class RecordingSession:
"""A recording session with multiple cameras.
Attributes:
camera_group: `CameraGroup` object containing cameras in the session.
frame_groups: Dictionary mapping frame index to `FrameGroup`.
videos: List of `Video` objects linked to `Camera`s in the session.
cameras: List of `Camera` objects linked to `Video`s in the session.
metadata: Dictionary of metadata.
"""
camera_group: CameraGroup = field(
factory=CameraGroup, validator=instance_of(CameraGroup)
)
_video_by_camera: "dict[Camera, Video]" = field(
factory=dict, validator=instance_of(dict)
)
_camera_by_video: "dict[Video, Camera]" = field(
factory=dict, validator=instance_of(dict)
)
_frame_group_by_frame_idx: "dict[int, FrameGroup]" = field(
factory=dict, validator=instance_of(dict)
)
metadata: dict = field(factory=dict, validator=instance_of(dict))
@property
def frame_groups(self) -> "dict[int, FrameGroup]":
"""Get dictionary of `FrameGroup` objects by frame index.
Returns:
Dictionary of `FrameGroup` objects by frame index.
"""
return self._frame_group_by_frame_idx
@property
def videos(self) -> list[Video]:
"""Get list of `Video` objects in the `RecordingSession`.
Returns:
List of `Video` objects in `RecordingSession`.
"""
return list(self._video_by_camera.values())
@property
def cameras(self) -> "list[Camera]":
"""Get list of `Camera` objects linked to `Video`s in the `RecordingSession`.
Returns:
List of `Camera` objects in `RecordingSession`.
"""
return list(self._video_by_camera.keys())
def get_camera(self, video: Video) -> "Camera | None":
"""Get `Camera` associated with `video`.
Args:
video: `Video` to get `Camera`
Returns:
`Camera` associated with `video` or None if not found
"""
return self._camera_by_video.get(video, None)
def get_video(self, camera: "Camera") -> Video | None:
"""Get `Video` associated with `camera`.
Args:
camera: `Camera` to get `Video`
Returns:
`Video` associated with `camera` or None if not found
"""
return self._video_by_camera.get(camera, None)
def add_video(self, video: Video, camera: "Camera"):
"""Add `video` to `RecordingSession` and mapping to `camera`.
Args:
video: `Video` object to add to `RecordingSession`.
camera: `Camera` object to associate with `video`.
Raises:
ValueError: If `camera` is not in associated `CameraGroup`.
ValueError: If `video` is not a `Video` object.
"""
# Raise ValueError if camera is not in associated camera group
self.camera_group.cameras.index(camera)
# Raise ValueError if `Video` is not a `Video` object
if not isinstance(video, Video):
raise ValueError(
f"Expected `Video` object, but received {type(video)} object."
)
# Add camera to video mapping
self._video_by_camera[camera] = video
# Add video to camera mapping
self._camera_by_video[video] = camera
def remove_video(self, video: Video):
"""Remove `video` from `RecordingSession` and mapping to `Camera`.
Args:
video: `Video` object to remove from `RecordingSession`.
Raises:
ValueError: If `video` is not in associated `RecordingSession`.
"""
# Remove video from camera mapping
camera = self._camera_by_video.pop(video)
# Remove camera from video mapping
self._video_by_camera.pop(camera)
def __repr__(self) -> str:
"""Return a readable representation of the session."""
return (
"RecordingSession("
f"camera_group={len(self.camera_group.cameras)}cameras, "
f"videos={len(self.videos)}, "
f"frame_groups={len(self.frame_groups)}"
")"
)
__annotations__ = {'camera_group': 'CameraGroup', '_video_by_camera': "'dict[Camera, Video]'", '_camera_by_video': "'dict[Video, Camera]'", '_frame_group_by_frame_idx': "'dict[int, FrameGroup]'", 'metadata': 'dict'}
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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = 'A recording session with multiple cameras.\n\nAttributes:\n camera_group: `CameraGroup` object containing cameras in the session.\n frame_groups: Dictionary mapping frame index to `FrameGroup`.\n videos: List of `Video` objects linked to `Camera`s in the session.\n cameras: List of `Camera` objects linked to `Video`s in the session.\n metadata: Dictionary of metadata.\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__ = 128
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 literal.
int('0b100', base=0) 4
__match_args__ = ('camera_group', '_video_by_camera', '_camera_by_video', '_frame_group_by_frame_idx', 'metadata')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.camera'
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__ = ('camera_group', '_video_by_camera', '_camera_by_video', '_frame_group_by_frame_idx', 'metadata', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
cameras
property
¶
Get list of Camera objects linked to Videos in the RecordingSession.
Returns:
| Type | Description |
|---|---|
|
List of |
frame_groups
property
¶
Get dictionary of FrameGroup objects by frame index.
Returns:
| Type | Description |
|---|---|
|
Dictionary of |
videos
property
¶
Get list of Video objects in the RecordingSession.
Returns:
| Type | Description |
|---|---|
|
List of |
__init__(camera_group=NOTHING, video_by_camera=NOTHING, camera_by_video=NOTHING, frame_group_by_frame_idx=NOTHING, metadata=NOTHING)
¶
Method generated by attrs for class RecordingSession.
Source code in sleap_io/model/camera.py
"""Data structure for a single camera view in a multi-camera setup."""
from __future__ import annotations
import attrs
import numpy as np
from attrs import define, field
from attrs.validators import instance_of
from sleap_io.model.instance import Instance
from sleap_io.model.labeled_frame import LabeledFrame
from sleap_io.model.video import Video
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between:
1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix
2. A 3x3 rotation matrix to a 3D rotation vector
Args:
input_matrix: A 3x3 rotation matrix or a 3x1 rotation vector.
Returns:
A tuple containing the converted matrix/vector and the Jacobian (None for now).
Raises:
__replace__(**changes)
¶
Method generated by attrs for class RecordingSession.
__repr__()
¶
Return a readable representation of the session.
__setattr__(name, val)
¶
Method generated by attrs for class RecordingSession.
add_video(video, camera)
¶
Add video to RecordingSession and mapping to camera.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Video
|
|
required |
camera
|
Camera
|
|
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ValueError
|
If |
Source code in sleap_io/model/camera.py
def add_video(self, video: Video, camera: "Camera"):
"""Add `video` to `RecordingSession` and mapping to `camera`.
Args:
video: `Video` object to add to `RecordingSession`.
camera: `Camera` object to associate with `video`.
Raises:
ValueError: If `camera` is not in associated `CameraGroup`.
ValueError: If `video` is not a `Video` object.
"""
# Raise ValueError if camera is not in associated camera group
self.camera_group.cameras.index(camera)
# Raise ValueError if `Video` is not a `Video` object
if not isinstance(video, Video):
raise ValueError(
f"Expected `Video` object, but received {type(video)} object."
)
# Add camera to video mapping
self._video_by_camera[camera] = video
# Add video to camera mapping
self._camera_by_video[video] = camera
get_camera(video)
¶
get_video(camera)
¶
remove_video(video)
¶
Remove video from RecordingSession and mapping to Camera.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Video
|
|
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Source code in sleap_io/model/camera.py
def remove_video(self, video: Video):
"""Remove `video` from `RecordingSession` and mapping to `Camera`.
Args:
video: `Video` object to remove from `RecordingSession`.
Raises:
ValueError: If `video` is not in associated `RecordingSession`.
"""
# Remove video from camera mapping
camera = self._camera_by_video.pop(video)
# Remove camera from video mapping
self._video_by_camera.pop(camera)
Video
¶
Video class used by sleap to represent videos and data associated with them.
This class is used to store information regarding a video and its components.
It is used to store the video's filename, shape, and the video's backend.
To create a Video object, use the from_filename method which will select the
backend appropriately.
Attributes:
| Name | Type | Description |
|---|---|---|
filename |
The filename(s) of the video. Supported extensions: "mp4", "avi", "mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif", "tiff", "bmp". If the filename is a list, a list of image filenames are expected. If filename is a folder, it will be searched for images. |
|
backend |
An object that implements the basic methods for reading and manipulating frames of a specific video type. |
|
backend_metadata |
A dictionary of metadata specific to the backend. This is useful for storing metadata that requires an open backend (e.g., shape information) without having access to the video file itself. |
|
source_video |
The source video object if this is a proxy video. This is present when the video contains an embedded subset of frames from another video. |
|
open_backend |
Whether to open the backend when the video is available. If |
Notes
Instances of this class are hashed by identity, not by value. This means that
two Video instances with the same attributes will NOT be considered equal in a
set or dict.
Media Video Plugin Support
For media files (mp4, avi, etc.), the following plugins are supported: - "opencv": Uses OpenCV (cv2) for video reading - "FFMPEG": Uses imageio-ffmpeg for video reading - "pyav": Uses PyAV for video reading
Plugin aliases (case-insensitive): - opencv: "opencv", "cv", "cv2", "ocv" - FFMPEG: "FFMPEG", "ffmpeg", "imageio-ffmpeg", "imageio_ffmpeg" - pyav: "pyav", "av"
Plugin selection priority: 1. Explicitly specified plugin parameter 2. Backend metadata plugin value 3. Global default (set via sio.set_default_video_plugin) 4. Auto-detection based on available packages
See Also
VideoBackend: The backend interface for reading video data. sleap_io.set_default_video_plugin: Set global default plugin. sleap_io.get_default_video_plugin: Get current default plugin.
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Post init syntactic sugar. |
__deepcopy__ |
Deep copy the video object. |
__getitem__ |
Return the frames of the video at the given indices. |
__init__ |
Method generated by attrs for class Video. |
__len__ |
Return the length of the video as the number of frames. |
__replace__ |
Method generated by attrs for class Video. |
__repr__ |
Informal string representation (for print or format). |
__str__ |
Informal string representation (for print or format). |
close |
Close the video backend. |
deduplicate_with |
Create a new video with duplicate images removed. |
exists |
Check if the video file exists and is accessible. |
frame_to_seconds |
Convert a frame index to timestamp in seconds. |
from_filename |
Create a Video from a filename. |
has_overlapping_images |
Check if this video has overlapping images with another video. |
matches_content |
Check if this video has the same content as another video. |
matches_path |
Check if this video has the same path as another video. |
matches_shape |
Check if this video has the same shape as another video. |
merge_with |
Merge another video's images into this one. |
open |
Open the video backend for reading. |
replace_filename |
Update the filename of the video, optionally opening the backend. |
save |
Save video frames to a new video file. |
seconds_to_frame |
Convert a timestamp in seconds to frame index. |
set_video_plugin |
Set the video plugin and reopen the video. |
Source code in sleap_io/model/video.py
@attrs.define(eq=False)
class Video:
"""`Video` class used by sleap to represent videos and data associated with them.
This class is used to store information regarding a video and its components.
It is used to store the video's `filename`, `shape`, and the video's `backend`.
To create a `Video` object, use the `from_filename` method which will select the
backend appropriately.
Attributes:
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.
backend: An object that implements the basic methods for reading and
manipulating frames of a specific video type.
backend_metadata: A dictionary of metadata specific to the backend. This is
useful for storing metadata that requires an open backend (e.g., shape
information) without having access to the video file itself.
source_video: The source video object if this is a proxy video. This is present
when the video contains an embedded subset of frames from another video.
open_backend: Whether to open the backend when the video is available. If `True`
(the default), the backend will be automatically opened if the video exists.
Set this to `False` when you want to manually open the backend, or when the
you know the video file does not exist and you want to avoid trying to open
the file.
Notes:
Instances of this class are hashed by identity, not by value. This means that
two `Video` instances with the same attributes will NOT be considered equal in a
set or dict.
Media Video Plugin Support:
For media files (mp4, avi, etc.), the following plugins are supported:
- "opencv": Uses OpenCV (cv2) for video reading
- "FFMPEG": Uses imageio-ffmpeg for video reading
- "pyav": Uses PyAV for video reading
Plugin aliases (case-insensitive):
- opencv: "opencv", "cv", "cv2", "ocv"
- FFMPEG: "FFMPEG", "ffmpeg", "imageio-ffmpeg", "imageio_ffmpeg"
- pyav: "pyav", "av"
Plugin selection priority:
1. Explicitly specified plugin parameter
2. Backend metadata plugin value
3. Global default (set via sio.set_default_video_plugin)
4. Auto-detection based on available packages
See Also:
VideoBackend: The backend interface for reading video data.
sleap_io.set_default_video_plugin: Set global default plugin.
sleap_io.get_default_video_plugin: Get current default plugin.
"""
filename: str | list[str]
backend: VideoBackend | None = None
backend_metadata: dict[str, any] = attrs.field(factory=dict)
source_video: "Video | None" = None
open_backend: bool = True
EXTS = MediaVideo.EXTS + HDF5Video.EXTS + ImageVideo.EXTS
@property
def original_video(self) -> "Video | None":
"""The root video in the provenance chain.
For embedded videos, this returns the ultimate source video by
traversing the source_video chain. Returns None if this video
has no source_video (i.e., it IS an original).
This property is computed by following the source_video chain to find
the root. For a single-level embedding (A embeds from B), original_video
returns B. For multi-level embedding (A <- B <- C), it returns C.
"""
if self.source_video is None:
return None # This IS the original
# Traverse to root
v = self.source_video
while v.source_video is not None:
v = v.source_video
return v
def __attrs_post_init__(self):
"""Post init syntactic sugar."""
if self.open_backend and self.backend is None and self.exists():
try:
self.open()
except Exception:
# If we can't open the backend, just ignore it for now so we don't
# prevent the user from building the Video object entirely.
pass
def __deepcopy__(self, memo):
"""Deep copy the video object."""
if id(self) in memo:
return memo[id(self)]
reopen = False
if self.is_open:
reopen = True
self.close()
new_video = Video(
filename=self.filename,
backend=None,
backend_metadata=self.backend_metadata.copy(),
source_video=self.source_video,
open_backend=self.open_backend,
)
memo[id(self)] = new_video
if reopen:
self.open()
return new_video
@classmethod
def from_filename(
cls,
filename: str | list[str],
dataset: str | None = None,
grayscale: bool | None = None,
keep_open: bool = True,
source_video: "Video | None" = None,
**kwargs,
) -> VideoBackend:
"""Create a Video from a filename.
Args:
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.
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.
source_video: The source video object if this is a proxy video. This is
present when the video contains an embedded subset of frames from
another video.
**kwargs: Additional backend-specific arguments passed to
VideoBackend.from_filename. See VideoBackend.from_filename for supported
arguments.
Returns:
Video instance with the appropriate backend instantiated.
"""
backend = VideoBackend.from_filename(
filename,
dataset=dataset,
grayscale=grayscale,
keep_open=keep_open,
**kwargs,
)
# If filename is a directory, VideoBackend.from_filename will expand it
# to a list of paths to images contained within the directory. In this
# case we want to use the expanded list as filename
return cls(
filename=backend.filename,
backend=backend,
source_video=source_video,
)
@property
def shape(self) -> tuple[int, int, int, int] | None:
"""Return the shape of the video as (num_frames, height, width, channels).
If the video backend is not set or it cannot determine the shape of the video,
this will return None.
"""
return self._get_shape()
def _get_shape(self) -> tuple[int, int, int, int] | None:
"""Return the shape of the video as (num_frames, height, width, channels).
This suppresses errors related to querying the backend for the video shape, such
as when it has not been set or when the video file is not found.
"""
try:
return self.backend.shape
except Exception:
if "shape" in self.backend_metadata:
return self.backend_metadata["shape"]
return None
@property
def grayscale(self) -> bool | None:
"""Return whether the video is grayscale.
If the video backend is not set or it cannot determine whether the video is
grayscale, this will return None.
"""
shape = self.shape
if shape is not None:
return shape[-1] == 1
else:
grayscale = None
if "grayscale" in self.backend_metadata:
grayscale = self.backend_metadata["grayscale"]
return grayscale
@grayscale.setter
def grayscale(self, value: bool):
"""Set the grayscale value and adjust the backend."""
if self.backend is not None:
self.backend.grayscale = value
self.backend._cached_shape = None
self.backend_metadata["grayscale"] = value
@property
def fps(self) -> float | None:
"""Return the frames per second of the video.
For MediaVideo backends, this reads FPS from the video container metadata.
For other backends (ImageVideo, HDF5Video, TiffVideo), this returns the
explicitly set value or None if not set.
Returns:
The FPS if known, or None if unavailable/unknown.
"""
if self.backend is not None:
return self.backend.fps
return self.backend_metadata.get("fps")
@fps.setter
def fps(self, value: float | None):
"""Set the frames per second.
Args:
value: Frames per second. Must be positive if not None.
Raises:
ValueError: If value is not positive.
Notes:
For MediaVideo backends, setting FPS overrides the value from container
metadata. For other backends, this sets the FPS directly.
"""
if value is not None and value <= 0:
raise ValueError(f"FPS must be positive, got {value}")
if self.backend is not None:
self.backend.fps = value
self.backend_metadata["fps"] = value
def frame_to_seconds(self, frame_idx: int) -> float | None:
"""Convert a frame index to timestamp in seconds.
Args:
frame_idx: Zero-indexed frame number.
Returns:
Time in seconds, or None if FPS is unknown.
Notes:
This assumes constant frame rate. For variable frame rate videos,
the returned timestamp may be approximate.
"""
if self.fps is None or self.fps <= 0:
return None
return frame_idx / self.fps
def seconds_to_frame(self, seconds: float) -> int | None:
"""Convert a timestamp in seconds to frame index.
Args:
seconds: Time in seconds from video start.
Returns:
Zero-indexed frame number (rounded down), or None if FPS unknown.
"""
if self.fps is None or self.fps <= 0:
return None
return int(seconds * self.fps)
def __len__(self) -> int:
"""Return the length of the video as the number of frames."""
shape = self.shape
return 0 if shape is None else shape[0]
def __repr__(self) -> str:
"""Informal string representation (for print or format)."""
dataset = (
f"dataset={self.backend.dataset}, "
if getattr(self.backend, "dataset", "")
else ""
)
return (
"Video("
f'filename="{self.filename}", '
f"shape={self.shape}, "
f"{dataset}"
f"backend={type(self.backend).__name__}"
")"
)
def __str__(self) -> str:
"""Informal string representation (for print or format)."""
return self.__repr__()
def __getitem__(self, inds: int | list[int] | slice) -> np.ndarray:
"""Return the frames of the video at the given indices.
Args:
inds: 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: VideoBackend.get_frame, VideoBackend.get_frames
"""
if not self.is_open:
if self.open_backend:
self.open()
else:
raise ValueError(
"Video backend is not open. Call video.open() or set "
"video.open_backend to True to do automatically on frame read."
)
return self.backend[inds]
def exists(self, check_all: bool = False, dataset: str | None = None) -> bool:
"""Check if the video file exists and is accessible.
Args:
check_all: If `True`, check that all filenames in a list exist. If `False`
(the default), check that the first filename exists.
dataset: Name of dataset in HDF5 file. If specified, this will function will
return `False` if the dataset does not exist.
Returns:
`True` if the file exists and is accessible, `False` otherwise.
"""
if isinstance(self.filename, list):
if check_all:
for f in self.filename:
if not is_file_accessible(f):
return False
return True
else:
return is_file_accessible(self.filename[0])
file_is_accessible = is_file_accessible(self.filename)
if not file_is_accessible:
return False
if dataset is None or dataset == "":
dataset = self.backend_metadata.get("dataset", None)
if dataset is not None and dataset != "":
has_dataset = False
if (
self.backend is not None
and type(self.backend) is HDF5Video
and self.backend._open_reader is not None
):
has_dataset = dataset in self.backend._open_reader
else:
with h5py.File(self.filename, "r") as f:
has_dataset = dataset in f
return has_dataset
return True
@property
def is_open(self) -> bool:
"""Check if the video backend is open."""
return self.exists() and self.backend is not None
def open(
self,
filename: str | None = None,
dataset: str | None = None,
grayscale: str | None = None,
keep_open: bool = True,
plugin: str | None = None,
):
"""Open the video backend for reading.
Args:
filename: Filename to open. If not specified, will use the filename set on
the video object.
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.
plugin: Video plugin to use for MediaVideo files. One of "opencv",
"FFMPEG", or "pyav". Also accepts aliases (case-insensitive).
If not specified, uses the backend metadata, global default,
or auto-detection in that order.
Notes:
This is useful for opening the video backend to read frames and then closing
it after reading all the necessary frames.
If the backend was already open, it will be closed before opening a new one.
Values for the HDF5 dataset and grayscale will be remembered if not
specified.
"""
if filename is not None:
self.replace_filename(filename, open=False)
# Try to remember values from previous backend if available and not specified.
if self.backend is not None:
if dataset is None:
dataset = getattr(self.backend, "dataset", None)
if grayscale is None:
grayscale = getattr(self.backend, "grayscale", None)
else:
if dataset is None and "dataset" in self.backend_metadata:
dataset = self.backend_metadata["dataset"]
if grayscale is None:
if "grayscale" in self.backend_metadata:
grayscale = self.backend_metadata["grayscale"]
elif "shape" in self.backend_metadata:
grayscale = self.backend_metadata["shape"][-1] == 1
if not self.exists(dataset=dataset):
msg = (
f"Video does not exist or cannot be opened for reading: {self.filename}"
)
if dataset is not None:
msg += f" (dataset: {dataset})"
raise FileNotFoundError(msg)
# Close previous backend if open.
self.close()
# Handle plugin parameter
backend_kwargs = {}
if plugin is not None:
from sleap_io.io.video_reading import normalize_plugin_name
plugin = normalize_plugin_name(plugin)
self.backend_metadata["plugin"] = plugin
if "plugin" in self.backend_metadata:
backend_kwargs["plugin"] = self.backend_metadata["plugin"]
# Create new backend.
self.backend = VideoBackend.from_filename(
self.filename,
dataset=dataset,
grayscale=grayscale,
keep_open=keep_open,
**backend_kwargs,
)
def close(self):
"""Close the video backend."""
if self.backend is not None:
# Try to remember values from previous backend if available and not
# specified.
try:
self.backend_metadata["dataset"] = getattr(
self.backend, "dataset", None
)
self.backend_metadata["grayscale"] = getattr(
self.backend, "grayscale", None
)
self.backend_metadata["shape"] = getattr(self.backend, "shape", None)
self.backend_metadata["fps"] = getattr(self.backend, "fps", None)
except Exception:
pass
del self.backend
self.backend = None
def replace_filename(
self, new_filename: str | Path | list[str] | list[Path], open: bool = True
):
"""Update the filename of the video, optionally opening the backend.
Args:
new_filename: New filename to set for the video.
open: If `True` (the default), open the backend with the new filename. If
the new filename does not exist, no error is raised.
"""
if isinstance(new_filename, Path):
new_filename = new_filename.as_posix()
if isinstance(new_filename, list):
new_filename = [
p.as_posix() if isinstance(p, Path) else p for p in new_filename
]
self.filename = new_filename
self.backend_metadata["filename"] = new_filename
if open:
if self.exists():
self.open()
else:
self.close()
def matches_path(self, other: "Video", strict: bool = False) -> bool:
"""Check if this video has the same path as another video.
Args:
other: Another video to compare with.
strict: If True, require exact path match. If False, consider videos
with the same filename (basename) as matching.
Returns:
True if the videos have matching paths, False otherwise.
Notes:
For HDF5 video backends (e.g., embedded videos in .pkg.slp files),
matching prioritizes the source_filename attribute since multiple
videos can share the same HDF5 file path but reference different
source videos. Falls back to dataset name matching if source_filename
is not available.
"""
# Handle HDF5 backends specially - prioritize source_filename matching
self_is_hdf5 = isinstance(self.backend, HDF5Video)
other_is_hdf5 = isinstance(other.backend, HDF5Video)
if self_is_hdf5 and other_is_hdf5:
# Both are HDF5 videos - match by source_filename first
self_source = self.backend.source_filename
other_source = other.backend.source_filename
if self_source is not None and other_source is not None:
if strict:
return Path(self_source).resolve() == Path(other_source).resolve()
else:
return Path(self_source).name == Path(other_source).name
# Fall back to dataset name matching if source_filename is not available
self_dataset = self.backend.dataset
other_dataset = other.backend.dataset
if self_dataset is not None and other_dataset is not None:
return self_dataset == other_dataset
# If neither source_filename nor dataset available, cannot match
return False
if isinstance(self.filename, list) and isinstance(other.filename, list):
# Both are image sequences
if strict:
return self.filename == other.filename
else:
# Compare basenames
self_basenames = [Path(f).name for f in self.filename]
other_basenames = [Path(f).name for f in other.filename]
return self_basenames == other_basenames
elif isinstance(self.filename, list) or isinstance(other.filename, list):
# One is image sequence, other is single file
return False
else:
# Both are single files
if strict:
return Path(self.filename).resolve() == Path(other.filename).resolve()
else:
return Path(self.filename).name == Path(other.filename).name
def matches_content(self, other: "Video") -> bool:
"""Check if this video has the same content as another video.
Args:
other: Another video to compare with.
Returns:
True if the videos have the same shape and backend type.
Notes:
This compares metadata like shape and backend type, not actual frame data.
"""
# Compare shapes
self_shape = self.shape
other_shape = other.shape
if self_shape != other_shape:
return False
# Compare backend types
if self.backend is None and other.backend is None:
return True
elif self.backend is None or other.backend is None:
return False
return type(self.backend).__name__ == type(other.backend).__name__
def matches_shape(self, other: "Video") -> bool:
"""Check if this video has the same shape as another video.
Args:
other: Another video to compare with.
Returns:
True if the videos have the same height, width, and channels.
Notes:
This only compares spatial dimensions, not the number of frames.
"""
# Try to get shape from backend metadata first if shape is not available
if self.backend is None and "shape" in self.backend_metadata:
self_shape = self.backend_metadata["shape"]
else:
self_shape = self.shape
if other.backend is None and "shape" in other.backend_metadata:
other_shape = other.backend_metadata["shape"]
else:
other_shape = other.shape
# Handle None shapes
if self_shape is None or other_shape is None:
return False
# Compare only height, width, channels (not frames)
return self_shape[1:] == other_shape[1:]
def has_overlapping_images(self, other: "Video") -> bool:
"""Check if this video has overlapping images with another video.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to compare with.
Returns:
True if both are ImageVideo instances with overlapping image files.
False if either video is not an ImageVideo or no overlap exists.
Notes:
Only works with ImageVideo backends where filename is a list.
Compares individual image filenames (basenames only).
"""
# Both must be image sequences
if not (isinstance(self.filename, list) and isinstance(other.filename, list)):
return False
# Get basenames for comparison
self_basenames = set(Path(f).name for f in self.filename)
other_basenames = set(Path(f).name for f in other.filename)
# Check if there's any overlap
return len(self_basenames & other_basenames) > 0
def deduplicate_with(self, other: "Video") -> "Video":
"""Create a new video with duplicate images removed.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to deduplicate against. Must also be ImageVideo.
Returns:
A new Video object with duplicate images removed from this video,
or None if all images were duplicates.
Raises:
ValueError: If either video is not an ImageVideo backend.
Notes:
Only works with ImageVideo backends where filename is a list.
Images are considered duplicates if they have the same basename.
The returned video contains only images from this video that are
not present in the other video.
"""
if not isinstance(self.filename, list):
raise ValueError("deduplicate_with only works with ImageVideo backends")
if not isinstance(other.filename, list):
raise ValueError("Other video must also be ImageVideo backend")
# Get basenames from other video
other_basenames = set(Path(f).name for f in other.filename)
# Keep only non-duplicate images
deduplicated_paths = [
f for f in self.filename if Path(f).name not in other_basenames
]
if not deduplicated_paths:
# All images were duplicates
return None
# Create new video with deduplicated images
return Video.from_filename(deduplicated_paths, grayscale=self.grayscale)
def merge_with(self, other: "Video") -> "Video":
"""Merge another video's images into this one.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to merge with. Must also be ImageVideo.
Returns:
A new Video object with unique images from both videos.
Raises:
ValueError: If either video is not an ImageVideo backend.
Notes:
Only works with ImageVideo backends where filename is a list.
The merged video contains all unique images from both videos,
with automatic deduplication based on image basename.
"""
if not isinstance(self.filename, list):
raise ValueError("merge_with only works with ImageVideo backends")
if not isinstance(other.filename, list):
raise ValueError("Other video must also be ImageVideo backend")
# Get all unique images (by basename) preserving order
seen_basenames = set()
merged_paths = []
for path in self.filename:
basename = Path(path).name
if basename not in seen_basenames:
merged_paths.append(path)
seen_basenames.add(basename)
for path in other.filename:
basename = Path(path).name
if basename not in seen_basenames:
merged_paths.append(path)
seen_basenames.add(basename)
# Create new video with merged images
return Video.from_filename(merged_paths, grayscale=self.grayscale)
def save(
self,
save_path: str | Path,
frame_inds: list[int] | np.ndarray | None = None,
fps: float | None = None,
video_kwargs: dict[str, Any] | None = None,
) -> "Video":
"""Save video frames to a new video file.
Args:
save_path: Path to the new video file. Should end in MP4.
frame_inds: Frame indices to save. Can be specified as a list or array of
frame integers. If not specified, saves all video frames.
fps: Frames per second for the output video. If not specified, uses the
source video's FPS if available, otherwise defaults to 30.
video_kwargs: A dictionary of keyword arguments to provide to
`sio.save_video` for video compression.
Returns:
A new `Video` object pointing to the new video file.
"""
video_kwargs = {} if video_kwargs is None else video_kwargs.copy()
frame_inds = np.arange(len(self)) if frame_inds is None else frame_inds
# Use source video FPS if not explicitly specified
if fps is None:
fps = self.fps
if fps is not None and "fps" not in video_kwargs:
video_kwargs["fps"] = fps
with VideoWriter(save_path, **video_kwargs) as vw:
for frame_ind in frame_inds:
vw(self[frame_ind])
new_video = Video.from_filename(save_path, grayscale=self.grayscale)
return new_video
def set_video_plugin(self, plugin: str) -> None:
"""Set the video plugin and reopen the video.
Args:
plugin: Video plugin to use. One of "opencv", "FFMPEG", or "pyav".
Also accepts aliases (case-insensitive).
Raises:
ValueError: If the video is not a MediaVideo type.
Examples:
>>> video.set_video_plugin("opencv")
>>> video.set_video_plugin("CV2") # Same as "opencv"
"""
from sleap_io.io.video_reading import MediaVideo, normalize_plugin_name
if not self.filename.endswith(MediaVideo.EXTS):
raise ValueError(f"Cannot set plugin for non-media video: {self.filename}")
plugin = normalize_plugin_name(plugin)
# Close current backend if open
was_open = self.is_open
if was_open:
self.close()
# Update backend metadata
self.backend_metadata["plugin"] = plugin
# Reopen with new plugin if it was open
if was_open:
self.open()
EXTS = ('mp4', 'avi', 'mov', 'mj2', 'mkv', 'h5', 'hdf5', 'slp', '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__ = {'filename': 'str | list[str]', 'backend': 'VideoBackend | None', 'backend_metadata': 'dict[str, any]', 'source_video': "'Video | None'", 'open_backend': '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__ = 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=False, 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 0x7f41e68fca40>, field_transformer=None)
class-attribute
¶
Effective class properties as derived from parameters to attr.s() or
define() decorators.
This is the same data structure that attrs uses internally to decide how to construct the final class.
Warning:
This feature is currently **experimental** and is not covered by our
strict backwards-compatibility guarantees.
Attributes:
| Name | Type | Description |
|---|---|---|
is_exception |
bool
|
Whether the class is treated as an exception class. |
is_slotted |
bool
|
Whether the class is |
has_weakref_slot |
bool
|
Whether the class has a slot for weak references. |
is_frozen |
bool
|
Whether the class is frozen. |
kw_only |
KeywordOnly
|
Whether / how the class enforces keyword-only arguments on the
|
collected_fields_by_mro |
bool
|
Whether the class fields were collected by method resolution order.
That is, correctly but unlike |
added_init |
bool
|
Whether the class has an attrs-generated |
added_repr |
bool
|
Whether the class has an attrs-generated |
added_eq |
bool
|
Whether the class has attrs-generated equality methods. |
added_ordering |
bool
|
Whether the class has attrs-generated ordering methods. |
hashability |
Hashability
|
How |
added_match_args |
bool
|
Whether the class supports positional |
added_str |
bool
|
Whether the class has an attrs-generated |
added_pickling |
bool
|
Whether the class has attrs-generated |
on_setattr_hook |
Callable[[Any, Attribute[Any], Any], Any] | None
|
The class's |
field_transformer |
Callable[[Attribute[Any]], Attribute[Any]] | None
|
The class's |
.. versionadded:: 25.4.0
__doc__ = '`Video` class used by sleap to represent videos and data associated with them.\n\nThis class is used to store information regarding a video and its components.\nIt is used to store the video\'s `filename`, `shape`, and the video\'s `backend`.\n\nTo create a `Video` object, use the `from_filename` method which will select the\nbackend appropriately.\n\nAttributes:\n filename: The filename(s) of the video. Supported extensions: "mp4", "avi",\n "mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",\n "tiff", "bmp". If the filename is a list, a list of image filenames are\n expected. If filename is a folder, it will be searched for images.\n backend: An object that implements the basic methods for reading and\n manipulating frames of a specific video type.\n backend_metadata: A dictionary of metadata specific to the backend. This is\n useful for storing metadata that requires an open backend (e.g., shape\n information) without having access to the video file itself.\n source_video: The source video object if this is a proxy video. This is present\n when the video contains an embedded subset of frames from another video.\n open_backend: Whether to open the backend when the video is available. If `True`\n (the default), the backend will be automatically opened if the video exists.\n Set this to `False` when you want to manually open the backend, or when the\n you know the video file does not exist and you want to avoid trying to open\n the file.\n\nNotes:\n Instances of this class are hashed by identity, not by value. This means that\n two `Video` instances with the same attributes will NOT be considered equal in a\n set or dict.\n\nMedia Video Plugin Support:\n For media files (mp4, avi, etc.), the following plugins are supported:\n - "opencv": Uses OpenCV (cv2) for video reading\n - "FFMPEG": Uses imageio-ffmpeg for video reading\n - "pyav": Uses PyAV for video reading\n\n Plugin aliases (case-insensitive):\n - opencv: "opencv", "cv", "cv2", "ocv"\n - FFMPEG: "FFMPEG", "ffmpeg", "imageio-ffmpeg", "imageio_ffmpeg"\n - pyav: "pyav", "av"\n\n Plugin selection priority:\n 1. Explicitly specified plugin parameter\n 2. Backend metadata plugin value\n 3. Global default (set via sio.set_default_video_plugin)\n 4. Auto-detection based on available packages\n\nSee Also:\n VideoBackend: The backend interface for reading video data.\n sleap_io.set_default_video_plugin: Set global default plugin.\n sleap_io.get_default_video_plugin: Get current default plugin.\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__ = 21
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 literal.
int('0b100', base=0) 4
__match_args__ = ('filename', 'backend', 'backend_metadata', 'source_video', 'open_backend')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__module__ = 'sleap_io.model.video'
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', 'backend', 'backend_metadata', 'source_video', 'open_backend', '__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__ = ('backend', 'filename')
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
¶
Return the frames per second of the video.
For MediaVideo backends, this reads FPS from the video container metadata. For other backends (ImageVideo, HDF5Video, TiffVideo), this returns the explicitly set value or None if not set.
Returns:
| Type | Description |
|---|---|
|
The FPS if known, or None if unavailable/unknown. |
grayscale
property
¶
Return whether the video is grayscale.
If the video backend is not set or it cannot determine whether the video is grayscale, this will return None.
is_open
property
¶
Check if the video backend is open.
original_video
property
¶
The root video in the provenance chain.
For embedded videos, this returns the ultimate source video by traversing the source_video chain. Returns None if this video has no source_video (i.e., it IS an original).
This property is computed by following the source_video chain to find the root. For a single-level embedding (A embeds from B), original_video returns B. For multi-level embedding (A <- B <- C), it returns C.
shape
property
¶
Return the shape of the video as (num_frames, height, width, channels).
If the video backend is not set or it cannot determine the shape of the video, this will return None.
__attrs_post_init__()
¶
Post init syntactic sugar.
Source code in sleap_io/model/video.py
__deepcopy__(memo)
¶
Deep copy the video object.
Source code in sleap_io/model/video.py
def __deepcopy__(self, memo):
"""Deep copy the video object."""
if id(self) in memo:
return memo[id(self)]
reopen = False
if self.is_open:
reopen = True
self.close()
new_video = Video(
filename=self.filename,
backend=None,
backend_metadata=self.backend_metadata.copy(),
source_video=self.source_video,
open_backend=self.open_backend,
)
memo[id(self)] = new_video
if reopen:
self.open()
return new_video
__getitem__(inds)
¶
Return the frames of the video at the given indices.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
inds
|
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 |
See also: VideoBackend.get_frame, VideoBackend.get_frames
Source code in sleap_io/model/video.py
def __getitem__(self, inds: int | list[int] | slice) -> np.ndarray:
"""Return the frames of the video at the given indices.
Args:
inds: 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: VideoBackend.get_frame, VideoBackend.get_frames
"""
if not self.is_open:
if self.open_backend:
self.open()
else:
raise ValueError(
"Video backend is not open. Call video.open() or set "
"video.open_backend to True to do automatically on frame read."
)
return self.backend[inds]
__init__(filename, backend=None, backend_metadata=NOTHING, source_video=None, open_backend=True)
¶
Method generated by attrs for class Video.
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class Video.
__repr__()
¶
Informal string representation (for print or format).
Source code in sleap_io/model/video.py
def __repr__(self) -> str:
"""Informal string representation (for print or format)."""
dataset = (
f"dataset={self.backend.dataset}, "
if getattr(self.backend, "dataset", "")
else ""
)
return (
"Video("
f'filename="{self.filename}", '
f"shape={self.shape}, "
f"{dataset}"
f"backend={type(self.backend).__name__}"
")"
)
__str__()
¶
close()
¶
Close the video backend.
Source code in sleap_io/model/video.py
def close(self):
"""Close the video backend."""
if self.backend is not None:
# Try to remember values from previous backend if available and not
# specified.
try:
self.backend_metadata["dataset"] = getattr(
self.backend, "dataset", None
)
self.backend_metadata["grayscale"] = getattr(
self.backend, "grayscale", None
)
self.backend_metadata["shape"] = getattr(self.backend, "shape", None)
self.backend_metadata["fps"] = getattr(self.backend, "fps", None)
except Exception:
pass
del self.backend
self.backend = None
deduplicate_with(other)
¶
Create a new video with duplicate images removed.
This method is specifically for ImageVideo backends (image sequences).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to deduplicate against. Must also be ImageVideo. |
required |
Returns:
| Type | Description |
|---|---|
Video
|
A new Video object with duplicate images removed from this video, or None if all images were duplicates. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If either video is not an ImageVideo backend. |
Notes
Only works with ImageVideo backends where filename is a list. Images are considered duplicates if they have the same basename. The returned video contains only images from this video that are not present in the other video.
Source code in sleap_io/model/video.py
def deduplicate_with(self, other: "Video") -> "Video":
"""Create a new video with duplicate images removed.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to deduplicate against. Must also be ImageVideo.
Returns:
A new Video object with duplicate images removed from this video,
or None if all images were duplicates.
Raises:
ValueError: If either video is not an ImageVideo backend.
Notes:
Only works with ImageVideo backends where filename is a list.
Images are considered duplicates if they have the same basename.
The returned video contains only images from this video that are
not present in the other video.
"""
if not isinstance(self.filename, list):
raise ValueError("deduplicate_with only works with ImageVideo backends")
if not isinstance(other.filename, list):
raise ValueError("Other video must also be ImageVideo backend")
# Get basenames from other video
other_basenames = set(Path(f).name for f in other.filename)
# Keep only non-duplicate images
deduplicated_paths = [
f for f in self.filename if Path(f).name not in other_basenames
]
if not deduplicated_paths:
# All images were duplicates
return None
# Create new video with deduplicated images
return Video.from_filename(deduplicated_paths, grayscale=self.grayscale)
exists(check_all=False, dataset=None)
¶
Check if the video file exists and is accessible.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
check_all
|
bool
|
If |
False
|
dataset
|
str | None
|
Name of dataset in HDF5 file. If specified, this will function will
return |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in sleap_io/model/video.py
def exists(self, check_all: bool = False, dataset: str | None = None) -> bool:
"""Check if the video file exists and is accessible.
Args:
check_all: If `True`, check that all filenames in a list exist. If `False`
(the default), check that the first filename exists.
dataset: Name of dataset in HDF5 file. If specified, this will function will
return `False` if the dataset does not exist.
Returns:
`True` if the file exists and is accessible, `False` otherwise.
"""
if isinstance(self.filename, list):
if check_all:
for f in self.filename:
if not is_file_accessible(f):
return False
return True
else:
return is_file_accessible(self.filename[0])
file_is_accessible = is_file_accessible(self.filename)
if not file_is_accessible:
return False
if dataset is None or dataset == "":
dataset = self.backend_metadata.get("dataset", None)
if dataset is not None and dataset != "":
has_dataset = False
if (
self.backend is not None
and type(self.backend) is HDF5Video
and self.backend._open_reader is not None
):
has_dataset = dataset in self.backend._open_reader
else:
with h5py.File(self.filename, "r") as f:
has_dataset = dataset in f
return has_dataset
return True
frame_to_seconds(frame_idx)
¶
Convert a frame index to timestamp in seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame_idx
|
int
|
Zero-indexed frame number. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
Time in seconds, or None if FPS is unknown. |
Notes
This assumes constant frame rate. For variable frame rate videos, the returned timestamp may be approximate.
Source code in sleap_io/model/video.py
def frame_to_seconds(self, frame_idx: int) -> float | None:
"""Convert a frame index to timestamp in seconds.
Args:
frame_idx: Zero-indexed frame number.
Returns:
Time in seconds, or None if FPS is unknown.
Notes:
This assumes constant frame rate. For variable frame rate videos,
the returned timestamp may be approximate.
"""
if self.fps is None or self.fps <= 0:
return None
return frame_idx / self.fps
from_filename(filename, dataset=None, grayscale=None, keep_open=True, source_video=None, **kwargs)
classmethod
¶
Create a Video from a filename.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename
|
str | list[str]
|
The filename(s) of the video. Supported extensions: "mp4", "avi", "mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif", "tiff", "bmp". If the filename is a list, a list of image filenames are expected. If filename is a folder, it will be searched for images. |
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
|
source_video
|
Video | None
|
The source video object if this is a proxy video. This is present when the video contains an embedded subset of frames from another video. |
None
|
**kwargs
|
Additional backend-specific arguments passed to VideoBackend.from_filename. See VideoBackend.from_filename for supported arguments. |
required |
Returns:
| Type | Description |
|---|---|
VideoBackend
|
Video instance with the appropriate backend instantiated. |
Source code in sleap_io/model/video.py
@classmethod
def from_filename(
cls,
filename: str | list[str],
dataset: str | None = None,
grayscale: bool | None = None,
keep_open: bool = True,
source_video: "Video | None" = None,
**kwargs,
) -> VideoBackend:
"""Create a Video from a filename.
Args:
filename: The filename(s) of the video. Supported extensions: "mp4", "avi",
"mov", "mj2", "mkv", "h5", "hdf5", "slp", "png", "jpg", "jpeg", "tif",
"tiff", "bmp". If the filename is a list, a list of image filenames are
expected. If filename is a folder, it will be searched for images.
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.
source_video: The source video object if this is a proxy video. This is
present when the video contains an embedded subset of frames from
another video.
**kwargs: Additional backend-specific arguments passed to
VideoBackend.from_filename. See VideoBackend.from_filename for supported
arguments.
Returns:
Video instance with the appropriate backend instantiated.
"""
backend = VideoBackend.from_filename(
filename,
dataset=dataset,
grayscale=grayscale,
keep_open=keep_open,
**kwargs,
)
# If filename is a directory, VideoBackend.from_filename will expand it
# to a list of paths to images contained within the directory. In this
# case we want to use the expanded list as filename
return cls(
filename=backend.filename,
backend=backend,
source_video=source_video,
)
has_overlapping_images(other)
¶
Check if this video has overlapping images with another video.
This method is specifically for ImageVideo backends (image sequences).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to compare with. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if both are ImageVideo instances with overlapping image files. False if either video is not an ImageVideo or no overlap exists. |
Notes
Only works with ImageVideo backends where filename is a list. Compares individual image filenames (basenames only).
Source code in sleap_io/model/video.py
def has_overlapping_images(self, other: "Video") -> bool:
"""Check if this video has overlapping images with another video.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to compare with.
Returns:
True if both are ImageVideo instances with overlapping image files.
False if either video is not an ImageVideo or no overlap exists.
Notes:
Only works with ImageVideo backends where filename is a list.
Compares individual image filenames (basenames only).
"""
# Both must be image sequences
if not (isinstance(self.filename, list) and isinstance(other.filename, list)):
return False
# Get basenames for comparison
self_basenames = set(Path(f).name for f in self.filename)
other_basenames = set(Path(f).name for f in other.filename)
# Check if there's any overlap
return len(self_basenames & other_basenames) > 0
matches_content(other)
¶
Check if this video has the same content as another video.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to compare with. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the videos have the same shape and backend type. |
Notes
This compares metadata like shape and backend type, not actual frame data.
Source code in sleap_io/model/video.py
def matches_content(self, other: "Video") -> bool:
"""Check if this video has the same content as another video.
Args:
other: Another video to compare with.
Returns:
True if the videos have the same shape and backend type.
Notes:
This compares metadata like shape and backend type, not actual frame data.
"""
# Compare shapes
self_shape = self.shape
other_shape = other.shape
if self_shape != other_shape:
return False
# Compare backend types
if self.backend is None and other.backend is None:
return True
elif self.backend is None or other.backend is None:
return False
return type(self.backend).__name__ == type(other.backend).__name__
matches_path(other, strict=False)
¶
Check if this video has the same path as another video.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to compare with. |
required |
strict
|
bool
|
If True, require exact path match. If False, consider videos with the same filename (basename) as matching. |
False
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the videos have matching paths, False otherwise. |
Notes
For HDF5 video backends (e.g., embedded videos in .pkg.slp files), matching prioritizes the source_filename attribute since multiple videos can share the same HDF5 file path but reference different source videos. Falls back to dataset name matching if source_filename is not available.
Source code in sleap_io/model/video.py
def matches_path(self, other: "Video", strict: bool = False) -> bool:
"""Check if this video has the same path as another video.
Args:
other: Another video to compare with.
strict: If True, require exact path match. If False, consider videos
with the same filename (basename) as matching.
Returns:
True if the videos have matching paths, False otherwise.
Notes:
For HDF5 video backends (e.g., embedded videos in .pkg.slp files),
matching prioritizes the source_filename attribute since multiple
videos can share the same HDF5 file path but reference different
source videos. Falls back to dataset name matching if source_filename
is not available.
"""
# Handle HDF5 backends specially - prioritize source_filename matching
self_is_hdf5 = isinstance(self.backend, HDF5Video)
other_is_hdf5 = isinstance(other.backend, HDF5Video)
if self_is_hdf5 and other_is_hdf5:
# Both are HDF5 videos - match by source_filename first
self_source = self.backend.source_filename
other_source = other.backend.source_filename
if self_source is not None and other_source is not None:
if strict:
return Path(self_source).resolve() == Path(other_source).resolve()
else:
return Path(self_source).name == Path(other_source).name
# Fall back to dataset name matching if source_filename is not available
self_dataset = self.backend.dataset
other_dataset = other.backend.dataset
if self_dataset is not None and other_dataset is not None:
return self_dataset == other_dataset
# If neither source_filename nor dataset available, cannot match
return False
if isinstance(self.filename, list) and isinstance(other.filename, list):
# Both are image sequences
if strict:
return self.filename == other.filename
else:
# Compare basenames
self_basenames = [Path(f).name for f in self.filename]
other_basenames = [Path(f).name for f in other.filename]
return self_basenames == other_basenames
elif isinstance(self.filename, list) or isinstance(other.filename, list):
# One is image sequence, other is single file
return False
else:
# Both are single files
if strict:
return Path(self.filename).resolve() == Path(other.filename).resolve()
else:
return Path(self.filename).name == Path(other.filename).name
matches_shape(other)
¶
Check if this video has the same shape as another video.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to compare with. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if the videos have the same height, width, and channels. |
Notes
This only compares spatial dimensions, not the number of frames.
Source code in sleap_io/model/video.py
def matches_shape(self, other: "Video") -> bool:
"""Check if this video has the same shape as another video.
Args:
other: Another video to compare with.
Returns:
True if the videos have the same height, width, and channels.
Notes:
This only compares spatial dimensions, not the number of frames.
"""
# Try to get shape from backend metadata first if shape is not available
if self.backend is None and "shape" in self.backend_metadata:
self_shape = self.backend_metadata["shape"]
else:
self_shape = self.shape
if other.backend is None and "shape" in other.backend_metadata:
other_shape = other.backend_metadata["shape"]
else:
other_shape = other.shape
# Handle None shapes
if self_shape is None or other_shape is None:
return False
# Compare only height, width, channels (not frames)
return self_shape[1:] == other_shape[1:]
merge_with(other)
¶
Merge another video's images into this one.
This method is specifically for ImageVideo backends (image sequences).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Video
|
Another video to merge with. Must also be ImageVideo. |
required |
Returns:
| Type | Description |
|---|---|
Video
|
A new Video object with unique images from both videos. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If either video is not an ImageVideo backend. |
Notes
Only works with ImageVideo backends where filename is a list. The merged video contains all unique images from both videos, with automatic deduplication based on image basename.
Source code in sleap_io/model/video.py
def merge_with(self, other: "Video") -> "Video":
"""Merge another video's images into this one.
This method is specifically for ImageVideo backends (image sequences).
Args:
other: Another video to merge with. Must also be ImageVideo.
Returns:
A new Video object with unique images from both videos.
Raises:
ValueError: If either video is not an ImageVideo backend.
Notes:
Only works with ImageVideo backends where filename is a list.
The merged video contains all unique images from both videos,
with automatic deduplication based on image basename.
"""
if not isinstance(self.filename, list):
raise ValueError("merge_with only works with ImageVideo backends")
if not isinstance(other.filename, list):
raise ValueError("Other video must also be ImageVideo backend")
# Get all unique images (by basename) preserving order
seen_basenames = set()
merged_paths = []
for path in self.filename:
basename = Path(path).name
if basename not in seen_basenames:
merged_paths.append(path)
seen_basenames.add(basename)
for path in other.filename:
basename = Path(path).name
if basename not in seen_basenames:
merged_paths.append(path)
seen_basenames.add(basename)
# Create new video with merged images
return Video.from_filename(merged_paths, grayscale=self.grayscale)
open(filename=None, dataset=None, grayscale=None, keep_open=True, plugin=None)
¶
Open the video backend for reading.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename
|
str | None
|
Filename to open. If not specified, will use the filename set on the video object. |
None
|
dataset
|
str | None
|
Name of dataset in HDF5 file. |
None
|
grayscale
|
str | 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
|
plugin
|
str | None
|
Video plugin to use for MediaVideo files. One of "opencv", "FFMPEG", or "pyav". Also accepts aliases (case-insensitive). If not specified, uses the backend metadata, global default, or auto-detection in that order. |
None
|
Notes
This is useful for opening the video backend to read frames and then closing it after reading all the necessary frames.
If the backend was already open, it will be closed before opening a new one. Values for the HDF5 dataset and grayscale will be remembered if not specified.
Source code in sleap_io/model/video.py
def open(
self,
filename: str | None = None,
dataset: str | None = None,
grayscale: str | None = None,
keep_open: bool = True,
plugin: str | None = None,
):
"""Open the video backend for reading.
Args:
filename: Filename to open. If not specified, will use the filename set on
the video object.
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.
plugin: Video plugin to use for MediaVideo files. One of "opencv",
"FFMPEG", or "pyav". Also accepts aliases (case-insensitive).
If not specified, uses the backend metadata, global default,
or auto-detection in that order.
Notes:
This is useful for opening the video backend to read frames and then closing
it after reading all the necessary frames.
If the backend was already open, it will be closed before opening a new one.
Values for the HDF5 dataset and grayscale will be remembered if not
specified.
"""
if filename is not None:
self.replace_filename(filename, open=False)
# Try to remember values from previous backend if available and not specified.
if self.backend is not None:
if dataset is None:
dataset = getattr(self.backend, "dataset", None)
if grayscale is None:
grayscale = getattr(self.backend, "grayscale", None)
else:
if dataset is None and "dataset" in self.backend_metadata:
dataset = self.backend_metadata["dataset"]
if grayscale is None:
if "grayscale" in self.backend_metadata:
grayscale = self.backend_metadata["grayscale"]
elif "shape" in self.backend_metadata:
grayscale = self.backend_metadata["shape"][-1] == 1
if not self.exists(dataset=dataset):
msg = (
f"Video does not exist or cannot be opened for reading: {self.filename}"
)
if dataset is not None:
msg += f" (dataset: {dataset})"
raise FileNotFoundError(msg)
# Close previous backend if open.
self.close()
# Handle plugin parameter
backend_kwargs = {}
if plugin is not None:
from sleap_io.io.video_reading import normalize_plugin_name
plugin = normalize_plugin_name(plugin)
self.backend_metadata["plugin"] = plugin
if "plugin" in self.backend_metadata:
backend_kwargs["plugin"] = self.backend_metadata["plugin"]
# Create new backend.
self.backend = VideoBackend.from_filename(
self.filename,
dataset=dataset,
grayscale=grayscale,
keep_open=keep_open,
**backend_kwargs,
)
replace_filename(new_filename, open=True)
¶
Update the filename of the video, optionally opening the backend.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_filename
|
str | Path | list[str] | list[Path]
|
New filename to set for the video. |
required |
open
|
bool
|
If |
True
|
Source code in sleap_io/model/video.py
def replace_filename(
self, new_filename: str | Path | list[str] | list[Path], open: bool = True
):
"""Update the filename of the video, optionally opening the backend.
Args:
new_filename: New filename to set for the video.
open: If `True` (the default), open the backend with the new filename. If
the new filename does not exist, no error is raised.
"""
if isinstance(new_filename, Path):
new_filename = new_filename.as_posix()
if isinstance(new_filename, list):
new_filename = [
p.as_posix() if isinstance(p, Path) else p for p in new_filename
]
self.filename = new_filename
self.backend_metadata["filename"] = new_filename
if open:
if self.exists():
self.open()
else:
self.close()
save(save_path, frame_inds=None, fps=None, video_kwargs=None)
¶
Save video frames to a new video file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
save_path
|
str | Path
|
Path to the new video file. Should end in MP4. |
required |
frame_inds
|
list[int] | ndarray | None
|
Frame indices to save. Can be specified as a list or array of frame integers. If not specified, saves all video frames. |
None
|
fps
|
float | None
|
Frames per second for the output video. If not specified, uses the source video's FPS if available, otherwise defaults to 30. |
None
|
video_kwargs
|
dict[str, Any] | None
|
A dictionary of keyword arguments to provide to
|
None
|
Returns:
| Type | Description |
|---|---|
Video
|
A new |
Source code in sleap_io/model/video.py
def save(
self,
save_path: str | Path,
frame_inds: list[int] | np.ndarray | None = None,
fps: float | None = None,
video_kwargs: dict[str, Any] | None = None,
) -> "Video":
"""Save video frames to a new video file.
Args:
save_path: Path to the new video file. Should end in MP4.
frame_inds: Frame indices to save. Can be specified as a list or array of
frame integers. If not specified, saves all video frames.
fps: Frames per second for the output video. If not specified, uses the
source video's FPS if available, otherwise defaults to 30.
video_kwargs: A dictionary of keyword arguments to provide to
`sio.save_video` for video compression.
Returns:
A new `Video` object pointing to the new video file.
"""
video_kwargs = {} if video_kwargs is None else video_kwargs.copy()
frame_inds = np.arange(len(self)) if frame_inds is None else frame_inds
# Use source video FPS if not explicitly specified
if fps is None:
fps = self.fps
if fps is not None and "fps" not in video_kwargs:
video_kwargs["fps"] = fps
with VideoWriter(save_path, **video_kwargs) as vw:
for frame_ind in frame_inds:
vw(self[frame_ind])
new_video = Video.from_filename(save_path, grayscale=self.grayscale)
return new_video
seconds_to_frame(seconds)
¶
Convert a timestamp in seconds to frame index.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seconds
|
float
|
Time in seconds from video start. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
Zero-indexed frame number (rounded down), or None if FPS unknown. |
Source code in sleap_io/model/video.py
def seconds_to_frame(self, seconds: float) -> int | None:
"""Convert a timestamp in seconds to frame index.
Args:
seconds: Time in seconds from video start.
Returns:
Zero-indexed frame number (rounded down), or None if FPS unknown.
"""
if self.fps is None or self.fps <= 0:
return None
return int(seconds * self.fps)
set_video_plugin(plugin)
¶
Set the video plugin and reopen the video.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
plugin
|
str
|
Video plugin to use. One of "opencv", "FFMPEG", or "pyav". Also accepts aliases (case-insensitive). |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the video is not a MediaVideo type. |
Examples:
Source code in sleap_io/model/video.py
def set_video_plugin(self, plugin: str) -> None:
"""Set the video plugin and reopen the video.
Args:
plugin: Video plugin to use. One of "opencv", "FFMPEG", or "pyav".
Also accepts aliases (case-insensitive).
Raises:
ValueError: If the video is not a MediaVideo type.
Examples:
>>> video.set_video_plugin("opencv")
>>> video.set_video_plugin("CV2") # Same as "opencv"
"""
from sleap_io.io.video_reading import MediaVideo, normalize_plugin_name
if not self.filename.endswith(MediaVideo.EXTS):
raise ValueError(f"Cannot set plugin for non-media video: {self.filename}")
plugin = normalize_plugin_name(plugin)
# Close current backend if open
was_open = self.is_open
if was_open:
self.close()
# Update backend metadata
self.backend_metadata["plugin"] = plugin
# Reopen with new plugin if it was open
if was_open:
self.open()
rodrigues_transformation(input_matrix)
¶
Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between: 1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix 2. A 3x3 rotation matrix to a 3D rotation vector
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
input_matrix
|
ndarray
|
A 3x3 rotation matrix or a 3x1 rotation vector. |
required |
Returns:
| Type | Description |
|---|---|
tuple[ndarray, ndarray]
|
A tuple containing the converted matrix/vector and the Jacobian (None for now). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the input is not a valid rotation matrix or vector. |
Source code in sleap_io/model/camera.py
def rodrigues_transformation(input_matrix: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Convert between rotation vector and rotation matrix using Rodrigues' formula.
This function implements the Rodrigues' rotation formula to convert between:
1. A 3D rotation vector (axis-angle representation) to a 3x3 rotation matrix
2. A 3x3 rotation matrix to a 3D rotation vector
Args:
input_matrix: A 3x3 rotation matrix or a 3x1 rotation vector.
Returns:
A tuple containing the converted matrix/vector and the Jacobian (None for now).
Raises:
ValueError: If the input is not a valid rotation matrix or vector.
"""
# Matrix to vector conversion
if input_matrix.shape == (3, 3):
# Get the rotation angle (trace(R) = 1 + 2*cos(theta))
cos_theta = (np.trace(input_matrix) - 1) / 2.0
cos_theta = np.clip(cos_theta, -1.0, 1.0) # Ensure numerical stability
theta = np.arccos(cos_theta)
# Handle small angles or identity rotation
if np.isclose(theta, 0.0, atol=1e-8):
# For small angles or identity, return zero vector
return np.zeros(3), None
# Compute the rotation axis
sin_theta = np.sin(theta)
if np.isclose(sin_theta, 0.0, atol=1e-8):
# Handle 180-degree rotation (sin_theta = 0)
# Find the largest diagonal element
diag = np.diag(input_matrix)
k = np.argmax(diag)
axis = np.zeros(3)
if diag[k] > -1.0:
# Extract the column with largest diagonal
axis[k] = 1.0
v = input_matrix[:, k] + axis
axis = v / np.linalg.norm(v)
rvec = theta * axis
else:
# Normal case: extract the skew-symmetric part
axis = np.array(
[
input_matrix[2, 1] - input_matrix[1, 2],
input_matrix[0, 2] - input_matrix[2, 0],
input_matrix[1, 0] - input_matrix[0, 1],
]
) / (2.0 * sin_theta)
# Ensure the axis is a unit vector
axis_norm = np.linalg.norm(axis)
if axis_norm > 0:
axis = axis / axis_norm
rvec = theta * axis
return rvec, None
# Vector to matrix conversion
elif input_matrix.shape == (3,) or input_matrix.shape == (3, 1):
# Handle both flat and column vectors
rvec = input_matrix.ravel()
theta = np.linalg.norm(rvec)
# Handle small angles
if np.isclose(theta, 0.0, atol=1e-8):
return np.eye(3), None
# Normalize the rotation axis
axis = rvec / theta
# Create the cross-product matrix
K = np.array(
[[0, -axis[2], axis[1]], [axis[2], 0, -axis[0]], [-axis[1], axis[0], 0]]
)
# Rodrigues' formula: R = I + sin(θ)K + (1-cos(θ))K²
sin_theta = np.sin(theta)
cos_theta = np.cos(theta)
K_squared = np.dot(K, K)
rotation_matrix = np.eye(3) + sin_theta * K + (1.0 - cos_theta) * K_squared
return rotation_matrix, None
else:
raise ValueError(
f"Input must be a 3x3 matrix or a 3-element vector, got shape "
f"{input_matrix.shape}"
)