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:
@@ -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
|
||||||
|
|||||||
@@ -48,14 +48,21 @@ 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):
|
||||||
self.filename = self.selector.next(specific=specific)
|
if path is not None:
|
||||||
|
self.filename = path
|
||||||
|
else:
|
||||||
|
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,
|
||||||
self.cfg.max_frames,
|
self.cfg.max_frames,
|
||||||
@@ -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,10 +279,63 @@ 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):
|
||||||
self.save()
|
mask_path = self.out_dir / self.filename.stem / self.cfg.filenames.mask
|
||||||
self._advance_clip()
|
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._advance_clip()
|
||||||
|
|
||||||
def skip_clip(self):
|
def skip_clip(self):
|
||||||
self._advance_clip()
|
self._advance_clip()
|
||||||
|
|||||||
Reference in New Issue
Block a user