From 646cc9bf2fb59a1d88ebd654c66c07094db24814 Mon Sep 17 00:00:00 2001 From: Mathis Mensing Date: Thu, 15 Oct 2026 21:19:59 +0000 Subject: [PATCH] faster resizing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The horizontal pass reads the coefficients outside the pixel loop, the vertical pass sums each value over its rows in a variable (rounded to float32 after every row, as the Float32Array did) instead of reading and writing the output once per row, with the weights of up to 4 rows in variables. The results are bit-identical: checked against master on 1593 images (every mode, shrinking and enlarging). About 10% faster for halving a 1920×1080 photo. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ --- CHANGELOG.md | 1 + src/util/resample.ts | 68 ++++++++++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfa7fe23..844f5d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ computed without branches, 8-bit RGB images are converted without per-sample function calls. - Lossless WebP encoding is about 15% faster, with identical files: backward reference candidates that can't be longer are skipped, the per-channel predictor helpers are unrolled. +- Resizing (bilinear, bicubic and area) is about 10% faster, with identical results. ### Deprecated diff --git a/src/util/resample.ts b/src/util/resample.ts index ea49e060..4372bee2 100644 --- a/src/util/resample.ts +++ b/src/util/resample.ts @@ -69,34 +69,34 @@ function coefficients(inSize: number, outSize: number, filter: Filter): Coeffici export function resample(src: Uint8ClampedArray, width: number, height: number, outWidth: number, outHeight: number, filter: Filter, out: Uint8ClampedArray): void { // premultiplied float rows, scaled horizontally (premultiplying on the fly) let plane = new Float32Array(outWidth * height * 4); - const horizontal = outWidth !== width ? coefficients(width, outWidth, filter) : null; - - for (let y = 0; y < height; y++) { - const row = y * width * 4; - for (let x = 0; x < outWidth; x++) { - let r = 0, g = 0, b = 0, a = 0; - if (horizontal) { - const { start, count, weights, size } = horizontal; - for (let k = 0, s = row + start[x] * 4; k < count[x]; k++, s += 4) { - const wa = weights[x * size + k] * src[s + 3]; + + if (outWidth !== width) { + const { start, count, weights, size } = coefficients(width, outWidth, filter); + for (let y = 0, o = 0; y < height; y++) { + const row = y * width * 4; + for (let x = 0; x < outWidth; x++, o += 4) { + let r = 0, g = 0, b = 0, a = 0; + for (let k = 0, s = row + start[x] * 4, w = x * size, n = count[x]; k < n; k++, s += 4) { + const wa = weights[w + k] * src[s + 3]; r += src[s] * wa; g += src[s + 1] * wa; b += src[s + 2] * wa; a += wa; } - } else { - const s = row + x * 4; - a = src[s + 3]; - r = src[s] * a; - g = src[s + 1] * a; - b = src[s + 2] * a; - } - const o = (y * outWidth + x) * 4; - plane[o] = r / 255; - plane[o + 1] = g / 255; - plane[o + 2] = b / 255; - plane[o + 3] = a; + plane[o] = r / 255; + plane[o + 1] = g / 255; + plane[o + 2] = b / 255; + plane[o + 3] = a; + } + } + } else { + for (let s = 0; s < plane.length; s += 4) { + const a = src[s + 3]; + plane[s] = src[s] * a / 255; + plane[s + 1] = src[s + 1] * a / 255; + plane[s + 2] = src[s + 2] * a / 255; + plane[s + 3] = a; } } @@ -105,12 +105,26 @@ export function resample(src: Uint8ClampedArray, width: number, height: number, const next = new Float32Array(outWidth * outHeight * 4); const stride = outWidth * 4; + // every sum is rounded to float32 after each row it adds, as accumulating in a Float32Array would. Up to 4 rows + // (bilinear and bicubic, from half the size up) are added with the weights in variables, more in a loop for (let y = 0; y < outHeight; y++) { - const o = y * stride; - for (let k = 0; k < count[y]; k++) { - const w = weights[y * size + k]; - const s = (start[y] + k) * stride; - for (let x = 0; x < stride; x++) next[o + x] += plane[s + x] * w; + const o = y * stride, n = count[y], w = y * size; + const s0 = start[y] * stride, s1 = s0 + stride, s2 = s1 + stride, s3 = s2 + stride; + const w0 = weights[w], w1 = weights[w + 1], w2 = weights[w + 2], w3 = weights[w + 3]; + if (n === 1) for (let x = 0; x < stride; x++) next[o + x] = 0 + plane[s0 + x] * w0; + else if (n === 2) for (let x = 0; x < stride; x++) next[o + x] = Math.fround(0 + plane[s0 + x] * w0) + plane[s1 + x] * w1; + else if (n === 3) { + for (let x = 0; x < stride; x++) + next[o + x] = Math.fround(Math.fround(0 + plane[s0 + x] * w0) + plane[s1 + x] * w1) + plane[s2 + x] * w2; + } else if (n === 4) { + for (let x = 0; x < stride; x++) + next[o + x] = Math.fround(Math.fround(Math.fround(0 + plane[s0 + x] * w0) + plane[s1 + x] * w1) + plane[s2 + x] * w2) + plane[s3 + x] * w3; + } else { + for (let x = 0; x < stride; x++) { + let sum = 0; + for (let k = 0, s = s0 + x; k < n; k++, s += stride) sum = Math.fround(sum + plane[s] * weights[w + k]); + next[o + x] = sum; + } } }