From 25c1f79fc136db332662705f71a9e832e65eedc5 Mon Sep 17 00:00:00 2001 From: Ty Balascio Date: Fri, 25 Sep 2026 09:09:30 -0700 Subject: [PATCH] fix(fe): downscale mask flood-fill to a working resolution The border flood-fill on a full-res 2256x4000 (9M px) photo was too slow in JS and the async mask pass timed out into the catch branch (overlay stayed null, 'Mask settled' never rendered). Downscale to <=1200px for the fill, then nearest-neighbor upsample the slab bitset back to full size so minAreaRect/slabBounds stay in source-pixel coordinates. --- frontend/src/capture/session.ts | 34 ++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/src/capture/session.ts b/frontend/src/capture/session.ts index 6616354..ee8bf11 100644 --- a/frontend/src/capture/session.ts +++ b/frontend/src/capture/session.ts @@ -117,7 +117,39 @@ function walkMask(decoded: DecodedPhoto, params: MaskParams): Uint8Array { const { width, height, rgba } = decoded; const tone = sheetToneFor(params.sheet, decoded); const tolerance = 20 + (params.sensitivity / 100) * 80; // 20..100 distance - return removeBackground(width, height, rgba, { tolerance, sheetTone: tone }); + // Downscale to a working resolution so the flood-fill stays fast on large + // photos; the bitset is remapped back to full size for downstream geometry. + const MAX = 1200; + const scale = Math.min(1, MAX / Math.max(width, height)); + if (scale >= 0.999) { + return removeBackground(width, height, rgba, { tolerance, sheetTone: tone }); + } + const sw = Math.round(width * scale); + const sh = Math.round(height * scale); + const small = new Uint8ClampedArray(sw * sh * 4); + for (let y = 0; y < sh; y++) { + for (let x = 0; x < sw; x++) { + const sx = Math.min(width - 1, Math.round((x / sw) * width)); + const sy = Math.min(height - 1, Math.round((y / sh) * height)); + const si = (sy * width + sx) * 4; + const di = (y * sw + x) * 4; + small[di] = rgba[si]; + small[di + 1] = rgba[si + 1]; + small[di + 2] = rgba[si + 2]; + small[di + 3] = 255; + } + } + const smallBitset = removeBackground(sw, sh, small, { tolerance, sheetTone: tone }); + // Remap the small bitset back to full resolution (nearest-neighbor upsample). + const full = new Uint8Array(width * height); + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const tx = Math.min(sw - 1, Math.round((x / width) * sw)); + const ty = Math.min(sh - 1, Math.round((y / height) * sh)); + full[y * width + x] = smallBitset[ty * sw + tx]; + } + } + return full; } /** Sheet tone to anchor the flood fill: sampled border, biased by the choice. */