You asked for an algorithm, you will get one.
I researched this topic when I was rendering the data from the HYG database in Python3.5, with Pyglet and MongoDB. I'm happy with how my stars look in my starmap. The colors can be found at the bottom of this answer.
1. Color Index (B-V) to Temperature (K)
This is the function I used on the B-V (ci) data from the HYG database. In this example, ci is a B-V value from a list I'm running through.
temp = 4600 * (1 / (0.92 * ci + 1.7) + 1 / (0.92 * ci + 0.62))
2. Get a big table.
I took this one and I suggest you do too. Select the temperature column and the RGB or rgb values column as reference
3. Preprocess the data.
From the rgb table data, I generated three ordered lists (n=391) (my method: cleanup and selection with spreadsheet software and a text editor capable of having millions of cursors at a time, then imported the resulting comma-separated file by mongoDB so I could easily work with the lists of values in python through the pymongo wrapper, without too much clutter in the script file). The benefit of the method I will be laying out is that you can pluck color data from other tables that might use CMYK or HSV and adapt accordingly. You could even cross-reference. However, you should end up with lists that look like this from the (s)RGB table I suggested;
reds = [255, 255, ... , 155, 155]
greens = [56, 71, ..., 188,188]
blues = [0, 0, ..., 255, 255]
""" this temps list is also (n=391) and corresponds to the table values."""
temps = []
for i in range(1000,40100,100):
temps.append(i)
After this, I've applied some Gaussian smoothing to these lists (it helps to get better polynomials, since it gets rid of some fluctuation), after which I applied the polyfit() method (polynomial regression) from the numpy package to the temperature values with respect to the R, G and B values:
colors = [reds,greens,blues]
""" you can tweak the degree value to see if you can get better coeffs. """
def smoothListGaussian2(myarray, degree=3):
myarray = np.pad(myarray, (degree-1,degree-1), mode='edge')
window=degree*2-1
weight=np.arange(-degree+1, degree)/window
weight = np.exp(-(16*weight**2))
weight /= sum(weight)
smoothed = np.convolve(myarray, weight, mode='valid')
return smoothed
i=0
for color in colors:
color = smoothListGaussian2(color)
x = np.array(temps)
y = np.array(color)
names = ["reds","greens","blues"]
""" raise/lower the k value (third one) in c """
z = np.polyfit(x, y, 20)
f = np.poly1d(z)
#plt.plot(x,f(x),str(names[i][0]+"-"))
print("%sPoly = " % names[i], z)
i += 1
plt.show()
That gives you (n) coefficients (a) for polynomials of form:
.
Come to think of it now, you could probably use polyfit to come up with the coefficients to convert CI straight to RGB... and skip the CI to temperature conversion step, but by converting to temp first, the relation between temperature and the chosen color space is more clear.
4. The actual Algorithm: Plug temperature values into the RGB polynomials
As I said before, you can use other spectral data and other color spaces to fit polynomial curves to, this step would still be the same (with slight modifications)
Anyway, here's the simple code in full that I used (also, this is with k=20 polynomials):
import numpy as np
redco = [ 1.62098281e-82, -5.03110845e-77, 6.66758278e-72, -4.71441850e-67, 1.66429493e-62, -1.50701672e-59, -2.42533006e-53, 8.42586475e-49, 7.94816523e-45, -1.68655179e-39, 7.25404556e-35, -1.85559350e-30, 3.23793430e-26, -4.00670131e-22, 3.53445102e-18, -2.19200432e-14, 9.27939743e-11, -2.56131914e-07, 4.29917840e-04, -3.88866019e-01, 3.97307766e+02]
greenco = [ 1.21775217e-82, -3.79265302e-77, 5.04300808e-72, -3.57741292e-67, 1.26763387e-62, -1.28724846e-59, -1.84618419e-53, 6.43113038e-49, 6.05135293e-45, -1.28642374e-39, 5.52273817e-35, -1.40682723e-30, 2.43659251e-26, -2.97762151e-22, 2.57295370e-18, -1.54137817e-14, 6.14141996e-11, -1.50922703e-07, 1.90667190e-04, -1.23973583e-02,-1.33464366e+01]
blueco = [ 2.17374683e-82, -6.82574350e-77, 9.17262316e-72, -6.60390151e-67, 2.40324203e-62, -5.77694976e-59, -3.42234361e-53, 1.26662864e-48, 8.75794575e-45, -2.45089758e-39, 1.10698770e-34, -2.95752654e-30, 5.41656027e-26, -7.10396545e-22, 6.74083578e-18, -4.59335728e-14, 2.20051751e-10, -7.14068799e-07, 1.46622559e-03, -1.60740964e+00, 6.85200095e+02]
redco = np.poly1d(redco)
greenco = np.poly1d(greenco)
blueco = np.poly1d(blueco)
def temp2rgb(temp):
red = redco(temp)
green = greenco(temp)
blue = blueco(temp)
if red > 255:
red = 255
elif red < 0:
red = 0
if green > 255:
green = 255
elif green < 0:
green = 0
if blue > 255:
blue = 255
elif blue < 0:
blue = 0
color = (int(red),
int(green),
int(blue))
print(color)
return color
Oh, and some more notes and imagery...
The OBAFGKM black body temperature scale from my polynomials:
The plot for RGB [0-255] over temp [0-40000K],
- + : table data
- curves : polynomial fit
A zoom-in on the least-fidelity values:
Here's the purple
As you can see, there's some deviation, but it is hardly noticeable with the naked eye and if you really want to improve on it (I don't), you have some other options:
- Divide the lists where the green value is highest and see if you get better polynomials for the new left and right parts of the lists. A bit like this:
- Write exception rules (maybe a simple k=2 or k=3 poly) for the values in this least-fidelity window.
- Try other smoothing algorithms before you polyfit().
- Try other sources or color spaces.
I'm also happy with the overall performance of my polynomials. When I'm loading the ~120000 star objects of my starmap with at minimum 18 colored vertices each, it only takes a few seconds, much to my surprise. There is room for improvement, however. For a more realistic view (instead of just running with the blackbody light radiation), I could add gravitational lensing, atmospheric effects, relativistic doppler, etc...
Oh, and the PURPLE, as promised.
Some other useful links: