-
Notifications
You must be signed in to change notification settings - Fork 664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[css-color-4] Linear transformation matrices are very slightly inaccurate #7675
Comments
Thanks for reporting this. I agree the values should be as accurate as possible. When a lot of conversions are being done, small error residuals can really build up. Recently these matrices were updated as a result of this pull request to use rational numbers where possible. You can see these in the editors draft. We didn't update the ProPhoto ones, because the rational forms exceed the precision of JavaScript math. And that pull request did not update the Bradford CAT matrices. So it would be good to get the most accurate versions of these. I wonder if you would mind checking your results above against the new rational forms? |
At a quick spot check, it looks like my results match up exactly with the values from #7320 where available. I've also confirmed that the XYZ-to-XYZ whitepoint-changing matrices have rational forms that are too large to represent as rationals. |
If these are updated, Oklab should also be updated as its matrices are affected when the XYZ to sRGB matrix changes. Just an FYI as it is easy to forget. |
@facelessuser Can you elaborate? I thought the OKLab matrices just came verbatim from https://bottosson.github.io/posts/oklab/#converting-from-xyz-to-oklab. |
@nex3 Sure. The matrices found on that page are calculated from the RGB transforms from XYZ assuming some white point. That whitepoint did not initially match what the CSS spec was using and caused conversions to not be as good. So, we recalculated, based on the author's guidance. Now, if we are changing the RGB to XYZ transforms again, conversions will be off again as the Oklab matrices will transform the color from XYZ to sRGB Linear with a matrix other than what was done to get the color from sRGB Linear. They must be in sync. Ensuring rational numbers for sRGB and then calculating Oklab matrices, this is what I get: sRGB
Oklab
|
I'm still a little confused because the sRGB linear -> LMS matrix and its inverse don't seem to appear in the sample code, and I believe the other LMS-related matrices don't depend on anything related to sRGB. |
You are correct that he doesn't show exactly how to calculate them, he only mentions:
I'd have to try and dig up the thread here in the CSS issues where Björn Ottosson specified how to calculate the matrices, but the basic algorithm is here https://github.com/facelessuser/coloraide/blob/main/tools/calc_oklab_matrices.py. |
Found the Oklab author's original response. Just so it doesn't look like I'm making stuff up 🙃. #6642 (comment) |
I'm afraid I'm still a bit lost 😅. I see that you're deriving |
Forgive me, the SRGBL_TO_LMS conversion (for Okhsl and Okhsv) needs it, not the XYZ to LMS conversion 🤦🏻. With that said, this whole step, if using rational numbers, can be improved:
Now, I am currently a bit skeptical as to how much a difference this will help in the end as the differences are so far out (in decimal places), and most color spaces when being developed don't even bother going through this effort either, but I don't think is bad either. I'm just not sure in real-world cases whether the ever slightly better matrices will be noticed 🙂 . |
The only reason I would suggest updating the Oklab matrices is they are indeed calculated for CSS just like the RGB matrices. If it is important enough to do it for RGB matrices, then it is probably important enough to do it for Oklab matrices as well 🤷🏻. |
All XYZ values assume adaptation to a particular white point. The D65 white point assumed in the @bottosson post is the ASTM value which is not identical to the value used throughout CSS Color 4. The difference is only in the 5th decimal place, but this was already enough to give significant deviation from the OKLab neutral axis for neutral greys (in sRGB, display-p3, and any other space with a D65 whitepoint) until we fixed it. Which we did by taking the published sRGB to LMS matrix and factoring out the (revised) sRGB to XYZ-D65 part, giving a new M1. |
Related to this: the floating-point errors in the sample code are substantial enough that they're actually visible in the test cases. For example, the xyz-d50-001 test case lists the conversion for #8000 as Obviously it's unrealistic to expect implementations to do these transformations losslessly, but it seems like the test cases maybe should reflect the true mathematical values with the expectation that individual implementations will be less accurate than that given the realities of floating-point arithmetic. |
I'm not sure if that test case was implying that the XYZ D50 value is meant to be the exact translation of One thing that can be noted is that the XYZ value is not off due to rounding errors as the non-rational matrices are no where near that lossy. They actually have pretty decent round tripping. >>> xyz = Color('#008000').convert('xyz-d50')
>>> xyz
color(xyz-d50 0.08314 0.15475 0.02096 / 1)
>>> xyz.convert('srgb').convert('xyz-d50')
color(xyz-d50 0.08314 0.15475 0.02096 / 1) We can also see the test holds true as well since you have to round things off to get the hex values. >>> Color('color(xyz-d50 0.08312 0.154746 0.020961)').convert('srgb').to_string(hex=True)
#008000 EDIT: This isn't arguing against using rational numbers, just some objective observations. I was mainly curious to see how much of a difference using the matrices calculated using rational numbers made with other conversions. On a side note. I did some experiments using rational numbers for the RGB matrices in my own library. Using approximate values for the RGB matrices, I get some pretty clean conversions to lab: >>> Color('white').convert('lab-d65')[:]
[100.0, 0.0, 0.0, 1.0]
>>> Color('white').convert('lab')[:]
[100.0, 0.0, 0.0, 1.0] But when I used rational matrices for the RGB matrices, I get: >>> Color('white').convert('lab-d65')[:]
[100.0, 0.0, 0.0, 1.0]
>>> Color('white').convert('lab')[:]
[100.0, 1.1102230246251565e-13, 0.0, 1.0] Now, does that mean the rational-matrices aren't as good? No, they definitely ensure less error is introduced during the matrix calculation, they just don't prevent all the other errors introduced during the rest of the calculations. Since most of these color spaces are designed using approximate values anyways, it's probably just dumb luck that the approximate matrices give me such nice values. Would I get even better values for the rational-matrix examples if I ensured the chromatic adaptation matrices also used rational numbers and ensured that all other calculations used rational numbers? Maybe, but it's not realistic to ensure the whole conversion path conforms to using rational numbers. In general, I really didn't see significant improvements in overall calculations to warrant using rational numbers in my personal library. I also didn't see any significantly worse calculations to argue against using them either. Since the difference between the rational matrices and the approximate matrices is so slight, I guess this is to be expected. If someone was building an application, I'd personally argue it probably isn't going to matter which is used. |
Converting Converting
That is very close to what I got with the current color.js, which is not using the rational form. |
The Oklab matrices are now 64bit accurate and were copied over from this color.js PR Bradford matrices were updated as a result of |
All matrices are now using either the rational form, if it exists, or the 64 bit accurate form. |
The sample code for color conversions in Color Level 4 lists a number of matrices to use to convert between color values. I've been pre-computing the linear transformations from one colorspace to another for Sass using lossless numeric logic to avoid floating-point rounding errors, and in doing so I noticed some very very slight inaccuracies in the listed matrices. These are all at the level of floating-point rounding errors, so unlikely to be particularly significant in the real world, but I thought it might be desirable to have the example code be as accurate as possible up to the limits of the floating-point representation.
Here are the most precise 64-bit floating-point representations of the various transformations I've looked at so far, generated by this script:
The text was updated successfully, but these errors were encountered: