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:
lm 2025-10-15 20:18:21 +02:00
parent 02255f5dee
commit f9e8d01a70
8 changed files with 401 additions and 76 deletions

View File

@ -1,5 +1,5 @@
"""Application package.""" """Application package."""
from .app import PurpleTunerApp, start_app from .app import ColorCalcApp, start_app
__all__ = ["PurpleTunerApp", "start_app"] __all__ = ["ColorCalcApp", "start_app"]

View File

@ -8,7 +8,7 @@ from .gui import ColorPickerMixin, ExclusionMixin, ThemeMixin, UIBuilderMixin
from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin from .logic import DEFAULTS, ImageProcessingMixin, ResetMixin
class PurpleTunerApp( class ColorCalcApp(
ThemeMixin, ThemeMixin,
UIBuilderMixin, UIBuilderMixin,
ImageProcessingMixin, ImageProcessingMixin,
@ -16,11 +16,11 @@ class PurpleTunerApp(
ColorPickerMixin, ColorPickerMixin,
ResetMixin, ResetMixin,
): ):
"""Tkinter based application for highlighting purple hues in images.""" """Tkinter based application for isolating configurable colour ranges."""
def __init__(self, root: tk.Tk): def __init__(self, root: tk.Tk):
self.root = root self.root = root
self.root.title("Purple Tuner — Bild + Overlay") self.root.title("ColorCalc — Bild + Overlay")
try: try:
self.root.state("zoomed") self.root.state("zoomed")
except Exception: except Exception:
@ -66,8 +66,8 @@ class PurpleTunerApp(
def start_app() -> None: def start_app() -> None:
"""Entry point used by the CLI script.""" """Entry point used by the CLI script."""
root = tk.Tk() root = tk.Tk()
app = PurpleTunerApp(root) app = ColorCalcApp(root)
root.mainloop() root.mainloop()
__all__ = ["PurpleTunerApp", "start_app"] __all__ = ["ColorCalcApp", "start_app"]

View File

@ -11,19 +11,34 @@ class ColorPickerMixin:
"""Handles colour selection from dialogs and mouse clicks.""" """Handles colour selection from dialogs and mouse clicks."""
ref_hue: float | None ref_hue: float | None
hue_span: float = 45.0 # degrees around the picked hue
def choose_color(self): def choose_color(self):
rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen") rgb, hex_colour = colorchooser.askcolor(title="Farbe wählen")
if rgb is None: if rgb is None:
return return
r, g, b = [int(channel) for channel in rgb] r, g, b = (int(round(channel)) for channel in rgb)
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
self.ref_hue = h * 360.0 label = hex_colour or f"RGB({r}, {g}, {b})"
span = 30 self.status.config(
self.hue_min.set((self.ref_hue - span) % 360) text=f"Farbe gewählt: {label} — Hue {hue_deg:.1f}°, S {sat_pct:.0f}%, V {val_pct:.0f}%"
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}°)") 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): def enable_pick_mode(self):
if self.preview_img is None: if self.preview_img is None:
@ -47,14 +62,62 @@ class ColorPickerMixin:
r, g, b, a = self.preview_img.getpixel((x, y)) r, g, b, a = self.preview_img.getpixel((x, y))
if a == 0: if a == 0:
return return
h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0) hue_deg, sat_pct, val_pct = self._apply_rgb_selection(r, g, b)
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.disable_pick_mode() 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.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"] __all__ = ["ColorPickerMixin"]

View File

@ -57,7 +57,12 @@ class ThemeMixin:
else: else:
self.scale_style = "Horizontal.TScale" 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] self.root.configure(bg=bg) # type: ignore[attr-defined]
s = self.style s = self.style
@ -69,6 +74,14 @@ class ThemeMixin:
) )
s.map("TButton", background=[("active", "#d0d0d0")]) 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: def detect_system_theme(self) -> str:
"""Best-effort detection of the OS theme preference.""" """Best-effort detection of the OS theme preference."""
try: try:

View File

@ -3,10 +3,9 @@
from __future__ import annotations from __future__ import annotations
import tkinter as tk import tkinter as tk
import tkinter.font as tkfont
from tkinter import ttk from tkinter import ttk
from .theme import HAS_TTKBOOTSTRAP
class UIBuilderMixin: class UIBuilderMixin:
"""Constructs the Tkinter UI and common widgets.""" """Constructs the Tkinter UI and common widgets."""
@ -24,13 +23,33 @@ class UIBuilderMixin:
("🔄 Slider zurücksetzen", self.reset_sliders), ("🔄 Slider zurücksetzen", self.reset_sliders),
("🌓 Theme umschalten", self.toggle_theme), ("🌓 Theme umschalten", self.toggle_theme),
] ]
for text, command in buttons: self._toolbar_buttons: list[dict[str, object]] = []
if HAS_TTKBOOTSTRAP:
from ttkbootstrap import Button # type: ignore
Button(toolbar, text=text, command=command, bootstyle="secondary").pack(side=tk.LEFT, padx=6) buttons_frame = ttk.Frame(toolbar)
else: buttons_frame.pack(side=tk.LEFT)
ttk.Button(toolbar, text=text, command=command).pack(side=tk.LEFT, padx=6) 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 = ttk.Frame(self.root)
sliders_frame.pack(fill=tk.X, padx=12, pady=4) sliders_frame.pack(fill=tk.X, padx=12, pady=4)
@ -49,30 +68,45 @@ class UIBuilderMixin:
main = ttk.Frame(self.root) main = ttk.Frame(self.root)
main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12) main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)
self.canvas_orig = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat") left_column = ttk.Frame(main)
self.canvas_orig.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6)) 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("<Button-1>", self.on_canvas_click)
self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start) self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
self.canvas_orig.bind("<B3-Motion>", self._exclude_drag) self.canvas_orig.bind("<B3-Motion>", self._exclude_drag)
self.canvas_orig.bind("<ButtonRelease-3>", self._exclude_end) self.canvas_orig.bind("<ButtonRelease-3>", self._exclude_end)
self.canvas_overlay = tk.Canvas(main, bg="#1e1e1e", highlightthickness=0, relief="flat") right_column = ttk.Frame(main)
self.canvas_overlay.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0)) right_column.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
status_frame = ttk.Frame(self.root) self.canvas_overlay = tk.Canvas(right_column, bg="#1e1e1e", highlightthickness=0, relief="flat")
status_frame.pack(fill=tk.X, padx=12, pady=6) self.canvas_overlay.pack(fill=tk.BOTH, expand=True)
self.status = ttk.Label(status_frame, text="Keine Datei geladen.")
self.status.pack(anchor="w") info_frame = ttk.Frame(self.root)
self._attach_copy_menu(self.status) info_frame.pack(fill=tk.X, padx=12, pady=(0, 12))
self.ratio_label = ttk.Label(status_frame, text="Purple (mit Excludes): —") self.filename_label = ttk.Label(
self.ratio_label.pack(anchor="w") info_frame,
self._attach_copy_menu(self.ratio_label) text="",
self.hint = ttk.Label( foreground="#f2c744",
status_frame, font=("Segoe UI", 10, "bold"),
text="Tipp: Rechtsklick + Ziehen auf dem linken Bild, um Bereiche auszuschließen. Esc beendet den Pick-Modus.", anchor="center",
justify="center",
) )
self.hint.pack(anchor="w", pady=(2, 0)) self.filename_label.pack(anchor="center")
self._attach_copy_menu(self.hint) 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) self.root.bind("<Escape>", self.disable_pick_mode)
@ -114,6 +148,211 @@ class UIBuilderMixin:
pass pass
self._update_job = self.root.after(self.update_delay_ms, self.update_preview) 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): def _init_copy_menu(self):
self._copy_target = None self._copy_target = None
self.copy_menu = tk.Menu(self.root, tearoff=0) self.copy_menu = tk.Menu(self.root, tearoff=0)

View File

@ -14,7 +14,7 @@ except ModuleNotFoundError: # pragma: no cover - fallback for <3.11
if "tomllib" not in globals(): if "tomllib" not in globals():
tomllib = None # type: ignore[assignment] tomllib = None # type: ignore[assignment]
PREVIEW_MAX_SIZE = (1200, 800) PREVIEW_MAX_SIZE = (900, 660)
BASE_DIR = Path(__file__).resolve().parents[2] BASE_DIR = Path(__file__).resolve().parents[2]
IMAGES_DIR = BASE_DIR / "images" IMAGES_DIR = BASE_DIR / "images"

View File

@ -40,9 +40,12 @@ class ImageProcessingMixin:
self.orig_img = image self.orig_img = image
self.prepare_preview() self.prepare_preview()
self.update_preview() self.update_preview()
self.status.config( dimensions = f"{self.orig_img.width}x{self.orig_img.height}"
text=f"Geladen: {self.image_path.name}{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: def save_overlay(self) -> None:
if self.orig_img is None: if self.orig_img is None:
@ -57,7 +60,7 @@ class ImageProcessingMixin:
self.exclude_rects, self.exclude_rects,
alpha=int(self.alpha.get()), alpha=int(self.alpha.get()),
scale_from_preview=self.preview_img.size, 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) merged = Image.alpha_composite(self.orig_img.convert("RGBA"), overlay)
@ -100,18 +103,18 @@ class ImageProcessingMixin:
stats = self.compute_stats_preview() stats = self.compute_stats_preview()
if stats: if stats:
p_all, t_all = stats["all"] matches_all, total_all = stats["all"]
p_keep, t_keep = stats["keep"] matches_keep, total_keep = stats["keep"]
p_ex, t_ex = stats["excl"] matches_ex, total_ex = stats["excl"]
r_with = (p_keep / t_keep * 100) if t_keep else 0.0 r_with = (matches_keep / total_keep * 100) if total_keep else 0.0
r_no = (p_all / t_all * 100) if t_all else 0.0 r_no = (matches_all / total_all * 100) if total_all else 0.0
excl_share = (t_ex / t_all * 100) if t_all else 0.0 excl_share = (total_ex / total_all * 100) if total_all else 0.0
excl_purp = (p_ex / t_ex * 100) if t_ex else 0.0 excl_match = (matches_ex / total_ex * 100) if total_ex else 0.0
self.ratio_label.config( self.ratio_label.config(
text=( text=(
f"Purple (mit Excludes): {r_with:.2f}% | " f"Treffer (mit Excludes): {r_with:.2f}% | "
f"Purple (ohne Excludes): {r_no:.2f}% | " f"Treffer (ohne Excludes): {r_no:.2f}% | "
f"Excluded: {excl_share:.2f}% vom Bild, davon {excl_purp:.2f}% purple" 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] r, g, b, a = pixels[x, y]
if a == 0: if a == 0:
continue 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)) draw.point((x, y), fill=(255, 0, 0, alpha))
merged = Image.alpha_composite(base, overlay) merged = Image.alpha_composite(base, overlay)
for (x0, y0, x1, y1) in self.exclude_rects: for (x0, y0, x1, y1) in self.exclude_rects:
@ -147,9 +150,9 @@ class ImageProcessingMixin:
return None return None
px = self.preview_img.convert("RGBA").load() px = self.preview_img.convert("RGBA").load()
width, height = self.preview_img.size width, height = self.preview_img.size
purple_all = total_all = 0 matches_all = total_all = 0
purple_keep = total_keep = 0 matches_keep = total_keep = 0
purple_excl = total_excl = 0 matches_excl = total_excl = 0
for y in range(height): for y in range(height):
for x in range(width): for x in range(width):
r, g, b, a = px[x, y] r, g, b, a = px[x, y]
@ -157,19 +160,23 @@ class ImageProcessingMixin:
continue continue
is_excluded = self._is_excluded(x, y) is_excluded = self._is_excluded(x, y)
total_all += 1 total_all += 1
if self.is_purple_pixel(r, g, b): if self.matches_target_color(r, g, b):
purple_all += 1 matches_all += 1
if not is_excluded: if not is_excluded:
total_keep += 1 total_keep += 1
if self.is_purple_pixel(r, g, b): if self.matches_target_color(r, g, b):
purple_keep += 1 matches_keep += 1
else: else:
total_excl += 1 total_excl += 1
if self.is_purple_pixel(r, g, b): if self.matches_target_color(r, g, b):
purple_excl += 1 matches_excl += 1
return {"all": (purple_all, total_all), "keep": (purple_keep, total_keep), "excl": (purple_excl, total_excl)} 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) h, s, v = colorsys.rgb_to_hsv(r / 255.0, g / 255.0, b / 255.0)
hue = h * 360.0 hue = h * 360.0
hmin = float(self.hue_min.get()) hmin = float(self.hue_min.get())
@ -209,7 +216,7 @@ class ImageProcessingMixin:
*, *,
alpha: int, alpha: int,
scale_from_preview: Tuple[int, int], scale_from_preview: Tuple[int, int],
is_purple_fn, is_match_fn,
) -> Image.Image: ) -> Image.Image:
overlay = Image.new("RGBA", image.size, (0, 0, 0, 0)) overlay = Image.new("RGBA", image.size, (0, 0, 0, 0))
draw = ImageDraw.Draw(overlay) draw = ImageDraw.Draw(overlay)
@ -223,7 +230,7 @@ class ImageProcessingMixin:
r, g, b, a = pixels[x, y] r, g, b, a = pixels[x, y]
if a == 0: if a == 0:
continue continue
if is_purple_fn(r, g, b): if is_match_fn(r, g, b):
draw.point((x, y), fill=(255, 0, 0, alpha)) draw.point((x, y), fill=(255, 0, 0, alpha))
return overlay return overlay

View File

@ -12,6 +12,9 @@ class ResetMixin:
self.val_max.set(self.DEFAULTS["val_max"]) self.val_max.set(self.DEFAULTS["val_max"])
self.alpha.set(self.DEFAULTS["alpha"]) self.alpha.set(self.DEFAULTS["alpha"])
self.update_preview() self.update_preview()
default_text = getattr(self, "status_default_text", "Standardwerte aktiv.")
if hasattr(self, "status"):
self.status.config(text=default_text)
__all__ = ["ResetMixin"] __all__ = ["ResetMixin"]