Enable exclusion drawing in PySide6 UI
This commit is contained in:
parent
f46af5a735
commit
57bb896545
|
|
@ -19,13 +19,23 @@ class Stats:
|
|||
total_all: int = 0
|
||||
matches_keep: int = 0
|
||||
total_keep: int = 0
|
||||
matches_excl: int = 0
|
||||
total_excl: int = 0
|
||||
|
||||
def summary(self, translate) -> str:
|
||||
if self.total_all == 0:
|
||||
return translate("stats.placeholder")
|
||||
with_pct = (self.matches_keep / self.total_keep * 100) if self.total_keep else 0.0
|
||||
without_pct = (self.matches_all / self.total_all * 100) if self.total_all else 0.0
|
||||
return translate("stats.summary", with_pct=with_pct, without_pct=without_pct, excluded_pct=0.0, excluded_match_pct=0.0)
|
||||
excluded_pct = (self.total_excl / self.total_all * 100) if self.total_all else 0.0
|
||||
excluded_match_pct = (self.matches_excl / self.total_excl * 100) if self.total_excl else 0.0
|
||||
return translate(
|
||||
"stats.summary",
|
||||
with_pct=with_pct,
|
||||
without_pct=without_pct,
|
||||
excluded_pct=excluded_pct,
|
||||
excluded_match_pct=excluded_match_pct,
|
||||
)
|
||||
|
||||
|
||||
class QtImageProcessor:
|
||||
|
|
@ -55,6 +65,7 @@ class QtImageProcessor:
|
|||
self.alpha = self.defaults["alpha"]
|
||||
|
||||
self.exclude_shapes: list[dict[str, object]] = []
|
||||
self.reset_exclusions_on_switch: bool = False
|
||||
|
||||
def set_defaults(self, defaults: dict) -> None:
|
||||
for key in self.defaults:
|
||||
|
|
@ -62,6 +73,7 @@ class QtImageProcessor:
|
|||
self.defaults[key] = int(defaults[key])
|
||||
for key, value in self.defaults.items():
|
||||
setattr(self, key, value)
|
||||
self._rebuild_overlay()
|
||||
|
||||
# thresholds -------------------------------------------------------------
|
||||
|
||||
|
|
@ -78,6 +90,8 @@ class QtImageProcessor:
|
|||
self.preview_paths = [path]
|
||||
self.current_index = 0
|
||||
self._build_preview()
|
||||
if self.reset_exclusions_on_switch:
|
||||
self.exclude_shapes = []
|
||||
self._rebuild_overlay()
|
||||
|
||||
def load_folder(self, paths: Iterable[Path], start_index: int = 0) -> None:
|
||||
|
|
@ -120,6 +134,7 @@ class QtImageProcessor:
|
|||
def _rebuild_overlay(self) -> None:
|
||||
if self.preview_img is None:
|
||||
self.overlay_img = None
|
||||
self.stats = Stats()
|
||||
return
|
||||
base = self.preview_img.convert("RGBA")
|
||||
overlay = Image.new("RGBA", base.size, (0, 0, 0, 0))
|
||||
|
|
@ -128,19 +143,40 @@ class QtImageProcessor:
|
|||
width, height = base.size
|
||||
highlight = (255, 0, 0, int(self.alpha))
|
||||
matches_all = total_all = 0
|
||||
matches_keep = total_keep = 0
|
||||
matches_excl = total_excl = 0
|
||||
|
||||
mask = self._build_exclusion_mask(base.size)
|
||||
mask_px = mask.load() if mask else None
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b, a = pixels[x, y]
|
||||
if a == 0:
|
||||
continue
|
||||
excluded = bool(mask_px and mask_px[x, y])
|
||||
total_all += 1
|
||||
if self._matches(r, g, b):
|
||||
draw.point((x, y), fill=highlight)
|
||||
matches_all += 1
|
||||
if excluded:
|
||||
total_excl += 1
|
||||
if self._matches(r, g, b):
|
||||
matches_excl += 1
|
||||
else:
|
||||
total_keep += 1
|
||||
if self._matches(r, g, b):
|
||||
matches_keep += 1
|
||||
|
||||
self.overlay_img = overlay
|
||||
self.stats = Stats(matches_all=matches_all, total_all=total_all, matches_keep=matches_all, total_keep=total_all)
|
||||
self.stats = Stats(
|
||||
matches_all=matches_all,
|
||||
total_all=total_all,
|
||||
matches_keep=matches_keep,
|
||||
total_keep=total_keep,
|
||||
matches_excl=matches_excl,
|
||||
total_excl=total_excl,
|
||||
)
|
||||
|
||||
# helpers ----------------------------------------------------------------
|
||||
|
||||
|
|
@ -173,3 +209,34 @@ class QtImageProcessor:
|
|||
buffer = image.tobytes("raw", "RGBA")
|
||||
qt_image = QtGui.QImage(buffer, image.width, image.height, QtGui.QImage.Format_RGBA8888)
|
||||
return QtGui.QPixmap.fromImage(qt_image)
|
||||
|
||||
# exclusions -------------------------------------------------------------
|
||||
|
||||
def set_exclusions(self, shapes: list[dict[str, object]]) -> None:
|
||||
copied: list[dict[str, object]] = []
|
||||
for shape in shapes:
|
||||
kind = shape.get("kind")
|
||||
if kind == "rect":
|
||||
coords = tuple(shape.get("coords", (0, 0, 0, 0))) # type: ignore[assignment]
|
||||
copied.append({"kind": "rect", "coords": tuple(int(c) for c in coords)})
|
||||
elif kind == "polygon":
|
||||
pts = shape.get("points", [])
|
||||
copied.append({"kind": "polygon", "points": [(int(x), int(y)) for x, y in pts]})
|
||||
self.exclude_shapes = copied
|
||||
self._rebuild_overlay()
|
||||
|
||||
def _build_exclusion_mask(self, size: Tuple[int, int]) -> Image.Image | None:
|
||||
if not self.exclude_shapes:
|
||||
return None
|
||||
mask = Image.new("L", size, 0)
|
||||
draw = ImageDraw.Draw(mask)
|
||||
for shape in self.exclude_shapes:
|
||||
kind = shape.get("kind")
|
||||
if kind == "rect":
|
||||
x0, y0, x1, y1 = shape["coords"] # type: ignore[index]
|
||||
draw.rectangle([x0, y0, x1, y1], fill=255)
|
||||
elif kind == "polygon":
|
||||
points = shape.get("points", [])
|
||||
if len(points) >= 3:
|
||||
draw.polygon(points, fill=255)
|
||||
return mask
|
||||
|
|
|
|||
|
|
@ -232,6 +232,177 @@ class SliderControl(QtWidgets.QWidget):
|
|||
)
|
||||
|
||||
|
||||
class CanvasView(QtWidgets.QGraphicsView):
|
||||
"""Interactive canvas for drawing exclusion shapes over the preview image."""
|
||||
|
||||
shapes_changed = QtCore.Signal(list)
|
||||
|
||||
def __init__(self, parent: QtWidgets.QWidget | None = None) -> None:
|
||||
super().__init__(parent)
|
||||
self.setRenderHints(QtGui.QPainter.Antialiasing | QtGui.QPainter.SmoothPixmapTransform)
|
||||
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
|
||||
self.setMouseTracking(True)
|
||||
self._scene = QtWidgets.QGraphicsScene(self)
|
||||
self.setScene(self._scene)
|
||||
|
||||
self._pixmap_item: QtWidgets.QGraphicsPixmapItem | None = None
|
||||
self._shape_items: list[QtWidgets.QGraphicsItem] = []
|
||||
self._rubber_item: QtWidgets.QGraphicsRectItem | None = None
|
||||
self._stroke_item: QtWidgets.QGraphicsPathItem | None = None
|
||||
|
||||
self.shapes: list[dict[str, object]] = []
|
||||
self.mode: str = "rect"
|
||||
self._drawing = False
|
||||
self._start_pos = QtCore.QPointF()
|
||||
self._last_pos = QtCore.QPointF()
|
||||
self._path = QtGui.QPainterPath()
|
||||
self._accent = QtGui.QColor("#ffd700")
|
||||
|
||||
def set_pixmap(self, pixmap: QtGui.QPixmap) -> None:
|
||||
self._scene.clear()
|
||||
self._shape_items.clear()
|
||||
self._pixmap_item = self._scene.addPixmap(pixmap)
|
||||
self._scene.setSceneRect(pixmap.rect())
|
||||
self.resetTransform()
|
||||
self.fitInView(self._scene.sceneRect(), QtCore.Qt.KeepAspectRatio)
|
||||
self._redraw_shapes()
|
||||
|
||||
def clear_canvas(self) -> None:
|
||||
if self._scene:
|
||||
self._scene.clear()
|
||||
self._pixmap_item = None
|
||||
self._shape_items.clear()
|
||||
self.shapes = []
|
||||
|
||||
def set_shapes(self, shapes: list[dict[str, object]]) -> None:
|
||||
self.shapes = shapes
|
||||
self._redraw_shapes()
|
||||
|
||||
def set_mode(self, mode: str) -> None:
|
||||
self.mode = mode
|
||||
|
||||
def set_accent(self, colour: str) -> None:
|
||||
self._accent = QtGui.QColor(colour)
|
||||
self._redraw_shapes()
|
||||
|
||||
def undo_last(self) -> None:
|
||||
if not self.shapes:
|
||||
return
|
||||
self.shapes.pop()
|
||||
self._redraw_shapes()
|
||||
self.shapes_changed.emit(self.shapes.copy())
|
||||
|
||||
def clear_shapes(self) -> None:
|
||||
self.shapes = []
|
||||
self._redraw_shapes()
|
||||
self.shapes_changed.emit([])
|
||||
|
||||
# event handling --------------------------------------------------------
|
||||
|
||||
def mousePressEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
if event.button() == QtCore.Qt.RightButton and self._pixmap_item:
|
||||
self._drawing = True
|
||||
scene_pos = self.mapToScene(event.position().toPoint())
|
||||
self._start_pos = self._clamp_to_image(scene_pos)
|
||||
if self.mode == "rect":
|
||||
pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
brush = QtGui.QBrush(QtCore.Qt.NoBrush)
|
||||
self._rubber_item = self._scene.addRect(QtCore.QRectF(self._start_pos, self._start_pos), pen, brush)
|
||||
else:
|
||||
pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
self._path = QtGui.QPainterPath(self._start_pos)
|
||||
self._stroke_item = self._scene.addPath(self._path, pen)
|
||||
event.accept()
|
||||
return
|
||||
super().mousePressEvent(event)
|
||||
|
||||
def mouseMoveEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
if self._drawing and self._pixmap_item:
|
||||
scene_pos = self.mapToScene(event.position().toPoint())
|
||||
self._last_pos = self._clamp_to_image(scene_pos)
|
||||
if self.mode == "rect" and self._rubber_item:
|
||||
rect = QtCore.QRectF(self._start_pos, self._last_pos).normalized()
|
||||
self._rubber_item.setRect(rect)
|
||||
elif self.mode == "free" and self._stroke_item:
|
||||
self._path.lineTo(self._last_pos)
|
||||
self._stroke_item.setPath(self._path)
|
||||
event.accept()
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None:
|
||||
if self._drawing and event.button() == QtCore.Qt.RightButton and self._pixmap_item:
|
||||
scene_pos = self.mapToScene(event.position().toPoint())
|
||||
end_pos = self._clamp_to_image(scene_pos)
|
||||
if self.mode == "rect" and self._rubber_item:
|
||||
rect = QtCore.QRectF(self._start_pos, end_pos).normalized()
|
||||
if rect.width() > 2 and rect.height() > 2:
|
||||
shape = {
|
||||
"kind": "rect",
|
||||
"coords": (
|
||||
int(rect.left()),
|
||||
int(rect.top()),
|
||||
int(rect.right()),
|
||||
int(rect.bottom()),
|
||||
),
|
||||
}
|
||||
self.shapes.append(shape)
|
||||
self._scene.removeItem(self._rubber_item)
|
||||
self._rubber_item = None
|
||||
elif self.mode == "free" and self._stroke_item:
|
||||
self._path.lineTo(self._start_pos)
|
||||
points = [(int(pt.x()), int(pt.y())) for pt in self._path.toFillPolygon()]
|
||||
if len(points) >= 3:
|
||||
shape = {"kind": "polygon", "points": points}
|
||||
self.shapes.append(shape)
|
||||
self._scene.removeItem(self._stroke_item)
|
||||
self._stroke_item = None
|
||||
self._drawing = False
|
||||
self._redraw_shapes()
|
||||
self.shapes_changed.emit(self.shapes.copy())
|
||||
event.accept()
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
# helpers ----------------------------------------------------------------
|
||||
|
||||
def _clamp_to_image(self, pos: QtCore.QPointF) -> QtCore.QPointF:
|
||||
if not self._pixmap_item:
|
||||
return pos
|
||||
pixmap = self._pixmap_item.pixmap()
|
||||
x = min(max(0.0, pos.x()), float(pixmap.width() - 1))
|
||||
y = min(max(0.0, pos.y()), float(pixmap.height() - 1))
|
||||
return QtCore.QPointF(x, y)
|
||||
|
||||
def _redraw_shapes(self) -> None:
|
||||
for item in self._shape_items:
|
||||
self._scene.removeItem(item)
|
||||
self._shape_items.clear()
|
||||
if not self._pixmap_item:
|
||||
return
|
||||
pen = QtGui.QPen(self._accent, 2, QtCore.Qt.SolidLine, QtCore.Qt.RoundCap, QtCore.Qt.RoundJoin)
|
||||
pen.setCosmetic(True)
|
||||
for shape in self.shapes:
|
||||
kind = shape.get("kind")
|
||||
if kind == "rect":
|
||||
x0, y0, x1, y1 = shape["coords"] # type: ignore[index]
|
||||
rect_item = self._scene.addRect(QtCore.QRectF(x0, y0, x1 - x0, y1 - y0), pen)
|
||||
self._shape_items.append(rect_item)
|
||||
elif kind == "polygon":
|
||||
points = shape.get("points", [])
|
||||
if len(points) < 2:
|
||||
continue
|
||||
path = QtGui.QPainterPath()
|
||||
first = QtCore.QPointF(*points[0])
|
||||
path.moveTo(first)
|
||||
for px, py in points[1:]:
|
||||
path.lineTo(px, py)
|
||||
path.closeSubpath()
|
||||
path_item = self._scene.addPath(path, pen)
|
||||
self._shape_items.append(path_item)
|
||||
|
||||
|
||||
class TitleBar(QtWidgets.QWidget):
|
||||
"""Custom title bar with native window controls."""
|
||||
|
||||
|
|
@ -385,6 +556,10 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self._toolbar_actions: Dict[str, Callable[[], None]] = {}
|
||||
self._register_default_actions()
|
||||
|
||||
self.exclude_mode = "rect"
|
||||
self.image_view.set_mode(self.exclude_mode)
|
||||
self.image_view.shapes_changed.connect(self._on_shapes_changed)
|
||||
|
||||
self._sync_sliders_from_processor()
|
||||
self._update_colour_display(DEFAULT_COLOUR, self._t("palette.current"))
|
||||
|
||||
|
|
@ -499,12 +674,12 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self.prev_button.clicked.connect(lambda: self._invoke_action("show_previous_image"))
|
||||
layout.addWidget(self.prev_button, 0, 0, QtCore.Qt.AlignVCenter)
|
||||
|
||||
self.image_view = QtWidgets.QLabel("<No image loaded>")
|
||||
self.image_view.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.image_view = CanvasView()
|
||||
layout.addWidget(self.image_view, 0, 1)
|
||||
|
||||
self.overlay_view = QtWidgets.QLabel("<No image loaded>")
|
||||
self.overlay_view.setAlignment(QtCore.Qt.AlignCenter)
|
||||
self.overlay_view.setScaledContents(True)
|
||||
layout.addWidget(self.overlay_view, 0, 2)
|
||||
|
||||
self.next_button = QtWidgets.QToolButton()
|
||||
|
|
@ -537,12 +712,12 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
self._toolbar_actions = {
|
||||
"open_image": self.open_image,
|
||||
"open_folder": self.open_folder,
|
||||
"choose_color": self._coming_soon,
|
||||
"choose_color": self.choose_colour,
|
||||
"pick_from_image": self._coming_soon,
|
||||
"save_overlay": self._coming_soon,
|
||||
"toggle_free_draw": self._coming_soon,
|
||||
"clear_excludes": self._coming_soon,
|
||||
"undo_exclude": self._coming_soon,
|
||||
"save_overlay": self.save_overlay,
|
||||
"toggle_free_draw": self.toggle_free_draw,
|
||||
"clear_excludes": self.clear_exclusions,
|
||||
"undo_exclude": self.undo_exclusion,
|
||||
"reset_sliders": self._reset_sliders,
|
||||
"toggle_theme": self.toggle_theme,
|
||||
"show_previous_image": self.show_previous_image,
|
||||
|
|
@ -568,6 +743,9 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), str(exc))
|
||||
return
|
||||
self._current_image_path = path
|
||||
if self.processor.reset_exclusions_on_switch:
|
||||
self.image_view.clear_shapes()
|
||||
self.processor.set_exclusions([])
|
||||
self._refresh_views()
|
||||
|
||||
def open_folder(self) -> None:
|
||||
|
|
@ -596,6 +774,9 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
return
|
||||
self.processor.previous_image()
|
||||
self._current_image_path = self.processor.preview_paths[self.processor.current_index]
|
||||
if self.processor.reset_exclusions_on_switch:
|
||||
self.image_view.clear_shapes()
|
||||
self.processor.set_exclusions([])
|
||||
self._refresh_views()
|
||||
|
||||
def show_next_image(self) -> None:
|
||||
|
|
@ -604,6 +785,9 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
return
|
||||
self.processor.next_image()
|
||||
self._current_image_path = self.processor.preview_paths[self.processor.current_index]
|
||||
if self.processor.reset_exclusions_on_switch:
|
||||
self.image_view.clear_shapes()
|
||||
self.processor.set_exclusions([])
|
||||
self._refresh_views()
|
||||
|
||||
# Helpers ----------------------------------------------------------------
|
||||
|
|
@ -616,16 +800,17 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
|
||||
def _on_slider_change(self, key: str, value: int) -> None:
|
||||
self.processor.set_threshold(key, value)
|
||||
formatted = self._t("status.defaults_restored")
|
||||
self.status_label.setText(f"{formatted} ({key} → {value})")
|
||||
label = self._slider_title(key)
|
||||
self.status_label.setText(f"{label}: {value}")
|
||||
self._refresh_overlay_only()
|
||||
|
||||
def _reset_sliders(self) -> None:
|
||||
for _, attr, _, _ in SLIDER_SPECS:
|
||||
control = self._slider_controls.get(attr)
|
||||
if control:
|
||||
default_value = int(getattr(self.processor, attr))
|
||||
default_value = int(self.processor.defaults.get(attr, getattr(self.processor, attr)))
|
||||
control.set_value(default_value)
|
||||
self.processor.set_threshold(attr, default_value)
|
||||
self.status_label.setText(self._t("status.defaults_restored"))
|
||||
self._refresh_overlay_only()
|
||||
|
||||
|
|
@ -636,6 +821,47 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
"Feature coming soon in the PySide6 migration.",
|
||||
)
|
||||
|
||||
def choose_colour(self) -> None:
|
||||
colour = QtWidgets.QColorDialog.getColor(parent=self, title=self._t("dialog.choose_colour_title"))
|
||||
if not colour.isValid():
|
||||
return
|
||||
hex_code = colour.name()
|
||||
self._update_colour_display(hex_code, self._t("dialog.choose_colour_title"))
|
||||
|
||||
def save_overlay(self) -> None:
|
||||
pixmap = self.processor.overlay_pixmap()
|
||||
if pixmap.isNull():
|
||||
QtWidgets.QMessageBox.information(self, self._t("dialog.info_title"), self._t("dialog.no_preview_available"))
|
||||
return
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
self,
|
||||
self._t("dialog.save_overlay_title"),
|
||||
"overlay.png",
|
||||
"PNG (*.png)",
|
||||
)
|
||||
if not filename:
|
||||
return
|
||||
if not pixmap.save(filename, "PNG"):
|
||||
QtWidgets.QMessageBox.warning(self, self._t("dialog.error_title"), self._t("dialog.image_open_failed", error="Unable to save file"))
|
||||
return
|
||||
self.status_label.setText(self._t("dialog.overlay_saved", path=filename))
|
||||
|
||||
def toggle_free_draw(self) -> None:
|
||||
self.exclude_mode = "free" if self.exclude_mode == "rect" else "rect"
|
||||
self.image_view.set_mode(self.exclude_mode)
|
||||
message_key = "status.free_draw_enabled" if self.exclude_mode == "free" else "status.free_draw_disabled"
|
||||
self.status_label.setText(self._t(message_key))
|
||||
|
||||
def clear_exclusions(self) -> None:
|
||||
self.image_view.clear_shapes()
|
||||
self.processor.set_exclusions([])
|
||||
self.status_label.setText(self._t("toolbar.clear_excludes"))
|
||||
self._refresh_overlay_only()
|
||||
|
||||
def undo_exclusion(self) -> None:
|
||||
self.image_view.undo_last()
|
||||
self.status_label.setText(self._t("toolbar.undo_exclude"))
|
||||
|
||||
def toggle_theme(self) -> None:
|
||||
self.current_theme = "light" if self.current_theme == "dark" else "dark"
|
||||
self._apply_theme(self.current_theme)
|
||||
|
|
@ -643,8 +869,13 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
def _apply_theme(self, mode: str) -> None:
|
||||
colours = THEMES[mode]
|
||||
self.content.setStyleSheet(f"background-color: {colours['window_bg']};")
|
||||
self.image_view.setStyleSheet(f"border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};")
|
||||
self.overlay_view.setStyleSheet(f"border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};")
|
||||
self.image_view.setStyleSheet(
|
||||
f"background-color: {colours['panel_bg']}; border: 1px solid {colours['border']}; border-radius: 12px;"
|
||||
)
|
||||
self.image_view.set_accent(colours["highlight"])
|
||||
self.overlay_view.setStyleSheet(
|
||||
f"background-color: {colours['panel_bg']}; border: 1px solid {colours['border']}; border-radius: 12px; color: {colours['text_dim']};"
|
||||
)
|
||||
|
||||
self.status_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
self.current_label.setStyleSheet(f"color: {colours['text_muted']}; font-weight: 500;")
|
||||
|
|
@ -676,11 +907,25 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
if control:
|
||||
control.set_value(int(getattr(self.processor, attr)))
|
||||
|
||||
def _slider_title(self, key: str) -> str:
|
||||
for title_key, attr, _, _ in SLIDER_SPECS:
|
||||
if attr == key:
|
||||
return self._t(title_key)
|
||||
return key
|
||||
|
||||
def _refresh_views(self) -> None:
|
||||
preview_pix = self.processor.preview_pixmap()
|
||||
overlay_pix = self.processor.overlay_pixmap()
|
||||
self.image_view.setPixmap(preview_pix)
|
||||
self.overlay_view.setPixmap(overlay_pix)
|
||||
if preview_pix.isNull():
|
||||
self.image_view.clear_canvas()
|
||||
self.image_view.set_shapes([])
|
||||
self.overlay_view.setText("<No image loaded>")
|
||||
self.overlay_view.setPixmap(QtGui.QPixmap())
|
||||
else:
|
||||
self.image_view.set_pixmap(preview_pix)
|
||||
self.image_view.set_shapes(self.processor.exclude_shapes.copy())
|
||||
self.overlay_view.setText("")
|
||||
self.overlay_view.setPixmap(overlay_pix)
|
||||
if self._current_image_path and self.processor.preview_img:
|
||||
width, height = self.processor.preview_img.size
|
||||
total = len(self.processor.preview_paths)
|
||||
|
|
@ -697,5 +942,15 @@ class MainWindow(QtWidgets.QMainWindow, I18nMixin):
|
|||
def _refresh_overlay_only(self) -> None:
|
||||
if self.processor.preview_img is None:
|
||||
return
|
||||
self.overlay_view.setPixmap(self.processor.overlay_pixmap())
|
||||
pix = self.processor.overlay_pixmap()
|
||||
if pix.isNull():
|
||||
self.overlay_view.setText("<No overlay>")
|
||||
self.overlay_view.setPixmap(QtGui.QPixmap())
|
||||
else:
|
||||
self.overlay_view.setText("")
|
||||
self.overlay_view.setPixmap(pix)
|
||||
self.ratio_label.setText(self.processor.stats.summary(self._t))
|
||||
|
||||
def _on_shapes_changed(self, shapes: list[dict[str, object]]) -> None:
|
||||
self.processor.set_exclusions(shapes)
|
||||
self._refresh_overlay_only()
|
||||
|
|
|
|||
Loading…
Reference in New Issue