Skip to content

Instantly share code, notes, and snippets.

@PhiBabin
Created December 2, 2025 00:55
Show Gist options
  • Select an option

  • Save PhiBabin/77e3748fa007241251d72701e67d0657 to your computer and use it in GitHub Desktop.

Select an option

Save PhiBabin/77e3748fa007241251d72701e67d0657 to your computer and use it in GitHub Desktop.
Compare different color luminance function by plotting how they order color
# pip install matplotlib
import matplotlib.pyplot as plt
import numpy as np
safe_colors = ["#000000","#000033","#000066","#000099","#0000CC","#0000FF","#003300","#003333","#003366","#003399","#0033CC","#0033FF","#006600","#006633","#006666","#006699","#0066CC","#0066FF","#009900","#009933","#009966","#009999","#0099CC","#0099FF","#00CC00","#00CC33","#00CC66","#00CC99","#00CCCC","#00CCFF","#00FF00","#00FF33","#00FF66","#00FF99","#00FFCC","#00FFFF","#330000","#330033","#330066","#330099","#3300CC","#3300FF","#333300","#333333","#333366","#333399","#3333CC","#3333FF","#336600","#336633","#336666","#336699","#3366CC","#3366FF","#339900","#339933","#339966","#339999","#3399CC","#3399FF","#33CC00","#33CC33","#33CC66","#33CC99","#33CCCC","#33CCFF","#33FF00","#33FF33","#33FF66","#33FF99","#33FFCC","#33FFFF","#660000","#660033","#660066","#660099","#6600CC","#6600FF","#663300","#663333","#663366","#663399","#6633CC","#6633FF","#666600","#666633","#666666","#666699","#6666CC","#6666FF","#669900","#669933","#669966","#669999","#6699CC","#6699FF","#66CC00","#66CC33","#66CC66","#66CC99","#66CCCC","#66CCFF","#66FF00","#66FF33","#66FF66","#66FF99","#66FFCC","#66FFFF","#990000","#990033","#990066","#990099","#9900CC","#9900FF","#993300","#993333","#993366","#993399","#9933CC","#9933FF","#996600","#996633","#996666","#996699","#9966CC","#9966FF","#999900","#999933","#999966","#999999","#9999CC","#9999FF","#99CC00","#99CC33","#99CC66","#99CC99","#99CCCC","#99CCFF","#99FF00","#99FF33","#99FF66","#99FF99","#99FFCC","#99FFFF","#CC0000","#CC0033","#CC0066","#CC0099","#CC00CC","#CC00FF","#CC3300","#CC3333","#CC3366","#CC3399","#CC33CC","#CC33FF","#CC6600","#CC6633","#CC6666","#CC6699","#CC66CC","#CC66FF","#CC9900","#CC9933","#CC9966","#CC9999","#CC99CC","#CC99FF","#CCCC00","#CCCC33","#CCCC66","#CCCC99","#CCCCCC","#CCCCFF","#CCFF00","#CCFF33","#CCFF66","#CCFF99","#CCFFCC","#CCFFFF","#FF0000","#FF0033","#FF0066","#FF0099","#FF00CC","#FF00FF","#FF3300","#FF3333","#FF3366","#FF3399","#FF33CC","#FF33FF","#FF6600","#FF6633","#FF6666","#FF6699","#FF66CC","#FF66FF","#FF9900","#FF9933","#FF9966","#FF9999","#FF99CC","#FF99FF","#FFCC00","#FFCC33","#FFCC66","#FFCC99","#FFCCCC","#FFCCFF","#FFFF00","#FFFF33","#FFFF66","#FFFF99","#FFFFCC","#FFFFFF"]
# 2 teams 8 vs 8
bar_8v8_colors = ["#0B3EF3",
"#0CE908",
"#00f5e5",
"#6941f2",
"#8fff94",
"#1b702f",
"#7cc2ff",
"#a294ff",
"#FF1005",
"#FFD200",
"#FF6107",
"#F80889",
"#FCEEA4",
"#8a2828",
"#F190B3",
"#C88B2F",]
# 2 teams 20 vs 20
bar_colors = ["#0B3EF3",
"#0CE908",
"#00f5e5",
"#6941f2",
"#8fff94",
"#1b702f",
"#7cc2ff",
"#a294ff",
"#0B849B",
"#689E3D",
"#4F2684",
"#2C32AC",
"#6968A0",
"#D8EEFF",
"#3475FF",
"#7EB900",
"#4A4376",
"#B7EA63",
"#C4A9FF",
"#37713A",
"#FF1005",
"#FFD200",
"#FF6107",
"#F80889",
"#FCEEA4",
"#8a2828",
"#F190B3",
"#C88B2F",
"#B04523",
"#FFBB7C",
"#A35274",
"#773A01",
"#F5A200",
"#BBA28B",
"#971C48",
"#FF68EA",
"#DD783F",
"#FFAAF3",
"#764A4A",
"#9F0D05",
]
def srgb_to_linear_srgb(c):
"""Converts a single sRGB component (0-1) to linear sRGB."""
if c <= 0.04045:
return c / 12.92
else:
return ((c + 0.055) / 1.055) ** 2.4
def rgb_to_linear_srgb(r, g, b):
"""Convert RGB to linear sRGB"""
# Normalize to 0-1 range
r_norm, g_norm, b_norm = r / 255.0, g / 255.0, b / 255.0
# Convert to linear sRGB
linear_rgb = np.array([
srgb_to_linear_srgb(r_norm),
srgb_to_linear_srgb(g_norm),
srgb_to_linear_srgb(b_norm)
])
return linear_rgb
def linear_srgb_to_xyz(linear_rgb):
"""Converts linear sRGB to CIE XYZ."""
# sRGB to XYZ matrix (D65 white point)
m = np.array([
[0.4124564, 0.3575761, 0.1804375],
[0.2126729, 0.7151522, 0.0721750],
[0.0193339, 0.1191920, 0.9503041]
])
return np.dot(m, linear_rgb)
def srgb_to_oklab(rgb):
"""Converts linnear sRGB to OkLab."""
# XYZ to LMS matrix
m1 = np.array([
[0.4122214708, 0.5363325363, 0.0514459929],
[0.2119034982, 0.6806995451, 0.1073969566],
[0.0883024619, 0.2817188376, 0.6299787005]
])
lms = np.dot(m1, rgb)
# Apply cube root to LMS
lms_prime = np.cbrt(lms)
# LMS' to OkLab matrix
m2 = np.array([
[0.2104542553, 0.7936177850, -0.0040720468],
[1.9779984951, -2.4285922050, 0.4505937099],
[0.0259080371, 0.7827717662, -0.8086757660]
])
return np.dot(m2, lms_prime)
def rgb_to_xyz(r, g, b):
"""Converts 8-bit sRGB to XYZ."""
linear_rgb = rgb_to_linear_srgb(r, g, b)
return linear_srgb_to_xyz(linear_rgb)
def rgb_to_oklab(r, g, b):
"""Converts 8-bit sRGB to OkLab."""
oklab = srgb_to_oklab(rgb_to_linear_srgb(r,g,b))
return oklab
def rgb_to_oklch_h(r,g,b):
l, a, b = rgb_to_oklab(r, g, b)
return np.rad2deg(np.arctan2(b, a))
def y_of_xyz_simplify(r, g, b):
"""This is just the Y of XYZ"""
lr, lg, lb = rgb_to_linear_srgb(r, g, b)
return 0.2126*lr + 0.7152*lg + 0.0722*lb
def y_of_yiq_simplify(r, g, b):
"""Y component of YIQ"""
# lr, lg, lb = rgb_to_linear_srgb(r, g, b)
# return 0.299*lr + 0.587*lg + 0.114*lb
# lr, lg, lb = rgb_to_linear_srgb(r, g, b)
return 0.299*r/255.0 + 0.587*g/255.0 + 0.114*b/255.0
def p_of_hsp_simplify(r, g, b):
"""P component of HSP"""
lr, lg, lb = rgb_to_linear_srgb(r, g, b)
return np.sqrt(0.241*lr*lr + 0.691*lg*lg + 0.068*lb*lb)
def hex_to_tuple(hex) -> tuple[int, int, int]:
"""Parse hex RGB color into a tuple of ints 0-255"""
return (int(hex[1:3], 16), int(hex[3:5], 16), int(hex[5:7], 16))
def beyond_all_reason(r, g, b):
"""BAR implementation in ColorIsDark()."""
return r/255. + g * 1.2/255. + b * 0.4/255.
# There are a lot of safe colors, so use 1 out of 3
hexcolors = safe_colors#[::3]
hexcolors = bar_colors # bar_8v8_colors # bar_colors
print(f"There are {len(bar_colors)} BAR colors and {len(safe_colors)} safe colors")
colors = [hex_to_tuple(hexcolor) for hexcolor in hexcolors]
lum_fcts = [
("Current ColorIsDark()", (lambda c : beyond_all_reason(*c)), 0.65),
("Y' component of XYZ", (lambda c : rgb_to_xyz(*c)[1]), 0.2),
("L component of Oklab", (lambda c : rgb_to_oklab(*c)[0]), 0.59),
# ("Y component of YIQ", (lambda c : y_of_yiq_simplify(*c)), None),
("P component of HSP", (lambda c : p_of_hsp_simplify(*c)), None),
]
fig, axs = plt.subplots(nrows=len(lum_fcts))
fig.subplots_adjust(top=0.90, bottom=0.1, left=0.3, right=0.99,
wspace=0.05)
fig.suptitle('Colors Ordered by "Luminance"', fontsize=16, x=0.6)
for ax, (name, lum_fct, dark_threshold_lum) in zip(axs, lum_fcts):
# Order the color by the "luminance" returned by the function
ordered_color = sorted(colors, key=lum_fct)
# Convert the ordered list of colors to 1 x N x 3 array
color_img = np.array([ordered_color])
# Draw color map
ax.imshow(color_img, aspect='auto', interpolation="none")
# if dark_threshold_lum:
# ax.hlines(dark_threshold_lum, xmin=0,xmax=len(ordered_color))
# lum = [lum_fct(c) for c in ordered_color]
# ordered_color_0_1 = [(r / 255.0, g / 255.0, b / 255.0) for r, g, b in ordered_color]
# ax.scatter(np.arange(len(lum)), lum, c=ordered_color_0_1)
# Add side text
pos = list(ax.get_position().bounds)
x_text = pos[0] - 0.01
y_text = pos[1] + pos[3]/2.
fig.text(x_text, y_text, name, va='center', ha='right', fontfamily="Monospace", fontsize=10)
# Turn off *all* ticks & spines, not just the ones with color maps.
for ax in axs.flat:
ax.set_axis_off()
# Draw axis at the bottom
fig.text(pos[0] + pos[2] - 0.01, pos[1] - 0.02, "Lighter-->", va='top', ha='right', fontsize=14)
fig.text(pos[0], pos[1] - 0.02, "<--Darker", va='top', ha='left', fontsize=14)
plt.savefig("foobar.png")
# some unit tests to make sure oklab works right
def are_equal(hex, expected_oklab):
oklab = rgb_to_oklab(*hex_to_tuple(hex))
diff_abs = np.abs(oklab - np.array(expected_oklab))
for i, letter in enumerate(["L", "A", "B"]):
if diff_abs[i] > 0.01:
print(f"{letter} are not equal {oklab[i]} != {expected_oklab[i]}")
# print(oklab, expected_oklab)
are_equal("#efb2e6", [0.84, 0.09, -0.05])
are_equal("#1C7876", [0.52, -0.08, -0.02])
def plot_oklab_l_vs_bar():
plt.style.use('dark_background')
fig, ax = plt.subplots(nrows=1)
fig.subplots_adjust()
fig.suptitle('Oklab L vs BAR\'s luminance')
# lum = [lum_fcts[0][1](c) for c in colors]
lum = [rgb_to_oklch_h(*c) for c in colors]
expected_lum = [lum_fcts[2][1](c) for c in colors]
colors_0_1 = [(r / 255.0, g / 255.0, b / 255.0) for r, g, b in colors]
ax.scatter(lum, expected_lum, c=colors_0_1)
ax.vlines(0.65, ymin=min(expected_lum),ymax=max(expected_lum), label="Current BAR threshold")
ax.set_xlabel("BAR luminance")
ax.set_ylabel("oklab perceived luminance")
# ax.set_yscale('log', base=10)
plt.savefig("foobar2.png")
for file_name, (name, lum_fct, dark_threshold_lum) in [('bar', lum_fcts[0]), ('xyz', lum_fcts[1]), ('oklab', lum_fcts[2])]:
plt.style.use('dark_background')
fig, ax = plt.subplots(nrows=1, figsize=(8, 3))
# fig.subplots_adjust()
fig.suptitle(f'{name} with a threshold of {dark_threshold_lum}')
# lum = [lum_fcts[0][1](c) for c in colors]
hue = [rgb_to_oklch_h(*c) for c in colors]
lum = [lum_fct(c) for c in colors]
colors_0_1 = [(r / 255.0, g / 255.0, b / 255.0) for r, g, b in colors]
ax.scatter(lum, hue, c=colors_0_1)
if min(lum) < dark_threshold_lum:
ax.text(dark_threshold_lum - 0.015, 190, "<-White Outline", horizontalalignment='right', verticalalignment='bottom')
ax.text(dark_threshold_lum + 0.015, 190, "Dark Outline->", horizontalalignment='left', verticalalignment='bottom')
ax.vlines(dark_threshold_lum, ymin=min(hue),ymax=max(hue), linestyles="dashed")
# ax.set_xlabel("BAR luminance")
ax.set_xlabel(f"Luminance from {name}")
ax.get_yaxis().set_visible(False)
ax.minorticks_on()
plt.tight_layout()
# ax.set_yscale('log', base=10)
plt.savefig(f"color_threshold_{file_name}.png")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment