Skip to content

Commit b44a677

Browse files
committed
JPEG XL: faster lossy encoding
1 parent 4ee7d86 commit b44a677

8 files changed

Lines changed: 293 additions & 161 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## 3.13.3
4+
5+
### Performance
6+
7+
- Lossy `encodeJXL()` is faster: a 1920x1080 photo at quality 90 takes 1.6 s instead of 2.3 s at effort 1 and 2.4 s
8+
instead of 4.8 s at effort 2 on one thread; with 8 threads 0.72 s instead of 0.81 s and 0.95 s instead of 1.46 s
9+
(3840x2160: 1.6 s instead of 2.0 s and 2.5 s instead of 4.1 s). The 4x8 and AFV transforms are computed directly
10+
instead of by a 64x64 matrix, the varblock search rounds without `Math.round()` (slow in V8), patch search skips
11+
large components after a quick labeling, and the search is split into smaller pieces for the threads. The files stay
12+
the same, except for rare coefficients the new transforms round differently (2 of 108 test encodes, both the same
13+
size).
14+
315
## 3.13.2
416

517
### Fixes

‎src/codecs/jxl/acs-enc.ts‎

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
*/
77
import { runChunks, shared, sharedOf, type ChunkTask, type Thread } from '../../util/threads.ts';
88
import { forwardTransform } from './transforms-enc.ts';
9-
import { inverseTransform } from './dct.ts';
9+
import { idct8, inverseTransform } from './dct.ts';
10+
import { idct16, idct32, idct64 } from './idct.ts';
1011
import { defaultDequant, MATRIX_INDEX } from './quant.ts';
11-
import { SIZES, transposed } from './vardct.ts';
12+
import { SIZES, single, transposed } from './vardct.ts';
1213
import type { QuantField } from './aq-enc.ts';
1314

1415
const DCT8 = 0, IDENTITY = 1, DCT2X2 = 2, DCT4X4 = 3, DCT16X16 = 4, DCT32X32 = 5, DCT16X8 = 6, DCT8X16 = 7;
@@ -41,6 +42,19 @@ function inverseDequant(type: number): Float32Array[] {
4142
return inv;
4243
}
4344

45+
// the inverse DCT of a w x h block (8 to 64 points: like dct2d, rows then columns, by direct calls)
46+
function idct2(buf: Float32Array, w: number, h: number): void {
47+
for (let y = 0; y < h; y++) idct(buf, y * w, 1, w);
48+
for (let x = 0; x < w; x++) idct(buf, x, w, h);
49+
}
50+
51+
function idct(buf: Float32Array, o: number, step: number, n: number): void {
52+
if (n === 8) idct8(buf, o, step);
53+
else if (n === 16) idct16(buf, o, step);
54+
else if (n === 32) idct32(buf, o, step);
55+
else idct64(buf, o, step);
56+
}
57+
4458
// the estimated cost of a varblock of a type at pixels x, y: its bits (by the square roots of the quantized
4559
// coefficients and the non-zero counts) and the information lost (an 8-norm of the masked errors in pixels)
4660
function estimateEntropy(type: number, entropyMul: number, x: number, y: number, cfg: Config): number {
@@ -65,14 +79,17 @@ function estimateEntropy(type: number, entropyMul: number, x: number, y: number,
6579
for (let c = 0; c < 3; c++) {
6680
const src = scratch[c], ys = scratch[1], m = matrices[c], im = inverses[c], cmap = c === 2 ? 1 : 0;
6781
let sqrtSum = 0, nonZeros = 0;
82+
// (rounded by Math.floor(v + 0.5): Math.round but for values within an ulp below halves, and faster in V8)
6883
for (let i = 0; i < size; i++) {
69-
const v = (src[i] - ys[i] * cmap) * im[i] * quant, r = Math.round(v);
84+
const v = (src[i] - ys[i] * cmap) * im[i] * quant, r = Math.floor(v + 0.5);
7085
errors[i] = m[i] * (v - r);
71-
const q = Math.abs(r);
72-
sqrtSum += Math.sqrt(q);
73-
if (q) nonZeros++;
86+
if (r) {
87+
sqrtSum += Math.sqrt(Math.abs(r));
88+
nonZeros++;
89+
}
7490
}
75-
inverseTransform({ buf: errors, offset: 0, stride: w, w, h }, type);
91+
if (type === 0 || !single(type)) idct2(errors, w, h);
92+
else inverseTransform({ buf: errors, offset: 0, stride: w, w, h }, type);
7693
let lossc = 0;
7794
for (let py = 0; py < h; py++) {
7895
const mo = (y + py) * stride + x;
@@ -293,8 +310,8 @@ export async function chooseVarblocks(planes: Float32Array[], stride: number, bw
293310
planes: threads ? planes.map(sharedOf) : planes,
294311
field: threads ? { quant: shared(field.quant), mask: shared(field.mask), mask1x1: sharedOf(field.mask1x1) } : field,
295312
};
296-
// (a row of 64x64 areas each, independent of the others)
297-
await runChunks(threads, task, Math.ceil(bh / 8), varblockChunks(task));
313+
// (up to 8 64x64 areas of a row each, independent of the others: small enough to keep all threads busy)
314+
await runChunks(threads, task, Math.ceil(bh / 8) * Math.ceil(bw / 64), varblockChunks(task));
298315
return types;
299316
}
300317

@@ -310,7 +327,7 @@ export interface VarblockTask extends ChunkTask {
310327
types: Int8Array;
311328
}
312329

313-
/** The search of a row of 64x64 areas (the chunk), its types set */
330+
/** The search of up to 8 64x64 areas of a row (the chunk: of each row's segments of 8), its types set */
314331
export function varblockChunks({ planes, stride, bw, bh, field, distance, speed, types }: VarblockTask): (chunk: number) => void {
315332
const ratio = (distance + 0.13731742964354549) / (1 + 0.13731742964354549);
316333
const cfg: Config = {
@@ -320,8 +337,9 @@ export function varblockChunks({ planes, stride, bw, bh, field, distance, speed,
320337
costDelta: 10.833273317067883 * ratio ** 0.36702940662370243
321338
};
322339
const s = new Strategy(bw, bh, types);
323-
return row => {
324-
const by = row * 8;
325-
for (let bx = 0; bx < bw; bx += 8) processRect(bx, by, Math.min(8, bw - bx), Math.min(8, bh - by), speed, cfg, s);
340+
const segments = Math.ceil(bw / 64);
341+
return chunk => {
342+
const by = Math.floor(chunk / segments) * 8, x0 = (chunk % segments) * 64;
343+
for (let bx = x0; bx < Math.min(bw, x0 + 64); bx += 8) processRect(bx, by, Math.min(8, bw - bx), Math.min(8, bh - by), speed, cfg, s);
326344
};
327345
}

‎src/codecs/jxl/aq-enc.ts‎

Lines changed: 47 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -138,62 +138,67 @@ function perBlockModulations(distance: number, px: Float32Array, py: Float32Arra
138138
if (distance >= 2) dampen = Math.max(0, 1 - (distance - 2) / (14 - 2));
139139
const mul = scale * dampen, add = (1 - dampen) * baseLevel;
140140
for (let by = 0; by < bh; by++) {
141-
for (let bx = 0; bx < bw; bx++) {
142-
const o = by * 8 * w + bx * 8;
143-
let v = computeMask(aq[by * bw + bx]);
144-
// high frequencies: the (limited) differences of Y to the right and below
145-
let hf = 0;
146-
for (let dy = 0; dy < 8; dy++) {
147-
const r = o + dy * w, next = dy === 7 ? r : r + w;
148-
for (let dx = 0; dx < 8; dx++) {
149-
const p = py[r + dx];
150-
if (dx < 7) hf += Math.min(0.0206, Math.abs(p - py[r + dx + 1]));
151-
hf += Math.min(0.0206, Math.abs(p - py[next + dx]));
152-
}
153-
}
154-
v += hf * -0.38 + 0.42;
155-
// gamma
156-
let ratio = 0;
157-
for (let dy = 0; dy < 8; dy++) {
158-
for (let dx = 0; dx < 8; dx++) {
159-
const iny = py[o + dy * w + dx] + 0.16, inx = px[o + dy * w + dx];
160-
ratio += gammaRatioInverse(iny - inx) + gammaRatioInverse(iny + inx);
161-
}
162-
}
163-
v += 0.1005613337192697 * Math.log2(ratio * (0.5 / 64));
164-
// blue (but not all blue)
165-
const LIMIT = 0.027121074570634722;
166-
let blue = 0;
167-
for (let dy = 0; dy < 8; dy++) {
168-
for (let dx = 0; dx < 8; dx++) {
169-
const i = o + dy * w + dx, eff = py[i] + 0.084381641171960495 + Math.abs(px[i]);
170-
if (pb[i] > eff) blue += Math.min(pb[i] - eff, LIMIT);
171-
}
172-
}
173-
if (blue >= 32 * LIMIT) blue = 64 * LIMIT - blue;
174-
if (blue >= 15.398788439047934 * LIMIT) blue = 15.398788439047934 * LIMIT;
175-
v += blue * 0.14207000358439159;
176-
aq[by * bw + bx] = Math.exp(v) * mul + add;
141+
for (let bx = 0; bx < bw; bx++) aq[by * bw + bx] = Math.exp(blockModulation(px, py, pb, by * 8 * w + bx * 8, w, aq[by * bw + bx])) * mul + add;
142+
}
143+
}
144+
145+
// the modulation of a block (at o; e: its erosion) before the exponentiation (a function of its own: V8 kept leaving
146+
// and re-entering perBlockModulations' inner loops compiled on stack)
147+
function blockModulation(px: Float32Array, py: Float32Array, pb: Float32Array, o: number, w: number, e: number): number {
148+
let v = computeMask(e);
149+
// high frequencies: the (limited) differences of Y to the right and below
150+
let hf = 0;
151+
for (let dy = 0; dy < 8; dy++) {
152+
const r = o + dy * w, next = dy === 7 ? r : r + w;
153+
for (let dx = 0; dx < 8; dx++) {
154+
const p = py[r + dx];
155+
if (dx < 7) hf += Math.min(0.0206, Math.abs(p - py[r + dx + 1]));
156+
hf += Math.min(0.0206, Math.abs(p - py[next + dx]));
177157
}
178158
}
159+
v += hf * -0.38 + 0.42;
160+
// gamma
161+
let ratio = 0;
162+
for (let dy = 0; dy < 8; dy++) {
163+
for (let dx = 0; dx < 8; dx++) {
164+
const iny = py[o + dy * w + dx] + 0.16, inx = px[o + dy * w + dx];
165+
ratio += gammaRatioInverse(iny - inx) + gammaRatioInverse(iny + inx);
166+
}
167+
}
168+
v += 0.1005613337192697 * Math.log2(ratio * (0.5 / 64));
169+
// blue (but not all blue)
170+
const LIMIT = 0.027121074570634722;
171+
let blue = 0;
172+
for (let dy = 0; dy < 8; dy++) {
173+
for (let dx = 0; dx < 8; dx++) {
174+
const i = o + dy * w + dx, eff = py[i] + 0.084381641171960495 + Math.abs(px[i]);
175+
if (pb[i] > eff) blue += Math.min(pb[i] - eff, LIMIT);
176+
}
177+
}
178+
if (blue >= 32 * LIMIT) blue = 64 * LIMIT - blue;
179+
if (blue >= 15.398788439047934 * LIMIT) blue = 15.398788439047934 * LIMIT;
180+
return v + blue * 0.14207000358439159;
179181
}
180182

181183
// rows y0 to y1 of a symmetric 5x5 convolution with mirrored borders (libjxl's Symmetric5): the weights of the center,
182184
// the edges 1 and 2 away, the diagonals 1 and 2 away and the knight moves
183185
function symmetric5(m: Float32Array, w: number, h: number, [c, r, R, d, D, L]: number[], out: Float32Array, y0: number, y1: number): void {
184-
const weights = [D, L, R, L, D, L, d, r, d, L, R, r, c, r, R, L, d, r, d, L, D, L, R, L, D];
185186
const mirror = (v: number, n: number): number => {
186187
while (v < 0 || v >= n) v = v < 0 ? -v - 1 : 2 * n - 1 - v;
187188
return v;
188189
};
190+
// (the weights by column: of rows -2 to 2; summed by column, then row)
189191
for (let y = y0; y < y1; y++) {
190-
const rows = [-2, -1, 0, 1, 2].map(dy => mirror(y + dy, h) * w);
192+
const r0 = mirror(y - 2, h) * w, r1 = mirror(y - 1, h) * w, r2 = y * w, r3 = mirror(y + 1, h) * w, r4 = mirror(y + 2, h) * w;
191193
for (let x = 0; x < w; x++) {
194+
const inside = x >= 2 && x + 2 < w;
195+
const c0 = inside ? x - 2 : mirror(x - 2, w), c1 = inside ? x - 1 : mirror(x - 1, w), c3 = inside ? x + 1 : mirror(x + 1, w), c4 = inside ? x + 2 : mirror(x + 2, w);
192196
let sum = 0;
193-
for (let dx = -2; dx <= 2; dx++) {
194-
const col = x + dx >= 0 && x + dx < w ? x + dx : mirror(x + dx, w);
195-
for (let k = 0; k < 5; k++) sum += weights[k * 5 + dx + 2] * m[rows[k] + col];
196-
}
197+
sum += D * m[r0 + c0]; sum += L * m[r1 + c0]; sum += R * m[r2 + c0]; sum += L * m[r3 + c0]; sum += D * m[r4 + c0];
198+
sum += L * m[r0 + c1]; sum += d * m[r1 + c1]; sum += r * m[r2 + c1]; sum += d * m[r3 + c1]; sum += L * m[r4 + c1];
199+
sum += R * m[r0 + x]; sum += r * m[r1 + x]; sum += c * m[r2 + x]; sum += r * m[r3 + x]; sum += R * m[r4 + x];
200+
sum += L * m[r0 + c3]; sum += d * m[r1 + c3]; sum += r * m[r2 + c3]; sum += d * m[r3 + c3]; sum += L * m[r4 + c3];
201+
sum += D * m[r0 + c4]; sum += L * m[r1 + c4]; sum += R * m[r2 + c4]; sum += L * m[r3 + c4]; sum += D * m[r4 + c4];
197202
out[y * w + x] = sum;
198203
}
199204
}

‎src/codecs/jxl/dct.ts‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ const work = { row: new Float64Array(256), scratch: new Float64Array(256) };
102102
const SEC8 = secHalf(8);
103103

104104
// the inverse 8-point DCT of buf[o + k * step] in place (like dct's recursion, unrolled)
105-
function idct8(buf: Float32Array, o: number, step: number): void {
105+
export function idct8(buf: Float32Array, o: number, step: number): void {
106106
const v0 = buf[o], v1 = buf[o + step], v2 = buf[o + 2 * step], v3 = buf[o + 3 * step];
107107
const v4 = buf[o + 4 * step], v5 = buf[o + 5 * step], v6 = buf[o + 6 * step], v7 = buf[o + 7 * step];
108108
// the even half: v0, v2, v4, v6
@@ -229,7 +229,28 @@ const set = (b: Block, x: number, y: number, v: number): void => {
229229
b.buf[b.offset + y * b.stride + x] = v;
230230
};
231231
const tmp = { a: new Float32Array(64), b: new Float32Array(64) };
232-
const blockOf = (buf: Float32Array, offset: number, w: number, h: number, stride = w): Block => ({ buf, offset, stride, w, h });
232+
233+
// the inverse 4-point DCT of buf[o + k * step] in place (dct4's)
234+
function idct4(buf: Float32Array, o: number, step: number): void {
235+
const i0 = buf[o], i1 = buf[o + step], i2 = buf[o + 2 * step], i3 = buf[o + 3 * step];
236+
const t0 = i1 * Math.SQRT2, t1 = i1 + i3, out0 = (t0 + t1) * SEC0, out1 = (t0 - t1) * SEC1;
237+
const sum02 = i0 + i2, sub02 = i0 - i2;
238+
buf[o] = sum02 + out0;
239+
buf[o + step] = sub02 + out1;
240+
buf[o + 2 * step] = sub02 - out1;
241+
buf[o + 3 * step] = sum02 - out0;
242+
}
243+
244+
// the inverse 2D DCTs of 8x4 and 4x4 blocks at buf[o] (rows, then columns: dct2d's)
245+
function idct8x4(buf: Float32Array, o: number): void {
246+
for (let y = 0; y < 4; y++) idct8(buf, o + y * 8, 1);
247+
for (let x = 0; x < 8; x++) idct4(buf, o + x, 8);
248+
}
249+
250+
function idct4x4(buf: Float32Array, o: number): void {
251+
for (let y = 0; y < 4; y++) idct4(buf, o + y * 4, 1);
252+
for (let x = 0; x < 4; x++) idct4(buf, o + x, 4);
253+
}
233254

234255
function idct2InPlace(b: Block, size: number): void {
235256
const half = size / 2, s = tmp.a;
@@ -259,7 +280,7 @@ function dct4x4(b: Block): void {
259280
const o = (y * 2 + x) * 16;
260281
// (transposed)
261282
for (let iy = 0; iy < 4; iy++) for (let ix = 0; ix < 4; ix++) s[o + ix * 4 + iy] = get(b, x + ix * 2, y + iy * 2);
262-
dct2d(blockOf(s, o, 4, 4), true);
283+
idct4x4(s, o);
263284
}
264285
}
265286
for (let y = 0; y < 2; y++) for (let x = 0; x < 2; x++) for (let iy = 0; iy < 4; iy++) for (let ix = 0; ix < 4; ix++) set(b, x * 4 + ix, y * 4 + iy, s[(y * 2 + x) * 16 + iy * 4 + ix]);
@@ -290,27 +311,28 @@ function dct4x8(b: Block, transposed: boolean): void {
290311
const s = tmp.b;
291312
for (let i = 0; i < 2; i++) {
292313
for (let iy = 0; iy < 4; iy++) for (let ix = 0; ix < 8; ix++) s[i * 32 + iy * 8 + ix] = get(b, ix, iy * 2 + i);
293-
dct2d(blockOf(s, i * 32, 8, 4), true);
314+
idct8x4(s, i * 32);
294315
}
295316
for (let y = 0; y < 8; y++) for (let x = 0; x < 8; x++) {
296317
if (transposed) set(b, y, x, s[y * 8 + x]);
297318
else set(b, x, y, s[y * 8 + x]);
298319
}
299320
}
300321

322+
const afvScratch = { coeffs: new Float64Array(16), samples: new Float64Array(16), s4: new Float32Array(16), s8: new Float32Array(32) };
323+
301324
function afv(b: Block, n: number): void {
302325
const flipX = n % 2, flipY = n >> 1;
303-
const coeffs = new Float64Array(16), samples = new Float64Array(16);
326+
const coeffs = afvScratch.coeffs, samples = afvScratch.samples.fill(0), s4 = afvScratch.s4, s8 = afvScratch.s8;
304327
coeffs[0] = (get(b, 0, 0) + get(b, 1, 0) + get(b, 0, 1)) * 4;
305328
for (let i = 1; i < 16; i++) coeffs[i] = get(b, 2 * (i % 4), 2 * (i >> 2));
306329
for (let i = 0; i < 16; i++) for (let k = 0; k < 16; k++) samples[k] += coeffs[i] * AFV_BASIS[i * 16 + k];
307-
const s4 = new Float32Array(16), s8 = new Float32Array(32);
308330
s4[0] = get(b, 0, 0) - get(b, 1, 0) + get(b, 0, 1);
309331
for (let iy = 0; iy < 4; iy++) for (let ix = 0; ix < 4; ix++) if (ix | iy) s4[ix * 4 + iy] = get(b, 2 * ix + 1, 2 * iy);
310-
dct2d(blockOf(s4, 0, 4, 4), true);
332+
idct4x4(s4, 0);
311333
s8[0] = get(b, 0, 0) - get(b, 0, 1);
312334
for (let iy = 0; iy < 4; iy++) for (let ix = 0; ix < 8; ix++) if (ix | iy) s8[iy * 8 + ix] = get(b, ix, 2 * iy + 1);
313-
dct2d(blockOf(s8, 0, 8, 4), true);
335+
idct8x4(s8, 0);
314336
for (let iy = 0; iy < 4; iy++) {
315337
const ay = flipY ? 3 - iy : iy;
316338
for (let ix = 0; ix < 4; ix++) set(b, flipX * 4 + ix, flipY * 4 + iy, samples[ay * 4 + (flipX ? 3 - ix : ix)]);
@@ -337,7 +359,7 @@ export function inverseTransform(b: Block, type: number): void {
337359
export { sub as subBlock };
338360

339361
// the AFV basis (16 x 16)
340-
const AFV_BASIS = [
362+
export const AFV_BASIS = [
341363
0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25,
342364
0.876902929799142, 0.2206518106944235, -0.10140050393753763, -0.1014005039375375, 0.2206518106944236, -0.10140050393753777, -0.10140050393753772, -0.10140050393753763, -0.10140050393753758, -0.10140050393753769, -0.1014005039375375, -0.10140050393753768, -0.10140050393753768, -0.10140050393753759, -0.10140050393753763, -0.10140050393753741,
343365
0.0, 0.0, 0.40670075830260755, 0.44444816619734445, 0.0, 0.0, 0.19574399372042936, 0.2929100136981264, -0.40670075830260716, -0.19574399372042872, 0.0, 0.11379074460448091, -0.44444816619734384, -0.29291001369812636, -0.1137907446044814, 0.0,

0 commit comments

Comments
 (0)