Source code for annie.media.compose

"""Compose a video's vdet and track annotations into per-frame overlays (service).

A :class:`~annie.models.VideoEntry` aggregates one video's raw detections and all
of its tracks. To draw a frame we merge the relevant boxes into a single
:class:`~annie.models.FrameAnnotation`, then hand it to
:func:`annie.color.draw_overlay`, which colours each box by the standing rules:

* vdet boxes (``track_id is None``) → flat **blue**;
* track boxes → a **stable unique colour** per track id (never blue/green);
* the **active / protagonist** track → **green**, overriding its palette colour.

Two inclusion modes are used: the Browse five-frame **strip** shows vdet + only
the active track; the full **render** shows vdet + every track.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from annie.core.models import FrameAnnotation
from annie.parsers.track import load_track
from annie.parsers.vdet import load_vdet

if TYPE_CHECKING:
    from annie.core.models import VideoEntry


[docs] def load_entry_annotations( entry: VideoEntry, ) -> tuple[dict[int, FrameAnnotation], dict[int, dict[int, FrameAnnotation]]]: """Load and index a video's vdet and track annotations by frame. Args: entry: The video whose annotation files to read. Returns: A ``(vdet_by_frame, tracks_by_id)`` pair where ``vdet_by_frame`` maps a frame index to its raw detections, and ``tracks_by_id`` maps each track id to its own ``frame_index -> FrameAnnotation`` mapping. """ vdet_by_frame: dict[int, FrameAnnotation] = {} if entry.vdet_path is not None: vdet_by_frame = {fa.frame_idx: fa for fa in load_vdet(entry.vdet_path)} tracks_by_id: dict[int, dict[int, FrameAnnotation]] = {} for path in entry.track_paths: track_id, frames = load_track(path) tracks_by_id[track_id] = {fa.frame_idx: fa for fa in frames} return vdet_by_frame, tracks_by_id
[docs] def merge_frame( frame_idx: int, vdet_by_frame: dict[int, FrameAnnotation], tracks_by_id: dict[int, dict[int, FrameAnnotation]], include_track_ids: list[int], ) -> FrameAnnotation: """Combine vdet and selected track boxes for one frame. Args: frame_idx: The frame to assemble. vdet_by_frame: Per-frame raw detections (from :func:`load_entry_annotations`). tracks_by_id: Per-track per-frame annotations (from :func:`load_entry_annotations`). include_track_ids: Which track ids to include on this frame. Returns: A single :class:`~annie.models.FrameAnnotation` holding the vdet boxes followed by the selected track boxes present on that frame. """ boxes = list(vdet_by_frame[frame_idx].boxes) if frame_idx in vdet_by_frame else [] for track_id in include_track_ids: frames = tracks_by_id.get(track_id) if frames is not None and frame_idx in frames: boxes.extend(frames[frame_idx].boxes) return FrameAnnotation(frame_idx=frame_idx, boxes=boxes)
[docs] def strip_track_ids(entry: VideoEntry) -> list[int]: """Track ids to draw on the Browse strip: just the active one, if any. Args: entry: The video being shown. Returns: ``[active_track_id]`` when a protagonist is assigned, else ``[]``. """ return [entry.active_track_id] if entry.has_active_track else []