Skip to content

Instantly share code, notes, and snippets.

@yuttie
Created November 20, 2015 04:12
Show Gist options
  • Select an option

  • Save yuttie/f833742d5cfd343ead9d to your computer and use it in GitHub Desktop.

Select an option

Save yuttie/f833742d5cfd343ead9d to your computer and use it in GitHub Desktop.
Find a color for terminals that is perceptually similar to a given color
#!/usr/bin/python
from math import *
from random import *
import sys
# ANSI colors
ANSI_COLORS = [(0x00, 0x00, 0x00),
(0x80, 0x00, 0x00),
(0x00, 0x80, 0x00),
(0x80, 0x80, 0x00),
(0x00, 0x00, 0x80),
(0x80, 0x00, 0x80),
(0x00, 0x80, 0x80),
(0xc0, 0xc0, 0xc0),
(0x80, 0x80, 0x80),
(0xff, 0x00, 0x00),
(0x00, 0xff, 0x00),
(0xff, 0xff, 0x00),
(0x00, 0x00, 0xff),
(0xff, 0x00, 0xff),
(0x00, 0xff, 0xff),
(0xff, 0xff, 0xff)]
# xterm's 256 colors
CUBE_INTENSITIES = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff]
def index2xtermrgb(i):
if i < 16:
return ANSI_COLORS[i]
elif i < 232:
tmp, b = divmod(i - 16, 6)
tmp, g = divmod(tmp, 6)
tmp, r = divmod(tmp, 6)
r = CUBE_INTENSITIES[r]
g = CUBE_INTENSITIES[g]
b = CUBE_INTENSITIES[b]
return (r, g, b)
else:
v = 10 * (i - 232) + 8
return (v, v, v)
XTERM_COLORS = [index2xtermrgb(i) for i in range(256)]
# Conversion
def rgb2xyz(color):
def srgb2linear(c):
a = 0.055
return c / 12.92 if c <= 0.04045 else ((c + a) / (1 + a))**2.4
r, g, b = color
r = r / 0xff
g = g / 0xff
b = b / 0xff
r_lin = srgb2linear(r)
g_lin = srgb2linear(g)
b_lin = srgb2linear(b)
x = 0.4124 * r_lin + 0.3576 * g_lin + 0.1805 * b_lin
y = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin
z = 0.0193 * r_lin + 0.1192 * g_lin + 0.9505 * b_lin
return (x, y, z)
def xyz2lab(color):
def f(t):
return t**(1/3) if t > (6/29)**3 else (29/6)**2 * t / 3 + 4/29
x_n = 0.95047
y_n = 1.00000
z_n = 1.08883
x, y, z = color
l = 116 * f(y / y_n) - 16
a = 500 * (f(x / x_n) - f(y / y_n))
b = 200 * (f(y / y_n) - f(z / z_n))
return (l, a, b)
# Approximation
def distance(color1, color2):
r1, g1, b1 = color1
r2, g2, b2 = color2
return sqrt((r2 - r1)**2 + (g2 - g1)**2 + (b2 - b1)**2)
def deltaE(color1, color2):
l1, a1, b1 = color1
l2, a2, b2 = color2
d_l = l1 - l2
c1 = sqrt(a1**2 + b1**2)
c2 = sqrt(a2**2 + b2**2)
d_c = c1 - c2
d_a = a1 - a2
d_b = b1 - b2
d_h2 = d_a**2 + d_b**2 - d_c**2
d_h = 0 if d_h2 < 0 else sqrt(d_h2)
k_l = 1
k_c = 1
k_h = 1
k1 = 0.045
k2 = 0.015
s_l = 1
s_c = 1 + k1 * c1
s_h = 1 + k2 * c1
d_e2 = (d_l / (k_l * s_l))**2 + (d_c / (k_c * s_c))**2 + (d_h / (k_h * s_h))**2
d_e = 0 if d_e2 < 0 else sqrt(d_e2)
return d_e
def approx(color, palette, dist_func = distance):
min_i = -1
min_dist = 1000000000
for i, c in enumerate(palette):
d = dist_func(color, c)
if d < min_dist:
min_i = i
min_dist = d
return min_i
# Benchmark
XTERM_COLORS_LAB = list(map(lambda c: xyz2lab(rgb2xyz(c)), XTERM_COLORS))
print('''<!doctype html>
<head>
<style>
.g {
display: inline-flex;
flex-direction: row;
width: 4em;
height: 4em;
}
.c {
flex: auto;
}
</style>
</head>
''')
count = 0
for rgb in ((r, g, b) for r in range(0xff) for g in range(0xff) for b in range(0xff)):
lab = xyz2lab(rgb2xyz(rgb))
i_srgb = approx(rgb, XTERM_COLORS)
i_lab = approx(lab, XTERM_COLORS_LAB)
i_lab_deltae = approx(lab, XTERM_COLORS_LAB, deltaE)
if i_srgb != i_lab or i_srgb != i_lab_deltae:
print('<div class="g">')
print(' <div class="c" style="background:#{:02x}{:02x}{:02x};"></div>'.format(*XTERM_COLORS[i_srgb]))
print(' <div class="c" style="background:#{:02x}{:02x}{:02x};"></div>'.format(*rgb))
print(' <div class="c" style="background:#{:02x}{:02x}{:02x};"></div>'.format(*XTERM_COLORS[i_lab]))
print(' <div class="c" style="background:#{:02x}{:02x}{:02x};"></div>'.format(*XTERM_COLORS[i_lab_deltae]))
print('</div>')
count = count + 1
sys.stderr.write("\r{:d}".format(count))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment