fix: protect people in product locked scale adjust
This commit is contained in:
parent
f6faf6bb44
commit
f7a5accb61
@ -128,6 +128,7 @@ type ScaleAdjustPatchReference = {
|
||||
compositeClipMaskUrl?: string | null;
|
||||
productLockMethod?: string | null;
|
||||
productLockOverlayUrl?: string | null;
|
||||
productLockProtectMaskUrl?: string | null;
|
||||
sourceCropUrl: string;
|
||||
structureLockedDraft?: boolean;
|
||||
targetDraftFullUrl?: string | null;
|
||||
@ -145,6 +146,7 @@ type ScaleAdjustStructureLockedComposite = {
|
||||
method?: "pixel-mask" | "rect-crop-guide" | "rect-crop-guide-rmbg-lock" | "rmbg-cutout";
|
||||
overlay: { input: Buffer; left: number; top: number };
|
||||
productLock?: { input: Buffer; left: number; top: number };
|
||||
productLockProtectMask?: { input: Buffer; left: number; top: number };
|
||||
targetClip?: { input: Buffer; left: number; top: number };
|
||||
wash: { input: Buffer; left: number; top: number };
|
||||
};
|
||||
@ -1586,14 +1588,17 @@ function buildScaleAdjustStructureInitialMask(params: {
|
||||
}
|
||||
|
||||
function buildScaleAdjustProtectedPersonSeedMask(params: {
|
||||
background?: { b: number; g: number; luma: number; r: number } | null;
|
||||
box: { left: number; top: number; width: number; height: number };
|
||||
raw: Buffer;
|
||||
side: ScaleAdjustProtectedPersonSide;
|
||||
width: number;
|
||||
}) {
|
||||
const { box, raw, side, width } = params;
|
||||
const { background = null, box, raw, side, width } = params;
|
||||
const candidate = new Uint8Array(box.width * box.height);
|
||||
const anchor = new Uint8Array(box.width * box.height);
|
||||
const visited = new Uint8Array(box.width * box.height);
|
||||
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;
|
||||
@ -1611,17 +1616,106 @@ function buildScaleAdjustProtectedPersonSeedMask(params: {
|
||||
const b = raw[rawIndex + 2] || 0;
|
||||
const { luma, saturation } = getScaleAdjustPixelStats(r, g, b);
|
||||
const likelySkin = isScaleAdjustLikelySkinPixel(r, g, b);
|
||||
const colorDistance = background
|
||||
? Math.sqrt(
|
||||
(r - background.r) ** 2 +
|
||||
(g - background.g) ** 2 +
|
||||
(b - background.b) ** 2
|
||||
)
|
||||
: 999;
|
||||
const backgroundLikeLightPixel =
|
||||
Boolean(background) &&
|
||||
luma >= (background?.luma || 0) - 3 &&
|
||||
colorDistance < 18;
|
||||
const likelyWhiteFabric =
|
||||
isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) &&
|
||||
luma < 246 &&
|
||||
saturation < 0.22;
|
||||
saturation < 0.22 &&
|
||||
!backgroundLikeLightPixel &&
|
||||
(colorDistance > 16 || !background || luma < background.luma - 5);
|
||||
if (!likelySkin && !likelyWhiteFabric) continue;
|
||||
seed[localY * box.width + localX] = 255;
|
||||
seedCount += 1;
|
||||
const index = localY * box.width + localX;
|
||||
candidate[index] = 255;
|
||||
if (likelySkin) {
|
||||
anchor[index] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const queueX: number[] = [];
|
||||
const queueY: number[] = [];
|
||||
let seedCount = 0;
|
||||
let anchorCount = 0;
|
||||
for (let y = 0; y < box.height; y += 1) {
|
||||
for (let x = 0; x < box.width; x += 1) {
|
||||
const startIndex = y * box.width + x;
|
||||
if (candidate[startIndex] === 0 || visited[startIndex]) continue;
|
||||
|
||||
queueX.length = 0;
|
||||
queueY.length = 0;
|
||||
queueX.push(x);
|
||||
queueY.push(y);
|
||||
visited[startIndex] = 1;
|
||||
let head = 0;
|
||||
let area = 0;
|
||||
let anchors = 0;
|
||||
let minX = x;
|
||||
let minY = y;
|
||||
let maxX = x;
|
||||
let maxY = y;
|
||||
|
||||
while (head < queueX.length) {
|
||||
const cx = queueX[head] || 0;
|
||||
const cy = queueY[head] || 0;
|
||||
head += 1;
|
||||
area += 1;
|
||||
if (anchor[cy * box.width + cx]) anchors += 1;
|
||||
minX = Math.min(minX, cx);
|
||||
minY = Math.min(minY, cy);
|
||||
maxX = Math.max(maxX, cx);
|
||||
maxY = Math.max(maxY, cy);
|
||||
|
||||
for (let dy = -1; dy <= 1; dy += 1) {
|
||||
for (let dx = -1; dx <= 1; dx += 1) {
|
||||
if (dx === 0 && dy === 0) continue;
|
||||
const nx = cx + dx;
|
||||
const ny = cy + dy;
|
||||
if (nx < 0 || ny < 0 || nx >= box.width || ny >= box.height) continue;
|
||||
const nextIndex = ny * box.width + nx;
|
||||
if (candidate[nextIndex] === 0 || visited[nextIndex]) continue;
|
||||
visited[nextIndex] = 1;
|
||||
queueX.push(nx);
|
||||
queueY.push(ny);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const componentWidth = maxX - minX + 1;
|
||||
const componentHeight = maxY - minY + 1;
|
||||
const broadBackgroundStripe =
|
||||
anchors === 0 &&
|
||||
componentHeight >= box.height * 0.48 &&
|
||||
componentWidth >= box.width * 0.16;
|
||||
const tinyUnanchoredPatch = anchors === 0 && area < box.width * box.height * 0.004;
|
||||
if (anchors === 0 || broadBackgroundStripe || tinyUnanchoredPatch) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let index = 0; index < queueX.length; index += 1) {
|
||||
const pixelX = queueX[index] || 0;
|
||||
const pixelY = queueY[index] || 0;
|
||||
const seedIndex = pixelY * box.width + pixelX;
|
||||
if (seed[seedIndex]) continue;
|
||||
seed[seedIndex] = 255;
|
||||
seedCount += 1;
|
||||
}
|
||||
anchorCount += anchors;
|
||||
}
|
||||
}
|
||||
|
||||
const seedRatio = seedCount / Math.max(1, box.width * box.height);
|
||||
const anchorRatio = anchorCount / Math.max(1, box.width * box.height);
|
||||
if (anchorCount < 12 || anchorRatio < 0.00012) return null;
|
||||
if (seedCount < 24 || seedRatio < 0.0006) return null;
|
||||
|
||||
return dilateScaleAdjustStructureMask({
|
||||
@ -2148,6 +2242,19 @@ function thresholdScaleAdjustAlphaMask(params: {
|
||||
return output;
|
||||
}
|
||||
|
||||
function removeScaleAdjustMaskFromAlpha(params: {
|
||||
alphaMask: Buffer | Uint8Array;
|
||||
removeMask: Uint8Array | null;
|
||||
}) {
|
||||
const { alphaMask, removeMask } = params;
|
||||
if (!removeMask || removeMask.length !== alphaMask.length) return alphaMask;
|
||||
const output = Buffer.from(alphaMask);
|
||||
for (let index = 0; index < output.length; index += 1) {
|
||||
if (removeMask[index]) output[index] = 0;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async function applyScaleAdjustAlphaMaskToRgba(params: {
|
||||
alphaMask: Uint8Array;
|
||||
height: number;
|
||||
@ -2343,7 +2450,9 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
userId,
|
||||
imageBase64: sourceCropBuffer.toString("base64"),
|
||||
imageBuffer: sourceCropBuffer,
|
||||
prompt: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
|
||||
prompt: protectedPersonSide
|
||||
? "比例调整家具/商品抠图:只保留框选区域内需要缩放的目标家具/商品主体和它本身的配件结构,明确排除非目标人物、手、脚、腿、衣服、书本、身体遮挡物和背景,输出透明背景 PNG"
|
||||
: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
|
||||
skipGallerySave: true,
|
||||
sourceImageUrl,
|
||||
});
|
||||
@ -2378,7 +2487,25 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
rawRgba: cutoutRaw,
|
||||
width: cutoutWidth,
|
||||
});
|
||||
const protectedCleanedAlpha =
|
||||
const background = estimateScaleAdjustBoxBackground({
|
||||
box: placement.sourceBox,
|
||||
raw,
|
||||
width: sourceWidth,
|
||||
});
|
||||
const protectedPersonMask =
|
||||
protectedPersonSide && sameSizeCutout && sourceCropRaw
|
||||
? validateScaleAdjustProtectedPersonMask({
|
||||
box: { width: cutoutWidth, height: cutoutHeight },
|
||||
mask: buildScaleAdjustProtectedPersonSeedMask({
|
||||
background,
|
||||
box: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
|
||||
raw: sourceCropRaw,
|
||||
side: protectedPersonSide,
|
||||
width: cutoutWidth,
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
const sideProtectedCleanedAlpha =
|
||||
protectedPersonSide && sameSizeCutout && sourceCropRaw
|
||||
? suppressScaleAdjustProtectedFurnitureDarkAlpha({
|
||||
alphaMask: Buffer.from(cleanedAlpha),
|
||||
@ -2389,10 +2516,12 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
sourceBox: placement.sourceBox,
|
||||
})
|
||||
: cleanedAlpha;
|
||||
if (protectedCleanedAlpha !== cleanedAlpha) {
|
||||
for (let index = 0; index < protectedCleanedAlpha.length; index += 1) {
|
||||
cleanedRaw[index * 4 + 3] = protectedCleanedAlpha[index] || 0;
|
||||
}
|
||||
const protectedCleanedAlpha = removeScaleAdjustMaskFromAlpha({
|
||||
alphaMask: sideProtectedCleanedAlpha,
|
||||
removeMask: protectedPersonMask,
|
||||
});
|
||||
for (let index = 0; index < protectedCleanedAlpha.length; index += 1) {
|
||||
cleanedRaw[index * 4 + 3] = protectedCleanedAlpha[index] || 0;
|
||||
}
|
||||
const highConfidenceCutoutMask = thresholdScaleAdjustAlphaMask({
|
||||
maskBuffer: protectedCleanedAlpha,
|
||||
@ -2427,27 +2556,10 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
);
|
||||
return null;
|
||||
}
|
||||
const background = estimateScaleAdjustBoxBackground({
|
||||
box: placement.sourceBox,
|
||||
raw,
|
||||
width: sourceWidth,
|
||||
});
|
||||
const highConfidenceSourceMask = thresholdScaleAdjustAlphaMask({
|
||||
maskBuffer: sourceMaskBuffer,
|
||||
threshold: 96,
|
||||
});
|
||||
const protectedPersonMask =
|
||||
protectedPersonSide && sameSizeCutout && sourceCropRaw
|
||||
? validateScaleAdjustProtectedPersonMask({
|
||||
box: { width: cutoutWidth, height: cutoutHeight },
|
||||
mask: buildScaleAdjustProtectedPersonSeedMask({
|
||||
box: { left: 0, top: 0, width: cutoutWidth, height: cutoutHeight },
|
||||
raw: sourceCropRaw,
|
||||
side: protectedPersonSide,
|
||||
width: cutoutWidth,
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
const filteredSourceMask = filterScaleAdjustStructureComponents({
|
||||
box: placement.sourceBox,
|
||||
mask: highConfidenceSourceMask,
|
||||
@ -2517,7 +2629,7 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
});
|
||||
const sourceCompositeClipMask = dilateScaleAdjustStructureMask({
|
||||
height: placement.sourceBox.height,
|
||||
mask: filteredSourceBbox ? filteredSourceMask : highConfidenceSourceMask,
|
||||
mask: sourceWashBaseMask,
|
||||
radius: Math.max(
|
||||
1,
|
||||
Math.round(
|
||||
@ -2602,6 +2714,23 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
.toBuffer();
|
||||
const targetClipMaskPatch =
|
||||
await buildScaleAdjustOverlayClipMaskPatch(structureOverlay);
|
||||
const productLockProtectMaskPatch = protectedPersonMask
|
||||
? await buildScaleAdjustCompositeClipMaskPatch({
|
||||
box: placement.sourceBox,
|
||||
maskBuffer: dilateScaleAdjustStructureMask({
|
||||
height: placement.sourceBox.height,
|
||||
mask: protectedPersonMask,
|
||||
radius: Math.max(
|
||||
2,
|
||||
Math.round(
|
||||
Math.min(placement.sourceBox.width, placement.sourceBox.height) *
|
||||
0.012
|
||||
)
|
||||
),
|
||||
width: placement.sourceBox.width,
|
||||
}),
|
||||
})
|
||||
: null;
|
||||
|
||||
console.info("[Canvas Local Edit] Scale adjust RMBG structure draft applied:", {
|
||||
alphaRatio: Number(alphaRatio.toFixed(3)),
|
||||
@ -2637,6 +2766,15 @@ async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
top: Math.round(targetCenterY - placedHeight / 2),
|
||||
},
|
||||
...(productLockProtectMaskPatch
|
||||
? {
|
||||
productLockProtectMask: {
|
||||
input: productLockProtectMaskPatch,
|
||||
left: placement.sourceBox.left,
|
||||
top: placement.sourceBox.top,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
targetClip: {
|
||||
input: targetClipMaskPatch,
|
||||
left: Math.round(targetCenterX - placedWidth / 2),
|
||||
@ -3220,11 +3358,16 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
const productLockComposites: Array<{
|
||||
input: Buffer;
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
const productLockComposites: Array<{
|
||||
input: Buffer;
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
const productLockProtectMaskComposites: Array<{
|
||||
input: Buffer;
|
||||
left: number;
|
||||
top: number;
|
||||
}> = [];
|
||||
|
||||
for (const region of regions) {
|
||||
const placement = getScaleAdjustVisualGuidePlacement({
|
||||
@ -3284,15 +3427,20 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
if (structureLockedComposite.compositeClip) {
|
||||
compositeClipComposites.push(structureLockedComposite.compositeClip);
|
||||
}
|
||||
if (structureLockedComposite.targetClip) {
|
||||
targetClipComposites.push(structureLockedComposite.targetClip);
|
||||
if (structureLockedComposite.targetClip && !structureLockedComposite.productLock) {
|
||||
targetClipComposites.push(structureLockedComposite.targetClip);
|
||||
}
|
||||
if (structureLockedComposite.productLock) {
|
||||
productLockComposites.push(structureLockedComposite.productLock);
|
||||
}
|
||||
if (structureLockedComposite.productLockProtectMask) {
|
||||
productLockProtectMaskComposites.push(
|
||||
structureLockedComposite.productLockProtectMask
|
||||
);
|
||||
}
|
||||
composites.push(structureLockedComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
if (structureLockedComposite.productLock) {
|
||||
productLockComposites.push(structureLockedComposite.productLock);
|
||||
}
|
||||
composites.push(structureLockedComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
|
||||
const shouldUseShrinkCutoutDraft =
|
||||
process.env.CANVAS_SCALE_ADJUST_ENABLE_RMBG_SHRINK_CUTOUT === "1" &&
|
||||
@ -3317,15 +3465,20 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
if (shrinkCutoutComposite.compositeClip) {
|
||||
compositeClipComposites.push(shrinkCutoutComposite.compositeClip);
|
||||
}
|
||||
if (shrinkCutoutComposite.targetClip) {
|
||||
targetClipComposites.push(shrinkCutoutComposite.targetClip);
|
||||
if (shrinkCutoutComposite.targetClip && !shrinkCutoutComposite.productLock) {
|
||||
targetClipComposites.push(shrinkCutoutComposite.targetClip);
|
||||
}
|
||||
if (shrinkCutoutComposite.productLock) {
|
||||
productLockComposites.push(shrinkCutoutComposite.productLock);
|
||||
}
|
||||
if (shrinkCutoutComposite.productLockProtectMask) {
|
||||
productLockProtectMaskComposites.push(
|
||||
shrinkCutoutComposite.productLockProtectMask
|
||||
);
|
||||
}
|
||||
composites.push(shrinkCutoutComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
if (shrinkCutoutComposite.productLock) {
|
||||
productLockComposites.push(shrinkCutoutComposite.productLock);
|
||||
}
|
||||
composites.push(shrinkCutoutComposite.overlay);
|
||||
continue;
|
||||
}
|
||||
|
||||
sourceFootprintBoxes.push({
|
||||
...placement.sourceFootprintBox,
|
||||
@ -3412,31 +3565,50 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
: null;
|
||||
const productLockOverlayBuffer = productLockComposites.length
|
||||
? await sharp({
|
||||
create: {
|
||||
background: { alpha: 0, b: 0, g: 0, r: 0 },
|
||||
const productLockOverlayBuffer = productLockComposites.length
|
||||
? await sharp({
|
||||
create: {
|
||||
background: { alpha: 0, b: 0, g: 0, r: 0 },
|
||||
channels: 4,
|
||||
height: sourceHeight,
|
||||
width: sourceWidth,
|
||||
},
|
||||
})
|
||||
.composite(productLockComposites)
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
: null;
|
||||
const compositeClipMaskBuffer =
|
||||
compositeClipComposites.length || targetClipComposites.length
|
||||
? await buildScaleAdjustCompositeClipMaskBuffer({
|
||||
height: sourceHeight,
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
: null;
|
||||
const productLockProtectMaskBuffer = productLockProtectMaskComposites.length
|
||||
? await sharp({
|
||||
create: {
|
||||
background: { alpha: 0, b: 255, g: 255, r: 255 },
|
||||
channels: 4,
|
||||
height: sourceHeight,
|
||||
width: sourceWidth,
|
||||
},
|
||||
})
|
||||
.composite(productLockProtectMaskComposites)
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer()
|
||||
: null;
|
||||
const compositeClipMaskBuffer =
|
||||
compositeClipComposites.length || targetClipComposites.length
|
||||
? await buildScaleAdjustCompositeClipMaskBuffer({
|
||||
height: sourceHeight,
|
||||
sourceMaskComposites: compositeClipComposites,
|
||||
targetMaskComposites: targetClipComposites,
|
||||
width: sourceWidth,
|
||||
})
|
||||
: null;
|
||||
|
||||
return { backgroundPlateBuffer, buffer, compositeClipMaskBuffer, productLockOverlayBuffer };
|
||||
}
|
||||
return {
|
||||
backgroundPlateBuffer,
|
||||
buffer,
|
||||
compositeClipMaskBuffer,
|
||||
productLockOverlayBuffer,
|
||||
productLockProtectMaskBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
async function createScaleAdjustPatchReference(params: {
|
||||
fullFrameReferenceOnly?: boolean;
|
||||
@ -3478,12 +3650,13 @@ async function createScaleAdjustPatchReference(params: {
|
||||
const targetDraftBuffer = targetDraftReference.buffer;
|
||||
const suffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
if (params.fullFrameReferenceOnly && structureLockAppliedRef.value) {
|
||||
const [
|
||||
targetDraftFullUpload,
|
||||
backgroundPlateFullUpload,
|
||||
productLockOverlayUpload,
|
||||
compositeClipMaskUpload,
|
||||
] = await Promise.all([
|
||||
const [
|
||||
targetDraftFullUpload,
|
||||
backgroundPlateFullUpload,
|
||||
productLockOverlayUpload,
|
||||
productLockProtectMaskUpload,
|
||||
compositeClipMaskUpload,
|
||||
] = await Promise.all([
|
||||
uploadFile(
|
||||
`canvas-scale-adjust-full-target-draft-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftBuffer)
|
||||
@ -3494,15 +3667,21 @@ async function createScaleAdjustPatchReference(params: {
|
||||
toArrayBuffer(targetDraftReference.backgroundPlateBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockOverlayBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.compositeClipMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
|
||||
targetDraftReference.productLockOverlayBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockProtectMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.compositeClipMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
@ -3510,10 +3689,11 @@ async function createScaleAdjustPatchReference(params: {
|
||||
|
||||
return {
|
||||
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockOverlayUrl: productLockOverlayUpload?.url || null,
|
||||
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockOverlayUrl: productLockOverlayUpload?.url || null,
|
||||
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
|
||||
sourceCropUrl: sourceImageUrl || targetDraftFullUpload.url,
|
||||
structureLockedDraft: structureLockAppliedRef.value,
|
||||
targetDraftFullUrl: targetDraftFullUpload.url,
|
||||
targetDraftUrl: targetDraftFullUpload.url,
|
||||
@ -3532,14 +3712,15 @@ async function createScaleAdjustPatchReference(params: {
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer(),
|
||||
]);
|
||||
const [
|
||||
sourceCropUpload,
|
||||
targetDraftUpload,
|
||||
targetDraftFullUpload,
|
||||
backgroundPlateFullUpload,
|
||||
productLockOverlayUpload,
|
||||
compositeClipMaskUpload,
|
||||
] = await Promise.all([
|
||||
const [
|
||||
sourceCropUpload,
|
||||
targetDraftUpload,
|
||||
targetDraftFullUpload,
|
||||
backgroundPlateFullUpload,
|
||||
productLockOverlayUpload,
|
||||
productLockProtectMaskUpload,
|
||||
compositeClipMaskUpload,
|
||||
] = await Promise.all([
|
||||
uploadFile(
|
||||
`canvas-scale-adjust-workspace-source-${suffix}.png`,
|
||||
toArrayBuffer(sourceCropBuffer)
|
||||
@ -3558,15 +3739,21 @@ async function createScaleAdjustPatchReference(params: {
|
||||
toArrayBuffer(targetDraftReference.backgroundPlateBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockOverlayBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.compositeClipMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
|
||||
targetDraftReference.productLockOverlayBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-overlay-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockOverlayBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.productLockProtectMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-product-lock-protect-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.productLockProtectMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
targetDraftReference.compositeClipMaskBuffer
|
||||
? uploadFile(
|
||||
`canvas-scale-adjust-composite-clip-mask-${suffix}.png`,
|
||||
toArrayBuffer(targetDraftReference.compositeClipMaskBuffer)
|
||||
)
|
||||
: Promise.resolve(null),
|
||||
@ -3574,10 +3761,11 @@ async function createScaleAdjustPatchReference(params: {
|
||||
|
||||
return {
|
||||
backgroundPlateFullUrl: backgroundPlateFullUpload?.url || null,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockOverlayUrl: productLockOverlayUpload?.url || null,
|
||||
sourceCropUrl: sourceCropUpload.url,
|
||||
compositeClipMaskUrl: compositeClipMaskUpload?.url || null,
|
||||
productLockMethod: productLockOverlayUpload ? "rmbg-cutout" : null,
|
||||
productLockOverlayUrl: productLockOverlayUpload?.url || null,
|
||||
productLockProtectMaskUrl: productLockProtectMaskUpload?.url || null,
|
||||
sourceCropUrl: sourceCropUpload.url,
|
||||
structureLockedDraft: structureLockAppliedRef.value,
|
||||
targetDraftFullUrl: targetDraftFullUpload.url,
|
||||
targetDraftUrl: targetDraftUpload.url,
|
||||
@ -5201,10 +5389,12 @@ export async function POST(request: NextRequest) {
|
||||
useScaleAdjustProductLockComposite,
|
||||
scaleAdjustProductLockMethod:
|
||||
scaleAdjustPatchReference?.productLockMethod || null,
|
||||
scaleAdjustProductLockOverlayUrl:
|
||||
scaleAdjustPatchReference?.productLockOverlayUrl || null,
|
||||
scaleAdjustPatchCompositeClipMaskUrl:
|
||||
scaleAdjustPatchReference?.compositeClipMaskUrl || null,
|
||||
scaleAdjustProductLockOverlayUrl:
|
||||
scaleAdjustPatchReference?.productLockOverlayUrl || null,
|
||||
scaleAdjustProductLockProtectMaskUrl:
|
||||
scaleAdjustPatchReference?.productLockProtectMaskUrl || null,
|
||||
scaleAdjustPatchCompositeClipMaskUrl:
|
||||
scaleAdjustPatchReference?.compositeClipMaskUrl || null,
|
||||
scaleAdjustPatchWorkspacePixelRect:
|
||||
scaleAdjustPatchReference?.workspacePixelRect || null,
|
||||
scaleAdjustPatchWorkspaceRect:
|
||||
@ -5365,13 +5555,15 @@ export async function POST(request: NextRequest) {
|
||||
workspaceRegion: scaleAdjustPatchReference.workspaceRect,
|
||||
}
|
||||
: undefined,
|
||||
scaleAdjustProductLockComposite: useScaleAdjustProductLockComposite
|
||||
? {
|
||||
overlayImageUrl:
|
||||
scaleAdjustPatchReference?.productLockOverlayUrl as string,
|
||||
sourceHeight,
|
||||
sourceWidth,
|
||||
}
|
||||
scaleAdjustProductLockComposite: useScaleAdjustProductLockComposite
|
||||
? {
|
||||
overlayImageUrl:
|
||||
scaleAdjustPatchReference?.productLockOverlayUrl as string,
|
||||
protectMaskImageUrl:
|
||||
scaleAdjustPatchReference?.productLockProtectMaskUrl || undefined,
|
||||
sourceHeight,
|
||||
sourceWidth,
|
||||
}
|
||||
: undefined,
|
||||
scaleAdjustVerification: scaleAdjustVerificationConfig,
|
||||
localEditComposite: isScaleAdjustOperation
|
||||
|
||||
@ -73,6 +73,7 @@ export interface CanvasScaleAdjustPatchCompositeConfig {
|
||||
|
||||
export interface CanvasScaleAdjustProductLockCompositeConfig {
|
||||
overlayImageUrl: string;
|
||||
protectMaskImageUrl?: string;
|
||||
sourceHeight: number;
|
||||
sourceWidth: number;
|
||||
}
|
||||
@ -555,12 +556,16 @@ function normalizeScaleAdjustProductLockCompositeConfig(
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const overlayImageUrl = String(value.overlayImageUrl || "").trim();
|
||||
if (!/^https?:\/\//i.test(overlayImageUrl)) return null;
|
||||
const protectMaskImageUrl = String(value.protectMaskImageUrl || "").trim();
|
||||
const sourceWidth = Math.round(Number(value.sourceWidth || 0));
|
||||
const sourceHeight = Math.round(Number(value.sourceHeight || 0));
|
||||
if (sourceWidth <= 0 || sourceHeight <= 0) return null;
|
||||
|
||||
return {
|
||||
overlayImageUrl,
|
||||
...(protectMaskImageUrl && /^https?:\/\//i.test(protectMaskImageUrl)
|
||||
? { protectMaskImageUrl }
|
||||
: {}),
|
||||
sourceHeight,
|
||||
sourceWidth,
|
||||
};
|
||||
@ -2807,6 +2812,44 @@ async function applyRawAlphaToImage(params: {
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function applyScaleAdjustProductProtectMaskToOverlay(params: {
|
||||
height: number;
|
||||
overlayBuffer: Buffer;
|
||||
protectMaskBuffer: Buffer;
|
||||
width: number;
|
||||
}) {
|
||||
const { height, overlayBuffer, protectMaskBuffer, width } = params;
|
||||
const overlayRaw = await sharp(overlayBuffer, { failOn: "none" })
|
||||
.resize(width, height, { fit: "fill" })
|
||||
.ensureAlpha()
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const protectRaw = await sharp(protectMaskBuffer, { failOn: "none" })
|
||||
.rotate()
|
||||
.resize(width, height, { fit: "fill" })
|
||||
.ensureAlpha()
|
||||
.extractChannel(3)
|
||||
.raw()
|
||||
.toBuffer();
|
||||
const pixelCount = Math.min(
|
||||
protectRaw.length,
|
||||
Math.floor(overlayRaw.length / 4)
|
||||
);
|
||||
for (let index = 0; index < pixelCount; index += 1) {
|
||||
const protect = protectRaw[index] || 0;
|
||||
if (protect <= 0) continue;
|
||||
const alphaIndex = index * 4 + 3;
|
||||
const alpha = overlayRaw[alphaIndex] || 0;
|
||||
overlayRaw[alphaIndex] = Math.round((alpha * Math.max(0, 255 - protect)) / 255);
|
||||
}
|
||||
|
||||
return sharp(overlayRaw, {
|
||||
raw: { channels: 4, height, width },
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
}
|
||||
|
||||
async function applyLocalEditCompositeToImages(params: {
|
||||
images: ParsedImage[];
|
||||
composite: CanvasLocalEditCompositeConfig | null;
|
||||
@ -3204,6 +3247,9 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
|
||||
.ensureAlpha()
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const protectMaskBuffer = composite.protectMaskImageUrl
|
||||
? await fetchImageBufferFromUrl(composite.protectMaskImageUrl)
|
||||
: null;
|
||||
const overlayMetadata = await sharp(overlayBuffer, { failOn: "none" }).metadata();
|
||||
const overlayWidth = Math.round(Number(overlayMetadata.width || 0));
|
||||
const overlayHeight = Math.round(Number(overlayMetadata.height || 0));
|
||||
@ -3230,13 +3276,21 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
|
||||
continue;
|
||||
}
|
||||
|
||||
const resizedOverlayBuffer = await sharp(overlayBuffer, { failOn: "none" })
|
||||
const resizedOverlayBaseBuffer = await sharp(overlayBuffer, { failOn: "none" })
|
||||
.resize(generatedWidth, generatedHeight, {
|
||||
fit: "fill",
|
||||
kernel: sharp.kernel.lanczos3,
|
||||
})
|
||||
.png({ compressionLevel: 6 })
|
||||
.toBuffer();
|
||||
const resizedOverlayBuffer = protectMaskBuffer
|
||||
? await applyScaleAdjustProductProtectMaskToOverlay({
|
||||
height: generatedHeight,
|
||||
overlayBuffer: resizedOverlayBaseBuffer,
|
||||
protectMaskBuffer,
|
||||
width: generatedWidth,
|
||||
})
|
||||
: resizedOverlayBaseBuffer;
|
||||
const mergedBuffer = await sharp(normalizedGeneratedBuffer, {
|
||||
failOn: "none",
|
||||
})
|
||||
@ -3271,6 +3325,7 @@ async function applyScaleAdjustProductLockCompositeToImages(params: {
|
||||
imageCount: mergedImages.length,
|
||||
overlayHeight,
|
||||
overlayWidth,
|
||||
protectMaskApplied: Boolean(protectMaskBuffer),
|
||||
sourceHeight: composite.sourceHeight,
|
||||
sourceWidth: composite.sourceWidth,
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user