Files
HVideoTool/hvideotool/ui/image_view.py
T

129 lines
5.2 KiB
Python

"""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.
"""
from __future__ import annotations
import cv2
import numpy as np
from PySide6.QtCore import QPointF, QRectF, Qt
from PySide6.QtGui import QBrush, QColor, QFont, QImage, QPainter, QPen, QPolygonF
from PySide6.QtWidgets import QWidget
from ..config import OverlayConfig
from ..core.detection.types import CensorType, Detection
class ImageView(QWidget):
def __init__(self, overlay_cfg: OverlayConfig, parent: QWidget | None = None) -> None:
super().__init__(parent)
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)
# ------------------------------------------------------------------ slots
def set_image(self, image_bgr: np.ndarray | None, dets: list[Detection]) -> None:
if image_bgr is None:
self._qimage = None
else:
rgb = np.ascontiguousarray(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB))
h, w, ch = rgb.shape
# .copy() so the QImage owns its pixels (the numpy buffer can be freed).
self._qimage = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
self._dets = dets
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()
def set_highlight(self, index: int | None) -> None:
self._highlight = index
self.update()
# ------------------------------------------------------------------ paint
def _color(self, ctype: CensorType) -> QColor:
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
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.end()
return
iw, ih = self._qimage.width(), self._qimage.height()
scale = min(self.width() / iw, self.height() / ih)
dw, dh = iw * scale, ih * scale
ox, oy = (self.width() - dw) / 2, (self.height() - dh) / 2
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
painter.drawImage(QRectF(ox, oy, dw, dh), self._qimage)
if self._overlay_enabled and self._dets:
painter.setRenderHint(QPainter.Antialiasing, True)
for i, d in enumerate(self._dets):
highlighted = i == self._highlight
# A highlighted detection is always drawn; others respect the threshold.
if not highlighted and d.score < self._threshold:
continue
dim = self._highlight is not None and not highlighted
self._draw_detection(painter, d, ox, oy, scale, highlighted, dim)
painter.end()
def _draw_detection(
self, painter: QPainter, d: Detection, ox: float, oy: float,
scale: float, highlighted: bool, dim: bool,
) -> None:
color = self._color(d.type)
width = self._cfg.line_width * (2 if highlighted else 1)
pen_color = QColor(color)
if dim:
pen_color.setAlpha(70)
painter.setPen(QPen(pen_color, width))
fill = QColor(color)
fill.setAlpha(0 if dim else (self._cfg.fill_alpha * 2 if highlighted else self._cfg.fill_alpha))
painter.setBrush(QBrush(fill))
points = d.polygon if len(d.polygon) >= 3 else self._bbox_points(d.bbox)
poly = QPolygonF([QPointF(ox + x * scale, oy + y * scale) for x, y in points])
painter.drawPolygon(poly)
if self._cfg.show_labels and not dim:
x, y, _w, _h = d.bbox
self._draw_label(painter, f"{d.type.value} {d.score:.2f}", ox + x * scale, oy + y * scale, color)
@staticmethod
def _bbox_points(bbox: tuple[int, int, int, int]) -> list[tuple[int, int]]:
x, y, w, h = bbox
return [(x, y), (x + w, y), (x + w, y + h), (x, y + h)]
def _draw_label(self, painter: QPainter, text: str, x: float, y: float, color: QColor) -> None:
font = QFont()
font.setPointSize(9)
painter.setFont(font)
metrics = painter.fontMetrics()
tw = metrics.horizontalAdvance(text) + 8
th = metrics.height() + 2
bg = QRectF(x, max(0.0, y - th), tw, th)
painter.fillRect(bg, color)
painter.setPen(QColor(0, 0, 0))
painter.drawText(bg, Qt.AlignCenter, text)