fix: reject scale adjust whole-frame changes

This commit is contained in:
134 2026-06-20 10:30:17 +08:00
parent 52cd365925
commit 4f06ff7151

View File

@ -93,6 +93,9 @@ const LOCAL_EDIT_COMPOSITE_MAX_PADDING_RATIO = 0.06;
const LOCAL_EDIT_COMPOSITE_FEATHER_MIN_PX = 10;
const LOCAL_EDIT_COMPOSITE_FEATHER_MAX_PX = 44;
const SCALE_ADJUST_VISUAL_DIFF_THRESHOLD = 14;
const SCALE_ADJUST_OUTSIDE_DIFF_THRESHOLD = 18;
const SCALE_ADJUST_OUTSIDE_DIFF_MIN_CHANGED_RATIO = 0.45;
const SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN = 0.035;
const SCALE_ADJUST_DIFF_SAMPLE_SIZE = 112;
const SCALE_ADJUST_FALLBACK_FEATHER_MIN_PX = 8;
const SCALE_ADJUST_FALLBACK_FEATHER_MAX_PX = 36;
@ -295,6 +298,75 @@ function scaleRegionAroundCenter(
return { x, y, width, height };
}
function expandCanvasRegion(
region: CanvasLocalEditCompositeRegion,
margin: number
): CanvasLocalEditCompositeRegion {
const x = clampRatio(region.x - margin);
const y = clampRatio(region.y - margin);
const right = clampRatio(region.x + region.width + margin);
const bottom = clampRatio(region.y + region.height + margin);
return {
x,
y,
width: Math.max(0, right - x),
height: Math.max(0, bottom - y),
};
}
function canvasRegionsIntersect(
left: CanvasLocalEditCompositeRegion,
right: CanvasLocalEditCompositeRegion
) {
return !(
left.x + left.width <= right.x ||
right.x + right.width <= left.x ||
left.y + left.height <= right.y ||
right.y + right.height <= left.y
);
}
function getScaleAdjustProtectedRegions(
verification: CanvasScaleAdjustVerificationConfig
) {
return verification.regions.flatMap((region) => [
expandCanvasRegion(region, SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN),
expandCanvasRegion(
scaleRegionAroundCenter(region, verification.scalePercent),
SCALE_ADJUST_OUTSIDE_PROTECTED_MARGIN
),
]);
}
function getScaleAdjustOutsideSampleRegions(
verification: CanvasScaleAdjustVerificationConfig
) {
const protectedRegions = getScaleAdjustProtectedRegions(verification);
const candidates: CanvasLocalEditCompositeRegion[] = [
{ x: 0.03, y: 0.03, width: 0.2, height: 0.16 },
{ x: 0.4, y: 0.03, width: 0.2, height: 0.16 },
{ x: 0.77, y: 0.03, width: 0.2, height: 0.16 },
{ x: 0.03, y: 0.28, width: 0.2, height: 0.16 },
{ x: 0.77, y: 0.28, width: 0.2, height: 0.16 },
{ x: 0.03, y: 0.55, width: 0.2, height: 0.16 },
{ x: 0.77, y: 0.55, width: 0.2, height: 0.16 },
{ x: 0.03, y: 0.81, width: 0.2, height: 0.16 },
{ x: 0.4, y: 0.81, width: 0.2, height: 0.16 },
{ x: 0.77, y: 0.81, width: 0.2, height: 0.16 },
{ x: 0.28, y: 0.28, width: 0.18, height: 0.16 },
{ x: 0.54, y: 0.28, width: 0.18, height: 0.16 },
{ x: 0.28, y: 0.55, width: 0.18, height: 0.16 },
{ x: 0.54, y: 0.55, width: 0.18, height: 0.16 },
];
return candidates.filter(
(candidate) =>
!protectedRegions.some((protectedRegion) =>
canvasRegionsIntersect(candidate, protectedRegion)
)
);
}
function rectToPixelBox(params: {
region: CanvasLocalEditCompositeRegion;
width: number;
@ -424,6 +496,49 @@ async function getScaleAdjustDiffScores(params: {
return scores;
}
async function getScaleAdjustOutsideDiffScores(params: {
generatedBuffer: Buffer;
sourceBuffer: Buffer;
sourceWidth: number;
sourceHeight: number;
verification: CanvasScaleAdjustVerificationConfig;
}) {
const sampleRegions = getScaleAdjustOutsideSampleRegions(params.verification);
const scores: number[] = [];
for (const region of sampleRegions) {
const [sourceSample, generatedSample] = await Promise.all([
getScaleAdjustRegionSample({
buffer: params.sourceBuffer,
sourceWidth: params.sourceWidth,
sourceHeight: params.sourceHeight,
region,
}),
getScaleAdjustRegionSample({
buffer: params.generatedBuffer,
sourceWidth: params.sourceWidth,
sourceHeight: params.sourceHeight,
region,
}),
]);
scores.push(calculateMeanAbsoluteDiff(sourceSample, generatedSample));
}
return scores;
}
function isScaleAdjustOutsideChanged(scores: number[]) {
if (scores.length < 2) return false;
const changedCount = scores.filter(
(score) => score >= SCALE_ADJUST_OUTSIDE_DIFF_THRESHOLD
).length;
const requiredCount = Math.max(
2,
Math.ceil(scores.length * SCALE_ADJUST_OUTSIDE_DIFF_MIN_CHANGED_RATIO)
);
return changedCount >= requiredCount;
}
async function buildScaleAdjustFallbackImage(params: {
sourceBuffer: Buffer;
sourceWidth: number;
@ -547,6 +662,7 @@ async function createScaleAdjustFallbackUpload(params: {
imageIndex?: number;
reason: string;
diffScores?: number[];
outsideDiffScores?: number[];
}) {
const fallbackBuffer = await buildScaleAdjustFallbackImage({
sourceBuffer: params.sourceBuffer,
@ -576,6 +692,9 @@ async function createScaleAdjustFallbackUpload(params: {
debugLabel: params.debugLabel,
diffScores: params.diffScores?.map((score) => Math.round(score * 10) / 10),
imageIndex: params.imageIndex,
outsideDiffScores: params.outsideDiffScores?.map(
(score) => Math.round(score * 10) / 10
),
reason: params.reason,
regionCount: params.verification.regions.length,
scalePercent: params.verification.scalePercent,
@ -615,15 +734,26 @@ async function applyScaleAdjustVerificationToImages(params: {
sourceWidth,
regions: verification.regions,
});
const outsideDiffScores = await getScaleAdjustOutsideDiffScores({
generatedBuffer,
sourceBuffer,
sourceHeight,
sourceWidth,
verification,
});
const isVisuallyUnchanged =
diffScores.length > 0 &&
diffScores.every((score) => score < SCALE_ADJUST_VISUAL_DIFF_THRESHOLD);
const outsideChanged = isScaleAdjustOutsideChanged(outsideDiffScores);
if (!isVisuallyUnchanged) {
if (!isVisuallyUnchanged && !outsideChanged) {
console.info("[Canvas Generate Image] Scale adjust output accepted:", {
debugLabel,
diffScores: diffScores.map((score) => Math.round(score * 10) / 10),
imageIndex: index + 1,
outsideDiffScores: outsideDiffScores.map(
(score) => Math.round(score * 10) / 10
),
scalePercent: verification.scalePercent,
});
checkedImages.push(image);
@ -634,7 +764,8 @@ async function applyScaleAdjustVerificationToImages(params: {
debugLabel,
diffScores,
imageIndex: index + 1,
reason: "unchanged-output",
outsideDiffScores,
reason: outsideChanged ? "outside-frame-changed" : "unchanged-output",
sourceBuffer,
sourceHeight,
sourceWidth,