diff --git a/README.md b/README.md index 8ea2cf5..f68d029 100644 --- a/README.md +++ b/README.md @@ -89,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: diff --git a/config/config.example.yaml b/config/config.example.yaml index c197db8..4cfa720 100644 --- a/config/config.example.yaml +++ b/config/config.example.yaml @@ -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// # 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// # 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 diff --git a/config/questions.yaml b/config/questions.yaml index 4a5740c..5bdfb78 100644 --- a/config/questions.yaml +++ b/config/questions.yaml @@ -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" diff --git a/src/clip_annotator/annotator.py b/src/clip_annotator/annotator.py index c6adcee..91c2bbd 100644 --- a/src/clip_annotator/annotator.py +++ b/src/clip_annotator/annotator.py @@ -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: diff --git a/src/clip_annotator/mask_canvas.py b/src/clip_annotator/mask_canvas.py index c52e167..060f070 100644 --- a/src/clip_annotator/mask_canvas.py +++ b/src/clip_annotator/mask_canvas.py @@ -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")