Skip to content

Instantly share code, notes, and snippets.

@yell0wsuit
Created January 3, 2026 07:11
Show Gist options
  • Select an option

  • Save yell0wsuit/9933474f33f9ad4dc8ef60ce99fc8e62 to your computer and use it in GitHub Desktop.

Select an option

Save yell0wsuit/9933474f33f9ad4dc8ef60ce99fc8e62 to your computer and use it in GitHub Desktop.
Python script to extract accent colors from images using K-means clustering in perceptually uniform color spaces (Lab/Oklab). Outputs colors in hex, RGB, and Oklch formats
"""
Extract accent colors from images using perceptual color spaces.
Provides hex, RGB, and Oklch color representations for UI theming.
"""
import colorsys
import sys
from typing import cast
from PIL import Image
import numpy as np
from sklearn.cluster import KMeans
from colormath.color_objects import sRGBColor, LabColor
from colormath.color_conversions import convert_color
import colour
def load_pixels(path, max_size=128, alpha_threshold=10):
"""
Load and downsample image, filtering transparent pixels.
Args:
max_size: Maximum dimension for downsampling (performance optimization)
alpha_threshold: Minimum alpha value to consider pixel (0-255)
"""
img = Image.open(path).convert("RGBA")
img.thumbnail((max_size, max_size), Image.Resampling.LANCZOS)
data = np.array(img)
rgb = data[..., :3]
alpha = data[..., 3]
mask = alpha > alpha_threshold
pixels = rgb[mask]
if len(pixels) == 0:
raise ValueError("No visible pixels found (all transparent)")
return pixels.astype(np.float32) / 255.0
def is_near_gray(rgb, threshold=0.08):
"""Check if RGB color is near grayscale using standard deviation."""
return np.std(rgb) < threshold
def perceptual_average_color(pixels):
"""Compute average color in perceptually uniform Lab color space."""
lab_pixels = []
for r, g, b in pixels:
srgb = sRGBColor(r, g, b)
lab = cast(LabColor, convert_color(srgb, LabColor, target_illuminant="d65"))
lab_pixels.append([lab.lab_l, lab.lab_a, lab.lab_b])
lab_avg = np.mean(lab_pixels, axis=0)
lab_result = LabColor(*lab_avg)
rgb = cast(sRGBColor, convert_color(lab_result, sRGBColor))
return np.clip(rgb.get_value_tuple(), 0, 1)
def dominant_color(pixels, k=4, gray_threshold=0.08):
"""
Find dominant non-gray color using K-means clustering.
Args:
k: Number of clusters (should be < number of pixels)
gray_threshold: Standard deviation threshold for gray detection
"""
n_clusters = min(k, len(pixels))
kmeans = KMeans(n_clusters=n_clusters, n_init=10, random_state=None)
labels = kmeans.fit_predict(pixels)
centers = kmeans.cluster_centers_
counts = np.bincount(labels)
sorted_idx = np.argsort(counts)[::-1]
for idx in sorted_idx:
color = centers[idx]
if not is_near_gray(color, gray_threshold):
return np.clip(color, 0, 1)
return np.clip(centers[sorted_idx[0]], 0, 1)
def clamp_accent(rgb, sat_range=(0.55, 0.8), light_range=(0.4, 0.65)):
"""
Clamp color to accent-friendly saturation and lightness ranges.
Uses HLS color space (Hue, Lightness, Saturation).
"""
hue, lightness, saturation = colorsys.rgb_to_hls(*rgb)
saturation = np.clip(saturation, sat_range[0], sat_range[1])
lightness = np.clip(lightness, light_range[0], light_range[1])
return colorsys.hls_to_rgb(hue, lightness, saturation)
def rgb_to_oklch(rgb):
"""
Convert sRGB to Oklch using colour-science library.
Returns:
tuple: (lightness, chroma, hue) where lightness is 0-1,
chroma is unbounded, hue is 0-360 degrees
"""
# colour-science expects RGB in 0-1 range
oklab = colour.convert(rgb, "sRGB", "Oklab")
# Convert Oklab to Oklch (polar coordinates)
lightness = oklab[0]
a_axis = oklab[1]
b_axis = oklab[2]
chroma = np.sqrt(a_axis**2 + b_axis**2)
hue = np.arctan2(b_axis, a_axis) * 180 / np.pi
if hue < 0:
hue += 360
return lightness, chroma, hue
def format_color_info(name, rgb):
"""Format color with hex, RGB, and Oklch representations."""
r, g, b = [int(np.clip(c, 0, 1) * 255) for c in rgb]
hex_color = f"#{r:02X}{g:02X}{b:02X}"
lightness, chroma, hue = rgb_to_oklch(rgb)
return {
"name": name,
"hex": hex_color,
"rgb": f"rgb({r}, {g}, {b})",
"oklch": f"oklch({lightness:.4f} {chroma:.4f} {hue:.2f})",
}
def extract_colors(image_path, max_size=128, n_clusters=4):
"""
Extract accent colors from an image using multiple methods.
Returns:
list: Color info dictionaries with hex, RGB, and Oklch formats
"""
pixels = load_pixels(image_path, max_size=max_size)
avg = perceptual_average_color(pixels)
dom = dominant_color(pixels, k=n_clusters)
accent = clamp_accent(dom)
return [
format_color_info("Perceptual average", avg),
format_color_info("Dominant color", dom),
format_color_info("Accent color", accent),
]
if __name__ == "__main__":
# Requirements: pip install pillow numpy scikit-learn colormath colour-science
if len(sys.argv) < 2:
print("Usage: python script.py <image_path>")
sys.exit(1)
img_path = sys.argv[1]
colors = extract_colors(img_path)
for color_info in colors:
print(f"\n{color_info['name']}:")
print(f" HEX: {color_info['hex']}")
print(f" RGB: {color_info['rgb']}")
print(f" OKLCH: {color_info['oklch']}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment