Compare commits
9 Commits
refactor-f
...
32bca14661
| Author | SHA1 | Date | |
|---|---|---|---|
| 32bca14661 | |||
| 281a5e104c | |||
| da53d6cd4a | |||
| 8c793e4488 | |||
| 16f891e6eb | |||
| 2820edc871 | |||
| 84f2b4e1e8 | |||
| 08513d643a | |||
| 6aed29ff9b |
19
README.md
19
README.md
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user