Refactor HVideoTool's configuration and project management: updated project handling in core/project.py, streamlined restoration settings in config.py, and improved documentation in CLAUDE.md. Removed unused parameters and enhanced type hints for better clarity.
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
"""Widget that renders an image and draws detection overlays.
|
||||
|
||||
Overlay visibility and the confidence threshold are applied at paint time, so
|
||||
toggling them is instant. One detection can be *highlighted* (selected in the
|
||||
detail table) — it is drawn boldly even if below the threshold, while the others
|
||||
dim, so the user can inspect exactly what the detector found.
|
||||
The confidence threshold is applied at paint time, so changing it is instant. One
|
||||
detection can be *highlighted* (selected in the detail table) — it is drawn boldly
|
||||
even if below the threshold, while the others dim, so the user can inspect exactly
|
||||
what the detector found.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -24,7 +24,6 @@ class ImageView(QWidget):
|
||||
self._cfg = overlay_cfg
|
||||
self._qimage: QImage | None = None
|
||||
self._dets: list[Detection] = []
|
||||
self._overlay_enabled = True
|
||||
self._threshold = 0.0
|
||||
self._highlight: int | None = None
|
||||
self.setMinimumSize(480, 360)
|
||||
@@ -42,10 +41,6 @@ class ImageView(QWidget):
|
||||
self._highlight = None
|
||||
self.update()
|
||||
|
||||
def set_overlay_enabled(self, enabled: bool) -> None:
|
||||
self._overlay_enabled = enabled
|
||||
self.update()
|
||||
|
||||
def set_threshold(self, threshold: float) -> None:
|
||||
self._threshold = threshold
|
||||
self.update()
|
||||
@@ -59,13 +54,13 @@ class ImageView(QWidget):
|
||||
r, g, b = self._cfg.colors.get(ctype.value, (255, 0, 0))
|
||||
return QColor(r, g, b)
|
||||
|
||||
def paintEvent(self, event) -> None: # noqa: N802 - Qt signature
|
||||
def paintEvent(self, event) -> None:
|
||||
painter = QPainter(self)
|
||||
painter.fillRect(self.rect(), QColor(18, 18, 18))
|
||||
|
||||
if self._qimage is None:
|
||||
painter.setPen(QColor(160, 160, 160))
|
||||
painter.drawText(self.rect(), Qt.AlignCenter, "Откройте папку с картинками (Файл → Открыть папку…)")
|
||||
painter.drawText(self.rect(), Qt.AlignCenter, "Откройте проект (Файл → Открыть проект…)")
|
||||
painter.end()
|
||||
return
|
||||
|
||||
@@ -77,7 +72,7 @@ class ImageView(QWidget):
|
||||
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
|
||||
painter.drawImage(QRectF(ox, oy, dw, dh), self._qimage)
|
||||
|
||||
if self._overlay_enabled and self._dets:
|
||||
if self._dets:
|
||||
painter.setRenderHint(QPainter.Antialiasing, True)
|
||||
for i, d in enumerate(self._dets):
|
||||
highlighted = i == self._highlight
|
||||
|
||||
@@ -5,23 +5,24 @@ A project is a folder (``project.json`` + ``frames/`` + ``detections.json`` +
|
||||
model, threshold, restore engine) live in ``project.json``; the global
|
||||
``settings.json`` only seeds defaults for new projects.
|
||||
|
||||
Layout: a toolbar (new/open project · from-video · detector · model · calc-frame ·
|
||||
detect-all · threshold), then a splitter with three panes — left: collection
|
||||
controls + the file list; center: the image with overlays; right: a detail table
|
||||
of every detection. Collection controls sit by the file list (they act on its
|
||||
selection), keeping the toolbar to detection/entry actions only.
|
||||
Layout: a toolbar (new/open project · from-video · model · calc-frame · detect-all ·
|
||||
restore · threshold), then a splitter with three panes — left: collection controls +
|
||||
the file list; center: the image with overlays; right: a detail table of every
|
||||
detection. Collection controls sit by the file list (they act on its selection),
|
||||
keeping the toolbar to detection/entry actions only.
|
||||
|
||||
Viewing and detecting are decoupled, so browsing a big project stays instant even
|
||||
with a slow (CPU) detector:
|
||||
Detection is YOLO-only; restoration is DeepMosaics-only. Viewing and detecting are
|
||||
decoupled, so browsing a big project stays instant even with a slow (CPU) detector:
|
||||
- selecting a file just **shows** it (with its cached result, if any);
|
||||
- **double-clicking** a file, or "Рассчитать кадр", runs the detector on it;
|
||||
- "Детектировать все" runs the whole project.
|
||||
Both project loading and detect-all show a progress bar. Results are cached in the
|
||||
project; switching detector/model clears the cache.
|
||||
project; switching the model clears the cache.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
@@ -77,13 +78,12 @@ class MainWindow(QMainWindow):
|
||||
self._detector = None
|
||||
self._detector_key = None
|
||||
self._project: Project | None = None # the open project (None until one is opened)
|
||||
self._folder: Path | None = None # == project.frames_dir while a project is open
|
||||
self._files: list[Path] = []
|
||||
self._results: dict[str, list[Detection]] = {} # path -> detections (cache)
|
||||
self._current: Path | None = None
|
||||
self._restorer = None # un-censor engine, built lazily from config
|
||||
self._restorer_key = None
|
||||
self._restored: dict[str, "object"] = {} # path -> restored image (BGR ndarray)
|
||||
self._restored: dict[str, object] = {} # path -> restored image (BGR ndarray)
|
||||
self._showing_restored = False
|
||||
self._nav_sync = False # guard against slider<->list signal loops
|
||||
self._busy = False # a long operation is running
|
||||
@@ -547,7 +547,7 @@ class MainWindow(QMainWindow):
|
||||
if Project.is_project(p):
|
||||
try:
|
||||
self._open_project(Project.load(p))
|
||||
except (OSError, ValueError) as exc: # noqa: BLE001 - surface to the user
|
||||
except (OSError, ValueError) as exc:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось открыть проект:\n{exc}")
|
||||
elif p.is_dir():
|
||||
QMessageBox.information(
|
||||
@@ -562,10 +562,8 @@ class MainWindow(QMainWindow):
|
||||
"""On startup, reopen the last project if it still exists (best-effort)."""
|
||||
last = settings_store.last_project()
|
||||
if last and Project.is_project(last):
|
||||
try:
|
||||
with contextlib.suppress(OSError, ValueError):
|
||||
self._open_project(Project.load(last))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
def _new_project_root(self, default_name: str = "") -> Path | None:
|
||||
"""Prompt for a parent dir + name; return a fresh (empty) project root or None."""
|
||||
@@ -662,10 +660,8 @@ class MainWindow(QMainWindow):
|
||||
# Carry over an old sidecar detection cache (basename-keyed) if present.
|
||||
old_sidecar = src / ".hvideotool_detections.json"
|
||||
if old_sidecar.is_file():
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
shutil.copy2(str(old_sidecar), str(project.cache_path))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
self.statusBar().showMessage(f"Импортировано {copied} картинок → {project.name}")
|
||||
self._open_project(project)
|
||||
@@ -764,7 +760,7 @@ class MainWindow(QMainWindow):
|
||||
str(video), str(out), step=step, keyframes_only=keyframes_only,
|
||||
max_dim=max_dim, progress=cb,
|
||||
)
|
||||
except Exception as exc: # noqa: BLE001 - surface decode errors to the user
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "Ошибка", f"Не удалось извлечь кадры:\n{exc}")
|
||||
return
|
||||
finally:
|
||||
@@ -788,7 +784,6 @@ class MainWindow(QMainWindow):
|
||||
self.statusBar().showMessage(f"Сканирую папку: {folder}…")
|
||||
QApplication.processEvents()
|
||||
files = sorted(p for p in folder.iterdir() if p.suffix.lower() in _IMAGE_EXTS)
|
||||
self._folder = folder
|
||||
self._files = files
|
||||
self._results.clear()
|
||||
self._current = None
|
||||
@@ -850,7 +845,7 @@ class MainWindow(QMainWindow):
|
||||
img = imread_unicode(str(path))
|
||||
if img is None:
|
||||
raise RuntimeError(f"Не удалось прочитать: {Path(path).name}")
|
||||
dets = detector.detect(Frame(image=img, index=0, pts=0.0))
|
||||
dets = detector.detect(Frame(image=img))
|
||||
dets.sort(key=lambda d: d.score, reverse=True)
|
||||
return dets
|
||||
|
||||
@@ -1057,8 +1052,7 @@ class MainWindow(QMainWindow):
|
||||
self._start_job(fn, total, on_done=done)
|
||||
|
||||
def _make_restorer(self):
|
||||
key = (self._cfg.restorer, self._cfg.dm_dir, self._cfg.dm_model,
|
||||
self._cfg.dm_python, self._cfg.dm_gpu)
|
||||
key = (self._cfg.restorer, self._cfg.dm_dir, self._cfg.dm_model, self._cfg.dm_gpu)
|
||||
if key != self._restorer_key:
|
||||
self._restorer = build_restorer(self._cfg.restorer, self._cfg) # may raise
|
||||
self._restorer_key = key
|
||||
@@ -1266,7 +1260,7 @@ class MainWindow(QMainWindow):
|
||||
self.view.set_threshold(value)
|
||||
self._persist_settings()
|
||||
|
||||
def closeEvent(self, event) -> None: # noqa: N802 - Qt override
|
||||
def closeEvent(self, event) -> None:
|
||||
if self._job is not None: # stop a running background job before tearing down
|
||||
self._job.cancel()
|
||||
self._pool.waitForDone(3000)
|
||||
|
||||
@@ -27,11 +27,6 @@ class MarkerSlider(QSlider):
|
||||
self._marks = marks
|
||||
self.update()
|
||||
|
||||
def clear_marks(self) -> None:
|
||||
if self._marks:
|
||||
self._marks = set()
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event) -> None:
|
||||
super().paintEvent(event)
|
||||
if not self._marks or self.maximum() <= self.minimum():
|
||||
|
||||
@@ -33,7 +33,7 @@ class _Signals(QObject):
|
||||
class Job(QRunnable):
|
||||
"""Runs ``fn(job)`` on a thread pool, marshaling progress/result to the GUI."""
|
||||
|
||||
def __init__(self, fn: Callable[["Job"], Any]) -> None:
|
||||
def __init__(self, fn: Callable[[Job], Any]) -> None:
|
||||
super().__init__()
|
||||
self.setAutoDelete(False) # the GUI keeps a reference until `done`/`failed`
|
||||
self.signals = _Signals()
|
||||
@@ -56,12 +56,12 @@ class Job(QRunnable):
|
||||
self.signals.tick.emit(payload)
|
||||
|
||||
# -- thread entry point --
|
||||
def run(self) -> None: # noqa: D401 - QRunnable override
|
||||
def run(self) -> None:
|
||||
try:
|
||||
result = self._fn(self)
|
||||
except Cancelled:
|
||||
self.signals.done.emit(None)
|
||||
except Exception as exc: # noqa: BLE001 - surface engine/model errors to the GUI
|
||||
except Exception as exc:
|
||||
self.signals.failed.emit(str(exc))
|
||||
else:
|
||||
self.signals.done.emit(result)
|
||||
|
||||
Reference in New Issue
Block a user