Add stable fallback for low-color artwork
This commit is contained in:
parent
d7e6451a14
commit
2bc6344ed9
1 changed files with 46 additions and 8 deletions
|
|
@ -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] = {}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue