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. */