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 Pointer's Gamut Check #317

Open
AntonPalmqvist opened this issue Jun 16, 2023 · 17 comments
Open

Add Pointer's Gamut Check #317

AntonPalmqvist opened this issue Jun 16, 2023 · 17 comments
Labels
API change For PRs that require API design review enhancement New feature or request

Comments

@AntonPalmqvist
Copy link

I know this is a bit of a niche use case, as it would be mainly used by CG artists.

Pointer's Gamut is based on a large dataset of colors found in the real world. By checking if a color is inside this gamut, you know as a CG artist that you are staying inside physically correct color values.

Here's a Python library that does this: https://github.com/colour-science/colour/blob/68342cb857c587eb5d5eb1df41e6d2e2666617f8/colour/volume/pointer_gamut.py#L32

Thanks,
Anton

@LeaVerou
Copy link
Member

Interesting. Right now the API is structured around the idea that there are color spaces, some of which have a restricted gamut. So, to check if a color is in gamut of a given space, you convert to that space and then use inGamut().
If I’m reading this right, it appears there is no color space here, and this is basically a function that operates on XYZ values? (not sure which XYZ, but I haven't looked too carefully).

@AntonPalmqvist
Copy link
Author

That's correct!

@LeaVerou LeaVerou added enhancement New feature or request API change For PRs that require API design review labels Jun 16, 2023
@facelessuser
Copy link
Collaborator

The Pointer’s gamut is (an approximation of) the gamut of real surface colors as can be seen by the human eye, based on the research by Michael R. Pointer (1980). The linked library simply generates a mesh using hard coded points and Delauney's method and checks if a given point is within the mesh (using some threshold). The points come from documented tables, though I'd have to hunt around for the exact source. I don't think it is necessarily trivial to add though unless you depend on an external library to generate the mesh...I guess you could pre-generate mesh, then you just need an algorithm to resolve whether a point is within the mesh. You'd probably need a separate API method inPointersGamut() I imagine.

@LeaVerou
Copy link
Member

That would be less trivial than it seems to implement then, as it would require a restructing of the Color.js API itself to support this sort of thing.
Unless it's just implemented as an ad-hoc method, and doesn't hook into isGamut(). That would be pretty unfortunate though.

@facelessuser
Copy link
Collaborator

I'm sure you could special case isGamut('pointers') so that is probably fine, if you plan to implement this.

@Myndex
Copy link
Contributor

Myndex commented Jun 16, 2023

that you are staying inside physically correct color values

Staying within non-luminous, non-florescent surface colors, but it doesn't guarantee that you'll be within a given display space.

If the working space is a display space such as sRGB or P3, by definition you'll always have "physically correct" color values that are physically realizable by the display. The Pointer’s gamut mostly exceeds the physically realizable colors of those display spaces.

If the working space is a space built around imaginary primaries is like ACES or ProPhoto, that's a different story—but arguably, if you're going to clamp to a gamut I'd suggest it's usually best to clamp to your largest realizable destination space.

For reference, the triangle is sRGB/rec709, squiggly line defines Pointer’s gamut.
Pointers gamut compared to sRGB

@LeaVerou
Copy link
Member

I'm sure you could special case isGamut('pointers') so that is probably fine, if you plan to implement this.

I could, but it would be a code smell. Architecturally, inGamut() should be color space and gamut agnostic, and the logic specifics should live elsewhere. In terms of modularity, we don't want everybody using inGamut() to have to also pull in code for Pointer's Gamut (or any other similar gamut that may arise in the future). Something like Pointer's Gamut would need to be able to live in a separate module, and "hook" into inGamut() through an extension point.

For reference, this is the implementation of inGamut().

One solution would be to convert the space parameter to spaceOrGamut, i.e. it could be a color space, or the name of a
custom gamut ("pointers"). Then, have a data structure as a named export:

// inGamut.js
const gamuts = {};
export { gamuts };
// in inGamut(): 
if ( spaceOrGamut in gamuts) {
	return gamuts[spaceOrGamut](color, {epsilon});
}

that other modules could import and add to:

// pointers.js
import { gamuts } from "/path/to/src/space.js";
export default const pointers = gamuts.pointers = function(color, {epsilon} = {}) {
	/* logic here */
}

The downside of this is that a string argument could be either a color space id or a named gamut and you can't tell which one until runtime. Also, in theory they could conflict.
Alternatively, these could be separate options (i.e. have a gamut option), but then either you have an optional argument that is not at the end (antipattern), or you have a pointless empty space argument.
Or we make a backwards incompatible API change and move the space argument into the options dictionary, since it’s optional so it should have been there in the first place.
But if they’re separate options, how do you deal with both being specified? Error, conjunction, or precedence?

As you can see, this is not simple at all! 😃

@AntonPalmqvist
Copy link
Author

Thank you all for your input, it's much appreciated!

If the working space is a display space such as sRGB or P3, by definition you'll always have "physically correct" color values that are physically realizable by the display. The Pointer’s gamut mostly exceeds the physically realizable colors of those display spaces.

This illustration (from https://twitter.com/bjornornorn/status/1347633870343200770 ) better shows that even within sRGB there are colors that would be physically implausible.
Also, as you mentioned, working with larger gamuts such as ACEScg (which is common in visual effects), being able to check against Pointer's Gamut would be even more relevant.
Some more reading about this here: https://chrisbrejon.com/articles/albedo-and-pointers-gamut/

image

@facelessuser
Copy link
Collaborator

Pointer gamut data: https://www.rit-mcsl.org/UsefulData/PointerData.xls

@facelessuser
Copy link
Collaborator

I imagine it is possible to use the Pointer Data to calculate an appropriate max chroma for a given lightness and hue in LCh (using the specified SC illuminant) for a given color and then compare it against that max chroma. Then you wouldn't need to build a pointer surface with a mesh.

@facelessuser
Copy link
Collaborator

facelessuser commented Jun 17, 2023

Yeah, it seems the easiest approach is to just compare the data within the data's provided model (LCh with the SC illuminant). Though, you could certainly transform the colors into an XYZ surface mesh and compare the points against that.

The data represents the gamut at discrete lightness levels, so you can bisect to find the two closest lightness values and hue values and then interpolate the maximum chroma in that LCh (SC) space for the given color. Obviously, you need to also handle lightness that falls outside the gamut's boundary. No complicated mesh is needed. Resolving what the API looks like is probably the most complicated part.

Red dots fall outside the gamut.

red

orange

Randomly generated points at the same lightness.

random

EDIT: Fix bad lightness in title of images

@facelessuser
Copy link
Collaborator

facelessuser commented Jun 17, 2023

If you wanted to fit a color to the Pointer gamut, you could probably just clamp the lightness and chroma within the LCh (SC) space. This assumes you've already gamut-mapped the color with the current working color space if desired.

fit

EDIT: Corrected title showing wrong lightness

@svgeesus
Copy link
Member

svgeesus commented Jul 6, 2023

Pointer's gamut is an under-estimation of the gamut of physically realizable, non-fluorescent, non-emissive matt surface colors. Oh and for objects where the bidirectional reflectance distribution function (BRDF) is also flat (no dependence on angle of illumination)

I forget the name now but there is another one which is an over-estimate, because it constrains all reflectance spectra to a step function with one transition (so full reflectance for part of the wavelength range and zero reflectance for the rest). That gives a theoretical limit.

So an interesting functionality to have, but unrelated to the color.js gamut checks which are colorspace specific.

@facelessuser
Copy link
Collaborator

Yeah, it is definitely not a color-specific gamut, and I came to the same conclusion that it should probably be something separate from the "gamut" check that exists in color.js currently.

Anyways, it is pretty easy to implement. I have the code I used to implement it here (for anyone anxious to port it here): https://github.com/facelessuser/coloraide/blob/main/coloraide/gamut/pointer.py

pointer

@eeeps
Copy link

eeeps commented Jan 2, 2024

Possibly I should file a separate issue, but this is just to say: it would also be nice to be able to check if a color is in the visible gamut.

@facelessuser from facelessuser/coloraide#333 it sounds like this is doable with a lookup table, as you've done with the Pointer's Gamut, although an algorithmic solution would be more precise?

@LeaVerou
Copy link
Member

LeaVerou commented Jan 3, 2024

Possibly I should file a separate issue, but this is just to say: it would also be nice to be able to check if a color is in the visible gamut.

Yes, this would need a separate issue, but I don't think we have sufficiently granular info about the display to be able to tell…

@eeeps
Copy link

eeeps commented Jan 3, 2024

@LeaVerou filed #382

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API change For PRs that require API design review enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

6 participants