Made project river-agnostic

This commit is contained in:
2026-05-27 10:02:20 +02:00
parent 23dbbc1555
commit 69eab7514f
13 changed files with 81 additions and 97 deletions

View File

@@ -1,6 +1,6 @@
# River Annotation Tool
# Video Annotation Tool
A desktop GUI 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.
A desktop GUI application for manually annotating video clips. Annotators draw pixel-level segmentation masks over footage and answer structured survey questions defined in a config file.
## Requirements
@@ -24,7 +24,7 @@ cp config/clips.example.txt config/clips.txt
# Edit config/questions.yaml to customise survey questions (optional)
# 4. Run
uv run python -m river_annotation_tool.annotation_script
uv run python -m clip_annotator.annotation_script
```
## Installation
@@ -81,9 +81,9 @@ The `clips_file` (the list of clip filenames to annotate) is always read from th
## Usage
```sh
uv run python -m river_annotation_tool.annotation_script
uv run python -m clip_annotator.annotation_script
# or, if you have the venv activated:
python -m river_annotation_tool.annotation_script
python -m clip_annotator.annotation_script
```
### Arguments
@@ -94,7 +94,7 @@ python -m river_annotation_tool.annotation_script
| `--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 |
| `--clip` | *(first unannotated in list)* | Open a specific clip by its stem name (filename without extension, e.g. `clip_20230501T120000`) |
| `--extras` | off | Also save GIFs and extra PNGs (see Output section) |
| `--no-skip` | off | Show already-annotated clips instead of skipping them |
@@ -102,16 +102,16 @@ python -m river_annotation_tool.annotation_script
```sh
# Annotate clips listed in config/clips.txt (default)
uv run python -m river_annotation_tool.annotation_script
uv run python -m clip_annotator.annotation_script
# Use a different config file
uv run python -m river_annotation_tool.annotation_script --config config/my_config.yaml
uv run python -m clip_annotator.annotation_script --config config/my_config.yaml
# Override paths from the command line
uv run python -m river_annotation_tool.annotation_script --data data/clips --out data/out
uv run python -m clip_annotator.annotation_script --data data/clips --out data/out
# Annotate a single specific clip
uv run python -m river_annotation_tool.annotation_script --clip left_20230615T120000
uv run python -m clip_annotator.annotation_script --clip clip_20230615T120000
```
## Configuration
@@ -121,8 +121,8 @@ Main settings live in `config/config.yaml`. Copy `config/config.example.yaml` to
```yaml
storage: local # required: 'local' or 's3'
data_dir: # required: directory containing ZIP archives (local path or bucket/prefix for S3)
out_dir: # required: where to write annotations
data_dir: # required: read-only source of ZIP archives (local path or bucket/prefix for S3)
out_dir: # required: write destination for annotations (can be same bucket as data_dir with a different prefix, or a separate location)
clips_file: config/clips.txt
optical_flow_config_file: config/optical_flow_config.yaml
@@ -139,7 +139,7 @@ filenames:
zip_extension: .zip
```
Output filenames (`mask.png`, `metadata.json`, etc.) have sensible defaults and can be overridden in the `filenames:` block — see [`config.py`](src/river_annotation_tool/config.py) for the full list.
Output filenames (`mask.png`, `metadata.json`, etc.) have sensible defaults and can be overridden in the `filenames:` block — see [`config.py`](src/clip_annotator/config.py) for the full list.
### Survey questions
@@ -147,9 +147,9 @@ Survey questions are defined in `config/questions.yaml` (committed to the repo).
### Optical flow segmentation
`config/optical_flow_config.yaml` controls the **Auto Segment** button. When pressed, the tool computes a river mask from the loaded frames and replaces the current mask (undoable). The segmentation combines two criteria:
`config/optical_flow_config.yaml` controls the **Auto Segment** button. When pressed, the tool computes a segmentation mask from the loaded frames and replaces the current mask (undoable). The segmentation combines two criteria:
- **Optical flow magnitude** — pixels where the temporal median of frame-to-frame flow (scaled by FPS) exceeds a fraction of the maximum are considered moving water.
- **Optical flow magnitude** — pixels where the temporal median of frame-to-frame flow (scaled by FPS) exceeds a fraction of the maximum are considered moving.
- **Brightness** — pixels outside a brightness window are excluded (removes sky, saturated glare, etc.).
```yaml
@@ -164,41 +164,25 @@ brightness_range: [2, 253] # [min, max] greyscale brightness to keep
## 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.
`config/clips.txt` lists bare clip filenames (not full paths) to annotate, one per line — the tool prepends `data_dir` automatically. Each entry must be the exact filename of the ZIP archive as it appears in `data_dir` (e.g. `clip_20230501T120000.zip`). Lines starting with `#` are ignored. Clips are processed in order; a clip is considered already-annotated when `<out_dir>/<clip_stem>/mask.png` exists and is skipped automatically. Pass `--no-skip` to include already-annotated clips. When the last clip is reached, a dialog appears and the app exits.
```
# Example clips.txt
left_20230501T120000.zip
left_20230502T120000.zip
clip_20230501T120000.zip
clip_20230502T120000.zip
```
Copy `config/clips.example.txt` as a starting point.
## Multi-annotator setup
Pre-made clip lists for 7 annotators are included in `config/annotator_A.txt` through `config/annotator_G.txt`. Each annotator is assigned exactly 5 recording days (non-consecutive where possible), covering all 24 available days across the dataset.
To run the tool for a specific annotator, pass their file via `--clips`:
To distribute clips across multiple annotators, create one clips file per annotator (e.g. `config/annotator_A.txt`) listing their assigned clip filenames, then pass it via `--clips`:
```sh
uv run python -m river_annotation_tool.annotation_script --clips config/annotator_A.txt
uv run python -m clip_annotator.annotation_script --clips config/annotator_A.txt
```
### Assignment
11 of the 24 days are reviewed by two annotators (the theoretical maximum given 7 × 5 = 35 slots and 24 days), giving 11 days with double coverage for inter-annotator agreement checks.
| Annotator | Days | Clips |
|---|---|---|
| A | 2025-11-17 · 2025-12-03 · 2026-01-01 · 2026-01-09 · 2026-02-11 | 94 |
| B | 2025-11-18 · 2025-12-05 · 2026-01-06 · 2026-02-12 · 2026-03-02 | 128 |
| C | 2025-11-22 · 2025-12-12 · 2026-01-07 · 2026-02-16 · 2026-03-03 | 146 |
| D | 2025-11-18 · 2025-11-24 · 2025-12-16 · 2026-01-08 · 2026-03-02 | 102 |
| E | 2025-11-25 · 2025-12-03 · 2026-01-09 · 2026-01-12 · 2026-03-03 | 80 |
| F | 2025-11-25 · 2025-12-16 · 2026-01-10 · 2026-01-13 · 2026-03-11 | 93 |
| G | 2025-11-22 · 2025-12-05 · 2026-01-12 · 2026-02-11 · 2026-03-12 | 110 |
Days covered by two annotators: 2025-11-18 (B, D) · 2025-11-22 (C, G) · 2025-11-25 (E, F) · 2025-12-03 (A, E) · 2025-12-05 (B, G) · 2025-12-16 (D, F) · 2026-01-09 (A, E) · 2026-01-12 (E, G) · 2026-02-11 (A, G) · 2026-03-02 (B, D) · 2026-03-03 (C, E)
Assigning non-overlapping clip lists lets each annotator work independently. Intentionally overlapping a subset of clips across annotators enables inter-annotator agreement checks.
## Controls
@@ -218,7 +202,7 @@ Three drawing tools are available in the tool row. The active tool is highlighte
| Action | How |
|---|---|
| Draw water mask | Click and drag on the video |
| Draw mask | Click and drag on the video |
| Erase mask | Toggle **Eraser** button (turns orange when active), then drag |
| Brush preview | A white circle follows the cursor showing the current brush size |
| Adjust brush size | **Brush size** slider (250 px, default 5); click **↺** to reset |
@@ -261,7 +245,7 @@ Polygons are drawn as overlays and do not affect the mask until you use **Fill**
| Action | How |
|---|---|
| Load mask from previous clip | **Load Prev Mask** — copies the saved mask of the previous clip onto the current one; undoable |
| Optical flow first guess | **Auto Segment** — replaces the current mask with an automatic river segmentation; undoable. Disabled when `enabled: false` in `config/optical_flow_config.yaml`. |
| Optical flow first guess | **Auto Segment** — replaces the current mask with an automatic segmentation based on motion and brightness; undoable. Disabled when `enabled: false` in `config/optical_flow_config.yaml`. |
### Image display adjustments
@@ -288,7 +272,7 @@ Click **↺** below any slider to restore its default value.
Each annotated clip produces a folder `<out_dir>/<clip_stem>/` with:
```
mask.png # Binary water mask at full source resolution (always)
mask.png # Binary segmentation 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)
@@ -323,7 +307,7 @@ Keys and values are determined by `config/questions.yaml`. With the default ques
### 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`).
Each clip is a ZIP archive containing a video file. The video inside the archive must be named `left.mp4` by default; if your archives use a different internal name, set `filenames.video_in_zip` in `config.yaml`. The ZIP filename becomes the output folder name (e.g. `clip_20230501T120000.zip``<out_dir>/clip_20230501T120000/`).
### Frame loading
@@ -350,14 +334,14 @@ config/
clips.example.txt # Example clip list
questions.yaml # Survey question definitions
optical_flow_config.yaml # Optical flow parameters (set enabled: false to disable Auto Segment)
src/river_annotation_tool/
src/clip_annotator/
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
filesystem.py # Storage backend — local passthrough or S3 via s3fs
mask_canvas.py # Drawing widget — brush, undo, erase, mouse events
video_loader.py # ZIP extraction and frame resizing
compute_optical_flow.py # Optical flow river segmentation (Auto Segment button)
compute_optical_flow.py # Optical flow segmentation (Auto Segment button)
config.py # AppConfig dataclass and YAML loader
__init__.py # Package version
pyproject.toml # Project metadata and dependencies