Enhance HVideoTool's detection and restoration features: added support for tracking the model used in detections, improved temporal coherence by allowing the use of already-restored frames in the restoration process, and updated the UI to reflect these changes with new indicators and configuration options. Documentation in CLAUDE.md has been updated accordingly.

This commit is contained in:
Leonid Pershin
2026-06-07 08:40:57 +03:00
parent 7a225efa85
commit cabb4e3d3d
11 changed files with 168 additions and 27 deletions
+48 -3
View File
@@ -88,6 +88,7 @@ class MainWindow(QMainWindow):
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_count = 0 # frames with output in restored/ (for the progress summary)
self._showing_restored = False
self._nav_sync = False # guard against slider<->list signal loops
self._busy = False # a long operation is running
@@ -218,6 +219,11 @@ class MainWindow(QMainWindow):
clayout.setSpacing(2)
clayout.addWidget(self.view, 1)
clayout.addWidget(self._build_nav_bar())
# Processing summary under the scrubber: how much of the sequence is done.
self.stats_label = QLabel("")
self.stats_label.setAlignment(Qt.AlignCenter)
self.stats_label.setStyleSheet("QLabel{color:#888; padding:1px;}")
clayout.addWidget(self.stats_label)
right = QWidget()
rlayout = QVBoxLayout(right)
@@ -225,8 +231,10 @@ class MainWindow(QMainWindow):
self.detail_header = QLabel("Детекции")
self.detail_header.setWordWrap(True)
rlayout.addWidget(self.detail_header)
self.detail_table = QTableWidget(0, 4)
self.detail_table.setHorizontalHeaderLabels(["Категория", "Увер.", "BBox (x,y,w,h)", "Полигон"])
self.detail_table = QTableWidget(0, 5)
self.detail_table.setHorizontalHeaderLabels(
["Категория", "Модель", "Увер.", "BBox (x,y,w,h)", "Полигон"]
)
self.detail_table.verticalHeader().setVisible(False)
self.detail_table.setSelectionBehavior(QTableWidget.SelectRows)
self.detail_table.setEditTriggers(QTableWidget.NoEditTriggers)
@@ -864,6 +872,7 @@ class MainWindow(QMainWindow):
self._end_busy()
loaded = self._load_cached_results() # reuse a matching on-disk cache
self._refresh_marks()
self._refresh_restored_marks()
if not files:
self.view.set_image(None, [])
@@ -1031,6 +1040,7 @@ class MainWindow(QMainWindow):
self._showing_restored = True
self.view.set_image(restored, [])
self._update_restore_actions()
self._refresh_restored_marks()
self.statusBar().showMessage(f"Расцензурено ({engine}): {Path(k).name}")
self.statusBar().showMessage(f"Восстановление: {path.name}")
@@ -1098,6 +1108,7 @@ class MainWindow(QMainWindow):
self._showing_restored = True
self.view.set_image(img, [])
self._update_restore_actions()
self._refresh_restored_marks()
self.statusBar().showMessage(
"Расцензуривание отменено" if cancelled
else f"Готово: результаты в {out_dir.name}/ ({total} кадров)"
@@ -1196,6 +1207,7 @@ class MainWindow(QMainWindow):
elif self.file_list.count() == 0:
self.view.set_image(None, [])
self._refresh_marks() # rows shifted — remap marks to new indices
self._refresh_restored_marks()
self._update_nav()
@staticmethod
@@ -1276,6 +1288,39 @@ class MainWindow(QMainWindow):
if self._results.get(self.file_list.item(i).data(Qt.UserRole))
}
self.frame_slider.set_marks(marks)
self._update_counts_label()
def _update_counts_label(self) -> None:
"""Refresh the processing summary under the scrubber (cheap; counts only)."""
n = len(self._files)
detected = sum(1 for p in self._files if str(p) in self._results)
hits = sum(1 for p in self._files if self._results.get(str(p)))
if n == 0:
self.stats_label.setText("")
return
self.stats_label.setText(
f"Кадров: {n} · детектировано: {detected}/{n} (с цензурой: {hits})"
f" · расцензурено: {self._restored_count}/{n}"
)
def _refresh_restored_marks(self) -> None:
"""Scan ``restored/`` and project restored frames onto the scrubber (green).
Done only on load / after a restore op (not per-frame) — it touches the disk.
"""
stems: set[str] = set()
if self._project is not None and self._project.restored_dir.is_dir():
stems = {p.stem for p in self._project.restored_dir.glob("*.jpg")}
mem = set(self._restored) # single-frame restores held in memory (not on disk yet)
rows: set[int] = set()
if stems or mem:
for i in range(self.file_list.count()):
fp = Path(self.file_list.item(i).data(Qt.UserRole))
if fp.stem in stems or str(fp) in mem:
rows.add(i)
self._restored_count = len(rows)
self.frame_slider.set_restored_marks(rows)
self._update_counts_label()
def _fill_detail_table(self, path: Path, img, dets: list[Detection] | None) -> None:
h, w = (img.shape[0], img.shape[1]) if img is not None else (0, 0)
@@ -1298,7 +1343,7 @@ class MainWindow(QMainWindow):
self.detail_table.setRowCount(len(dets))
for row, d in enumerate(dets):
x, y, bw, bh = d.bbox
cells = [d.display, f"{d.score:.2f}", f"{x},{y},{bw},{bh}", str(len(d.polygon))]
cells = [d.display, d.model or "", f"{d.score:.2f}", f"{x},{y},{bw},{bh}", str(len(d.polygon))]
for col, text in enumerate(cells):
self.detail_table.setItem(row, col, QTableWidgetItem(text))
self.detail_table.blockSignals(False)