Add end-of-clips dialog and --no-skip flag

Show a modal dialog when all clips have been processed and quit cleanly.
Add --no-skip CLI flag to include already-annotated clips (default remains to skip them).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 14:08:47 +02:00
parent b4daa28354
commit 4aa1e32681
4 changed files with 36 additions and 5 deletions

View File

@@ -51,6 +51,7 @@ python -m river_annotation_tool.annotation_script
| `--clips` | *(from config)* | Override `clips_file` from config | | `--clips` | *(from config)* | Override `clips_file` from config |
| `--clip` | *(first unannotated in list)* | Open a specific clip by stem name (e.g. `left_20230501`) | | `--clip` | *(first unannotated in list)* | Open a specific clip by stem name (e.g. `left_20230501`) |
| `--extras` | off | Also save GIFs and extra PNGs (see Output section) | | `--extras` | off | Also save GIFs and extra PNGs (see Output section) |
| `--no-skip` | off | Show already-annotated clips instead of skipping them |
### Typical workflows ### Typical workflows
@@ -109,7 +110,7 @@ Add, remove, or reorder questions directly in the YAML — the UI rebuilds autom
## Clip list file ## Clip list file
`config/clips.txt` lists the clip filenames to annotate, one per line. Lines starting with `#` are ignored. Clips are processed in order; already-annotated clips (those with an existing `mask.png`) are skipped automatically. `config/clips.txt` lists the clip filenames to annotate, one per line. Lines starting with `#` are ignored. Clips are processed in order; already-annotated clips (those with an existing `mask.png`) are skipped automatically. Pass `--no-skip` to include them. When the last clip is reached, a dialog appears and the app exits.
``` ```
# Example clips.txt # Example clips.txt

View File

@@ -1,4 +1,5 @@
import argparse import argparse
import sys
from pathlib import Path from pathlib import Path
from matplotlib import use from matplotlib import use
@@ -6,7 +7,7 @@ from matplotlib import use
use("QtAgg") use("QtAgg")
from PySide6.QtWidgets import QApplication from PySide6.QtWidgets import QApplication, QMessageBox
from .annotator import Annotator from .annotator import Annotator
from .config import load_config from .config import load_config
@@ -30,6 +31,11 @@ def parse_args():
action="store_true", action="store_true",
help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.", help="Also save GIFs, frame PNG, overlay PNG, and mask_vis PNG alongside the mask.",
) )
parser.add_argument(
"--no-skip",
action="store_true",
help="Show already-annotated clips instead of skipping them.",
)
return parser.parse_args() return parser.parse_args()
@@ -45,6 +51,15 @@ if __name__ == "__main__":
cfg.clips_file = args.clips cfg.clips_file = args.clips
app = QApplication([]) app = QApplication([])
win = Annotator(cfg, clip=args.clip, extras=args.extras) try:
win = Annotator(
cfg,
clip=args.clip,
extras=args.extras,
skip_annotated=not args.no_skip,
)
except RuntimeError as e:
QMessageBox.information(None, "No clips", str(e))
sys.exit(0)
win.show() win.show()
app.exec() app.exec()

View File

@@ -6,11 +6,13 @@ import numpy as np
from PIL import Image from PIL import Image
from PySide6.QtCore import QTimer from PySide6.QtCore import QTimer
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication,
QButtonGroup, QButtonGroup,
QGroupBox, QGroupBox,
QHBoxLayout, QHBoxLayout,
QLabel, QLabel,
QMainWindow, QMainWindow,
QMessageBox,
QPushButton, QPushButton,
QRadioButton, QRadioButton,
QVBoxLayout, QVBoxLayout,
@@ -29,6 +31,7 @@ class Annotator(QMainWindow):
config: AppConfig, config: AppConfig,
clip: str = None, clip: str = None,
extras: bool = False, extras: bool = False,
skip_annotated: bool = True,
): ):
super().__init__() super().__init__()
@@ -42,6 +45,7 @@ class Annotator(QMainWindow):
clips_file=Path(config.clips_file), clips_file=Path(config.clips_file),
mask_filename=config.filenames.mask, mask_filename=config.filenames.mask,
zip_extension=config.filenames.zip_extension, zip_extension=config.filenames.zip_extension,
skip_annotated=skip_annotated,
) )
self.setWindowTitle("River Annotator") self.setWindowTitle("River Annotator")
@@ -251,7 +255,16 @@ class Annotator(QMainWindow):
self._set_answers(answers) self._set_answers(answers)
def _advance_clip(self): def _advance_clip(self):
self._load_clip() 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,

View File

@@ -9,11 +9,13 @@ class ClipSelector:
clips_file: Path, clips_file: Path,
mask_filename: str = "mask.png", mask_filename: str = "mask.png",
zip_extension: str = ".zip", zip_extension: str = ".zip",
skip_annotated: bool = True,
): ):
self.data_dir = data_dir self.data_dir = data_dir
self.out_dir = out_dir self.out_dir = out_dir
self.mask_filename = mask_filename self.mask_filename = mask_filename
self.zip_extension = zip_extension self.zip_extension = zip_extension
self.skip_annotated = skip_annotated
self.clips = self._load_clips(clips_file) self.clips = self._load_clips(clips_file)
self.index = 0 self.index = 0
@@ -46,6 +48,6 @@ class ClipSelector:
while self.index < len(self.clips): while self.index < len(self.clips):
clip = self.clips[self.index] clip = self.clips[self.index]
self.index += 1 self.index += 1
if not self.is_annotated(clip): if not self.skip_annotated or not self.is_annotated(clip):
return clip return clip
raise RuntimeError("No remaining clips to annotate") raise RuntimeError("No remaining clips to annotate")