Jump to content


Spectral Power Distributions and Conversions


20 replies to this topic

#1 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 26 June 2007 - 02:48 AM

Hello, I'm attempting to represent colors by SPD's rather than simply using RGB. Right now I'm simply using a one-dimensional float array centered around the center of the spectrum ((830+360)/2). Alternatively I could just use a two-dimensional array explicitly storing the wavelength.

This is all well and good, but I don't know how to create a SPD for any color, specifically red, green, and blue. I can find graphs online of various SPD's, but no tables. Could use a normal distribution graph centered around the wavelength of the color I want?

I can't find the standard CIE RGB color matching functions, only the xyz ones, and neither can I find a good, standard linear transformation between XYZ and RGB.

One other thing I'm wondering about is how much information I'm losing when I scale the RGB components to [0, 255] and truncate them. Right now I'm using CImg (http://cimg.sourceforge.net/) to display renders, and I'm using unsigned char's for each RGB value. Is this of any concern, or are the underlying operations on SPD's far more affective on the outcome of a color?

Finally I'm looking for a good reference on natural spline interpolation.

Thank you for any help possible.

#2 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 26 June 2007 - 05:24 AM

First of all, creating an SPD for a specific color isn't a totally well-defined problem, since as you know there are many different SPDs that are perceived as the same color (metamers). Red, green, and blue, as well as all the colors of the rainbow, should be acheivable as monochromatic spectra, with all the power in a single wavelength (think laser pointers). For white, you could use CIE Illuminant D65, which is a standard illuminant based on daylight (50% mixture of sunny and overcast).

Another interesting way to generate SPDs is the Planck black-body formula, which lets you compute the radiation emitted by a hot object - for instance, with a temperature of 5700 Kelvin it should mimic sunlight (not including the influence of atmospheric scattering).

For SPDs for a few more light sources as well as spectral reflectance distributions for some common dielectric materials, visit this site. (Reflectance distributions for metals can be calculated using the Fresnel equations, with wavelength-dependent complex refraction indices provided by Luxpop.)

I don't think there is a "standard" CIE RGB color matching function, since the definition of RGB varies. Some RGB color matching functions are tabulated on this site (click the "E/W" buttons to see the tabulated data), but I would stick with using the XYZ color matching functions and transforming into the desired RGB space. Various XYZ-RGB transformations are detailed here. If you're not sure which one to use, sRGB (second from the bottom) is the standard. I couldn't find color matching functions for sRGB, but you could compute them yourself using the XYZ color matching functions and the XYZ->RGB transformation.

Regarding scaling and truncating RGB components, how much damage that does depends how bright your scene is. You'll probably want to look into tone mapping. A simple tone mapping that gives decent results is exposure, which is detailed here. There are many more complex methods of tone mapping too, but I've posted enough links already so I'll let you go research those on your own. ;)
reedbeta.com - developer blog, OpenGL demos, and other projects

#3 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 26 June 2007 - 06:23 PM

Thank you! To use the color matching functions would I actually use a trapezoid or Simpson's rule with the product of the SPD and the color matching function?

And what's the difference between the 10-degree color matching function and the 2-degree color matching function?

#4 geon

    Senior Member

  • Members
  • PipPipPipPip
  • 893 posts

Posted 26 June 2007 - 11:33 PM

How many channels are you planning to use?

I thought about exacly this about 1 year ago, but I quite quickly abandoned the idea as I could only find a very minor gain: Materials that look different depending on the spectra of the light source.

(Example: 2 surfaces could look identical under one light source, but very different from each other under another light.)

That would be the result of the spectra of a light source, multiplied by the "spectral reflectance distributions" of the surface, multiplied by the response curves of the film/retina/CCD-chip have many solutions for the same result.

Wha was your plan for this?

#5 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 26 June 2007 - 11:56 PM

I'm planning on using an arbitrary number of samples, with a default value of probably 3. I'm doing this to make my ray-tracer more physically based; my photon mapper could create a nice spectrum going through glass, and I could create nice SPD's using temperature, etc.

The only extra computation is converting to RGB if I hard-code the number of samples.

#6 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 27 June 2007 - 01:57 AM

flux00 said:

Thank you! To use the color matching functions would I actually use a trapezoid or Simpson's rule with the product of the SPD and the color matching function?

The trapezoidal/Simpson's rule probably isn't necessary; you can just average the CMF over the wavelength range corresponding to each of your channels and multiply that with the SPD (I'm assuming your SPDs will have the same number wavelength bins as output channels in the image; if not, then you may be able to achieve greater accuracy using the trapezoidal rule).

Quote

And what's the difference between the 10-degree color matching function and the 2-degree color matching function?

I'm not entirely sure, but I believe it has to do with the angle of incidence to the human eye - it's how far the point on the retina they're measuring is from the eye's focal point. So, I guess the 2-degree functions are for looking directly at something, and the 10-degree ones are for peripheral vision.
reedbeta.com - developer blog, OpenGL demos, and other projects

#7 roel

    Senior Member

  • Members
  • PipPipPipPip
  • 697 posts

Posted 27 June 2007 - 12:37 PM

geon said:

I thought about exacly this about 1 year ago, but I quite quickly abandoned the idea as I could only find a very minor gain: Materials that look different depending on the spectra of the light source.

Something more important: refraction depends on the wavelength. If you increase the number of channels, prisms look better :) And atmospheric scattering is also a function of the wavelength.

#8 geon

    Senior Member

  • Members
  • PipPipPipPip
  • 893 posts

Posted 27 June 2007 - 01:56 PM

roel said:

refraction depends on the wavelength. If you increase the number of channels, prisms look better

Only for lights with a very un-even spectral distrubution. White light with a flat frequency curve would look fine anyway.

Of course, you could raytrace a scene with 100+ channels to see the induvidual spikes in the spectra of a light source. But that's exctly what I call a "minor gain".

#9 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 27 June 2007 - 04:35 PM

How would you use an index-of-refraction(wavelength) function with only RGB colors? More to the point, what if you wanted a caustic rainbow to be more continuous?

This is also nice because colors are no longer treated like 3-vectors. And yes, in general scenes, there isn't much gain, but there isn't much lost either.

Quick question about c++, if I declare a massive two-dimensional of floats, of say, (830-360+1)*3 values, would that be completely cleared from memory when it went out of scope? I sort of want to load select values from a table at compile-time, but don't want the table taking up memory during run-time.

#10 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 27 June 2007 - 08:34 PM

If it's declared statically, then space will be allocated for it as a BSS segment in the executable. It won't be whiped from memory ever. If it's declared as a local variable in a function, with values that can vary at runtime, it'll be allocated on the stack (hence deallocated when it goes out of scope). If it's local to a function but is compile-time constant it'll probably be in BSS again.
reedbeta.com - developer blog, OpenGL demos, and other projects

#11 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 27 June 2007 - 09:20 PM

Err, so.. could I do something like...
function
{
	array = ...
	array[0][0] += 0.F;
}

A lot of the SPD function usages I've seen involve having values of 0 everywhere except the sample points. This way, the integral is just the sum of the product of each sample point and the color matching function at that point. I imagine this is done for the sake of efficiency, and if I actually went through and used some estimate of the integral, wouldn't it be much larger than just adding up the samples?

#12 geon

    Senior Member

  • Members
  • PipPipPipPip
  • 893 posts

Posted 27 June 2007 - 09:27 PM

flux00 said:

How would you use an index-of-refraction(wavelength) function with only RGB colors? More to the point, what if you wanted a caustic rainbow to be more continuous?

With distrubution raytracing, where the color of the refracted ray is randomized with the intensity of the spectra (of the unrefracted ray) as the weights. The chosen color would have a single frequency (= full saturation), and thus a single Index Of Refraction. To make the average color correct, the result of the trace would need to be scaled by the number of channels.

At least, that was what I thought. I haven't implemented this...

#13 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 27 June 2007 - 10:33 PM

That's an option, but I think that would require a lot of rays to converge to a good result. That would still involve going from color to wavelength to index of refraction.

#14 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 28 June 2007 - 02:51 AM

flux00 said:

Err, so.. could I do something like...
function
{
	array = ...
	array[0][0] += 0.F;
}

I don't understand. Can you be more specific? What are you trying to do?

Quote

A lot of the SPD function usages I've seen involve having values of 0 everywhere except the sample points. This way, the integral is just the sum of the product of each sample point and the color matching function at that point. I imagine this is done for the sake of efficiency, and if I actually went through and used some estimate of the integral, wouldn't it be much larger than just adding up the samples?

It's not that they're taking the SPD to be zero everywhere aside from the sample points, it's just that they're integrating it using a rectangle for each sample point instead of e.g. trapezoids. Strictly speaking you should scale by the width of the rectangle - for instance, if the SPD is sampled at 10 nanometer intervals, you'd scale the result by 10. But that's not really necessary; the numbers output by a raytracer are relative anyway. (Mathematically, you can just define your units of wavelength such that the scale goes away. For instance, if the SPD is sampled at 10 nanometer intervals, define the unit of wavelength as 10 nanometers so that the width of each rectangle is 1.)

Whatever integration method you choose, scale each CMF so that it integrates to one across the visual spectrum, and you should be fine.
reedbeta.com - developer blog, OpenGL demos, and other projects

#15 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 28 June 2007 - 05:13 PM

I was wondering if I could do something pointless like that to keep the compiler from making the array static.

Ok, say I have the sampled SPD and the XYZ color matching functions. I multiply each sample of the SPD by the each respective color matching function, resulting in 3 functions. Simply summing up the samples of each function would mean integration by rectangles. Would integration by the trapezoid rule or Simpson's rule just mean weighting each sample?

e.g. the trapezoid rule would just become

(b-a)/(2n) * (f(x[0]) + 2f(x[1]) + ... + 2f(x[n-2]) + f(x[n-1])

to

.5*f(x[0]) + f(x[1]) + ... + f(x[n-2]) + .5*f(x[n-1])


Or would I still divide the sum by n? How would I know expected range so I could scale each resulting XYZ component? Should I divide each component by the largest, or by the sum or every component? Would either work?

#16 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 28 June 2007 - 06:52 PM

Yes, the trapezoidal rule will get you all the middle samples weighted by one with the first and last weighted by 0.5. Don't divide by n, just multiply by the width of each trapezoid, which as I mentioned you can define to be 1.

I'm not sure what you're asking about the expected range of values - the radiance values in your SPD can be anywhere from 0 to infinity, so the XYZ/RGB values you'll get out of all this will be similiar. You use tone mapping later on to map this into the actual output range of your display.
reedbeta.com - developer blog, OpenGL demos, and other projects

#17 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 29 June 2007 - 09:13 AM

Ok, that makes sense. One last question (possibly): in the ray-tracer PBRT, they use they transformation matrix from RGB to XYZ to weigh the SPD samples. I tried actually summing over the product of the color matching function and the spectrum but this yields better results.
http://www.cs.sfu.ca..._8h-source.html

void XYZ(float xyz[3]) const {

	xyz[0] = xyz[1] = xyz[2] = 0.;

	for (int i = 0; i < COLOR_SAMPLES; ++i) {

		xyz[0] += XWeight[i] * c[i];

		xyz[1] += YWeight[i] * c[i];

		xyz[2] += ZWeight[i] * c[i];

	}

}

float Spectrum::XWeight[COLOR_SAMPLES] = {

	0.412453f, 0.357580f, 0.180423f

};

float Spectrum::YWeight[COLOR_SAMPLES] = {

	0.212671f, 0.715160f, 0.072169f

};

float Spectrum::ZWeight[COLOR_SAMPLES] = {

	0.019334f, 0.119193f, 0.950227f

};


Are these values derived from the color matching function in some way?

#18 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 29 June 2007 - 04:17 PM

I'll have to consult the pbrt book to be sure, but I suspect that those numbers are the integral of the XYZ CMFs over the range of wavelengths covered by each of the three wavelength samples. In other words they're like a downsampled version of the CMF.
reedbeta.com - developer blog, OpenGL demos, and other projects

#19 flux00

    Valued Member

  • Members
  • PipPipPip
  • 108 posts

Posted 30 June 2007 - 05:50 AM

Ok, one last thing I'm a little confused on. The sRGB color space isn't the same as the "color cube" (white=(1, 1, 1), black=(0, 0, 0)) is it?

#20 Reedbeta

    DevMaster Staff

  • Administrators
  • 4979 posts
  • LocationBellevue, WA

Posted 30 June 2007 - 07:27 AM

Well, there's no such thing as "the" color cube. There's a color cube in every RGB space, of which sRGB is just one example. The numbers given for R, G, and B are meaningless unless it's known what they stand for. An RGB space consists of three SPDs called primaries - one each for red, green, and blue, and colors in that RGB space are made by taking linear combinations of those SPDs. For instance, the RGB triple (1, 1, 1) means 1 * R + 1 * G + 1 * B where R, G, and B are the SPD primaries.

The question becomes how to choose the RGB primaries you use. Ideally, you would use primaries that match the spectra of light emitted by the red, green, and blue phosphors or liquid crystals in your monitor. In reality that may not be practical, since you might not know what those spectra are or you might be trying to make the program work for many different monitors. Hence the use of sRGB, which represents a sort of average over typical monitors, and also allows for image processing programs to convert the image into the primaries for a specific display device (which can be done using a linear transformation from sRGB).

The caveat about sRGB, and all common RGB color spaces, is that it's not possible to represent all visible colors using RGB values in the range [0, 1]^3. You can represent all visible colors if you allow RGB values outside that range, but of course in real life, monitors can only display values in [0, 1]^3, since they have a maximum power and they can't use negative voltages. XYZ is a set of primaries for which all visible colors fall inside the [0, 1]^3 cube.
reedbeta.com - developer blog, OpenGL demos, and other projects





1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users