Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Wide gamut support #73

Open
boronine opened this issue Apr 14, 2020 · 4 comments
Open

Wide gamut support #73

boronine opened this issue Apr 14, 2020 · 4 comments

Comments

@boronine
Copy link
Member

New screens support colour spaces wider than sRGB.

E.g. CSS will be supporting multiple such colour spaces: https://www.w3.org/TR/css-color-4/#icc-colors

Strictly speaking, HSLuv is limited to sRGB because its constants are plugged into its equations:
https://github.com/hsluv/hsluv/blob/master/math/cie.mac#L10

Of course, for most practical purposes one can simply take the RGB output of HSLuv and plug it into a wide-gamut RGB colour space.

Maybe this is really all we need. In which case we should document that on the website and/or README.

Maybe we want more "proper" wide gamut support. This would be quite an undertaking that I would personally not attempt right now, but I will keep this issue open for discussion.

@pretzelhammer
Copy link

Yes! I've been using HSLuv for years for small web projects and recently wanted to get into Generative Art but got distracted by color science and basically spent the last 3 days reading about color spaces.

I love that HSLuv is perceptually uniform (for the most part), intuitive, and easy to work with but it is a bit of bummer that it's limited to the sRGB gamut. It would be amazing if an updated version can be derived to be limited instead by the P3 gamut. Pretty much every Apple product released in the past 4 years has 100% support for the P3 color space, and most flagship phones and monitors released over the past 3 years from companies like Google, Samsung, HTC, OnePlus, Asus, and Acer also have 100% support for the P3 color space. And as you mentioned, support for wide-gamut color spaces is coming to CSS and SVG but has already existed in for a while in image formats like PNG and JPEG which people can leverage in their web designs today. I found this cool interactive demo online that shows the difference between sRGB encoded and wide-gamut color space encoded images (note: it only works if you're viewing it on a wide-gamut monitor).

Maybe we want more "proper" wide gamut support. This would be quite an undertaking that I would personally not attempt right now, but I will keep this issue open for discussion.

I'll admit that most of the math goes over my head but can't we just plug-in the Display-P3 constants into the equations and get an HSLuv over the Display-P3 color space? Display-P3 has the same white point (D65) and blue primary (0.150, 0.060) as sRGB and only differs in its red primary (0.680, 0.320) and green primary (0.265, 0.690) so... why would it be "quite an undertaking"?

Also, along the same vein of modernizing HSLuv, it's currently derived from Luv, but would it be possible to derive it from CAM16-UCS? It would be amazing if in 2021 we could get 3 new HSL-like color spaces, each derived from CAM16-UCS, and bound/normalized/limited to the sRGB, Display-P3, and Rec2020 gamuts respectively.

Please let me know if there's anything I can do to help make the above happen!

@boronine
Copy link
Member Author

Thanks for your interest @pretzelhammer!

... got distracted by color science and basically spent the last 3 days reading about color spaces

This is how HSLuv was born :)

can't we just plug-in the Display-P3 constants ... why would it be "quite an undertaking"?

The hard part is documenting and packaging it in all the different languages.

I think before we consider it we should evaluate if one can simply take RGB values returned by HSLuv and interpret them as, for example, Display-P3. My suspicion is that the result will be just fine in spite of those sRGB primaries. If we were to measure the performance of this sleigh of hand approach, we could publish it to hsluv.org and proclaim wide-gamut support.

would it be possible to derive it from CAM16-UCS?

Should be possible using the same approach as HSLuv takes with CIELUV. I'd be curious to see it. I wouldn't attempt this myself as I doubt the improvement will be worth the work especially the packaging stuff.

@pretzelhammer
Copy link

pretzelhammer commented Jan 2, 2021

The hard part is documenting and packaging it in all the different languages.

Oh, I thought the hard part would be the math, not the administrative stuff. This is just my opinion, but I think only a single well-documented reference implementation needs to exist. The rest of the dev community will take care of all the language ports on an as-needed basis. To get the most mileage out of the reference implementation it usually needs to be in JS, since everyone knows JS.

I think before we consider it we should evaluate if one can simply take RGB values returned by HSLuv and interpret them as, for example, Display-P3.

That would probably work? Given P3's greener green primary and redder red primary the re-interpreted results would probably skew towards being more saturated in the direction of green and red. Hard to say how noticeable this skew will be though.

If we were to measure the performance of this sleigh of hand approach, we could publish it to hsluv.org and proclaim wide-gamut support.

You really hate documenting and packaging stuff that much, huh? :)

I took a stab at deriving the boundary lines for HSLuv for the sRGB gamut, the DisplayP3 gamut, and for the Rec2020 gamut. I think I got all the math right: https://gist.github.com/pretzelhammer/22a6e61e31d97c691bbc95627d91ea04#file-srgb-displayp3-rec2020-luv-jzazbz-mac-L385-L457

Actually, regarding the math, I'm a bit confused on how the results of hsluv.mac are being translated to the reference Haxe implementation. Maxima outputs this:

(%i6) solve(LUV_to_XYZ(L,U,V) . [m1,m2,m3] = t,V)[1]
                                                        3
                                                (L + 16)       27 L
(%o6) V = - (769860 L t + (94839 (if L > 8 then --------- else -----) U
                                                 1560896       24389
                                   3
                           (L + 16)       27 L
 - 838422 L (if L > 8 then --------- else -----)) m3
                            1560896       24389
                                   3
                           (L + 16)       27 L
 - 769860 L (if L > 8 then --------- else -----) m2
                            1560896       24389
                                     3
                             (L + 16)       27 L
 + ((- 284517 (if L > 8 then --------- else -----) U)
                              1560896       24389
                                   3
                           (L + 16)       27 L
 - 731718 L (if L > 8 then --------- else -----)) m1)
                            1560896       24389
                                           3
                                   (L + 16)       27 L
/(126452 t + 632260 (if L > 8 then --------- else -----) m3
                                    1560896       24389
                                 3
                         (L + 16)       27 L
 - 126452 (if L > 8 then --------- else -----) m2)
                          1560896       24389

But then the reference implementation is this:

// maxima says if L > 8 then (L+16)^3 / 1560896 else 27L / 24389
// but this haxe code says if (L+16)^3 / 1560896 > epsilon then (L+16)^3 / 1560896 else L / kappa
// which after simplification becomes if L > 8 then (L+16)^3 / 1560896 else 27L / 24389 so it's mathematically equivalent but at first it was very confusing since it didn't match the maxima output
var sub1:Float = Math.pow(L + 16, 3) / 1560896;
var sub2:Float = sub1 > epsilon ? sub1 : L / kappa;

for (c in 0...3) {
    var m1:Float = m[c][0];
    var m2:Float = m[c][1];
    var m3:Float = m[c][2];

    for (t in 0...2) {
       // how is the above single equation output by maxima being decomposed into these 3 separate "top1", "top2", and "bottom" equations?
        var top1:Float = (284517 * m1 - 94839 * m3) * sub2;
        var top2:Float = (838422 * m3 + 769860 * m2 + 731718 * m1) * L * sub2 - 769860 * t * L;
        var bottom:Float = (632260 * m3 - 126452 * m2) * sub2 + 126452 * t;

       // I'm also confused on this part but I imagine the answer to the above question will also answer this
        result.push({
            slope: top1 / bottom,
            intercept: top2 / bottom
        });
    }
}

[Regarding deriving HSLuv from CAM16-UCS it] should be possible using the same approach as HSLuv takes with CIELUV. I'd be curious to see it.

I'd also be curious to see it! While looking into CAM16-UCS I discovered the Jzazbz color space which seems to match CAM16-UCS's perceptual uniformity while being conceptually and computationally simpler. I believe I implemented the math correctly, I double and triple checked it, but Maxima is not outputting any solutions, and I can't figure out why, maybe it's a Maxima bug? Can you please take a look over this and let me know if you have any ideas on how to fix it: https://gist.github.com/pretzelhammer/22a6e61e31d97c691bbc95627d91ea04#file-srgb-displayp3-rec2020-luv-jzazbz-mac-L460-L480

@boronine
Copy link
Member Author

Hi, sorry for the delay. Couldn't find time to look at this until now.

how is the above single equation output by maxima being decomposed into these 3 separate "top1", "top2", and "bottom" equations?

It should be the same equation with the if ... else ... expression substituted by sub2. Have you tried it on paper? Does it not work?

See also:

hsluv/math/hsluv.mac

Lines 36 to 38 in 84dcb72

/* When you think of L, m1, m2, m3 and t as constants, the solution becomes
a line equation with x axis represented by U and y axis represented by V.
For example, given L=50, crossing this line will push the red channel below 0: */

With that in mind, you can see that equation as V = s * U + i where s is slope of the line, and i is where the line intercepts the y axis. We represent these as lines because it helps visualize what we're doing:

Screen Shot 2021-01-18 at 1 22 00 PM

As for your Jzazbz color space works, that does seem like a promising choice but I'm afraid I won't have time to look over your work. Maxima must be struggling with something so you may need to decompose the problem somehow if you want Maxima to help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants