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