From 5ec43fa8a34196c838cabdadd0bd9fee619a7952 Mon Sep 17 00:00:00 2001 From: 134 Date: Fri, 3 Jul 2026 21:50:34 +0800 Subject: [PATCH] fix: reject banded rmbg scale drafts --- app/api/canvas/local-edit/route.ts | 111 ++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 43d55ec03..4461bcccd 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1709,6 +1709,83 @@ function getScaleAdjustAlphaBoundingBox(params: { }; } +function thresholdScaleAdjustAlphaMask(params: { + maskBuffer: Buffer | Uint8Array; + threshold?: number; +}) { + const { maskBuffer, threshold = 96 } = params; + const output = new Uint8Array(maskBuffer.length); + for (let index = 0; index < maskBuffer.length; index += 1) { + output[index] = (maskBuffer[index] || 0) >= threshold ? 255 : 0; + } + return output; +} + +function getScaleAdjustAlphaBandingStats(params: { + height: number; + maskBuffer: Buffer | Uint8Array; + threshold?: number; + width: number; +}) { + const { height, maskBuffer, threshold = 24, width } = params; + const mask = thresholdScaleAdjustAlphaMask({ maskBuffer, threshold }); + const bbox = getScaleAdjustMaskBoundingBox({ height, mask, width }); + if (!bbox) { + return { + activeRowRatio: 0, + bboxHeightRatio: 0, + bboxWidthRatio: 0, + detected: false, + transitionRatio: 0, + wideRowRatio: 0, + }; + } + + let activeRows = 0; + let transitions = 0; + let wideRows = 0; + let previousCoverage: number | null = null; + for (let y = bbox.top; y < bbox.top + bbox.height; y += 1) { + let rowCount = 0; + for (let x = bbox.left; x < bbox.left + bbox.width; x += 1) { + if (mask[y * width + x] !== 0) rowCount += 1; + } + const coverage = rowCount / Math.max(1, bbox.width); + if (coverage >= 0.08) activeRows += 1; + if (coverage >= 0.45) wideRows += 1; + if ( + previousCoverage !== null && + Math.abs(coverage - previousCoverage) >= 0.28 + ) { + transitions += 1; + } + previousCoverage = coverage; + } + + const bboxHeightRatio = bbox.height / Math.max(1, height); + const bboxWidthRatio = bbox.width / Math.max(1, width); + const activeRowRatio = activeRows / Math.max(1, bbox.height); + const transitionRatio = transitions / Math.max(1, bbox.height - 1); + const wideRowRatio = wideRows / Math.max(1, bbox.height); + const detected = + (bboxWidthRatio >= 0.38 && + bboxHeightRatio >= 0.22 && + wideRowRatio >= 0.18 && + transitionRatio >= 0.12) || + (bboxWidthRatio >= 0.55 && + activeRowRatio >= 0.45 && + transitionRatio >= 0.09); + + return { + activeRowRatio: Number(activeRowRatio.toFixed(3)), + bboxHeightRatio: Number(bboxHeightRatio.toFixed(3)), + bboxWidthRatio: Number(bboxWidthRatio.toFixed(3)), + detected, + transitionRatio: Number(transitionRatio.toFixed(3)), + wideRowRatio: Number(wideRowRatio.toFixed(3)), + }; +} + async function cleanScaleAdjustAlphaChannel(params: { height: number; referenceRawRgb?: Buffer | null; @@ -1883,14 +1960,45 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { targetWidth: placement.sourceBox.width, width: cutoutWidth, }); + const alphaBandingStats = getScaleAdjustAlphaBandingStats({ + height: placement.sourceBox.height, + maskBuffer: sourceMaskBuffer, + width: placement.sourceBox.width, + }); + if (alphaBandingStats.detected) { + console.warn( + "[Canvas Local Edit] Scale adjust RMBG alpha banding detected; fallback to pixel mask:", + alphaBandingStats + ); + return null; + } const background = estimateScaleAdjustBoxBackground({ box: placement.sourceBox, raw, width: sourceWidth, }); + const highConfidenceSourceMask = thresholdScaleAdjustAlphaMask({ + maskBuffer: sourceMaskBuffer, + threshold: 96, + }); + const filteredSourceMask = filterScaleAdjustStructureComponents({ + box: placement.sourceBox, + mask: highConfidenceSourceMask, + }); + const filteredSourceBbox = getScaleAdjustMaskBoundingBox({ + height: placement.sourceBox.height, + mask: filteredSourceMask, + width: placement.sourceBox.width, + }); + const sourceWashBaseMask = filteredSourceBbox + ? solidifyScaleAdjustStructureMask({ + box: placement.sourceBox, + mask: filteredSourceMask, + }) + : highConfidenceSourceMask; const sourceWashMask = dilateScaleAdjustStructureMask({ height: placement.sourceBox.height, - mask: new Uint8Array(sourceMaskBuffer), + mask: sourceWashBaseMask, radius: Math.max( 2, Math.round( @@ -1977,6 +2085,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: { placedHeight, placedWidth, sameSizeCutout, + alphaBandingStats, sourceHeight: placement.sourceBox.height, sourceWidth: placement.sourceBox.width, });