feat: frame-spanning Event annotations (data model + Labels + SLP format 2.6) (#539)#540
Open
talmo wants to merge 4 commits into
Open
feat: frame-spanning Event annotations (data model + Labels + SLP format 2.6) (#539)#540talmo wants to merge 4 commits into
talmo wants to merge 4 commits into
Conversation
First pass of #539: introduce the `Event` annotation as a standalone data model -- the first sleap-io annotation with a temporal extent. Every existing annotation (Instance, Centroid, BoundingBox, SegmentationMask, LabelImage, ROI) is strictly per-frame; an Event is a (video, start_frame, end_frame, type) interval for anything with a duration (behavior bouts, stimulus epochs, physiological events, review flags). New module `sleap_io/model/event.py`: - `EventType`: lightweight name-matched catalog entry (the "ethogram"), following the Track/Identity pattern. - `Event`: abstract base (@define(eq=False)) with the interval, subject/target participants (Track|Identity|None), and metadata. Guards direct instantiation, fills end_frame to start_frame when omitted, validates end_frame >= start_frame. Inclusive frame convention. - `UserEvent` / `PredictedEvent`: human-vs-model split. PredictedEvent adds independent optional `scores` (framewise float32 trace) and `score` (scalar). Also exports the four classes from the package root and adds a model-reference docs page. Labels attachment and SLP serialization are deferred to follow-up PRs per the issue's build order. Design decisions resolved per the issue's leans: keep EventType catalog (strings auto-promote), union subject/target fields, inclusive frames, defer from_predicted, field name `type`. Tests: 100% line + branch coverage of event.py (32 focused tests); all docs pycon blocks execute without raising. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #540 +/- ##
==========================================
+ Coverage 93.34% 93.45% +0.10%
==========================================
Files 55 56 +1
Lines 20868 21232 +364
Branches 4685 4757 +72
==========================================
+ Hits 19479 19842 +363
Misses 663 663
- Partials 726 727 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Docs Preview
Changed pages: API changes detected - View API Reference Configuration files changed - full rebuild performed. This preview will be removed when the PR is closed. |
…2.6) Complete the frame-spanning Event feature end to end (the follow-ups from #539 folded into one PR): Labels integration: - Add `Labels.events` / `Labels.event_types` top-level lists (kw_only), siblings of videos/tracks/suggestions -- not on LabeledFrame, since events may cover unlabeled frames. - Auto-collection: `_collect_events` registers referenced EventTypes (deduped by name, canonicalizing each event's `type`) and subject/target Track/Identity into the tracks/identities catalogs, at construction (`update`) and save time. - Accessors: `get_events(video, subject, type, frame_idx, predicted)` (frame_idx matches events whose span covers the frame) and `events_at(video, frame_idx, subject)`. - `copy()` deep-copies events with references remapped onto the copied catalogs (eager via deepcopy memo; lazy path via an explicit shared memo). - `merge()` concatenates events, dedupes event_types by name, and rebinds each event's video/subject/target/type onto the merged catalogs without mutating the source. SLP serialization (format_id -> 2.6, additive + presence-guarded): - `/event_types` catalog group (name + optional description + EAV metadata), mirroring `/identity`. - `/events` columnar group (video, start/end_frame int64 inclusive, type, subject/target kind+idx, is_predicted, score, name/source, per-event EAV metadata) + ragged CSR pair (`scores` flat float32 + `score_offsets`) for optional framewise PredictedEvent.scores. Scalar score column, trace datasets, and metadata table each omitted when unused. - Wired into eager + lazy read/write paths; registered in `_KNOWN_TOPLEVEL_HDF5_NAMES`; format-version-history docstring updated. Docs: un-defer docs/model/events.md (Labels + SLP sections), add a HITL behavior-segmentation example to docs/examples.md, and document the /event_types + /events groups (format 2.6) in docs/formats/slp.md. Tests: Labels event tests (collection, get_events/events_at, copy, merge) in test_labels.py; SLP round-trip / predicted-scores / presence-guard / format- version / lazy / lazy-copy / backward-compat / direct read-write tests in test_slp.py. event.py stays at 100% coverage; all new labels/slp code covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj
…ths) Adversarial review surfaced two silent-data-loss bugs where an event's `video` (a required field) was dropped to None on save/load: 1. `_collect_events` registered event subject/target participants and the event type but never the event's own `video`, so a video referenced *only* by an event (the core use case: annotating spans over frames with no pose labels) was absent from the written /videos catalog. write_events then encoded it as the -1 sentinel and read_events decoded -1 -> None. Fixed by collecting `ev.video` into `labels.videos` (mirroring suggestion-video collection), and by moving the save-time `_collect_events()` call ahead of `write_videos` in both the eager and lazy write paths (so a post-hoc `labels.events.append(...)` with a new video is persisted). 2. `Labels.replace_videos` remapped labeled frames, ROIs, suggestions, and the videos list but not events, so the standard portable save `save_slp(embed=True)` (and `apply_crops()`) stranded every event's video -> written as -1 -> None on reload. Fixed by remapping `ev.video` through the video_map, mirroring the suggestions loop. Regression tests: event-only video round-trip (eager + lazy), post-hoc append, replace_videos remap, and event.video surviving embed=True. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj
Locks in the merge manifestation of the event.video fix flagged by review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #539 in full — the frame-spanning
Eventannotation, end to end: datamodel,
Labelsintegration, SLP serialization, docs, and tests, in one PR.Eventisthe first sleap-io annotation with a temporal extent (a
(video, start_frame, end_frame, type)interval) — every existing annotation (Instance,Centroid,BoundingBox,SegmentationMask,LabelImage,ROI) is strictly per-frame. Eventsmodel anything with a duration: behavior bouts (HITL behavior-segmentation), stimulus
epochs, physiological events, feeding bouts, review flags.
Key changes
Data model —
sleap_io/model/event.pyEventType— lightweight, name-matched catalog entry (the "ethogram"), followingthe
Track/Identitypattern (@define(eq=False),name/description/metadata,matches()).Event— abstract base carrying the interval,subject/targetparticipants(
Track | Identity | None), and metadata. Guards abstract instantiation, fillsend_frame→start_framewhen omitted, validatesend_frame >= start_frame.Inclusive frame convention. Properties
is_predicted/is_directed/is_instantaneous/n_frames/frames; methodscontains()/overlaps().UserEvent/PredictedEvent— human-vs-model split.PredictedEventadds twoindependent optional fields:
scores(framewise(n_frames,)float32trace) andscore(scalar); neither is derived from the other.LabelsintegrationLabels.eventsandLabels.event_types(kw_only), siblingsof
videos/tracks/suggestions— not onLabeledFrame(events may coverunlabeled frames).
EventTypes (deduped by name, canonicalizingeach event's
type), subject/targetTrack/Identity, and each event'svideointo the respective catalogs — at construction and at save time.
get_events(video, subject, type, frame_idx, predicted)(frame_idxmatches events whose span covers the frame) and
events_at(video, frame_idx, subject).copy()(eager + lazy) andmerge()carry events across, remapping everyreference onto the copied/merged catalogs;
merge()dedupesevent_typesby name andnever mutates the source.
SLP serialization — format
2.6(additive, presence-guarded)/event_types— catalog group (name+ optionaldescription+ EAVmeta_*),mirroring
/identity./events— columnar struct-of-arrays (one dataset per field, like/bboxes):video,start_frame/end_frame(int64, inclusive),type,subject_kind/target_kind(int8: 0=none/self, 1=track, 2=identity),subject_idx/target_idx,is_predicted,score(float64, NaN=unset),name/source, per-event EAV metadata —plus a ragged CSR pair (
scoresflat float32 +score_offsets) for optionalframewise traces. The scalar
scorecolumn, both trace datasets, and the metadatatable are each omitted when unused; the whole group is omitted when there are no events,
so event-free files stay byte-identical.
_KNOWN_TOPLEVEL_HDF5_NAMES;format_idbumped to 2.6 only when events are present;format-version-history docstring updated.
Docs
docs/model/events.md— full model reference (frame convention, participants,overlap, Labels attachment, SLP persistence).
docs/examples.md— a HITL behavior-segmentation example.docs/formats/slp.md— the/event_types+/eventsgroups and format 2.6.Example
Design decisions (per the issue's "leans")
Keep the
EventTypecatalog (strings auto-promote) · unionsubject/targetfields ·inclusive
[start_frame, end_frame]· deferfrom_predicted· field nametype.Testing
test_event.py): 32 tests, 100% line + branch coverage ofevent.py.test_labels.py): collection (name-dedup + rebind, participants, video),get_events/events_at,copy,merge,replace_videosremap.test_slp.py): round-trip (eager + lazy), framewise/scalar scores, presenceguards, format 2.6, backward-compat (pre-2.6 → empty), the
-1 = nonetype sentinel,event-only-video round-trip, and
event.videosurvivingembed=True.tests/suite green (4293 passed, 3 skipped); all doc code blocks execute; ruffclean.
An adversarial multi-agent review (SLP / merge-copy / compat lenses, each finding
independently verified) caught and this PR fixes two silent-data-loss bugs where
event.videowas dropped toNone— for event-only videos and on theembed=True/apply_crops()path — both now covered by regression tests.Closes #539.
🤖 Generated with Claude Code
https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj