color.js: Converting sRGB to LCH when: Red Green and Blue are Equal (Bug)

When attempting to convert an sRGB to LCH, where the chroma should be 0, produces unexpected values in the Chroma and Hue channels. The chroma value appears to have a floating point error when it should be 0, and the hue value is always NaN, which causes math problems.

Take the following examples:

new Color('srgb', [0.1, 0.1, 0.1]).to('lch').coords
(3) [9.010443527068848, 0.000006337680094534959, NaN]

new Color('srgb', [0.13333, 0.13333, 0.13333]).to('lch').coords
(3) [13.227498037447411, 0.000007406287423610401, NaN]

new Color('srgb', [0.4, 0.4, 0.4]).to('lch').coords
(3) [43.192291386569806, 0.000014999406423138985, NaN]

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 16

Commits related to this issue

Most upvoted comments

Unless I do the first-principles spectral conversion and get different results in the first 5 figures, it will be these ones going forward.

I ran your Python program with these XYZ values, and got the same results as my JavaScript program.

So I’ve gone through the math, I now understand why color.js is so off.

The sRGB matrix that color.js uses is based off a calculated white point for D65:

white = [0.3127 / 0.3290, 0.3290 / 0.3290, 0.3583 / 0.3290]

This gives us

[0.9504559270516716, 1.0, 1.0890577507598784]

But then we use the following D65 white point any time we do chromatic adaption:

[0.95047, 1.00000, 1.08883]

This gives us quite a bit of error.

With the following code, doing nothing tricky, I follow the algorithm calculating the XYZ conversion matrix based on this algorithm: http://www.brucelindbloom.com/index.html?Math.html.

I will output the first results based on using a consistent white point with what is used in chromatic adaption and then I’ll output the same matrix with a calculated white point.

import numpy as np

white_consistent = [0.95047, 1.00000, 1.08883]
white_calc = [0.3127 / 0.3290, 0.3290 / 0.3290, 0.3583 / 0.3290]

def get_matrix(wp):
    # Red to xyY
    xr = 0.64
    yr = 0.33
    # Green to xyY
    xg = 0.30
    yg = 0.60
    # Blue to xyY
    xb = 0.15
    yb = 0.06

    m = [
        [xr/yr, 1.0, (1.0-xr-yr)/yr],
        [xg/yg, 1.0, (1.0-xg-yg)/yg],
        [xb/yb, 1.0, (1.0-xb-yb)/yb]
    ]
    mi = np.linalg.inv(m)

    r, g, b = np.dot(wp, mi)
    rgb = [
        [r],
        [g],
        [b]
    ]
    rgb2xyz = np.multiply(rgb, m).transpose()
    xyz2rgb = np.linalg.inv(rgb2xyz)
    print(rgb2xyz)
    print(xyz2rgb)

print('======== Using a consistent white point with our chromatic adaption')
get_matrix(white_consistent)

print('======== Using the calculated white point')
get_matrix(white_calc)

The first results match what is used in the brucelindbloom calculator and give much better results. The second matches the matrix that is currently used.

======== Using a consistent white point with our chromatic adaption
[[0.41245644 0.35757608 0.18043748]
 [0.21267285 0.71515216 0.07217499]
 [0.0193339  0.11919203 0.95030408]]
[[ 3.24045416 -1.53713851 -0.49853141]
 [-0.96926603  1.87601085  0.04155602]
 [ 0.05564343 -0.20402591  1.05722519]]
======== Using the calculated white point
[[0.4123908  0.35758434 0.18048079]
 [0.21263901 0.71516868 0.07219232]
 [0.01933082 0.11919478 0.95053215]]
[[ 3.24096994 -1.53738318 -0.49861076]
 [-0.96924364  1.8759675   0.04155506]
 [ 0.05563008 -0.20397696  1.05697151]]

Based on this, I am much more confident with my opinion on this. My recommendation would be to use the matrix that is consistent with the white point that is being used everywhere else or update the white point in chromatic adaption to match what is used in the matrix calculations.