# Does a constant reflection over the light spectrum lead to grayscale value?

I'm currently fighting with the spectrum->RGB color conversions, my algorithm seems to make an error somewhere, e.g. I get values >1 for some spectral responses.

Now there is a way to calculate an rgb value for normal incidence from index of refraction:

F(0°) = $(\frac{n-1}{n+1})^2$

Where n is the index of refraction (Real-Time Rendering 3rd, p.234 f.) And if $n$ is constant, you can use it for all RGB channels.

However, if I calculate the spectral value with my spectrum->RGB conversion and just use a constant $n$, I get different values for R, G and B.

Specifically, $n = 1.52$ would lead to $RGB_{single} = (0.04257999496;0.04257999496;0.04257999496)$ whereas my program calculates $RGB_{spectrum} = 0.054223;0.042692;0.041095$ (evaluated at $390nm - 780nm$ with $\Delta\lambda = 10nm$).

So my question now is: would a constant index of refraction really lead to a grayscale (i.e. all channels the same) value? Because the conversion functions depend on the CIE functions, taking the integral of those and clearly the integrals are different for $x, y, z$.

Also: since I seem to have a problem with my program in the first place: is there a step by step tutorial for creating the conversion function somewhere?

Edit 01.09.2017 10:25 After being asked to share my code, here goes (in a somewhat top-down approach): First I read the CIE functions from file and store them in a std::vector> with wavelength as key and function value as value. the vector just encapsulates x, y and z independently.

std::vector<std::map<float, float>> cieFunctions = readCieFunctions(ciePath);


Now cieFunctions.at(0) is the x function and so forth. I read in the spectral values basically the same way, into a map with key being the wavelength and value being the reflectivity. Here I take care to match the units, wavelength as $nm$ and reflectivity in values $\in [0, 1]$ (rather than $\in [0, 100]$%).

Having reached this, I call my calculateRgbValue(std::map<float, float>spectralValue) function, where I use the matrix for a D65 white point monitor. I calculate the coordinate, divide it by the integral of Y over the visible wavelength spectrum (constant taken from Physically Based Rendering book) and use that in a multiplcation with said matrix to get the RGB value:

QString SpectralToRGBConverter::calculateRgbValue(std::map<float, float> spectralValues)
{
QString result;
Vec3 rgb;

Mat3 conversionMatrix(
3.240479, -1.537150, -0.498535,
-0.969256, 1.875992, 0.041556,
0.055648, -0.204043, 1.057311);

float X = calcCieCoordinate(spectralValues, cieFunctions.at(0));
float Y = calcCieCoordinate(spectralValues, cieFunctions.at(1));
float Z = calcCieCoordinate(spectralValues, cieFunctions.at(2));

float yIntegral = 106.856895;   //from Phyiscally Based Rendering 3rd, p. 325

Vec3 XYZ(X, Y, Z);
XYZ /= yIntegral;

rgb = conversionMatrix*XYZ;

result = (std::to_string(rgb.x) + ";" + std::to_string(rgb.y) + ";" + std::to_string(rgb.z)).c_str();
return result;
}


The idea of calculating the cie coordinate is best described with the mathematical formula: $coord = \sum_{i=0}^{n-1}x_i c_i \Delta\lambda$ where $n$ is the number of samples in the spectrum, $x_i$ is the $i-th$ cie function value for the wavelength of the $i-th$ sample and $c_i$ is the $i-th$ function value of the spectrum and $\Delta\lambda$ is the stride between two samples $\Delta\lambda = \lambda_{i+1} - \lambda_i$

float SpectralToRGBConverter::calcCieCoordinate(std::map<float, float> spectralValues, std::map<float, float> function)
{
//http://www.brucelindbloom.com/index.html?Eqn_Spect_to_XYZ.html
//Coordinate = Int_lambda function(lambda) * spectralValue(lambda) dlambda
//for discrete values:
//Coordinate = Sum_i function(i) * spectalValue(i) * Delta lambda
//where Delta lambda is the spacing between two measurements
float deltaLambda;// in nm
auto first = spectralValues.begin();
auto second = std::next(first, 1);

{
deltaLambda = (second->first - first->first) * 1000;
}
else
{
deltaLambda = second->first - first->first;
}

float coordinate = 0;
for (auto pairWavelengthReflectance : spectralValues)
{
coordinate += getFunctionValue(function, pairWavelengthReflectance.first) * pairWavelengthReflectance.second * deltaLambda;
}

return coordinate;
}


getFunctionValue is basically a nearest neighbor sampling:

float SpectralToRGBConverter::getFunctionValue(std::map<float, float> function, float x)
{
float conversionFactorWaveLength;
{
//if x is not in nm like function.first
conversionFactorWaveLength = 1000;
}
else
{
conversionFactorWaveLength = 1;
}

float conversionFactorReflectivityScale;
{
//if the reflectivity is given in percentages [0, 100] rather than proportions [0, 1]
conversionFactorReflectivityScale = 0.01;
}
else
{
conversionFactorReflectivityScale = 1;
}

float xLookUp = x * conversionFactorWaveLength;
float previous = 0;
for (auto pairXY : function)
{   //maps are sorted
float current = pairXY.first;
if (current == xLookUp)
{
return function[current] * conversionFactorReflectivityScale;
}
else
if (current > xLookUp)
{
//get the closest value to x
float min;
if (xLookUp - previous < current - xLookUp)
{
min = previous;
}
else
{
min = current;
}
return function[min] * conversionFactorReflectivityScale;
}
previous = current;
}
return -1;
}

• Are you able to share your code? Sep 1, 2017 at 6:43
• I will try to make it readable later and then share it (or at least the important parts). It is (or should be) what they do in Physically Based Rendering Third (p.324), if by chance you have that book.
– Tare
Sep 1, 2017 at 7:57
• No the result is probably not grayscale. What happens is there is a very sophisticated white balance in the system. I mean put tinted glasses on and pretty soon you nolonger see the tint. Sep 1, 2017 at 17:19
• @joojaa thanks but I was looking more for a proof, i.e. someone who has a working spectrum->rgb converter who would calculate the rgb response you get for using an $ior = 1.52 \iff reflection intensity = 0.04257999496$ on the wavelengths $390-780nm$
– Tare
Sep 2, 2017 at 13:46