Files
clip-annotator/README.md
asreva d0f7cc64fc Update README controls table for latest UI changes
Adds brush preview, Undo×10, Redo, and Hide/Show Mask.
Removes Reload Saved (button was dropped).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 14:24:05 +02:00

8.8 KiB
Raw Blame History

River Annotation Tool

A desktop application for manually annotating river video clips as part of the HydroScan project. Annotators draw pixel-level water masks over river footage and answer structured survey questions about flow conditions, lighting, and scene quality.

Requirements

  • Python 3.12
  • uv (recommended) or pip

Installation

# Clone the repository
git clone <repo-url>
cd river-annotation-tool

# Install with uv (creates the virtual environment automatically)
uv sync

# Or with pip
python -m venv .venv
.venv\Scripts\activate        # Windows
# source .venv/bin/activate   # macOS/Linux
pip install -e .

Setup

Before running, create your config and clip list from the provided examples:

cp config/config.example.yaml config/config.yaml
cp config/clips.example.txt config/clips.txt

Edit config/config.yaml to set your data_dir and out_dir, then edit config/clips.txt to list the clips you want to annotate.

Usage

python -m river_annotation_tool.annotation_script

Arguments

Argument Default Description
--config config/config.yaml Path to the config YAML file
--data (from config) Override data_dir from config
--out (from config) Override out_dir from config
--clips (from config) Override clips_file from config
--clip (first unannotated in list) Open a specific clip by stem name (e.g. left_20230501)
--extras off Also save GIFs and extra PNGs (see Output section)
--no-skip off Show already-annotated clips instead of skipping them

Typical workflows

# Annotate clips listed in config/clips.txt (default)
python -m river_annotation_tool.annotation_script

# Use a different config file
python -m river_annotation_tool.annotation_script --config config/my_config.yaml

# Override paths from the command line
python -m river_annotation_tool.annotation_script --data data/clips --out data/out

# Annotate a single specific clip
python -m river_annotation_tool.annotation_script --clip left_20230615T120000

Configuration

All settings live in config/config.yaml. Copy config/config.example.yaml to get started.

filenames:
  video_in_zip: left.mp4           # video filename inside each ZIP archive
  video_tmp_suffix: .mp4           # suffix for the extraction temp file
  zip_extension: .zip              # extension used when resolving clip names
  mask: mask.png                   # saved water mask
  metadata: metadata.json          # saved survey answers
  frame: frame.png                 # middle frame snapshot
  overlay: overlay.png             # frame with mask blended in green
  mask_vis: mask_vis.png           # greyscale mask PNG (--extras only)
  gif_original_hires: video_original_hires.gif
  gif_original_lowres: video_original_lowres.gif
  gif_overlay_hires: video_overlay_hires.gif
  gif_overlay_lowres: video_overlay_lowres.gif

display_max: 720          # longest side in pixels for display
fps_fallback: 25          # FPS to use if the video header is missing
max_frames: 100           # max frames to extract per clip

data_dir: data/clips      # directory containing ZIP archives
out_dir: data/annotation_results
clips_file: config/clips.txt

questions:
  - section: River
    items:
      - key: flow
        label: "Flow Regime"
        options: [Turbulent, Laminar, Uncertain]
        default: Laminar
      # add more items or sections as needed

Add, remove, or reorder questions directly in the YAML — the UI rebuilds automatically. key is what gets saved in metadata.json; default selects the pre-checked option (omit or set to null to leave unselected).

Clip list file

config/clips.txt lists the clip filenames to annotate, one per line. Lines starting with # are ignored. Clips are processed in order; already-annotated clips (those with an existing mask.png) are skipped automatically. Pass --no-skip to include them. When the last clip is reached, a dialog appears and the app exits.

# Example clips.txt
left_20230501T120000.zip
left_20230502T120000.zip

Copy config/clips.example.txt as a starting point.

Controls

The window shows the video on the left (auto-playing) and the survey panel on the right.

Action How
Draw water mask Click and drag on the video
Erase mask Toggle Eraser button, then drag
Brush preview A white circle follows the cursor showing the current brush size
Undo last stroke Undo
Undo 10 strokes Undo×10
Redo Redo — steps forward through undone strokes
Clear entire mask Clear
Adjust brush size Slider next to the erase controls
Toggle mask overlay Hide Mask / Show Mask — hides or reveals the green overlay without affecting the mask data
Save and continue Next — saves current clip and loads the next one. If the clip already has a saved annotation a dialog asks whether to replace it or keep the existing save.
Go back Previous — saves current clip and returns to the previously viewed clip. Disabled on the first clip.
Skip without saving Skip — discards any unsaved changes and loads the next clip without writing anything to disk.

Output

Each annotated clip produces a folder <out_dir>/<clip_stem>/ with:

mask.png          # Binary water mask at full source resolution (always)
metadata.json     # Survey answers as JSON (always)
frame.png         # Middle frame of the clip (always)
overlay.png       # That frame with the mask blended in green (always)

# Only with --extras:
mask_vis.png               # Mask rendered as a greyscale PNG
video_original_hires.gif   # All frames at display resolution
video_original_lowres.gif  # All frames at 50% of display resolution
video_overlay_hires.gif    # Overlay GIF at display resolution
video_overlay_lowres.gif   # Overlay GIF at 50% of display resolution

All output filenames can be overridden via the filenames: section in config/config.yaml.

Survey answers (metadata.json)

Keys and values are determined by the questions section in config/config.yaml. With the default config:

{
  "flow":          "Turbulent | Laminar | Uncertain",
  "shadows":       "Yes | No | Uncertain",
  "artifacts":     "Yes | No | Uncertain",
  "lighting":      "Day | Night | Uncertain",
  "exposure":      "Overexposed | Underexposed | Both | Normal | Uncertain",
  "snowing":       "Yes | No | Uncertain",
  "snow_on_ground":"Yes | No | Uncertain"
}

How it works

Clip format

Each clip is a ZIP archive containing a video file (default left.mp4, configurable via filenames.video_in_zip). The filename encodes the recording timestamp (e.g. left_20230615T120000.zip).

Frame loading

Up to max_frames frames are extracted from the video and scaled so the longest side is display_max px. This display-resolution copy is what the annotator works on; the full-resolution dimensions are remembered separately so the saved mask is upscaled back to the original size on export.

Mask drawing

The mask is a binary NumPy array matching the display frame size. Each brush stroke stamps a filled circle of the selected radius, setting pixels to 1 (draw) or 0 (erase). The history stack stores a copy of the mask before each stroke, enabling unlimited undo. On save the mask is resized to the original video resolution with nearest-neighbour interpolation and written as an 8-bit PNG (0 or 255).

Resuming

When a clip is loaded that already has a saved mask.png and metadata.json, the mask is restored at display resolution and the survey answers are pre-filled. Reload Saved lets you revert to the last save at any point during the current session.

Repository structure

config/
    config.yaml             # Your local config (git-ignored, copy from example)
    config.example.yaml     # Example config to copy and edit
    clips.txt               # Your clip list (git-ignored, copy from example)
    clips.example.txt       # Example clip list
src/river_annotation_tool/
    annotation_script.py    # Entry point — argument parsing and app launch
    annotator.py            # Main QMainWindow — orchestrates all components
    clip_selector.py        # Reads the clip list and picks the next clip
    mask_canvas.py          # Drawing widget — brush, undo, erase, mouse events
    video_loader.py         # ZIP extraction and frame resizing
    config.py               # AppConfig dataclass and YAML loader
    __init__.py             # Package version
pyproject.toml              # Project metadata and dependencies

Development

# Install pre-commit hooks
pre-commit install
pre-commit run --all-files   # Run manually once

# Add a dependency
uv add <package>
uv add --dev <package>       # Development-only