Skip to content

Commit 5955b04

Browse files
matmenclaude
andcommitted
JXL: faster patch search
The connected components outside the background are traversed with a growing Int32Array stack of pixel indices instead of a JS array of coordinate pairs, popped in the same order (the first background pixel reached stays the reference): the same files (20 encodes at efforts 0-2). A 1920x1080 photo's search, which finds no patches, takes about 550 instead of 750 ms here (most pixels form one large component). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GAqbnFSNaYDBGD1Km2
1 parent 4bf2c55 commit 5955b04

2 files changed

Lines changed: 17 additions & 7 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
instead of 640 ms lossy and 328 ms instead of 671 ms lossless, a 5120x2880 one in 568 ms instead of 1121 ms and
2525
487 ms instead of 1122 ms. Lossy frames no longer keep the coefficients of the whole frame (about 100 MB at 4K): each
2626
group is rendered once its passes are read.
27+
- `encodeJXL({ quality, effort: 2 })` searches patches about 30% faster (the connected components with a typed stack of
28+
pixel indices instead of an array of coordinates, in the same order): the same files.
2729
- AVIF decoding uses worker threads (`DecodeOptions.threads`) from 4 megapixels on: the conversion to RGB runs in
2830
bands of rows (started at the beginning, the threads load while the AV1 image is decoded) and the tiles of grids are
2931
decoded in parallel, with the same pixels. A 3840x2160 4:2:0 image decodes in 355 ms instead of 466 ms, a 6400x2880

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,18 @@ function candidates(planes: Float32Array[], stride: number, width: number, heigh
122122
}
123123

124124
// small connected components outside the background, bordered by one background color
125+
// (a stack of pixel indices, grown as needed: a pixel is pushed by each neighbor reaching it before it's visited)
125126
const visited = new Uint8Array(width * height), found: Candidate[] = [];
127+
let stack = new Int32Array(1024);
126128
for (let y = 0; y < height; y++) {
127129
for (let x = 0; x < width; x++) {
128130
if (isBackground[y * width + x] || visited[y * width + x]) continue;
129-
const stack = [x, y];
130-
let minX = x, maxX = x, minY = y, maxY = y, reference = -1, allSimilar = true;
131-
while (stack.length) {
132-
const cy = stack.pop() as number, cx = stack.pop() as number;
133-
if (visited[cy * width + cx]) continue;
134-
visited[cy * width + cx] = 1;
131+
stack[0] = y * width + x;
132+
let top = 1, minX = x, maxX = x, minY = y, maxY = y, reference = -1, allSimilar = true;
133+
while (top) {
134+
const pixel = stack[--top], cy = pixel / width | 0, cx = pixel - cy * width;
135+
if (visited[pixel]) continue;
136+
visited[pixel] = 1;
135137
minX = Math.min(minX, cx); maxX = Math.max(maxX, cx); minY = Math.min(minY, cy); maxY = Math.max(maxY, cy);
136138
for (let dx = -1; dx <= 1; dx++) {
137139
for (let dy = -1; dy <= 1; dy++) {
@@ -140,7 +142,13 @@ function candidates(planes: Float32Array[], stride: number, width: number, heigh
140142
if (nx < 0 || ny < 0 || nx >= width || ny >= height) continue;
141143
const i = ny * width + nx;
142144
if (!isBackground[i]) {
143-
if (!visited[i]) stack.push(nx, ny);
145+
if (visited[i]) continue;
146+
if (top === stack.length) {
147+
const grown = new Int32Array(2 * top);
148+
grown.set(stack);
149+
stack = grown;
150+
}
151+
stack[top++] = i;
144152
}
145153
else if (reference < 0) reference = i;
146154
else if (!similar(background, i, background, reference, VERY_SIMILAR)) allSimilar = false;

0 commit comments

Comments
 (0)