8 Commits

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
# 1. Clone and install
git clone https://gitlab.datascience.ch/industry/aimsight/river-annotation-tool
cd river-annotation-tool
git clone https://exchange.sensima.ch/ivan.sievering/clip-annotator
cd clip-annotator
uv sync
# 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
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)
# Edit config/clips.txt (list clips to annotate)
# 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:
```sh
# macOS/Linux
cp config/config.example.yaml config/config.yaml
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.
@@ -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:
```sh
cp .env.example .env
cp .env.example .env # macOS/Linux
copy .env.example .env # Windows
# 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
> **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
uv run python -m clip_annotator
# or, if you have the venv activated:

View File

@@ -1,8 +1,8 @@
storage: local # 'local' or '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
out_dir: # e.g. /data/out or for S3: hydroscan-data/annotations/<name>/ # Put your name here
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
# For S3 credentials, copy .env.example to .env and fill in:
# S3_ACCESS_KEY, S3_SECRET_ACCESS_KEY, S3_ENDPOINT_URL

View File

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

View File

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

View File

@@ -41,6 +41,7 @@ class MaskCanvas:
def _build_figure(self, frames):
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.ax = self.fig.add_subplot(111)
self.ax.axis("off")