by adom
2020 Addressable RGB LED · 800 Kbps single-line cascade · 16 mA per channel (V1.0 sheet), 12 mA from V1.2
master
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
#!/usr/bin/env python3
"""Dominant wavelength (or CIE xy) -> sRGB. CIE 1931 2-degree spectral-locus chromaticities (5 nm table),
linear interpolation, XYZ->linear sRGB (IEC 61966-2-1), primary result = along_white()
(keeps lambda_d exactly); the OKLCh gamut cusp at the same hue is printed for reference only."""
import sys, json
LOC = {460:(.1440,.0297),465:(.1355,.0399),470:(.1241,.0578),475:(.1096,.0868),480:(.0913,.1327),
515:(.0389,.8120),520:(.0743,.8338),525:(.1142,.8262),530:(.1547,.8059),535:(.1929,.7816),
595:(.6029,.3965),600:(.6270,.3725),605:(.6482,.3514),610:(.6658,.3340),615:(.6801,.3197),
620:(.6915,.3083),625:(.7006,.2993),630:(.7079,.2920),635:(.7140,.2859),640:(.7190,.2809)}
def locus(wl):
lo = max(k for k in LOC if k <= wl); hi = min(k for k in LOC if k >= wl)
if lo == hi: return LOC[lo]
t = (wl-lo)/(hi-lo); return tuple(LOC[lo][i]+(LOC[hi][i]-LOC[lo][i])*t for i in (0,1))
M = [[3.2406,-1.5372,-0.4986],[-0.9689,1.8758,0.0415],[0.0557,-0.2040,1.0570]]
def xy2lin(x, y):
X, Y, Z = x/y, 1.0, (1-x-y)/y
return [M[i][0]*X+M[i][1]*Y+M[i][2]*Z for i in range(3)]
def enc(c): return 12.92*c if c <= 0.0031308 else 1.055*c**(1/2.4)-0.055
def along_white(lin):
"""Intersection of the white->spectral line (in xy) with the sRGB gamut: adds the least D65 white that
makes every channel >= 0, so the result keeps the dominant wavelength EXACTLY (the definition of lambda_d)."""
w = xy2lin(.3127, .3290)
if min(lin) < 0:
k = max(-lin[i]/w[i] for i in range(3) if lin[i] < 0); lin = [lin[i]+k*w[i] for i in range(3)]
mx = max(lin); lin = [max(0, c/mx) for c in lin]
return [round(enc(c), 4) for c in lin]
def cbrt(v): return v**(1/3) if v >= 0 else -((-v)**(1/3))
def lin2ok(c):
r,g,b = c
l = cbrt(.4122214708*r+.5363325363*g+.0514459929*b); m = cbrt(.2119034982*r+.6806995451*g+.1073969566*b); s = cbrt(.0883024619*r+.2817188376*g+.6299787005*b)
return [.2104542553*l+.7936177850*m-.0040720468*s, 1.9779984951*l-2.4285922050*m+.4505937099*s, .0259040371*l+.7827717662*m-.8086757660*s]
def ok2lin(o):
L,a,b = o
l = (L+.3963377774*a+.2158037573*b)**3; m = (L-.1055613458*a-.0638541728*b)**3; s = (L-.0894841775*a-1.2914855480*b)**3
return [4.0767416621*l-3.3077115913*m+.2309699292*s, -1.2684380046*l+2.6097574011*m-.3413193965*s, -.0041960863*l-.7034186147*m+1.7076147010*s]
def to_srgb(lin):
mx = max(lin); lin = [c/mx for c in lin]
L, a, b = lin2ok(lin); lo, hi = 0.0, 1.0
ok = lambda k: all(-1e-6 <= c <= 1+1e-6 for c in ok2lin([L, a*k, b*k]))
if not ok(1.0):
for _ in range(40):
mid = (lo+hi)/2
if ok(mid): lo = mid
else: hi = mid
lin = [min(1, max(0, c)) for c in ok2lin([L, a*lo, b*lo])]
return [round(enc(c), 4) for c in lin], [round(c, 4) for c in lin]
def cusp(lin):
"""Most chromatic in-gamut sRGB colour at the spectral colour's OKLab hue (the gamut cusp)."""
import math
L0, a, b = lin2ok(lin); h = math.atan2(b, a); best = (0, None)
for i in range(200):
L = 0.2 + 0.78*i/199; lo, hi = 0.0, 0.5
for _ in range(40):
mid = (lo+hi)/2
if all(-1e-7 <= c <= 1+1e-7 for c in ok2lin([L, mid*math.cos(h), mid*math.sin(h)])): lo = mid
else: hi = mid
if lo > best[0]: best = (lo, L)
C, L = best
out = [min(1, max(0, c)) for c in ok2lin([best[1], best[0]*math.cos(h), best[0]*math.sin(h)])]
return [round(enc(c), 4) for c in out], round(math.degrees(h) % 360, 1)
if __name__ == '__main__':
a = sys.argv[1]
x, y = (map(float, a.split(','))) if ',' in a else locus(float(a))
d = along_white(xy2lin(x, y)); c, h = cusp(xy2lin(x, y))
hx = lambda s: '#%02X%02X%02X' % tuple(round(v*255) for v in s)
print(json.dumps({'input': a, 'xy': [round(x,4), round(y,4)], 'srgb': d, 'hex': hx(d), 'method': 'lambda_d-preserving: white-point line to spectral locus, clipped at the sRGB gamut edge, max channel 1', 'reference_oklch_cusp_hex': hx(c), 'oklab_hue_deg': h}))