asreva 5468712a4a Replace hardcoded config and directory scan with YAML config and explicit clip list
- config.py constants -> config/config.yaml (user-editable, git-ignored)
- Questions and defaults now defined in the YAML, including per-question defaults
- ClipSelector no longer scans the data dir; reads a user-provided clips.txt instead
- Removed --daily / --time / --skip-existing-day args
- video_loader now samples frames evenly across the full clip
- pyyaml added as a dependency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-20 13:42:48 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:32:48 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:24:14 +02:00
2026-05-01 09:24:14 +02:00

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)

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.

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.

# 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
Undo last stroke Undo
Clear entire mask Clear
Adjust brush size Slider next to the erase controls
Save and continue Next — saves current clip and loads the next one
Skip without saving Skip — discards changes and loads the next one
Save only Save — writes to disk without advancing
Restore last save Reload Saved — reverts mask and answers to what was last written

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

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 left.mp4 video. 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
Description
Tool to manually annotate mask over a clip of images
Readme 230 KiB
Languages
Python 100%