Skip to content

feat: frame-spanning Event annotations (data model + Labels + SLP format 2.6) (#539)#540

Open
talmo wants to merge 4 commits into
mainfrom
feature/event-model
Open

feat: frame-spanning Event annotations (data model + Labels + SLP format 2.6) (#539)#540
talmo wants to merge 4 commits into
mainfrom
feature/event-model

Conversation

@talmo

@talmo talmo commented Jul 8, 2026

Copy link
Copy Markdown
Member

Summary

Implements #539 in full — the frame-spanning Event annotation, end to end: data
model, Labels integration, SLP serialization, docs, and tests, in one PR. Event is
the 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. Events
model 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.py

  • EventType — lightweight, name-matched catalog entry (the "ethogram"), following
    the Track / Identity pattern (@define(eq=False), name / description /
    metadata, matches()).
  • Event — abstract base carrying the interval, subject / target participants
    (Track | Identity | None), and metadata. Guards abstract instantiation, fills
    end_framestart_frame when omitted, validates end_frame >= start_frame.
    Inclusive frame convention. Properties is_predicted / is_directed /
    is_instantaneous / n_frames / frames; methods contains() / overlaps().
  • UserEvent / PredictedEvent — human-vs-model split. PredictedEvent adds two
    independent optional fields: scores (framewise (n_frames,) float32 trace) and
    score (scalar); neither is derived from the other.

Labels integration

  • New top-level lists Labels.events and Labels.event_types (kw_only), siblings
    of videos / tracks / suggestionsnot on LabeledFrame (events may cover
    unlabeled frames).
  • Auto-collection: registers referenced EventTypes (deduped by name, canonicalizing
    each event's type), subject/target Track / Identity, and each event's video
    into the respective catalogs — at construction and at 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() (eager + lazy) and merge() carry events across, remapping every
    reference onto the copied/merged catalogs; merge() dedupes event_types by name and
    never mutates the source.

SLP serialization — format 2.6 (additive, presence-guarded)

  • /event_types — catalog group (name + optional description + EAV meta_*),
    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 (scores flat float32 + score_offsets) for optional
    framewise traces. The scalar score column, both trace datasets, and the metadata
    table are each omitted when unused; the whole group is omitted when there are no events,
    so event-free files stay byte-identical.
  • Wired into the eager and lazy read/write paths; registered in
    _KNOWN_TOPLEVEL_HDF5_NAMES; format_id bumped 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 + /events groups and format 2.6.

Example

import sleap_io as sio

labels = sio.load_slp("session.slp")
video, (m1, m2) = labels.videos[0], labels.tracks[:2]
attack = sio.EventType(name="attack", metadata={"color": "#e6194b"})

# Model proposal (framewise + scalar confidence, both optional/independent)
labels.events.append(sio.PredictedEvent(
    type=attack, video=video, start_frame=100, end_frame=140,   # inclusive
    subject=m1, target=m2, scores=[0.6]*41, score=0.74))
# Human ground truth (bare string auto-promotes; target=None => non-directed)
labels.events.append(sio.UserEvent(type="rear", video=video, start_frame=120, subject=m1))

labels.get_events(type="attack", predicted=True)   # proposals to review
labels.events_at(video, 130)                        # events covering frame 130
labels.save("scored.slp")                           # /event_types + /events (format 2.6)

Design decisions (per the issue's "leans")

Keep the EventType catalog (strings auto-promote) · union subject/target fields ·
inclusive [start_frame, end_frame] · defer from_predicted · field name type.

Testing

  • Model (test_event.py): 32 tests, 100% line + branch coverage of event.py.
  • Labels (test_labels.py): collection (name-dedup + rebind, participants, video),
    get_events / events_at, copy, merge, replace_videos remap.
  • SLP (test_slp.py): round-trip (eager + lazy), framewise/scalar scores, presence
    guards, format 2.6, backward-compat (pre-2.6 → empty), the -1 = none type sentinel,
    event-only-video round-trip, and event.video surviving embed=True.
  • Full tests/ suite green (4293 passed, 3 skipped); all doc code blocks execute; ruff
    clean.

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.video was dropped to None — for event-only videos and on the embed=True /
apply_crops() path — both now covered by regression tests.

Closes #539.

🤖 Generated with Claude Code

https://claude.ai/code/session_011DiaqkmRcp1L9yD8dx4jLj

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

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.72527% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 93.45%. Comparing base (27191e2) to head (8ff748b).

Files with missing lines Patch % Lines
sleap_io/model/labels.py 98.76% 0 Missing and 1 partial ⚠️
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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

github-actions Bot pushed a commit that referenced this pull request Jul 8, 2026
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Docs Preview

Preview URL https://io.sleap.ai/pr/540/
Commit 8ff748b55cd6452600ad791b2a10753c437a5706

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
github-actions Bot pushed a commit that referenced this pull request Jul 9, 2026
github-actions Bot and others added 2 commits July 8, 2026 19:36
…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
@talmo talmo changed the title feat(model): add frame-spanning Event annotation data model (#539, first pass) feat: frame-spanning Event annotations (data model + Labels + SLP format 2.6) (#539) Jul 9, 2026
github-actions Bot pushed a commit that referenced this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add event annotations (frame-spanning time ranges) to the data model + SLP

1 participant