9 Commits

Author SHA1 Message Date
32bca14661 Merge pull request 'changes-wrt-feedback' (#1) from changes-wrt-feedback into main
Reviewed-on: #1
2026-06-03 12:15:49 +02:00
281a5e104c config: update annotation questions for flow, lighting, weather and sediments 2026-06-03 12:13:21 +02:00
da53d6cd4a ui: show clip position counter in window title 2026-06-03 11:58:48 +02:00
8c793e4488 ui: maximize canvas area and compact side panels 2026-06-03 11:56:10 +02:00
16f891e6eb fix: corrected paths in config example 2026-06-03 11:50:14 +02:00
2820edc871 docs: add river annotation reference link to README 2026-06-03 11:47:45 +02:00
84f2b4e1e8 Added bash commands for windows 2026-06-02 14:35:44 +02:00
08513d643a Update git references for Sensima 2026-06-02 14:32:09 +02:00
6aed29ff9b Merge branch 'refactor-for-aimsight' into 'main'
Refactor for AimSight: modular architecture, S3 storage, and new annotation tools

See merge request industry/aimsight/river-annotation-tool!1
2026-06-02 09:53:59 +00:00
5 changed files with 58 additions and 29 deletions

View File

@@ -11,14 +11,17 @@ A desktop GUI application for manually annotating video clips. Annotators draw p
```sh ```sh
# 1. Clone and install # 1. Clone and install
git clone https://gitlab.datascience.ch/industry/aimsight/river-annotation-tool git clone https://exchange.sensima.ch/ivan.sievering/clip-annotator
cd river-annotation-tool cd clip-annotator
uv sync uv sync
# 2. Create config and clip list from examples # 2. Create config and clip list from examples
cp config/config.example.yaml config/config.yaml cp config/config.example.yaml config/config.yaml # macOS/Linux
cp config/clips.example.txt config/clips.txt cp config/clips.example.txt config/clips.txt
copy config\config.example.yaml config\config.yaml # Windows
copy config\clips.example.txt config\clips.txt
# 3. Edit config/config.yaml (set data_dir and out_dir) # 3. Edit config/config.yaml (set data_dir and out_dir)
# Edit config/clips.txt (list clips to annotate) # Edit config/clips.txt (list clips to annotate)
# Edit config/questions.yaml to customise survey questions (optional) # Edit config/questions.yaml to customise survey questions (optional)
@@ -45,8 +48,13 @@ pip install -e .
Before running, create your config and clip list from the provided examples: Before running, create your config and clip list from the provided examples:
```sh ```sh
# macOS/Linux
cp config/config.example.yaml config/config.yaml cp config/config.example.yaml config/config.yaml
cp config/clips.example.txt config/clips.txt cp config/clips.example.txt config/clips.txt
# Windows
copy config\config.example.yaml config\config.yaml
copy 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. Survey questions are defined in `config/questions.yaml` (committed to the repo; edit to customise). See the [Configuration](#configuration) section for all available options. 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. Survey questions are defined in `config/questions.yaml` (committed to the repo; edit to customise). See the [Configuration](#configuration) section for all available options.
@@ -64,7 +72,8 @@ out_dir: my-bucket/annotation_results
Copy `.env.example` to `.env` and fill in your credentials — the app loads this file automatically at startup: Copy `.env.example` to `.env` and fill in your credentials — the app loads this file automatically at startup:
```sh ```sh
cp .env.example .env cp .env.example .env # macOS/Linux
copy .env.example .env # Windows
# edit .env with your credentials # edit .env with your credentials
``` ```
@@ -80,6 +89,8 @@ The `clips_file` (the list of clip filenames to annotate) is always read from th
## Usage ## Usage
> **River annotation reference:** If you are annotating river footage, consult the [river annotation guide](https://docs.google.com/document/d/1iPN9JxiDtb60kC0yjO8tTM0XEfDTGM5ysw33WY-BEDQ/edit?usp=sharing) for guidance on how to draw masks correctly.
```sh ```sh
uv run python -m clip_annotator uv run python -m clip_annotator
# or, if you have the venv activated: # or, if you have the venv activated:

View File

@@ -1,8 +1,8 @@
storage: local # 'local' or 's3' storage: local # 'local' or 's3'
# Required: set these to your actual paths (local path or bucket/prefix for S3) # Required: set these to your actual paths (local path or bucket/prefix for S3)
data_dir: # e.g. /data/clips or for S3: hydroscan-data/GRAMMONT/clips data_dir: # e.g. data/clips or for S3: hydroscan-data/GRAMMONT/
out_dir: # e.g. /data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here out_dir: # e.g. data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here
# For S3 credentials, copy .env.example to .env and fill in: # For S3 credentials, copy .env.example to .env and fill in:
# S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL # S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL

View File

@@ -1,34 +1,38 @@
- section: River - section: River
items: items:
- key: flow - key: flow
label: Flow Regime label: Flow
options: [Turbulent, Laminar, Uncertain] options: ["No", Standard, High, Uncertain]
default: Laminar default: Standard
- key: shadows - key: shadows
label: Strong Shadows label: Strong Shadows
options: [Yes, No, Uncertain] options: ["Yes", "No", Uncertain]
default: No default: "No"
- key: sediments
label: Sediments
options: ["Yes", "No", Uncertain]
default: "No"
- key: artifacts - key: artifacts
label: Artifacts on River label: Artifacts on River
options: [Yes, No, Uncertain] options: ["Yes", "No", Uncertain]
default: No default: "No"
- section: Scene - section: Scene
items: items:
- key: lighting - key: lighting
label: Lighting label: Lighting
options: [Day, Night, Uncertain] options: [Bright, Dark, Uncertain]
default: Day default: Bright
- key: exposure - key: exposure
label: Exposure label: Exposure
options: [Overexposed, Underexposed, Both, Normal, Uncertain] options: [Overexposed, Underexposed, Both, Normal, Uncertain]
default: Normal default: Normal
- section: Weather - section: Weather
items: items:
- key: snowing - key: precipitation
label: Snowing label: Precipitation
options: [Yes, No, Uncertain] options: ["Yes", "No", Uncertain]
default: No default: "No"
- key: snow_on_ground - key: snow_on_ground
label: Snow on Ground label: Snow on Ground
options: [Yes, No, Uncertain] options: ["Yes", "No", Uncertain]
default: No default: "No"

View File

@@ -148,6 +148,15 @@ class Annotator(QMainWindow):
return None return None
return self._json_read(meta_path) return self._json_read(meta_path)
# ── helpers ────────────────────────────────────────────────────
def _update_window_title(self):
total = len(self.selector.clips)
try:
idx = self.selector.clips.index(self.filename) + 1
except ValueError:
idx = "?"
self.setWindowTitle(f"Clip Annotator ({idx} / {total})")
# ── UI setup ─────────────────────────────────────────────────── # ── UI setup ───────────────────────────────────────────────────
def _init_ui(self): def _init_ui(self):
self.mc = MaskCanvas(self.frames, self.dh, self.dw) self.mc = MaskCanvas(self.frames, self.dh, self.dw)
@@ -213,9 +222,9 @@ class Annotator(QMainWindow):
vert_panel = QHBoxLayout() vert_panel = QHBoxLayout()
vert_panel.setContentsMargins(0, 0, 4, 0) vert_panel.setContentsMargins(0, 0, 4, 0)
for label_text, slider, reset_btn in [ for label_text, slider, reset_btn in [
("Brightness", self.mc.brightness_slider, self.mc.brightness_reset), ("B", self.mc.brightness_slider, self.mc.brightness_reset),
("Contrast", self.mc.contrast_slider, self.mc.contrast_reset), ("C", self.mc.contrast_slider, self.mc.contrast_reset),
("Gamma", self.mc.gamma_slider, self.mc.gamma_reset), ("G", self.mc.gamma_slider, self.mc.gamma_reset),
]: ]:
col = QVBoxLayout() col = QVBoxLayout()
lbl = QLabel(label_text) lbl = QLabel(label_text)
@@ -243,8 +252,9 @@ class Annotator(QMainWindow):
right_widget.setLayout(question_panel) right_widget.setLayout(question_panel)
main = QHBoxLayout() main = QHBoxLayout()
main.addWidget(left_widget, 3) right_widget.setMaximumWidth(160)
main.addWidget(right_widget, 1) main.addWidget(left_widget, 1)
main.addWidget(right_widget, 0)
container = QWidget() container = QWidget()
container.setLayout(main) container.setLayout(main)
@@ -264,6 +274,8 @@ class Annotator(QMainWindow):
self._set_answers(self._pending_answers) self._set_answers(self._pending_answers)
self._pending_answers = None self._pending_answers = None
self._update_window_title()
def _build_question_panel(self) -> QVBoxLayout: def _build_question_panel(self) -> QVBoxLayout:
vbox = QVBoxLayout() vbox = QVBoxLayout()
for section, qs in self.cfg.get_questions(): for section, qs in self.cfg.get_questions():
@@ -272,19 +284,19 @@ class Annotator(QMainWindow):
for key, label, options, default in qs: for key, label, options, default in qs:
gvbox.addWidget(QLabel(label)) gvbox.addWidget(QLabel(label))
btn_group = QButtonGroup(self) btn_group = QButtonGroup(self)
row = QHBoxLayout() col = QVBoxLayout()
buttons = [] buttons = []
for opt in options: for opt in options:
btn = QRadioButton(opt) btn = QRadioButton(opt)
btn_group.addButton(btn) btn_group.addButton(btn)
row.addWidget(btn) col.addWidget(btn)
buttons.append(btn) buttons.append(btn)
if default == opt: if default == opt:
btn.setChecked(True) btn.setChecked(True)
if default is None and buttons: if default is None and buttons:
buttons[-1].setChecked(True) buttons[-1].setChecked(True)
self.q_widgets[key] = (btn_group, buttons, options) self.q_widgets[key] = (btn_group, buttons, options)
gvbox.addLayout(row) gvbox.addLayout(col)
group.setLayout(gvbox) group.setLayout(gvbox)
vbox.addWidget(group) vbox.addWidget(group)
return vbox return vbox
@@ -408,6 +420,7 @@ class Annotator(QMainWindow):
self._set_answers(self._pending_answers) self._set_answers(self._pending_answers)
self._pending_answers = None self._pending_answers = None
self.btn_prev.setEnabled(self.history_pos > 0) self.btn_prev.setEnabled(self.history_pos > 0)
self._update_window_title()
def _advance_clip(self): def _advance_clip(self):
if self.history_pos < len(self.history) - 1: if self.history_pos < len(self.history) - 1:

View File

@@ -41,6 +41,7 @@ class MaskCanvas:
def _build_figure(self, frames): def _build_figure(self, frames):
self.fig = Figure(figsize=(self.dw / 80, self.dh / 80)) self.fig = Figure(figsize=(self.dw / 80, self.dh / 80))
self.fig.subplots_adjust(left=0, right=1, top=0.97, bottom=0)
self.canvas = FigureCanvas(self.fig) self.canvas = FigureCanvas(self.fig)
self.ax = self.fig.add_subplot(111) self.ax = self.fig.add_subplot(111)
self.ax.axis("off") self.ax.axis("off")