Add Previous button, remove Save button, warn before overwriting annotations

- Previous: saves current clip and navigates back through session history;
  disabled on the first clip, re-enabled automatically as you advance.
- Next: shows a dialog when a saved annotation already exists, letting the
  annotator choose to replace it or keep the existing save before advancing.
- Removed the standalone Save button; Next auto-saves on every advance.
- Skip already wrote nothing to disk; clarified in README.
- Refactored _advance_clip into _switch_ui_to_clip (shared with prev/next).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 14:17:45 +02:00
parent 4aa1e32681
commit 5b6efc7158
2 changed files with 77 additions and 21 deletions

View File

@@ -131,9 +131,9 @@ The window shows the video on the left (auto-playing) and the survey panel on th
| Undo last stroke | **Undo** | | Undo last stroke | **Undo** |
| Clear entire mask | **Clear** | | Clear entire mask | **Clear** |
| Adjust brush size | Slider next to the erase controls | | Adjust brush size | Slider next to the erase controls |
| Save and continue | **Next** — saves current clip and loads the next one | | Save and continue | **Next** — saves current clip and loads the next one. If the clip already has a saved annotation a dialog asks whether to replace it or keep the existing save. |
| Skip without saving | **Skip** — discards changes and loads the next one | | Go back | **Previous** — saves current clip and returns to the previously viewed clip. Disabled on the first clip. |
| Save only | **Save**writes to disk without advancing | | Skip without saving | **Skip**discards any unsaved changes and loads the next clip without writing anything to disk. |
| Restore last save | **Reload Saved** — reverts mask and answers to what was last written | | Restore last save | **Reload Saved** — reverts mask and answers to what was last written |
## Output ## Output

View File

@@ -48,13 +48,20 @@ class Annotator(QMainWindow):
skip_annotated=skip_annotated, skip_annotated=skip_annotated,
) )
self.history: list[Path] = []
self.history_pos: int = -1
self.setWindowTitle("River Annotator") self.setWindowTitle("River Annotator")
self._load_clip(specific=clip) self._load_clip(specific=clip)
self._history_push()
self._init_ui() self._init_ui()
self._init_timer() self._init_timer()
# ── clip loading ─────────────────────────────────────────────── # ── clip loading ───────────────────────────────────────────────
def _load_clip(self, specific: str = None): def _load_clip(self, specific: str = None, path: Path = None):
if path is not None:
self.filename = path
else:
self.filename = self.selector.next(specific=specific) self.filename = self.selector.next(specific=specific)
self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames( self.frames, self.fps, self.dh, self.dw, self.h, self.w = load_frames(
self.filename, self.filename,
@@ -66,6 +73,11 @@ class Annotator(QMainWindow):
) )
self._pending_answers = self._read_saved_answers() self._pending_answers = self._read_saved_answers()
def _history_push(self):
del self.history[self.history_pos + 1 :]
self.history.append(self.filename)
self.history_pos = len(self.history) - 1
def _read_saved_mask(self): def _read_saved_mask(self):
mask_path = self.out_dir / self.filename.stem / self.cfg.filenames.mask mask_path = self.out_dir / self.filename.stem / self.cfg.filenames.mask
if not mask_path.exists(): if not mask_path.exists():
@@ -93,7 +105,8 @@ class Annotator(QMainWindow):
self.q_widgets = {} self.q_widgets = {}
question_panel = self._build_question_panel() question_panel = self._build_question_panel()
btn_save = QPushButton("Save") self.btn_prev = QPushButton("Previous")
self.btn_prev.setEnabled(False)
btn_next = QPushButton("Next") btn_next = QPushButton("Next")
btn_skip = QPushButton("Skip") btn_skip = QPushButton("Skip")
btn_clear = QPushButton("Clear") btn_clear = QPushButton("Clear")
@@ -101,7 +114,7 @@ class Annotator(QMainWindow):
btn_reload = QPushButton("Reload Saved") btn_reload = QPushButton("Reload Saved")
row1 = QHBoxLayout() row1 = QHBoxLayout()
for b in [btn_save, btn_next, btn_skip]: for b in [self.btn_prev, btn_next, btn_skip]:
row1.addWidget(b) row1.addWidget(b)
row2 = QHBoxLayout() row2 = QHBoxLayout()
@@ -128,7 +141,7 @@ class Annotator(QMainWindow):
container.setLayout(main) container.setLayout(main)
self.setCentralWidget(container) self.setCentralWidget(container)
btn_save.clicked.connect(self.save) self.btn_prev.clicked.connect(self.prev_clip)
btn_next.clicked.connect(self.next_clip) btn_next.clicked.connect(self.next_clip)
btn_skip.clicked.connect(self.skip_clip) btn_skip.clicked.connect(self.skip_clip)
btn_clear.clicked.connect(self.mc.clear) btn_clear.clicked.connect(self.mc.clear)
@@ -254,17 +267,7 @@ class Annotator(QMainWindow):
if answers: if answers:
self._set_answers(answers) self._set_answers(answers)
def _advance_clip(self): def _switch_ui_to_clip(self):
try:
self._load_clip()
except RuntimeError:
msg = QMessageBox(self)
msg.setWindowTitle("All done!")
msg.setText("You have reached the end of all clips.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
msg.exec()
QApplication.instance().quit()
return
self.frame_i = 0 self.frame_i = 0
self.mc.load_clip( self.mc.load_clip(
self.frames, self.frames,
@@ -276,8 +279,61 @@ class Annotator(QMainWindow):
if self._pending_answers: if self._pending_answers:
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)
def _advance_clip(self):
if self.history_pos < len(self.history) - 1:
self.history_pos += 1
self._load_clip(path=self.history[self.history_pos])
self._switch_ui_to_clip()
return
try:
self._load_clip()
except RuntimeError:
msg = QMessageBox(self)
msg.setWindowTitle("All done!")
msg.setText("You have reached the end of all clips.")
msg.setStandardButtons(QMessageBox.StandardButton.Ok)
msg.exec()
QApplication.instance().quit()
return
self._history_push()
self._switch_ui_to_clip()
def prev_clip(self):
if self.history_pos <= 0:
return
self.save()
self.history_pos -= 1
self._load_clip(path=self.history[self.history_pos])
self._switch_ui_to_clip()
def next_clip(self): def next_clip(self):
mask_path = self.out_dir / self.filename.stem / self.cfg.filenames.mask
if mask_path.exists():
msg = QMessageBox(self)
msg.setWindowTitle("Existing annotation found")
msg.setText(
f"'{self.filename.stem}' already has a saved annotation.\n"
"Replace it with your current work, or keep the existing save?"
)
btn_replace = msg.addButton(
"Replace & Continue", QMessageBox.ButtonRole.AcceptRole
)
btn_keep = msg.addButton(
"Keep Existing & Continue", QMessageBox.ButtonRole.AcceptRole
)
msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
msg.setDefaultButton(btn_replace)
msg.exec()
clicked = msg.clickedButton()
if clicked == btn_replace:
self.save()
self._advance_clip()
elif clicked == btn_keep:
self._advance_clip()
# Cancel: do nothing
else:
self.save() self.save()
self._advance_clip() self._advance_clip()