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); if (ui.radioButtonMicroMeter->isChecked()) { 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 (ui.radioButtonMicroMeter->isChecked()) { //if x is not in nm like function.first conversionFactorWaveLength = 1000; } else { conversionFactorWaveLength = 1; } float conversionFactorReflectivityScale; if (ui.radioButtonPercentage->isChecked()) { //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; }