From 8ecece64e13df89ad7ad8514b2d57a3028319e28 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 20:20:54 +0800 Subject: [PATCH] fix: filter cutout matte artifacts --- app/api/canvas/local-edit/route.ts | 57 ++++++++++++++++++++---------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 0a0059076..5ad3bb8aa 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -111,8 +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 = 96; -const SCALE_ADJUST_RMBG_ALPHA_MEDIAN_SIZE = 5; +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; @@ -1712,36 +1711,48 @@ function getScaleAdjustAlphaBoundingBox(params: { async function cleanScaleAdjustAlphaChannel(params: { height: number; + referenceRawRgb?: Buffer | null; rawRgba: Buffer; threshold?: number; width: number; }) { const { height, + referenceRawRgb = null, rawRgba, threshold = SCALE_ADJUST_RMBG_ALPHA_CLEAN_THRESHOLD, width, } = params; const pixelCount = Math.floor(rawRgba.length / 4); - const initialAlpha = Buffer.alloc(pixelCount); - for (let index = 0; index < pixelCount; index += 1) { - const sourceAlpha = rawRgba[index * 4 + 3] || 0; - initialAlpha[index] = sourceAlpha >= threshold ? 255 : 0; - } - - const alphaData = await sharp(initialAlpha, { - raw: { channels: 1, height, width }, - }) - .median(SCALE_ADJUST_RMBG_ALPHA_MEDIAN_SIZE) - .threshold(128) - .raw() - .toBuffer(); const cleanedRaw = Buffer.from(rawRgba); const cleanedAlpha = Buffer.alloc(pixelCount); + const range = Math.max(1, 255 - threshold); + const hasReference = + referenceRawRgb && referenceRawRgb.length >= pixelCount * 3; for (let index = 0; index < pixelCount; index += 1) { + const colorIndex = index * 4; const alphaIndex = index * 4 + 3; - const nextAlpha = alphaData[index] >= 128 ? 255 : 0; + const sourceAlpha = rawRgba[alphaIndex] || 0; + let matteArtifact = false; + if (hasReference && sourceAlpha >= threshold) { + const r = rawRgba[colorIndex] || 0; + const g = rawRgba[colorIndex + 1] || 0; + const b = rawRgba[colorIndex + 2] || 0; + const luma = getScaleAdjustPixelLuma(r, g, b); + const channelSpread = Math.max(r, g, b) - Math.min(r, g, b); + const refIndex = index * 3; + const refLuma = getScaleAdjustPixelLuma( + referenceRawRgb?.[refIndex] || 0, + referenceRawRgb?.[refIndex + 1] || 0, + referenceRawRgb?.[refIndex + 2] || 0 + ); + matteArtifact = luma <= 18 && channelSpread <= 18 && refLuma >= 42; + } + const nextAlpha = + sourceAlpha < threshold || matteArtifact + ? 0 + : Math.max(0, Math.min(255, Math.round(((sourceAlpha - threshold) / range) * 255))); cleanedRaw[alphaIndex] = nextAlpha; cleanedAlpha[index] = nextAlpha; } @@ -1818,6 +1829,16 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { const cutoutWidth = Math.round(Number(cutoutMetadata.width || 0)); const cutoutHeight = Math.round(Number(cutoutMetadata.height || 0)); if (cutoutWidth <= 0 || cutoutHeight <= 0) return null; + const sameSizeCutout = + Math.abs(cutoutWidth - placement.sourceBox.width) <= 2 && + Math.abs(cutoutHeight - placement.sourceBox.height) <= 2; + const sourceCropRaw = sameSizeCutout + ? await sharp(sourceCropBuffer, { failOn: "none" }) + .removeAlpha() + .toColorspace("srgb") + .raw() + .toBuffer() + : null; const cutoutRaw = await sharp(normalizedCutout, { failOn: "none" }) .ensureAlpha() @@ -1825,6 +1846,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { .toBuffer(); const { cleanedAlpha, cleanedRaw } = await cleanScaleAdjustAlphaChannel({ height: cutoutHeight, + referenceRawRgb: sourceCropRaw, rawRgba: cutoutRaw, width: cutoutWidth, }); @@ -1851,9 +1873,6 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { return null; } - const sameSizeCutout = - Math.abs(cutoutWidth - placement.sourceBox.width) <= 2 && - Math.abs(cutoutHeight - placement.sourceBox.height) <= 2; const fullAlphaMask = cleanedAlpha; const sourceMaskBuffer = sameSizeCutout ? fullAlphaMask