Refine UI layout and theme behavior
Generalize color selection, reorganize GUI, add presets, dynamic slider adjustments, config defaults, multiple UI updates etc
This commit is contained in:
parent
02255f5dee
commit
f9e8d01a70
|
|
@ -1,5 +1,5 @@
|
|||
"""Application package."""
|
||||
|
||||
from .app import PurpleTunerApp, start_app
|
||||
from .app import ColorCalcApp, start_app
|
||||
|
||||
__all__ = ["PurpleTunerApp", "start_app"]
|
||||
__all__ = ["ColorCalcApp", "start_app"]
|
||||
|
|
|
|||
10
app/app.py
10
app/app.py
|
|
@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
|
|||
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
|
||||
|
||||
|
||||
class PurpleTunerApp(
|
||||
class ColorCalcApp(
|
||||
ThemeMixin,
|
||||
UIBuilderMixin,
|
||||
ImageProcessingMixin,
|
||||
|
|
@ -16,11 +16,11 @@ class PurpleTunerApp(
|
|||
ColorPickerMixin,
|
||||
ResetMixin,
|
||||
):
|
||||
"""Tkinter based application for highlighting purple hues in images."""
|
||||
"""Tkinter based application for isolating configurable colour ranges."""
|
||||
|
||||
def __init__(self, root: tk.Tk):
|
||||
self.root = root
|
||||
self.root.title("Purple Tuner — Bild + Overlay")
|
||||
self.root.title("ColorCalc — Bild + Overlay")
|
||||
try:
|
||||
self.root.state("zoomed")
|
||||
except Exception:
|
||||
|
|
@ -66,8 +66,8 @@ class PurpleTunerApp(
|
|||
def start_app() -> None:
|
||||
"""Entry point used by the CLI script."""
|
||||
root = tk.Tk()
|
||||
app = PurpleTunerApp(root)
|
||||
app = ColorCalcApp(root)
|
||||
root.mainloop()
|
||||
|
||||
|
||||
__all__ = ["PurpleTunerApp", "start_app"]
|
||||
__all__ = ["ColorCalcApp", "start_app"]
|
||||
|
|
|
|||
|
|
@ -11,19 +11,34 @@ class ColorPickerMixin:
|
|||
"""Handles colour selection from dialogs and mouse clicks."""
|
||||
|
||||
ref_hue: float | None
|
||||
hue_span: float = 45.0 # degrees around the picked hue
|
||||
|
||||
def choose_color(self):
|
||||
rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen")
|
||||
if rgb is None:
|
||||
return
|
||||
r, g, b = [int(channel) for channel in rgb]
|
||||
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
self.ref_hue = h * 360.0
|
||||
span = 30
|
||||
self.hue_min.set((self.ref_hue - span) % 360)
|
||||
self.hue_max.set((self.ref_hue + span) % 360)
|
||||
self.update_preview()
|
||||
self.status.config(text=f"Farbe gewählt: {hex_colour} (Hue {self.ref_hue:.1f}°)")
|
||||
r, g, b = (int(round(channel)) for channel in rgb)
|
||||
hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
|
||||
label = hex_colour or f"RGB({r}, {g}, {b})"
|
||||
self.status.config(
|
||||
text=f"Farbe gewählt: {label} — Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
|
||||
)
|
||||
|
||||
def apply_sample_colour(self, hex_colour: str, name: str | None = None) -> None:
|
||||
"""Apply a predefined colour preset."""
|
||||
rgb = self._parse_hex_colour(hex_colour)
|
||||
if rgb is None:
|
||||
return
|
||||
hue_deg, sat_pct, val_pct = self._apply_rgb_selection(*rgb)
|
||||
if self.pick_mode:
|
||||
self.pick_mode = False
|
||||
label = name or hex_colour.upper()
|
||||
self.status.config(
|
||||
text=(
|
||||
f"Beispielfarbe gewählt: {label} ({hex_colour}) — "
|
||||
f"Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
|
||||
)
|
||||
)
|
||||
|
||||
def enable_pick_mode(self):
|
||||
if self.preview_img is None:
|
||||
|
|
@ -47,14 +62,62 @@ class ColorPickerMixin:
|
|||
r, g, b, a = self.preview_img.getpixel((x, y))
|
||||
if a == 0:
|
||||
return
|
||||
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
self.ref_hue = h * 360.0
|
||||
span = 30
|
||||
self.hue_min.set((self.ref_hue - span) % 360)
|
||||
self.hue_max.set((self.ref_hue + span) % 360)
|
||||
hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
|
||||
self.disable_pick_mode()
|
||||
self.status.config(
|
||||
text=f"Farbe vom Bild gewählt: Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
|
||||
)
|
||||
|
||||
def _apply_rgb_selection(self, r: int, g: int, b: int) -> tuple[float, float, float]:
|
||||
"""Update slider ranges based on an RGB colour and return HSV summary."""
|
||||
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
hue_deg = (h * 360.0) % 360.0
|
||||
self.ref_hue = hue_deg
|
||||
self._set_slider_targets(hue_deg, s, v)
|
||||
self.update_preview()
|
||||
self.status.config(text=f"Farbe vom Bild gewählt: Hue {self.ref_hue:.1f}°")
|
||||
return hue_deg, s * 100.0, v * 100.0
|
||||
|
||||
def _set_slider_targets(self, hue_deg: float, saturation: float, value: float) -> None:
|
||||
span = getattr(self, "hue_span", 45.0)
|
||||
self.hue_min.set((hue_deg - span) % 360)
|
||||
self.hue_max.set((hue_deg + span) % 360)
|
||||
|
||||
sat_pct = saturation * 100.0
|
||||
sat_margin = 35.0
|
||||
sat_min = max(0.0, min(100.0, sat_pct - sat_margin))
|
||||
if saturation <= 0.05:
|
||||
sat_min = 0.0
|
||||
self.sat_min.set(sat_min)
|
||||
|
||||
v_pct = value * 100.0
|
||||
val_margin = 35.0
|
||||
val_min = max(0.0, v_pct - val_margin)
|
||||
val_max = min(100.0, v_pct + val_margin)
|
||||
if value <= 0.15:
|
||||
val_max = min(45.0, max(val_max, 25.0))
|
||||
if value >= 0.85:
|
||||
val_min = max(55.0, min(val_min, 80.0))
|
||||
if val_max <= val_min:
|
||||
val_max = min(100.0, val_min + 10.0)
|
||||
self.val_min.set(val_min)
|
||||
self.val_max.set(val_max)
|
||||
|
||||
@staticmethod
|
||||
def _parse_hex_colour(hex_colour: str | None) -> tuple[int, int, int] | None:
|
||||
if not hex_colour:
|
||||
return None
|
||||
value = hex_colour.strip().lstrip("#")
|
||||
if len(value) == 3:
|
||||
value = "".join(ch * 2 for ch in value)
|
||||
if len(value) != 6:
|
||||
return None
|
||||
try:
|
||||
r = int(value[0:2], 16)
|
||||
g = int(value[2:4], 16)
|
||||
b = int(value[4:6], 16)
|
||||
except ValueError:
|
||||
return None
|
||||
return r, g, b
|
||||
|
||||
|
||||
__all__ = ["ColorPickerMixin"]
|
||||
|
|
|
|||
|
|
@ -57,7 +57,12 @@ class ThemeMixin:
|
|||
else:
|
||||
self.scale_style = "Horizontal.TScale"
|
||||
|
||||
bg, fg = ("#0f0f10", "#f1f1f1") if self.theme == "dark" else ("#f2f2f7", "#202020")
|
||||
if self.theme == "dark":
|
||||
bg, fg = "#0f0f10", "#f1f1f1"
|
||||
status_fg = "#f5f5f5"
|
||||
else:
|
||||
bg, fg = "#ededf2", "#202020"
|
||||
status_fg = "#1c1c1c"
|
||||
self.root.configure(bg=bg) # type: ignore[attr-defined]
|
||||
|
||||
s = self.style
|
||||
|
|
@ -69,6 +74,14 @@ class ThemeMixin:
|
|||
)
|
||||
s.map("TButton", background=[("active", "#d0d0d0")])
|
||||
|
||||
button_refresher = getattr(self, "_refresh_toolbar_buttons_theme", None)
|
||||
if callable(button_refresher):
|
||||
button_refresher()
|
||||
|
||||
status_refresher = getattr(self, "_refresh_status_palette", None)
|
||||
if callable(status_refresher) and hasattr(self, "status"):
|
||||
status_refresher(status_fg)
|
||||
|
||||
def detect_system_theme(self) -> str:
|
||||
"""Best-effort detection of the OS theme preference."""
|
||||
try:
|
||||
|
|
|
|||
289
app/gui/ui.py
289
app/gui/ui.py
|
|
@ -3,10 +3,9 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import tkinter as tk
|
||||
import tkinter.font as tkfont
|
||||
from tkinter import ttk
|
||||
|
||||
from .theme import HAS_TTKBOOTSTRAP
|
||||
|
||||
|
||||
class UIBuilderMixin:
|
||||
"""Constructs the Tkinter UI and common widgets."""
|
||||
|
|
@ -24,13 +23,33 @@ class UIBuilderMixin:
|
|||
("🔄 Slider zurücksetzen", self.reset_sliders),
|
||||
("🌓 Theme umschalten", self.toggle_theme),
|
||||
]
|
||||
for text, command in buttons:
|
||||
if HAS_TTKBOOTSTRAP:
|
||||
from ttkbootstrap import Button # type: ignore
|
||||
self._toolbar_buttons: list[dict[str, object]] = []
|
||||
|
||||
Button(toolbar, text=text, command=command, bootstyle="secondary").pack(side=tk.LEFT, padx=6)
|
||||
else:
|
||||
ttk.Button(toolbar, text=text, command=command).pack(side=tk.LEFT, padx=6)
|
||||
buttons_frame = ttk.Frame(toolbar)
|
||||
buttons_frame.pack(side=tk.LEFT)
|
||||
for text, command in buttons:
|
||||
self._add_toolbar_button(buttons_frame, text, command)
|
||||
|
||||
status_container = ttk.Frame(toolbar)
|
||||
status_container.pack(side=tk.RIGHT, expand=True, fill=tk.X)
|
||||
self.status = ttk.Label(
|
||||
status_container,
|
||||
text="Keine Datei geladen.",
|
||||
anchor="e",
|
||||
foreground="#efefef",
|
||||
)
|
||||
self.status.pack(fill=tk.X)
|
||||
self._attach_copy_menu(self.status)
|
||||
self.status_default_text = self.status.cget("text")
|
||||
self._status_palette = {"fg": self.status.cget("foreground")}
|
||||
|
||||
palette_frame = ttk.Frame(self.root)
|
||||
palette_frame.pack(fill=tk.X, padx=12, pady=(0, 8))
|
||||
ttk.Label(palette_frame, text="Beispielfarben:").pack(side=tk.LEFT, padx=(0, 8))
|
||||
swatch_container = ttk.Frame(palette_frame)
|
||||
swatch_container.pack(side=tk.LEFT)
|
||||
for name, hex_code in self._preset_colours():
|
||||
self._add_palette_swatch(swatch_container, name, hex_code)
|
||||
|
||||
sliders_frame = ttk.Frame(self.root)
|
||||
sliders_frame.pack(fill=tk.X, padx=12, pady=4)
|
||||
|
|
@ -49,30 +68,45 @@ class UIBuilderMixin:
|
|||
main = ttk.Frame(self.root)
|
||||
main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)
|
||||
|
||||
self.canvas_orig = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat")
|
||||
self.canvas_orig.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
|
||||
left_column = ttk.Frame(main)
|
||||
left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
|
||||
|
||||
self.canvas_orig = tk.Canvas(left_column, bg="#1e1e1e", highlightthickness=0, relief="flat")
|
||||
self.canvas_orig.pack(fill=tk.BOTH, expand=True)
|
||||
self.canvas_orig.bind("<Button-1>", self.on_canvas_click)
|
||||
self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
|
||||
self.canvas_orig.bind("<B3-Motion>", self._exclude_drag)
|
||||
self.canvas_orig.bind("<ButtonRelease-3>", self._exclude_end)
|
||||
|
||||
self.canvas_overlay = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat")
|
||||
self.canvas_overlay.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
||||
right_column = ttk.Frame(main)
|
||||
right_column.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
||||
|
||||
status_frame = ttk.Frame(self.root)
|
||||
status_frame.pack(fill=tk.X, padx=12, pady=6)
|
||||
self.status = ttk.Label(status_frame, text="Keine Datei geladen.")
|
||||
self.status.pack(anchor="w")
|
||||
self._attach_copy_menu(self.status)
|
||||
self.ratio_label = ttk.Label(status_frame, text="Purple (mit Excludes): —")
|
||||
self.ratio_label.pack(anchor="w")
|
||||
self._attach_copy_menu(self.ratio_label)
|
||||
self.hint = ttk.Label(
|
||||
status_frame,
|
||||
text="Tipp: Rechtsklick + Ziehen auf dem linken Bild, um Bereiche auszuschließen. Esc beendet den Pick-Modus.",
|
||||
self.canvas_overlay = tk.Canvas(right_column, bg="#1e1e1e", highlightthickness=0, relief="flat")
|
||||
self.canvas_overlay.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
info_frame = ttk.Frame(self.root)
|
||||
info_frame.pack(fill=tk.X, padx=12, pady=(0, 12))
|
||||
self.filename_label = ttk.Label(
|
||||
info_frame,
|
||||
text="—",
|
||||
foreground="#f2c744",
|
||||
font=("Segoe UI", 10, "bold"),
|
||||
anchor="center",
|
||||
justify="center",
|
||||
)
|
||||
self.hint.pack(anchor="w", pady=(2, 0))
|
||||
self._attach_copy_menu(self.hint)
|
||||
self.filename_label.pack(anchor="center")
|
||||
self._attach_copy_menu(self.filename_label)
|
||||
|
||||
self.ratio_label = ttk.Label(
|
||||
info_frame,
|
||||
text="Treffer (mit Excludes): —",
|
||||
foreground="#f2c744",
|
||||
font=("Segoe UI", 10, "bold"),
|
||||
anchor="center",
|
||||
justify="center",
|
||||
)
|
||||
self.ratio_label.pack(anchor="center", pady=(4, 0))
|
||||
self._attach_copy_menu(self.ratio_label)
|
||||
|
||||
self.root.bind("<Escape>", self.disable_pick_mode)
|
||||
|
||||
|
|
@ -114,6 +148,211 @@ class UIBuilderMixin:
|
|||
pass
|
||||
self._update_job = self.root.after(self.update_delay_ms, self.update_preview)
|
||||
|
||||
def _preset_colours(self):
|
||||
return [
|
||||
("Rot", "#ff3b30"),
|
||||
("Orange", "#ff9500"),
|
||||
("Gelb", "#ffd60a"),
|
||||
("Grün", "#34c759"),
|
||||
("Türkis", "#5ac8fa"),
|
||||
("Blau", "#0a84ff"),
|
||||
("Violett", "#af52de"),
|
||||
("Magenta", "#ff2d55"),
|
||||
("Weiß", "#ffffff"),
|
||||
("Grau", "#8e8e93"),
|
||||
("Schwarz", "#000000"),
|
||||
]
|
||||
|
||||
def _add_palette_swatch(self, parent, name: str, hex_code: str) -> None:
|
||||
swatch = tk.Canvas(
|
||||
parent,
|
||||
width=26,
|
||||
height=26,
|
||||
highlightthickness=1,
|
||||
highlightbackground="#b1b1b6",
|
||||
background="#ffffff",
|
||||
bd=0,
|
||||
relief="flat",
|
||||
takefocus=1,
|
||||
cursor="hand2",
|
||||
)
|
||||
swatch.pack(side=tk.LEFT, padx=4, pady=2)
|
||||
swatch.create_rectangle(3, 3, 23, 23, outline="#4a4a4a", fill=hex_code)
|
||||
|
||||
def trigger(_event=None, colour=hex_code, label=name):
|
||||
self.apply_sample_colour(colour, label)
|
||||
|
||||
swatch.bind("<Button-1>", trigger)
|
||||
swatch.bind("<space>", trigger)
|
||||
swatch.bind("<Return>", trigger)
|
||||
swatch.bind("<Enter>", lambda _e: swatch.configure(highlightbackground="#71717a"))
|
||||
swatch.bind("<Leave>", lambda _e: swatch.configure(highlightbackground="#b1b1b6"))
|
||||
|
||||
def _add_toolbar_button(self, parent, text: str, command) -> None:
|
||||
font = tkfont.Font(root=self.root, family="Segoe UI", size=10, weight="bold")
|
||||
padding_x = 18
|
||||
width = font.measure(text) + padding_x * 2
|
||||
height = 32
|
||||
radius = 12
|
||||
bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
|
||||
canvas = tk.Canvas(
|
||||
parent,
|
||||
width=width,
|
||||
height=height,
|
||||
bd=0,
|
||||
highlightthickness=0,
|
||||
bg=bg,
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
takefocus=1,
|
||||
)
|
||||
canvas.pack(side=tk.LEFT, padx=6)
|
||||
|
||||
palette = self._toolbar_palette()
|
||||
rect_id = self._create_round_rect(
|
||||
canvas,
|
||||
1,
|
||||
1,
|
||||
width - 1,
|
||||
height - 1,
|
||||
radius,
|
||||
fill=palette["normal"],
|
||||
outline=palette["outline"],
|
||||
width=1,
|
||||
)
|
||||
text_id = canvas.create_text(
|
||||
width / 2,
|
||||
height / 2,
|
||||
text=text,
|
||||
font=font,
|
||||
fill=palette["text"],
|
||||
)
|
||||
|
||||
button_data = {
|
||||
"canvas": canvas,
|
||||
"rect": rect_id,
|
||||
"text_id": text_id,
|
||||
"command": command,
|
||||
"palette": palette.copy(),
|
||||
"dimensions": (width, height, radius),
|
||||
}
|
||||
self._toolbar_buttons.append(button_data)
|
||||
|
||||
def set_fill(state: str) -> None:
|
||||
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
||||
canvas.itemconfigure(rect_id, fill=pal[state]) # type: ignore[index]
|
||||
|
||||
def execute():
|
||||
command()
|
||||
|
||||
def on_press(_event=None):
|
||||
set_fill("active")
|
||||
|
||||
def on_release(event=None):
|
||||
if event is not None and (
|
||||
event.x < 0 or event.y < 0 or event.x > width or event.y > height
|
||||
):
|
||||
set_fill("normal")
|
||||
return
|
||||
set_fill("hover")
|
||||
self.root.after_idle(execute)
|
||||
|
||||
def on_enter(_event):
|
||||
set_fill("hover")
|
||||
|
||||
def on_leave(_event):
|
||||
set_fill("normal")
|
||||
|
||||
def on_focus_in(_event):
|
||||
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
||||
canvas.itemconfigure(rect_id, outline=pal["outline_focus"]) # type: ignore[index]
|
||||
|
||||
def on_focus_out(_event):
|
||||
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
||||
canvas.itemconfigure(rect_id, outline=pal["outline"]) # type: ignore[index]
|
||||
|
||||
def invoke_keyboard(_event=None):
|
||||
set_fill("active")
|
||||
canvas.after(120, lambda: set_fill("hover"))
|
||||
self.root.after_idle(execute)
|
||||
|
||||
canvas.bind("<ButtonPress-1>", on_press)
|
||||
canvas.bind("<ButtonRelease-1>", on_release)
|
||||
canvas.bind("<Enter>", on_enter)
|
||||
canvas.bind("<Leave>", on_leave)
|
||||
canvas.bind("<FocusIn>", on_focus_in)
|
||||
canvas.bind("<FocusOut>", on_focus_out)
|
||||
canvas.bind("<space>", invoke_keyboard)
|
||||
canvas.bind("<Return>", invoke_keyboard)
|
||||
|
||||
@staticmethod
|
||||
def _create_round_rect(canvas: tk.Canvas, x1, y1, x2, y2, radius, **kwargs):
|
||||
points = [
|
||||
x1 + radius,
|
||||
y1,
|
||||
x2 - radius,
|
||||
y1,
|
||||
x2,
|
||||
y1,
|
||||
x2,
|
||||
y1 + radius,
|
||||
x2,
|
||||
y2 - radius,
|
||||
x2,
|
||||
y2,
|
||||
x2 - radius,
|
||||
y2,
|
||||
x1 + radius,
|
||||
y2,
|
||||
x1,
|
||||
y2,
|
||||
x1,
|
||||
y2 - radius,
|
||||
x1,
|
||||
y1 + radius,
|
||||
x1,
|
||||
y1,
|
||||
]
|
||||
return canvas.create_polygon(points, smooth=True, splinesteps=24, **kwargs)
|
||||
|
||||
def _toolbar_palette(self) -> dict[str, str]:
|
||||
is_dark = getattr(self, "theme", "light") == "dark"
|
||||
if is_dark:
|
||||
return {
|
||||
"normal": "#2f2f35",
|
||||
"hover": "#3a3a40",
|
||||
"active": "#1f1f25",
|
||||
"outline": "#4d4d50",
|
||||
"outline_focus": "#7c7c88",
|
||||
"text": "#f1f1f5",
|
||||
}
|
||||
return {
|
||||
"normal": "#ffffff",
|
||||
"hover": "#ededf4",
|
||||
"active": "#dcdce6",
|
||||
"outline": "#d0d0d8",
|
||||
"outline_focus": "#a9a9b2",
|
||||
"text": "#1f1f1f",
|
||||
}
|
||||
|
||||
def _refresh_toolbar_buttons_theme(self) -> None:
|
||||
if not getattr(self, "_toolbar_buttons", None):
|
||||
return
|
||||
bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
|
||||
palette = self._toolbar_palette()
|
||||
for data in self._toolbar_buttons:
|
||||
canvas = data["canvas"] # type: ignore[index]
|
||||
rect_id = data["rect"] # type: ignore[index]
|
||||
text_id = data["text_id"] # type: ignore[index]
|
||||
data["palette"] = palette.copy()
|
||||
canvas.configure(bg=bg)
|
||||
canvas.itemconfigure(rect_id, fill=palette["normal"], outline=palette["outline"])
|
||||
canvas.itemconfigure(text_id, fill=palette["text"])
|
||||
|
||||
def _refresh_status_palette(self, fg: str) -> None:
|
||||
self.status.configure(foreground=fg)
|
||||
self._status_palette["fg"] = fg
|
||||
|
||||
def _init_copy_menu(self):
|
||||
self._copy_target = None
|
||||
self.copy_menu = tk.Menu(self.root, tearoff=0)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ except ModuleNotFoundError: # pragma: no cover - fallback for <3.11
|
|||
if "tomllib" not in globals():
|
||||
tomllib = None # type: ignore[assignment]
|
||||
|
||||
PREVIEW_MAX_SIZE = (1200, 800)
|
||||
PREVIEW_MAX_SIZE = (900, 660)
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parents[2]
|
||||
IMAGES_DIR = BASE_DIR / "images"
|
||||
|
|
|
|||
|
|
@ -40,9 +40,12 @@ class ImageProcessingMixin:
|
|||
self.orig_img = image
|
||||
self.prepare_preview()
|
||||
self.update_preview()
|
||||
self.status.config(
|
||||
text=f"Geladen: {self.image_path.name} — {self.orig_img.width}x{self.orig_img.height}"
|
||||
)
|
||||
dimensions = f"{self.orig_img.width}x{self.orig_img.height}"
|
||||
status_text = f"Geladen: {self.image_path.name} — {dimensions}"
|
||||
self.status.config(text=status_text)
|
||||
self.status_default_text = status_text
|
||||
if hasattr(self, "filename_label"):
|
||||
self.filename_label.config(text=f"{self.image_path.name} — {dimensions}")
|
||||
|
||||
def save_overlay(self) -> None:
|
||||
if self.orig_img is None:
|
||||
|
|
@ -57,7 +60,7 @@ class ImageProcessingMixin:
|
|||
self.exclude_rects,
|
||||
alpha=int(self.alpha.get()),
|
||||
scale_from_preview=self.preview_img.size,
|
||||
is_purple_fn=self.is_purple_pixel,
|
||||
is_match_fn=self.matches_target_color,
|
||||
)
|
||||
merged = Image.alpha_composite(self.orig_img.convert("RGBA"), overlay)
|
||||
|
||||
|
|
@ -100,18 +103,18 @@ class ImageProcessingMixin:
|
|||
|
||||
stats = self.compute_stats_preview()
|
||||
if stats:
|
||||
p_all, t_all = stats["all"]
|
||||
p_keep, t_keep = stats["keep"]
|
||||
p_ex, t_ex = stats["excl"]
|
||||
r_with = (p_keep / t_keep * 100) if t_keep else 0.0
|
||||
r_no = (p_all / t_all * 100) if t_all else 0.0
|
||||
excl_share = (t_ex / t_all * 100) if t_all else 0.0
|
||||
excl_purp = (p_ex / t_ex * 100) if t_ex else 0.0
|
||||
matches_all, total_all = stats["all"]
|
||||
matches_keep, total_keep = stats["keep"]
|
||||
matches_ex, total_ex = stats["excl"]
|
||||
r_with = (matches_keep / total_keep * 100) if total_keep else 0.0
|
||||
r_no = (matches_all / total_all * 100) if total_all else 0.0
|
||||
excl_share = (total_ex / total_all * 100) if total_all else 0.0
|
||||
excl_match = (matches_ex / total_ex * 100) if total_ex else 0.0
|
||||
self.ratio_label.config(
|
||||
text=(
|
||||
f"Purple (mit Excludes): {r_with:.2f}% | "
|
||||
f"Purple (ohne Excludes): {r_no:.2f}% | "
|
||||
f"Excluded: {excl_share:.2f}% vom Bild, davon {excl_purp:.2f}% purple"
|
||||
f"Treffer (mit Excludes): {r_with:.2f}% | "
|
||||
f"Treffer (ohne Excludes): {r_no:.2f}% | "
|
||||
f"Excluded: {excl_share:.2f}% vom Bild, davon {excl_match:.2f}% Treffer"
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -135,7 +138,7 @@ class ImageProcessingMixin:
|
|||
r, g, b, a = pixels[x, y]
|
||||
if a == 0:
|
||||
continue
|
||||
if self.is_purple_pixel(r, g, b):
|
||||
if self.matches_target_color(r, g, b):
|
||||
draw.point((x, y), fill=(255, 0, 0, alpha))
|
||||
merged = Image.alpha_composite(base, overlay)
|
||||
for (x0, y0, x1, y1) in self.exclude_rects:
|
||||
|
|
@ -147,9 +150,9 @@ class ImageProcessingMixin:
|
|||
return None
|
||||
px = self.preview_img.convert("RGBA").load()
|
||||
width, height = self.preview_img.size
|
||||
purple_all = total_all = 0
|
||||
purple_keep = total_keep = 0
|
||||
purple_excl = total_excl = 0
|
||||
matches_all = total_all = 0
|
||||
matches_keep = total_keep = 0
|
||||
matches_excl = total_excl = 0
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
r, g, b, a = px[x, y]
|
||||
|
|
@ -157,19 +160,23 @@ class ImageProcessingMixin:
|
|||
continue
|
||||
is_excluded = self._is_excluded(x, y)
|
||||
total_all += 1
|
||||
if self.is_purple_pixel(r, g, b):
|
||||
purple_all += 1
|
||||
if self.matches_target_color(r, g, b):
|
||||
matches_all += 1
|
||||
if not is_excluded:
|
||||
total_keep += 1
|
||||
if self.is_purple_pixel(r, g, b):
|
||||
purple_keep += 1
|
||||
if self.matches_target_color(r, g, b):
|
||||
matches_keep += 1
|
||||
else:
|
||||
total_excl += 1
|
||||
if self.is_purple_pixel(r, g, b):
|
||||
purple_excl += 1
|
||||
return {"all": (purple_all, total_all), "keep": (purple_keep, total_keep), "excl": (purple_excl, total_excl)}
|
||||
if self.matches_target_color(r, g, b):
|
||||
matches_excl += 1
|
||||
return {
|
||||
"all": (matches_all, total_all),
|
||||
"keep": (matches_keep, total_keep),
|
||||
"excl": (matches_excl, total_excl),
|
||||
}
|
||||
|
||||
def is_purple_pixel(self, r, g, b) -> bool:
|
||||
def matches_target_color(self, r, g, b) -> bool:
|
||||
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
|
||||
hue = h * 360.0
|
||||
hmin = float(self.hue_min.get())
|
||||
|
|
@ -209,7 +216,7 @@ class ImageProcessingMixin:
|
|||
*,
|
||||
alpha: int,
|
||||
scale_from_preview: Tuple[int, int],
|
||||
is_purple_fn,
|
||||
is_match_fn,
|
||||
) -> Image.Image:
|
||||
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(overlay)
|
||||
|
|
@ -223,7 +230,7 @@ class ImageProcessingMixin:
|
|||
r, g, b, a = pixels[x, y]
|
||||
if a == 0:
|
||||
continue
|
||||
if is_purple_fn(r, g, b):
|
||||
if is_match_fn(r, g, b):
|
||||
draw.point((x, y), fill=(255, 0, 0, alpha))
|
||||
return overlay
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ class ResetMixin:
|
|||
self.val_max.set(self.DEFAULTS["val_max"])
|
||||
self.alpha.set(self.DEFAULTS["alpha"])
|
||||
self.update_preview()
|
||||
default_text = getattr(self, "status_default_text", "Standardwerte aktiv.")
|
||||
if hasattr(self, "status"):
|
||||
self.status.config(text=default_text)
|
||||
|
||||
|
||||
__all__ = ["ResetMixin"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue