From 2bc6344ed9c867e602f7cf0a7af1c230ec7d2ba5 Mon Sep 17 00:00:00 2001 From: ltadeu6 Date: Wed, 15 Apr 2026 18:40:44 -0300 Subject: [PATCH] Add stable fallback for low-color artwork --- luz-musica.py | 54 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/luz-musica.py b/luz-musica.py index b19ca6a..35582f8 100644 --- a/luz-musica.py +++ b/luz-musica.py @@ -28,6 +28,14 @@ OKLCH_LIGHTNESS = 0.72 OKLCH_CHROMA = 0.24 MAX_BRIGHTNESS_STEP = 0.10 PLAYERCTL_BIN = os.environ.get("PLAYERCTL_BIN", "playerctl") +YELLOW_HUE_START = 0.12 +YELLOW_HUE_END = 0.18 +AMBER_TARGET_HUE = 0.10 +YELLOW_CHROMA_SCALE = 0.82 +YELLOW_LIGHTNESS_SCALE = 0.94 +NEUTRAL_CHROMA_THRESHOLD = 0.05 +NEUTRAL_SATURATION_THRESHOLD = 0.18 +PURPLE_FALLBACK_HUE = 0.78 def run_playerctl(field: str, player: str | None = None) -> str: @@ -176,6 +184,16 @@ def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]: return (int(r * 255), int(g * 255), int(b * 255)) +def tame_yellow_oklch(lightness: float, chroma: float, hue: float) -> tuple[float, float, float]: + if YELLOW_HUE_START <= hue <= YELLOW_HUE_END: + return ( + lightness * YELLOW_LIGHTNESS_SCALE, + chroma * YELLOW_CHROMA_SCALE, + AMBER_TARGET_HUE, + ) + return (lightness, chroma, hue) + + def srgb_channel_to_linear(channel: float) -> float: if channel <= 0.04045: return channel / 12.92 @@ -244,12 +262,13 @@ def oklch_to_rgb(lightness: float, chroma: float, hue: float) -> tuple[int, int, def pure_tone_palette() -> list[tuple[int, int, int]]: return [ - oklch_to_rgb(OKLCH_LIGHTNESS, OKLCH_CHROMA, index / PALETTE_SIZE) + oklch_to_rgb(*tame_yellow_oklch(OKLCH_LIGHTNESS, OKLCH_CHROMA, index / PALETTE_SIZE)) for index in range(PALETTE_SIZE) ] PURE_TONE_PALETTE = pure_tone_palette() +PURPLE_FALLBACK_RGB = oklch_to_rgb(OKLCH_LIGHTNESS, OKLCH_CHROMA, PURPLE_FALLBACK_HUE) def clamp_brightness(rgb: tuple[int, int, int], previous_rgb: tuple[int, int, int] | None) -> tuple[int, int, int]: @@ -272,7 +291,7 @@ def snap_to_palette(rgb: tuple[int, int, int]) -> tuple[int, int, int]: ) best_lightness, _, best_hue = rgb_to_oklch(best) - return oklch_to_rgb(best_lightness, OKLCH_CHROMA, best_hue) + return oklch_to_rgb(*tame_yellow_oklch(best_lightness, OKLCH_CHROMA, best_hue)) def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]: @@ -297,16 +316,35 @@ def good_pixels(img: Image.Image) -> list[tuple[int, int, int]]: return filtered +def color_information_score(pixels: list[tuple[int, int, int]]) -> tuple[float, float]: + if not pixels: + return (0.0, 0.0) + + chroma_total = 0.0 + saturation_total = 0.0 + for pixel in pixels: + _, chroma, _ = rgb_to_oklch(pixel) + _, saturation, _ = rgb_to_hsv(pixel) + chroma_total += chroma + saturation_total += saturation + + count = len(pixels) + return (chroma_total / count, saturation_total / count) + + +def should_use_purple_fallback(pixels: list[tuple[int, int, int]]) -> bool: + avg_chroma, avg_saturation = color_information_score(pixels) + return avg_chroma < NEUTRAL_CHROMA_THRESHOLD or avg_saturation < NEUTRAL_SATURATION_THRESHOLD + + def dominant_color(img: Image.Image) -> tuple[int, int, int]: pixels = good_pixels(img) if not pixels: # fallback simples - small = img.resize((50, 50)) - colors = small.getcolors(50 * 50) - if not colors: - return (255, 255, 255) - colors.sort(reverse=True) - return colors[0][1] + return PURPLE_FALLBACK_RGB + + if should_use_purple_fallback(pixels): + return PURPLE_FALLBACK_RGB # Quantiza para agrupar tons parecidos buckets: dict[tuple[int, int, int], int] = {}