2

The XYZ color space encompasses all possible colors, not just those which can be generated by a particular device like a monitor. Not all XYZ triplets represent a color that is physically possible. Is there a way, given an XYZ triplet, to determine if it represents a real color?

I wanted to generate a CIE 1931 chromaticity diagram (seen bellow) for myself, but wasn't sure how to go about it. It's easy to, for example, take all combinations of sRGB triplets and then transform them into the xy coordinates of the chromaticity diagram and then plot them. You cannot use this same approach in the XYZ color space though since not all combinations are valid colors. So far the best I have come up with is a stochastic approach, where I generate a random spectral distribution by summing a random number of random Gaussians, then converting it to XYZ using the standard observer functions.

enter image description here

Chris_F
  • 4,991
  • 5
  • 33
  • 63
  • I don’t understand. If converting sRGB values to xy leads to a valid diagram, why doesn’t converting them to XYZ lead to a valid diagram? The observable colors span a larger space than sRGB, so you don’t get the whole thing, but this is true in both cases. – Cris Luengo Nov 09 '19 at 14:34
  • 1
    This is a duplicate of https://stackoverflow.com/questions/48390558/how-is-the-visible-gamut-bounded? Note that [Colour](https://github.com/colour-science/colour) has now the `colour. is_within_visible_spectrum` definition for that exact purpose. – Kel Solaar Nov 09 '19 at 19:20
  • @Cris The sRGB color space is a subset of the XYZ space and will therefore only fill in a portion of the diagram. – Chris_F Nov 09 '19 at 20:15
  • But how is that different from the case if the xy plot you show? – Cris Luengo Nov 09 '19 at 20:25
  • @CrisLuengo The diagram represents the chromaticity for all possible (as in possible within the human eye) colors. The XYZ color space is a superset of these, which is why some XYZ values do not represent real colors. The sRGB color space is a **sub**set. That means if you mapped all possible sRGB colors into the above diagram they would only occupy a small region within that spectral "lotus" shape. The sRGB color space does *not* include all possible colors. This image demonstrates this: https://upload.wikimedia.org/wikipedia/commons/6/60/Cie_Chart_with_sRGB_gamut_by_spigget.png – Chris_F Nov 09 '19 at 23:12
  • I know sRGB does not cover all colors we can perceive. I said that earlier. What I’m trying to say is that you, in your question, suggest it’s possible to make a xy plot by mapping all sRGB triplets. I’m wondering, if you’re happy with that, why you couldn’t do the same for XYZ. XYZ is just a rotation of the RGB cube, and Yxy is derived trivially from XYZ. I’m unclear why you think XYZ is so different from Yxy. – Cris Luengo Nov 09 '19 at 23:33
  • @CrisLuengo if you do the same for XYZ you will be filling in areas of the graph that do not correspond to any kind of physically plausible color. – Chris_F Nov 10 '19 at 00:12
  • @Chris_F: Did you look at my comment, this answers your question, – Kel Solaar Nov 10 '19 at 01:24
  • @Kel Thanks for the comment. I think Colour's method is similar to what I mention in my answer. – Chris_F Nov 10 '19 at 04:29

1 Answers1

0

Having thought about it a little more I felt the obvious solution is to generate a list of xy points around the edge of spectral locus, corresponding to pure monochromatic colors. It seems to me that this can be done by directly inputting the visible frequencies (~380-780nm) into the CIE XYZ standard observer color matching functions. Treating these points like a convex polygon you could determine if a point is within the spectral locus using one algorithm or another. In my case, since what I really wanted to do is simply generate the chromaticity diagram, I simply input these points into a graphics library's polygon drawing routine and then for each pixel of the polygon I can transform it into sRGB.

I believe this solution is similar to the one used by the library that Kel linked in a comment. I'm not entirely sure, as I am not familiar with Python.

function RGBfromXYZ(X, Y, Z) {
    const R = 3.2404542 * X - 1.5371385 * Y - 0.4985314 * Z
    const G = -0.969266 * X + 1.8760108 * Y + 0.0415560 * Z
    const B = 0.0556434 * X - 0.2040259 * Y + 1.0572252 * Z
    return [R, G, B]
}

function XYZfromYxy(Y, x, y) {
    const X = Y / y * x
    const Z = Y / y * (1 - x - y)
    return [X, Y, Z]
}

function srgb_from_linear(x) {
    if (x <= 0.0031308) {
        return x * 12.92
    } else {
        return 1.055 * Math.pow(x, 1/2.4) - 0.055
    }
}

// Analytic Approximations to the CIE XYZ Color Matching Functions
// from Sloan http://jcgt.org/published/0002/02/01/paper.pdf

function xFit_1931(x) {
    const t1 = (x - 442) * (x < 442 ? 0.0624 : 0.0374)
    const t2 = (x -599.8) * (x < 599.8 ? 0.0264 : 0.0323)
    const t3 = (x - 501.1) * (x < 501.1 ? 0.0490 : 0.0382)
    return 0.362 * Math.exp(-0.5 * t1 * t1) + 1.056 * Math.exp(-0.5 * t2 * t2) - 0.065 * Math.exp(-0.5 * t3 * t3)
}

function yFit_1931(x) {
    const t1 = (x - 568.8) * (x < 568.8 ? 0.0213 : 0.0247)
    const t2 = (x - 530.9) * (x < 530.9 ? 0.0613 : 0.0322)
    return 0.821 * Math.exp(-0.5 * t1 * t1) + 0.286 * Math.exp(-0.5 * t2 * t2)
}

function zFit_1931(x) {
    const t1 = (x - 437) * (x < 437 ? 0.0845 : 0.0278)
    const t2 = (x - 459) * (x < 459 ? 0.0385 : 0.0725)
    return 1.217 * Math.exp(-0.5 * t1 * t1) + 0.681 * Math.exp(-0.5 * t2 * t2)
}

const canvas = document.createElement("canvas")
document.body.append(canvas)
canvas.width = canvas.height = 512
const ctx = canvas.getContext("2d")

const locus_points = []

for (let i = 440; i < 650; ++i) {
    const [X, Y, Z] = [xFit_1931(i), yFit_1931(i), zFit_1931(i)]
    const x = (X / (X + Y + Z)) * canvas.width
    const y = (Y / (X + Y + Z)) * canvas.height
    locus_points.push([x, y])
}

ctx.beginPath()
ctx.moveTo(...locus_points[0])
locus_points.slice(1).forEach(point => ctx.lineTo(...point))
ctx.closePath()
ctx.fill()

const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height)

for (let y = 0; y < canvas.height; ++y) {
    for (let x = 0; x < canvas.width; ++x) {
        const alpha = imageData.data[(y * canvas.width + x) * 4 + 3]
        if (alpha > 0) {
            const [X, Y, Z] = XYZfromYxy(1, x / canvas.width, y / canvas.height)
            const [R, G, B] = RGBfromXYZ(X, Y, Z)
            const r = Math.round(srgb_from_linear(R / Math.sqrt(R**2 + G**2 + B**2)) * 255)
            const g = Math.round(srgb_from_linear(G / Math.sqrt(R**2 + G**2 + B**2)) * 255)
            const b = Math.round(srgb_from_linear(B / Math.sqrt(R**2 + G**2 + B**2)) * 255)
            imageData.data[(y * canvas.width + x) * 4 + 0] = r
            imageData.data[(y * canvas.width + x) * 4 + 1] = g
            imageData.data[(y * canvas.width + x) * 4 + 2] = b
        }
    }
}

ctx.putImageData(imageData, 0, 0)
Chris_F
  • 4,991
  • 5
  • 33
  • 63
  • It seems like you are after two different things here, generating the Chromaticity diagram and checking whether some given CIE XYZ tristimulus values are representing colour values are quite different tasks. For the former, you really just need to convert a grid of xy chromaticity coordinates to sRGB and clip them with the spectral locus which is given by converting the CMFs to xy chromaticity coordinates. For the latter you need to proceed as mentioned in my comment and the linked question. – Kel Solaar Nov 10 '19 at 09:14
  • @KelSolaar I mentioned how this method could be used to determine if a color was real or not by determining if the coordinates of it fall within the shape of the spectral locus. If you can do one you can do the other. – Chris_F Nov 10 '19 at 09:54
  • Sorry, I should have been clearer: your test is only valid if you can work with HDR colours: you will find points in CIE xyY, e.g. xyY = [0.2, 0.1, 0.9], sRGB = [ 1.30842 0.20565 6.57576], that would not fit in the boundaries of the "bounded" Visible Spectrum if you are not able to use HDR colours. Take a look at that page: https://www.colour-science.org:8020/ and keep only the Visible Spectrum and Spectral Locus components. – Kel Solaar Nov 10 '19 at 19:31
  • Something worth adding is that some CMFS are not convex once plotted in the chromaticity diagram, so the 2d method would not work here. – Kel Solaar May 09 '20 at 05:48