fix: clip scale targets by subject alpha
This commit is contained in:
parent
9ee2d2b50b
commit
50a59b82af
@ -140,6 +140,7 @@ type ScaleAdjustStructureLockedComposite = {
|
||||
compositeClip?: { input: Buffer; left: number; top: number };
|
||||
method?: "rmbg-cutout" | "pixel-mask";
|
||||
overlay: { input: Buffer; left: number; top: number };
|
||||
targetClip?: { input: Buffer; left: number; top: number };
|
||||
wash: { input: Buffer; left: number; top: number };
|
||||
};
|
||||
type AiModelWithProviderForSelection = Awaited<
|
||||
@ -1893,84 +1894,42 @@ async function buildScaleAdjustCompositeClipMaskPatch(params: {
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
function buildScaleAdjustCompositeClipTargetSvg(params: {
|
||||
height: number;
|
||||
placements: Array<ReturnType<typeof getScaleAdjustVisualGuidePlacement>>;
|
||||
width: number;
|
||||
}) {
|
||||
const { height, placements, width } = params;
|
||||
const minSide = Math.max(1, Math.min(width, height));
|
||||
const blur = Math.max(4, Math.min(24, Math.round(minSide * 0.014)));
|
||||
const pad = Math.max(3, Math.round(blur * 0.7));
|
||||
const rects = placements
|
||||
.map((placement) => {
|
||||
const left = clamp(
|
||||
Math.min(placement.placedLeft, placement.targetBox.left) - pad,
|
||||
0,
|
||||
Math.max(0, width - 1)
|
||||
);
|
||||
const top = clamp(
|
||||
Math.min(placement.placedTop, placement.targetBox.top) - pad,
|
||||
0,
|
||||
Math.max(0, height - 1)
|
||||
);
|
||||
const right = clamp(
|
||||
Math.max(
|
||||
placement.placedLeft + placement.placedWidth,
|
||||
placement.targetBox.left + placement.targetBox.width
|
||||
) + pad,
|
||||
left + 1,
|
||||
width
|
||||
);
|
||||
const bottom = clamp(
|
||||
Math.max(
|
||||
placement.placedTop + placement.placedHeight,
|
||||
placement.targetBox.top + placement.targetBox.height
|
||||
) + pad,
|
||||
top + 1,
|
||||
height
|
||||
);
|
||||
const rectWidth = Math.max(1, right - left);
|
||||
const rectHeight = Math.max(1, bottom - top);
|
||||
const radius = Math.max(4, Math.round(blur * 1.2));
|
||||
return `<rect x="${left}" y="${top}" width="${rectWidth}" height="${rectHeight}" rx="${radius}" ry="${radius}" fill="#ffffff" filter="url(#target-soft)" /><rect x="${left}" y="${top}" width="${rectWidth}" height="${rectHeight}" rx="${radius}" ry="${radius}" fill="#ffffff" fill-opacity="0.96" />`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return Buffer.from(
|
||||
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}">
|
||||
<defs>
|
||||
<filter id="target-soft" x="-45%" y="-45%" width="190%" height="190%">
|
||||
<feGaussianBlur stdDeviation="${blur}" />
|
||||
</filter>
|
||||
</defs>
|
||||
${rects}
|
||||
</svg>`
|
||||
);
|
||||
async function buildScaleAdjustOverlayClipMaskPatch(imageBuffer: Buffer) {
|
||||
const metadata = await sharp(imageBuffer, { failOn: "none" }).metadata();
|
||||
const width = Math.max(1, Math.round(Number(metadata.width || 0)));
|
||||
const height = Math.max(1, Math.round(Number(metadata.height || 0)));
|
||||
const alpha = await sharp(imageBuffer, { failOn: "none" })
|
||||
.ensureAlpha()
|
||||
.extractChannel(3)
|
||||
.blur(Math.max(0.8, Math.min(width, height) * 0.006))
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const rgba = Buffer.alloc(width * height * 4);
|
||||
for (let index = 0; index < width * height; index += 1) {
|
||||
const offset = index * 4;
|
||||
const value = alpha[index] || 0;
|
||||
rgba[offset] = 255;
|
||||
rgba[offset + 1] = 255;
|
||||
rgba[offset + 2] = 255;
|
||||
rgba[offset + 3] = value < 6 ? 0 : Math.min(255, Math.round(value * 1.35));
|
||||
}
|
||||
return sharp(rgba, {
|
||||
raw: { channels: 4, height, width },
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function buildScaleAdjustCompositeClipMaskBuffer(params: {
|
||||
height: number;
|
||||
placements: Array<ReturnType<typeof getScaleAdjustVisualGuidePlacement>>;
|
||||
sourceMaskComposites: Array<{ input: Buffer; left: number; top: number }>;
|
||||
targetMaskComposites: Array<{ input: Buffer; left: number; top: number }>;
|
||||
width: number;
|
||||
}) {
|
||||
const { height, placements, sourceMaskComposites, width } = params;
|
||||
if (placements.length === 0 && sourceMaskComposites.length === 0) return null;
|
||||
const targetMask =
|
||||
placements.length > 0
|
||||
? [
|
||||
{
|
||||
input: buildScaleAdjustCompositeClipTargetSvg({
|
||||
height,
|
||||
placements,
|
||||
width,
|
||||
}),
|
||||
left: 0,
|
||||
top: 0,
|
||||
},
|
||||
]
|
||||
: [];
|
||||
const { height, sourceMaskComposites, targetMaskComposites, width } = params;
|
||||
if (sourceMaskComposites.length === 0 && targetMaskComposites.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sharp({
|
||||
create: {
|
||||
@ -1980,7 +1939,7 @@ async function buildScaleAdjustCompositeClipMaskBuffer(params: {
|
||||
width,
|
||||
},
|
||||
})
|
||||
.composite([...sourceMaskComposites, ...targetMask])
|
||||
.composite([...sourceMaskComposites, ...targetMaskComposites])
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
}
|
||||
@ -2430,6 +2389,8 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const targetClipMaskPatch =
|
||||
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
|
||||
|
||||
console.info("[Canvas Local Edit] Scale adjust RMBG structure draft applied:", {
|
||||
alphaRatio: Number(alphaRatio.toFixed(3)),
|
||||
@ -2455,6 +2416,11 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
},
|
||||
targetClip: {
|
||||
input: targetClipMaskPatch,
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
},
|
||||
wash: {
|
||||
input: sourceWashPatch,
|
||||
left: placement.sourceBox.left,
|
||||
@ -2614,6 +2580,8 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const targetClipMaskPatch =
|
||||
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
|
||||
|
||||
return {
|
||||
compositeClip: {
|
||||
@ -2627,6 +2595,11 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
},
|
||||
targetClip: {
|
||||
input: targetClipMaskPatch,
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
},
|
||||
wash: {
|
||||
input: sourceWashPatch,
|
||||
left: placement.sourceBox.left,
|
||||
@ -2770,6 +2743,11 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
const targetClipComposites: Array<{
|
||||
input: Buffer;
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
|
||||
for (const region of regions) {
|
||||
const placement = getScaleAdjustVisualGuidePlacement({
|
||||
@ -2816,6 +2794,9 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
if (structureLockedComposite.compositeClip) {
|
||||
compositeClipComposites.push(structureLockedComposite.compositeClip);
|
||||
}
|
||||
if (structureLockedComposite.targetClip) {
|
||||
targetClipComposites.push(structureLockedComposite.targetClip);
|
||||
}
|
||||
composites.push(structureLockedComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
@ -2839,6 +2820,12 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
|
||||
if (shrinkCutoutComposite) {
|
||||
subjectWashComposites.push(shrinkCutoutComposite.wash);
|
||||
if (shrinkCutoutComposite.compositeClip) {
|
||||
compositeClipComposites.push(shrinkCutoutComposite.compositeClip);
|
||||
}
|
||||
if (shrinkCutoutComposite.targetClip) {
|
||||
targetClipComposites.push(shrinkCutoutComposite.targetClip);
|
||||
}
|
||||
composites.push(shrinkCutoutComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
@ -2919,11 +2906,12 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
])
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const compositeClipMaskBuffer = compositeClipComposites.length
|
||||
const compositeClipMaskBuffer =
|
||||
compositeClipComposites.length || targetClipComposites.length
|
||||
? await buildScaleAdjustCompositeClipMaskBuffer({
|
||||
height: sourceHeight,
|
||||
placements,
|
||||
sourceMaskComposites: compositeClipComposites,
|
||||
targetMaskComposites: targetClipComposites,
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
@ -4515,7 +4503,7 @@ export async function POST(request: NextRequest) {
|
||||
scaleAdjustPromptStrategy: isScaleAdjustOperation
|
||||
? scaleAdjustPatchReference
|
||||
? scaleAdjustPatchReference.structureLockedDraft
|
||||
? "ai-patch-workspace-structure-locked-draft-v86-mask-clipped-one-shot"
|
||||
? "ai-patch-workspace-structure-locked-draft-v87-subject-alpha-clipped-one-shot"
|
||||
: "ai-patch-workspace-draft-v75-soft-wipe-erase-ghost-one-shot"
|
||||
: useScaleAdjustEditMask
|
||||
? "mask-edit-gpt-image-2-scale-shrink-v74-one-shot"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user