"""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)