Add stable fallback for low-color artwork

This commit is contained in:
ltadeu6 2026-04-15 18:40:44 -03:00
parent d7e6451a14
commit 2bc6344ed9
No known key found for this signature in database
GPG key ID: FB9FDAB6B6F3418D

View file

@ -28,6 +28,14 @@ OKLCH_LIGHTNESS = 0.72
OKLCH_CHROMA = 0.24 OKLCH_CHROMA = 0.24
MAX_BRIGHTNESS_STEP = 0.10 MAX_BRIGHTNESS_STEP = 0.10
PLAYERCTL_BIN = os.environ.get("PLAYERCTL_BIN", "playerctl") 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: 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)) 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: def srgb_channel_to_linear(channel: float) -> float:
if channel <= 0.04045: if channel <= 0.04045:
return channel / 12.92 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]]: def pure_tone_palette() -> list[tuple[int, int, int]]:
return [ 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) for index in range(PALETTE_SIZE)
] ]
PURE_TONE_PALETTE = pure_tone_palette() 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]: 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) 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]]: 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 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]: def dominant_color(img: Image.Image) -> tuple[int, int, int]:
pixels = good_pixels(img) pixels = good_pixels(img)
if not pixels: if not pixels:
# fallback simples # fallback simples
small = img.resize((50, 50)) return PURPLE_FALLBACK_RGB
colors = small.getcolors(50 * 50)
if not colors: if should_use_purple_fallback(pixels):
return (255, 255, 255) return PURPLE_FALLBACK_RGB
colors.sort(reverse=True)
return colors[0][1]
# Quantiza para agrupar tons parecidos # Quantiza para agrupar tons parecidos
buckets: dict[tuple[int, int, int], int] = {} buckets: dict[tuple[int, int, int], int] = {}