# River Annotation Tool A desktop application for manually annotating river video clips as part of the [HydroScan](https://github.com/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](https://docs.astral.sh/uv/) (recommended) or pip ## Installation ```sh # Clone the repository git clone 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: ```sh 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 ```sh 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 ```sh # 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. ```yaml 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 `//` 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: ```json { "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 ```sh # Install pre-commit hooks pre-commit install pre-commit run --all-files # Run manually once # Add a dependency uv add uv add --dev # Development-only ```