Skip to content

Commit

Permalink
Change remaining color spaces to use faster matrix multiplication
Browse files Browse the repository at this point in the history
  • Loading branch information
lloydk committed Aug 26, 2024
1 parent 6332f3a commit d4eba65
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 51 deletions.
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

0 comments on commit d4eba65

Please sign in to comment.