fix: use source plate for scale target guide
This commit is contained in:
parent
0f27bf1b10
commit
28080924f8
@ -139,7 +139,7 @@ type ScaleAdjustPatchReference = {
|
||||
};
|
||||
type ScaleAdjustStructureLockedComposite = {
|
||||
compositeClip?: { input: Buffer; left: number; top: number };
|
||||
method?: "rmbg-cutout" | "pixel-mask";
|
||||
method?: "pixel-mask" | "rect-crop-guide" | "rmbg-cutout";
|
||||
overlay: { input: Buffer; left: number; top: number };
|
||||
targetClip?: { input: Buffer; left: number; top: number };
|
||||
wash: { input: Buffer; left: number; top: number };
|
||||
@ -2644,18 +2644,27 @@ function suppressScaleAdjustProtectedFurnitureDarkAlpha(params: {
|
||||
const isProtectedSideArea = (sourceX: number, sourceY: number) => {
|
||||
if (protectedPersonSide === "right") {
|
||||
return (
|
||||
(sourceX >= sourceBox.width * 0.54 && sourceY >= sourceBox.height * 0.25) ||
|
||||
(sourceX >= sourceBox.width * 0.68 && sourceY >= sourceBox.height * 0.12)
|
||||
(sourceX >= sourceBox.width * 0.72 && sourceY >= sourceBox.height * 0.22) ||
|
||||
(sourceX >= sourceBox.width * 0.58 && sourceY >= sourceBox.height * 0.68)
|
||||
);
|
||||
}
|
||||
if (protectedPersonSide === "left") {
|
||||
return (
|
||||
(sourceX <= sourceBox.width * 0.46 && sourceY >= sourceBox.height * 0.25) ||
|
||||
(sourceX <= sourceBox.width * 0.32 && sourceY >= sourceBox.height * 0.12)
|
||||
(sourceX <= sourceBox.width * 0.28 && sourceY >= sourceBox.height * 0.22) ||
|
||||
(sourceX <= sourceBox.width * 0.42 && sourceY >= sourceBox.height * 0.68)
|
||||
);
|
||||
}
|
||||
return sourceY >= sourceBox.height * 0.3;
|
||||
};
|
||||
const isFarProtectedSideArea = (sourceX: number, sourceY: number) => {
|
||||
if (protectedPersonSide === "right") {
|
||||
return sourceX >= sourceBox.width * 0.84 && sourceY >= sourceBox.height * 0.3;
|
||||
}
|
||||
if (protectedPersonSide === "left") {
|
||||
return sourceX <= sourceBox.width * 0.16 && sourceY >= sourceBox.height * 0.3;
|
||||
}
|
||||
return sourceY >= sourceBox.height * 0.58;
|
||||
};
|
||||
|
||||
let clearedPixels = 0;
|
||||
let markedPixels = 0;
|
||||
@ -2674,16 +2683,22 @@ function suppressScaleAdjustProtectedFurnitureDarkAlpha(params: {
|
||||
const sourceX = bbox.left + localX;
|
||||
const sourceY = bbox.top + localY;
|
||||
const inProtectedSideArea = isProtectedSideArea(sourceX, sourceY);
|
||||
const farProtectedSideArea = isFarProtectedSideArea(sourceX, sourceY);
|
||||
const likelyHumanPixel =
|
||||
isScaleAdjustLikelySkinPixel(r, g, b) ||
|
||||
(isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) &&
|
||||
luma < 246 &&
|
||||
saturation < 0.24);
|
||||
const neutralDark =
|
||||
luma < 150 &&
|
||||
(saturation < 0.55 || (r < 110 && g < 110 && b < 110));
|
||||
const lowContrastDark = luma < 132 && saturation < 0.32;
|
||||
const shouldClear =
|
||||
inProtectedSideArea ||
|
||||
neutralDark ||
|
||||
lowContrastDark ||
|
||||
luma < 100 ||
|
||||
(luma < 132 && saturation < 0.22);
|
||||
(inProtectedSideArea && likelyHumanPixel) ||
|
||||
(farProtectedSideArea &&
|
||||
(likelyHumanPixel ||
|
||||
(neutralDark && luma < 72) ||
|
||||
(lowContrastDark && saturation < 0.18)));
|
||||
if (!shouldClear) continue;
|
||||
|
||||
output[alphaIndex] = 0;
|
||||
@ -2821,10 +2836,26 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
});
|
||||
const sourceWashMask = dilateScaleAdjustStructureMask({
|
||||
height: placement.sourceBox.height,
|
||||
mask: subjectMask,
|
||||
mask: protectedPersonSide
|
||||
? dilateScaleAdjustStructureMask({
|
||||
height: placement.sourceBox.height,
|
||||
mask,
|
||||
radius: Math.max(
|
||||
3,
|
||||
Math.round(
|
||||
Math.min(placement.sourceBox.width, placement.sourceBox.height) *
|
||||
0.034
|
||||
)
|
||||
),
|
||||
width: placement.sourceBox.width,
|
||||
})
|
||||
: subjectMask,
|
||||
radius: Math.max(
|
||||
2,
|
||||
Math.round(Math.min(placement.sourceBox.width, placement.sourceBox.height) * 0.012)
|
||||
Math.round(
|
||||
Math.min(placement.sourceBox.width, placement.sourceBox.height) *
|
||||
(protectedPersonSide ? 0.02 : 0.012)
|
||||
)
|
||||
),
|
||||
width: placement.sourceBox.width,
|
||||
});
|
||||
@ -2900,16 +2931,42 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
((sourceCenterY - placement.sourceFootprintBox.top) /
|
||||
Math.max(1, placement.sourceFootprintBox.height)) *
|
||||
placement.targetBox.height;
|
||||
const structureOverlay = await sharp(subjectCrop, { failOn: "none" })
|
||||
.joinChannel(subjectAlpha)
|
||||
.resize(placedWidth, placedHeight, {
|
||||
fit: "fill",
|
||||
kernel: sharp.kernel.lanczos3,
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const useRectCropTargetGuide = preferPixelStructure && Boolean(protectedPersonSide);
|
||||
const structureOverlay = useRectCropTargetGuide
|
||||
? await sharp(sourceBuffer, { failOn: "none" })
|
||||
.extract(placement.sourceBox)
|
||||
.resize(placement.placedWidth, placement.placedHeight, {
|
||||
fit: "fill",
|
||||
kernel: sharp.kernel.lanczos3,
|
||||
})
|
||||
.ensureAlpha()
|
||||
.composite([
|
||||
{
|
||||
input: buildScaleAdjustPatchMaskSvg({
|
||||
height: placement.placedHeight,
|
||||
width: placement.placedWidth,
|
||||
}),
|
||||
blend: "dest-in",
|
||||
},
|
||||
])
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
: await sharp(subjectCrop, { failOn: "none" })
|
||||
.joinChannel(subjectAlpha)
|
||||
.resize(placedWidth, placedHeight, {
|
||||
fit: "fill",
|
||||
kernel: sharp.kernel.lanczos3,
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const targetClipMaskPatch =
|
||||
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
|
||||
const overlayLeft = useRectCropTargetGuide
|
||||
? placement.placedLeft
|
||||
: Math.round(targetCenterX - placedWidth / 2);
|
||||
const overlayTop = useRectCropTargetGuide
|
||||
? placement.placedTop
|
||||
: Math.round(targetCenterY - placedHeight / 2);
|
||||
|
||||
return {
|
||||
compositeClip: {
|
||||
@ -2917,16 +2974,16 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
left: placement.sourceBox.left,
|
||||
top: placement.sourceBox.top,
|
||||
},
|
||||
method: "pixel-mask",
|
||||
method: useRectCropTargetGuide ? "rect-crop-guide" : "pixel-mask",
|
||||
overlay: {
|
||||
input: structureOverlay,
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
left: overlayLeft,
|
||||
top: overlayTop,
|
||||
},
|
||||
targetClip: {
|
||||
input: targetClipMaskPatch,
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
left: overlayLeft,
|
||||
top: overlayTop,
|
||||
},
|
||||
wash: {
|
||||
input: sourceWashPatch,
|
||||
@ -3131,43 +3188,6 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
if (structureLockAppliedRef) {
|
||||
structureLockAppliedRef.value = true;
|
||||
}
|
||||
if (protectedPersonSide && sourceRaw) {
|
||||
const washRight =
|
||||
protectedPersonSide === "right"
|
||||
? Math.round(placement.sourceFootprintBox.width * 0.74)
|
||||
: placement.sourceFootprintBox.width;
|
||||
const washLeft =
|
||||
protectedPersonSide === "left"
|
||||
? placement.sourceFootprintBox.left +
|
||||
Math.round(placement.sourceFootprintBox.width * 0.26)
|
||||
: placement.sourceFootprintBox.left;
|
||||
const washWidth =
|
||||
protectedPersonSide === "left"
|
||||
? Math.max(
|
||||
1,
|
||||
placement.sourceFootprintBox.width -
|
||||
Math.round(placement.sourceFootprintBox.width * 0.26)
|
||||
)
|
||||
: Math.max(1, washRight);
|
||||
sourceFootprintBoxes.push({
|
||||
left: washLeft,
|
||||
top: placement.sourceFootprintBox.top,
|
||||
width: washWidth,
|
||||
height: placement.sourceFootprintBox.height,
|
||||
background: estimateScaleAdjustBoxBackground({
|
||||
box: {
|
||||
left: washLeft,
|
||||
top: placement.sourceFootprintBox.top,
|
||||
width: washWidth,
|
||||
height: placement.sourceFootprintBox.height,
|
||||
},
|
||||
raw: sourceRaw,
|
||||
width: sourceWidth,
|
||||
}),
|
||||
innerOpacity: 0.5,
|
||||
opacity: 0.72,
|
||||
});
|
||||
}
|
||||
subjectWashComposites.push(structureLockedComposite.wash);
|
||||
if (structureLockedComposite.compositeClip) {
|
||||
compositeClipComposites.push(structureLockedComposite.compositeClip);
|
||||
@ -4351,22 +4371,23 @@ function buildScaleAdjustFullFramePatchPrompt(
|
||||
: widthFactor > 1.08 || heightFactor > 1.08
|
||||
? "make the same selected furniture/product visibly larger"
|
||||
: "move the same selected furniture/product to the final footprint";
|
||||
return `Region ${region.index}: original selected search area in the full photo is x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; final target footprint in input image 1 is x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}; requested scale width=${widthFactor.toFixed(2)}, height=${heightFactor.toFixed(2)}. User request: ${region.prompt || action}. Main action: ${action}.`;
|
||||
return `Region ${region.index}: original selected search area in input image 1 is x=${region.pixelRect.left}, y=${region.pixelRect.top}, w=${region.pixelRect.width}, h=${region.pixelRect.height}; final target footprint shown by input image 2 is x=${targetPixelRect.left}, y=${targetPixelRect.top}, w=${targetPixelRect.width}, h=${targetPixelRect.height}; requested scale width=${widthFactor.toFixed(2)}, height=${heightFactor.toFixed(2)}. User request: ${region.prompt || action}. Main action: ${action}.`;
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"Full-frame local image edit for furniture/product scale adjustment.",
|
||||
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 2. Do not crop, zoom, pan, or resize the whole photo.",
|
||||
"Input image 1 is a full-photo target draft. The selected furniture/product has already been moved and scaled with source-photo pixels to the required final footprint. Treat the size, position, silhouette, product structure, material, perspective, wheels, rods, panels, hooks, weave, and visible details in input image 1 as the hard target.",
|
||||
"Input image 2 is the original clean full photo. Use it only for clean background texture, lighting, perspective, non-target people, non-target objects, and untouched areas. Do not restore the selected furniture/product to its old size or old position from input image 2.",
|
||||
"AI work is limited to natural background completion, boundary blending, occlusion, contact shadows, and removing rough draft artifacts. Do not repaint, redesign, replace, stretch, duplicate, or distort the selected furniture/product interior.",
|
||||
"The target draft can contain translucent pasted pixels, faint old-footprint washes, clipped dark clothing fragments, suppressed dark details, or partial mask silhouettes. These are draft artifacts only. Use input image 2 to recover clean product details at the target size, but do not copy any dark blob, black hole, ghost fragment, transparent residue, pasted silhouette, or old-size placement as final photo content.",
|
||||
"Return exactly one clean realistic full image with the same framing, camera, aspect ratio, and canvas extent as input image 1. Do not crop, zoom, pan, or resize the whole photo.",
|
||||
"Input image 1 is the original clean full photo and the locked background plate for the final output. Preserve its framing, lighting, perspective, non-target people, non-target objects, and untouched areas.",
|
||||
"Input image 2 is a full-photo target draft guide. It only shows where the selected furniture/product should end up and how large its final visual footprint should be. Treat the target draft size, center, silhouette, product structure, material, perspective, wheels, rods, panels, hooks, weave, and visible details as the hard target instruction.",
|
||||
"The selected furniture/product in input image 1 is still at the old size and old position. Do not restore, keep, or prefer that old footprint. Use input image 1 for clean pixels and identity, then move/scale only the selected furniture/product to match input image 2.",
|
||||
"AI work is limited to moving/scaling the locked selected furniture/product, natural background completion, boundary blending, occlusion, and contact shadows. Do not repaint, redesign, replace, stretch, duplicate, or distort the selected furniture/product interior.",
|
||||
"The target draft can contain translucent pasted pixels, faint old-footprint washes, clipped fragments, suppressed dark details, partial mask silhouettes, or rectangular crop-guide edges. These are draft artifacts only. Use input image 1 to recover clean product details at the target size, but do not copy any dark blob, black hole, ghost fragment, transparent residue, pasted silhouette, crop rectangle, or old-size placement as final photo content.",
|
||||
"If the target footprint intersects a non-target person, preserve that person as the foreground occluder exactly as in the original photo: do not move, cover, deform, redraw, or change their face, body, clothes, hands, legs, feet, or pose. Blend the furniture/product behind the person naturally where needed.",
|
||||
"Erase the old larger furniture/product footprint outside the final target footprint and rebuild newly visible wall, curtain, floor, rug, shadow, and background naturally. Do not leave ghost copies, old-size remnants, rectangular haze, pale wash blocks, patch edges, black holes, dirty speckles, or visible seams.",
|
||||
"Keep all non-target content stable: person identity and anatomy, sofa, window, curtain, cabinet, lamp, wall art, floor, rug, lighting, textural grain, and global color must remain consistent.",
|
||||
"Do not output boxes, masks, labels, coordinate marks, UI marks, arrows, guide overlays, crop borders, or any annotation artifacts.",
|
||||
"中文硬约束:输入图1是完整画面的目标草稿,目标家具/商品已经在最终大小和最终位置;输入图2只是原始干净画面参考,绝对不能恢复旧尺寸。只做背景补全、遮挡融合、阴影和边缘清理。草稿里的半透明贴片、暗色残块、黑洞、旧尺寸残影都只是错误遮罩提示,最终必须删除。非目标人物必须保持原样,不能被目标家具盖住、变形、换脸、改姿势或改腿脚。最终必须是一张完整自然照片,不能有矩形雾块、贴片边、黑洞、残影、标注框。",
|
||||
"中文硬约束:输入图1是原始干净图,也是最终输出的锁定底板;输入图2只是完整画面的目标草稿,用来说明目标家具/商品的最终大小和最终位置。绝对不能把输入图1里的旧尺寸家具保留下来,也不能把输入图2的贴片痕迹当成最终内容。只移动/等比缩放目标家具并做背景补全、遮挡融合、阴影和边缘清理。草稿里的半透明贴片、暗色残块、黑洞、旧尺寸残影、矩形裁切边都只是错误遮罩提示,最终必须删除。非目标人物必须保持原样,不能被目标家具盖住、变形、换脸、改姿势或改腿脚。最终必须是一张完整自然照片,不能有矩形雾块、贴片边、黑洞、残影、标注框。",
|
||||
"",
|
||||
"Region instructions:",
|
||||
regionLines,
|
||||
@ -4797,8 +4818,8 @@ export async function POST(request: NextRequest) {
|
||||
? scaleAdjustPatchReference
|
||||
? useScaleAdjustFullFrameStructurePatch
|
||||
? [
|
||||
scaleAdjustPatchReference.targetDraftFullUrl as string,
|
||||
sourceImageUrl,
|
||||
scaleAdjustPatchReference.targetDraftFullUrl as string,
|
||||
...scaleAdjustCropReferenceImageUrls,
|
||||
...manualRegionReferenceImageUrls,
|
||||
]
|
||||
@ -4910,7 +4931,9 @@ export async function POST(request: NextRequest) {
|
||||
? resolvedScaleAdjustModelEntry?.provider?.type || null
|
||||
: null,
|
||||
cleanSourceReferenceIndex:
|
||||
scaleAdjustPatchReference &&
|
||||
scaleAdjustPatchReference && useScaleAdjustFullFrameStructurePatch
|
||||
? 1
|
||||
: scaleAdjustPatchReference &&
|
||||
useScaleAdjustPatchTargetDraftPrimary &&
|
||||
!includeScaleAdjustPatchSourceReference
|
||||
? null
|
||||
@ -4937,7 +4960,7 @@ export async function POST(request: NextRequest) {
|
||||
isScaleAdjustFalNanoBananaEdit && hasScaleAdjustShrink,
|
||||
scaleAdjustPatchReferenceOrder:
|
||||
scaleAdjustPatchReference && useScaleAdjustFullFrameStructurePatch
|
||||
? "full-target-draft-full-source"
|
||||
? "full-source-full-target-draft"
|
||||
: scaleAdjustPatchReference && useScaleAdjustPatchTargetDraftPrimary
|
||||
? includeScaleAdjustPatchSourceReference
|
||||
? "target-draft-source-identity"
|
||||
@ -5004,9 +5027,9 @@ export async function POST(request: NextRequest) {
|
||||
scaleAdjustTargetRegionCount: regions.filter((region) => region.targetRect)
|
||||
.length,
|
||||
scaleAdjustPromptStrategy: isScaleAdjustOperation
|
||||
? scaleAdjustPatchReference
|
||||
? useScaleAdjustFullFrameStructurePatch
|
||||
? "full-frame-structure-locked-draft-v7-protected-side-cleared-one-shot"
|
||||
? scaleAdjustPatchReference
|
||||
? useScaleAdjustFullFrameStructurePatch
|
||||
? "full-frame-source-locked-target-guide-v8-one-shot"
|
||||
: scaleAdjustPatchReference.structureLockedDraft
|
||||
? "ai-patch-workspace-structure-locked-draft-v87-subject-alpha-clipped-one-shot"
|
||||
: "ai-patch-workspace-draft-v75-soft-wipe-erase-ghost-one-shot"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user