- From: Dan Burzo via GitHub <sysbot+gh@w3.org>
- Date: Sun, 26 Mar 2023 17:11:39 +0000
- To: public-css-archive@w3.org
This is my first pass at the algorithm:
<details>
<summary>Expand JavaScript code</summary>
```js
const OKLAB_TO_LMS = [
[0.99999999845051981432, 0.39633779217376785678, 0.21580375806075880339],
[1.0000000088817607767, -0.1055613423236563494, -0.063854174771705903402],
[1.0000000546724109177, -0.089484182094965759684, -1.2914855378640917399]
];
const LMS_TO_XYZ_D65 = [
[1.2268798733741557, -0.5578149965554813, 0.28139105017721583],
[-0.04057576262431372, 1.1122868293970594, -0.07171106666151701],
[-0.07637294974672142, -0.4214933239627914, 1.5869240244272418]
];
const XYZ_D65_TO_LRGB = [
[3.2409699419045226, -1.537383177570094, -0.4986107602930034],
[-0.9692436362808796, 1.8759675015077202, 0.04155505740717559],
[0.05563007969699366, -0.20397695888897652, 1.0569715142428786]
];
const LMS_TO_LRGB = multiplyMatrices(XYZ_D65_TO_LRGB, LMS_TO_XYZ_D65);
function multiplyMatrices(a, b) {
let res = [[], [], []];
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
res[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j] + a[i][2] * b[2][j];
}
}
return res;
}
function multiplyMatrixWithVector(m, v) {
return [
m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2]
];
}
function multiplyComponentWise(arr1, arr2) {
return arr1.map((it, i) => it * arr2[i]);
}
function multiplyVectors(arr1, arr2) {
return arr1.reduce((acc, curr, i) => {
return acc + curr * arr2[i];
}, 0);
}
export function GamutMapNewton(original) {
const zero_a_b = [0, original[1], original[2]];
let alpha = 1;
let res_oklab = original.slice();
let res_rgb = multiplyMatrixWithVector(
LMS_TO_LRGB,
multiplyMatrixWithVector(OKLAB_TO_LMS, res_oklab).map(it => it ** 3)
);
for (let comp = 0; comp < 3; comp++) {
if (res_rgb[comp] >= 0 && res_rgb[comp] <= 1) continue;
let target = res_rgb[comp] > 1 ? 1 : 0;
for (let iter = 0; iter < 6; iter++) {
let residual =
multiplyVectors(
LMS_TO_LRGB[comp],
multiplyMatrixWithVector(OKLAB_TO_LMS, res_oklab).map(it => it ** 3)
) - target;
if (Math.abs(residual) < 1e-15) break;
let gradient = multiplyVectors(
LMS_TO_LRGB[comp],
multiplyComponentWise(
multiplyMatrixWithVector(OKLAB_TO_LMS, res_oklab).map(it => 3 * it ** 2),
multiplyMatrixWithVector(OKLAB_TO_LMS, zero_a_b)
)
);
alpha -= residual / gradient;
res_oklab[1] = alpha * original[1];
res_oklab[2] = alpha * original[2];
}
res_rgb = multiplyMatrixWithVector(
LMS_TO_LRGB,
multiplyMatrixWithVector(OKLAB_TO_LMS, res_oklab).map(it => it ** 3)
);
}
return [res_oklab, res_rgb];
}
```
</details>
I've updated [my gamut mapping demo page](https://danburzo.ro/demos/color/gamut-mapping.html) to include this new algorithm (see the _newton (@ccameron)_ method).
There seem to be some visual glitches outside the sRGB gamut, so I can't exclude translation errors on my part either in the code above, or in the glue code for the demo page, but I hope it's a good starting point.
<img width="659" alt="comparison between chroma-reduction algorithm based on binary search and newton method shows visual glitches in the latter for hue = 104" src="https://user-images.githubusercontent.com/205375/227792268-34954e3f-3b5a-4885-81a0-25fde73f0528.png">
Assuming the algorithm is sound and the glitches can be ironed out, what I'm observing is that it's very close to (indistinguishable from) basic binary search (the `chroma-reduce` method) when rendering lch() / oklch() / etc. canvases, but seems to converge faster to the solution. (I'll update the demo page to include lab() / oklab() canvases)
--
GitHub Notification of comment by danburzo
Please view or discuss this issue at https://github.com/w3c/csswg-drafts/issues/7653#issuecomment-1484161031 using your GitHub account
--
Sent via github-notify-ml as configured in https://github.com/w3c/github-notify-ml-config
Received on Sunday, 26 March 2023 17:11:41 UTC