Merging annotations¶
Merging combines annotations from multiple sources into a single dataset.
Quick start¶
import sleap_io as sio
base = sio.load_file("manual_annotations.slp")
predictions = sio.load_file("predictions.slp")
base.merge(predictions)
base.save("merged.slp")
How merging works¶
Merging proceeds in five steps:
- Match skeletons — Find corresponding skeletons by node structure
- Match videos — Identify same videos across datasets
- Match tracks — Map track identities between datasets
- Merge frames — Combine frames based on the
framestrategy - Match instances — Pair instances within overlapping frames
Preset options¶
These options are controlled via parameters to Labels.merge():
| Parameter | Controls | Options |
|---|---|---|
skeleton |
How skeletons are matched | "structure" (default), "subset", "overlap", "exact" |
video |
How videos are matched | "auto" (default), "path", "basename", "content", "shape", "image_dedup" |
track |
How tracks are matched | "name" (default), "identity" |
frame |
How overlapping frames are combined | "auto" (default), "replace_predictions", "keep_original", "keep_new", "keep_both", "update_tracks" |
instance |
How instances are paired within frames | "spatial" (default), "identity", "iou" |
base.merge(predictions) # All defaults
base.merge(predictions, video="auto", frame="auto") # Explicit defaults
Step 1: Skeleton matching¶
Before merging can proceed, skeletons from both datasets must be matched. Each skeleton in the incoming dataset is compared against skeletons in the base dataset to find correspondence.
Matching methods¶
| Method | Behavior | Use case |
|---|---|---|
"structure" |
Match if same node names, regardless of order | Default. Most common case |
"subset" |
Match if incoming skeleton nodes are a subset of base | Merging partial annotations |
"overlap" |
Match if sufficient overlap between node sets | Flexible matching with threshold |
"exact" |
Match only if nodes and edges are identical | Strict validation |
If no match is found, the skeleton is added as new to the base dataset.
String configuration¶
# Default: match by structure (same nodes, any order)
base.merge(other, skeleton="structure")
# Allow partial matches (incoming can have fewer nodes)
base.merge(other, skeleton="subset")
# Exact match required (nodes and edges must be identical)
base.merge(other, skeleton="exact")
Object configuration¶
For advanced control, use SkeletonMatcher:
from sleap_io.model.matching import SkeletonMatcher
# Overlap matching with custom threshold (70% of nodes must match)
matcher = SkeletonMatcher(method="overlap", threshold=0.7)
base.merge(other, skeleton=matcher)
Step 2: Video matching¶
Videos must match for frames to merge. If video matching fails, the video is added as new—no frames merge because there's no overlap.
Design philosophy¶
The default AUTO algorithm prioritizes avoiding false positives (matching wrong videos) over avoiding false negatives (failing to match correct videos):
| Error type | Consequence | Severity |
|---|---|---|
| False positive | Annotations merged to wrong video | Data corruption (often unrecoverable) |
| False negative | Video added as new | Safe (easily fixed, see below) |
When uncertain, AUTO adds the video as new rather than risk a wrong match.
How AUTO matching works¶
For each incoming video, AUTO runs these checks in order:
| Step | Check | Result |
|---|---|---|
| 1 | Shape incompatible (frames, H, W differ) | Reject |
| 2 | Provenance conflict (different original_video) |
Reject |
| 3 | Same physical file (os.path.samefile) |
Match |
| 4 | Exact path string match | Match |
| 5 | Unique basename/parent suffix match | Match |
| 6 | No match found | Add as new |
Shape is for rejection only. Same resolution does NOT imply a match—it just means the videos aren't rejected. This prevents matching unrelated videos that happen to have the same dimensions.
Examples¶
Cross-platform paths with unique basenames:
Base: C:/Users/alice/data/fly.mp4
Other: /home/bob/data/fly.mp4
Result: MATCH — "fly.mp4" is unique in both sets
Ambiguous basenames, parent disambiguates:
Base: /data/exp1/fly.mp4, /data/exp2/fly.mp4
Other: /remote/exp2/fly.mp4
Result: MATCH to exp2/fly.mp4 — parent directory disambiguates
Same basename, different content:
Base: fly.mp4 (1000 frames)
Other: fly.mp4 (500 frames)
Result: NOT MATCH — shape rejection (different frame counts)
PKG.SLP predictions to external video:
Base: project.slp with /data/fly.mp4
Other: predictions.pkg.slp (embedded, original_video=/data/fly.mp4)
Result: MATCH — provenance chain links to same file
String configuration¶
# Default: safe AUTO cascade
base.merge(other, video="auto")
# Exact path match only
base.merge(other, video="path")
# Match by filename only (ignores directory)
base.merge(other, video="basename")
Object configuration¶
For advanced control, use VideoMatcher:
from sleap_io.model.matching import VideoMatcher
# Strict path matching (paths must be identical, no normalization)
matcher = VideoMatcher(method="path", strict=True)
base.merge(other, video=matcher)
Other video matching methods¶
| Method | Behavior | Use case |
|---|---|---|
"auto" |
Safe cascade (default) | Most situations |
"path" |
Exact path match only | Strict control |
"basename" |
Filename only, ignores directory | Cross-platform (use with caution) |
"content" |
Shape + backend type | Dangerous — matches any same-resolution video |
"shape" |
Match and merge by shape | Image list merging |
"image_dedup" |
Deduplicate image lists | Remove duplicate images |
Handling false negatives¶
If AUTO doesn't match videos that ARE the same, you have a false negative. This is safe—the video was added as new rather than corrupting data. Here's how to detect and fix it:
Step 1: Detect — Check video count after merge:
base = sio.load_file("base.slp")
other = sio.load_file("other.slp")
print(f"Before: {len(base.videos)} videos")
result = base.merge(other)
print(f"After: {len(base.videos)} videos")
# If count increased unexpectedly, videos weren't matched
for v in base.videos:
print(f" {v.filename}")
Step 2: Verify — Confirm the videos ARE the same:
# Check shapes match
video_a = base.videos[0]
video_b = base.videos[1] # The one that should have matched
print(f"Video A: {video_a.shape}") # e.g., (1000, 480, 640, 3)
print(f"Video B: {video_b.shape}")
# If files exist, check content
if video_a.exists() and video_b.exists():
# Compare first frame visually or by hash
frame_a = video_a[0]
frame_b = video_b[0]
Step 3: Fix — Use replace_filenames before merging:
# Reload and fix paths before merge
base = sio.load_file("base.slp")
other = sio.load_file("other.slp")
# Option A: Map the specific file
other.replace_filenames(filename_map={
"/remote/path/fly.mp4": "/local/path/fly.mp4"
})
# Option B: Map by prefix (for multiple videos)
other.replace_filenames(prefix_map={
"/remote/data": "/local/data"
})
# Now merge
result = base.merge(other)
print(f"Videos after fix: {len(base.videos)}") # Should match original count
Alternative: Force match with explicit matcher:
# Only use this if you're CERTAIN the videos are the same
base.merge(other, video="basename") # Match by filename only
Step 3: Track matching¶
Tracks represent identities (e.g., individual animals) that persist across frames. During merge, tracks from the incoming dataset are matched to tracks in the base dataset.
Matching methods¶
| Method | Behavior | Use case |
|---|---|---|
"name" |
Match tracks with identical names | Default. Named individuals |
"identity" |
Match by track object identity | Same Track object in memory |
If no match is found, the track is added as new to the base dataset.
String configuration¶
# Default: match by track name
base.merge(other, track="name")
# Match by object identity (same Track instance)
base.merge(other, track="identity")
Object configuration¶
For advanced control, use TrackMatcher:
from sleap_io.model.matching import TrackMatcher
# Explicit name matching
matcher = TrackMatcher(method="name")
base.merge(other, track=matcher)
Step 4: Frame strategies¶
The frame parameter controls what happens when both datasets have the same frame (same video and frame index).
auto (default)¶
The recommended strategy for human-in-the-loop workflows. Preserves user labels, updates predictions.
| Base instance | Other instance | Result |
|---|---|---|
| User label | Prediction | Keep user label |
| User label | User label | Keep base (conflict) |
| Prediction | User label | Replace with user label |
| Prediction | Prediction | Replace with newer |
Unmatched instances from other are added.
# Typical HITL workflow: merge predictions into labeled project
base.merge(predictions) # Uses auto by default
replace_predictions¶
Replace all predictions in base with predictions from other. User labels are preserved.
# Re-ran inference, want to update predictions
base.merge(new_predictions, frame="replace_predictions")
| Instance type | From base | From other |
|---|---|---|
| User label | Keep | Ignore |
| Prediction | Remove | Add |
Other frame strategies¶
| Strategy | Behavior | Use case |
|---|---|---|
"keep_original" |
Ignore other entirely for overlapping frames | Preserve base annotations |
"keep_new" |
Replace base with other for overlapping frames | Overwrite with new annotations |
"keep_both" |
Concatenate all instances (creates duplicates) | Manual deduplication later |
"update_tracks" |
Copy track assignments only, don't modify poses | Update identity labels |
# Keep only the original annotations
base.merge(other, frame="keep_original")
# Replace with new annotations
base.merge(other, frame="keep_new")
# Keep everything (may create duplicates)
base.merge(other, frame="keep_both")
# Update track assignments without changing poses
base.merge(other, frame="update_tracks")
Step 5: Instance matching¶
For frame strategies that need to pair instances (auto, update_tracks), instance matching determines how instances in the base frame correspond to instances in the incoming frame.
Matching methods¶
| Method | Behavior | Use case |
|---|---|---|
"spatial" |
Match by centroid distance | Default. Position-based matching |
"identity" |
Match by track identity | Same track assignment |
"iou" |
Match by bounding box IoU | Overlap-based matching |
String configuration¶
# Default: spatial matching with 5px threshold
base.merge(other, instance="spatial")
# Match by track identity
base.merge(other, instance="identity")
# Match by bounding box overlap
base.merge(other, instance="iou")
Object configuration¶
For advanced control, use InstanceMatcher:
from sleap_io.model.matching import InstanceMatcher
# Tighter spatial matching (2px threshold)
matcher = InstanceMatcher(method="spatial", threshold=2.0)
base.merge(other, instance=matcher)
# IoU matching with 50% overlap threshold
matcher = InstanceMatcher(method="iou", threshold=0.5)
base.merge(other, instance=matcher)
Troubleshooting¶
Videos weren't matched (false negative)¶
See Handling false negatives above.
Videos matched incorrectly (false positive)¶
This shouldn't happen with AUTO matching. If it does:
- Check if videos have identical shapes AND ambiguous paths
- Use
video="path"for strict matching - Report as a bug—AUTO should be conservative
Duplicate instances after merge¶
Use auto instead of keep_both, or tighten the instance match threshold:
from sleap_io.model.matching import InstanceMatcher
base.merge(other, instance=InstanceMatcher(method="spatial", threshold=2.0))
Reference¶
Labels.merge¶
sleap_io.model.labels.Labels.merge(other, skeleton=None, video=None, track=None, frame='auto', instance=None, validate=True, progress_callback=None, error_mode='continue')
¶
Merge another Labels object into this one.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
other
|
Labels
|
Another Labels object to merge into this one. |
required |
skeleton
|
str | SkeletonMatcher | None
|
Skeleton matching method. Can be a string ("structure", "subset", "overlap", "exact") or a SkeletonMatcher object for advanced configuration. Default is "structure". |
None
|
video
|
str | VideoMatcher | None
|
Video matching method. Can be a string ("auto", "path", "basename", "content", "shape", "image_dedup") or a VideoMatcher object for advanced configuration. Default is "auto". |
None
|
track
|
str | TrackMatcher | None
|
Track matching method. Can be a string ("name", "identity") or a TrackMatcher object. Default is "name". |
None
|
frame
|
str
|
Frame merge strategy. One of "auto", "keep_original", "keep_new", "keep_both", "update_tracks", "replace_predictions". Default is "auto". |
'auto'
|
instance
|
str | InstanceMatcher | None
|
Instance matching method for spatial frame strategies. Can be a string ("spatial", "identity", "iou") or an InstanceMatcher object. Default is "spatial" with 5px tolerance. |
None
|
validate
|
bool
|
If True, validate for conflicts before merging. |
True
|
progress_callback
|
Callable | None
|
Optional callback for progress updates. Should accept (current, total, message) arguments. |
None
|
error_mode
|
str
|
How to handle errors: - "continue": Log errors but continue - "strict": Raise exception on first error - "warn": Print warnings but continue |
'continue'
|
Returns:
| Type | Description |
|---|---|
MergeResult
|
MergeResult object with statistics and any errors/conflicts. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If Labels is lazy-loaded. |
Notes
This method modifies the Labels object in place. The merge is designed to handle common workflows like merging predictions back into a project.
Provenance tracking: Each merge operation appends a record to
self.provenance["merge_history"] containing:
timestamp: ISO format timestamp of the mergesource_filename: Path from source's provenance (Noneif in-memory)target_filename: Path from target's provenance (Noneif in-memory)source_labels: Statistics about the source Labelsstrategy: The frame strategy usedsleap_io_version: Version of sleap-io that performed the mergeresult: Merge statistics (frames_merged, instances_added, conflicts)
Source code in sleap_io/model/labels.py
def merge(
self,
other: "Labels",
skeleton: "str | SkeletonMatcher | None" = None,
video: "str | VideoMatcher | None" = None,
track: "str | TrackMatcher | None" = None,
frame: str = "auto",
instance: "str | InstanceMatcher | None" = None,
validate: bool = True,
progress_callback: Callable | None = None,
error_mode: str = "continue",
) -> "MergeResult":
"""Merge another Labels object into this one.
Args:
other: Another Labels object to merge into this one.
skeleton: Skeleton matching method. Can be a string ("structure",
"subset", "overlap", "exact") or a SkeletonMatcher object for
advanced configuration. Default is "structure".
video: Video matching method. Can be a string ("auto", "path",
"basename", "content", "shape", "image_dedup") or a VideoMatcher
object for advanced configuration. Default is "auto".
track: Track matching method. Can be a string ("name", "identity") or
a TrackMatcher object. Default is "name".
frame: Frame merge strategy. One of "auto", "keep_original",
"keep_new", "keep_both", "update_tracks", "replace_predictions".
Default is "auto".
instance: Instance matching method for spatial frame strategies. Can be
a string ("spatial", "identity", "iou") or an InstanceMatcher object.
Default is "spatial" with 5px tolerance.
validate: If True, validate for conflicts before merging.
progress_callback: Optional callback for progress updates.
Should accept (current, total, message) arguments.
error_mode: How to handle errors:
- "continue": Log errors but continue
- "strict": Raise exception on first error
- "warn": Print warnings but continue
Returns:
MergeResult object with statistics and any errors/conflicts.
Raises:
RuntimeError: If Labels is lazy-loaded.
Notes:
This method modifies the Labels object in place. The merge is designed to
handle common workflows like merging predictions back into a project.
Provenance tracking: Each merge operation appends a record to
``self.provenance["merge_history"]`` containing:
- ``timestamp``: ISO format timestamp of the merge
- ``source_filename``: Path from source's provenance (``None`` if in-memory)
- ``target_filename``: Path from target's provenance (``None`` if in-memory)
- ``source_labels``: Statistics about the source Labels
- ``strategy``: The frame strategy used
- ``sleap_io_version``: Version of sleap-io that performed the merge
- ``result``: Merge statistics (frames_merged, instances_added, conflicts)
"""
self._check_not_lazy("merge")
from datetime import datetime
from pathlib import Path
import sleap_io
from sleap_io.model.matching import (
ConflictResolution,
ErrorMode,
InstanceMatcher,
InstanceMatchMethod,
MergeError,
MergeResult,
SkeletonMatcher,
SkeletonMatchMethod,
SkeletonMismatchError,
TrackMatcher,
TrackMatchMethod,
VideoMatcher,
VideoMatchMethod,
)
# Coerce string arguments to Matcher objects
if skeleton is None:
skeleton_matcher = SkeletonMatcher(method=SkeletonMatchMethod.STRUCTURE)
elif isinstance(skeleton, str):
skeleton_matcher = SkeletonMatcher(method=SkeletonMatchMethod(skeleton))
else:
skeleton_matcher = skeleton
if video is None:
video_matcher = VideoMatcher()
elif isinstance(video, str):
video_matcher = VideoMatcher(method=VideoMatchMethod(video))
else:
video_matcher = video
if track is None:
track_matcher = TrackMatcher()
elif isinstance(track, str):
track_matcher = TrackMatcher(method=TrackMatchMethod(track))
else:
track_matcher = track
if instance is None:
instance_matcher = InstanceMatcher()
elif isinstance(instance, str):
instance_matcher = InstanceMatcher(method=InstanceMatchMethod(instance))
else:
instance_matcher = instance
# Parse error mode
error_mode_enum = ErrorMode(error_mode)
# Initialize result
result = MergeResult(successful=True)
# Track merge history in provenance
if "merge_history" not in self.provenance:
self.provenance["merge_history"] = []
merge_record = {
"timestamp": datetime.now().isoformat(),
"source_filename": other.provenance.get("filename"),
"target_filename": self.provenance.get("filename"),
"source_labels": {
"n_frames": len(other.labeled_frames),
"n_videos": len(other.videos),
"n_skeletons": len(other.skeletons),
"n_tracks": len(other.tracks),
},
"strategy": frame,
"sleap_io_version": sleap_io.__version__,
}
try:
# Step 1: Match and merge skeletons
skeleton_map = {}
for other_skel in other.skeletons:
matched = False
for self_skel in self.skeletons:
if skeleton_matcher.match(self_skel, other_skel):
skeleton_map[other_skel] = self_skel
matched = True
break
if not matched:
if validate and error_mode_enum == ErrorMode.STRICT:
raise SkeletonMismatchError(
message=f"No matching skeleton found for {other_skel.name}",
details={"skeleton": other_skel},
)
elif error_mode_enum == ErrorMode.WARN:
print(f"Warning: No matching skeleton for {other_skel.name}")
# Add new skeleton if no match
self.skeletons.append(other_skel)
skeleton_map[other_skel] = other_skel
# Step 2: Match and merge videos
video_map = {}
frame_idx_map = {} # Maps (old_video, old_idx) -> (new_video, new_idx)
for other_video in other.videos:
matched = False
matched_video = None
# IMAGE_DEDUP and SHAPE need special post-match processing
if video_matcher.method in (
VideoMatchMethod.IMAGE_DEDUP,
VideoMatchMethod.SHAPE,
):
for self_video in self.videos:
if video_matcher.match(self_video, other_video):
matched_video = self_video
if video_matcher.method == VideoMatchMethod.IMAGE_DEDUP:
# Deduplicate images from other_video
deduped_video = other_video.deduplicate_with(self_video)
if deduped_video is None:
# All images were duplicates, map to existing video
video_map[other_video] = self_video
# Build frame index mapping for deduplicated frames
if isinstance(
other_video.filename, list
) and isinstance(self_video.filename, list):
other_basenames = [
Path(f).name for f in other_video.filename
]
self_basenames = [
Path(f).name for f in self_video.filename
]
for old_idx, basename in enumerate(
other_basenames
):
if basename in self_basenames:
new_idx = self_basenames.index(basename)
frame_idx_map[
(other_video, old_idx)
] = (
self_video,
new_idx,
)
else:
# Add deduplicated video as new
self.videos.append(deduped_video)
video_map[other_video] = deduped_video
# Build frame index mapping for remaining frames
if isinstance(
other_video.filename, list
) and isinstance(deduped_video.filename, list):
other_basenames = [
Path(f).name for f in other_video.filename
]
deduped_basenames = [
Path(f).name for f in deduped_video.filename
]
self_basenames = [
Path(f).name for f in self_video.filename
]
for old_idx, basename in enumerate(
other_basenames
):
if basename in deduped_basenames:
new_idx = deduped_basenames.index(
basename
)
frame_idx_map[
(other_video, old_idx)
] = (
deduped_video,
new_idx,
)
else:
# Cases where the image was a duplicate,
# present in both self and other labels
# See Issue #239.
assert basename in self_basenames, (
"Unexpected basename mismatch, \
possible file corruption."
)
new_idx = self_basenames.index(basename)
frame_idx_map[
(other_video, old_idx)
] = (
self_video,
new_idx,
)
elif video_matcher.method == VideoMatchMethod.SHAPE:
# Merge videos with same shape
merged_video = self_video.merge_with(other_video)
# Replace self_video with merged version
self_video_idx = self.videos.index(self_video)
self.videos[self_video_idx] = merged_video
video_map[other_video] = merged_video
video_map[self_video] = (
merged_video # Update mapping for self too
)
# Build frame index mapping
if isinstance(
other_video.filename, list
) and isinstance(merged_video.filename, list):
other_basenames = [
Path(f).name for f in other_video.filename
]
merged_basenames = [
Path(f).name for f in merged_video.filename
]
for old_idx, basename in enumerate(other_basenames):
if basename in merged_basenames:
new_idx = merged_basenames.index(basename)
frame_idx_map[(other_video, old_idx)] = (
merged_video,
new_idx,
)
matched = True
break
else:
# All other methods: use find_match() for the full matching cascade
matched_video = video_matcher.find_match(other_video, self.videos)
if matched_video is not None:
video_map[other_video] = matched_video
matched = True
if not matched:
# Add new video if no match
self.videos.append(other_video)
video_map[other_video] = other_video
# Step 3: Match and merge tracks
track_map = {}
for other_track in other.tracks:
matched = False
for self_track in self.tracks:
if track_matcher.match(self_track, other_track):
track_map[other_track] = self_track
matched = True
break
if not matched:
# Add new track if no match
self.tracks.append(other_track)
track_map[other_track] = other_track
# Step 4: Merge frames
total_frames = len(other.labeled_frames)
for frame_idx, other_frame in enumerate(other.labeled_frames):
if progress_callback:
progress_callback(
frame_idx,
total_frames,
f"Merging frame {frame_idx + 1}/{total_frames}",
)
# Check if frame index needs remapping (for deduplicated/merged videos)
if (other_frame.video, other_frame.frame_idx) in frame_idx_map:
mapped_video, mapped_frame_idx = frame_idx_map[
(other_frame.video, other_frame.frame_idx)
]
else:
# Map video to self
mapped_video = video_map.get(other_frame.video, other_frame.video)
mapped_frame_idx = other_frame.frame_idx
# Find matching frame in self
matching_frames = self.find(mapped_video, mapped_frame_idx)
if len(matching_frames) == 0:
# No matching frame, create new one
new_frame = LabeledFrame(
video=mapped_video,
frame_idx=mapped_frame_idx,
instances=[],
)
# Map instances to new skeleton/track
for inst in other_frame.instances:
new_inst = self._map_instance(inst, skeleton_map, track_map)
new_frame.instances.append(new_inst)
result.instances_added += 1
self.append(new_frame)
result.frames_merged += 1
else:
# Merge into existing frame
self_frame = matching_frames[0]
# Merge instances using frame-level merge
merged_instances, conflicts = self_frame.merge(
other_frame,
instance=instance_matcher,
frame=frame,
)
# Remap skeleton and track references for instances from other frame
remapped_instances = []
for inst in merged_instances:
# Check if instance needs remapping (from other_frame)
if inst.skeleton in skeleton_map:
# Instance needs remapping
remapped_inst = self._map_instance(
inst, skeleton_map, track_map
)
remapped_instances.append(remapped_inst)
else:
# Instance already has correct skeleton (from self_frame)
remapped_instances.append(inst)
merged_instances = remapped_instances
# Count changes
n_before = len(self_frame.instances)
n_after = len(merged_instances)
result.instances_added += max(0, n_after - n_before)
# Record conflicts
for orig, new, resolution in conflicts:
result.conflicts.append(
ConflictResolution(
frame=self_frame,
conflict_type="instance_conflict",
original_data=orig,
new_data=new,
resolution=resolution,
)
)
# Update frame instances
self_frame.instances = merged_instances
result.frames_merged += 1
# Step 5: Merge suggestions
for other_suggestion in other.suggestions:
mapped_video = video_map.get(
other_suggestion.video, other_suggestion.video
)
# Check if suggestion already exists
exists = False
for self_suggestion in self.suggestions:
if (
self_suggestion.video == mapped_video
and self_suggestion.frame_idx == other_suggestion.frame_idx
):
exists = True
break
if not exists:
# Create new suggestion with mapped video
new_suggestion = SuggestionFrame(
video=mapped_video, frame_idx=other_suggestion.frame_idx
)
self.suggestions.append(new_suggestion)
# Update merge record
merge_record["result"] = {
"frames_merged": result.frames_merged,
"instances_added": result.instances_added,
"conflicts": len(result.conflicts),
}
self.provenance["merge_history"].append(merge_record)
except MergeError as e:
result.successful = False
result.errors.append(e)
if error_mode_enum == ErrorMode.STRICT:
raise
except Exception as e:
result.successful = False
result.errors.append(
MergeError(message=str(e), details={"exception": type(e).__name__})
)
if error_mode_enum == ErrorMode.STRICT:
raise
if progress_callback:
progress_callback(total_frames, total_frames, "Merge complete")
return result
Labels.add_video¶
sleap_io.model.labels.Labels.add_video(video)
¶
Add a video to the labels, preventing duplicates.
This method provides safe video addition by checking if a video with the same file identity already exists. Unlike direct list append, this prevents duplicate videos even when different Video objects point to the same underlying file.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video
|
Video
|
The video to add. |
required |
Returns:
| Type | Description |
|---|---|
Video
|
The video that should be used. If a duplicate was detected, returns the existing video; otherwise returns the input video. |
Notes
This method uses is_same_file() for duplicate detection, which: - Considers source_video for embedded videos (PKG.SLP) - Uses strict path comparison (same basename in different dirs != same) - Handles ImageVideo lists correctly
Use this instead of labels.videos.append(video) to prevent duplicates.
Source code in sleap_io/model/labels.py
def add_video(self, video: Video) -> Video:
"""Add a video to the labels, preventing duplicates.
This method provides safe video addition by checking if a video with
the same file identity already exists. Unlike direct list append, this
prevents duplicate videos even when different Video objects point to
the same underlying file.
Args:
video: The video to add.
Returns:
The video that should be used. If a duplicate was detected, returns
the existing video; otherwise returns the input video.
Notes:
This method uses is_same_file() for duplicate detection, which:
- Considers source_video for embedded videos (PKG.SLP)
- Uses strict path comparison (same basename in different dirs != same)
- Handles ImageVideo lists correctly
Use this instead of `labels.videos.append(video)` to prevent duplicates.
"""
from sleap_io.model.matching import is_same_file
for existing in self.videos:
if is_same_file(existing, video):
return existing
self.videos.append(video)
return video
Labels.replace_filenames¶
sleap_io.model.labels.Labels.replace_filenames(new_filenames=None, filename_map=None, prefix_map=None, open_videos=True)
¶
Replace video filenames.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_filenames
|
list[str | Path] | None
|
List of new filenames. Must have the same length as the number of videos in the labels. |
None
|
filename_map
|
dict[str | Path, str | Path] | None
|
Dictionary mapping old filenames (keys) to new filenames (values). |
None
|
prefix_map
|
dict[str | Path, str | Path] | None
|
Dictionary mapping old prefixes (keys) to new prefixes (values). |
None
|
open_videos
|
bool
|
If |
True
|
Notes
Only one of the argument types can be provided.
Source code in sleap_io/model/labels.py
def replace_filenames(
self,
new_filenames: list[str | Path] | None = None,
filename_map: dict[str | Path, str | Path] | None = None,
prefix_map: dict[str | Path, str | Path] | None = None,
open_videos: bool = True,
):
"""Replace video filenames.
Args:
new_filenames: List of new filenames. Must have the same length as the
number of videos in the labels.
filename_map: Dictionary mapping old filenames (keys) to new filenames
(values).
prefix_map: Dictionary mapping old prefixes (keys) to new prefixes (values).
open_videos: If `True` (the default), attempt to open the video backend for
I/O after replacing the filename. If `False`, the backend will not be
opened (useful for operations with costly file existence checks).
Notes:
Only one of the argument types can be provided.
"""
n = 0
if new_filenames is not None:
n += 1
if filename_map is not None:
n += 1
if prefix_map is not None:
n += 1
if n != 1:
raise ValueError(
"Exactly one input method must be provided to replace filenames."
)
if new_filenames is not None:
if len(self.videos) != len(new_filenames):
raise ValueError(
f"Number of new filenames ({len(new_filenames)}) does not match "
f"the number of videos ({len(self.videos)})."
)
for video, new_filename in zip(self.videos, new_filenames):
video.replace_filename(new_filename, open=open_videos)
elif filename_map is not None:
for video in self.videos:
for old_fn, new_fn in filename_map.items():
if type(video.filename) is list:
new_fns = []
for fn in video.filename:
if Path(fn) == Path(old_fn):
new_fns.append(new_fn)
else:
new_fns.append(fn)
video.replace_filename(new_fns, open=open_videos)
else:
if Path(video.filename) == Path(old_fn):
video.replace_filename(new_fn, open=open_videos)
elif prefix_map is not None:
for video in self.videos:
for old_prefix, new_prefix in prefix_map.items():
# Sanitize old_prefix for cross-platform matching
old_prefix_sanitized = sanitize_filename(old_prefix)
# Check if old prefix ends with a separator
old_ends_with_sep = old_prefix_sanitized.endswith("/")
if type(video.filename) is list:
new_fns = []
for fn in video.filename:
# Sanitize filename for matching
fn_sanitized = sanitize_filename(fn)
if fn_sanitized.startswith(old_prefix_sanitized):
# Calculate the remainder after removing the prefix
remainder = fn_sanitized[len(old_prefix_sanitized) :]
# Build the new filename
if remainder.startswith("/"):
# Remainder has separator, remove it to avoid double
# slash
remainder = remainder[1:]
# Always add separator between prefix and remainder
if new_prefix and not new_prefix.endswith(
("/", "\\")
):
new_fn = new_prefix + "/" + remainder
else:
new_fn = new_prefix + remainder
elif old_ends_with_sep:
# Old prefix had separator, preserve it in the new
# one
if new_prefix and not new_prefix.endswith(
("/", "\\")
):
new_fn = new_prefix + "/" + remainder
else:
new_fn = new_prefix + remainder
else:
# No separator in old prefix, don't add one
new_fn = new_prefix + remainder
new_fns.append(new_fn)
else:
new_fns.append(fn)
video.replace_filename(new_fns, open=open_videos)
else:
# Sanitize filename for matching
fn_sanitized = sanitize_filename(video.filename)
if fn_sanitized.startswith(old_prefix_sanitized):
# Calculate the remainder after removing the prefix
remainder = fn_sanitized[len(old_prefix_sanitized) :]
# Build the new filename
if remainder.startswith("/"):
# Remainder has separator, remove it to avoid double
# slash
remainder = remainder[1:]
# Always add separator between prefix and remainder
if new_prefix and not new_prefix.endswith(("/", "\\")):
new_fn = new_prefix + "/" + remainder
else:
new_fn = new_prefix + remainder
elif old_ends_with_sep:
# Old prefix had separator, preserve it in the new one
if new_prefix and not new_prefix.endswith(("/", "\\")):
new_fn = new_prefix + "/" + remainder
else:
new_fn = new_prefix + remainder
else:
# No separator in old prefix, don't add one
new_fn = new_prefix + remainder
video.replace_filename(new_fn, open=open_videos)
FrameStrategy¶
sleap_io.model.matching.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'.
VideoMatcher¶
sleap_io.model.matching.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}")
SkeletonMatcher¶
sleap_io.model.matching.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}")
TrackMatcher¶
sleap_io.model.matching.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)
¶
InstanceMatcher¶
sleap_io.model.matching.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}")
MergeResult¶
sleap_io.model.matching.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)