matching
sleap_io.model.matching
¶
Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks, and videos during merge operations. The matchers use various strategies to determine when data structures should be considered equivalent during merging.
Key features: - Skeleton matching: exact, structure-based, overlap, and subset matching - Instance matching: spatial proximity, track identity, and bounding box IoU - Track matching: by name or object identity - Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and automatic strategies.
Classes:
| Name | Description |
|---|---|
ConflictResolution |
Information about a conflict that was resolved during merging. |
ErrorMode |
Error handling modes for merge operations. |
FrameStrategy |
Strategies for handling frame merging. |
Instance |
This class represents a ground truth instance such as an animal. |
InstanceMatchMethod |
Methods for matching instances. |
InstanceMatcher |
Matcher for comparing and matching instances. |
LabeledFrame |
Labeled data for a single frame of a video. |
MergeError |
Base exception for merge errors. |
MergeProgressBar |
Context manager for merge progress tracking using tqdm. |
MergeResult |
Result of a merge operation. |
Skeleton |
A description of a set of landmark types and connections between them. |
SkeletonMatchMethod |
Methods for matching skeletons. |
SkeletonMatcher |
Matcher for comparing and matching skeletons. |
SkeletonMismatchError |
Raised when skeletons don't match during merge. |
Track |
An object that represents the same animal/object across multiple detections. |
TrackMatchMethod |
Methods for matching tracks. |
TrackMatcher |
Matcher for comparing and matching tracks. |
Video |
|
VideoMatchMethod |
Methods for matching videos. |
VideoMatcher |
Matcher for comparing and matching videos. |
Functions:
| Name | Description |
|---|---|
is_same_file |
Check if two videos refer to the same underlying file. |
original_videos_conflict |
Check if two videos have conflicting original_video references. |
shapes_compatible |
Check if two videos have compatible shapes. |
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO_VIDEO_MATCHER |
Matcher for comparing and matching videos. |
|
BASENAME_VIDEO_MATCHER |
Matcher for comparing and matching videos. |
|
DUPLICATE_MATCHER |
Matcher for comparing and matching instances. |
|
IDENTITY_INSTANCE_MATCHER |
Matcher for comparing and matching instances. |
|
IDENTITY_TRACK_MATCHER |
Matcher for comparing and matching tracks. |
|
IMAGE_DEDUP_VIDEO_MATCHER |
Matcher for comparing and matching videos. |
|
IOU_MATCHER |
Matcher for comparing and matching instances. |
|
NAME_TRACK_MATCHER |
Matcher for comparing and matching tracks. |
|
OVERLAP_SKELETON_MATCHER |
Matcher for comparing and matching skeletons. |
|
PATH_VIDEO_MATCHER |
Matcher for comparing and matching videos. |
|
SHAPE_VIDEO_MATCHER |
Matcher for comparing and matching videos. |
|
STRUCTURE_SKELETON_MATCHER |
Matcher for comparing and matching skeletons. |
|
SUBSET_SKELETON_MATCHER |
Matcher for comparing and matching skeletons. |
|
__cached__ |
str(object='') -> str |
|
__doc__ |
str(object='') -> str |
|
__file__ |
str(object='') -> str |
|
__name__ |
str(object='') -> str |
|
__package__ |
str(object='') -> str |
AUTO_VIDEO_MATCHER = VideoMatcher(method=<VideoMatchMethod.AUTO: 'auto'>, strict=False)
module-attribute
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
BASENAME_VIDEO_MATCHER = VideoMatcher(method=<VideoMatchMethod.BASENAME: 'basename'>, strict=False)
module-attribute
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
DUPLICATE_MATCHER = InstanceMatcher(method=<InstanceMatchMethod.SPATIAL: 'spatial'>, threshold=5.0)
module-attribute
¶
Matcher for comparing and matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be an InstanceMatchMethod enum value or a string that will be converted to the enum. Default is SPATIAL. |
|
threshold |
The threshold value used for matching. For SPATIAL method, this is the maximum pixel distance. For IOU method, this is the minimum IoU value. Not used for IDENTITY method. Default is 5.0. |
IDENTITY_INSTANCE_MATCHER = InstanceMatcher(method=<InstanceMatchMethod.IDENTITY: 'identity'>, threshold=5.0)
module-attribute
¶
Matcher for comparing and matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be an InstanceMatchMethod enum value or a string that will be converted to the enum. Default is SPATIAL. |
|
threshold |
The threshold value used for matching. For SPATIAL method, this is the maximum pixel distance. For IOU method, this is the minimum IoU value. Not used for IDENTITY method. Default is 5.0. |
IDENTITY_TRACK_MATCHER = TrackMatcher(method=<TrackMatchMethod.IDENTITY: 'identity'>)
module-attribute
¶
Matcher for comparing and matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a TrackMatchMethod enum value or a string that will be converted to the enum. Default is NAME. |
IMAGE_DEDUP_VIDEO_MATCHER = VideoMatcher(method=<VideoMatchMethod.IMAGE_DEDUP: 'image_dedup'>, strict=False)
module-attribute
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
IOU_MATCHER = InstanceMatcher(method=<InstanceMatchMethod.IOU: 'iou'>, threshold=0.5)
module-attribute
¶
Matcher for comparing and matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be an InstanceMatchMethod enum value or a string that will be converted to the enum. Default is SPATIAL. |
|
threshold |
The threshold value used for matching. For SPATIAL method, this is the maximum pixel distance. For IOU method, this is the minimum IoU value. Not used for IDENTITY method. Default is 5.0. |
NAME_TRACK_MATCHER = TrackMatcher(method=<TrackMatchMethod.NAME: 'name'>)
module-attribute
¶
Matcher for comparing and matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a TrackMatchMethod enum value or a string that will be converted to the enum. Default is NAME. |
OVERLAP_SKELETON_MATCHER = SkeletonMatcher(method=<SkeletonMatchMethod.OVERLAP: 'overlap'>, require_same_order=False, min_overlap=0.7)
module-attribute
¶
Matcher for comparing and matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a SkeletonMatchMethod enum value or a string that will be converted to the enum. Default is STRUCTURE. |
|
require_same_order |
Whether to require nodes in the same order for STRUCTURE matching. Only used when method is STRUCTURE. Default is False. |
|
min_overlap |
Minimum Jaccard similarity required for OVERLAP matching. Only used when method is OVERLAP. Default is 0.5. |
PATH_VIDEO_MATCHER = VideoMatcher(method=<VideoMatchMethod.PATH: 'path'>, strict=True)
module-attribute
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
SHAPE_VIDEO_MATCHER = VideoMatcher(method=<VideoMatchMethod.SHAPE: 'shape'>, strict=False)
module-attribute
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
STRUCTURE_SKELETON_MATCHER = SkeletonMatcher(method=<SkeletonMatchMethod.STRUCTURE: 'structure'>, require_same_order=False, min_overlap=0.5)
module-attribute
¶
Matcher for comparing and matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a SkeletonMatchMethod enum value or a string that will be converted to the enum. Default is STRUCTURE. |
|
require_same_order |
Whether to require nodes in the same order for STRUCTURE matching. Only used when method is STRUCTURE. Default is False. |
|
min_overlap |
Minimum Jaccard similarity required for OVERLAP matching. Only used when method is OVERLAP. Default is 0.5. |
SUBSET_SKELETON_MATCHER = SkeletonMatcher(method=<SkeletonMatchMethod.SUBSET: 'subset'>, require_same_order=False, min_overlap=0.5)
module-attribute
¶
Matcher for comparing and matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a SkeletonMatchMethod enum value or a string that will be converted to the enum. Default is STRUCTURE. |
|
require_same_order |
Whether to require nodes in the same order for STRUCTURE matching. Only used when method is STRUCTURE. Default is False. |
|
min_overlap |
Minimum Jaccard similarity required for OVERLAP matching. Only used when method is OVERLAP. Default is 0.5. |
__cached__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/__pycache__/matching.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__ = 'Unified matcher system for comparing and matching data structures during merging.\n\nThis module provides configurable matchers for comparing skeletons, instances, tracks,\nand videos during merge operations. The matchers use various strategies to determine\nwhen data structures should be considered equivalent during merging.\n\nKey features:\n- Skeleton matching: exact, structure-based, overlap, and subset matching\n- Instance matching: spatial proximity, track identity, and bounding box IoU\n- Track matching: by name or object identity\n- Video matching: path, basename, content, and auto matching\n\nVideo matching supports path-based, filename-based, content-based, and\nautomatic strategies.\n'
module-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__file__ = '/home/runner/work/sleap-io/sleap-io/sleap_io/model/matching.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.matching'
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'.
ConflictResolution
¶
Information about a conflict that was resolved during merging.
Attributes:
| Name | Type | Description |
|---|---|---|
frame |
The labeled frame where the conflict occurred. |
|
conflict_type |
Type of conflict (e.g., "duplicate_instance", "skeleton_mismatch"). |
|
original_data |
The original data before resolution. |
|
new_data |
The new/incoming data that caused the conflict. |
|
resolution |
Description of how the conflict was resolved. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class ConflictResolution. |
__init__ |
Method generated by attrs for class ConflictResolution. |
__replace__ |
Method generated by attrs for class ConflictResolution. |
__repr__ |
Method generated by attrs for class ConflictResolution. |
Source code in sleap_io/model/matching.py
@attrs.define
class ConflictResolution:
"""Information about a conflict that was resolved during merging.
Attributes:
frame: The labeled frame where the conflict occurred.
conflict_type: Type of conflict (e.g., "duplicate_instance",
"skeleton_mismatch").
original_data: The original data before resolution.
new_data: The new/incoming data that caused the conflict.
resolution: Description of how the conflict was resolved.
"""
frame: LabeledFrame
conflict_type: str
original_data: Any
new_data: Any
resolution: str
__annotations__ = {'frame': 'LabeledFrame', 'conflict_type': 'str', 'original_data': 'Any', 'new_data': 'Any', 'resolution': 'str'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 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__ = 'Information about a conflict that was resolved during merging.\n\nAttributes:\n frame: The labeled frame where the conflict occurred.\n conflict_type: Type of conflict (e.g., "duplicate_instance",\n "skeleton_mismatch").\n original_data: The original data before resolution.\n new_data: The new/incoming data that caused the conflict.\n resolution: Description of how the conflict was resolved.\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__ = 710
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', 'conflict_type', 'original_data', 'new_data', 'resolution')
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.matching'
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', 'conflict_type', 'original_data', 'new_data', 'resolution', '__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__(frame, conflict_type, original_data, new_data, resolution)
¶
__replace__(**changes)
¶
Method generated by attrs for class ConflictResolution.
__repr__()
¶
Method generated by attrs for class ConflictResolution.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
ErrorMode
¶
Bases: builtins.str, enum.Enum
Error handling modes for merge operations.
Attributes:
| Name | Type | Description |
|---|---|---|
CONTINUE |
Continue merging on errors, collecting them in the result. |
|
STRICT |
Raise an exception on the first error encountered. |
|
WARN |
Issue warnings about errors but continue merging. |
Source code in sleap_io/model/matching.py
class ErrorMode(str, Enum):
"""Error handling modes for merge operations.
Attributes:
CONTINUE: Continue merging on errors, collecting them in the result.
STRICT: Raise an exception on the first error encountered.
WARN: Issue warnings about errors but continue merging.
"""
CONTINUE = "continue"
STRICT = "strict"
WARN = "warn"
CONTINUE = <ErrorMode.CONTINUE: 'continue'>
class-attribute
¶
Error handling modes for merge operations.
Attributes:
| Name | Type | Description |
|---|---|---|
CONTINUE |
Continue merging on errors, collecting them in the result. |
|
STRICT |
Raise an exception on the first error encountered. |
|
WARN |
Issue warnings about errors but continue merging. |
STRICT = <ErrorMode.STRICT: 'strict'>
class-attribute
¶
Error handling modes for merge operations.
Attributes:
| Name | Type | Description |
|---|---|---|
CONTINUE |
Continue merging on errors, collecting them in the result. |
|
STRICT |
Raise an exception on the first error encountered. |
|
WARN |
Issue warnings about errors but continue merging. |
WARN = <ErrorMode.WARN: 'warn'>
class-attribute
¶
Error handling modes for merge operations.
Attributes:
| Name | Type | Description |
|---|---|---|
CONTINUE |
Continue merging on errors, collecting them in the result. |
|
STRICT |
Raise an exception on the first error encountered. |
|
WARN |
Issue warnings about errors but continue merging. |
__doc__ = 'Error handling modes for merge operations.\n\nAttributes:\n CONTINUE: Continue merging on errors, collecting them in the result.\n STRICT: Raise an exception on the first error encountered.\n WARN: Issue warnings about errors but continue merging.\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'.
__module__ = 'sleap_io.model.matching'
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'.
FrameStrategy
¶
Bases: builtins.str, enum.Enum
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
Source code in sleap_io/model/matching.py
class FrameStrategy(str, Enum):
"""Strategies for handling frame merging.
Attributes:
AUTO: Automatic merging that preserves user labels over predictions when
they overlap.
KEEP_ORIGINAL: Always keep instances from the original (base) frame.
KEEP_NEW: Always keep instances from the new (incoming) frame.
KEEP_BOTH: Keep all instances from both frames without filtering.
UPDATE_TRACKS: Update track assignments only without modifying poses.
REPLACE_PREDICTIONS: Keep user instances from base, remove base predictions,
add only predictions from incoming frame.
"""
AUTO = "auto"
KEEP_ORIGINAL = "keep_original"
KEEP_NEW = "keep_new"
KEEP_BOTH = "keep_both"
UPDATE_TRACKS = "update_tracks"
REPLACE_PREDICTIONS = "replace_predictions"
AUTO = <FrameStrategy.AUTO: 'auto'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
KEEP_BOTH = <FrameStrategy.KEEP_BOTH: 'keep_both'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
KEEP_NEW = <FrameStrategy.KEEP_NEW: 'keep_new'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
KEEP_ORIGINAL = <FrameStrategy.KEEP_ORIGINAL: 'keep_original'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
REPLACE_PREDICTIONS = <FrameStrategy.REPLACE_PREDICTIONS: 'replace_predictions'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
UPDATE_TRACKS = <FrameStrategy.UPDATE_TRACKS: 'update_tracks'>
class-attribute
¶
Strategies for handling frame merging.
Attributes:
| Name | Type | Description |
|---|---|---|
AUTO |
Automatic merging that preserves user labels over predictions when they overlap. |
|
KEEP_ORIGINAL |
Always keep instances from the original (base) frame. |
|
KEEP_NEW |
Always keep instances from the new (incoming) frame. |
|
KEEP_BOTH |
Keep all instances from both frames without filtering. |
|
UPDATE_TRACKS |
Update track assignments only without modifying poses. |
|
REPLACE_PREDICTIONS |
Keep user instances from base, remove base predictions, add only predictions from incoming frame. |
__doc__ = 'Strategies for handling frame merging.\n\nAttributes:\n AUTO: Automatic merging that preserves user labels over predictions when\n they overlap.\n KEEP_ORIGINAL: Always keep instances from the original (base) frame.\n KEEP_NEW: Always keep instances from the new (incoming) frame.\n KEEP_BOTH: Keep all instances from both frames without filtering.\n UPDATE_TRACKS: Update track assignments only without modifying poses.\n REPLACE_PREDICTIONS: Keep user instances from base, remove base predictions,\n add only predictions from incoming frame.\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'.
__module__ = 'sleap_io.model.matching'
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'.
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
InstanceMatchMethod
¶
Bases: builtins.str, enum.Enum
Methods for matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
SPATIAL |
Match instances by spatial proximity using Euclidean distance. |
|
IDENTITY |
Match instances by track identity (same track object). |
|
IOU |
Match instances by bounding box Intersection over Union. |
Source code in sleap_io/model/matching.py
class InstanceMatchMethod(str, Enum):
"""Methods for matching instances.
Attributes:
SPATIAL: Match instances by spatial proximity using Euclidean distance.
IDENTITY: Match instances by track identity (same track object).
IOU: Match instances by bounding box Intersection over Union.
"""
SPATIAL = "spatial"
IDENTITY = "identity"
IOU = "iou"
IDENTITY = <InstanceMatchMethod.IDENTITY: 'identity'>
class-attribute
¶
Methods for matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
SPATIAL |
Match instances by spatial proximity using Euclidean distance. |
|
IDENTITY |
Match instances by track identity (same track object). |
|
IOU |
Match instances by bounding box Intersection over Union. |
IOU = <InstanceMatchMethod.IOU: 'iou'>
class-attribute
¶
Methods for matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
SPATIAL |
Match instances by spatial proximity using Euclidean distance. |
|
IDENTITY |
Match instances by track identity (same track object). |
|
IOU |
Match instances by bounding box Intersection over Union. |
SPATIAL = <InstanceMatchMethod.SPATIAL: 'spatial'>
class-attribute
¶
Methods for matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
SPATIAL |
Match instances by spatial proximity using Euclidean distance. |
|
IDENTITY |
Match instances by track identity (same track object). |
|
IOU |
Match instances by bounding box Intersection over Union. |
__doc__ = 'Methods for matching instances.\n\nAttributes:\n SPATIAL: Match instances by spatial proximity using Euclidean distance.\n IDENTITY: Match instances by track identity (same track object).\n IOU: Match instances by bounding box Intersection over Union.\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'.
__module__ = 'sleap_io.model.matching'
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'.
InstanceMatcher
¶
Matcher for comparing and matching instances.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be an InstanceMatchMethod enum value or a string that will be converted to the enum. Default is SPATIAL. |
|
threshold |
The threshold value used for matching. For SPATIAL method, this is the maximum pixel distance. For IOU method, this is the minimum IoU value. Not used for IDENTITY method. Default is 5.0. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class InstanceMatcher. |
__init__ |
Method generated by attrs for class InstanceMatcher. |
__replace__ |
Method generated by attrs for class InstanceMatcher. |
__repr__ |
Method generated by attrs for class InstanceMatcher. |
__setattr__ |
Method generated by attrs for class InstanceMatcher. |
find_matches |
Find all matching instances between two lists. |
match |
Check if two instances match according to the configured method. |
Source code in sleap_io/model/matching.py
@attrs.define
class InstanceMatcher:
"""Matcher for comparing and matching instances.
Attributes:
method: The matching method to use. Can be an InstanceMatchMethod enum value
or a string that will be converted to the enum. Default is SPATIAL.
threshold: The threshold value used for matching. For SPATIAL method, this is
the maximum pixel distance. For IOU method, this is the minimum IoU value.
Not used for IDENTITY method. Default is 5.0.
"""
method: InstanceMatchMethod | str = attrs.field(
default=InstanceMatchMethod.SPATIAL,
converter=lambda x: InstanceMatchMethod(x) if isinstance(x, str) else x,
)
threshold: float = 5.0
def match(self, instance1: Instance, instance2: Instance) -> bool:
"""Check if two instances match according to the configured method."""
if self.method == InstanceMatchMethod.SPATIAL:
return instance1.same_pose_as(instance2, tolerance=self.threshold)
elif self.method == InstanceMatchMethod.IDENTITY:
return instance1.same_identity_as(instance2)
elif self.method == InstanceMatchMethod.IOU:
return instance1.overlaps_with(instance2, iou_threshold=self.threshold)
else:
raise ValueError(f"Unknown instance match method: {self.method}")
def find_matches(
self, instances1: list[Instance], instances2: list[Instance]
) -> list[tuple[int, int, float]]:
"""Find all matching instances between two lists.
Returns:
List of (idx1, idx2, score) tuples for matching instances.
"""
matches = []
for i, inst1 in enumerate(instances1):
for j, inst2 in enumerate(instances2):
if self.match(inst1, inst2):
# Calculate match score based on method
if self.method == InstanceMatchMethod.SPATIAL:
# Use inverse distance as score
pts1 = inst1.numpy()
pts2 = inst2.numpy()
valid = ~(np.isnan(pts1[:, 0]) | np.isnan(pts2[:, 0]))
if valid.any():
distances = np.linalg.norm(
pts1[valid] - pts2[valid], axis=1
)
score = 1.0 / (1.0 + np.mean(distances))
else:
score = 0.0
elif self.method == InstanceMatchMethod.IOU:
# Calculate actual IoU as score
bbox1 = inst1.bounding_box()
bbox2 = inst2.bounding_box()
if bbox1 is not None and bbox2 is not None:
# Calculate IoU
intersection_min = np.maximum(bbox1[0], bbox2[0])
intersection_max = np.minimum(bbox1[1], bbox2[1])
if np.all(intersection_min < intersection_max):
intersection_area = np.prod(
intersection_max - intersection_min
)
area1 = np.prod(bbox1[1] - bbox1[0])
area2 = np.prod(bbox2[1] - bbox2[0])
union_area = area1 + area2 - intersection_area
score = (
intersection_area / union_area
if union_area > 0
else 0
)
else:
score = 0.0
else:
score = 0.0
else:
score = 1.0 # Binary match for identity
matches.append((i, j, score))
return matches
__annotations__ = {'method': 'InstanceMatchMethod | str', 'threshold': 'float'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = True
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=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__ = 'Matcher for comparing and matching instances.\n\nAttributes:\n method: The matching method to use. Can be an InstanceMatchMethod enum value\n or a string that will be converted to the enum. Default is SPATIAL.\n threshold: The threshold value used for matching. For SPATIAL method, this is\n the maximum pixel distance. For IOU method, this is the minimum IoU value.\n Not used for IDENTITY method. Default is 5.0.\n'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__firstlineno__ = 400
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__ = ('method', 'threshold')
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.matching'
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__ = ('method', 'threshold', '__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__(method=<InstanceMatchMethod.SPATIAL: 'spatial'>, threshold=5.0)
¶
__replace__(**changes)
¶
Method generated by attrs for class InstanceMatcher.
__repr__()
¶
Method generated by attrs for class InstanceMatcher.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
__setattr__(name, val)
¶
Method generated by attrs for class InstanceMatcher.
find_matches(instances1, instances2)
¶
Find all matching instances between two lists.
Returns:
| Type | Description |
|---|---|
list[tuple[int, int, float]]
|
List of (idx1, idx2, score) tuples for matching instances. |
Source code in sleap_io/model/matching.py
def find_matches(
self, instances1: list[Instance], instances2: list[Instance]
) -> list[tuple[int, int, float]]:
"""Find all matching instances between two lists.
Returns:
List of (idx1, idx2, score) tuples for matching instances.
"""
matches = []
for i, inst1 in enumerate(instances1):
for j, inst2 in enumerate(instances2):
if self.match(inst1, inst2):
# Calculate match score based on method
if self.method == InstanceMatchMethod.SPATIAL:
# Use inverse distance as score
pts1 = inst1.numpy()
pts2 = inst2.numpy()
valid = ~(np.isnan(pts1[:, 0]) | np.isnan(pts2[:, 0]))
if valid.any():
distances = np.linalg.norm(
pts1[valid] - pts2[valid], axis=1
)
score = 1.0 / (1.0 + np.mean(distances))
else:
score = 0.0
elif self.method == InstanceMatchMethod.IOU:
# Calculate actual IoU as score
bbox1 = inst1.bounding_box()
bbox2 = inst2.bounding_box()
if bbox1 is not None and bbox2 is not None:
# Calculate IoU
intersection_min = np.maximum(bbox1[0], bbox2[0])
intersection_max = np.minimum(bbox1[1], bbox2[1])
if np.all(intersection_min < intersection_max):
intersection_area = np.prod(
intersection_max - intersection_min
)
area1 = np.prod(bbox1[1] - bbox1[0])
area2 = np.prod(bbox2[1] - bbox2[0])
union_area = area1 + area2 - intersection_area
score = (
intersection_area / union_area
if union_area > 0
else 0
)
else:
score = 0.0
else:
score = 0.0
else:
score = 1.0 # Binary match for identity
matches.append((i, j, score))
return matches
match(instance1, instance2)
¶
Check if two instances match according to the configured method.
Source code in sleap_io/model/matching.py
def match(self, instance1: Instance, instance2: Instance) -> bool:
"""Check if two instances match according to the configured method."""
if self.method == InstanceMatchMethod.SPATIAL:
return instance1.same_pose_as(instance2, tolerance=self.threshold)
elif self.method == InstanceMatchMethod.IDENTITY:
return instance1.same_identity_as(instance2)
elif self.method == InstanceMatchMethod.IOU:
return instance1.overlaps_with(instance2, iou_threshold=self.threshold)
else:
raise ValueError(f"Unknown instance match method: {self.method}")
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
MergeError
¶
Bases: builtins.Exception
Base exception for merge errors.
Attributes:
| Name | Type | Description |
|---|---|---|
message |
Human-readable error message. |
|
details |
Dictionary containing additional error details and context. |
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class MergeError. |
__replace__ |
Method generated by attrs for class MergeError. |
__repr__ |
Method generated by attrs for class MergeError. |
Source code in sleap_io/model/matching.py
__annotations__ = {'message': 'str', 'details': '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__ = 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=True, 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__ = 'Base exception for merge errors.\n\nAttributes:\n message: Human-readable error message.\n details: Dictionary containing additional error details and context.\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__ = 730
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__ = ('message', 'details')
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.matching'
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__ = ('message', 'details', '__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
__init__(message, details=NOTHING)
¶
__replace__(**changes)
¶
Method generated by attrs for class MergeError.
__repr__()
¶
Method generated by attrs for class MergeError.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
MergeProgressBar
¶
Context manager for merge progress tracking using tqdm.
This provides a clean interface for tracking merge progress with visual feedback.
Example
with MergeProgressBar("Merging predictions") as progress: result = labels.merge(predictions, progress_callback=progress.callback)
Methods:
| Name | Description |
|---|---|
__enter__ |
Enter the context manager. |
__exit__ |
Exit the context manager and close the progress bar. |
__init__ |
Initialize the progress bar. |
callback |
Progress callback for merge operations. |
Attributes:
| Name | Type | Description |
|---|---|---|
__dict__ |
Read-only proxy of a mapping. |
|
__doc__ |
str(object='') -> str |
|
__firstlineno__ |
int([x]) -> integer |
|
__module__ |
str(object='') -> str |
|
__static_attributes__ |
Built-in immutable sequence. |
|
__weakref__ |
list of weak references to the object |
Source code in sleap_io/model/matching.py
class MergeProgressBar:
"""Context manager for merge progress tracking using tqdm.
This provides a clean interface for tracking merge progress with visual feedback.
Example:
with MergeProgressBar("Merging predictions") as progress:
result = labels.merge(predictions, progress_callback=progress.callback)
"""
def __init__(self, desc: str = "Merging", leave: bool = True):
"""Initialize the progress bar.
Args:
desc: Description to show in the progress bar.
leave: Whether to leave the progress bar on screen after completion.
"""
self.desc = desc
self.leave = leave
self.pbar = None
def __enter__(self):
"""Enter the context manager."""
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""Exit the context manager and close the progress bar."""
if self.pbar is not None:
self.pbar.close()
def callback(self, current: int, total: int, message: str = ""):
"""Progress callback for merge operations.
Args:
current: Current progress value.
total: Total items to process.
message: Optional message to display.
"""
from tqdm import tqdm
if self.pbar is None and total:
self.pbar = tqdm(total=total, desc=self.desc, leave=self.leave)
if self.pbar:
if message:
self.pbar.set_description(f"{self.desc}: {message}")
else:
self.pbar.set_description(self.desc)
self.pbar.n = current
self.pbar.refresh()
__dict__ = mappingproxy({'__module__': 'sleap_io.model.matching', '__firstlineno__': 802, '__doc__': 'Context manager for merge progress tracking using tqdm.\n\nThis provides a clean interface for tracking merge progress with visual feedback.\n\nExample:\n with MergeProgressBar("Merging predictions") as progress:\n result = labels.merge(predictions, progress_callback=progress.callback)\n', '__init__': <function MergeProgressBar.__init__ at 0x7f41aad979c0>, '__enter__': <function MergeProgressBar.__enter__ at 0x7f41aad97ec0>, '__exit__': <function MergeProgressBar.__exit__ at 0x7f41aad96e80>, 'callback': <function MergeProgressBar.callback at 0x7f41aad974c0>, '__static_attributes__': ('desc', 'leave', 'pbar'), '__dict__': <attribute '__dict__' of 'MergeProgressBar' objects>, '__weakref__': <attribute '__weakref__' of 'MergeProgressBar' objects>})
class-attribute
¶
Read-only proxy of a mapping.
__doc__ = 'Context manager for merge progress tracking using tqdm.\n\nThis provides a clean interface for tracking merge progress with visual feedback.\n\nExample:\n with MergeProgressBar("Merging predictions") as progress:\n result = labels.merge(predictions, progress_callback=progress.callback)\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__ = 802
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
__module__ = 'sleap_io.model.matching'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__ = ('desc', 'leave', 'pbar')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
__enter__()
¶
__exit__(exc_type, exc_val, exc_tb)
¶
__init__(desc='Merging', leave=True)
¶
Initialize the progress bar.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
desc
|
str
|
Description to show in the progress bar. |
'Merging'
|
leave
|
bool
|
Whether to leave the progress bar on screen after completion. |
True
|
Source code in sleap_io/model/matching.py
callback(current, total, message='')
¶
Progress callback for merge operations.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
current
|
int
|
Current progress value. |
required |
total
|
int
|
Total items to process. |
required |
message
|
str
|
Optional message to display. |
''
|
Source code in sleap_io/model/matching.py
def callback(self, current: int, total: int, message: str = ""):
"""Progress callback for merge operations.
Args:
current: Current progress value.
total: Total items to process.
message: Optional message to display.
"""
from tqdm import tqdm
if self.pbar is None and total:
self.pbar = tqdm(total=total, desc=self.desc, leave=self.leave)
if self.pbar:
if message:
self.pbar.set_description(f"{self.desc}: {message}")
else:
self.pbar.set_description(self.desc)
self.pbar.n = current
self.pbar.refresh()
MergeResult
¶
Result of a merge operation.
Attributes:
| Name | Type | Description |
|---|---|---|
successful |
Whether the merge completed successfully. |
|
frames_merged |
Number of frames that were merged. |
|
instances_added |
Number of new instances added. |
|
instances_updated |
Number of existing instances that were updated. |
|
instances_skipped |
Number of instances that were skipped. |
|
conflicts |
List of conflicts that were resolved during merging. |
|
errors |
List of errors encountered during merging. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class MergeResult. |
__init__ |
Method generated by attrs for class MergeResult. |
__replace__ |
Method generated by attrs for class MergeResult. |
__repr__ |
Method generated by attrs for class MergeResult. |
summary |
Generate a human-readable summary of the merge result. |
Source code in sleap_io/model/matching.py
@attrs.define
class MergeResult:
"""Result of a merge operation.
Attributes:
successful: Whether the merge completed successfully.
frames_merged: Number of frames that were merged.
instances_added: Number of new instances added.
instances_updated: Number of existing instances that were updated.
instances_skipped: Number of instances that were skipped.
conflicts: List of conflicts that were resolved during merging.
errors: List of errors encountered during merging.
"""
successful: bool
frames_merged: int = 0
instances_added: int = 0
instances_updated: int = 0
instances_skipped: int = 0
conflicts: list[ConflictResolution] = attrs.field(factory=list)
errors: list[MergeError] = attrs.field(factory=list)
def summary(self) -> str:
"""Generate a human-readable summary of the merge result."""
lines = []
if self.successful:
lines.append("✓ Merge completed successfully")
else:
lines.append("✗ Merge completed with errors")
lines.append(f" Frames merged: {self.frames_merged}")
lines.append(f" Instances added: {self.instances_added}")
if self.instances_updated:
lines.append(f" Instances updated: {self.instances_updated}")
if self.instances_skipped:
lines.append(f" Instances skipped: {self.instances_skipped}")
if self.conflicts:
lines.append(f" Conflicts resolved: {len(self.conflicts)}")
if self.errors:
lines.append(f" Errors encountered: {len(self.errors)}")
for error in self.errors[:5]: # Show first 5 errors
lines.append(f" - {error.message}")
if len(self.errors) > 5:
lines.append(f" ... and {len(self.errors) - 5} more")
return "\n".join(lines)
__annotations__ = {'successful': 'bool', 'frames_merged': 'int', 'instances_added': 'int', 'instances_updated': 'int', 'instances_skipped': 'int', 'conflicts': 'list[ConflictResolution]', 'errors': 'list[MergeError]'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 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__ = 'Result of a merge operation.\n\nAttributes:\n successful: Whether the merge completed successfully.\n frames_merged: Number of frames that were merged.\n instances_added: Number of new instances added.\n instances_updated: Number of existing instances that were updated.\n instances_skipped: Number of instances that were skipped.\n conflicts: List of conflicts that were resolved during merging.\n errors: List of errors encountered during merging.\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__ = 749
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__ = ('successful', 'frames_merged', 'instances_added', 'instances_updated', 'instances_skipped', 'conflicts', 'errors')
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.matching'
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__ = ('successful', 'frames_merged', 'instances_added', 'instances_updated', 'instances_skipped', 'conflicts', 'errors', '__weakref__')
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__static_attributes__ = ()
class-attribute
¶
Built-in immutable sequence.
If no argument is given, the constructor returns an empty tuple. If iterable is specified the tuple is initialized from iterable's items.
If the argument is a tuple, the return value is the same object.
__weakref__
property
¶
list of weak references to the object
__eq__(other)
¶
Method generated by attrs for class MergeResult.
Source code in sleap_io/model/matching.py
__init__(successful, frames_merged=0, instances_added=0, instances_updated=0, instances_skipped=0, conflicts=NOTHING, errors=NOTHING)
¶
Method generated by attrs for class MergeResult.
Source code in sleap_io/model/matching.py
from sleap_io.model.video import Video
class SkeletonMatchMethod(str, Enum):
"""Methods for matching skeletons.
Attributes:
EXACT: Exact match requiring same nodes in the same order.
STRUCTURE: Match requiring same nodes and edges, but order doesn't matter.
OVERLAP: Partial match based on overlapping nodes (uses Jaccard similarity).
SUBSET: Match if one skeleton's nodes are a subset of another's.
"""
EXACT = "exact"
__replace__(**changes)
¶
Method generated by attrs for class MergeResult.
__repr__()
¶
Method generated by attrs for class MergeResult.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
summary()
¶
Generate a human-readable summary of the merge result.
Source code in sleap_io/model/matching.py
def summary(self) -> str:
"""Generate a human-readable summary of the merge result."""
lines = []
if self.successful:
lines.append("✓ Merge completed successfully")
else:
lines.append("✗ Merge completed with errors")
lines.append(f" Frames merged: {self.frames_merged}")
lines.append(f" Instances added: {self.instances_added}")
if self.instances_updated:
lines.append(f" Instances updated: {self.instances_updated}")
if self.instances_skipped:
lines.append(f" Instances skipped: {self.instances_skipped}")
if self.conflicts:
lines.append(f" Conflicts resolved: {len(self.conflicts)}")
if self.errors:
lines.append(f" Errors encountered: {len(self.errors)}")
for error in self.errors[:5]: # Show first 5 errors
lines.append(f" - {error.message}")
if len(self.errors) > 5:
lines.append(f" ... and {len(self.errors) - 5} more")
return "\n".join(lines)
Skeleton
¶
A description of a set of landmark types and connections between them.
Skeletons are represented by a directed graph composed of a set of Nodes (landmark
types such as body parts) and Edges (connections between parts).
Attributes:
| Name | Type | Description |
|---|---|---|
nodes |
A list of |
|
edges |
A list of |
|
symmetries |
A list of |
|
name |
A descriptive name for the |
Methods:
| Name | Description |
|---|---|
__attrs_post_init__ |
Ensure nodes are |
__contains__ |
Check if a node is in the skeleton. |
__getitem__ |
Return a |
__init__ |
Method generated by attrs for class Skeleton. |
__len__ |
Return the number of nodes in the skeleton. |
__replace__ |
Method generated by attrs for class Skeleton. |
__repr__ |
Return a readable representation of the skeleton. |
__setattr__ |
Method generated by attrs for class Skeleton. |
add_edge |
Add an |
add_edges |
Add multiple |
add_node |
Add a |
add_nodes |
Add multiple |
add_symmetries |
Add multiple |
add_symmetry |
Add a symmetry relationship to the skeleton. |
get_flipped_node_inds |
Returns node indices that should be switched when horizontally flipping. |
index |
Return the index of a node specified as a |
match_nodes |
Return the order of nodes in the skeleton. |
matches |
Check if this skeleton matches another skeleton's structure. |
node_similarities |
Calculate node overlap metrics with another skeleton. |
rebuild_cache |
Rebuild the node name/index to |
remove_node |
Remove a single node from the skeleton. |
remove_nodes |
Remove nodes from the skeleton. |
rename_node |
Rename a single node in the skeleton. |
rename_nodes |
Rename nodes in the skeleton. |
reorder_nodes |
Reorder nodes in the skeleton. |
require_node |
Return a |
Source code in sleap_io/model/skeleton.py
@define(eq=False)
class Skeleton:
"""A description of a set of landmark types and connections between them.
Skeletons are represented by a directed graph composed of a set of `Node`s (landmark
types such as body parts) and `Edge`s (connections between parts).
Attributes:
nodes: A list of `Node`s. May be specified as a list of strings to create new
nodes from their names.
edges: A list of `Edge`s. May be specified as a list of 2-tuples of string names
or integer indices of `nodes`. Each edge corresponds to a pair of source and
destination nodes forming a directed edge.
symmetries: A list of `Symmetry`s. Each symmetry corresponds to symmetric body
parts, such as `"left eye", "right eye"`. This is used when applying flip
(reflection) augmentation to images in order to appropriately swap the
indices of symmetric landmarks.
name: A descriptive name for the `Skeleton`.
"""
def _nodes_on_setattr(self, attr, new_nodes):
"""Callback to update caches when nodes are set."""
self.rebuild_cache(nodes=new_nodes)
return new_nodes
nodes: list[Node] = field(
factory=list,
on_setattr=_nodes_on_setattr,
)
edges: list[Edge] = field(factory=list)
symmetries: list[Symmetry] = field(factory=list)
name: str | None = None
_name_to_node_cache: dict[str, Node] = field(init=False, repr=False, eq=False)
_node_to_ind_cache: dict[Node, int] = field(init=False, repr=False, eq=False)
def __attrs_post_init__(self):
"""Ensure nodes are `Node`s, edges are `Edge`s, and `Node` map is updated."""
self._convert_nodes()
self._convert_edges()
self._convert_symmetries()
self.rebuild_cache()
def _convert_nodes(self):
"""Convert nodes to `Node` objects if needed."""
if isinstance(self.nodes, np.ndarray):
object.__setattr__(self, "nodes", self.nodes.tolist())
for i, node in enumerate(self.nodes):
if type(node) is str:
self.nodes[i] = Node(node)
def _convert_edges(self):
"""Convert list of edge names or integers to `Edge` objects if needed."""
if isinstance(self.edges, np.ndarray):
self.edges = self.edges.tolist()
node_names = self.node_names
for i, edge in enumerate(self.edges):
if type(edge) is Edge:
continue
src, dst = edge
if type(src) is str:
try:
src = node_names.index(src)
except ValueError:
raise ValueError(
f"Node '{src}' specified in the edge list is not in the nodes."
)
if type(src) is int or (
np.isscalar(src) and np.issubdtype(src.dtype, np.integer)
):
src = self.nodes[src]
if type(dst) is str:
try:
dst = node_names.index(dst)
except ValueError:
raise ValueError(
f"Node '{dst}' specified in the edge list is not in the nodes."
)
if type(dst) is int or (
np.isscalar(dst) and np.issubdtype(dst.dtype, np.integer)
):
dst = self.nodes[dst]
self.edges[i] = Edge(src, dst)
def _convert_symmetries(self):
"""Convert list of symmetric node names or integers to `Symmetry` objects."""
if isinstance(self.symmetries, np.ndarray):
self.symmetries = self.symmetries.tolist()
node_names = self.node_names
for i, symmetry in enumerate(self.symmetries):
if type(symmetry) is Symmetry:
continue
node1, node2 = symmetry
if type(node1) is str:
try:
node1 = node_names.index(node1)
except ValueError:
raise ValueError(
f"Node '{node1}' specified in the symmetry list is not in the "
"nodes."
)
if type(node1) is int or (
np.isscalar(node1) and np.issubdtype(node1.dtype, np.integer)
):
node1 = self.nodes[node1]
if type(node2) is str:
try:
node2 = node_names.index(node2)
except ValueError:
raise ValueError(
f"Node '{node2}' specified in the symmetry list is not in the "
"nodes."
)
if type(node2) is int or (
np.isscalar(node2) and np.issubdtype(node2.dtype, np.integer)
):
node2 = self.nodes[node2]
self.symmetries[i] = Symmetry({node1, node2})
def rebuild_cache(self, nodes: list[Node] | None = None):
"""Rebuild the node name/index to `Node` map caches.
Args:
nodes: A list of `Node` objects to update the cache with. If not provided,
the cache will be updated with the current nodes in the skeleton. If
nodes are provided, the cache will be updated with the provided nodes,
but the current nodes in the skeleton will not be updated. Default is
`None`.
Notes:
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or `Node` object.
This is done automatically when nodes are added or removed from the skeleton
using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node
list directly.
"""
if nodes is None:
nodes = self.nodes
self._name_to_node_cache = {node.name: node for node in nodes}
self._node_to_ind_cache = {node: i for i, node in enumerate(nodes)}
@property
def node_names(self) -> list[str]:
"""Names of the nodes associated with this skeleton as a list of strings."""
return [node.name for node in self.nodes]
@property
def edge_inds(self) -> list[tuple[int, int]]:
"""Edges indices as a list of 2-tuples."""
return [
(self.nodes.index(edge.source), self.nodes.index(edge.destination))
for edge in self.edges
]
@property
def edge_names(self) -> list[str, str]:
"""Edge names as a list of 2-tuples with string node names."""
return [(edge.source.name, edge.destination.name) for edge in self.edges]
@property
def symmetry_inds(self) -> list[tuple[int, int]]:
"""Symmetry indices as a list of 2-tuples."""
return [
tuple(sorted((self.index(symmetry[0]), self.index(symmetry[1]))))
for symmetry in self.symmetries
]
@property
def symmetry_names(self) -> list[str, str]:
"""Symmetry names as a list of 2-tuples with string node names."""
return [
(self.nodes[i].name, self.nodes[j].name) for (i, j) in self.symmetry_inds
]
def get_flipped_node_inds(self) -> list[int]:
"""Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when
doing data augmentation.
Example:
>>> skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"])
>>> skel.add_symmetry("B_left", "B_right")
>>> skel.add_symmetry("D_left", "D_right")
>>> skel.flipped_node_inds
[0, 2, 1, 3, 5, 4]
>>> pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> pose[skel.flipped_node_inds]
array([[0, 0],
[2, 2],
[1, 1],
[3, 3],
[5, 5],
[4, 4]])
"""
flip_idx = np.arange(len(self.nodes))
if len(self.symmetries) > 0:
symmetry_inds = np.array(
[(self.index(a), self.index(b)) for a, b in self.symmetries]
)
flip_idx[symmetry_inds[:, 0]] = symmetry_inds[:, 1]
flip_idx[symmetry_inds[:, 1]] = symmetry_inds[:, 0]
flip_idx = flip_idx.tolist()
return flip_idx
def __len__(self) -> int:
"""Return the number of nodes in the skeleton."""
return len(self.nodes)
def __repr__(self) -> str:
"""Return a readable representation of the skeleton."""
nodes = ", ".join([f'"{node}"' for node in self.node_names])
return f"Skeleton(nodes=[{nodes}], edges={self.edge_inds})"
def index(self, node: Node | str) -> int:
"""Return the index of a node specified as a `Node` or string name."""
if type(node) is str:
return self.index(self._name_to_node_cache[node])
elif type(node) is Node:
return self._node_to_ind_cache[node]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {node}")
def __getitem__(self, idx: NodeOrIndex) -> Node:
"""Return a `Node` when indexing by name or integer."""
if type(idx) is int:
return self.nodes[idx]
elif type(idx) is str:
return self._name_to_node_cache[idx]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {idx}")
def __contains__(self, node: NodeOrIndex) -> bool:
"""Check if a node is in the skeleton."""
if type(node) is str:
return node in self._name_to_node_cache
elif type(node) is Node:
return node in self.nodes
elif type(node) is int:
return 0 <= node < len(self.nodes)
else:
raise ValueError(f"Invalid node type for skeleton: {node}")
def add_node(self, node: Node | str):
"""Add a `Node` to the skeleton.
Args:
node: A `Node` object or a string name to create a new node.
Raises:
ValueError: If the node already exists in the skeleton or if the node is
not specified as a `Node` or string.
"""
if node in self:
raise ValueError(f"Node '{node}' already exists in the skeleton.")
if type(node) is str:
node = Node(node)
if type(node) is not Node:
raise ValueError(f"Invalid node type: {node} ({type(node)})")
self.nodes.append(node)
# Atomic update of the cache.
self._name_to_node_cache[node.name] = node
self._node_to_ind_cache[node] = len(self.nodes) - 1
def add_nodes(self, nodes: list[Node | str]):
"""Add multiple `Node`s to the skeleton.
Args:
nodes: A list of `Node` objects or string names to create new nodes.
"""
for node in nodes:
self.add_node(node)
def require_node(self, node: NodeOrIndex, add_missing: bool = True) -> Node:
"""Return a `Node` object, handling indexing and adding missing nodes.
Args:
node: A `Node` object, name or index.
add_missing: If `True`, missing nodes will be added to the skeleton. If
`False`, an error will be raised if the node is not found. Default is
`True`.
Returns:
The `Node` object.
Raises:
IndexError: If the node is not found in the skeleton and `add_missing` is
`False`.
"""
if node not in self:
if add_missing:
self.add_node(node)
else:
raise IndexError(f"Node '{node}' not found in the skeleton.")
if type(node) is Node:
return node
return self[node]
def add_edge(
self,
src: NodeOrIndex | Edge | tuple[NodeOrIndex, NodeOrIndex],
dst: NodeOrIndex | None = None,
):
"""Add an `Edge` to the skeleton.
Args:
src: The source node specified as a `Node`, name or index.
dst: The destination node specified as a `Node`, name or index.
"""
edge = None
if type(src) is tuple:
src, dst = src
if is_node_or_index(src):
if not is_node_or_index(dst):
raise ValueError("Destination node must be specified.")
src = self.require_node(src)
dst = self.require_node(dst)
edge = Edge(src, dst)
if type(src) is Edge:
edge = src
if edge not in self.edges:
self.edges.append(edge)
def add_edges(self, edges: list[Edge | tuple[NodeOrIndex, NodeOrIndex]]):
"""Add multiple `Edge`s to the skeleton.
Args:
edges: A list of `Edge` objects or 2-tuples of source and destination nodes.
"""
for edge in edges:
self.add_edge(edge)
def add_symmetry(
self, node1: Symmetry | NodeOrIndex = None, node2: NodeOrIndex | None = None
):
"""Add a symmetry relationship to the skeleton.
Args:
node1: The first node specified as a `Node`, name or index. If a `Symmetry`
object is provided, it will be added directly to the skeleton.
node2: The second node specified as a `Node`, name or index.
"""
symmetry = None
if type(node1) is Symmetry:
symmetry = node1
node1, node2 = symmetry
node1 = self.require_node(node1)
node2 = self.require_node(node2)
if symmetry is None:
symmetry = Symmetry({node1, node2})
if symmetry not in self.symmetries:
self.symmetries.append(symmetry)
def add_symmetries(
self, symmetries: list[Symmetry | tuple[NodeOrIndex, NodeOrIndex]]
):
"""Add multiple `Symmetry` relationships to the skeleton.
Args:
symmetries: A list of `Symmetry` objects or 2-tuples of symmetric nodes.
"""
for symmetry in symmetries:
self.add_symmetry(*symmetry)
def rename_nodes(self, name_map: dict[NodeOrIndex, str] | list[str]):
"""Rename nodes in the skeleton.
Args:
name_map: A dictionary mapping old node names to new node names. Keys can be
specified as `Node` objects, integer indices, or string names. Values
must be specified as string names.
If a list of strings is provided of the same length as the current
nodes, the nodes will be renamed to the names in the list in order.
Raises:
ValueError: If the new node names exist in the skeleton or if the old node
names are not found in the skeleton.
Notes:
This method should always be used when renaming nodes in the skeleton as it
handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton **do NOT need to be updated**
as the nodes are stored by reference in the skeleton, so changes are
reflected automatically.
Example:
>>> skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")])
>>> skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"})
>>> skel.node_names
["X", "Y", "Z"]
>>> skel.rename_nodes(["a", "b", "c"])
>>> skel.node_names
["a", "b", "c"]
"""
if type(name_map) is list:
if len(name_map) != len(self.nodes):
raise ValueError(
"List of new node names must be the same length as the current "
"nodes."
)
name_map = {node: name for node, name in zip(self.nodes, name_map)}
for old_name, new_name in name_map.items():
if type(old_name) is Node:
old_name = old_name.name
if type(old_name) is int:
old_name = self.nodes[old_name].name
if old_name not in self._name_to_node_cache:
raise ValueError(f"Node '{old_name}' not found in the skeleton.")
if new_name in self._name_to_node_cache:
raise ValueError(f"Node '{new_name}' already exists in the skeleton.")
node = self._name_to_node_cache[old_name]
node.name = new_name
self._name_to_node_cache[new_name] = node
del self._name_to_node_cache[old_name]
def rename_node(self, old_name: NodeOrIndex, new_name: str):
"""Rename a single node in the skeleton.
Args:
old_name: The name of the node to rename. Can also be specified as an
integer index or `Node` object.
new_name: The new name for the node.
"""
self.rename_nodes({old_name: new_name})
def remove_nodes(self, nodes: list[NodeOrIndex]):
"""Remove nodes from the skeleton.
Args:
nodes: A list of node names, indices, or `Node` objects to remove.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed nodes will also
be removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`instance.update_nodes()` on each instance that uses this skeleton.
"""
# Standardize input and make a pre-mutation copy before keys are changed.
rm_node_objs = [self.require_node(node, add_missing=False) for node in nodes]
# Remove nodes from the skeleton.
for node in rm_node_objs:
self.nodes.remove(node)
del self._name_to_node_cache[node.name]
# Remove edges connected to the removed nodes.
self.edges = [
edge
for edge in self.edges
if edge.source not in rm_node_objs and edge.destination not in rm_node_objs
]
# Remove symmetries connected to the removed nodes.
self.symmetries = [
symmetry
for symmetry in self.symmetries
if symmetry.nodes.isdisjoint(rm_node_objs)
]
# Update node index map.
self.rebuild_cache()
def remove_node(self, node: NodeOrIndex):
"""Remove a single node from the skeleton.
Args:
node: The node to remove. Can be specified as a string name, integer index,
or `Node` object.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed node will also be
removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
self.remove_nodes([node])
def reorder_nodes(self, new_order: list[NodeOrIndex]):
"""Reorder nodes in the skeleton.
Args:
new_order: A list of node names, indices, or `Node` objects specifying the
new order of the nodes.
Raises:
ValueError: If the new order of nodes is not the same length as the current
nodes.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Warning:
After reordering, instances using this skeleton do not need to be updated as
the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the `Instance.points` dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
`Labels.reorder_nodes()` method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
if len(new_order) != len(self.nodes):
raise ValueError(
"New order of nodes must be the same length as the current nodes."
)
new_nodes = [self.require_node(node, add_missing=False) for node in new_order]
self.nodes = new_nodes
def match_nodes(self, other_nodes: list[str, Node]) -> tuple[list[int], list[int]]:
"""Return the order of nodes in the skeleton.
Args:
other_nodes: A list of node names or `Node` objects.
Returns:
A tuple of `skeleton_inds, `other_inds`.
`skeleton_inds` contains the indices of the nodes in the skeleton that match
the input nodes.
`other_inds` contains the indices of the input nodes that match the nodes in
the skeleton.
These can be used to reorder point data to match the order of nodes in the
skeleton.
See also: match_nodes_cached
"""
if isinstance(other_nodes, np.ndarray):
other_nodes = other_nodes.tolist()
if type(other_nodes) is not tuple:
other_nodes = [x.name if type(x) is Node else x for x in other_nodes]
skeleton_inds, other_inds = match_nodes_cached(
tuple(self.node_names), tuple(other_nodes)
)
return list(skeleton_inds), list(other_inds)
def matches(self, other: "Skeleton", require_same_order: bool = False) -> bool:
"""Check if this skeleton matches another skeleton's structure.
Args:
other: Another skeleton to compare with.
require_same_order: If True, nodes must be in the same order.
If False, only the node names and edges need to match.
Returns:
True if the skeletons match, False otherwise.
Notes:
Two skeletons match if they have the same nodes (by name) and edges.
If require_same_order is True, the nodes must also be in the same order.
"""
# Check if we have the same number of nodes
if len(self.nodes) != len(other.nodes):
return False
# Check node names
if require_same_order:
if self.node_names != other.node_names:
return False
else:
if set(self.node_names) != set(other.node_names):
return False
# Check edges (considering node name mapping if order differs)
if len(self.edges) != len(other.edges):
return False
# Create edge sets for comparison
self_edge_set = {
(edge.source.name, edge.destination.name) for edge in self.edges
}
other_edge_set = {
(edge.source.name, edge.destination.name) for edge in other.edges
}
if self_edge_set != other_edge_set:
return False
# Check symmetries
if len(self.symmetries) != len(other.symmetries):
return False
self_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in self.symmetries
}
other_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in other.symmetries
}
return self_sym_set == other_sym_set
def node_similarities(self, other: "Skeleton") -> dict[str, float]:
"""Calculate node overlap metrics with another skeleton.
Args:
other: Another skeleton to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_common': Number of nodes in common
- 'n_self_only': Number of nodes only in this skeleton
- 'n_other_only': Number of nodes only in the other skeleton
- 'jaccard': Jaccard similarity (intersection/union)
- 'dice': Dice coefficient (2*intersection/(n_self + n_other))
"""
self_nodes = set(self.node_names)
other_nodes = set(other.node_names)
n_common = len(self_nodes & other_nodes)
n_self_only = len(self_nodes - other_nodes)
n_other_only = len(other_nodes - self_nodes)
n_union = len(self_nodes | other_nodes)
jaccard = n_common / n_union if n_union > 0 else 0
dice = (
2 * n_common / (len(self_nodes) + len(other_nodes))
if (len(self_nodes) + len(other_nodes)) > 0
else 0
)
return {
"n_common": n_common,
"n_self_only": n_self_only,
"n_other_only": n_other_only,
"jaccard": jaccard,
"dice": dice,
}
__annotations__ = {'nodes': 'list[Node]', 'edges': 'list[Edge]', 'symmetries': 'list[Symmetry]', 'name': 'str | None', '_name_to_node_cache': 'dict[str, Node]', '_node_to_ind_cache': 'dict[Node, int]'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = 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 description of a set of landmark types and connections between them.\n\nSkeletons are represented by a directed graph composed of a set of `Node`s (landmark\ntypes such as body parts) and `Edge`s (connections between parts).\n\nAttributes:\n nodes: A list of `Node`s. May be specified as a list of strings to create new\n nodes from their names.\n edges: A list of `Edge`s. May be specified as a list of 2-tuples of string names\n or integer indices of `nodes`. Each edge corresponds to a pair of source and\n destination nodes forming a directed edge.\n symmetries: A list of `Symmetry`s. Each symmetry corresponds to symmetric body\n parts, such as `"left eye", "right eye"`. This is used when applying flip\n (reflection) augmentation to images in order to appropriately swap the\n indices of symmetric landmarks.\n name: A descriptive name for the `Skeleton`.\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__ = 96
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__ = ('nodes', 'edges', 'symmetries', 'name')
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.skeleton'
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__ = ('nodes', 'edges', 'symmetries', 'name', '_name_to_node_cache', '_node_to_ind_cache', '__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__ = ('_name_to_node_cache', '_node_to_ind_cache', 'edges', 'nodes', 'symmetries')
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
edge_inds
property
¶
Edges indices as a list of 2-tuples.
edge_names
property
¶
Edge names as a list of 2-tuples with string node names.
node_names
property
¶
Names of the nodes associated with this skeleton as a list of strings.
symmetry_inds
property
¶
Symmetry indices as a list of 2-tuples.
symmetry_names
property
¶
Symmetry names as a list of 2-tuples with string node names.
__attrs_post_init__()
¶
Ensure nodes are Nodes, edges are Edges, and Node map is updated.
__contains__(node)
¶
Check if a node is in the skeleton.
Source code in sleap_io/model/skeleton.py
def __contains__(self, node: NodeOrIndex) -> bool:
"""Check if a node is in the skeleton."""
if type(node) is str:
return node in self._name_to_node_cache
elif type(node) is Node:
return node in self.nodes
elif type(node) is int:
return 0 <= node < len(self.nodes)
else:
raise ValueError(f"Invalid node type for skeleton: {node}")
__getitem__(idx)
¶
Return a Node when indexing by name or integer.
Source code in sleap_io/model/skeleton.py
__init__(nodes=NOTHING, edges=NOTHING, symmetries=NOTHING, name=None)
¶
Method generated by attrs for class Skeleton.
Source code in sleap_io/model/skeleton.py
"""Data model for skeletons.
Skeletons are collections of nodes and edges which describe the landmarks associated
with a pose model. The edges represent the connections between them and may be used
differently depending on the underlying pose model.
"""
from __future__ import annotations
import typing
from functools import lru_cache
import numpy as np
from attrs import define, field
__len__()
¶
__replace__(**changes)
¶
Method generated by attrs for class Skeleton.
__repr__()
¶
__setattr__(name, val)
¶
Method generated by attrs for class Skeleton.
add_edge(src, dst=None)
¶
Add an Edge to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
src
|
Union | Edge | tuple[Union, Union]
|
The source node specified as a |
required |
dst
|
Union | None
|
The destination node specified as a |
None
|
Source code in sleap_io/model/skeleton.py
def add_edge(
self,
src: NodeOrIndex | Edge | tuple[NodeOrIndex, NodeOrIndex],
dst: NodeOrIndex | None = None,
):
"""Add an `Edge` to the skeleton.
Args:
src: The source node specified as a `Node`, name or index.
dst: The destination node specified as a `Node`, name or index.
"""
edge = None
if type(src) is tuple:
src, dst = src
if is_node_or_index(src):
if not is_node_or_index(dst):
raise ValueError("Destination node must be specified.")
src = self.require_node(src)
dst = self.require_node(dst)
edge = Edge(src, dst)
if type(src) is Edge:
edge = src
if edge not in self.edges:
self.edges.append(edge)
add_edges(edges)
¶
Add multiple Edges to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
edges
|
list[Edge | tuple[Union, Union]]
|
A list of |
required |
add_node(node)
¶
Add a Node to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Node | str
|
A |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the node already exists in the skeleton or if the node is
not specified as a |
Source code in sleap_io/model/skeleton.py
def add_node(self, node: Node | str):
"""Add a `Node` to the skeleton.
Args:
node: A `Node` object or a string name to create a new node.
Raises:
ValueError: If the node already exists in the skeleton or if the node is
not specified as a `Node` or string.
"""
if node in self:
raise ValueError(f"Node '{node}' already exists in the skeleton.")
if type(node) is str:
node = Node(node)
if type(node) is not Node:
raise ValueError(f"Invalid node type: {node} ({type(node)})")
self.nodes.append(node)
# Atomic update of the cache.
self._name_to_node_cache[node.name] = node
self._node_to_ind_cache[node] = len(self.nodes) - 1
add_nodes(nodes)
¶
Add multiple Nodes to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Node | str]
|
A list of |
required |
add_symmetries(symmetries)
¶
Add multiple Symmetry relationships to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
symmetries
|
list[Symmetry | tuple[Union, Union]]
|
A list of |
required |
Source code in sleap_io/model/skeleton.py
add_symmetry(node1=None, node2=None)
¶
Add a symmetry relationship to the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node1
|
Symmetry | Union
|
The first node specified as a |
None
|
node2
|
Union | None
|
The second node specified as a |
None
|
Source code in sleap_io/model/skeleton.py
def add_symmetry(
self, node1: Symmetry | NodeOrIndex = None, node2: NodeOrIndex | None = None
):
"""Add a symmetry relationship to the skeleton.
Args:
node1: The first node specified as a `Node`, name or index. If a `Symmetry`
object is provided, it will be added directly to the skeleton.
node2: The second node specified as a `Node`, name or index.
"""
symmetry = None
if type(node1) is Symmetry:
symmetry = node1
node1, node2 = symmetry
node1 = self.require_node(node1)
node2 = self.require_node(node2)
if symmetry is None:
symmetry = Symmetry({node1, node2})
if symmetry not in self.symmetries:
self.symmetries.append(symmetry)
get_flipped_node_inds()
¶
Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when doing data augmentation.
Example
skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"]) skel.add_symmetry("B_left", "B_right") skel.add_symmetry("D_left", "D_right") skel.flipped_node_inds [0, 2, 1, 3, 5, 4] pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]]) pose[skel.flipped_node_inds] array([[0, 0], [2, 2], [1, 1], [3, 3], [5, 5], [4, 4]])
Source code in sleap_io/model/skeleton.py
def get_flipped_node_inds(self) -> list[int]:
"""Returns node indices that should be switched when horizontally flipping.
This is useful as a lookup table for flipping the landmark coordinates when
doing data augmentation.
Example:
>>> skel = Skeleton(["A", "B_left", "B_right", "C", "D_left", "D_right"])
>>> skel.add_symmetry("B_left", "B_right")
>>> skel.add_symmetry("D_left", "D_right")
>>> skel.flipped_node_inds
[0, 2, 1, 3, 5, 4]
>>> pose = np.array([[0, 0], [1, 1], [2, 2], [3, 3], [4, 4], [5, 5]])
>>> pose[skel.flipped_node_inds]
array([[0, 0],
[2, 2],
[1, 1],
[3, 3],
[5, 5],
[4, 4]])
"""
flip_idx = np.arange(len(self.nodes))
if len(self.symmetries) > 0:
symmetry_inds = np.array(
[(self.index(a), self.index(b)) for a, b in self.symmetries]
)
flip_idx[symmetry_inds[:, 0]] = symmetry_inds[:, 1]
flip_idx[symmetry_inds[:, 1]] = symmetry_inds[:, 0]
flip_idx = flip_idx.tolist()
return flip_idx
index(node)
¶
Return the index of a node specified as a Node or string name.
Source code in sleap_io/model/skeleton.py
def index(self, node: Node | str) -> int:
"""Return the index of a node specified as a `Node` or string name."""
if type(node) is str:
return self.index(self._name_to_node_cache[node])
elif type(node) is Node:
return self._node_to_ind_cache[node]
else:
raise IndexError(f"Invalid indexing argument for skeleton: {node}")
match_nodes(other_nodes)
¶
Return the order of nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other_nodes
|
list[str, Node]
|
A list of node names or |
required |
Returns:
| Type | Description |
|---|---|
tuple[list[int], list[int]]
|
A tuple of
These can be used to reorder point data to match the order of nodes in the skeleton. |
See also: match_nodes_cached
Source code in sleap_io/model/skeleton.py
def match_nodes(self, other_nodes: list[str, Node]) -> tuple[list[int], list[int]]:
"""Return the order of nodes in the skeleton.
Args:
other_nodes: A list of node names or `Node` objects.
Returns:
A tuple of `skeleton_inds, `other_inds`.
`skeleton_inds` contains the indices of the nodes in the skeleton that match
the input nodes.
`other_inds` contains the indices of the input nodes that match the nodes in
the skeleton.
These can be used to reorder point data to match the order of nodes in the
skeleton.
See also: match_nodes_cached
"""
if isinstance(other_nodes, np.ndarray):
other_nodes = other_nodes.tolist()
if type(other_nodes) is not tuple:
other_nodes = [x.name if type(x) is Node else x for x in other_nodes]
skeleton_inds, other_inds = match_nodes_cached(
tuple(self.node_names), tuple(other_nodes)
)
return list(skeleton_inds), list(other_inds)
matches(other, require_same_order=False)
¶
Check if this skeleton matches another skeleton's structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Skeleton
|
Another skeleton to compare with. |
required |
require_same_order
|
bool
|
If True, nodes must be in the same order. If False, only the node names and edges need to match. |
False
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the skeletons match, False otherwise. |
Notes
Two skeletons match if they have the same nodes (by name) and edges. If require_same_order is True, the nodes must also be in the same order.
Source code in sleap_io/model/skeleton.py
def matches(self, other: "Skeleton", require_same_order: bool = False) -> bool:
"""Check if this skeleton matches another skeleton's structure.
Args:
other: Another skeleton to compare with.
require_same_order: If True, nodes must be in the same order.
If False, only the node names and edges need to match.
Returns:
True if the skeletons match, False otherwise.
Notes:
Two skeletons match if they have the same nodes (by name) and edges.
If require_same_order is True, the nodes must also be in the same order.
"""
# Check if we have the same number of nodes
if len(self.nodes) != len(other.nodes):
return False
# Check node names
if require_same_order:
if self.node_names != other.node_names:
return False
else:
if set(self.node_names) != set(other.node_names):
return False
# Check edges (considering node name mapping if order differs)
if len(self.edges) != len(other.edges):
return False
# Create edge sets for comparison
self_edge_set = {
(edge.source.name, edge.destination.name) for edge in self.edges
}
other_edge_set = {
(edge.source.name, edge.destination.name) for edge in other.edges
}
if self_edge_set != other_edge_set:
return False
# Check symmetries
if len(self.symmetries) != len(other.symmetries):
return False
self_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in self.symmetries
}
other_sym_set = {
frozenset(node.name for node in sym.nodes) for sym in other.symmetries
}
return self_sym_set == other_sym_set
node_similarities(other)
¶
Calculate node overlap metrics with another skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Skeleton
|
Another skeleton to compare with. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, float]
|
A dictionary with similarity metrics: - 'n_common': Number of nodes in common - 'n_self_only': Number of nodes only in this skeleton - 'n_other_only': Number of nodes only in the other skeleton - 'jaccard': Jaccard similarity (intersection/union) - 'dice': Dice coefficient (2*intersection/(n_self + n_other)) |
Source code in sleap_io/model/skeleton.py
def node_similarities(self, other: "Skeleton") -> dict[str, float]:
"""Calculate node overlap metrics with another skeleton.
Args:
other: Another skeleton to compare with.
Returns:
A dictionary with similarity metrics:
- 'n_common': Number of nodes in common
- 'n_self_only': Number of nodes only in this skeleton
- 'n_other_only': Number of nodes only in the other skeleton
- 'jaccard': Jaccard similarity (intersection/union)
- 'dice': Dice coefficient (2*intersection/(n_self + n_other))
"""
self_nodes = set(self.node_names)
other_nodes = set(other.node_names)
n_common = len(self_nodes & other_nodes)
n_self_only = len(self_nodes - other_nodes)
n_other_only = len(other_nodes - self_nodes)
n_union = len(self_nodes | other_nodes)
jaccard = n_common / n_union if n_union > 0 else 0
dice = (
2 * n_common / (len(self_nodes) + len(other_nodes))
if (len(self_nodes) + len(other_nodes)) > 0
else 0
)
return {
"n_common": n_common,
"n_self_only": n_self_only,
"n_other_only": n_other_only,
"jaccard": jaccard,
"dice": dice,
}
rebuild_cache(nodes=None)
¶
Rebuild the node name/index to Node map caches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Node] | None
|
A list of |
None
|
Notes
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or Node object.
This is done automatically when nodes are added or removed from the skeleton using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node list directly.
Source code in sleap_io/model/skeleton.py
def rebuild_cache(self, nodes: list[Node] | None = None):
"""Rebuild the node name/index to `Node` map caches.
Args:
nodes: A list of `Node` objects to update the cache with. If not provided,
the cache will be updated with the current nodes in the skeleton. If
nodes are provided, the cache will be updated with the provided nodes,
but the current nodes in the skeleton will not be updated. Default is
`None`.
Notes:
This function should be called when nodes or node list is mutated to update
the lookup caches for indexing nodes by name or `Node` object.
This is done automatically when nodes are added or removed from the skeleton
using the convenience methods in this class.
This method only needs to be used when manually mutating nodes or the node
list directly.
"""
if nodes is None:
nodes = self.nodes
self._name_to_node_cache = {node.name: node for node in nodes}
self._node_to_ind_cache = {node: i for i, node in enumerate(nodes)}
remove_node(node)
¶
Remove a single node from the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Union
|
The node to remove. Can be specified as a string name, integer index,
or |
required |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Any edges and symmetries that are connected to the removed node will also be removed.
Warning
This method does NOT update instances that use this skeleton to reflect changes.
It is recommended to use the Labels.remove_nodes() method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
Instance.update_skeleton() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def remove_node(self, node: NodeOrIndex):
"""Remove a single node from the skeleton.
Args:
node: The node to remove. Can be specified as a string name, integer index,
or `Node` object.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed node will also be
removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained instances to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
self.remove_nodes([node])
remove_nodes(nodes)
¶
Remove nodes from the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nodes
|
list[Union]
|
A list of node names, indices, or |
required |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Any edges and symmetries that are connected to the removed nodes will also be removed.
Warning
This method does NOT update instances that use this skeleton to reflect changes.
It is recommended to use the Labels.remove_nodes() method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
instance.update_nodes() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def remove_nodes(self, nodes: list[NodeOrIndex]):
"""Remove nodes from the skeleton.
Args:
nodes: A list of node names, indices, or `Node` objects to remove.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Any edges and symmetries that are connected to the removed nodes will also
be removed.
Warning:
**This method does NOT update instances** that use this skeleton to reflect
changes.
It is recommended to use the `Labels.remove_nodes()` method which will
update all contained to reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`instance.update_nodes()` on each instance that uses this skeleton.
"""
# Standardize input and make a pre-mutation copy before keys are changed.
rm_node_objs = [self.require_node(node, add_missing=False) for node in nodes]
# Remove nodes from the skeleton.
for node in rm_node_objs:
self.nodes.remove(node)
del self._name_to_node_cache[node.name]
# Remove edges connected to the removed nodes.
self.edges = [
edge
for edge in self.edges
if edge.source not in rm_node_objs and edge.destination not in rm_node_objs
]
# Remove symmetries connected to the removed nodes.
self.symmetries = [
symmetry
for symmetry in self.symmetries
if symmetry.nodes.isdisjoint(rm_node_objs)
]
# Update node index map.
self.rebuild_cache()
rename_node(old_name, new_name)
¶
Rename a single node in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
old_name
|
Union
|
The name of the node to rename. Can also be specified as an
integer index or |
required |
new_name
|
str
|
The new name for the node. |
required |
Source code in sleap_io/model/skeleton.py
rename_nodes(name_map)
¶
Rename nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name_map
|
dict[Union, str] | list[str]
|
A dictionary mapping old node names to new node names. Keys can be
specified as If a list of strings is provided of the same length as the current nodes, the nodes will be renamed to the names in the list in order. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the new node names exist in the skeleton or if the old node names are not found in the skeleton. |
Notes
This method should always be used when renaming nodes in the skeleton as it handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton do NOT need to be updated as the nodes are stored by reference in the skeleton, so changes are reflected automatically.
Example
skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")]) skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"}) skel.node_names ["X", "Y", "Z"] skel.rename_nodes(["a", "b", "c"]) skel.node_names ["a", "b", "c"]
Source code in sleap_io/model/skeleton.py
def rename_nodes(self, name_map: dict[NodeOrIndex, str] | list[str]):
"""Rename nodes in the skeleton.
Args:
name_map: A dictionary mapping old node names to new node names. Keys can be
specified as `Node` objects, integer indices, or string names. Values
must be specified as string names.
If a list of strings is provided of the same length as the current
nodes, the nodes will be renamed to the names in the list in order.
Raises:
ValueError: If the new node names exist in the skeleton or if the old node
names are not found in the skeleton.
Notes:
This method should always be used when renaming nodes in the skeleton as it
handles updating the lookup caches necessary for indexing nodes by name.
After renaming, instances using this skeleton **do NOT need to be updated**
as the nodes are stored by reference in the skeleton, so changes are
reflected automatically.
Example:
>>> skel = Skeleton(["A", "B", "C"], edges=[("A", "B"), ("B", "C")])
>>> skel.rename_nodes({"A": "X", "B": "Y", "C": "Z"})
>>> skel.node_names
["X", "Y", "Z"]
>>> skel.rename_nodes(["a", "b", "c"])
>>> skel.node_names
["a", "b", "c"]
"""
if type(name_map) is list:
if len(name_map) != len(self.nodes):
raise ValueError(
"List of new node names must be the same length as the current "
"nodes."
)
name_map = {node: name for node, name in zip(self.nodes, name_map)}
for old_name, new_name in name_map.items():
if type(old_name) is Node:
old_name = old_name.name
if type(old_name) is int:
old_name = self.nodes[old_name].name
if old_name not in self._name_to_node_cache:
raise ValueError(f"Node '{old_name}' not found in the skeleton.")
if new_name in self._name_to_node_cache:
raise ValueError(f"Node '{new_name}' already exists in the skeleton.")
node = self._name_to_node_cache[old_name]
node.name = new_name
self._name_to_node_cache[new_name] = node
del self._name_to_node_cache[old_name]
reorder_nodes(new_order)
¶
Reorder nodes in the skeleton.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_order
|
list[Union]
|
A list of node names, indices, or |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the new order of nodes is not the same length as the current nodes. |
Notes
This method handles updating the lookup caches necessary for indexing nodes by name.
Warning
After reordering, instances using this skeleton do not need to be updated as the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the Instance.points dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
Labels.reorder_nodes() method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
Instance.update_skeleton() on each instance that uses this skeleton.
Source code in sleap_io/model/skeleton.py
def reorder_nodes(self, new_order: list[NodeOrIndex]):
"""Reorder nodes in the skeleton.
Args:
new_order: A list of node names, indices, or `Node` objects specifying the
new order of the nodes.
Raises:
ValueError: If the new order of nodes is not the same length as the current
nodes.
Notes:
This method handles updating the lookup caches necessary for indexing nodes
by name.
Warning:
After reordering, instances using this skeleton do not need to be updated as
the nodes are stored by reference in the skeleton.
However, the order that points are stored in the instances will not be
updated to match the new order of the nodes in the skeleton. This should not
matter unless the ordering of the keys in the `Instance.points` dictionary
is used instead of relying on the skeleton node order.
To make sure these are aligned, it is recommended to use the
`Labels.reorder_nodes()` method which will update all contained instances to
reflect the changes made to the skeleton.
To manually update instances after this method is called, call
`Instance.update_skeleton()` on each instance that uses this skeleton.
"""
if len(new_order) != len(self.nodes):
raise ValueError(
"New order of nodes must be the same length as the current nodes."
)
new_nodes = [self.require_node(node, add_missing=False) for node in new_order]
self.nodes = new_nodes
require_node(node, add_missing=True)
¶
Return a Node object, handling indexing and adding missing nodes.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
node
|
Union
|
A |
required |
add_missing
|
bool
|
If |
True
|
Returns:
| Type | Description |
|---|---|
Node
|
The |
Raises:
| Type | Description |
|---|---|
IndexError
|
If the node is not found in the skeleton and |
Source code in sleap_io/model/skeleton.py
def require_node(self, node: NodeOrIndex, add_missing: bool = True) -> Node:
"""Return a `Node` object, handling indexing and adding missing nodes.
Args:
node: A `Node` object, name or index.
add_missing: If `True`, missing nodes will be added to the skeleton. If
`False`, an error will be raised if the node is not found. Default is
`True`.
Returns:
The `Node` object.
Raises:
IndexError: If the node is not found in the skeleton and `add_missing` is
`False`.
"""
if node not in self:
if add_missing:
self.add_node(node)
else:
raise IndexError(f"Node '{node}' not found in the skeleton.")
if type(node) is Node:
return node
return self[node]
SkeletonMatchMethod
¶
Bases: builtins.str, enum.Enum
Methods for matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
EXACT |
Exact match requiring same nodes in the same order. |
|
STRUCTURE |
Match requiring same nodes and edges, but order doesn't matter. |
|
OVERLAP |
Partial match based on overlapping nodes (uses Jaccard similarity). |
|
SUBSET |
Match if one skeleton's nodes are a subset of another's. |
Source code in sleap_io/model/matching.py
class SkeletonMatchMethod(str, Enum):
"""Methods for matching skeletons.
Attributes:
EXACT: Exact match requiring same nodes in the same order.
STRUCTURE: Match requiring same nodes and edges, but order doesn't matter.
OVERLAP: Partial match based on overlapping nodes (uses Jaccard similarity).
SUBSET: Match if one skeleton's nodes are a subset of another's.
"""
EXACT = "exact"
STRUCTURE = "structure"
OVERLAP = "overlap"
SUBSET = "subset"
EXACT = <SkeletonMatchMethod.EXACT: 'exact'>
class-attribute
¶
Methods for matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
EXACT |
Exact match requiring same nodes in the same order. |
|
STRUCTURE |
Match requiring same nodes and edges, but order doesn't matter. |
|
OVERLAP |
Partial match based on overlapping nodes (uses Jaccard similarity). |
|
SUBSET |
Match if one skeleton's nodes are a subset of another's. |
OVERLAP = <SkeletonMatchMethod.OVERLAP: 'overlap'>
class-attribute
¶
Methods for matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
EXACT |
Exact match requiring same nodes in the same order. |
|
STRUCTURE |
Match requiring same nodes and edges, but order doesn't matter. |
|
OVERLAP |
Partial match based on overlapping nodes (uses Jaccard similarity). |
|
SUBSET |
Match if one skeleton's nodes are a subset of another's. |
STRUCTURE = <SkeletonMatchMethod.STRUCTURE: 'structure'>
class-attribute
¶
Methods for matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
EXACT |
Exact match requiring same nodes in the same order. |
|
STRUCTURE |
Match requiring same nodes and edges, but order doesn't matter. |
|
OVERLAP |
Partial match based on overlapping nodes (uses Jaccard similarity). |
|
SUBSET |
Match if one skeleton's nodes are a subset of another's. |
SUBSET = <SkeletonMatchMethod.SUBSET: 'subset'>
class-attribute
¶
Methods for matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
EXACT |
Exact match requiring same nodes in the same order. |
|
STRUCTURE |
Match requiring same nodes and edges, but order doesn't matter. |
|
OVERLAP |
Partial match based on overlapping nodes (uses Jaccard similarity). |
|
SUBSET |
Match if one skeleton's nodes are a subset of another's. |
__doc__ = "Methods for matching skeletons.\n\nAttributes:\n EXACT: Exact match requiring same nodes in the same order.\n STRUCTURE: Match requiring same nodes and edges, but order doesn't matter.\n OVERLAP: Partial match based on overlapping nodes (uses Jaccard similarity).\n SUBSET: Match if one skeleton's nodes are a subset of another's.\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'.
__module__ = 'sleap_io.model.matching'
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'.
SkeletonMatcher
¶
Matcher for comparing and matching skeletons.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a SkeletonMatchMethod enum value or a string that will be converted to the enum. Default is STRUCTURE. |
|
require_same_order |
Whether to require nodes in the same order for STRUCTURE matching. Only used when method is STRUCTURE. Default is False. |
|
min_overlap |
Minimum Jaccard similarity required for OVERLAP matching. Only used when method is OVERLAP. Default is 0.5. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class SkeletonMatcher. |
__init__ |
Method generated by attrs for class SkeletonMatcher. |
__replace__ |
Method generated by attrs for class SkeletonMatcher. |
__repr__ |
Method generated by attrs for class SkeletonMatcher. |
__setattr__ |
Method generated by attrs for class SkeletonMatcher. |
match |
Check if two skeletons match according to the configured method. |
Source code in sleap_io/model/matching.py
@attrs.define
class SkeletonMatcher:
"""Matcher for comparing and matching skeletons.
Attributes:
method: The matching method to use. Can be a SkeletonMatchMethod enum value
or a string that will be converted to the enum. Default is STRUCTURE.
require_same_order: Whether to require nodes in the same order for STRUCTURE
matching. Only used when method is STRUCTURE. Default is False.
min_overlap: Minimum Jaccard similarity required for OVERLAP matching.
Only used when method is OVERLAP. Default is 0.5.
"""
method: SkeletonMatchMethod | str = attrs.field(
default=SkeletonMatchMethod.STRUCTURE,
converter=lambda x: SkeletonMatchMethod(x) if isinstance(x, str) else x,
)
require_same_order: bool = False
min_overlap: float = 0.5
def match(self, skeleton1: Skeleton, skeleton2: Skeleton) -> bool:
"""Check if two skeletons match according to the configured method."""
if self.method == SkeletonMatchMethod.EXACT:
return skeleton1.matches(skeleton2, require_same_order=True)
elif self.method == SkeletonMatchMethod.STRUCTURE:
return skeleton1.matches(
skeleton2, require_same_order=self.require_same_order
)
elif self.method == SkeletonMatchMethod.OVERLAP:
metrics = skeleton1.node_similarities(skeleton2)
return metrics["jaccard"] >= self.min_overlap
elif self.method == SkeletonMatchMethod.SUBSET:
# Check if skeleton1 nodes are subset of skeleton2
nodes1 = set(skeleton1.node_names)
nodes2 = set(skeleton2.node_names)
return nodes1.issubset(nodes2)
else:
raise ValueError(f"Unknown skeleton match method: {self.method}")
__annotations__ = {'method': 'SkeletonMatchMethod | str', 'require_same_order': 'bool', 'min_overlap': 'float'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = True
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=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__ = 'Matcher for comparing and matching skeletons.\n\nAttributes:\n method: The matching method to use. Can be a SkeletonMatchMethod enum value\n or a string that will be converted to the enum. Default is STRUCTURE.\n require_same_order: Whether to require nodes in the same order for STRUCTURE\n matching. Only used when method is STRUCTURE. Default is False.\n min_overlap: Minimum Jaccard similarity required for OVERLAP matching.\n Only used when method is OVERLAP. Default is 0.5.\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__ = 360
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__ = ('method', 'require_same_order', 'min_overlap')
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.matching'
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__ = ('method', 'require_same_order', 'min_overlap', '__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__(method=<SkeletonMatchMethod.STRUCTURE: 'structure'>, require_same_order=False, min_overlap=0.5)
¶
__replace__(**changes)
¶
Method generated by attrs for class SkeletonMatcher.
__repr__()
¶
Method generated by attrs for class SkeletonMatcher.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
__setattr__(name, val)
¶
Method generated by attrs for class SkeletonMatcher.
match(skeleton1, skeleton2)
¶
Check if two skeletons match according to the configured method.
Source code in sleap_io/model/matching.py
def match(self, skeleton1: Skeleton, skeleton2: Skeleton) -> bool:
"""Check if two skeletons match according to the configured method."""
if self.method == SkeletonMatchMethod.EXACT:
return skeleton1.matches(skeleton2, require_same_order=True)
elif self.method == SkeletonMatchMethod.STRUCTURE:
return skeleton1.matches(
skeleton2, require_same_order=self.require_same_order
)
elif self.method == SkeletonMatchMethod.OVERLAP:
metrics = skeleton1.node_similarities(skeleton2)
return metrics["jaccard"] >= self.min_overlap
elif self.method == SkeletonMatchMethod.SUBSET:
# Check if skeleton1 nodes are subset of skeleton2
nodes1 = set(skeleton1.node_names)
nodes2 = set(skeleton2.node_names)
return nodes1.issubset(nodes2)
else:
raise ValueError(f"Unknown skeleton match method: {self.method}")
SkeletonMismatchError
¶
Bases: sleap_io.model.matching.MergeError
Raised when skeletons don't match during merge.
Attributes:
| Name | Type | Description |
|---|---|---|
__annotations__ |
dict() -> new empty dictionary |
|
__doc__ |
str(object='') -> str |
|
__firstlineno__ |
int([x]) -> integer |
|
__module__ |
str(object='') -> str |
|
__static_attributes__ |
Built-in immutable sequence. |
Source code in sleap_io/model/matching.py
__annotations__ = {}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__doc__ = "Raised when skeletons don't match during merge."
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__ = 743
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
__module__ = 'sleap_io.model.matching'
class-attribute
¶
str(object='') -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.str() (if defined) or repr(object). encoding defaults to 'utf-8'. errors defaults to 'strict'.
__static_attributes__ = ()
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.
Track
¶
An object that represents the same animal/object across multiple detections.
This allows tracking of unique entities in the video over time and space.
A Track may also be used to refer to unique identity classes that span multiple
videos, such as "female mouse".
Attributes:
| Name | Type | Description |
|---|---|---|
name |
A name given to this track for identification purposes. |
Notes
Tracks are compared by identity. This means that unique track objects with the
same name are considered to be different.
Methods:
| Name | Description |
|---|---|
__init__ |
Method generated by attrs for class Track. |
__replace__ |
Method generated by attrs for class Track. |
__repr__ |
Method generated by attrs for class Track. |
matches |
Check if this track matches another track. |
similarity_to |
Calculate similarity metrics with another track. |
Source code in sleap_io/model/instance.py
@attrs.define(eq=False)
class Track:
"""An object that represents the same animal/object across multiple detections.
This allows tracking of unique entities in the video over time and space.
A `Track` may also be used to refer to unique identity classes that span multiple
videos, such as `"female mouse"`.
Attributes:
name: A name given to this track for identification purposes.
Notes:
`Track`s are compared by identity. This means that unique track objects with the
same name are considered to be different.
"""
name: str = ""
def matches(self, other: "Track", method: str = "name") -> bool:
"""Check if this track matches another track.
Args:
other: Another track to compare with.
method: Matching method - "name" (match by name) or "identity"
(match by object identity).
Returns:
True if the tracks match according to the specified method.
"""
if method == "name":
return self.name == other.name
elif method == "identity":
return self is other
else:
raise ValueError(f"Unknown matching method: {method}")
def similarity_to(self, other: "Track") -> dict[str, any]:
"""Calculate similarity metrics with another track.
Args:
other: Another track to compare with.
Returns:
A dictionary with similarity metrics:
- 'same_name': Whether the tracks have the same name
- 'same_identity': Whether the tracks are the same object
- 'name_similarity': Simple string similarity score (0-1)
"""
# Calculate simple string similarity
if self.name and other.name:
# Simple character overlap similarity
common_chars = set(self.name.lower()) & set(other.name.lower())
all_chars = set(self.name.lower()) | set(other.name.lower())
name_similarity = len(common_chars) / len(all_chars) if all_chars else 0
else:
name_similarity = 1.0 if self.name == other.name else 0.0
return {
"same_name": self.name == other.name,
"same_identity": self is other,
"name_similarity": name_similarity,
}
__annotations__ = {'name': 'str'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = False
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=False, added_ordering=False, hashability=<Hashability.LEAVE_ALONE: 'leave_alone'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 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__ = 'An object that represents the same animal/object across multiple detections.\n\nThis allows tracking of unique entities in the video over time and space.\n\nA `Track` may also be used to refer to unique identity classes that span multiple\nvideos, such as `"female mouse"`.\n\nAttributes:\n name: A name given to this track for identification purposes.\n\nNotes:\n `Track`s are compared by identity. This means that unique track objects with the\n same name are considered to be different.\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__ = 321
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__ = ('name',)
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__ = ('name', '__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
__init__(name='')
¶
__replace__(**changes)
¶
Method generated by attrs for class Track.
__repr__()
¶
Method generated by attrs for class Track.
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.
"""
from __future__ import annotations
import attrs
import numpy as np
from sleap_io.model.skeleton import Node, Skeleton
matches(other, method='name')
¶
Check if this track matches another track.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Track
|
Another track to compare with. |
required |
method
|
str
|
Matching method - "name" (match by name) or "identity" (match by object identity). |
'name'
|
Returns:
| Type | Description |
|---|---|
bool
|
True if the tracks match according to the specified method. |
Source code in sleap_io/model/instance.py
def matches(self, other: "Track", method: str = "name") -> bool:
"""Check if this track matches another track.
Args:
other: Another track to compare with.
method: Matching method - "name" (match by name) or "identity"
(match by object identity).
Returns:
True if the tracks match according to the specified method.
"""
if method == "name":
return self.name == other.name
elif method == "identity":
return self is other
else:
raise ValueError(f"Unknown matching method: {method}")
similarity_to(other)
¶
Calculate similarity metrics with another track.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Track
|
Another track to compare with. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, any]
|
A dictionary with similarity metrics: - 'same_name': Whether the tracks have the same name - 'same_identity': Whether the tracks are the same object - 'name_similarity': Simple string similarity score (0-1) |
Source code in sleap_io/model/instance.py
def similarity_to(self, other: "Track") -> dict[str, any]:
"""Calculate similarity metrics with another track.
Args:
other: Another track to compare with.
Returns:
A dictionary with similarity metrics:
- 'same_name': Whether the tracks have the same name
- 'same_identity': Whether the tracks are the same object
- 'name_similarity': Simple string similarity score (0-1)
"""
# Calculate simple string similarity
if self.name and other.name:
# Simple character overlap similarity
common_chars = set(self.name.lower()) & set(other.name.lower())
all_chars = set(self.name.lower()) | set(other.name.lower())
name_similarity = len(common_chars) / len(all_chars) if all_chars else 0
else:
name_similarity = 1.0 if self.name == other.name else 0.0
return {
"same_name": self.name == other.name,
"same_identity": self is other,
"name_similarity": name_similarity,
}
TrackMatchMethod
¶
Bases: builtins.str, enum.Enum
Methods for matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
NAME |
Match tracks by their name attribute. |
|
IDENTITY |
Match tracks by object identity (same Python object). |
Source code in sleap_io/model/matching.py
IDENTITY = <TrackMatchMethod.IDENTITY: 'identity'>
class-attribute
¶
Methods for matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
NAME |
Match tracks by their name attribute. |
|
IDENTITY |
Match tracks by object identity (same Python object). |
NAME = <TrackMatchMethod.NAME: 'name'>
class-attribute
¶
Methods for matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
NAME |
Match tracks by their name attribute. |
|
IDENTITY |
Match tracks by object identity (same Python object). |
__doc__ = 'Methods for matching tracks.\n\nAttributes:\n NAME: Match tracks by their name attribute.\n IDENTITY: Match tracks by object identity (same Python object).\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'.
__module__ = 'sleap_io.model.matching'
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'.
TrackMatcher
¶
Matcher for comparing and matching tracks.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a TrackMatchMethod enum value or a string that will be converted to the enum. Default is NAME. |
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class TrackMatcher. |
__init__ |
Method generated by attrs for class TrackMatcher. |
__replace__ |
Method generated by attrs for class TrackMatcher. |
__repr__ |
Method generated by attrs for class TrackMatcher. |
__setattr__ |
Method generated by attrs for class TrackMatcher. |
match |
Check if two tracks match according to the configured method. |
Source code in sleap_io/model/matching.py
@attrs.define
class TrackMatcher:
"""Matcher for comparing and matching tracks.
Attributes:
method: The matching method to use. Can be a TrackMatchMethod enum value
or a string that will be converted to the enum. Default is NAME.
"""
method: TrackMatchMethod | str = attrs.field(
default=TrackMatchMethod.NAME,
converter=lambda x: TrackMatchMethod(x) if isinstance(x, str) else x,
)
def match(self, track1: Track, track2: Track) -> bool:
"""Check if two tracks match according to the configured method."""
return track1.matches(track2, method=self.method.value)
__annotations__ = {'method': 'TrackMatchMethod | str'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = True
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=True, added_ordering=False, hashability=<Hashability.UNHASHABLE: 'unhashable'>, added_match_args=True, added_str=False, added_pickling=True, on_setattr_hook=<function pipe.<locals>.wrapped_pipe at 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__ = 'Matcher for comparing and matching tracks.\n\nAttributes:\n method: The matching method to use. Can be a TrackMatchMethod enum value\n or a string that will be converted to the enum. Default is NAME.\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__ = 487
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__ = ('method',)
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.matching'
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__ = ('method', '__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__(method=<TrackMatchMethod.NAME: 'name'>)
¶
__replace__(**changes)
¶
Method generated by attrs for class TrackMatcher.
__repr__()
¶
Method generated by attrs for class TrackMatcher.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
__setattr__(name, val)
¶
Method generated by attrs for class TrackMatcher.
match(track1, track2)
¶
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()
VideoMatchMethod
¶
Bases: builtins.str, enum.Enum
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
Source code in sleap_io/model/matching.py
class VideoMatchMethod(str, Enum):
"""Methods for matching videos.
Attributes:
PATH: Match by exact file path (strict or lenient based on
VideoMatcher.strict setting).
BASENAME: Match by filename only, ignoring directory paths.
CONTENT: Match by video shape (frames, height, width, channels) and
backend type.
AUTO: Automatic matching - tries BASENAME first, then falls back to CONTENT.
IMAGE_DEDUP: (ImageVideo only) Match ImageVideo instances with overlapping
image files. Used to deduplicate individual images when merging datasets
where videos are image sequences.
SHAPE: Match videos by shape only (height, width, channels), ignoring
filenames and frame count. Commonly used with ImageVideo to merge
same-shaped image sequences.
"""
PATH = "path"
BASENAME = "basename"
CONTENT = "content"
AUTO = "auto"
IMAGE_DEDUP = "image_dedup"
SHAPE = "shape"
AUTO = <VideoMatchMethod.AUTO: 'auto'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
BASENAME = <VideoMatchMethod.BASENAME: 'basename'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
CONTENT = <VideoMatchMethod.CONTENT: 'content'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
IMAGE_DEDUP = <VideoMatchMethod.IMAGE_DEDUP: 'image_dedup'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
PATH = <VideoMatchMethod.PATH: 'path'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
SHAPE = <VideoMatchMethod.SHAPE: 'shape'>
class-attribute
¶
Methods for matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
PATH |
Match by exact file path (strict or lenient based on VideoMatcher.strict setting). |
|
BASENAME |
Match by filename only, ignoring directory paths. |
|
CONTENT |
Match by video shape (frames, height, width, channels) and backend type. |
|
AUTO |
Automatic matching - tries BASENAME first, then falls back to CONTENT. |
|
IMAGE_DEDUP |
(ImageVideo only) Match ImageVideo instances with overlapping image files. Used to deduplicate individual images when merging datasets where videos are image sequences. |
|
SHAPE |
Match videos by shape only (height, width, channels), ignoring filenames and frame count. Commonly used with ImageVideo to merge same-shaped image sequences. |
__doc__ = 'Methods for matching videos.\n\nAttributes:\n PATH: Match by exact file path (strict or lenient based on\n VideoMatcher.strict setting).\n BASENAME: Match by filename only, ignoring directory paths.\n CONTENT: Match by video shape (frames, height, width, channels) and\n backend type.\n AUTO: Automatic matching - tries BASENAME first, then falls back to CONTENT.\n IMAGE_DEDUP: (ImageVideo only) Match ImageVideo instances with overlapping\n image files. Used to deduplicate individual images when merging datasets\n where videos are image sequences.\n SHAPE: Match videos by shape only (height, width, channels), ignoring\n filenames and frame count. Commonly used with ImageVideo to merge\n same-shaped image sequences.\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'.
__module__ = 'sleap_io.model.matching'
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'.
VideoMatcher
¶
Matcher for comparing and matching videos.
Attributes:
| Name | Type | Description |
|---|---|---|
method |
The matching method to use. Can be a VideoMatchMethod enum value or a string that will be converted to the enum. Default is AUTO. |
|
strict |
Whether to use strict path matching for the PATH method. When True, paths must be exactly identical. When False, paths are normalized before comparison. Only used when method is PATH. Default is False. |
Notes
For AUTO method, use find_match() when matching against a list of candidates. The match() method for AUTO uses a simplified pairwise check that doesn't include the full leaf-uniqueness algorithm.
Methods:
| Name | Description |
|---|---|
__eq__ |
Method generated by attrs for class VideoMatcher. |
__init__ |
Method generated by attrs for class VideoMatcher. |
__replace__ |
Method generated by attrs for class VideoMatcher. |
__repr__ |
Method generated by attrs for class VideoMatcher. |
__setattr__ |
Method generated by attrs for class VideoMatcher. |
find_match |
Find a matching video from candidates using the configured method. |
match |
Check if two videos match according to the configured method. |
Source code in sleap_io/model/matching.py
@attrs.define
class VideoMatcher:
"""Matcher for comparing and matching videos.
Attributes:
method: The matching method to use. Can be a VideoMatchMethod enum value
or a string that will be converted to the enum. Default is AUTO.
strict: Whether to use strict path matching for the PATH method.
When True, paths must be exactly identical. When False, paths
are normalized before comparison. Only used when method is PATH.
Default is False.
Notes:
For AUTO method, use find_match() when matching against a list of
candidates. The match() method for AUTO uses a simplified pairwise
check that doesn't include the full leaf-uniqueness algorithm.
"""
method: VideoMatchMethod | str = attrs.field(
default=VideoMatchMethod.AUTO,
converter=lambda x: VideoMatchMethod(x) if isinstance(x, str) else x,
)
strict: bool = False
def match(self, video1: Video, video2: Video) -> bool:
"""Check if two videos match according to the configured method.
For AUTO method, this performs pairwise checks (file identity, path match).
For full AUTO matching with leaf-uniqueness, use find_match() instead.
"""
if self.method == VideoMatchMethod.AUTO:
# Pairwise AUTO: rejection checks + definitive identity + path match
# (Leaf-uniqueness requires full candidate list - use find_match())
# Rejection: incompatible shapes
if shapes_compatible(video1, video2) is False:
return False
# Rejection: conflicting provenance
if original_videos_conflict(video1, video2):
return False
# Definitive: same file identity
if is_same_file(video1, video2):
return True
# String: strict path match
if video1.matches_path(video2, strict=True):
return True
# String: basename match (for pairwise, this is the fallback)
if video1.matches_path(video2, strict=False):
return True
return False
elif self.method == VideoMatchMethod.PATH:
return video1.matches_path(video2, strict=self.strict)
elif self.method == VideoMatchMethod.BASENAME:
return video1.matches_path(video2, strict=False)
elif self.method == VideoMatchMethod.CONTENT:
return video1.matches_content(video2)
elif self.method == VideoMatchMethod.IMAGE_DEDUP:
# Match ImageVideo instances with overlapping images (ImageVideo only)
return video1.has_overlapping_images(video2)
elif self.method == VideoMatchMethod.SHAPE:
# Match videos by shape only (height, width, channels)
return video1.matches_shape(video2)
else:
raise ValueError(f"Unknown video match method: {self.method}")
def find_match(
self,
incoming: Video,
candidates: list[Video],
) -> Video | None:
"""Find a matching video from candidates using the configured method.
This is the preferred method for AUTO matching as it implements the
full safe matching cascade including leaf-uniqueness disambiguation.
Args:
incoming: The video to find a match for.
candidates: List of existing videos to search for matches.
Returns:
The matched video, or None if no match found.
Notes:
For AUTO method, implements the safe matching cascade:
1. Shape rejection (filter candidates)
2. original_video conflict rejection (filter candidates)
3. Definitive file identity (is_same_file)
4. Strict path match
5. Leaf uniqueness matching at increasing depths
Shape is for REJECTION only - compatible shapes don't imply a match.
"""
from pathlib import Path
from sleap_io.io.utils import sanitize_filename
if self.method == VideoMatchMethod.AUTO:
# Build list of viable candidates (not rejected by shape/provenance)
viable = []
for candidate in candidates:
# REJECTION CHECK 1: Shape compatibility
shape_compat = shapes_compatible(candidate, incoming)
if shape_compat is False:
# Definitely incompatible shapes - skip
continue
# REJECTION CHECK 2: original_video conflict
if original_videos_conflict(candidate, incoming):
# Both have provenance pointing to different files - skip
continue
viable.append(candidate)
# DEFINITIVE CHECK: File identity (handles source_video chains)
for candidate in viable:
if is_same_file(candidate, incoming):
return candidate
# STRING CHECK: Full path match
for candidate in viable:
if candidate.matches_path(incoming, strict=True):
return candidate
# STRING CHECK: Leaf path uniqueness
# Match paths by comparing suffixes at increasing depths
if viable:
def get_path_parts(video: Video) -> tuple[str, ...]:
"""Get path parts for comparison."""
fn = video.filename
if isinstance(fn, list):
fn = fn[0] # Use first for ImageVideo
return Path(sanitize_filename(fn)).parts
incoming_parts = get_path_parts(incoming)
candidate_parts = [(v, get_path_parts(v)) for v in viable]
# Also need all existing videos for uniqueness check
all_existing_parts = [(v, get_path_parts(v)) for v in candidates]
# Compare at increasing depths until we find a unique match
max_depth = max(
len(incoming_parts),
max((len(p) for _, p in all_existing_parts), default=0),
)
for depth in range(1, max_depth + 1):
if len(incoming_parts) < depth:
continue
incoming_leaf = "/".join(incoming_parts[-depth:])
# Find all viable candidates that match at this depth
matches_at_depth = []
for candidate, parts in candidate_parts:
if len(parts) < depth:
continue
candidate_leaf = "/".join(parts[-depth:])
if candidate_leaf == incoming_leaf:
matches_at_depth.append(candidate)
# If exactly one match at this depth, use it
if len(matches_at_depth) == 1:
return matches_at_depth[0]
# If no matches, try deeper
# If multiple matches, continue deeper to disambiguate
# No match found
return None
else:
# Non-AUTO methods: use pairwise match()
for candidate in candidates:
if self.match(candidate, incoming):
return candidate
return None
__annotations__ = {'method': 'VideoMatchMethod | str', 'strict': 'bool'}
class-attribute
¶
dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)
__attrs_own_setattr__ = True
class-attribute
¶
Returns True when the argument is true, False otherwise. The builtins True and False are the only two instances of the class bool. The class bool is a subclass of the class int, and cannot be subclassed.
__attrs_props__ = ClassProps(is_exception=False, is_slotted=True, has_weakref_slot=True, is_frozen=False, kw_only=<KeywordOnly.NO: 'no'>, collected_fields_by_mro=True, added_init=True, added_repr=True, added_eq=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__ = "Matcher for comparing and matching videos.\n\nAttributes:\n method: The matching method to use. Can be a VideoMatchMethod enum value\n or a string that will be converted to the enum. Default is AUTO.\n strict: Whether to use strict path matching for the PATH method.\n When True, paths must be exactly identical. When False, paths\n are normalized before comparison. Only used when method is PATH.\n Default is False.\n\nNotes:\n For AUTO method, use find_match() when matching against a list of\n candidates. The match() method for AUTO uses a simplified pairwise\n check that doesn't include the full leaf-uniqueness algorithm.\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__ = 506
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__ = ('method', 'strict')
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.matching'
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__ = ('method', 'strict', '__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__(method=<VideoMatchMethod.AUTO: 'auto'>, strict=False)
¶
__replace__(**changes)
¶
Method generated by attrs for class VideoMatcher.
__repr__()
¶
Method generated by attrs for class VideoMatcher.
Source code in sleap_io/model/matching.py
"""Unified matcher system for comparing and matching data structures during merging.
This module provides configurable matchers for comparing skeletons, instances, tracks,
and videos during merge operations. The matchers use various strategies to determine
when data structures should be considered equivalent during merging.
Key features:
- Skeleton matching: exact, structure-based, overlap, and subset matching
- Instance matching: spatial proximity, track identity, and bounding box IoU
- Track matching: by name or object identity
- Video matching: path, basename, content, and auto matching
Video matching supports path-based, filename-based, content-based, and
automatic strategies.
"""
__setattr__(name, val)
¶
Method generated by attrs for class VideoMatcher.
find_match(incoming, candidates)
¶
Find a matching video from candidates using the configured method.
This is the preferred method for AUTO matching as it implements the full safe matching cascade including leaf-uniqueness disambiguation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
incoming
|
Video
|
The video to find a match for. |
required |
candidates
|
list[Video]
|
List of existing videos to search for matches. |
required |
Returns:
| Type | Description |
|---|---|
Video | None
|
The matched video, or None if no match found. |
Notes
For AUTO method, implements the safe matching cascade: 1. Shape rejection (filter candidates) 2. original_video conflict rejection (filter candidates) 3. Definitive file identity (is_same_file) 4. Strict path match 5. Leaf uniqueness matching at increasing depths
Shape is for REJECTION only - compatible shapes don't imply a match.
Source code in sleap_io/model/matching.py
def find_match(
self,
incoming: Video,
candidates: list[Video],
) -> Video | None:
"""Find a matching video from candidates using the configured method.
This is the preferred method for AUTO matching as it implements the
full safe matching cascade including leaf-uniqueness disambiguation.
Args:
incoming: The video to find a match for.
candidates: List of existing videos to search for matches.
Returns:
The matched video, or None if no match found.
Notes:
For AUTO method, implements the safe matching cascade:
1. Shape rejection (filter candidates)
2. original_video conflict rejection (filter candidates)
3. Definitive file identity (is_same_file)
4. Strict path match
5. Leaf uniqueness matching at increasing depths
Shape is for REJECTION only - compatible shapes don't imply a match.
"""
from pathlib import Path
from sleap_io.io.utils import sanitize_filename
if self.method == VideoMatchMethod.AUTO:
# Build list of viable candidates (not rejected by shape/provenance)
viable = []
for candidate in candidates:
# REJECTION CHECK 1: Shape compatibility
shape_compat = shapes_compatible(candidate, incoming)
if shape_compat is False:
# Definitely incompatible shapes - skip
continue
# REJECTION CHECK 2: original_video conflict
if original_videos_conflict(candidate, incoming):
# Both have provenance pointing to different files - skip
continue
viable.append(candidate)
# DEFINITIVE CHECK: File identity (handles source_video chains)
for candidate in viable:
if is_same_file(candidate, incoming):
return candidate
# STRING CHECK: Full path match
for candidate in viable:
if candidate.matches_path(incoming, strict=True):
return candidate
# STRING CHECK: Leaf path uniqueness
# Match paths by comparing suffixes at increasing depths
if viable:
def get_path_parts(video: Video) -> tuple[str, ...]:
"""Get path parts for comparison."""
fn = video.filename
if isinstance(fn, list):
fn = fn[0] # Use first for ImageVideo
return Path(sanitize_filename(fn)).parts
incoming_parts = get_path_parts(incoming)
candidate_parts = [(v, get_path_parts(v)) for v in viable]
# Also need all existing videos for uniqueness check
all_existing_parts = [(v, get_path_parts(v)) for v in candidates]
# Compare at increasing depths until we find a unique match
max_depth = max(
len(incoming_parts),
max((len(p) for _, p in all_existing_parts), default=0),
)
for depth in range(1, max_depth + 1):
if len(incoming_parts) < depth:
continue
incoming_leaf = "/".join(incoming_parts[-depth:])
# Find all viable candidates that match at this depth
matches_at_depth = []
for candidate, parts in candidate_parts:
if len(parts) < depth:
continue
candidate_leaf = "/".join(parts[-depth:])
if candidate_leaf == incoming_leaf:
matches_at_depth.append(candidate)
# If exactly one match at this depth, use it
if len(matches_at_depth) == 1:
return matches_at_depth[0]
# If no matches, try deeper
# If multiple matches, continue deeper to disambiguate
# No match found
return None
else:
# Non-AUTO methods: use pairwise match()
for candidate in candidates:
if self.match(candidate, incoming):
return candidate
return None
match(video1, video2)
¶
Check if two videos match according to the configured method.
For AUTO method, this performs pairwise checks (file identity, path match). For full AUTO matching with leaf-uniqueness, use find_match() instead.
Source code in sleap_io/model/matching.py
def match(self, video1: Video, video2: Video) -> bool:
"""Check if two videos match according to the configured method.
For AUTO method, this performs pairwise checks (file identity, path match).
For full AUTO matching with leaf-uniqueness, use find_match() instead.
"""
if self.method == VideoMatchMethod.AUTO:
# Pairwise AUTO: rejection checks + definitive identity + path match
# (Leaf-uniqueness requires full candidate list - use find_match())
# Rejection: incompatible shapes
if shapes_compatible(video1, video2) is False:
return False
# Rejection: conflicting provenance
if original_videos_conflict(video1, video2):
return False
# Definitive: same file identity
if is_same_file(video1, video2):
return True
# String: strict path match
if video1.matches_path(video2, strict=True):
return True
# String: basename match (for pairwise, this is the fallback)
if video1.matches_path(video2, strict=False):
return True
return False
elif self.method == VideoMatchMethod.PATH:
return video1.matches_path(video2, strict=self.strict)
elif self.method == VideoMatchMethod.BASENAME:
return video1.matches_path(video2, strict=False)
elif self.method == VideoMatchMethod.CONTENT:
return video1.matches_content(video2)
elif self.method == VideoMatchMethod.IMAGE_DEDUP:
# Match ImageVideo instances with overlapping images (ImageVideo only)
return video1.has_overlapping_images(video2)
elif self.method == VideoMatchMethod.SHAPE:
# Match videos by shape only (height, width, channels)
return video1.matches_shape(video2)
else:
raise ValueError(f"Unknown video match method: {self.method}")
is_same_file(video1, video2)
¶
Check if two videos refer to the same underlying file.
This provides definitive file identity checking by: - Traversing source_video/original_video chains to find root videos - Using os.path.samefile() when files exist (handles symlinks) - Falling back to path resolution and string comparison
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video1
|
Video
|
First video to compare. |
required |
video2
|
Video
|
Second video to compare. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if both videos refer to the same underlying file. |
Notes
This is stricter than matches_path(strict=False) - it only returns True when files are verifiably the same, not just same basename.
Source code in sleap_io/model/matching.py
def is_same_file(video1: Video, video2: Video) -> bool:
"""Check if two videos refer to the same underlying file.
This provides definitive file identity checking by:
- Traversing source_video/original_video chains to find root videos
- Using os.path.samefile() when files exist (handles symlinks)
- Falling back to path resolution and string comparison
Args:
video1: First video to compare.
video2: Second video to compare.
Returns:
True if both videos refer to the same underlying file.
Notes:
This is stricter than matches_path(strict=False) - it only returns True
when files are verifiably the same, not just same basename.
"""
root1 = _get_root_video(video1)
root2 = _get_root_video(video2)
return _is_same_file_direct(root1, root2)
original_videos_conflict(video1, video2)
¶
Check if two videos have conflicting original_video references.
This is used for REJECTION in video matching. If both videos have provenance info but point to different root files, they definitely don't match (even if shapes are identical).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video1
|
Video
|
First video to compare. |
required |
video2
|
Video
|
Second video to compare. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True if both have provenance AND their roots point to different files. False otherwise (including if either or both have no provenance). |
Example
Two videos with identical basenames and shapes but from different directories would conflict if both have original_video set to their respective source paths.
Source code in sleap_io/model/matching.py
def original_videos_conflict(video1: Video, video2: Video) -> bool:
"""Check if two videos have conflicting original_video references.
This is used for REJECTION in video matching. If both videos have
provenance info but point to different root files, they definitely
don't match (even if shapes are identical).
Args:
video1: First video to compare.
video2: Second video to compare.
Returns:
True if both have provenance AND their roots point to different files.
False otherwise (including if either or both have no provenance).
Example:
Two videos with identical basenames and shapes but from different
directories would conflict if both have original_video set to their
respective source paths.
"""
root1 = _get_root_video(video1)
root2 = _get_root_video(video2)
# Only conflict if BOTH have non-trivial chains (provenance info set)
# AND the roots are different
has_provenance1 = (
video1.original_video is not None or video1.source_video is not None
)
has_provenance2 = (
video2.original_video is not None or video2.source_video is not None
)
if not (has_provenance1 and has_provenance2):
# At least one has no provenance - no conflict
return False
# Both have provenance - check if roots are the same file
return not _is_same_file_direct(root1, root2)
shapes_compatible(video1, video2)
¶
Check if two videos have compatible shapes.
This is used for REJECTION only in video matching - incompatible shapes mean the videos definitely don't match. Compatible shapes (or unknown) do NOT imply the videos are the same.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video1
|
Video
|
First video to compare. |
required |
video2
|
Video
|
Second video to compare. |
required |
Returns:
| Type | Description |
|---|---|
bool | None
|
False if shapes are definitely incompatible (different frames, H, or W). True if shapes are compatible. None if shape cannot be determined (missing metadata). |
Notes
Per algorithm design: Compare (frames, height, width) but NOT channels. Channels are excluded because grayscale detection is noisy (affected by compression) and user-configurable.
Source code in sleap_io/model/matching.py
def shapes_compatible(video1: Video, video2: Video) -> bool | None:
"""Check if two videos have compatible shapes.
This is used for REJECTION only in video matching - incompatible shapes
mean the videos definitely don't match. Compatible shapes (or unknown)
do NOT imply the videos are the same.
Args:
video1: First video to compare.
video2: Second video to compare.
Returns:
False if shapes are definitely incompatible (different frames, H, or W).
True if shapes are compatible.
None if shape cannot be determined (missing metadata).
Notes:
Per algorithm design: Compare (frames, height, width) but NOT channels.
Channels are excluded because grayscale detection is noisy (affected
by compression) and user-configurable.
"""
shape1 = _get_effective_shape(video1)
shape2 = _get_effective_shape(video2)
# If either shape is unknown, we can't determine compatibility
if shape1 is None or shape2 is None:
return None
# Compare frames, height, width (indices 0, 1, 2) - NOT channels (index 3)
return (
shape1[0] == shape2[0] # frames
and shape1[1] == shape2[1] # height
and shape1[2] == shape2[2] # width
)