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

Change remaining color spaces to use faster matrix multiplication #588

Merged
merged 1 commit into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 18 additions & 13 deletions src/spaces/cam16.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,34 @@
import ColorSpace from "../ColorSpace.js";
import {multiplyMatrices, interpolate, copySign, spow, zdiv, bisectLeft} from "../util.js";
import {multiply_v3_m3x3, interpolate, copySign, spow, zdiv, bisectLeft} from "../util.js";
import {constrain} from "../angles.js";
import xyz_d65 from "./xyz-d65.js";
import {WHITES} from "../adapt.js";

// Type "imports"
/** @typedef {import("../types.js").Coords} Coords */
/** @typedef {import("../types.js").Matrix3x3} Matrix3x3 */
/** @typedef {import("../types.js").Vector3} Vector3 */

const white = WHITES.D65;
const adaptedCoef = 0.42;
const adaptedCoefInv = 1 / adaptedCoef;
const tau = 2 * Math.PI;

/** @type {Matrix3x3} */
const cat16 = [
[ 0.401288, 0.650173, -0.051461 ],
[ -0.250268, 1.204414, 0.045854 ],
[ -0.002079, 0.048952, 0.953127 ],
];

/** @type {Matrix3x3} */
const cat16Inv = [
[1.8620678550872327, -1.0112546305316843, 0.14918677544445175],
[0.38752654323613717, 0.6214474419314753, -0.008973985167612518],
[-0.015841498849333856, -0.03412293802851557, 1.0499644368778496],
];

/** @type {Matrix3x3} */
const m1 = [
[460.0, 451.0, 288.0],
[460.0, -891.0, -261.0],
Expand Down Expand Up @@ -126,9 +131,9 @@ export function environment (
env.discounting = discounting;
env.refWhite = refWhite;
env.surround = surround;
const xyzW = refWhite.map(c => {
const xyzW = /** @type {Vector3} */ (refWhite.map(c => {
return c * 100;
});
}));

// The average luminance of the environment in `cd/m^2cd/m` (a.k.a. nits)
env.la = adaptingLuminance;
Expand All @@ -138,7 +143,7 @@ export function environment (
const yw = xyzW[1];

// Cone response for reference white
const rgbW = multiplyMatrices(cat16, xyzW);
const rgbW = multiply_v3_m3x3(xyzW, cat16);

// Surround: dark, dim, and average
// @ts-expect-error surround is never used again
Expand Down Expand Up @@ -279,17 +284,17 @@ export function fromCam16 (cam16, env) {

// Calculate back from cone response to XYZ
const rgb_c = unadapt(
/** @type {[number, number, number]} */
(multiplyMatrices(m1, [p2, a, b]).map(c => {
/** @type {Vector3} */
(multiply_v3_m3x3([p2, a, b], m1).map(c => {
return c * 1 / 1403;
})),
env.fl,
);
return /** @type {[number, number, number]} */ (multiplyMatrices(
cat16Inv,
rgb_c.map((c, i) => {
return /** @type {Vector3} */ (multiply_v3_m3x3(
/** @type {Vector3} */(rgb_c.map((c, i) => {
return c * env.dRgbInv[i];
}),
})),
cat16Inv,
).map(c => {
return c / 100;
}));
Expand All @@ -303,12 +308,12 @@ export function fromCam16 (cam16, env) {
*/
export function toCam16 (xyzd65, env) {
// Cone response
const xyz100 = xyzd65.map(c => {
const xyz100 = /** @type {Vector3} */ (xyzd65.map(c => {
return c * 100;
});
}));
const rgbA = adapt(
/** @type {[number, number, number]} */
(multiplyMatrices(cat16, xyz100).map((c, i) => {
(multiply_v3_m3x3(xyz100, cat16).map((c, i) => {
return c * env.dRgb[i];
})),
env.fl,
Expand Down
36 changes: 27 additions & 9 deletions src/spaces/ictcp.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import ColorSpace from "../ColorSpace.js";
import {multiplyMatrices} from "../util.js";
import {multiply_v3_m3x3} from "../util.js";
import XYZ_Abs_D65 from "./xyz-abs-d65.js";

// Type "imports"
/** @typedef {import("../types.js").Matrix3x3} Matrix3x3 */
/** @typedef {import("../types.js").Vector3} Vector3 */

const c1 = 3424 / 4096;
const c2 = 2413 / 128;
const c3 = 2392 / 128;
Expand All @@ -12,6 +16,7 @@ const im2 = 32 / 2523;

// The matrix below includes the 4% crosstalk components
// and is from the Dolby "What is ICtCp" paper"
/** @type {Matrix3x3} */
const XYZtoLMS_M = [
[ 0.3592832590121217, 0.6976051147779502, -0.0358915932320290 ],
[ -0.1920808463704993, 1.1004767970374321, 0.0753748658519118 ],
Expand All @@ -32,13 +37,15 @@ const Rec2020toLMS_M = [
// the rotation, and the scaling to [-0.5,0.5] range
// rational terms from Fröhlich p.97
// and ITU-R BT.2124-0 pp.2-3
/** @type {Matrix3x3} */
const LMStoIPT_M = [
[ 2048 / 4096, 2048 / 4096, 0 ],
[ 6610 / 4096, -13613 / 4096, 7003 / 4096 ],
[ 17933 / 4096, -17390 / 4096, -543 / 4096 ],
];

// inverted matrices, calculated from the above
/** @type {Matrix3x3} */
const IPTtoLMS_M = [
[ 0.9999999999999998, 0.0086090370379328, 0.1110296250030260 ],
[ 0.9999999999999998, -0.0086090370379328, -0.1110296250030259 ],
Expand All @@ -51,6 +58,7 @@ const LMStoRec2020_M = [
[-0.025646662911506476363, -0.099240248643945566751, 1.1248869115554520431 ]
];
*/
/** @type {Matrix3x3} */
const LMStoXYZ_M = [
[ 2.0701522183894223, -1.3263473389671563, 0.2066510476294053 ],
[ 0.3647385209748072, 0.6805660249472273, -0.0453045459220347 ],
Expand Down Expand Up @@ -94,40 +102,50 @@ export default new ColorSpace({
base: XYZ_Abs_D65,
fromBase (XYZ) {
// move to LMS cone domain
let LMS = multiplyMatrices(XYZtoLMS_M, XYZ);
let LMS = multiply_v3_m3x3(XYZ, XYZtoLMS_M);

return LMStoICtCp(LMS);
},
toBase (ICtCp) {
let LMS = ICtCptoLMS(ICtCp);

return multiplyMatrices(LMStoXYZ_M, LMS);
return multiply_v3_m3x3(LMS, LMStoXYZ_M);
},
});

/**
*
* @param {Vector3} LMS
* @returns {Vector3}
*/
function LMStoICtCp (LMS) {
// apply the PQ EOTF
// we can't ever be dividing by zero because of the "1 +" in the denominator
let PQLMS = LMS.map (function (val) {
let PQLMS = /** @type {Vector3} */ (LMS.map (function (val) {
let num = c1 + (c2 * ((val / 10000) ** m1));
let denom = 1 + (c3 * ((val / 10000) ** m1));

return (num / denom) ** m2;
});
}));

// LMS to IPT, with rotation for Y'C'bC'r compatibility
return multiplyMatrices(LMStoIPT_M, PQLMS);
return multiply_v3_m3x3(PQLMS, LMStoIPT_M);
}

/**
*
* @param {Vector3} ICtCp
* @returns {Vector3}
*/
function ICtCptoLMS (ICtCp) {
let PQLMS = multiplyMatrices(IPTtoLMS_M, ICtCp);
let PQLMS = multiply_v3_m3x3(ICtCp, IPTtoLMS_M);

// From BT.2124-0 Annex 2 Conversion 3
let LMS = PQLMS.map (function (val) {
let LMS = /** @type {Vector3} */ (PQLMS.map (function (val) {
let num = Math.max((val ** im2) - c1, 0);
let denom = (c2 - (c3 * (val ** im2)));
return 10000 * ((num / denom) ** im1);
});
}));

return LMS;
}
27 changes: 18 additions & 9 deletions src/spaces/jzazbz.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import ColorSpace from "../ColorSpace.js";
import {multiplyMatrices} from "../util.js";
import {multiply_v3_m3x3} from "../util.js";
import XYZ_Abs_D65 from "./xyz-abs-d65.js";

// Type "imports"
/** @typedef {import("../types.js").Matrix3x3} Matrix3x3 */
/** @typedef {import("../types.js").Vector3} Vector3 */


const b = 1.15;
const g = 0.66;
const n = 2610 / (2 ** 14);
Expand All @@ -14,23 +19,27 @@ const pinv = (2 ** 5) / (1.7 * 2523);
const d = -0.56;
const d0 = 1.6295499532821566E-11;

/** @type {Matrix3x3} */
const XYZtoCone_M = [
[ 0.41478972, 0.579999, 0.0146480 ],
[ -0.2015100, 1.120649, 0.0531008 ],
[ -0.0166008, 0.264800, 0.6684799 ],
];
// XYZtoCone_M inverted
/** @type {Matrix3x3} */
const ConetoXYZ_M = [
[ 1.9242264357876067, -1.0047923125953657, 0.037651404030618 ],
[ 0.35031676209499907, 0.7264811939316552, -0.06538442294808501 ],
[ -0.09098281098284752, -0.3127282905230739, 1.5227665613052603 ],
];
/** @type {Matrix3x3} */
const ConetoIab_M = [
[ 0.5, 0.5, 0 ],
[ 3.524000, -4.066708, 0.542708 ],
[ 0.199076, 1.096799, -1.295875 ],
];
// ConetoIab_M inverted
/** @type {Matrix3x3} */
const IabtoCone_M = [
[ 1, 0.1386050432715393, 0.05804731615611886 ],
[ 0.9999999999999999, -0.1386050432715393, -0.05804731615611886 ],
Expand Down Expand Up @@ -67,18 +76,18 @@ export default new ColorSpace({
let Ym = (g * Ya) - ((g - 1) * Xa);

// move to LMS cone domain
let LMS = multiplyMatrices(XYZtoCone_M, [ Xm, Ym, Za ]);
let LMS = multiply_v3_m3x3([ Xm, Ym, Za ], XYZtoCone_M);

// PQ-encode LMS
let PQLMS = LMS.map (function (val) {
let PQLMS = /** @type {Vector3} } */ (LMS.map (function (val) {
let num = c1 + (c2 * ((val / 10000) ** n));
let denom = 1 + (c3 * ((val / 10000) ** n));

return (num / denom) ** p;
});
}));

// almost there, calculate Iz az bz
let [ Iz, az, bz] = multiplyMatrices(ConetoIab_M, PQLMS);
let [ Iz, az, bz] = multiply_v3_m3x3(PQLMS, ConetoIab_M);
// console.log({Iz, az, bz});

let Jz = ((1 + d) * Iz) / (1 + (d * Iz)) - d0;
Expand All @@ -89,19 +98,19 @@ export default new ColorSpace({
let Iz = (Jz + d0) / (1 + d - d * (Jz + d0));

// bring into LMS cone domain
let PQLMS = multiplyMatrices(IabtoCone_M, [ Iz, az, bz ]);
let PQLMS = multiply_v3_m3x3([ Iz, az, bz ], IabtoCone_M);

// convert from PQ-coded to linear-light
let LMS = PQLMS.map(function (val) {
let LMS = /** @type {Vector3} } */ (PQLMS.map(function (val) {
let num = (c1 - (val ** pinv));
let denom = (c3 * (val ** pinv)) - c2;
let x = 10000 * ((num / denom) ** ninv);

return (x); // luminance relative to diffuse white, [0, 70 or so].
});
}));

// modified abs XYZ
let [ Xm, Ym, Za ] = multiplyMatrices(ConetoXYZ_M, LMS);
let [ Xm, Ym, Za ] = multiply_v3_m3x3(LMS, ConetoXYZ_M);

// restore standard D50 relative XYZ, relative to media white
let Xa = (Xm + ((b - 1) * Za)) / b;
Expand Down
37 changes: 22 additions & 15 deletions src/spaces/okhsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,31 @@
import ColorSpace from "../ColorSpace.js";
import Oklab from "./oklab.js";
import {LabtoLMS_M} from "./oklab.js";
import {fromXYZ_M} from "./srgb-linear.js";
import {skipNone, spow} from "../util.js";
import {spow, multiply_v3_m3x3} from "../util.js";
import {constrain} from "../angles.js";
import multiplyMatrices from "../multiply-matrices.js";

// Type "imports"
/** @typedef {import("../types.js").Matrix3x3} Matrix3x3 */
/** @typedef {import("../types.js").Vector3} Vector3 */
/** @typedef {import("../types.js").OKCoeff} OKCoeff */

export const tau = 2 * Math.PI;

/** @type {Matrix3x3} */
export const toLMS = [
[0.4122214694707629, 0.5363325372617349, 0.0514459932675022],
[0.2119034958178251, 0.6806995506452344, 0.1073969535369405],
[0.0883024591900564, 0.2817188391361215, 0.6299787016738222],
];

/** @type {Matrix3x3} */
export const toSRGBLinear = [
[ 4.0767416360759583, -3.3077115392580629, 0.2309699031821043],
[-1.2684379732850315, 2.6097573492876882, -0.3413193760026570],
[-0.0041960761386756, -0.7034186179359362, 1.7076146940746117],
];

/** @type {OKCoeff} */
export const RGBCoeff = [
// Red
[
Expand Down Expand Up @@ -157,28 +163,29 @@ function getStMid (a, b) {
}

/**
* @param {number[]} lab
* @param {number[][]} lmsToRgb
* @param {Vector3} lab
* @param {Matrix3x3} lmsToRgb
*/
export function oklabToLinearRGB (lab, lmsToRgb) {
// Convert from Oklab to linear RGB.
//
// Can be any gamut as long as `lmsToRgb` is a matrix
// that transform the LMS values to the linear RGB space.

return multiplyMatrices(
lmsToRgb,
multiplyMatrices(LabtoLMS_M, lab).map(c => {
return c ** 3;
}),
);
let lms = multiply_v3_m3x3(lab, LabtoLMS_M);

lms[0] = lms[0] ** 3;
lms[1] = lms[1] ** 3;
lms[2] = lms[2] ** 3;

return multiply_v3_m3x3(lms, lmsToRgb, lms);
}

/**
* @param {number} a
* @param {number} b
* @param {number[][]} lmsToRgb
* @param {number[][]} okCoeff
* @param {Matrix3x3} lmsToRgb
* @param {OKCoeff} okCoeff
* @returns {[number, number]}
* @todo Could probably make these types more specific/better-documented if desired
*/
Expand All @@ -205,8 +212,8 @@ export function findCusp (a, b, lmsToRgb, okCoeff) {
* @param {number} l1
* @param {number} c1
* @param {number} l0
* @param {number[][]} lmsToRgb
* @param {number[][]} okCoeff
* @param {Matrix3x3} lmsToRgb
* @param {OKCoeff} okCoeff
* @param {[number, number]} cusp
* @returns {Number}
* @todo Could probably make these types more specific/better-documented if desired
Expand Down
Loading
Loading