Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Add CAM16 (JMh) #379

Merged
merged 5 commits into from Dec 18, 2023
Merged

Add CAM16 (JMh) #379

merged 5 commits into from Dec 18, 2023

Conversation

facelessuser
Copy link
Collaborator

This is the first step in porting over support for the HCT color space. This adds the CAM16 model along with the output in JMh for verification.

Copy link

netlify bot commented Dec 17, 2023

Deploy Preview for colorjs ready!

Name Link
🔨 Latest commit 5afedaa
🔍 Latest deploy log https://app.netlify.com/sites/colorjs/deploys/658080d17c667f000807e336
😎 Deploy Preview https://deploy-preview-379--colorjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

@facelessuser
Copy link
Collaborator Author

One thing to note, undefined values in CSS and this library are assumed to be zero when a real number is required. CAM16 has a non-linear achromatic response that often has achromatic values a non-zero chroma/colorfulness/saturation values. What this means is that chroma and hue both matter when converting achromatic values. Knowing what those values are allows for better round trip conversions.

There currently isn't a mechanism in Color.js to handle such things. Ideally, in the future, two things should be considered:

  1. A way to determine when a color is achromatic and assign undefined hues appropriately. We could do this fairly easy in the forward conversion to CAM16 JMh. We do not do this currently because of issue number 2.
  2. A way to resolve undefined values for achromatics when zero is not sufficient. The hue matters very much in the conversion from CAM16 JMh back to XYZ. How should color.js convert an undefined CAM16 JMh hue?

I do not think these questions need to be resolved now to implement the color space, but it should be noted that when undefined values in CAM16 are used, and in the future HCT, that it may not yield the achromatic values that are desired.

@facelessuser
Copy link
Collaborator Author

I will also note the "colorfulness" range is based on P3. This can be changed to whatever is desired.

@facelessuser
Copy link
Collaborator Author

facelessuser commented Dec 18, 2023

Adding a little more info on validation.

A number of references used:

Results were compared against Colour Science and my implementation in ColorAide as well: https://github.com/colour-science/colour

>>> import numpy as np
>>> import colour
>>> from coloraide.everything import ColorAll as Color
>>> xyz = np.array(Color('red').convert('xyz-d65').coords()) * 100
>>> colour.XYZ_to_CAM16(xyz, **colour.appearance.CAM_KWARGS_CIECAM02_sRGB)
CAM_Specification_CAM16(J=46.025701408152251, C=112.3966873794304, h=27.39325656758691, s=98.395239471843041, Q=83.926266178253229, M=81.254248158497788, H=9.2041940637581128, HC=None)
>>> Color('red').convert('cam16-jmh').coords()
[46.025701408152244, 81.25424815849782, 27.3932565675869]

Color.js

> const { default: Color } = require("colorjs.io");
undefined
> new Color('red').to('cam16-jmh').coords
[ 46.025701408152244, 81.25424815849786, 27.39325656758689 ]

@svgeesus
Copy link
Member

I have read the Nico Schlömer paper before, and it troubles me that the ISO document did not even consider things like division by zero when producing their specification. Effectively, this paper is the real specification.

@svgeesus
Copy link
Member

The full text of the Li et al paper originally defining CAM16 is available on ResearchGate, avoiding the Wiley paywall

@facelessuser
Copy link
Collaborator Author

The full text of the Li et al paper originally defining CAM16 is available on ResearchGate, avoiding the Wiley paywall

Oh, nice. I searched around for a non-paywall version for quite a bit. I can update the reference link.

Copy link
Member

@svgeesus svgeesus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall this looks great, thanks for doing it.

const adaptedCoefInv = 1 / adaptedCoef;
const tau = 2 * Math.PI

const cat16 = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A shame that Li et al only give M16 to 7 figures, but there we are

[ -0.002079, 0.048952, 0.953127 ]
];

const cat16Inv = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree that using a full-precision inverse is better than the rounded, 9 figure inverse published in Appendix A

@svgeesus
Copy link
Member

There should probably be some dim and dark tests, but that should not hold up merging this.

src/spaces/cam16.js Outdated Show resolved Hide resolved
@svgeesus svgeesus merged commit 1036174 into color-js:main Dec 18, 2023
4 checks passed
@svgeesus
Copy link
Member

2. A way to resolve undefined values for achromatics when zero is not sufficient. The hue matters very much in the conversion from CAM16 JMh back to XYZ. How should color.js convert an undefined CAM16 JMh hue?

I do not think these questions need to be resolved now to implement the color space, but it should be noted that when undefined values in CAM16 are used, and in the future HCT, that it may not yield the achromatic values that are desired.

Perhaps we can just say that hue is never powerless in CAM16-JMh and treat none as an error if it occurs?

@facelessuser
Copy link
Collaborator Author

Perhaps we can just say that hue is never powerless in CAM16-JMh and treat none as an error if it occurs?

Certainly a possibility. The two easiest approaches are:

  1. Do as you say, and treat none as an error.
  2. Treat none as zero and the user gets what they get.

I went for a more complicated approach in ColorAide. I provide some hook to allow undefined channels to resolve differently than zero and provide a resolution there.

I'm not suggesting that Color.js does this, but I calculated a "good enough" achromatic response and use a spline to tell me for a given lightness, what the ideal chroma and hue would be for that lightness. I kept it simple in that lightness was the decider (though a negative chroma will cause the ideal hue to be rotated). It does well enough, but obviously, this approximation for undefined hues and chroma comes at the cost of some accuracy to provide convenience. You also have to either generate the data on the fly at some point in memory or increase the size of your library to ship the pre-calculated data.

But it does provide a sane response. Obviously, a more invasive change with costs, so you can see why I did not suggest it by default. I imagine Color.js will likely want to go with one of the easier options. It was probably more work than I was ready for here as well 🙃.

Screenshot 2023-12-18 at 11 11 36 AM

@LeaVerou
Copy link
Member

Feel free to ignore this is not relevant, just a drive-by comment. I know that CAM16 involves a bunch of variables about the viewing conditions, and presumably we had to make some assumptions here. Would it make sense to add a factory method that produces ColorSpace objects for different viewing conditions?

@facelessuser
Copy link
Collaborator Author

Feel free to ignore this is not relevant, just a drive-by comment. I know that CAM16 involves a bunch of variables about the viewing conditions, and presumably we had to make some assumptions here. Would it make sense to add a factory method that produces ColorSpace objects for different viewing conditions?

These kind of questions I'm not sure I can answer as it really depends on the direction that Color.js wishes to go in. From CAM16, we could create CAM16-UCS, CAM16-SCD, CAM16-LCD, create their distancing algorithms. We could create a dynamic way to create special instances of all these CAM16 related spaces with different viewing conditions.

My main goal is to get HCT up and going, and that required getting CAM16 functional. JMh is provided as an easy way to make sure its all working. HCT will be built upon CAM16, but it has a very specific, defined set of viewing conditions. That will likely be my next PR. It will contain the HCT color space and a distancing algorithm.

My last PR will provide an HCT gamut mapping algorithm which is the last piece in the puzzle to create the tonal palettes that that are comparable with Google's. It'll need to be tuned to give similar values. I imagine that will require me to leverage the existing algorithm.

jgerigmeyer added a commit to oddbird/color.js that referenced this pull request Dec 19, 2023
* main:
  Add CAM16 (JMh) (color-js#379)
  [spaces/prophoto] Use 64 bit-accurate conversion matrices
  formatting
  [spaces/hsl] Better handling of negative saturation on very oog colors
  Point to the actual, working tests not the old ones
  CSS4 toGamut fixes (color-js#352)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants