fix: reject scale adjust whole-frame changes
This commit is contained in:
parent
52cd365925
commit
4f06ff7151
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user