-
Notifications
You must be signed in to change notification settings - Fork 0
Video Annotation
Annotix provides a full frame-based video annotation system built on native FFmpeg integration. Instead of annotating video directly, frames are extracted at a configurable FPS and annotated as images with tracking support.
The video annotation workflow is:
- Upload a video to a project.
- Extract frames at a chosen FPS.
- Create tracks for objects you want to follow across frames.
- Set keyframes with bounding boxes at specific frames.
- Interpolation fills in positions between keyframes automatically.
- Bake materializes the interpolated positions into real per-frame annotations.
Frame extraction uses FFmpeg (via the ffmpeg-the-third Rust crate) to decode the video and save individual frames as JPEG images.
| Setting | Description |
|---|---|
| FPS | User-configurable extraction rate (e.g. 1, 5, 10, 30 fps) |
| Format | JPEG at quality 90 |
| Batch Size | 50 frames per disk flush |
| Algorithm | PTS-based selection (selects frames at precise intervals, not every frame) |
Rather than extracting every frame and discarding extras, Annotix calculates the PTS (Presentation Timestamp) interval:
pts_interval = pts_per_second / fps_extraction
Only frames at or near each PTS interval boundary are saved. This is more efficient than extracting all frames.
If a frame extraction is interrupted (app crash, user closes app), Annotix automatically detects partially extracted videos on restart:
- Counts existing extracted frames in the video directory.
- Compares against expected frame count.
- Resumes extraction from where it left off.
- Emits progress events during resume.
| Status | Meaning |
|---|---|
pending |
Video uploaded, no frames extracted |
extracting |
Extraction in progress |
ready |
All frames extracted, ready for annotation |
A track represents a single object to follow across the video. Each track has:
| Field | Description |
|---|---|
id |
UUID |
class_id |
Class of the tracked object |
label |
Optional display name |
enabled |
Whether the track is active |
keyframes |
Array of keyframe positions |
| Operation | Description |
|---|---|
| Create Track | Shortcut T. Creates a new track with the active class. |
| Update Track | Change class, label, or enabled state. |
| Delete Track | Remove track and all its keyframes. |
A keyframe defines the position of a tracked object at a specific frame. Between keyframes, positions are interpolated automatically.
| Field | Description |
|---|---|
frame_index |
The frame number (0-based) |
x, y, width, height
|
Bounding box in pixel coordinates |
is_keyframe |
Always true for user-set keyframes |
enabled |
Whether this keyframe participates in interpolation |
| Operation | Description |
|---|---|
| Set Keyframe | Draw a bounding box on the current frame for a track. If a keyframe already exists at this frame, it's updated (upsert). |
| Delete Keyframe | Remove a keyframe at a specific frame index. |
| Toggle Enabled | Enable/disable a keyframe without deleting it. Disabled keyframes are skipped during interpolation. |
Keyframes are sorted by frame_index within each track.
Annotix uses linear interpolation to compute bounding box positions between keyframes. This runs on-the-fly as you scrub the timeline.
Given a frame index and a track's keyframes:
- Exact match — The frame is a keyframe. Return it directly.
- Before first keyframe — Hold/extend the first keyframe's position.
- After last keyframe — Hold/extend the last keyframe's position.
-
Between two keyframes — Linear interpolation:
t = (frameIndex - prevFrame) / (nextFrame - prevFrame) x = prev.x + (next.x - prev.x) * t y = prev.y + (next.y - prev.y) * t width = prev.width + (next.width - prev.width) * t height = prev.height + (next.height - prev.height) * t - Enabled logic — A frame is only enabled if both the previous and next keyframes are enabled.
The interpolation produces an InterpolatedBBox for each frame, containing:
| Field | Description |
|---|---|
trackId |
Which track this belongs to |
classId |
Track's class |
x, y, width, height |
Interpolated bounding box |
isKeyframe |
Whether this is a real keyframe or interpolated |
enabled |
Whether this position is active |
The bake operation converts the sparse keyframe representation into dense per-frame annotations stored in the project's image entries.
- For each extracted frame, iterate over all tracks.
- Compute the interpolated bounding box using the algorithm above.
- Create an
AnnotationEntrywithsource: "user"for each active interpolated position. - Replace the frame's existing annotations with the baked results.
- Write everything atomically to
project.json.
- Bake when you're satisfied with the track positions and want to export the annotations.
- Baked annotations appear in the gallery like normal image annotations.
- You can continue editing tracks after baking and re-bake to update.
The video view includes an interactive timeline for frame navigation:
- Scrubber — Drag to jump to any frame.
-
Frame-by-frame — Use
Left ArrowandRight Arrowkeys. - Keyframe indicators — Visual markers on the timeline showing where keyframes exist.
| Component | Purpose |
|---|---|
VideoTimeline |
Timeline scrubber with keyframe markers |
VideoView |
Frame display and navigation |
VideoAnnotationCanvas |
Interactive canvas for drawing bounding boxes on frames |
- Navigate to a frame using the timeline or arrow keys.
- The frame loads and all interpolated bounding boxes are rendered.
- Draw or adjust bounding boxes directly on the frame.
- Changes are saved as keyframes for the active track.
| Shortcut | Action |
|---|---|
T |
Create new track |
Left Arrow |
Previous frame |
Right Arrow |
Next frame |
Video files, tracks, keyframes, and frame images are not synced via P2P. Only the work assignment (which peer is responsible for which video) and pending approvals are synced. Each peer extracts frames locally after receiving the video assignment.
Annotix Wiki
Getting Started
Annotation
ML Pipeline
Collaboration & Automation
Reference