color.js icon indicating copy to clipboard operation
color.js copied to clipboard

Add support for color from wavelength

Open ChristianMayer opened this issue 4 years ago • 12 comments

A color should be defineable by a wavelength.

There are well known algorithms that convert a wavelength to XYZ coordinates, e.g. at https://en.wikipedia.org/wiki/CIE_1931_color_space#Analytical_approximation

Note: this might also help a bit with #71

ChristianMayer avatar Mar 21 '21 16:03 ChristianMayer

There are well known algorithms that convert a wavelength to XYZ coordinates,

That algorithm is for converting each wavelength (in the range300 to 780, typically) to the XYZ basis functions, before adding them (numerical integration) to get the actual XYZ value.

A color should be definable by a wavelength.

That formula would only do that if the color was on the spectral locus, and had 100% of the light energy at a single wavelength, zero elsewhere.

svgeesus avatar Mar 22 '21 20:03 svgeesus

The linked Color matching functions is just converting one wavelength. So I recon the result will be a point that lies on the well know outer shape of the color space chromaticity diagram.

It's the next chapter (Computing XYZ from spectral data) has the integral over all wavelengths to convert a spectrum to a XYZ color.

This single-wavelength-to-XYZ function is thus useful by itself (when I want to have such a single wavelength color) as well as a base for further computations (like converting a spectrum to a color).

ChristianMayer avatar Mar 22 '21 21:03 ChristianMayer

So I recon the result will be a point that lies on the well know outer shape of the color space chromaticity diagram.

It will, yes. That is called the spectral locus.

It's the next chapter (Computing XYZ from spectral data) has the integral over all wavelengths to convert a spectrum to a XYZ color.

Yes, that is how you get an XYZ color from a set of spectral measurements.

svgeesus avatar Mar 24 '21 22:03 svgeesus

so why is it hard to implement an algorithm for conversion between Wavelength and color values?? just wondering 🤔

I've been looking up for a good algorithm, so I ended up implementing with this one:

Wavelength to RGB I needed an algorithm for an art project I'm working on, so that's why I'm using a wavelength range between 380 and 760 (the color difference between 760 and 800 is almost nothing, it's all red).
const wl2color = (from, to = 'RGB') => {
var w = parseFloat(from);
const v = [380,440,490,510,580,645,760];
if (w >= v[0] && w < v[1])
{
  red   = -(w - v[1]) / (v[1] - v[0]);
  green = 0.0;
  blue  = 1.0;
}
else if (w >= v[1] && w < v[2])
{
  red   = 0.0;
  green = (w - v[1]) / (v[2] - v[1]);
  blue  = 1.0;
}
else if (w >= v[2] && w < v[3])
{
  red   = 0.0;
  green = 1.0;
  blue  = -(w - v[3]) / (v[3] - v[2]);
}
else if (w >= v[3] && w < v[4])
{
  red   = (w - v[3]) / (v[4] - v[3]);
  green = 1.0;
  blue  = 0.0;
}
else if (w >= v[4] && w < v[5])
{
  red   = 1.0;
  green = -(w - v[5]) / (v[5] - v[4]);
  blue  = 0.0;
}
else if (w >= v[5] && w < v[6])
{
  red   = 1.0;
  green = 0.0;
  blue  = 1 - ( (w - v[6]) / (v[5] - v[6]) );
}
else
{
  red   = 0.0;
  green = 0.0;
  blue  = 0.0;
}

// Let the intensity fall off near the vision limits
// const i = [380,420,701,781];

// if (w >= i[0] && w < i[1])
//     factor = 0.3 + 0.7*(w - i[0]) / (i[1] - i[0]);
// else if (w >= i[1] && w < i[2])
//     factor = 1.0;
// else if (w >= i[2] && w < i[3])
//     factor = 0.3 + 0.7*((i[3]-1) - w) / ((i[3]-1) - (i[2]-1));
// else
//     factor = 0.0;

return [red*255,green*255,blue*255]; // here the "to" variable would be used to use a color library to convert to
}

joex92 avatar Sep 25 '22 00:09 joex92

That algorithm is one special case of "map a range of arbitrary input values to a color in a color scale". Notably, it won't give you the actual color that you would see when viewing that wavelength; the algorithm has no colorimetric information and doesn't take any notice of the adapted white point.

svgeesus avatar Jan 16 '23 11:01 svgeesus

I ended up here trying to understand the shape of the CIE chromaticity diagram in different color spaces, and tracing around it by wavelength is one way to approach it.

I found https://colour.readthedocs.io/en/latest/generated/colour.wavelength_to_XYZ.html and wrote this code:

# pip3 install --user colour-science
from colour import wavelength_to_XYZ, MSDS_CMFS
import json

cie1931 = MSDS_CMFS["CIE 1931 2 Degree Standard Observer"]
cie2015 = MSDS_CMFS["CIE 2015 2 Degree Standard Observer"]

data = []

for wavelength in range(360, 781):
    xyz = wavelength_to_XYZ(wavelength, cie1931)
    data.append([1931, wavelength, *xyz])

for wavelength in range(390, 831):
    xyz = wavelength_to_XYZ(wavelength, cie2015)
    data.append([2015, wavelength, *xyz])

print(json.dumps(data))

Converting the points in XYZ to xyY and plotting them showed a familiar horseshoe shape, so I think the data is what it seems to be. I hope this might help others trying to figure this out.

foolip avatar Mar 16 '24 23:03 foolip

Converting the points in XYZ to xyY and plotting them showed a familiar horseshoe shape, so I think the data is what it seems to be. I hope this might help others trying to figure this out.

Yep, that is a great way to get the horseshoe shape. You can interpolate between the points to get finer resolution and/or calculate a spline through them to get a nice rounded shape. Colour also has built-in CIE diagram functions you can call as well (I don't recall them off hand though).

You can plot the chromaticity points for red, green, and blue for a given color spaces to get the shape within the horse shoe shape to get a feel for different spaces in relation to each other.

I know color science has some 3D plotting tools built in as well, I don't recall their names off hands, so you should be able to something similar, but you can take those xyY to the 3D world as well and compare the spaces to each other too. These are not done with Colour, but something similar I imagine could be done.

Screenshot 2024-03-16 at 6 09 39 PM

facelessuser avatar Mar 17 '24 00:03 facelessuser

You can interpolate between the points to get finer resolution and/or calculate a spline through them to get a nice rounded shape.

Given CIE_xyz_1931_2deg.csv, what is the correct way to interpolate? Looking into the details, things are a bit confusing:

  • Wikipedia makes it sound like the original data was in 5nm intervals from 360-830nm, and that 1nm datasets are interpolated. In the CSV it does look like every 5th row is different, using 4 significant digits at least early on in the table. A quick test shows that it's not linearly interpolated, but how then?
  • The dataset metadata (CIE_xyz_1931_2deg.csv_metadata.json) has interpolationMethod set to "linear". Does that really mean that the best interpolation method between integer wavelengths is linear? The docs are at https://github.com/CIEmetaSchemas/CIEmetaDigitalProduct/tree/main/v4.

foolip avatar Mar 18 '24 09:03 foolip

Yeah, it does state they are derived from interpolation.

Regardless, if you want anything in between whatever data you have, all you can do is interpolate. Using something like a catrom spline would run the spline through the data points and smooth out the plot if all you had is 5nm.

Screenshot 2024-03-18 at 6 43 55 AM Screenshot 2024-03-18 at 6 43 16 AM

If you are using 1nm values, which is what I use anyways, and what colour science provides, that should probably give you smooth enough plots though, but if not, you smooth out the rough edges.

Anyway, since you are using colour science, you can just have it plot the diagrams for you and then then plot whatever you want over them, like gamuts of RGB spaces, etc.

import colour

figure, axes = colour.plotting.plot_chromaticity_diagram_CIE1931(
        diagram_opacity=0.0,
        standalone=False,
        bounding_box=[-0.4, 1.0, -0.2, 1.25])

colour.plotting.render();
Screenshot 2024-03-18 at 7 03 00 AM

or for 10 degree:

import colour

figure, axes = colour.plotting.plot_chromaticity_diagram_CIE1931(
        cmfs="CIE 2015 10 Degree Standard Observer",
        diagram_opacity=0.0,
        standalone=False,
        bounding_box=[-0.4, 1.0, -0.2, 1.25])

colour.plotting.render();

facelessuser avatar Mar 18 '24 13:03 facelessuser

Thanks @facelessuser, do you know if colour can also plot chromaticity diagrams in other color spaces, specifically Oklab?

That's what I was really after, and I made my own visualization:

image

The cross is where a=b=0, and it looks a bit like a one-eyed ghost?

Regardless of the color space it's visualized in, there are some odd things that fall out of using the CIE 1931 2° Standard Observer data for a chromaticity diagram with constant lightness. At the beginning (violet, short wavelengths) the points are all very close to each other, and the line gets a bit erratic. A "color from wavelength" function should probably do some kind of adjustment there to get more smooth behavior.

foolip avatar Mar 18 '24 16:03 foolip

These are the chromaticity diagrams that are offered.

colour.plotting.plot_chromaticity_diagram_CIE1931
colour.plotting.plot_chromaticity_diagram_CIE1960UCS
colour.plotting.plot_chromaticity_diagram_CIE1976UCS

From my understanding 2˚ does not best represent the human eye, 10˚ would as there are more subtle differences can occur with wider area of view, like in the case of the blue region.

facelessuser avatar Mar 18 '24 17:03 facelessuser

You can see that the blue shifts a bit with the 10 deg observer. The 2015 CMFs seem to both be shifted.

Screenshot 2024-03-18 at 3 19 43 PM

Live example here

facelessuser avatar Mar 18 '24 21:03 facelessuser