Skip to content

Instantly share code, notes, and snippets.

@dbr
Created December 30, 2012 14:58
Show Gist options
  • Save dbr/4413132 to your computer and use it in GitHub Desktop.
Save dbr/4413132 to your computer and use it in GitHub Desktop.
"""Inspired by https://github.com/timbowman/blackbody
..this creates a Nuke ColorLookup node, which maps from Kelvin to xyY values,
using code from https://github.com/dbr/colourstuff
"""
def cie1931_standard_observer_rawdata(two_degree = False, ten_degree = False):
"""Returns dictionary with three keys: x, y and z. Each key contains
a dictionary mapping wavelength to value
Created using the from_txt_to_python function
"""
if not any([two_degree, ten_degree]) or all([two_degree, ten_degree]):
raise ValueError("Specify either two_degree or ten_degree")
cie1931_standard_observer_2deg = {
'x': {
380: 1368,
385: 2236,
390: 4243,
395: 7650,
400: 14310,
405: 23190,
410: 43510,
415: 77630,
420: 134380,
425: 214770,
430: 283900,
435: 328500,
440: 348280,
445: 348060,
450: 336200,
455: 318700,
460: 290800,
465: 251100,
470: 195360,
475: 142100,
480: 95640,
485: 57950,
490: 32010,
495: 14700,
500: 4900,
505: 2400,
510: 9300,
515: 29100,
520: 63270,
525: 109600,
530: 165500,
535: 225750,
540: 290400,
545: 359700,
550: 433450,
555: 512050,
560: 594500,
565: 678400,
570: 762100,
575: 842500,
580: 916300,
585: 978600,
590: 1026300,
595: 1056700,
600: 1062200,
605: 1045600,
610: 1002600,
615: 938400,
620: 854450,
625: 751400,
630: 642400,
635: 541900,
640: 447900,
645: 360800,
650: 283500,
655: 218700,
660: 164900,
665: 121200,
670: 87400,
675: 63600,
680: 46770,
685: 32900,
690: 22700,
695: 15840,
700: 11359,
705: 8111,
710: 5790,
715: 4109,
720: 2899,
725: 2049,
730: 1440,
735: 1000,
740: 690,
745: 476,
750: 332,
755: 235,
760: 166,
765: 117,
770: 83,
775: 59,
780: 42,
},
'y': {
380: 39,
385: 64,
390: 120,
395: 217,
400: 396,
405: 640,
410: 1210,
415: 2180,
420: 4000,
425: 7300,
430: 11600,
435: 16840,
440: 23000,
445: 29800,
450: 38000,
455: 48000,
460: 60000,
465: 73900,
470: 90980,
475: 112600,
480: 139020,
485: 169300,
490: 208020,
495: 258600,
500: 323000,
505: 407300,
510: 503000,
515: 608200,
520: 710000,
525: 793200,
530: 862000,
535: 914850,
540: 954000,
545: 980300,
550: 994950,
555: 1000000,
560: 995000,
565: 978600,
570: 952000,
575: 915400,
580: 870000,
585: 816300,
590: 757000,
595: 694900,
600: 631000,
605: 566800,
610: 503000,
615: 441200,
620: 381000,
625: 321000,
630: 265000,
635: 217000,
640: 175000,
645: 138200,
650: 107000,
655: 81600,
660: 61000,
665: 44580,
670: 32000,
675: 23200,
680: 17000,
685: 11920,
690: 8210,
695: 5723,
700: 4102,
705: 2929,
710: 2091,
715: 1484,
720: 1047,
725: 740,
730: 520,
735: 361,
740: 249,
745: 172,
750: 120,
755: 85,
760: 60,
765: 42,
770: 30,
775: 21,
780: 15,
},
'z': {
380: 6450,
385: 10550,
390: 20050,
395: 36210,
400: 67850,
405: 110200,
410: 207400,
415: 371300,
420: 645600,
425: 1039050,
430: 1385600,
435: 1622960,
440: 1747060,
445: 1782600,
450: 1772110,
455: 1744100,
460: 1669200,
465: 1528100,
470: 1287640,
475: 1041900,
480: 812950,
485: 616200,
490: 465180,
495: 353300,
500: 272000,
505: 212300,
510: 158200,
515: 111700,
520: 78250,
525: 57250,
530: 42160,
535: 29840,
540: 20300,
545: 13400,
550: 8750,
555: 5750,
560: 3900,
565: 2750,
570: 2100,
575: 1800,
580: 1650,
585: 1400,
590: 1100,
595: 1000,
600: 800,
605: 600,
610: 340,
615: 240,
620: 190,
625: 100,
630: 50,
635: 30,
640: 20,
645: 10,
650: 0,
655: 0,
660: 0,
665: 0,
670: 0,
675: 0,
680: 0,
685: 0,
690: 0,
695: 0,
700: 0,
705: 0,
710: 0,
715: 0,
720: 0,
725: 0,
730: 0,
735: 0,
740: 0,
745: 0,
750: 0,
755: 0,
760: 0,
765: 0,
770: 0,
775: 0,
780: 0,
},
}
cie1931_standard_observer_10deg = {
'x': {
380: 160,
385: 662,
390: 2362,
395: 7242,
400: 19110,
405: 43400,
410: 84736,
415: 140638,
420: 204492,
425: 264737,
430: 314679,
435: 357719,
440: 383734,
445: 386726,
450: 370702,
455: 342957,
460: 302273,
465: 254085,
470: 195618,
475: 132349,
480: 80507,
485: 41072,
490: 16172,
495: 5132,
500: 3816,
505: 15444,
510: 37465,
515: 71358,
520: 117749,
525: 172953,
530: 236491,
535: 304213,
540: 376772,
545: 451584,
550: 529826,
555: 616053,
560: 705224,
565: 793832,
570: 878655,
575: 951162,
580: 1014160,
585: 1074300,
590: 1118520,
595: 1134300,
600: 1123990,
605: 1089100,
610: 1030480,
615: 950740,
620: 856297,
625: 754930,
630: 647467,
635: 535110,
640: 431567,
645: 343690,
650: 268329,
655: 204300,
660: 152568,
665: 112210,
670: 81261,
675: 57930,
680: 40851,
685: 28623,
690: 19941,
695: 13842,
700: 9577,
705: 6605,
710: 4553,
715: 3145,
720: 2175,
725: 1506,
730: 1045,
735: 727,
740: 508,
745: 356,
750: 251,
755: 178,
760: 126,
765: 90,
770: 65,
775: 46,
780: 33,
},
'y': {
380: 17,
385: 72,
390: 253,
395: 769,
400: 2004,
405: 4509,
410: 8756,
415: 14456,
420: 21391,
425: 29497,
430: 38676,
435: 49602,
440: 62077,
445: 74704,
450: 89456,
455: 106256,
460: 128201,
465: 152761,
470: 185190,
475: 219940,
480: 253589,
485: 297665,
490: 339133,
495: 395379,
500: 460777,
505: 531360,
510: 606741,
515: 685660,
520: 761757,
525: 823330,
530: 875211,
535: 923810,
540: 961988,
545: 982200,
550: 991761,
555: 999110,
560: 997340,
565: 982380,
570: 955552,
575: 915175,
580: 868934,
585: 825623,
590: 777405,
595: 720353,
600: 658341,
605: 593878,
610: 527963,
615: 461834,
620: 398057,
625: 339554,
630: 283493,
635: 228254,
640: 179828,
645: 140211,
650: 107633,
655: 81187,
660: 60281,
665: 44096,
670: 31800,
675: 22602,
680: 15905,
685: 11130,
690: 7749,
695: 5375,
700: 3718,
705: 2565,
710: 1768,
715: 1222,
720: 846,
725: 586,
730: 407,
735: 284,
740: 199,
745: 140,
750: 98,
755: 70,
760: 50,
765: 36,
770: 25,
775: 18,
780: 13,
},
'z': {
380: 705,
385: 2928,
390: 10482,
395: 32344,
400: 86011,
405: 197120,
410: 389366,
415: 656760,
420: 972542,
425: 1282500,
430: 1553480,
435: 1798500,
440: 1967280,
445: 2027300,
450: 1994800,
455: 1900700,
460: 1745370,
465: 1554900,
470: 1317560,
475: 1030200,
480: 772125,
485: 570060,
490: 415254,
495: 302356,
500: 218502,
505: 159249,
510: 112044,
515: 82248,
520: 60709,
525: 43050,
530: 30451,
535: 20584,
540: 13676,
545: 7918,
550: 3988,
555: 1091,
560: 0,
565: 0,
570: 0,
575: 0,
580: 0,
585: 0,
590: 0,
595: 0,
600: 0,
605: 0,
610: 0,
615: 0,
620: 0,
625: 0,
630: 0,
635: 0,
640: 0,
645: 0,
650: 0,
655: 0,
660: 0,
665: 0,
670: 0,
675: 0,
680: 0,
685: 0,
690: 0,
695: 0,
700: 0,
705: 0,
710: 0,
715: 0,
720: 0,
725: 0,
730: 0,
735: 0,
740: 0,
745: 0,
750: 0,
755: 0,
760: 0,
765: 0,
770: 0,
775: 0,
780: 0,
},
}
if two_degree:
return cie1931_standard_observer_2deg
else:
return cie1931_standard_observer_10deg
def get_colour_matching_functions(two_degree = False, ten_degree = False):
"""Get the three colour matching functions, based of the CIE 1931 Standard
Colorimetric Observer (either the 2 or 10 degree observers)
Based on the 5nm tabulated data, linearly interpolates between samples.
Returned as a tuple of three callables, which take a wavelength in
nanometres, and return the reading.
"""
# Load the raw data
data = cie1931_standard_observer_rawdata(two_degree = two_degree, ten_degree = ten_degree)
# The outer function defines the channel, to prevent duplicating the
# function three times.
def getfunc(channel):
# The inner function uses the channel variable, and does the
# actual interpolation
def colour_matcher(value):
def lerp(a, b, mix):
return b*mix + a*(1-mix)
samples = sorted(data[channel])
if value < min(samples):
raise ValueError("Value %s below minimum sample: %s" % (value, min(samples)))
if value > max(samples):
raise ValueError("Value %s above minimum sample: %s" % (value, max(samples)))
deltas = [abs(a - value) for a in samples]
smallest = deltas.index(min(deltas))
ka, kb = samples[smallest-1], samples[smallest]
va, vb = data[channel][ka], data[channel][kb]
interpolant = (float(value) - ka) / (kb - ka)
return lerp(va, vb, interpolant)
colour_matcher.__doc__ = "Colour matching function for %r" % channel
return colour_matcher
return (getfunc("x"), getfunc("y"), getfunc("z"))
def planckian_locus_approx(T):
"""http://en.wikipedia.org/wiki/Planckian_locus#Approximation
"""
T = float(T)
if 1667 <= T <= 4000:
x = -0.2661239 * (10**9 / T**3) - 0.2343580 * (10**6/T**2) + 0.8776956 * (10**3/T) + 0.179910
elif 4000 <= T <= 25000:
x = -3.0258469 * (10**9 / T**3) + 2.107379 * (10**6/T**2) + 0.2226347*(10**3/T) + 0.240390
else:
raise ValueError("T out of range (should be between 1667K and 25000K)")
if 1667 <= T <= 2222:
y = -1.1063814 * x**3 - 1.34811020*x**2 + 2.18555832*x - 0.20219683
elif 2222 <= T <= 4000:
y = -0.9549476*x**3 - 1.37418593*x**2 + 2.09137015*x - 0.16748867
elif 4000 <= T <= 25000:
y = 3.0817580*x**3 - 5.87338670*x**2 + 3.75112997*x - 0.37001483
else:
raise ValueError("T out of range (should be between 1667K and 25000)")
return (x, y)
def plancks_law(wavelen, T):
"""wavelen is in nanometres, and T is in Kelvin
$I'(\lambda,T) =\frac{2 hc^2}{\lambda^5}\frac{1}{ e^{\frac{hc}{\lambda kT}}-1}$
http://en.wikipedia.org/wiki/Planck%27s_law
"""
#FIXME: Needs test cases, probably right, but maybe not
from math import exp, pi
nm = 10**-9 # nm to metres
h = 6.6260689633*10**-34 # Planck constant
k = 1.380650424*10**-23 # Boltzmann constant
c = 299792458 # m/s (speed of light in a vacuum)
c = 2.99792*10**8
# Left-hand chunk of expression (that's the official maths
# terminology, I'm sure)
# http://www.wolframalpha.com/input/?i=%282*pi*h*c^2%29+%2F+5600nm^2
p1 = (2*pi*h*(c**2)) / ((wavelen*nm)**5)
# Right-hand chunk of expression
# http://www.wolframalpha.com/input/?i=e^{%5Cfrac{%28Planck+constant%29%28speed+of+light+in+a+vacuum%29}{580nm+*+%28Boltzmann+constant%29+*+5600K}}
p2 = 1.0 / (exp((h*c) / (wavelen*nm * k * T)) - 1)
return p1 * p2
def integrate(f, a, b, N):
dx = (b-a)/N
s = 0
for i in range(N):
s += f(a+i*dx)
return s * dx
def planckian_locus(T):
"""XYZ colour coordinates of a theoretical incadescent black body
radiator at a given temperature, or something like that.
Can be used to calculate the chromaticity coordinates of, say, a
6500K light source
http://en.wikipedia.org/wiki/Planckian_locus
Argument T is in Kelvin, return value is a CIE XYZ value
"""
samples = (780-380)/5 # values every 5 nm are stored
X, Y, Z = get_colour_matching_functions(two_degree = True)
# Integrated between 380nm and 780nm, as that is the range of
# values contained in the colour-matching functions
X_T = integrate(lambda wavelen: plancks_law(wavelen, T) * X(wavelen), 380, 780, samples)
Y_T = integrate(lambda wavelen: plancks_law(wavelen, T) * Y(wavelen), 380, 780, samples)
Z_T = integrate(lambda wavelen: plancks_law(wavelen, T) * Z(wavelen), 380, 780, samples)
return (X_T, Y_T, Z_T)
node = nuke.nodes.ColorLookup()
for channel in range(1,4):
node.knob('lut').clearAnimated(channel)
node.knob('source').setSingleValue(True)
for T in range(1000, 10000, 100):
X,Y,Z = planckian_locus(T=T)
node['source'].setValue(T, 0)
# Input is temperature in kelvin, output is xyY
node['target'].setValue(X/sum((X,Y,Z)), 0)
node['target'].setValue(Y/sum((X,Y,Z)), 1)
node['target'].setValue(Y, 2)
node['setRGB'].execute()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment