Created
December 2, 2025 00:55
-
-
Save PhiBabin/77e3748fa007241251d72701e67d0657 to your computer and use it in GitHub Desktop.
Compare different color luminance function by plotting how they order color
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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