Skip to content

Commit fa52f06

Browse files
matmenclaude
andcommitted
JPEG XL: faster weighted predictor, sRGB encoding and Gaborish
The weighted predictor keeps its four subpredictors in fields instead of allocating arrays per pixel (lossless -e 7: 24% faster, identical output). Linear light becomes sRGB like libjxl's TF_SRGB, a rational polynomial of the square root within 5e-7 instead of a power (the conformance results are unchanged). Gaborish mirrors only at the edges (identical output). Lossy 2048x1536: about 1.2 s -> 0.95 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
1 parent 3bacbcd commit fa52f06

3 files changed

Lines changed: 72 additions & 36 deletions

File tree

‎src/codecs/jxl/color.ts‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,15 @@ import { ImageHeader, TF_PQ, TF_HLG, PRI_BT2100, PRI_P3, EXTRA_ALPHA, EXTRA_SPOT
77
import { toSRGB, hdrToLinear, ToneMapping, REFERENCE_WHITE } from '../hdr.ts';
88
import type { Planes } from './frame.ts';
99

10-
const srgbEncode = (v: number): number => v <= 0.0031308 ? 12.92 * v : 1.055 * v ** (1 / 2.4) - 0.055;
10+
// linear light to sRGB like libjxl (TF_SRGB::EncodedFromDisplay, lib/jxl/cms/transfer_functions-inl.h): a rational
11+
// polynomial of the square root instead of the power, within 5e-7
12+
const [P0, P1, P2, P3, P4] = [-5.135152395e-04, 5.287254571e-03, 3.903842876e-01, 1.474205315e+00, 7.352629620e-01].map(Math.fround);
13+
const [Q0, Q1, Q2, Q3, Q4] = [1.004519624e-02, 3.036675394e-01, 1.340816930e+00, 9.258482155e-01, 2.424867759e-02].map(Math.fround);
14+
const srgbEncode = (v: number): number => {
15+
if (v <= 0.0031308) return 12.92 * v;
16+
const x = Math.sqrt(v);
17+
return ((((P4 * x + P3) * x + P2) * x + P1) * x + P0) / ((((Q4 * x + Q3) * x + Q2) * x + Q1) * x + Q0);
18+
};
1119

1220
/**
1321
* How frames become output colors: XYB to sRGB (or to linear light, 1 being SDR white, for HDR images), other images

‎src/codecs/jxl/filters.ts‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ export function gaborish(channels: Float32Array[], w: number, h: number, weights
1313
const out = new Float32Array(w * h);
1414
for (let y = 0; y < h; y++) {
1515
const t = mirror(y - 1, h) * w, m = y * w, b = mirror(y + 1, h) * w;
16-
for (let x = 0; x < w; x++) {
17-
const l = mirror(x - 1, w), r = mirror(x + 1, w);
18-
out[m + x] = (src[m + x] + (src[t + x] + src[b + x] + src[m + l] + src[m + r]) * w0 + (src[t + l] + src[t + r] + src[b + l] + src[b + r]) * w1) * norm;
16+
const at = (x: number, l: number, r: number): number =>
17+
(src[m + x] + (src[t + x] + src[b + x] + src[m + l] + src[m + r]) * w0 + (src[t + l] + src[t + r] + src[b + l] + src[b + r]) * w1) * norm;
18+
out[m] = at(0, mirror(-1, w), mirror(1, w));
19+
// (the columns between the edges: no mirroring)
20+
for (let x = 1; x < w - 1; x++) {
21+
out[m + x] = (src[m + x] + (src[t + x] + src[b + x] + src[m + x - 1] + src[m + x + 1]) * w0 +
22+
(src[t + x - 1] + src[t + x + 1] + src[b + x - 1] + src[b + x + 1]) * w1) * norm;
1923
}
24+
if (w > 1) out[m + w - 1] = at(w - 1, w - 2, mirror(w, w));
2025
}
2126
return out;
2227
});

‎src/codecs/jxl/modular.ts‎

Lines changed: 55 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -44,33 +44,47 @@ function mulShift24(a: number, b: number): number {
4444
return Number((BigInt(a) * BigInt(b)) >> 24n);
4545
}
4646

47+
// a weight of the subpredictors from their errors around (x, y): the errors' sum (wrapping, as unsigned 32 bits)
48+
function weight(w: number, e: Uint32Array, posN: number, posNE: number, posNW: number): number {
49+
const sum = (e[posN] + e[posNE] + e[posNW]) >>> 0;
50+
const shift = sum === 0xffffffff ? 27 : Math.max(floorLog2(sum + 1) - 5, 0);
51+
return 4 + ((w * DIV_LOOKUP[sum >>> shift]) >>> shift);
52+
}
53+
4754
/** The self-correcting (weighted) predictor's state over a channel */
4855
class WeightedPredictor {
49-
private readonly predErrors: Uint32Array[];
56+
// (the subpredictors' errors and the error of the prediction, of two rows)
57+
private readonly e0: Uint32Array;
58+
private readonly e1: Uint32Array;
59+
private readonly e2: Uint32Array;
60+
private readonly e3: Uint32Array;
5061
private readonly error: Int32Array;
51-
private readonly prediction = new Float64Array(4);
62+
private readonly w0: number;
63+
private readonly w1: number;
64+
private readonly w2: number;
65+
private readonly w3: number;
66+
private p0 = 0;
67+
private p1 = 0;
68+
private p2 = 0;
69+
private p3 = 0;
5270
private pred = 0;
5371
/** the property (the largest error of the neighbors) of the last prediction */
5472
maxError = 0;
5573

5674
constructor(private readonly header: WPHeader, private readonly xsize: number) {
57-
this.predErrors = [0, 1, 2, 3].map(() => new Uint32Array((xsize + 2) * 2));
58-
this.error = new Int32Array((xsize + 2) * 2);
75+
const n = (xsize + 2) * 2;
76+
[this.e0, this.e1, this.e2, this.e3] = [0, 1, 2, 3].map(() => new Uint32Array(n));
77+
this.error = new Int32Array(n);
78+
[this.w0, this.w1, this.w2, this.w3] = header.w;
5979
}
6080

6181
/** the prediction (rounded) of (x, y) from its neighbors */
6282
predict(x: number, y: number, N: number, W: number, NE: number, NW: number, NN: number): number {
6383
const h = this.header, xsize = this.xsize;
6484
const cur = y & 1 ? 0 : xsize + 2, prev = y & 1 ? xsize + 2 : 0;
6585
const posN = prev + x, posNE = x < xsize - 1 ? posN + 1 : posN, posNW = x > 0 ? posN - 1 : posN;
66-
const weights = [0, 0, 0, 0];
67-
for (let i = 0; i < 4; i++) {
68-
const e = this.predErrors[i];
69-
// (wrapping, as unsigned 32 bits)
70-
const sum = (e[posN] + e[posNE] + e[posNW]) >>> 0;
71-
const shift = Math.max((sum === 0xffffffff ? 32 : floorLog2(sum + 1)) - 5, 0);
72-
weights[i] = 4 + ((h.w[i] * DIV_LOOKUP[Math.floor(sum / 2 ** shift)]) >>> shift);
73-
}
86+
let w0 = weight(this.w0, this.e0, posN, posNE, posNW), w1 = weight(this.w1, this.e1, posN, posNE, posNW);
87+
let w2 = weight(this.w2, this.e2, posN, posNE, posNW), w3 = weight(this.w3, this.e3, posN, posNE, posNW);
7488
N *= 8;
7589
W *= 8;
7690
NE *= 8;
@@ -83,35 +97,44 @@ class WeightedPredictor {
8397
if (Math.abs(teNW) > Math.abs(p)) p = teNW;
8498
if (Math.abs(teNE) > Math.abs(p)) p = teNE;
8599
this.maxError = p;
86-
const sumWN = teN + teW, pr = this.prediction;
87-
pr[0] = W + NE - N;
88-
pr[1] = N - Math.floor((sumWN + teNE) * h.p1 / 32);
89-
pr[2] = W - Math.floor((sumWN + teNW) * h.p2 / 32);
90-
pr[3] = N - Math.floor((teNW * h.p3a + teN * h.p3b + teNE * h.p3c + (NN - N) * h.p3d + (NW - W) * h.p3e) / 32);
100+
const sumWN = teN + teW;
101+
const p0 = this.p0 = W + NE - N;
102+
const p1 = this.p1 = N - Math.floor((sumWN + teNE) * h.p1 / 32);
103+
const p2 = this.p2 = W - Math.floor((sumWN + teNW) * h.p2 / 32);
104+
const p3 = this.p3 = N - Math.floor((teNW * h.p3a + teN * h.p3b + teNE * h.p3c + (NN - N) * h.p3d + (NW - W) * h.p3e) / 32);
91105
// the weighted average
92-
let weightSum = weights[0] + weights[1] + weights[2] + weights[3];
93-
const logWeight = floorLog2(weightSum) - 4;
94-
weightSum = 0;
95-
for (let i = 0; i < 4; i++) weightSum += weights[i] >>= logWeight;
96-
let sum = (weightSum >> 1) - 1;
97-
for (let i = 0; i < 4; i++) sum += pr[i] * weights[i];
106+
const logWeight = floorLog2(w0 + w1 + w2 + w3) - 4;
107+
w0 >>= logWeight;
108+
w1 >>= logWeight;
109+
w2 >>= logWeight;
110+
w3 >>= logWeight;
111+
const weightSum = w0 + w1 + w2 + w3;
112+
const sum = (weightSum >> 1) - 1 + p0 * w0 + p1 * w1 + p2 * w2 + p3 * w3;
98113
let pred = mulShift24(sum, DIV_LOOKUP[weightSum - 1]);
99114
// (clamped unless the errors have the same sign)
100-
if (((teN ^ teW) | (teN ^ teNW)) <= 0) pred = Math.min(Math.max(pred, Math.min(W, NE, N)), Math.max(W, NE, N));
115+
if (((teN ^ teW) | (teN ^ teNW)) <= 0) {
116+
const lo = W < NE ? (W < N ? W : N) : (NE < N ? NE : N), hi = W > NE ? (W > N ? W : N) : (NE > N ? NE : N);
117+
pred = pred < lo ? lo : pred > hi ? hi : pred;
118+
}
101119
this.pred = pred;
102120
return Math.floor((pred + 3) / 8);
103121
}
104122

105123
update(value: number, x: number, y: number): void {
106-
const xsize = this.xsize, cur = y & 1 ? 0 : xsize + 2, prev = y & 1 ? xsize + 2 : 0;
124+
const xsize = this.xsize, cur = y & 1 ? 0 : xsize + 2, i = cur + x, j = (y & 1 ? xsize + 2 : 0) + x + 1;
107125
const val = value * 8;
108-
this.error[cur + x] = this.pred - val;
109-
for (let i = 0; i < 4; i++) {
110-
const e = Math.floor((Math.abs(this.prediction[i] - val) + 3) / 8) >>> 0;
111-
const errors = this.predErrors[i];
112-
errors[cur + x] = e;
113-
errors[prev + x + 1] = (errors[prev + x + 1] + e) >>> 0;
114-
}
126+
this.error[i] = this.pred - val;
127+
const e0 = Math.floor((Math.abs(this.p0 - val) + 3) / 8) >>> 0, e1 = Math.floor((Math.abs(this.p1 - val) + 3) / 8) >>> 0;
128+
const e2 = Math.floor((Math.abs(this.p2 - val) + 3) / 8) >>> 0, e3 = Math.floor((Math.abs(this.p3 - val) + 3) / 8) >>> 0;
129+
const E0 = this.e0, E1 = this.e1, E2 = this.e2, E3 = this.e3;
130+
E0[i] = e0;
131+
E0[j] = (E0[j] + e0) >>> 0;
132+
E1[i] = e1;
133+
E1[j] = (E1[j] + e1) >>> 0;
134+
E2[i] = e2;
135+
E2[j] = (E2[j] + e2) >>> 0;
136+
E3[i] = e3;
137+
E3[j] = (E3[j] + e3) >>> 0;
115138
}
116139
}
117140

0 commit comments

Comments
 (0)