Closed sidewayss closed 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.
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.
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.
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...
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.
I'm not sure exactly what the question is, but yes, those values are way outside the displayable gamut.
color.display({space:"srgb"})
outputsrgb(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.
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.
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
@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.
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.
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.
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).
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.
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 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.
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.
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.
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
as0.000
andxyz-abs-d65
as000.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.
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??
@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:
As for the reading list and other info, I'll take a look at it and hopefully digest it well. Thanks.
Better yet, here's a screenshot using acescg:
Hi @sidewayss
Ahhh... that's helpful, so it says "lab" are you using LAB values?
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
...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.
@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.
@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.
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.
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.
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.
In short, I'm not sure if representing the full range up to 65504 in ACESCG is useful.
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?
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.
Hi Issac @facelessuser and @sidewayss
"...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.
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.
"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.
So does ACES work with standard gamut mapping methods, like MINDE Chroma Reduction? Or does it have its own special algorithm?
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.
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.
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" tocolor(--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 correctdisplay()
color inlab()
, 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 the0 – 1
range.