From 27b0098bf9d38c8e1679c5488ccf10f708a9b2f9 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 20:02:00 +0800 Subject: [PATCH] fix: clean cutout alpha for scale drafts --- app/api/canvas/local-edit/route.ts | 46 ++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 06bc92a5b..e5ac066c7 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -111,6 +111,7 @@ const SCALE_ADJUST_STRUCTURE_LOCK_MIN_MASK_RATIO = 0.006; const SCALE_ADJUST_STRUCTURE_LOCK_COMPONENT_MIN_RATIO = 0.00018; const SCALE_ADJUST_RMBG_MIN_ALPHA_RATIO = 0.012; const SCALE_ADJUST_RMBG_MAX_SOLID_ALPHA_RATIO = 0.92; +const SCALE_ADJUST_RMBG_ALPHA_CLEAN_THRESHOLD = 42; const MIN_REGION_EDGE_PX = 24; const SCALE_ADJUST_OPERATION = "scale-adjust"; type LocalEditOperation = "local-edit" | typeof SCALE_ADJUST_OPERATION; @@ -1708,17 +1709,28 @@ function getScaleAdjustAlphaBoundingBox(params: { }; } -function extractScaleAdjustAlphaChannel(params: { - height: number; +function cleanScaleAdjustAlphaChannel(params: { rawRgba: Buffer; - width: number; + threshold?: number; }) { - const { height, rawRgba, width } = params; - const alpha = Buffer.alloc(width * height); - for (let index = 0; index < width * height; index += 1) { - alpha[index] = rawRgba[index * 4 + 3] || 0; + const { rawRgba, threshold = SCALE_ADJUST_RMBG_ALPHA_CLEAN_THRESHOLD } = params; + const pixelCount = Math.floor(rawRgba.length / 4); + const cleanedRaw = Buffer.from(rawRgba); + const cleanedAlpha = Buffer.alloc(pixelCount); + const range = Math.max(1, 255 - threshold); + + for (let index = 0; index < pixelCount; index += 1) { + const alphaIndex = index * 4 + 3; + const sourceAlpha = rawRgba[alphaIndex] || 0; + const nextAlpha = + sourceAlpha < threshold + ? 0 + : Math.max(0, Math.min(255, Math.round(((sourceAlpha - threshold) / range) * 255))); + cleanedRaw[alphaIndex] = nextAlpha; + cleanedAlpha[index] = nextAlpha; } - return alpha; + + return { cleanedAlpha, cleanedRaw }; } async function resizeScaleAdjustAlphaMask(params: { @@ -1795,9 +1807,17 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { .ensureAlpha() .raw() .toBuffer(); + const { cleanedAlpha, cleanedRaw } = cleanScaleAdjustAlphaChannel({ + rawRgba: cutoutRaw, + }); + const cleanedCutout = await sharp(cleanedRaw, { + raw: { channels: 4, height: cutoutHeight, width: cutoutWidth }, + }) + .png({ compressionLevel: 6 }) + .toBuffer(); const alphaBbox = getScaleAdjustAlphaBoundingBox({ height: cutoutHeight, - rawRgba: cutoutRaw, + rawRgba: cleanedRaw, width: cutoutWidth, }); if (!alphaBbox) return null; @@ -1816,11 +1836,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { const sameSizeCutout = Math.abs(cutoutWidth - placement.sourceBox.width) <= 2 && Math.abs(cutoutHeight - placement.sourceBox.height) <= 2; - const fullAlphaMask = extractScaleAdjustAlphaChannel({ - height: cutoutHeight, - rawRgba: cutoutRaw, - width: cutoutWidth, - }); + const fullAlphaMask = cleanedAlpha; const sourceMaskBuffer = sameSizeCutout ? fullAlphaMask : await resizeScaleAdjustAlphaMask({ @@ -1852,7 +1868,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { maskBuffer: Buffer.from(sourceWashMask), }); - const subjectCutout = await sharp(normalizedCutout, { failOn: "none" }) + const subjectCutout = await sharp(cleanedCutout, { failOn: "none" }) .extract(alphaBbox) .png({ compressionLevel: 6 }) .toBuffer();