Closed svgeesus closed 4 years ago
but is a warm white.
1 part in 20 million doesn’t seem like an unreasonable rounding error. All three of those numbers are going to round to 255/255 (or whatever). For some values of L* you’ll get unlucky in quantization from floats to 8-bit integers and end up with one channel rounding up and another rounding down, but occasional stray pixels where RGB channels are off by 1 shouldn’t be too noticeable in practice.
If you want to take a shortcut and avoid stray rounding errors you could explicitly implement a univariate function taking in L* and spitting out three identical values for sRGB.
Converting the D65-adapted white back to Lab:
You didn’t do the reverse chromatic adaptation from D65 back to D50 so this is expected.
Overall the topic title here seems inaccurate, unless you’re commenting on some concrete (buggy) implementation.
If you want to short circuit it and avoid stray rounding errors you could explicitly implement a univariate function taking in L* and spitting out three identical values for sRGB.
To be explicit, you can first convert L* to XYZ Y (still D50 whitepoint), then you can combine the steps of taking Y ↦ XYZ, i.e. Y ↦ [0.34567Y / 0.35850, Y, 0.29583Y / 0.35850], whatever chromatic adaptation matrix you like, and a conversion from XYZ ↦ RGB; this can be distilled down to a single scalar to multiply by the D50 Y value to get all three of the D65 RGB values. Finally you can apply your RGB gamma function.
Edit: thinking a bit about it, for purely neutral values the combination of Y ↦ XYZ and the chromatic adaptation transformation is going to be the same as just pretending your Y value was D65 to begin with, and choosing the appropriate X and Z based on that. Then since our new white point is the white point for the RGB space, converting to R, G, or B is just a multiplication by 1.
So in short: this transformation boils down to 2 steps:
We can write this in JavaScript as:
function gamma (R) {
return (
(R > 0.0031308) * (1.055 * Math.pow(R, 0.4166666666666667) - 0.055) +
(R <= 0.0031308) * 12.92 * R);
}
function Lstar_to_Y(L) {
return (
(L > 8) * 6.406576735413506e-7 * (L + 16) * (L + 16) * (L + 16) +
(L <= 8) * 1.1070564598794537e-3 * L);
}
function gray_to_sRGB (L) {
const Rg = gamma(Lstar_to_Y(L));
return [Rg, Rg, Rg];
}
Sorry to say this in a way that is going to sound rude, but: what's your point here, Chris? ^_^
As @jrus says, the deviation here from pure sRGB white is ridiculously small, far below the threshold which would produce rendering differences.
Even if it did produce something that would round to a unit of difference between the channels, is that an issue?
It might however be worth explicitly pointing out in the spec that you can simplify this down to two nonlinear functions applied to a single value and then spread across three outputs, cutting out the matrix multiplications and redundant gamma function invocations.
Sorry to say this in a way that is going to sound rude, but: what's your point here, Chris? ^_^
My points were twofold:
rgb(100% 100% 100%)
at maximumSo in short: this transformation boils down to 2 steps:
Apply the nonlinear transformation L* ↦ Y Letting linear R = G = B = Y from step 1, apply the gamma encoding to convert RGB ↦ R′G′B′.
That is a transformation in XYZ, which is a von Kries chromatic adaptation.
My points were twofold:
kk, these are reasonable then. I'm still somewhat curious about how a deviation from triple-100% by ~a millionth of a percent is something to worry about, but if it's just a technical bugaboo you want to fix, sounds fine.
Does the D65 white give exactly-100% answers?
Does the D65 white give exactly-100% answers?
Yes.
If we change gray() to use a D65 white point, are we also going to change lab() and lch() to use the same white point?
Yes, we would.
My only hesitation is that spectrophotometers give an Lab readout relative to a D50 white (as they are primarily used in the print industry, and by designers working with print, which has had reliable color management for a couple of decades).
I have asked a contact at X-Rite whether their spectros can be optionally set to give a D65 readout. I'm currently traveling and don't have access to my i1Pro 2 to try it out.
If we change gray() to use a D65 white point, are we also going to change lab() and lch() to use the same white point?
It doesn’t matter either way as far as gray()
per se is concerned. The end result of gray()
in any arbitrary whitepoint composed with a Bradford-style CAT is going to result in exactly the same output (except for trivial rounding errors on the order of whatever rounding you applied to your matrix coefficients).
That was the point of my previous comment.
I would consider adding a comment in the spec explaining that if desired implementors can implement gray()
in just one channel by doing an L*-to-Y transformation (scaled so Y ranges from 0..1 rather than 0..100) composed with the sRGB gamma transformation, and then spread that result across all three RGB channels.
The following unfancy javascript code will yield the output of gray()
relative to any white point you like:
function gamma (R) {
return (
(R > 0.0031308) * (1.055 * Math.pow(R, 0.4166666666666667) - 0.055) +
(R <= 0.0031308) * 12.92 * R);
}
function Lstar_to_Y(L) {
return (
(L > 8) * 6.406576735413506e-7 * (L + 16) * (L + 16) * (L + 16) +
(L <= 8) * 1.1070564598794537e-3 * L);
}
function gray_to_sRGB (L) {
const Rg = gamma(Lstar_to_Y(L));
return [Rg, Rg, Rg];
}
My points were twofold: its wrong, and undesirable, and doesn't give rgb(100% 100% 100%) at maximum
It is not worth worrying about rounding errors this trivial in a context of color reproduction. None of the systems involved are anywhere close to precise enough for a rounding error of 1 part in 20 million to be meaningful. Even in the vanishingly rare case that subsequent quantization makes one 8-bit channel differ by 1 from the other two channels, the difference is not going to be apparent to human viewers.
But again, if you care about this, you can easily add a recommendation like my code above to the spec.
That is a transformation in XYZ, which is a von Kries chromatic adaptation.
I do not understand what you mean with this comment. Your current CSS spec of both lab
and gray
directly recommends using the Bradford CAT, a “von Kries” transformation.
If you skip the Bradford CAT and just apply the CIELAB model to un-adapted XYZ values taken relative to multiple different white points and then try to compare them, that includes within the CIELAB model itself (in the division X/Xn, etc.) what is called a “wrong von Kries” transformation, which will have very poor results, as you discovered for yourself in your original post. The lesson is: don’t ever do that.
Inre the tangential question of whether CIELAB color modes should be done relative to D65 or D50 whitepoint, with colors taken relative to other white points being transformed to that target whitepoint via a Bradford-style CAT:
Using a D65 whitepoint for CIELAB mode is the most sensible if you only care about screens and don’t care about interoperability with other code. For the dominant use cases of screen-to-screen types of transformations this will slightly simplify your code.
If you care about interoperability with existing ICC profiles, Photoshop, print workflows, hardware spectrophotometers, etc., then you should use a D50 whitepoint.
Either choice seems defensible to me.
The CSS Working Group just discussed gray() function produces a warm, D50 white
.
(except for trivial rounding errors on the order of whatever rounding you applied to your matrix coefficients).
Yes, the rounding errors are what I was seeing.
Today, CSSWG decided to continue to use D50 Lab, primarily for interop with ICC PCS and existing spectrophotometers.
@jrus thanks for your thoughtful comments which are much appreciated. This particular issue is now closed, because the CSS WG decided today to remove the gray()
function entirely, but I encourage you to send further review comments on this specification. I would also encourage you to join the Color on the Web Community Group.
The gray() function is defined in terms of CIE Lab, with the a and b chromatic axes set to 0. In the spec currently, Lab is defined relative to a D50 whitepoint, in accordance with typical (print) industry practice and for compatibility with most spectrophotometer default settings.
In consequence,
gray(100)
will give anLab
value of 100 0 0this gives an sRGB value of
rgb(99.99999809955643% 100.0000034987241% 100.00000100781432%)
which will be clipped ong
andb
to 100% but is a warm white.Converting the D65-adapted white back to Lab:
The difference is certainly noticeable (ΔE 2000 is 13.64).