fix: verify scale adjust footprint shrink

This commit is contained in:
134 2026-07-02 18:40:09 +08:00
parent 0515fd3e46
commit 1ae9b5a89b

View File

@ -170,6 +170,8 @@ const SCALE_ADJUST_DIFF_SAMPLE_SIZE = 112;
const SCALE_ADJUST_FALLBACK_MASK_DISTANCE_THRESHOLD = 42;
const SCALE_ADJUST_FALLBACK_MASK_LUMA_GAP = 18;
const SCALE_ADJUST_FALLBACK_INPAINT_ITERATIONS = 180;
const SCALE_ADJUST_SHRINK_OUTSIDE_FOOTPRINT_MAX_RATIO = 0.72;
const SCALE_ADJUST_SHRINK_MIN_SOURCE_OUTSIDE_PIXELS = 60;
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
"gemini",
@ -815,6 +817,109 @@ function isScaleAdjustOutsidePixelChanged(params: {
);
}
async function getScaleAdjustFootprintChangeStats(params: {
generatedBuffer: Buffer;
sourceBuffer: Buffer;
sourceWidth: number;
sourceHeight: number;
verification: CanvasScaleAdjustVerificationConfig;
}) {
const { generatedBuffer, sourceBuffer, sourceHeight, sourceWidth, verification } =
params;
const normalizeToRaw = (buffer: Buffer) =>
sharp(buffer, { failOn: "none" })
.rotate()
.resize(sourceWidth, sourceHeight, {
fit: "cover",
position: "center",
})
.removeAlpha()
.toColorspace("srgb")
.raw()
.toBuffer();
const [sourceRaw, generatedRaw] = await Promise.all([
normalizeToRaw(sourceBuffer),
normalizeToRaw(generatedBuffer),
]);
const shrinkStats: Array<{
generatedOutsidePixels: number;
outsideRatio: number;
passed: boolean;
regionIndex: number;
sourceOutsidePixels: number;
}> = [];
for (const [index, region] of verification.regions.entries()) {
const targetRegion =
verification.targetRegions?.[index] ||
(typeof verification.scalePercent === "number"
? scaleRegionAroundCenter(region, verification.scalePercent)
: null);
if (!targetRegion) continue;
const widthFactor = region.width > 0 ? targetRegion.width / region.width : 1;
const heightFactor = region.height > 0 ? targetRegion.height / region.height : 1;
const isShrink = widthFactor < 0.92 || heightFactor < 0.92;
if (!isShrink) continue;
const sourceBox = rectToPixelBox({
height: sourceHeight,
region,
width: sourceWidth,
});
const targetBox = rectToPixelBox({
height: sourceHeight,
region: targetRegion,
width: sourceWidth,
});
const sourceMask = buildScaleAdjustFallbackSubjectMask({
box: sourceBox,
raw: sourceRaw,
width: sourceWidth,
});
const generatedMask = buildScaleAdjustFallbackSubjectMask({
box: sourceBox,
raw: generatedRaw,
width: sourceWidth,
});
let sourceOutsidePixels = 0;
let generatedOutsidePixels = 0;
for (let localY = 0; localY < sourceBox.height; localY += 1) {
const y = sourceBox.top + localY;
for (let localX = 0; localX < sourceBox.width; localX += 1) {
const x = sourceBox.left + localX;
const insideTarget =
x >= targetBox.left &&
x < targetBox.left + targetBox.width &&
y >= targetBox.top &&
y < targetBox.top + targetBox.height;
if (insideTarget) continue;
const maskIndex = localY * sourceBox.width + localX;
if (sourceMask[maskIndex] > 0) sourceOutsidePixels += 1;
if (generatedMask[maskIndex] > 0) generatedOutsidePixels += 1;
}
}
const outsideRatio =
sourceOutsidePixels > 0 ? generatedOutsidePixels / sourceOutsidePixels : 0;
const passed =
sourceOutsidePixels < SCALE_ADJUST_SHRINK_MIN_SOURCE_OUTSIDE_PIXELS ||
outsideRatio <= SCALE_ADJUST_SHRINK_OUTSIDE_FOOTPRINT_MAX_RATIO;
shrinkStats.push({
generatedOutsidePixels,
outsideRatio,
passed,
regionIndex: index,
sourceOutsidePixels,
});
}
return {
failedRegionCount: shrinkStats.filter((item) => !item.passed).length,
passed: shrinkStats.every((item) => item.passed),
shrinkRegionCount: shrinkStats.length,
shrinkStats,
};
}
async function loadScaleAdjustSourceImage(
verification: CanvasScaleAdjustVerificationConfig
) {
@ -1038,6 +1143,7 @@ async function applyScaleAdjustVerificationToImages(params: {
changedInside: boolean;
cleanedImage?: ParsedImage;
diffScores: number[];
footprintStats: Awaited<ReturnType<typeof getScaleAdjustFootprintChangeStats>>;
image: ParsedImage;
index: number;
outsideChanged: boolean;
@ -1050,7 +1156,13 @@ async function applyScaleAdjustVerificationToImages(params: {
const generatedUrl = image.originalUrl || image.thumbnailUrl;
const generatedBuffer = await fetchImageBufferFromUrl(generatedUrl);
const changeRegions = getScaleAdjustEffectiveChangeRegions(verification);
const [diffScores, changeDiffScores, outsideDiffScores, outsidePixelStats] = await Promise.all([
const [
diffScores,
changeDiffScores,
outsideDiffScores,
outsidePixelStats,
footprintStats,
] = await Promise.all([
getScaleAdjustDiffScores({
generatedBuffer,
regions: verification.regions,
@ -1079,10 +1191,18 @@ async function applyScaleAdjustVerificationToImages(params: {
sourceWidth: sourceImage.sourceWidth,
verification,
}),
getScaleAdjustFootprintChangeStats({
generatedBuffer,
sourceBuffer: sourceImage.sourceBuffer,
sourceHeight: sourceImage.sourceHeight,
sourceWidth: sourceImage.sourceWidth,
verification,
}),
]);
const changedInside = changeDiffScores.some(
const visuallyChangedInside = changeDiffScores.some(
(score) => score >= SCALE_ADJUST_VISUAL_DIFF_THRESHOLD
);
const changedInside = visuallyChangedInside && footprintStats.passed;
const outsideChanged =
isScaleAdjustOutsideChanged(outsideDiffScores) ||
isScaleAdjustOutsidePixelChanged(outsidePixelStats);
@ -1090,6 +1210,18 @@ async function applyScaleAdjustVerificationToImages(params: {
console.info("[Canvas Generate Image] Scale adjust result guard:", {
debugLabel,
changedInside,
footprintChanged: footprintStats.passed,
footprintStats: {
failedRegionCount: footprintStats.failedRegionCount,
shrinkRegionCount: footprintStats.shrinkRegionCount,
shrinkStats: footprintStats.shrinkStats.map((item) => ({
generatedOutsidePixels: item.generatedOutsidePixels,
outsideRatio: Number(item.outsideRatio.toFixed(3)),
passed: item.passed,
regionIndex: item.regionIndex,
sourceOutsidePixels: item.sourceOutsidePixels,
})),
},
imageIndex: index,
outsideChanged,
outsideDiffScores: outsideDiffScores.map((score) => Number(score.toFixed(2))),
@ -1120,6 +1252,18 @@ async function applyScaleAdjustVerificationToImages(params: {
imageIndex: index,
reasons: guardWarnings,
changedInside,
footprintChanged: footprintStats.passed,
footprintStats: {
failedRegionCount: footprintStats.failedRegionCount,
shrinkRegionCount: footprintStats.shrinkRegionCount,
shrinkStats: footprintStats.shrinkStats.map((item) => ({
generatedOutsidePixels: item.generatedOutsidePixels,
outsideRatio: Number(item.outsideRatio.toFixed(3)),
passed: item.passed,
regionIndex: item.regionIndex,
sourceOutsidePixels: item.sourceOutsidePixels,
})),
},
outsideChanged,
outsideDiffScores: outsideDiffScores.map((score) => Number(score.toFixed(2))),
outsidePixelStats: {
@ -1155,6 +1299,7 @@ async function applyScaleAdjustVerificationToImages(params: {
changedInside,
cleanedImage,
diffScores,
footprintStats,
image,
index,
outsideChanged,
@ -1176,6 +1321,7 @@ async function applyScaleAdjustVerificationToImages(params: {
changedInside,
cleanedImage,
diffScores,
footprintStats,
image,
index,
outsideChanged,