w3c / csswg-drafts

CSS Working Group Editor Drafts
https://drafts.csswg.org/
Other
4.38k stars 642 forks source link

[css-color] Add OKLab, OKLCH #6642

Closed svgeesus closed 2 years ago

svgeesus commented 2 years ago

For the background and explanation of why to add this feature, please see this Color Workshop presentation which has a video presentation, plus associated slides and transcript. You can just skim the slides/transcript if you are pressed for time.

Having done so, and given the rationale presented, the specific proposal is to add OKLab as follows: Edit: items re-ordered so highest priority is first.

CSS Color 4

  1. in the gamut mapping section, require gamut reduction to use OKLCH chroma reduction and deltaE OK, rather than CIE LCH chroma reduction and deltaE 2000. This is both better and faster. If we don't do this, I am not confident of being able to write a satisfactory gamut mapping section in CSS Color 4. See examples below
  2. in the interpolation section, for non-legacy color formats, if the host syntax does not specify a colorspace, change lab to oklab as the default. If we don't do this, color interpolation in gradients, animations and transitions will not be as good, and will sometimes give surprising hue shifts.
  3. add oklab() and oklch() (as well as the existing lab() and lch() which are still useful) and add their implementation to the sample code. Not doing this means we have a colorspace of proven usefulness, implemented inside the browser, but we deny users access to it.

CSS Color 5

  1. add oklab and oklch as defined color spaces which can be used in color-mix(). Depends on 3.
  2. add oklab() and oklch() to Relative Color Syntax. Depends on 3.

@argyleink @una @tabatkins @LeaVerou

svgeesus commented 2 years ago

Note - the gamut mapping section does not yet exist, because gamut mapping in CIE LCH gave unacceptable results in the 270-330 degree hue range and also because deltaE 2000 is complex, but is needed for good results and deltaE 76 is fast but not good enough.

Using OKLCH and deltaEOK (which is the simple root-rum-of-squares) gives better results and is computationally simple.

argyleink commented 2 years ago

lgtm, nice attention to detail

facelessuser commented 2 years ago

Curious, would it use the new lightness discussed here? https://bottosson.github.io/posts/colorpicker/#intermission---a-new-lightness-estimate-for-oklab Or would it be based on the original Oklab work that doesn't adjust the lightness?

svgeesus commented 2 years ago

The new Lr lightness estimator brings in a dependency on viewing conditions, which the original OK L did not have; and which is an advantage of the original OK L especially if it allows extension to HDR or at least EDR. Lr also adds a little computational complexity.

I would like to examine it some more and also see how it impacts deltaE, but my initial impression is to stick with the original OK L. I'm very open to thoughts on which would be the better option though.

facelessuser commented 2 years ago

I would like to examine it some more and also see how it impacts deltaE, but my initial impression is to stick with the original OK L. I'm very open to thoughts on which would be the better option though.

Makes sense. I've only played with Lr to experiment with Okhsl and Okhsv, but use it in no other way. So there may be good reasons to avoid it if using it for distancing. I also hadn't thought of implications in regards to HDR. Something to experiment with though.

tabatkins commented 2 years ago

I quickly read thru the slides, and didn't catch this detail - can you elaborate on why we'd want to add these in addition to the existing lab()/lch(), rather than just changing those functions to use the OKLab space? Is there a good reason to keep the existing Lab functions?

svgeesus commented 2 years ago

why we'd want to add these in addition to the existing lab()/lch(), rather than just changing those functions to use the OKLab space? Is there a good reason to keep the existing Lab functions?

Good question, and the main reason I suggest keeping them around is because there is lots of hardware (instrumentation, like spectrophotometers) and software which generates Lab (or LCH, or both) values. The secondary reason is that Lab and LCH have decades of user experience with them, while OKLab/OKLCH have less than a year.

So:

tabatkins commented 2 years ago

My issue is just that, given a choice between "lab" and "oklab", where the two look practically identical in most cases, people will naturally reach for "lab". We should bless the shortest, most convenient names with the most preferred syntaxes, even if we allow other types in other ways. (And in particular, we've accidentally set a precedent of all the specialized color functions have three-letter names. That probably can't persist forever, but for now, at least, "oklab" would join "color" as looking a little out-of-place and possibly "advanced".

Presumably we'd be able to offer the old Lab via color()? (Not LCH, tho.)

Crissov commented 2 years ago

Three letters you say?

But this idea was already dismissed for cie(). #4481

cie() = cie( /* Lab */ [<percentage> && <number>{2}] 
           | /* Luv */  <percentage>{3} 
           | /* LCH */ [<number>{2} && <angle>] 
           [ / <alpha-value> ]? 
           ) 

okl() = okl( /* Lab */ [<percentage> && <number>{2}] 
           | /* LCH */ [<number>{2} && <angle>] 
           [ / <alpha-value> ]? 
           ) 
svgeesus commented 2 years ago

we've accidentally set a precedent of all the specialized color functions have three-letter names

I don't think any precedent has been set.

facelessuser commented 2 years ago

So, I am curious. It seems there is talk about using chroma reduction using Oklch for gamut mapping/reduction. I was playing around with this and was noticing some interesting things when gamut mapping using this method. Granted, maybe I'm doing something wrong here. This is based on the same algorithm used in color.js for Lch chroma and using the currently specified ∆Eok in that library.

When using something like display-p3, things get mapped very similar as they do with Lch vs Oklch, granted I think their blue range is very similar. But when using something like rec2020 which has a wider blue range, the mapping gets dull blues. (oklch chroma reduction on top).

Screen Shot 2021-09-29 at 7 16 57 AM

This doesn't happen as much with greens and lesser with reds. Now granted there doesn't seem to be an official algorithm posted yet, and so I'm more curious if all these edge cases have been evaluated. Maybe the actual algorithm that is to be used compensates for this. I'm more curious if it improves in some places, but lesser in others, or if there is a better algorithm (different from the CIELCH one used on colorjs.io) that is planned to be used that works well with Oklch?

It's possible I'm just not implementing correctly, and in that case, I will wait patiently to better understand how the reduction is actually done, or keep plugging away to see what I'm doing wrong 🙂.

svgeesus commented 2 years ago

Thanks for investigating. Any chance you could post your test code? In the diagram, are the colors being displayed in sRGB? Is it just the portion of the line from neutral to the sRGB gamut boundary, being displayed? (so Rec2020 blue to sRGB gamut boundary is not on that diagram)? It would be good to know what colors are at the boundary, for the two methods.

facelessuser commented 2 years ago

Yep, let me throw something together publicly that you can play around with.

tabatkins commented 2 years ago

we've accidentally set a precedent of all the specialized color functions have three-letter names

I don't think any precedent has been set.

rgb(), hsl(), hwb(), lab(), lch(). I specifically said it was accidental, but it's still there, and people pattern-match. color() is the exception, and it is an exceptional function compared to the others. This doesn't mean we're forced into this, but it does, I think, imply that people will reach for the 3-latter lab() over oklab().

Regardless, tho, that wasn't my point. My point is just that when having two names that are variants of each other, one of which is longer than the other, people will naturally reach for the shorter one by default; this is a relatively standard principle of language-design UX. If we think that OKLab is better than Lab for authors, then we'll be doing them a disservice by having lab() and oklab() as the naming. Having lab() be OKLab, and letting people still access Lab with color(cielab, ...) would enable people to have access to both when needed, but give authors the more preferable functionality with the easier syntax.

svgeesus commented 2 years ago

I do think that rec2020 gamut mapping needs more investigation (as noted on the next steps slide) and in particular I want to do some rec2020 to display-p3 mapping (with swatches output in display-p3).

I notice that OKLab and CIE Lab have different lightness estimation for high-chroma blues.

Here is rec2020 blue with OKLCH chroma reduced to zero without clip and with clip on deltaEOK < 0.02. In both, the colors on the upper part are the sRGB color (if in gamut) or salmon (if in display-p3) or red (if outside display-p3) while the lower part shows the linear-light display-p3 component values.

I can easily put the same thing together where the upper part is display-p3 and the gamut clip is to the display-p3 gamut (but it will currently only display on Safari TP and BFO Publisher)

svgeesus commented 2 years ago

Having lab() be OKLab, and letting people still access Lab with color(cielab, ...) would enable people to have access to both when needed, but give authors the more preferable functionality with the easier syntax.

oof. That would be super confusing, surely.

However, renaming lab() to cielab() and lch() to cielch() would be doable, and more self-describing, and would make oklab() the shorter one. I guess. (I'm not a fan of bikeshedding churn, which hurts early adopters).

facelessuser commented 2 years ago

So, here is a link to a working example with code. You can edit it and play around with it: https://facelessuser.github.io/coloraide/playground/?source=https%3A%2F%2Fgist.githubusercontent.com%2Ffacelessuser%2F26ff61052f1e4a180d2f98a499d08595%2Fraw%2F454c4a9911e626e537f184b281f51382e83025aa%2Fokfit.py

Basically, I am just interpolating between black and blue in the rec2020 space. Then I convert it to sRGB and fit it in that space and display it directly.

I scale the ∆Eok to by 100 as the difference between black and white is ~0 - 1 and ∆E2000 is ~0-100. I've done it without the scaling, adjusting the limits and such, and the results are basically the same. It seems green starts getting mixed in as chroma reduces.

Again. Maybe there are some assumptions here that just aren't right. This is the exact same algorithm used for the CIELCH fitting.

LeaVerou commented 2 years ago

I have been debating this with myself for a good while, hence the lack of comment.

I did watch @svgeesus' talk (and I really recommend watching it to anyone who wants to have an opinion on this), and I do agree there are significant advantages in Oklab over Lab. However, Lab has been established for 45 years. It has a ton of tooling around it. It has drawbacks, yes, but those are widely studied. Same as all the color formats currently in CSS, they all have drawbacks, but are all old and established, and their drawbacks studied and known. Oklab on the other hand is very new. It has not yet been fully explored, and it has almost no tooling that supports it. Its advantages are known, but its drawbacks are still being researched. It seems a little premature to add it to CSS, especially as a named function. I’m not saying we shouldn't, but it gives me pause.

As a secondary point, we need to decide when we add things as a named function and when as a color() keyword. Right now we have both, and some color spaces can only be expressed via color() and others via named functions. From an author point of view, the distinction is unclear. The spec lists color() as "Profiled, device dependent colors". Does that mean that any color space we add with a restricted gamut is under color() and anything else is not?

I disagree that authors will always reach for the shortest syntax. Firstly, a lot of the time colors are reused via variables, not re-specified. Secondly, if that were true, #RGB would be the most popular color syntax, which is not the case.

One issue with Oklab is that it uses different coordinates than Lab, namely 0-1 ranges instead of 0-100. If we do add such a syntax in CSS, I wonder if there is value in trying to harmonize the two a little.

tabatkins commented 2 years ago

Secondly, if that were true, #RGB would be the most popular color syntax, which is not the case.

I made my point pithily in the middle of a longer argument, but to be more specific: given two seemingly identical or very similar pieces of functionality, people will tend to reach for the one with shorter name/syntax, both because it's easier, and because (partially due to this effect) API designers usually give the preferred solution a shorter name. Hex notation, especially 3-char hex, is substantially different in both abilities and syntax to any other color function, so it doesn't necessarily fit into this. On the other hand, lab() and oklab() functions, which take essentially identical arguments and output approximately identical colors, are exactly the pattern I'm talking about.

(Also who would choose "oklab" over "lab"? If it was betterlab(), sure, but just ok? Pass. ^_^)

(Also, for the generic case of "I need a quick color and I know very roughly what it should be but don't care about the details right now", I personally do reach for 3-char hex first precisely because it is the shortest color syntax that satisfies that use-case.)

One issue with Oklab is that it uses different coordinates than Lab, namely 0-1 ranges instead of 0-100. If we do add such a syntax in CSS, I wonder if there is value in trying to harmonize the two a little.

Our lab() doesn't use a 0-100 syntax anyway, it uses 0%-100%. I don't see why we wouldn't use exactly the same thing here. Whoops I misremembered. Well anyway, yeah, if we did add an oklab() I'd match the coordinate ranges; whether a given paper uses 0-100, 0-1, or 0%-100% is completely arbitrary and up to the whims of the author, but CSS needs to be more predictable than that.

LeaVerou commented 2 years ago

@svgeesus made the point that we need OkLab to be able to specify a reasonable gamut mapping algorithm in Color 4, and as an interpolation space for gradients etc, so it doesn't actually matter whether authors use it, because we can still use it as an interpolation and gamut mapping space.

For me that tips the scales towards yes, let's add this. My question regarding named functions vs color() still stands.

svgeesus commented 2 years ago

if we did add an oklab() I'd match the coordinate ranges; whether a given paper uses 0-100, 0-1, or 0%-100% is completely arbitrary and up to the whims of the author, but CSS needs to be more predictable than that.

The oklab a and b are also on a [0,1] range rather than [0,150 or so, shrug] do we auto-scale them, too?

sRGB lime is oklab(0.866 -0.23 0.179) and CIE lab(87.72% -77.7 80.81) for example; or to compare Chroma rather more easily, oklch(0.866 0.291 142.1) to CIE lch(87.71% 111.9 133.9) which means Chroma is 111.9 / 0.291 = 384.53 times bigger in CIE LCH compared to OKLCH, for that example.

svgeesus commented 2 years ago

it doesn't actually matter whether authors use it, because we can still use it as an interpolation and gamut mapping space

We can just add it internally, yes; but forcing authors to convert it to another colorspace just to use it directly seems counterproductive.

My question regarding named functions vs color() still stands

Note that all the spaces in color() are rectangular orthogonal (i.e. none are cylindrical polar). At one point we had CIE Lab in color, too (but not CIE LCH), then took it out. And, as with the CIE versions, the cylindrical polar form is more useful because Chroma and Hue angle mean something and are separable concepts, while a and b only mean something together.

My concrete suggestion (and this would need sign-off from the two teams that already implemented lab() and lch) is to have:

Yes I know I am always complaining about the chilling effect of bikeshedding on early adoption but that would

  1. add clarity (there was no confusion before, but now there could be)
  2. make the OK forms shorter :)
LeaVerou commented 2 years ago

I would object to renaming lab() to cielab() for the sole purpose of making it longer as that is author hostile. lab() is the option most authors would try first, and would be constantly reminded that it doesn't exist. Also most tools output "Lab", so that is what people will be looking for.

I think it's fine to leave lab() and lch() as is. That is the name they have everywhere else and consistency is important.

Crissov commented 2 years ago

If Tab is saying that values scaled, in external specs, from 0.0 to 1.0 or 0 to 100 should, in CSS, preferably be expressed as <percentage>, I wholeheartedly agree.

Alternative suggestion to renaming lab() and lch(): Ditch color() in favor of established functional notations with known parameters and introduce the (now optional) colorspace identifier to them:

// <colorspace> defaults to "srgb":

rgb() = rgb( [<colorspace>,]? <channel> <channel> <channel> [/ <opacity>]? ) 
// basically replaces color()

hwb() = hwb( [<colorspace>,]? <hue> <channel> <channel> [/ <opacity>]? )

hsl() = hsl( [<colorspace>,]? <hue> <saturation> <lightness> [/ <opacity>]? )

// <colorspace> defaults to either "cie1976" or "ok":

lab() = lab( [<colorspace>,]? <lightness> <number> <number> [/ <opacity>]? )

lch() = lch( [<colorspace>,]? <lightness> <chroma> <hue> [/ <opacity>]? )

// components

<colorspace> = <ident> | <dashed-ident>
<channel>    = <percentage> | <integer[0, 255]>
<hue>        = <angle> | <number[0, 360]>
<saturation>, 
<chroma>     = <percentage>
<lightness>  = <percentage> | <number[0, 100]>
<opacity>    = <percentage> | <number[0.0, 1.0]>

Instead of lab(1 2 3) and either oklab(1 2 3) or cielab(1 2 3), this would lead to lab(1 2 3) being the implicit equivalent of either lab(ok, 1 2 3) or lab(cie, 1 2 3).

PS: Perhaps add luv() and cmyk() color function, too. PS: Perhaps add color-space property to specify colorspace for color functions without the first parameter set explicitly.

svgeesus commented 2 years ago

(Also who would choose "oklab" over "lab"? If it was betterlab(), sure, but just ok? Pass. ^_^)

image

https://twitter.com/jyasskin/status/1432446314961195010

svgeesus commented 2 years ago

But when using something like rec2020 which has a wider blue range, the mapping gets dull blues.

I'm finding the same thing, and I think it is a consequence of better hue preservation in OKLCH. Mapping in CIE LH gives you a higher chroma, but preserves hue less well.

These examples below require an implementation that supports color(display-p3) so try a recent Safari TP. Otherwise you won't see the actual colors, just black.

They show Rec2020 colors being brought into the display-p3 gamut by Chroma reduction (constant hue, constant Lightness) both with and without a final clipping stage. The upper part of each diagram shows the resulting color (if inside display-p3; red if not) and the lower part shows the linear-light display-p3 color components.

rec2020 blue to display-p3, OKLCH rec2020 blue to display-p3, CIE LCH rec2020 green to display-p3, OKLCH rec2020 yellow to display-p3, OKLCH

svgeesus commented 2 years ago

I disagree that authors will always reach for the shortest syntax. Firstly, a lot of the time colors are reused via variables, not re-specified. Secondly, if that were true, #RGB would be the most popular color syntax, which is not the case.

We have recent data on that, so

image

facelessuser commented 2 years ago

I'm finding the same thing, and I think it is a consequence of better hue preservation in OKLCH. Mapping in CIE LH gives you a higher chroma, but preserves hue less well.

Ah, very interesting! Yes, those links do illustrate it pretty good.

weinig commented 2 years ago

Alternative suggestion to renaming lab() and lch(): Ditch color() in favor of established functional notations with known parameters and introduce the (now optional) colorspace identifier to them:

I quite like this suggestion. It makes it so the functions map to a color model (akin to https://developer.apple.com/documentation/coregraphics/cgcolorspacemodel?language=objc), the only holdout would be xyz, since it doesn't really fit into any of those, so we would probably want an xyz() function as well.

Since we have been shipping color() in Safari for a while, we would need to continue to support it for a while.

css-meeting-bot commented 2 years ago

The CSS Working Group just discussed OKLab OKLH, and agreed to the following:

The full IRC log of that discussion <fantasai> Topic: OKLab OKLH
<fantasai> github: https://github.com/w3c/csswg-drafts/issues/6642
<fantasai> Chris: I gave a presentation on this, and documented my reasoning in depth
<fantasai> Chris: My first request is, in the gamut-mapping section, we use OKLCH instead of CIELCH
<fantasai> Chris: And we use OK delta E instead of deltaE 2000
<fantasai> Chris: Because much less computational complexity
<fantasai> Chris: And also much better results in blue/purple region
<fantasai> Chris: I otherwise can't produce a good result that I like
<fantasai> Chris: I posted some results in the last few comments
<florian> q+
<fantasai> Chris: You'll need to use Safari because it uses display-p3
<dbaron> s/???/CIE delta E/
<astearns> ack florian
<dbaron> (that was for the previous ???)
<fantasai> florian: I watched presentation, you made a compelling case
<lea> q?
<lea> q+
<fantasai> florian: The only worry is it's new, and some things maybe need to be discovered
<fantasai> florian: But overall seems compelling, including this first point
<fantasai> Chris: My thoughts also, which is why I put so much effort into it
<astearns> ack lea
<fantasai> Chris: IT's also been implemented in various libraries for JS, Python, etc. by now, so have more confidence now
<fantasai> lea: I have similar concerns as Florian, but given explanation that we primarily want to use it for gamma-mapping, it's OK
<fantasai> lea: ...
<fantasai> lea: So see no problem for having this
<fantasai> astearns: Is it implemented in any OSes?
<fantasai> chris: to my knowledge, no, only in color libraries
<fantasai> chris: but it is extremely trivial
<fantasai> astearns: so you don't expect objections from implementers?
<fantasai> chris: I do not
<fantasai> astearns: Anyone with gamut-mapping opinions?
<lea> s/lea: .../lea: If authors actually want to use the oklab() or oklch() functions instead of the established lab() and lch() ones, I suppose they have a reason, so I see nothing wrong with it either/
<fantasai> astearns: proposed resolution is to use OKLCH and OKLdeltaE for gamut-mapping
<fantasai> RESOLVED: Define gamut-mapping using OKLCH and OKLdeltaE
<lea> s/lea: I have similar concerns /lea: I had similar concerns /
<lea> s/OKLIE/OKLCH/
<fantasai> chris: In the interpolation section, currently say that legacy formats interpolate in sRGB and newer formats interpolate by default in CIE LAB
<fantasai> chris: would like to change that to OKLAB by default, because it will give better results
<fantasai> florian: I support this
<TabAtkins> +1 from me
<lea> No objection
<TabAtkins> "legacy" is anything defined in Color 3
<fantasai> smfr: If gradient uses legacy color at one end and non-legacy at the other end
<fantasai> lea: fancy new algorithm
<fantasai> smfr: spec should say that
<fantasai> lea: it does
<fantasai> astearns: any other comments?
<fantasai> RESOLVED: For non-legacy color formats, use OKLAB for interpolation by default (instead of CIE LAB)
<fantasai> chris: This gives us internal ability, but users can't use it
<lea> smfr: It is mentioned here: https://www.w3.org/TR/css-color-4/#interpolation-space
<fantasai> chris: So suggest to add oklab() and oklce() functions
<lea> s/oklce()/oklch()/
<fantasai> chris: keeping existing lab() and lch() functions as-is, since people are used to it
<fantasai> chris: so question is, should we add this, and also, what syntax?
<fantasai> astearns: If we're using interpolation, don't we need to add syntax for it anyway?
<lea> +1, if the browser implements it, it makes sense to expose it to authors too
<fantasai> to represent colors in the middle of interpolation, e.g.
<fantasai> florian: It does make sense to add it
<fantasai> florian: Only other possibility would be to use color()
<fantasai> chris: color() only takes rectangular forms, not polar form, and polar form is more useful here
<fantasai> florian: I support the suggestion, just want to understand alternatives
<fantasai> florian: If we add directly as a function, options are lab() and oklab(), or cielab() and lab()
<fantasai> florian: Given current tools report (unprefixed) lab as CIE LAB, Chris's suggestion seems to make sense to me
<fantasai> astearns: Anyone with concerns adding this at all?
<fantasai> +1 to add
<argyle> +1 to adding
<fantasai> astearns: Proposed resolution is to expose these by some syntax
<fantasai> astearns: Any objections?
<fantasai> smfr: So if you specify lab() colors, they'll interpret in oklab()? Is that OK?
<fantasai> chris: They would unless you ask them to interpolate a different color space
<fantasai> lea: We did discuss having two colors interpolate in their shared space, if any
<fantasai> lea: but that doesn't work well for RGB formats, because the interpolate badly in RGB
<fantasai> chris: In general you should see very similar results
<fantasai> astearns: Any other concerns to adding?
<fantasai> RESOLVED: Add syntax to represent OKLAB and OKLCH colors
<fantasai> astearns: OK, so next question is, do we like the oklab() and oklch() proposed syntax?
<fantasai> [silence]
<TabAtkins> Sufficiently okay with this. ^_^
<argyle> lol
<drott> :)
<fantasai> RESOLVED: Add oklab() and oklch()
<fantasai> chris: Do we add to color-mix()?
<fantasai> lea: that should be automatic
<fantasai> RESOLVED: Add these as keywords to color-mix()
<dbaron> One of the earlier OKLIE -> OKLCH corrections should have been "OKLIE" -> "OK delta E", I think.
<fantasai> chris: We have a color adjustment syntax, should be able to do that with OKLAB and OKLCH as well
<fantasai> astearns: proposed to add these to ???
<argyle> <3 rts, `oklab(from #f0f l a b / 20%)` 👍🏻
<fantasai> RESOLVED: Add these to color adjustment functions also
<dbaron> s/???/relative color syntax/
<fantasai> astearns: fwiw, I would consider the last two reasonable to do under editor's discretion
svgeesus commented 2 years ago

There was a question on the 13 Oct call about implementation complexity. I added conversion of XYZ to and from OKLab to the end of the sample code section, which as I said on the call is simply a 3x3 matrix multiply, cube root, another 3x3 matrix multiply.

  var LMS = multiplyMatrices(XYZtoLMS, XYZ);
  return multiplyMatrices(LMStoOKLab, LMS.map(c => Math.cbrt(c)));
facelessuser commented 2 years ago

I've mentioned this elsewhere, but maybe it will spark a conversation here.

My one issue is that the provided XYZ matrix assumes an XYZ -> Linear sRGB transfer matrix that doesn't align with what is actually suggested for sRGB in the CSS spec. Since the Oklab matrix is essentially a transform of, XYZ D65 -> Linear sRGB -> LMS, this does introduce some noise only because it is assuming a different XYZ -> Linear sRGB matrix than what is specified in the CSS spec.

If converting from XYZ using the matrix specified in https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab and comparing that to using the CSS XYZ -> Linear sRGB conversion and then using the Linear sRGB to Oklab transform as specified here: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab, you will get different results.

In short, using the XYZ matrix as specified by the Oklab article assumes the white point that he used, but CSS uses a different white point, so this introduces noise. If the conversion was done from Linear sRGB, this noise is reduced.

Or you can just calculate the XYZ matrix using your own XYZ -> sRGB Linear matrix.

Assuming the XYZ -> Linear sRGB transfer that is specified in the CSS docs:

        [
        [  3.2409699419045226,  -1.537383177570094,   -0.4986107602930034  ],
        [ -0.9692436362808796,   1.8759675015077202,   0.04155505740717559 ],
        [  0.05563007969699366, -0.20397695888897652,  1.0569715142428786  ]
    ]

The Oklab matrices below would be as follows which would reduce the noise.

===== XYZ D65 Linear -> lms =====
[[ 0.8190224432164319    0.3619062562801221   -0.12887378261216414 ]
 [ 0.0329836671980271    0.9292868468965546    0.03614466816999844 ]
 [ 0.048177199566046255  0.26423952494422764   0.6335478258136937  ]]
===== lms -> XYZ D65 =====
[[ 1.2268798733741557  -0.5578149965554813   0.28139105017721583]
 [-0.04057576262431372  1.1122868293970594  -0.07171106666151701]
 [-0.07637294974672142 -0.4214933239627914   1.5869240244272418 ]]
svgeesus commented 2 years ago

Thanks! I had started to look at this discrepancy.

I believe Björn used the ASTM D65 XYZ whitepoint while Color 4 and color.js use the four-digit x,y D65 whitepoint.

facelessuser commented 2 years ago

No problem. When you calculate your own, the discrepancy goes away. You just dot your XYZ -> sRGB Linear transform with the sRGB Linear -> LMS transform and then everything syncs up.

svgeesus commented 2 years ago

These edits have now done everything agreed, with the exception of Define gamut mapping which has it's own issue

svgeesus commented 2 years ago

In short, using the XYZ matrix as specified by the Oklab article assumes the white point that he used, but CSS uses a different white point, so this introduces noise. If the conversion was done from Linear sRGB, this noise is reduced.

Thanks! I tested it out and got much better results on neutrals (zeroes to 8dp).

Updated sample code (and also color.js)

facelessuser commented 2 years ago

Sounds good, glad I could help!

bottosson commented 2 years ago

Exciting stuff! This would certainly increase the reach of Oklab significantly. Let me know if I can be of assistance in any way.

One thing to discuss is that I've been considering making a second revision of Oklab for a few reasons. The things I'm considering addressing are:

  1. Switching D65 whitepoint variant to the four-digit x,y D65 whitepoint.
  2. Changing the definition of the M1 matrix so that the whitepoint is exact and not dependent on the rounding of the M1 matrix, instead limiting accuracy only by the precision of the computations.
  3. Adjust the scaling of a&b to more accurately predict color distances. By more rigorously deriving the scale higher accuracy of color distance predictions could be achieved. This wouldn't affect interpolation, hue predictions and lightness predictions, just the scaling of a,b and C.

1) and 2) I am fairly convinced are good ideas, that only minimally change Oklab and correct minor issues in the current definition. 3) I am less certain about, since it would make the resulting Oklab values incompatible and it has already seen somewhat widespread adoption. On the other hand it is mostly used for doing computations, not for communicating colors between applications, so the impact is a bit limited at least.

Do you have any thoughts around this? This would end up being the most significant usage of Oklab I think, so it makes sense to make sure it is fit for purpose!

Thanks!

facelessuser commented 2 years ago

@bottosson

  • Switching D65 whitepoint variant to the four-digit x,y D65 whitepoint.
  • Changing the definition of the M1 matrix so that the whitepoint is exact and not dependent on the rounding of the M1 matrix, instead limiting accuracy only by the precision of the computations.

I believe the most recent M1 matrix in the CSS spec no longer uses the M1 matrix from your article, but now uses a recalculated M1 based on the XYZ to Linear sRGB transfer function which is based on the 4 digit whitepoint now. Or are you referring to some other aspect of the M1 calculation that also should be changed?

svgeesus commented 2 years ago

@bottosson wrote

Switching D65 whitepoint variant to the four-digit x,y D65 whitepoint.

I must admit that I already did this, in the sample code which is part of CSS Color 4. I had been troubled by the neutral colors non-zero Chroma in OKLCH, which was worst at white and thus likely due to whitepoint differences. It seems you had used the ASTM XYZ values, to 6 significant figures; while for the various predefined RGB spaces I was getting the best results with the 4-digit x,y values.

@facelessuser posted in this thread explaining how to derive a new M1 based on your linear-srgb to LMS C++ code and my (well, the sRGB standard) XYZ to sRGB, so that is what I used and it means roundtripping is now excellent.

Changing the definition of the M1 matrix so that the whitepoint is exact and not dependent on the rounding of the M1 matrix, instead limiting accuracy only by the precision of the computations.

Yes in general, rounding is a consistent source of problems and they compound.

Adjust the scaling of a&b to more accurately predict color distances. By more rigorously deriving the scale higher accuracy of color distance predictions could be achieved. This wouldn't affect interpolation, hue predictions and lightness predictions, just the scaling of a,b and C.

That would be a breaking change but I would be interested to know more.

bottosson commented 2 years ago

Yeah, exactly.

Definitely makes sense that that solution works, although would be good to define the matrix in a way that is independent of sRGB.

I think a good way to define M1 would be along these lines (here in python and numpy, but it is just matrix operations):

# The rows have been scaled to sum to 1, and contains no whitepoint information.
M0 = np.array([
  [  0.77849780,  0.34399940,  -0.12249720],
  [  0.03303601,  0.93076195,   0.03620204],
  [  0.05092917,  0.27933344,   0.66973739]
])

d65_xyz = np.array([0.3127, 0.3290, 1-0.3127-0.3290])
d65_XYZ = d65_xyz/d65_xyz[1]

# Calculate M1 by scaling scale each row if M0 so that d65_XYZ transforms exactly to 1,1,1
M1 = M0 / np.outer(M0.dot(d65_XYZ), np.ones(3)) 

This way the accuracy of white mapping to zero chroma is only dependent on the accuracy of the computations, not the rounding of the matrices.

Regarding the scaling of a&b:

Yes, this would certainly be a breaking change, so I am a bit hesitant about it as well. On the other hand the potential improvement is pretty large.

I unfortunately didn't spend that much time calculating and validating that scaling factor when I first derived Oklab since I was mostly focused on the orthogonality between L, C and h (and I didn't expect it to become so widespread so quickly), and it seems like it is off by quite a bit.

I've recently done some tests with color distance datasets as implemented in Colorio and on both the Combvd dataset and the OSA-UCS dataset a scale factor of slightly more than 2 for a and b would give the best results (2.016 works best for Combvd and 2.045 for the OSA-UCS dataset).

svgeesus commented 2 years ago

(re-opening so that more people will notice this valuable discussion)

svgeesus commented 2 years ago

For the scaling of a & b , this statement in the OKlab defining article did give me slight pause:

The a and b plane is scaled so that around 50% gray the ratio of color differences along the lightness axis and the a and b plane is the same as the ratio for color differences predicted by CIEDE2000

because the formula for deltaE2000 introduces a mean-chroma-dependent asymmetry between a and b, strongest right on the neutral axis (recalculated a is 1.5 times b) then fading off to below 1.1 at chroma 27 and below 1.01 at chroma 40.

https://github.com/w3c/csswg-drafts/blob/b9ec4cb357e2a70596ed6863dff8b2bdde8d4060/css-color-4/deltaE2000.js#L27-L40

(For the curious, the values of Cbar, G, and adash are tabulated here)

This means that:

I was mostly focused on the orthogonality between L, C and h

Which is a very useful property of OKLab, for gamut mapping and gradient generation

bottosson commented 2 years ago

The a and b plane is scaled so that around 50% gray the ratio of color differences along the lightness axis and the a and b plane is the same as the ratio for color differences predicted by CIEDE2000

To be clear, this statement captures my intent, but I think it isn't quite how Oklab as published ended up being. I've only recently realized this as I started to look more closely at color distance predictions.

scaling based on some dataset of color pairs will depend on the distribution of mean chroma in that data set

This is in particularly true for small color distance datasets such as Combvd. For large scale distances this is less true, since there is less of a chroma compression effect.

More visually:

Small scale color distances show very strong elongation of spheres into ellipses: image

OSA-UCS dataset models larger color distances and has much less chroma compression. image

(It is also possible to much closer match the small color difference dataset as well, by introducing chroma compression, similar to what both CIEDE2000 and Ciecam02/16-UCS do, but that also adds quite a bit of complexity and assumptions on viewing conditions, this performs worse for tasks such as color interpolation)

facelessuser commented 2 years ago

The new matrix calculation really is much closer when calculating the transform for white. This is super useful to know. It is also fairly easy to get the sRGB Linear -> LMS calculation (for anyone who needs it to calculate Okhsl and Okhsv), you just need to take your higher precision XYZ D65 to LMS matrix and dot it with your RGB to XYZ transform so you are left with the RGB to LMS matrix.

svgeesus commented 2 years ago

Closing again as the edits have been made and discussion seems to have died down.

Ptico commented 1 year ago

Hi, small question out of curiosity, feel free to just ignore or delete it:

I'm an not a color expert, just an average web-developer, like, you know, 90% of those who use CSS daily. For almost 18 years in a web, I never ever seen a LAB or LCH in a production cycle, except some extremely rare cases of dealing with clunky printed brandbooks back in 2000s or the posts of some clever people on css-tricks. At the same time I've seen some pretty good adoption of HSL, because you don't need to be a Will Hunting to decipher on the fly that 100 100 50 is a toxic green or 360 50 30 is a color of the lipstick of your history teacher.

Last couple of years, thanks to @LeaVerou and others, I also see a huge progress in an implementation of a good practices in a a11y area, including proper color contrasts. And here is new perceptually uniform color spaces in CSS: easy to maintain color contrast, create accessible palettes, happy days! Except the fact we now should bring back a solar powered Casio calculator back to the working table and deal with acceptable values or AB mixture.

So the question is: oklab and oklch is great, but why we can't have an okhsl/okhsv for regular people who just want to add some color for their wordpress blog?

Thanks

LeaVerou commented 1 year ago

because you don't need to be a Will Hunting to decipher on the fly that 100 100 50 is a toxic green or 360 50 30 is a color of the lipstick of your history teacher

Both LCH and OkLCH have this property, just with different ranges. If anything, they have this property more because you know that e.g. lch(50% 60 30) is medium lightness due to the 50% regardless of the rest of the values of the other coordinates. HSL does not provide this guarantee at all: hsl(60 100% 50%) is a very light color (#ffff00) and hsl(240 100% 50%) a very dark one (#0000ff) despite having the same lightness. That's the exact problem these new color spaces are trying to solve: making things more predictable.

So the question is: oklab and oklch is great, but why we can't have an okhsl/okhsv for regular people who just want to add some color for their wordpress blog?

What would these do? How do you envision they'd work? (I'm not asking for implementation details, just trying to understand what it is you're asking for)

Ptico commented 1 year ago

What would these do? How do you envision they'd work? (I'm not asking for implementation details, just trying to understand what it is you're asking for)

OkLAB have an accompanying color spaces named OkHSL and OkHSV (https://bottosson.github.io/posts/colorpicker/) which address an issue (human-friendliness) I'm talking about in exchange of some trade-offs. Which is good enough for most of the use-cases. I thought if we can have a traditional HSL in a CSS Color 3 and OkLAB in Color 4 it would be natural to have at least OkHSL at some point

LeaVerou commented 1 year ago

@Ptico Fascinating, I didn't know Bjorn also created OkHSL and OkHSV spaces. A shame they are also restricted to sRGB. Anyhow, you should open another issue so we can discuss this properly.