fix: clamp scale patch alpha to edit regions

This commit is contained in:
134 2026-07-03 23:19:17 +08:00
parent b9e1671777
commit 53be242e89
2 changed files with 146 additions and 14 deletions

View File

@ -790,6 +790,29 @@ function buildScaleAdjustPatchCompositeRegions(
]);
}
function buildScaleAdjustProtectedChangeRegions(
regions: NormalizedLocalEditRegion[],
scalePercent?: ScaleAdjustPercent | null
) {
return regions.flatMap((region) => {
const targetRect = getScaleAdjustTargetRect(region, scalePercent);
const unionRect = getRectUnion(region.rect, targetRect);
const marginX = Math.min(
0.045,
Math.max(0.012, unionRect.width * 0.08)
);
const marginY = Math.min(
0.045,
Math.max(0.012, unionRect.height * 0.08)
);
return [
expandRectWithMargins(region.rect, marginX, marginY),
expandRectWithMargins(targetRect, marginX, marginY),
];
});
}
function getScaleAdjustPatchWorkspaceRect(
regions: NormalizedLocalEditRegion[],
scalePercent?: ScaleAdjustPercent | null
@ -3873,9 +3896,13 @@ export async function POST(request: NextRequest) {
const scaleAdjustPatchCompositeRegions = isScaleAdjustOperation
? buildScaleAdjustPatchCompositeRegions(regions, scalePercent)
: [];
const scaleAdjustProtectedChangeRegions =
isScaleAdjustOperation && scaleAdjustPatchReference
? buildScaleAdjustProtectedChangeRegions(regions, scalePercent)
: [];
const scaleAdjustProtectedRegions =
isScaleAdjustOperation && scaleAdjustPatchReference
? [...scaleAdjustCompositeRegions, scaleAdjustPatchReference.workspaceRect]
? scaleAdjustProtectedChangeRegions
: scaleAdjustCompositeRegions;
const scaleAdjustVerificationConfig = isScaleAdjustOperation
? {

View File

@ -2574,6 +2574,75 @@ async function buildLocalEditDiffCompositeMask(params: {
.toBuffer();
}
async function normalizeAlphaMaskToRaw(params: {
maskBuffer: Buffer;
width: number;
height: number;
}) {
const { height, maskBuffer, width } = params;
return sharp(maskBuffer, { failOn: "none" })
.resize(width, height, { fit: "fill" })
.removeAlpha()
.extractChannel(0)
.raw()
.toBuffer();
}
async function normalizeOpacityMaskToRaw(params: {
maskBuffer: Buffer;
width: number;
height: number;
}) {
const { height, maskBuffer, width } = params;
return sharp(maskBuffer, { failOn: "none" })
.resize(width, height, { fit: "fill" })
.ensureAlpha()
.extractChannel(3)
.raw()
.toBuffer();
}
function multiplyAlphaMasks(primaryMask: Buffer, clipMask: Buffer) {
if (primaryMask.length !== clipMask.length) return primaryMask;
const multiplied = Buffer.allocUnsafe(primaryMask.length);
for (let index = 0; index < primaryMask.length; index += 1) {
multiplied[index] = Math.round((primaryMask[index] * clipMask[index]) / 255);
}
return multiplied;
}
function boostAlphaMask(mask: Buffer, cutoff: number, multiplier: number) {
const boosted = Buffer.allocUnsafe(mask.length);
for (let index = 0; index < mask.length; index += 1) {
const value = mask[index];
boosted[index] =
value < cutoff ? 0 : Math.min(255, Math.round(value * multiplier));
}
return boosted;
}
async function applyRawAlphaToImage(params: {
imageBuffer: Buffer;
alphaMaskRaw: Buffer;
width: number;
height: number;
}) {
const { alphaMaskRaw, height, imageBuffer, width } = params;
const rgbRaw = await sharp(imageBuffer, { failOn: "none" })
.resize(width, height, { fit: "fill" })
.removeAlpha()
.toColorspace("srgb")
.raw()
.toBuffer();
return sharp(rgbRaw, { raw: { channels: 3, height, width } })
.joinChannel(alphaMaskRaw, {
raw: { channels: 1, height, width },
})
.png({ compressionLevel: 6 })
.toBuffer();
}
async function applyLocalEditCompositeToImages(params: {
images: ParsedImage[];
composite: CanvasLocalEditCompositeConfig | null;
@ -2613,19 +2682,22 @@ async function applyLocalEditCompositeToImages(params: {
.toBuffer();
const overlayBuffer =
composite.maskMode === "diff"
? await sharp(resizedGeneratedBuffer, { failOn: "none" })
.removeAlpha()
.joinChannel(
await buildLocalEditDiffCompositeMask({
? await applyRawAlphaToImage({
alphaMaskRaw: await normalizeAlphaMaskToRaw({
height: sourceHeight,
maskBuffer: await buildLocalEditDiffCompositeMask({
composite,
generatedBuffer: resizedGeneratedBuffer,
height: sourceHeight,
sourceBuffer,
width: sourceWidth,
})
)
.png({ compressionLevel: 6 })
.toBuffer()
}),
width: sourceWidth,
}),
height: sourceHeight,
imageBuffer: resizedGeneratedBuffer,
width: sourceWidth,
})
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
.ensureAlpha()
.composite([
@ -2778,6 +2850,25 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
touchesTop,
width: workspaceBox.width,
});
const regionClipPatchMask = maskRegionBoxes?.length
? buildScaleAdjustPatchCompositeMaskSvg({
featherPx,
height: workspaceBox.height,
regions: maskRegionBoxes,
touchesBottom,
touchesLeft,
touchesRight,
touchesTop,
width: workspaceBox.width,
})
: null;
const regionClipPatchMaskRaw = regionClipPatchMask
? await normalizeOpacityMaskToRaw({
height: workspaceBox.height,
maskBuffer: regionClipPatchMask,
width: workspaceBox.width,
})
: null;
const sourcePatchBuffer = await sharp(sourceBuffer, { failOn: "none" })
.extract(workspaceBox)
.png({ compressionLevel: 6 })
@ -2829,11 +2920,24 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
throw new Error("比例调整局部结果合成失败:无法生成合成蒙版");
}
const patchOverlayBuffer = diffPatchMask
? await sharp(resizedGeneratedBuffer, { failOn: "none" })
.removeAlpha()
.joinChannel(diffPatchMask)
.png({ compressionLevel: 6 })
.toBuffer()
? await applyRawAlphaToImage({
alphaMaskRaw: boostAlphaMask(
multiplyAlphaMasks(
await normalizeAlphaMaskToRaw({
height: workspaceBox.height,
maskBuffer: diffPatchMask,
width: workspaceBox.width,
}),
regionClipPatchMaskRaw ||
Buffer.alloc(workspaceBox.width * workspaceBox.height, 255)
),
8,
3
),
height: workspaceBox.height,
imageBuffer: resizedGeneratedBuffer,
width: workspaceBox.width,
})
: await sharp(resizedGeneratedBuffer, { failOn: "none" })
.ensureAlpha()
.composite([{ input: patchMask, blend: "dest-in" }])
@ -2877,6 +2981,7 @@ async function applyScaleAdjustPatchCompositeToImages(params: {
debugLabel,
featherPx,
imageCount: mergedImages.length,
diffRegionClipApplied: Boolean(regionClipPatchMask),
maskMode: composite.maskMode || "workspace",
maskRegionCount: maskRegionBoxes?.length || 0,
sourceHeight,