Alguém conhece alguma fórmula para converter uma frequência de luz em um valor RGB?
Alguém conhece alguma fórmula para converter uma frequência de luz em um valor RGB?
Respostas:
Aqui está uma explicação detalhada de todo o processo de conversão: http://www.fourmilab.ch/documents/specrend/ . Código-fonte incluído!
Para caras preguiçosos (como eu), aqui está uma implementação em java do código encontrado na resposta de @ user151323 (ou seja, apenas uma tradução simples do código pascal encontrado no Spectra Lab Report ):
static private final double Gamma = 0.80;
static private final double IntensityMax = 255;
/**
* Taken from Earl F. Glynn's web page:
* <a href="http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm">Spectra Lab Report</a>
*/
public static int[] waveLengthToRGB(double Wavelength) {
double factor;
double Red, Green, Blue;
if((Wavelength >= 380) && (Wavelength < 440)) {
Red = -(Wavelength - 440) / (440 - 380);
Green = 0.0;
Blue = 1.0;
} else if((Wavelength >= 440) && (Wavelength < 490)) {
Red = 0.0;
Green = (Wavelength - 440) / (490 - 440);
Blue = 1.0;
} else if((Wavelength >= 490) && (Wavelength < 510)) {
Red = 0.0;
Green = 1.0;
Blue = -(Wavelength - 510) / (510 - 490);
} else if((Wavelength >= 510) && (Wavelength < 580)) {
Red = (Wavelength - 510) / (580 - 510);
Green = 1.0;
Blue = 0.0;
} else if((Wavelength >= 580) && (Wavelength < 645)) {
Red = 1.0;
Green = -(Wavelength - 645) / (645 - 580);
Blue = 0.0;
} else if((Wavelength >= 645) && (Wavelength < 781)) {
Red = 1.0;
Green = 0.0;
Blue = 0.0;
} else {
Red = 0.0;
Green = 0.0;
Blue = 0.0;
}
// Let the intensity fall off near the vision limits
if((Wavelength >= 380) && (Wavelength < 420)) {
factor = 0.3 + 0.7 * (Wavelength - 380) / (420 - 380);
} else if((Wavelength >= 420) && (Wavelength < 701)) {
factor = 1.0;
} else if((Wavelength >= 701) && (Wavelength < 781)) {
factor = 0.3 + 0.7 * (780 - Wavelength) / (780 - 700);
} else {
factor = 0.0;
}
int[] rgb = new int[3];
// Don't want 0^x = 1 for x <> 0
rgb[0] = Red == 0.0 ? 0 : (int)Math.round(IntensityMax * Math.pow(Red * factor, Gamma));
rgb[1] = Green == 0.0 ? 0 : (int)Math.round(IntensityMax * Math.pow(Green * factor, Gamma));
rgb[2] = Blue == 0.0 ? 0 : (int)Math.round(IntensityMax * Math.pow(Blue * factor, Gamma));
return rgb;
}
Wavelength<=439
para Wavelength<440
.
Ideia geral:
As etapas 1 e 2 podem variar.
Existem várias funções de correspondência de cores, disponíveis como tabelas ou como aproximações analíticas (sugeridas por @Tarc e @Haochen Xie). As tabelas são melhores se você precisar de um resultado preciso e suave.
Não há um único espaço de cores RGB. Múltiplas matrizes de transformação e diferentes tipos de correção gama podem ser usados.
Abaixo está o código C # que criei recentemente. Ele usa interpolação linear sobre a tabela do "observador padrão CIE 1964" e matriz sRGB + correção de gama .
static class RgbCalculator {
const int
LEN_MIN = 380,
LEN_MAX = 780,
LEN_STEP = 5;
static readonly double[]
X = {
0.000160, 0.000662, 0.002362, 0.007242, 0.019110, 0.043400, 0.084736, 0.140638, 0.204492, 0.264737,
0.314679, 0.357719, 0.383734, 0.386726, 0.370702, 0.342957, 0.302273, 0.254085, 0.195618, 0.132349,
0.080507, 0.041072, 0.016172, 0.005132, 0.003816, 0.015444, 0.037465, 0.071358, 0.117749, 0.172953,
0.236491, 0.304213, 0.376772, 0.451584, 0.529826, 0.616053, 0.705224, 0.793832, 0.878655, 0.951162,
1.014160, 1.074300, 1.118520, 1.134300, 1.123990, 1.089100, 1.030480, 0.950740, 0.856297, 0.754930,
0.647467, 0.535110, 0.431567, 0.343690, 0.268329, 0.204300, 0.152568, 0.112210, 0.081261, 0.057930,
0.040851, 0.028623, 0.019941, 0.013842, 0.009577, 0.006605, 0.004553, 0.003145, 0.002175, 0.001506,
0.001045, 0.000727, 0.000508, 0.000356, 0.000251, 0.000178, 0.000126, 0.000090, 0.000065, 0.000046,
0.000033
},
Y = {
0.000017, 0.000072, 0.000253, 0.000769, 0.002004, 0.004509, 0.008756, 0.014456, 0.021391, 0.029497,
0.038676, 0.049602, 0.062077, 0.074704, 0.089456, 0.106256, 0.128201, 0.152761, 0.185190, 0.219940,
0.253589, 0.297665, 0.339133, 0.395379, 0.460777, 0.531360, 0.606741, 0.685660, 0.761757, 0.823330,
0.875211, 0.923810, 0.961988, 0.982200, 0.991761, 0.999110, 0.997340, 0.982380, 0.955552, 0.915175,
0.868934, 0.825623, 0.777405, 0.720353, 0.658341, 0.593878, 0.527963, 0.461834, 0.398057, 0.339554,
0.283493, 0.228254, 0.179828, 0.140211, 0.107633, 0.081187, 0.060281, 0.044096, 0.031800, 0.022602,
0.015905, 0.011130, 0.007749, 0.005375, 0.003718, 0.002565, 0.001768, 0.001222, 0.000846, 0.000586,
0.000407, 0.000284, 0.000199, 0.000140, 0.000098, 0.000070, 0.000050, 0.000036, 0.000025, 0.000018,
0.000013
},
Z = {
0.000705, 0.002928, 0.010482, 0.032344, 0.086011, 0.197120, 0.389366, 0.656760, 0.972542, 1.282500,
1.553480, 1.798500, 1.967280, 2.027300, 1.994800, 1.900700, 1.745370, 1.554900, 1.317560, 1.030200,
0.772125, 0.570060, 0.415254, 0.302356, 0.218502, 0.159249, 0.112044, 0.082248, 0.060709, 0.043050,
0.030451, 0.020584, 0.013676, 0.007918, 0.003988, 0.001091, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000,
0.000000
};
static readonly double[]
MATRIX_SRGB_D65 = {
3.2404542, -1.5371385, -0.4985314,
-0.9692660, 1.8760108, 0.0415560,
0.0556434, -0.2040259, 1.0572252
};
public static byte[] Calc(double len) {
if(len < LEN_MIN || len > LEN_MAX)
return new byte[3];
len -= LEN_MIN;
var index = (int)Math.Floor(len / LEN_STEP);
var offset = len - LEN_STEP * index;
var x = Interpolate(X, index, offset);
var y = Interpolate(Y, index, offset);
var z = Interpolate(Z, index, offset);
var m = MATRIX_SRGB_D65;
var r = m[0] * x + m[1] * y + m[2] * z;
var g = m[3] * x + m[4] * y + m[5] * z;
var b = m[6] * x + m[7] * y + m[8] * z;
r = Clip(GammaCorrect_sRGB(r));
g = Clip(GammaCorrect_sRGB(g));
b = Clip(GammaCorrect_sRGB(b));
return new[] {
(byte)(255 * r),
(byte)(255 * g),
(byte)(255 * b)
};
}
static double Interpolate(double[] values, int index, double offset) {
if(offset == 0)
return values[index];
var x0 = index * LEN_STEP;
var x1 = x0 + LEN_STEP;
var y0 = values[index];
var y1 = values[1 + index];
return y0 + offset * (y1 - y0) / (x1 - x0);
}
static double GammaCorrect_sRGB(double c) {
if(c <= 0.0031308)
return 12.92 * c;
var a = 0.055;
return (1 + a) * Math.Pow(c, 1 / 2.4) - a;
}
static double Clip(double c) {
if(c < 0)
return 0;
if(c > 1)
return 1;
return c;
}
}
Resultado para a faixa de 400-700 nm:
Embora esta seja uma pergunta antiga e já obtenha algumas respostas boas, quando tentei implementar essa funcionalidade de conversão em meu aplicativo não fiquei satisfeito com os algoritmos já listados aqui e fiz minha própria pesquisa, o que me deu alguns bons resultados. Vou postar uma nova resposta.
Depois de algumas pesquisas, encontrei este artigo, Aproximações analíticas simples às funções de correspondência de cores CIE XYZ , e tentei adotar o algoritmo de ajuste gaussiano por partes de vários lóbulos introduzido em meu aplicativo. O artigo descreveu apenas as funções para converter um comprimento de onda para os valores XYZ correspondentes , então implementei uma função para converter XYZ em RGB no espaço de cores sRGB e os combinei. O resultado é fantástico e vale a pena compartilhar:
/**
* Convert a wavelength in the visible light spectrum to a RGB color value that is suitable to be displayed on a
* monitor
*
* @param wavelength wavelength in nm
* @return RGB color encoded in int. each color is represented with 8 bits and has a layout of
* 00000000RRRRRRRRGGGGGGGGBBBBBBBB where MSB is at the leftmost
*/
public static int wavelengthToRGB(double wavelength){
double[] xyz = cie1931WavelengthToXYZFit(wavelength);
double[] rgb = srgbXYZ2RGB(xyz);
int c = 0;
c |= (((int) (rgb[0] * 0xFF)) & 0xFF) << 16;
c |= (((int) (rgb[1] * 0xFF)) & 0xFF) << 8;
c |= (((int) (rgb[2] * 0xFF)) & 0xFF) << 0;
return c;
}
/**
* Convert XYZ to RGB in the sRGB color space
* <p>
* The conversion matrix and color component transfer function is taken from http://www.color.org/srgb.pdf, which
* follows the International Electrotechnical Commission standard IEC 61966-2-1 "Multimedia systems and equipment -
* Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB"
*
* @param xyz XYZ values in a double array in the order of X, Y, Z. each value in the range of [0.0, 1.0]
* @return RGB values in a double array, in the order of R, G, B. each value in the range of [0.0, 1.0]
*/
public static double[] srgbXYZ2RGB(double[] xyz) {
double x = xyz[0];
double y = xyz[1];
double z = xyz[2];
double rl = 3.2406255 * x + -1.537208 * y + -0.4986286 * z;
double gl = -0.9689307 * x + 1.8757561 * y + 0.0415175 * z;
double bl = 0.0557101 * x + -0.2040211 * y + 1.0569959 * z;
return new double[] {
srgbXYZ2RGBPostprocess(rl),
srgbXYZ2RGBPostprocess(gl),
srgbXYZ2RGBPostprocess(bl)
};
}
/**
* helper function for {@link #srgbXYZ2RGB(double[])}
*/
private static double srgbXYZ2RGBPostprocess(double c) {
// clip if c is out of range
c = c > 1 ? 1 : (c < 0 ? 0 : c);
// apply the color component transfer function
c = c <= 0.0031308 ? c * 12.92 : 1.055 * Math.pow(c, 1. / 2.4) - 0.055;
return c;
}
/**
* A multi-lobe, piecewise Gaussian fit of CIE 1931 XYZ Color Matching Functions by Wyman el al. from Nvidia. The
* code here is adopted from the Listing 1 of the paper authored by Wyman et al.
* <p>
* Reference: Chris Wyman, Peter-Pike Sloan, and Peter Shirley, Simple Analytic Approximations to the CIE XYZ Color
* Matching Functions, Journal of Computer Graphics Techniques (JCGT), vol. 2, no. 2, 1-11, 2013.
*
* @param wavelength wavelength in nm
* @return XYZ in a double array in the order of X, Y, Z. each value in the range of [0.0, 1.0]
*/
public static double[] cie1931WavelengthToXYZFit(double wavelength) {
double wave = wavelength;
double x;
{
double t1 = (wave - 442.0) * ((wave < 442.0) ? 0.0624 : 0.0374);
double t2 = (wave - 599.8) * ((wave < 599.8) ? 0.0264 : 0.0323);
double t3 = (wave - 501.1) * ((wave < 501.1) ? 0.0490 : 0.0382);
x = 0.362 * Math.exp(-0.5 * t1 * t1)
+ 1.056 * Math.exp(-0.5 * t2 * t2)
- 0.065 * Math.exp(-0.5 * t3 * t3);
}
double y;
{
double t1 = (wave - 568.8) * ((wave < 568.8) ? 0.0213 : 0.0247);
double t2 = (wave - 530.9) * ((wave < 530.9) ? 0.0613 : 0.0322);
y = 0.821 * Math.exp(-0.5 * t1 * t1)
+ 0.286 * Math.exp(-0.5 * t2 * t2);
}
double z;
{
double t1 = (wave - 437.0) * ((wave < 437.0) ? 0.0845 : 0.0278);
double t2 = (wave - 459.0) * ((wave < 459.0) ? 0.0385 : 0.0725);
z = 1.217 * Math.exp(-0.5 * t1 * t1)
+ 0.681 * Math.exp(-0.5 * t2 * t2);
}
return new double[] { x, y, z };
}
meu código é escrito em Java 8, mas não deve ser difícil portá-lo para versões anteriores do Java e outras linguagens.
transfer
DoubleUnaryOperator estava fazendo (portanto, a explicação no meu comentário anterior não está correta), portanto, verifique o novo código.
1.
é apenas 1, mas o tipo será em double
vez deint
Você está falando sobre a conversão do comprimento de onda para um valor RGB.
Olha aqui, provavelmente vou responder sua pergunta. Têm um utilitário para fazer isso com o código-fonte, bem como alguma explicação.
Acho que também devo seguir meu comentário com uma resposta formal. A melhor opção é usar o espaço de cores HSV - embora o matiz represente o comprimento de onda, não é uma comparação de um para um.
Fiz um ajuste linear de frequências e valores de matiz conhecidos (excluindo vermelho e violeta porque eles se estendem tanto em valores de frequência que distorcem um pouco as coisas) e obtive uma equação de conversão aproximada.
É semelhante à
frequência (em THz) = 474 + (3/4) (Ângulo de matiz (em graus))
Tentei olhar em volta e ver se alguém descobriu essa equação, mas não encontrei nada até maio de 2010.
Método 1
Esta é uma versão C ++ 11 de @ haochen-xie um pouco limpa e testada. Também adicionei uma função que converte o valor 0 em 1 em um comprimento de onda no espectro visível que pode ser usado com este método. Você pode simplesmente colocar abaixo em um arquivo de cabeçalho e usá-lo sem quaisquer dependências. Esta versão será mantida aqui .
#ifndef common_utils_OnlineStats_hpp
#define common_utils_OnlineStats_hpp
namespace common_utils {
class ColorUtils {
public:
static void valToRGB(double val0To1, unsigned char& r, unsigned char& g, unsigned char& b)
{
//actual visible spectrum is 375 to 725 but outside of 400-700 things become too dark
wavelengthToRGB(val0To1 * (700 - 400) + 400, r, g, b);
}
/**
* Convert a wavelength in the visible light spectrum to a RGB color value that is suitable to be displayed on a
* monitor
*
* @param wavelength wavelength in nm
* @return RGB color encoded in int. each color is represented with 8 bits and has a layout of
* 00000000RRRRRRRRGGGGGGGGBBBBBBBB where MSB is at the leftmost
*/
static void wavelengthToRGB(double wavelength, unsigned char& r, unsigned char& g, unsigned char& b) {
double x, y, z;
cie1931WavelengthToXYZFit(wavelength, x, y, z);
double dr, dg, db;
srgbXYZ2RGB(x, y, z, dr, dg, db);
r = static_cast<unsigned char>(static_cast<int>(dr * 0xFF) & 0xFF);
g = static_cast<unsigned char>(static_cast<int>(dg * 0xFF) & 0xFF);
b = static_cast<unsigned char>(static_cast<int>(db * 0xFF) & 0xFF);
}
/**
* Convert XYZ to RGB in the sRGB color space
* <p>
* The conversion matrix and color component transfer function is taken from http://www.color.org/srgb.pdf, which
* follows the International Electrotechnical Commission standard IEC 61966-2-1 "Multimedia systems and equipment -
* Colour measurement and management - Part 2-1: Colour management - Default RGB colour space - sRGB"
*
* @param xyz XYZ values in a double array in the order of X, Y, Z. each value in the range of [0.0, 1.0]
* @return RGB values in a double array, in the order of R, G, B. each value in the range of [0.0, 1.0]
*/
static void srgbXYZ2RGB(double x, double y, double z, double& r, double& g, double& b) {
double rl = 3.2406255 * x + -1.537208 * y + -0.4986286 * z;
double gl = -0.9689307 * x + 1.8757561 * y + 0.0415175 * z;
double bl = 0.0557101 * x + -0.2040211 * y + 1.0569959 * z;
r = srgbXYZ2RGBPostprocess(rl);
g = srgbXYZ2RGBPostprocess(gl);
b = srgbXYZ2RGBPostprocess(bl);
}
/**
* helper function for {@link #srgbXYZ2RGB(double[])}
*/
static double srgbXYZ2RGBPostprocess(double c) {
// clip if c is out of range
c = c > 1 ? 1 : (c < 0 ? 0 : c);
// apply the color component transfer function
c = c <= 0.0031308 ? c * 12.92 : 1.055 * std::pow(c, 1. / 2.4) - 0.055;
return c;
}
/**
* A multi-lobe, piecewise Gaussian fit of CIE 1931 XYZ Color Matching Functions by Wyman el al. from Nvidia. The
* code here is adopted from the Listing 1 of the paper authored by Wyman et al.
* <p>
* Reference: Chris Wyman, Peter-Pike Sloan, and Peter Shirley, Simple Analytic Approximations to the CIE XYZ Color
* Matching Functions, Journal of Computer Graphics Techniques (JCGT), vol. 2, no. 2, 1-11, 2013.
*
* @param wavelength wavelength in nm
* @return XYZ in a double array in the order of X, Y, Z. each value in the range of [0.0, 1.0]
*/
static void cie1931WavelengthToXYZFit(double wavelength, double& x, double& y, double& z) {
double wave = wavelength;
{
double t1 = (wave - 442.0) * ((wave < 442.0) ? 0.0624 : 0.0374);
double t2 = (wave - 599.8) * ((wave < 599.8) ? 0.0264 : 0.0323);
double t3 = (wave - 501.1) * ((wave < 501.1) ? 0.0490 : 0.0382);
x = 0.362 * std::exp(-0.5 * t1 * t1)
+ 1.056 * std::exp(-0.5 * t2 * t2)
- 0.065 * std::exp(-0.5 * t3 * t3);
}
{
double t1 = (wave - 568.8) * ((wave < 568.8) ? 0.0213 : 0.0247);
double t2 = (wave - 530.9) * ((wave < 530.9) ? 0.0613 : 0.0322);
y = 0.821 * std::exp(-0.5 * t1 * t1)
+ 0.286 * std::exp(-0.5 * t2 * t2);
}
{
double t1 = (wave - 437.0) * ((wave < 437.0) ? 0.0845 : 0.0278);
double t2 = (wave - 459.0) * ((wave < 459.0) ? 0.0385 : 0.0725);
z = 1.217 * std::exp(-0.5 * t1 * t1)
+ 0.681 * std::exp(-0.5 * t2 * t2);
}
}
};
} //namespace
#endif
O gráfico de cores de 375 nm a 725 nm é semelhante a abaixo:
Um problema com este método é o fato de que ele funciona apenas entre 400-700 nm e fora disso cai abruptamente para o preto. Outro problema é o azul mais estreito.
Para comparação, abaixo estão as cores do Vision FAQ em maxmax.com:
Usei isso para visualizar o mapa de profundidade onde cada pixel representa o valor da profundidade em metros e isso se parece com o seguinte:
Método 2
Isso é implementado como parte da biblioteca bitmap_image de cabeçalho de arquivo único de Aeash Partow:
inline rgb_t convert_wave_length_nm_to_rgb(const double wave_length_nm)
{
// Credits: Dan Bruton http://www.physics.sfasu.edu/astro/color.html
double red = 0.0;
double green = 0.0;
double blue = 0.0;
if ((380.0 <= wave_length_nm) && (wave_length_nm <= 439.0))
{
red = -(wave_length_nm - 440.0) / (440.0 - 380.0);
green = 0.0;
blue = 1.0;
}
else if ((440.0 <= wave_length_nm) && (wave_length_nm <= 489.0))
{
red = 0.0;
green = (wave_length_nm - 440.0) / (490.0 - 440.0);
blue = 1.0;
}
else if ((490.0 <= wave_length_nm) && (wave_length_nm <= 509.0))
{
red = 0.0;
green = 1.0;
blue = -(wave_length_nm - 510.0) / (510.0 - 490.0);
}
else if ((510.0 <= wave_length_nm) && (wave_length_nm <= 579.0))
{
red = (wave_length_nm - 510.0) / (580.0 - 510.0);
green = 1.0;
blue = 0.0;
}
else if ((580.0 <= wave_length_nm) && (wave_length_nm <= 644.0))
{
red = 1.0;
green = -(wave_length_nm - 645.0) / (645.0 - 580.0);
blue = 0.0;
}
else if ((645.0 <= wave_length_nm) && (wave_length_nm <= 780.0))
{
red = 1.0;
green = 0.0;
blue = 0.0;
}
double factor = 0.0;
if ((380.0 <= wave_length_nm) && (wave_length_nm <= 419.0))
factor = 0.3 + 0.7 * (wave_length_nm - 380.0) / (420.0 - 380.0);
else if ((420.0 <= wave_length_nm) && (wave_length_nm <= 700.0))
factor = 1.0;
else if ((701.0 <= wave_length_nm) && (wave_length_nm <= 780.0))
factor = 0.3 + 0.7 * (780.0 - wave_length_nm) / (780.0 - 700.0);
else
factor = 0.0;
rgb_t result;
const double gamma = 0.8;
const double intensity_max = 255.0;
#define round(d) std::floor(d + 0.5)
result.red = static_cast<unsigned char>((red == 0.0) ? red : round(intensity_max * std::pow(red * factor, gamma)));
result.green = static_cast<unsigned char>((green == 0.0) ? green : round(intensity_max * std::pow(green * factor, gamma)));
result.blue = static_cast<unsigned char>((blue == 0.0) ? blue : round(intensity_max * std::pow(blue * factor, gamma)));
#undef round
return result;
}
O gráfico do comprimento de onda de 375-725nm se parece com abaixo:
Portanto, é mais utilizável em 400-725 nm. Quando eu visualizo o mesmo mapa de profundidade do método 1, eu fico abaixo. Há um problema óbvio com essas linhas pretas que eu acho que indica um pequeno bug neste código que não examinei mais profundamente. Além disso, as violetas são um pouco mais estreitas neste método, o que causa menos contraste para objetos distantes.
#!/usr/bin/ghci
ångstrømsfromTHz terahertz = 2997924.58 / terahertz
tristimulusXYZfromÅngstrøms å=map(sum.map(stimulus))[
[[1056,5998,379,310],[362,4420,160,267],[-65,5011,204,262]],
[[821,5688,469,405],[286,5309,163,311]],
[[1217,4370,118,360],[681,4590,260,138]]]
where stimulus[ω,μ,ς,σ]=ω/1000*exp(-((å-μ)/if å<μ then ς else σ)^2/2)
standardRGBfromTristimulusXYZ xyz=
map(gamma.sum.zipWith(*)(gamutConfine xyz))[
[3.2406,-1.5372,-0.4986],[-0.9689,1.8758,0.0415],[0.0557,-0.2040,1.057]]
gamma u=if u<=0.0031308 then 12.92*u else (u**(5/12)*211-11)/200
[red,green,blue,black]=
[[0.64,0.33],[0.3,0.6],[0.15,0.06],[0.3127,0.3290,0]]
ciexyYfromXYZ xyz=if xyz!!1==0 then black else map(/sum xyz)xyz
cieXYZfromxyY[x,y,l]=if y==0 then black else[x*l/y,l,(1-x-y)*l/y]
gamutConfine xyz=last$xyz:[cieXYZfromxyY[x0+t*(x1-x0),y0+t*(y1-y0),xyz!!1]|
x0:y0:_<-[black],x1:y1:_<-[ciexyYfromXYZ xyz],i<-[0..2],
[x2,y2]:[x3,y3]:_<-[drop i[red,green,blue,red]],
det<-[(x0-x1)*(y2-y3)-(y0-y1)*(x2-x3)],
t <-[((x0-x2)*(y2-y3)-(y0-y2)*(x2-x3))/det|det/=0],0<=t,t<=1]
sRGBfromÅ=standardRGBfromTristimulusXYZ.tristimulusXYZfromÅngstrøms
x s rgb=concat["\ESC[48;2;",
intercalate";"$map(show.(17*).round.(15*).max 0.min 1)rgb,
"m",s,"\ESC[49m"]
spectrum=concatMap(x" ".sRGBfromÅ)$takeWhile(<7000)$iterate(+60)4000
main=putStrLn spectrum