color-js / color.js

Color conversion & manipulation library by the editors of the CSS Color specifications
https://colorjs.io
MIT License
1.78k stars 76 forks source link

new Color("--acescg").space.coords[i].range does not match coordinates of actual colors #513

Closed sidewayss closed 1 week ago

sidewayss commented 1 week ago

This color.js docs entry illustrates it: https://colorjs.io/docs/spaces#acescg The range listed for each coordinate is 0 – 65,504. Yet the color.js picker converts CSS "darkred" to color(--acescg 0.158 0.018 0.005), which is what I see when debugging a test page.

Based on the range values for acescc, -0.36 – 1.47, and the fact that color.js produces the correct display() color in lab(), I assume that it's the range values that are wrong, not the coordinates for the acescg color space. But this official page says the range is correct: https://docs.acescentral.com/specifications/acescg/

Is acescg using 0 – 1 as its range for coordinates? Something is amiss. I have tried this with a variety of colors and all the coordinates are in the 0 – 1 range.

facelessuser commented 1 week ago

I don't understand, why do you think the acescg output is wrong? Yes, they do not cap the gamut at SDR white [1, 1, 1], but allow for enormous headroom upt to 65,504, but yes, white will convert to color(--acescg 1 1 1) even if up to 65,504 is allowed.

Here is another source that has implemented ACES: https://acescolorspace.com/rgb. Results match Color.js.

Screenshot 2024-05-06 at 2 14 01 PM
sidewayss commented 1 week ago

My ignorance on display once again. As I believe we discussed in another thread I wanted to use the range values to format coordinate values, number of decimals, padding, etc. 0 - 65504 would indicate to me that integer formatting is appropriate, but that's a mistaken assumption.

That sure is a lot of headroom. Are there displays that can display it? Or what is the reason for it?

I'll close this issue and make an exception to my formatting rules. Thanks for the clarification.

facelessuser commented 1 week ago

Yes, the headroom is absurdly large. There will never be an ACES display, it is more a space used in workflows: https://academyofanimatedart.com/aces-workflow/. You are supposed to be able to preserve the colors in ACES until you wish to constrain them to a displayable output.

sidewayss commented 1 week ago

Using that docs page's example values: color = new Color("color(--acescg 25700 9480 20800)") color.display() outputs lab(2818.6 1907.5 -597.81) which displays as a bright pink on Firefox, wait a minute.... color.display({space:"srgb"}) outputs rgb(100% 100% 100%) which displays as white.

...I try this in the Color.js color picker on Chrome and it displays white, not pink. So maybe Firefox is messing with my head.

Here is a codepen for lab(2818.6 1907.5 -597.81): https://codepen.io/sidewayss/pen/MWRNMgX

It's hot pink on Firefox and white on Chrome (on my Win11 machine). Is Firefox nuts, or ahead of its time? I'm recently reconsidering it as my default browser.

I see that Absolute XYZ D65 is the other color space with a wide range. I see much less information about that space than acescg on the web. Is that another space that must be constrained upon display? Maybe I'll have to experiment some to figure it out...

sidewayss commented 1 week ago

I just tested on the latest MacOS and iOS versions of Safari via browserstack, and MacOS Safari matches Firefox, while iOS matches Chrome on my Win11 box.

facelessuser commented 1 week ago

I'm not sure exactly what the question is, but yes, those values are way outside the displayable gamut.

color.display({space:"srgb"}) outputs rgb(100% 100% 100%) which displays as white.

Color.js gamut mapping will set the color to white if lightness exceeds the SDR range.

I see that Absolute XYZ D65 is the other color space with a wide range. I see much less information about that space than acescg on the web. Is that another space that must be constrained upon display? Maybe I'll have to experiment some to figure it out...

Absolute XYZ is just XYZ scaled by the luminance. The selected luminance is arbitrary. So instead of a range of 0 - 1 for XYZ, you may scale it by Yw = 100. In the case of Color.js, they use a Yw of 203 as mentioned in ITU-R BT.2408-3.

Screenshot 2024-05-06 at 4 24 36 PM
sidewayss commented 1 week ago

Thanks for the Absolute XYZ info.

Putting aside Color.js, https://codepen.io/sidewayss/pen/MWRNMgX on MacOS Safari and Windows Firefox displays lab(2818.6 1907.5 -597.81) as a hot pink color, while iOS Safari and Windows Chrome display it as white. I am deducing (hopefully correctly) that the pink is the result of gamut mapping, whereas the white is clamping aka clipping. But the future is with gamut mapping and finding a way to display these colors, right? Or are they simply two different approaches that will persist into the future?

This was causing me confusion because I couldn't understand how a non-displayable color could display in one space and not in another. RGB doesn't seem to get gamut mapped on any of the browsers, and maybe not HSL or HWB either. And I'm still not 100% clear about exactly how a gamut map functions, but there is info online about that - unless there's a specific source you'd recommend. Seeing a specific example would be very helpful to understanding it, running the numbers.

Is there a way to get the rgb equivalent for the mapped values? That hot pink color is displaying on my non-HDR monitor, so it's getting mapped into the integer rgb gamut. The Safari and Firefox pinks look the same, so maybe they're using the same map? Does Color.js provide those values as rgb or within gamut in the chosen color space? This assumes a set of common gamut maps.

I'm reading the Color.js gamut mapping page now.

sidewayss commented 1 week ago

Absolute XYZ is just XYZ scaled by the luminance. The selected luminance is arbitrary. So instead of a range of 0 - 1 for XYZ, you may scale it by Yw = 100. In the case of Color.js, they use a Yw of 203 as mentioned in ITU-R BT.2408-3.

So instead of 0 - 1, all three ranges would be 0 - 203? The docs page lists them as:

x   Xa  0 – 9,504.7
y   Ya  0 – 10,000
z   Za  0 – 10,888.3
facelessuser commented 1 week ago

@sidewayss IIRC, some browsers are always clipping lightness if it exceeds 100% Lab. Some browsers do not. I think this requirement may have been removed from the CSS spec. Anyway, all browsers are clipping in sRGB, but since some auto clamp lightness past 100% at parse time, the clip result will be either magenta or white depending.

So instead of 0 - 1, all three ranges would be 0 - 203? The docs page lists them as:

I belive SDR white is equivalent to whatever the white point is multiplied by 203, so something close to [192.9425531914893, 202.99999999999997, 221.07872340425533], but maximum HDR lightness, I think they are multiplying the white by 1000 cd/m2.

sidewayss commented 1 week ago

So that's "magenta", and any color lightness >100% will display that or white. I should have tried a few more values in my codepen. I took for granted that the values for acescg in that example represented a valid display color that happened to be magenta.

facelessuser commented 1 week ago

So that's "magenta", and any color lightness >100% will display that or white.

No, if we are talking about clipping, you only get white if all channels of the RGB color are greater than or equal to 1.

To get magenta, you have to have the red and blue channel greater than or equal to 1 with the green channel very low. You can certainly have colors that cause other channel combinations to be greater than. In this one case, that is how it worked out.

Myndex commented 1 week ago

Brief Background on ACES

So, the first ACES color space (AP0) was designed such that it fully encompasses the 1931 CIE chromaticity diagram. It was intended as a space for archival purposes, stored in an EXR 16 bit float file.

The idea was to be able to capture and store any film without concerns for clipping, regardless of future technology changes, providing a robust archival format. But…

In order to enclose the chromaticity inside a three point triangle, imaginary primaries were used. However as you can see from the diagram, the blue primary is negative. This makes AP0 inconvenient to work with as a practical intermediate for VFX or color grading, as a negative primary complicated what would otherwise be very simple math (I.e. for compositing).

CIE chromaticity showing AP0 and AP1, with sRGB for reference.

So AP1 was created for ACEScg, for use in post production. AP1 still has imaginary primaries, but all are positive, and are still fairly close to the spectral locus. It’s larger than Rec2020/2100, and as with all spaces that use imaginary primaries, it is not possible to create an “ACES display”. 3D Look up tables (3D LUTs) are used to transform to smaller color spaces for viewing.

Side Notes

Linear

When working in a linear (gamma 1.0) space, the math is simple and additive as light is in the real world. Primaries can be somewhat arbitrary, based on the assumptions of Grassmann’s laws, and the area inside the primary coordinates defines the outer limit of the gamut.

CIE RGB and XYZ

CIE RGB also had a negative primary, particularly a problem in 1931 before computers. So they created the transform into XYZ as a useful standard observer space. XYZ still uses imaginary primaries, but none are negative. XYZ transformed to xyY forms the chromaticity diagram above.

Turn a Negative into a Positive…

You may be wondering, if CIE 1931 was derived from color matching experiments by adjusting Red Green Blue primaries to match monochromatic (spectrally pure) colored lights, how could there possibly be a negative or imaginary primaries?

Because for colors that could not be matched using only additive RGB, one of the primary lights was also added to the monochromatic test light. Then, the value of that primary light, as added to the test light, was subtracted from the results of the corresponding primary from the matching RGB values.

sidewayss commented 1 week ago

Thanks for all the info. It's not easy to get a solid lay understanding of these color topics. There are, as the Color.js docs say, dense, print editions available for sale, but not much in the way of higher-level explanations available online. Maybe "mid-level" is what I'm looking for, "intermediate-level".

The 3D look-up table makes sense to me, as there are 3 coordinates. I assumed that all gamut mapping was 3D in that sense. I did a bunch of work programming in and for a multi-dimensional numerical database called Essbase, now owned by Oracle. It was accompanied by a load of front-end spreadsheets/reports and back-end multi-dimensional tables. That part I can grasp more quickly than other aspects of color spaces. That's why I asked for an example in a previous comment in this issue. It would all make a lot more sense to me if I could run some numbers through a spreadsheet and see the calculations in action.

fyi and fwiw - I will be formatting acescg as 0.000 and xyz-abs-d65 as 000.0. It's a fixed space font with five characters available for each coordinate. The 5-char limitation and range-to-format algorithm works well, except in these two cases (and whatever others I haven't fully tested yet). And with these two changes, coords for visible colors are formatted properly for these two spaces. If there's no Color.js or DOM error I assume the user text input is a valid CSS/Color.js value, so lab(2818.6 1907.5 -597.81) is valid and will exceed the 5 char limit, but it's also not a displayable color, so I'm ok with that.

Myndex commented 1 week ago

Thanks for all the info. It's not easy to get a solid lay understanding of these color topics. There are, as the Color.js docs say, dense, print editions available for sale, but not much in the way of higher-level explanations available online. Maybe "mid-level" is what I'm looking for, "intermediate-level"

It's a struggle to find the happy balance between an accurate description and an "academically dense tome".

For a general easy to get into site, Nate Baldwin's Color and Contrast is pretty good.

For a book, perhaps: Billmeyer and Saltzman's Principles of Color Technology 4th Edition
by Roy S. Berns

The 3D look-up table makes sense to me, as there are 3 coordinates. I assumed that all gamut mapping was 3D in that sense

Right, 3D LUTs when you are trying to maintain the relative colors.

1D LUT (i.e. simple gamma or a TRC) when you are mapping the lightness qualities, without much regard for hue.

A 1D LUT and a 9x9 matrix is what is used to, for instance, go from a log or gamma encoded RGB space to XYZ space, or RGB to some other RGB. But if the destination space is smaller, and you have very saturated colors in the larger space, then clipping can result. So then a 3D LUT or some gamut mapping framework.

fyi and fwiw - I will be formatting acescg as 0.000 and xyz-abs-d65 as 000.0. It's a fixed space font with five characters available for each coordinate. The 5-char limitation and range-to-format algorithm works well, except in these two cases (and whatever others I haven't fully tested yet). And with these two changes, coords for visible colors are formatted properly for these two spaces

Okay, so ACEScg typically defines the illuminant (white point) at ~6000K, and XYZ D65 is nominally 6504K. So you need a Bradford transformation as well as the matrix for the primaries.

Since ACEScg and XYZ are both already linear (gamma 1.0) you don't per se need a LUT or TRC, just the 9x9 matrixes. The Bradford can be combined with the primaries for a single 9x9 matrix.

For more on creating the matrixes for your specific application, see Brice Lindbloom.

Regarding Precision

Is fixed $0.000$ for the user interface only and the full precision is hidden?

Asking Because: fixed $0.000$ is a really low precision for these big spaces, and especially for HDR, and super especially because they are both linear. EXR uses 16bit half float, and negative values are permitted.

On negative values: yes, you can clamp to zero, but clipping, even to zero, is not a great way to gamut map, and prevents clean round trips. Even $0.00000$ would be clipping colors that, while they may seem small in linear, get expanded in a gamma encoded display space (e.g. raise to the power of 2.2 or 2.35 or 2.6, etc.)

For instance, a $0.001$ in linear becomes $0.056...$ for display (2.4 gamma). This means the display's black is clipped at #0E0E0E. The smallest 8bit grey above black in sRGB is #010101, and gamma encoded that is $0.00390625$ (gamma 2.2) but linearized for ACES or XYZ that is $0.000005033523219$.

All this a round-about way to say that, when working in linear spaces like XYZ and ACES, you need 16bit half-float precision at a minimum, and 32bit float is common for apps. IMO $0.000$ loses too much.

Is that for the user interface only? and the full float value hidden??

sidewayss commented 1 week ago

@Myndex - thanks for your concern for my interface, but it's not exactly mission critical stuff. It's one of currently three web pages for testing and demonstrating my js animation library, which for non-CSS-supported spaces uses Color.js to do the animation interpolation. I'll be posting a link to it in a discussion group here at github/color.js sometime soon...

The user inputs start and end color values the same way as in the Color.js color picker app, as a CSSish string. Then they select one or two color spaces (you can compare the way two different spaces interpolate the same line between start and end colors). It displays the start, end, and current coordinates for each space in a max 5-character format. I want it to fit in a vertical/portrait mobile screen, thus the limit on the precision of the coordinates. Underlying, unformatted float values are available via a button that copies all the animation values by frame to the clipboard in a tab-delimited format so that I can paste them into a spreadsheet to validate them.

So it won't be perfect, but it will certainly be good enough for displayable colors according to CSS, your browser, and Color.js, in that order. Here's a screenshot of the "controls" part of the page with some surrounding canvas that shows the midpoint of a linear animation/interpolation between these two colors across a98rgb vs jzczhz: image

As for the reading list and other info, I'll take a look at it and hopefully digest it well. Thanks.

sidewayss commented 1 week ago

Better yet, here's a screenshot using acescg: image

Myndex commented 1 week ago

Hi @sidewayss

Ahhh... that's helpful, so it says "lab" are you using LAB values?

sidewayss commented 1 week ago

That row, with the monitor icon and italics, is the "display" color space, which differs from the selected color space if CSS doesn't support the selected space on your device. In this page I am letting Color.js decide which display space to use, though Color.protototype.display() takes an object argument for options and I have a .displaySpace property, so you can specify, for example: color.display{{space:"srgb"}), if you want to force a display space. See here for Color.js docs on this topic: https://colorjs.io/docs/output#get-a-displayable-css-color-value

sidewayss commented 1 week ago

...and to be clear, the coordinates displayed in my web page are for the selected color spaces, not the displayed spaces. I don't display the display values if they differ. That's a whole other ball of wax that has nothing to do with the animation values or my code. The animation generates the coordinates in the selected space then uses Color.prototype.display() to apply the animation values to the target elements' color-related property/attribute.

sidewayss commented 1 week ago

@Myndex - I just started to read through the Baldwin site (I've visited Lindbloom's site and already understand the math/matrices for conversions to/from all the CSS-supported color spaces). Baldwin's start page leads with a Josef Albers-like graphic that animates colors via CSS. I don't assume that there's a lot of interest in animating web page colors, but it's reassuring to see that someone is doing it. I hope that the extended functionality provided by my upcoming js library is of use to at least a few folks out there. Otherwise it has still been an educational venture for me. It extends functionality for more than just colors, but integrating with Color.js (without importing it!) is pretty cool IMO. We'll see if others agree... Thanks again for all the info.

sidewayss commented 1 week ago

@facelessuser - I just stumbled on your ColorAide docs for gamut mapping. It's the most useful reading I've done on the topic so far, especially starting with the specific mapping methodologies here: https://facelessuser.github.io/coloraide/gamut/#minde-chroma-reduction.

The bisect and "iterate until < JND" method is used in bezier curve calculations too, at least in the web browsers. They use "epsilon" instead of "JND", as in the JavaScript Number.EPSILON. Not at all efficient, but it's the only way to animate timing curves in CSS beyond the few named "easeXX" curves. Devices are getting more powerful all the time, so it matters less going forward.

It's good to see that both ColorAide and CSS/Color.js agree on LCh for gamut mapping, even though they choose a different variant. I'm used to seeing so much disagreement or at least discord between the browsers regarding color.

facelessuser commented 1 week ago

The bisect and "iterate until < JND" method is used in bezier curve calculations too

Yeah, bisection is an easy way to approximate things, but as you'll note in the source you linked, bisect for Bezier is only used as a last resort and Newton is used to hopefully converge to an acceptable value without needing bisect, or at least get much closer before bisection is considered.

If a better guess could be initially made, you may even be able to eliminate the bisect. But yeah, I use Newton and fallback to bisect for Bezier as well. https://github.com/facelessuser/coloraide/blob/main/coloraide/easing.py#L67

It's good to see that both ColorAide and CSS/Color.js agree on LCh for gamut mapping, even though they choose a different variant.

Oh, you mean in relation to CIELCh vs OkLCh? If so, keep in mind, I do think OkLCh does a better job and I do make it available as well. I've considered changing the default to OkLCh, though I haven't yet. The only reason I have waffled about it is that even though CIELCh does not do as good a job, it currently performs predictably with more extreme cases while OkLCh breaks down in certain extreme cases. That's not to say CIELCh doesn't have its own issues, just that generally, it seems to give you something you can work with even in most more extreme cases, but it has its own issues as well.

sidewayss commented 1 week ago

I did mean "CIELCh vs OkLCh". Even better than you all fully agree. I wish the browsers could do likewise.

Now I understand in a basic way how gamut mapping is calculated. But to go back to the start of this thread, the gamut map to display full-spectrum acescg is an entirely different sort of map, right? I would think that it's just a matrix calculation like converting between any of the current CSS color spaces that are not RGB-restricted. Or that it would be a straight scaling from 65.504 to 1 within the acescg space.

facelessuser commented 1 week ago

Now I understand in a basic way how gamut mapping is calculated.

At least when talking about chroma reduction. Some of the browser's concerns are valid, but mainly because they want to process images with the same gamut mapping algorithm. CSS, originally, was concerned more with preserving lightness and hue as there are specific benefits there.

But to go back to the start of this thread, the gamut map to display full-spectrum acescg is an entirely different sort of map, right? I would think that it's just a matrix calculation like converting between any of the current CSS color spaces that are not RGB-restricted. Or that it would be a straight scaling from 65.504 to 1 within the acescg space.

As noted before ACES was originally developed for archival purposes. They allow for absurd ranges of colors to be preserved. The headroom is insane in those spaces. I'm not sure how practical it is to try and display the full range up to 65504. A Rec. 2100 PQ value of [1, 1, 1] comes nowhere close to that limit:

>>> Color('rec2100-pq', [1, 1, 1]).convert('acescg')
color(--acescg 49.261 49.261 49.261 / 1)

When dealing with things like HDR lightness, you start moving into the area of tone mapping and such.

I think it is likely that many perceptual spaces may not even give you reasonable values once pushed to extremes like 65504.

facelessuser commented 1 week ago

In short, I'm not sure if representing the full range up to 65504 in ACESCG is useful.

sidewayss commented 1 week ago

In short, I'm not sure if representing the full range up to 65504 in ACESCG is useful.

That puts it in a different perspective.

When dealing with things like HDR lightness, you start moving into the area of tone mapping and such.

So tone mapping would be yet another factor in gamut mapping or an alternate methodology? Necessary for the Absolute XYZ D65 space and 1000 cd/m2 for maximum HDR lightness mentioned above?

facelessuser commented 1 week ago

The current CSS gamut algorithm favors preserving lightness and hue, so often any lightness above the SDR max just becomes white, but if the lightness doesn't exceed the max SDR lightness, it will reduce chroma. If the perceptual space can't handle the extreme chroma very well, you'll get whatever it returns.

If you were gamut mapping to an HDR space, you could theoretically allow the max lightness to assume the HDR lightness. If you are going down to an SDR space and you want to preserve the contrast between colors, you need to make some decisions and scale lightness accordingly. Usually, such approaches will take into account the max lightness and min lightness of an image and scale accordingly.

I honestly haven't done much in this area yet. I have no idea exactly what CSS is recommending exactly.

Myndex commented 6 days ago

Hi Issac @facelessuser and @sidewayss

Color Counting Conundrum

"...I'm not sure if representing the full range up to 65504 in ACESCG is useful....They allow for absurd ranges of colors to be preserved. The headroom is insane in those spaces. I'm not sure how practical it is to try and display the full range up to 65504..."

Indeed, ACES/ACEScg is not a display space, and not intended as such. The same can be said of ProPhoto and CIEXYZ. ACES spaces use imaginary primaries, and they need to be gamut mapped to a display space for viewing. ACES image channels stored in the EXR format most commonly use 16 bit half float linear (gamma 1.0).

With the potential of 65504 code values per color channel, $RGB-EXR = 281,062,861,144,064$ colors. So yea, more than ever likely to be utilized for a single image as viewed, but also, viewing is not the use case.

Skiing Color Slopes

Because the data is linear, it can be more useful to consider the number of photographic stops available—"number of colors" isn't informative per se. EXR encodes 30 stops with 1024 code values per stop, for each R, G, B channel, plus an additional 10 bonus stops at a lower precision. This is a range greater than that of the human eye, greater than the range of photographic film.

Compare this to 256 codes values per channel for the entire range for 8bit gamma encoded RGB (about 7-ish stops). By comparison, a 12-bit PQ-curve image can be about 28 stops, a 10 bit HLG is considered between 14 and 16 stops, a 10 bit DPX log is considered about 13 stops.

The Ys Have it, the Eyes Not So Much

"All these colors" and working in linear color space is not intended to facilitate viewing, it's intended for intermediate operations.

Visual effects, image compositing operations, archival storage, intermediate image exchange, etc. etc. all make good use of linear floating point color spaces. Color grading and compositing applications typically have working-spaces of 32 bit float per channel, or even 64 bit per channel float.

Sidenotes:

Click here for more on ACES/EXR/Linear - _**Additional notes to satisfy my prolixity.**_ ### See Dis-Play Run ACES, ACEScg, and EXR are intermediate stage spaces/containers, not distribution/display spaces/containers. The overhead of linear floating point math and huge gamuts with imaginary portions that need gamut mapping to a display space is a terrible choice for delivery. And this goes for ProPhoto as well. For delivery, sRGB or P3 in a PNG or JPG container are better, with colors already in the intended display space, including the inverse display gamma or TRC, held using Integer values and a limited bit depth. ### Why Linear Needs Floating point The number of bits needed for storing images in a linear color space is substantially higher than needed with a gamma or TRC encoded space, as encoding colors with a transfer curve (like 2.2 gamma or a perceptual curve, whatever) maximizing the available data space by distributing more data to darker colors. This is especially helpful for integer formats. An advantage of linear (a straight line instead of a curve, i.e. gamma of 1) is that light in the real work behaves in a linear way. But integer formats are not well suited to use with linear. Having color light values encoded in linear has direct applications in compositing, where simple math can be used to add corresponding pixel color values together (additive mode) or multiply, ect, with the advantage that the result naturally emulate light in the real world. ### Don't See This The EXR format itself is a non-view format that can take arbitrary channels—in rendering CGI, it is common to render normals, geometry, lights, textures etc. as separate passes, and EXR can hold them all in a single file.
sidewayss commented 6 days ago

So does ACES work with standard gamut mapping methods, like MINDE Chroma Reduction? Or does it have its own special algorithm?

facelessuser commented 6 days ago

So does ACES work with standard gamut mapping methods, like MINDE Chroma Reduction? Or does it have its own special algorithm?

Yes, it works. It is unlikely that you will need to gamut map to ACES as the allowable ranges are ridiculous. You will often be gamut mapping from ACES to a displayable space. Your mileage will vary with extreme values though. If you are working in ACES within reasonable gamuts, I see no reason why you could not gamut map to a displayable space. Some perceptual spaces will likely break down at extreme values as they are often modeled for the visible spectrum.

As an example, here is the ACES space (max range of [1, 1, 1]) gamut mapped to sRGB. It does fine.

Screenshot 2024-05-12 at 8 20 29 AM

If you had colors beyond ACES limit, I guess the gamut mapping to the ACES gamut could be less accurate or even completely inaccurate only because I don't know any perceptual space that is designed for those ranges. The reality is simply ACES is not a space you gamut map to, you often gamut map from it.