From fa7d9d81ffbb5a60c509f305430498f71e2eb4da Mon Sep 17 00:00:00 2001 From: 134 Date: Sat, 4 Jul 2026 18:58:30 +0800 Subject: [PATCH] fix: exclude protected people from furniture scale masks --- app/api/canvas/local-edit/route.ts | 114 ++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 2 deletions(-) diff --git a/app/api/canvas/local-edit/route.ts b/app/api/canvas/local-edit/route.ts index 458af05a3..409144035 100644 --- a/app/api/canvas/local-edit/route.ts +++ b/app/api/canvas/local-edit/route.ts @@ -1356,6 +1356,24 @@ function isScaleAdjustFurnitureProductLikePrompt(prompt: string) { ); } +type ScaleAdjustProtectedPersonSide = "auto" | "left" | "right"; + +function getScaleAdjustProtectedPersonSide( + prompt: string +): ScaleAdjustProtectedPersonSide | null { + const text = normalizeScaleAdjustModelText(prompt); + if ( + !/person|people|human|woman|man|model|girl|boy|face|body|人物|人像|人|模特|女人|男人|女孩|男孩|脸|身体/.test( + text + ) + ) { + return null; + } + if (/right|右侧|右边|右方|右/.test(text)) return "right"; + if (/left|左侧|左边|左方|左/.test(text)) return "left"; + return "auto"; +} + function hasScaleAdjustPersonPixelSignal(params: { box: { left: number; top: number; width: number; height: number }; raw: Buffer; @@ -1550,11 +1568,61 @@ function buildScaleAdjustStructureInitialMask(params: { }); } +function buildScaleAdjustProtectedPersonSeedMask(params: { + box: { left: number; top: number; width: number; height: number }; + raw: Buffer; + side: ScaleAdjustProtectedPersonSide; + width: number; +}) { + const { box, raw, side, width } = params; + const seed = new Uint8Array(box.width * box.height); + let seedCount = 0; + const sideAllowsX = (localX: number) => { + if (side === "right") return localX >= box.width * 0.52; + if (side === "left") return localX <= box.width * 0.48; + return true; + }; + + for (let localY = 0; localY < box.height; localY += 1) { + const y = box.top + localY; + for (let localX = 0; localX < box.width; localX += 1) { + if (!sideAllowsX(localX)) continue; + const x = box.left + localX; + const rawIndex = getScaleAdjustRawPixelIndex(width, x, y); + const r = raw[rawIndex] || 0; + const g = raw[rawIndex + 1] || 0; + const b = raw[rawIndex + 2] || 0; + const { luma, saturation } = getScaleAdjustPixelStats(r, g, b); + const likelySkin = isScaleAdjustLikelySkinPixel(r, g, b); + const likelyWhiteFabric = + isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) && + luma < 246 && + saturation < 0.22; + if (!likelySkin && !likelyWhiteFabric) continue; + seed[localY * box.width + localX] = 255; + seedCount += 1; + } + } + + const seedRatio = seedCount / Math.max(1, box.width * box.height); + if (seedCount < 24 || seedRatio < 0.0006) return null; + + return dilateScaleAdjustStructureMask({ + height: box.height, + mask: seed, + radius: Math.max(4, Math.round(Math.min(box.width, box.height) * 0.032)), + width: box.width, + }); +} + function filterScaleAdjustStructureComponents(params: { box: { width: number; height: number }; mask: Uint8Array; + protectedPersonMask?: Uint8Array | null; + protectedPersonSide?: ScaleAdjustProtectedPersonSide | null; }) { - const { box, mask } = params; + const { box, mask, protectedPersonMask = null, protectedPersonSide = null } = + params; const width = box.width; const height = box.height; const visited = new Uint8Array(mask.length); @@ -1582,6 +1650,7 @@ function filterScaleAdjustStructureComponents(params: { let minY = y; let maxX = x; let maxY = y; + let protectedPersonOverlap = 0; while (head < queueX.length) { const cx = queueX[head] || 0; @@ -1592,6 +1661,9 @@ function filterScaleAdjustStructureComponents(params: { minY = Math.min(minY, cy); maxX = Math.max(maxX, cx); maxY = Math.max(maxY, cy); + if (protectedPersonMask?.[cy * width + cx]) { + protectedPersonOverlap += 1; + } for (let dy = -1; dy <= 1; dy += 1) { for (let dx = -1; dx <= 1; dx += 1) { @@ -1616,7 +1688,27 @@ function filterScaleAdjustStructureComponents(params: { const centerYRatio = (minY + componentHeight / 2) / height; const touchesTop = minY <= height * 0.04; const touchesRight = maxX >= width * 0.88; + const touchesLeft = minX <= width * 0.12; const edgeSliver = touchesRight && componentWidth < width * 0.18; + const touchesProtectedPersonSide = + protectedPersonSide === "right" + ? maxX >= width * 0.68 + : protectedPersonSide === "left" + ? minX <= width * 0.32 + : protectedPersonSide === "auto" + ? touchesLeft || touchesRight + : false; + const protectedPersonOverlapRatio = + protectedPersonOverlap / Math.max(1, area); + const likelyProtectedPersonComponent = + Boolean(protectedPersonMask) && + protectedPersonOverlap > 0 && + (protectedPersonOverlapRatio >= 0.012 || + (touchesProtectedPersonSide && + protectedPersonOverlap >= Math.max(6, area * 0.002))) && + (touchesProtectedPersonSide || + componentHeight >= height * 0.16 || + componentWidth >= width * 0.16); const broadTopBackground = touchesTop && centerYRatio <= 0.45 && @@ -1642,7 +1734,8 @@ function filterScaleAdjustStructureComponents(params: { likelyStructure && centeredEnough && !edgeSliver && - !broadTopBackground + !broadTopBackground && + !likelyProtectedPersonComponent ) { if (fillSurfaceComponent) { for (let fillY = minY; fillY <= maxY; fillY += 1) { @@ -2446,6 +2539,7 @@ async function buildScaleAdjustStructureLockedComposite(params: { includePersonPixels?: boolean; placement: ReturnType; preferPixelStructure?: boolean; + protectedPersonSide?: ScaleAdjustProtectedPersonSide | null; raw: Buffer; sourceBuffer: Buffer; sourceImageUrl?: string; @@ -2456,6 +2550,7 @@ async function buildScaleAdjustStructureLockedComposite(params: { includePersonPixels = false, placement, preferPixelStructure = false, + protectedPersonSide = null, raw, sourceBuffer, sourceImageUrl, @@ -2487,9 +2582,20 @@ async function buildScaleAdjustStructureLockedComposite(params: { raw, width: sourceWidth, }); + const protectedPersonMask = + preferPixelStructure && protectedPersonSide + ? buildScaleAdjustProtectedPersonSeedMask({ + box: placement.sourceBox, + raw, + side: protectedPersonSide, + width: sourceWidth, + }) + : null; const mask = filterScaleAdjustStructureComponents({ box: placement.sourceBox, mask: initialMask, + protectedPersonMask, + protectedPersonSide, }); const structureBbox = getScaleAdjustMaskBoundingBox({ height: placement.sourceBox.height, @@ -2781,6 +2887,9 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { const preferPixelStructure = isScaleAdjustFurnitureProductLikePrompt( region.prompt ); + const protectedPersonSide = preferPixelStructure + ? getScaleAdjustProtectedPersonSide(region.prompt) + : null; const hasPersonPixels = !preferPixelStructure && !!sourceRaw && @@ -2797,6 +2906,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: { includePersonPixels, placement, preferPixelStructure, + protectedPersonSide, raw: sourceRaw, sourceBuffer, sourceImageUrl,