fix: focus scale adjust subject guide

This commit is contained in:
134 2026-07-02 19:38:24 +08:00
parent 956f9e0fed
commit 98e987e75b
2 changed files with 135 additions and 10 deletions

View File

@ -791,8 +791,18 @@ function getScaleAdjustVisualGuidePlacement(params: {
sourceTop + 1,
height
);
const sourceWidth = Math.max(1, Math.round(sourceRight - sourceLeft));
const sourceHeight = Math.max(1, Math.round(sourceBottom - sourceTop));
const sourceFootprintBox = {
left: Math.round(sourceLeft),
top: Math.round(sourceTop),
width: Math.max(1, Math.round(sourceRight - sourceLeft)),
height: Math.max(1, Math.round(sourceBottom - sourceTop)),
};
const sourceBox = getScaleAdjustFocusedSourceBox({
region,
sourceBox: sourceFootprintBox,
});
const sourceWidth = sourceBox.width;
const sourceHeight = sourceBox.height;
const widthScale = target.width / Math.max(1, sourceWidth);
const heightScale = target.height / Math.max(1, sourceHeight);
const uniformScale = Math.max(
@ -831,16 +841,64 @@ function getScaleAdjustVisualGuidePlacement(params: {
placedTop: Math.max(0, Math.round(placedTop)),
placedWidth: Math.max(1, Math.round(placedWidth)),
sourceBox: {
left: Math.round(sourceLeft),
top: Math.round(sourceTop),
left: sourceBox.left,
top: sourceBox.top,
width: sourceWidth,
height: sourceHeight,
},
sourceFootprintBox,
targetBox: target,
uniformScale,
};
}
function getScaleAdjustFocusDirection(region: NormalizedLocalEditRegion) {
const text = normalizeScaleAdjustModelText(region.prompt);
const compact = text.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
if (text.includes("left") || compact.includes("左")) return "left";
if (text.includes("right") || compact.includes("右")) return "right";
if (text.includes("top") || compact.includes("上")) return "top";
if (text.includes("bottom") || compact.includes("下")) return "bottom";
return "";
}
function getScaleAdjustFocusedSourceBox(params: {
region: NormalizedLocalEditRegion;
sourceBox: { left: number; top: number; width: number; height: number };
}) {
const { region, sourceBox } = params;
const direction = getScaleAdjustFocusDirection(region);
if (!direction) return sourceBox;
const minWidth = Math.max(1, Math.round(sourceBox.width * 0.42));
const minHeight = Math.max(1, Math.round(sourceBox.height * 0.42));
let left = sourceBox.left;
let top = sourceBox.top;
let width = sourceBox.width;
let height = sourceBox.height;
if (direction === "left") {
width = Math.max(minWidth, Math.round(sourceBox.width * 0.68));
} else if (direction === "right") {
const nextWidth = Math.max(minWidth, Math.round(sourceBox.width * 0.68));
left = sourceBox.left + sourceBox.width - nextWidth;
width = nextWidth;
} else if (direction === "top") {
height = Math.max(minHeight, Math.round(sourceBox.height * 0.72));
} else if (direction === "bottom") {
const nextHeight = Math.max(minHeight, Math.round(sourceBox.height * 0.72));
top = sourceBox.top + sourceBox.height - nextHeight;
height = nextHeight;
}
return {
left,
top,
width,
height,
};
}
function buildScaleAdjustPatchMaskSvg(params: { height: number; width: number }) {
const { height, width } = params;
const blur = Math.max(2, Math.min(14, Math.round(Math.min(width, height) * 0.035)));
@ -966,7 +1024,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
scalePercent,
width: sourceWidth,
});
sourceFootprintBoxes.push(placement.sourceBox);
sourceFootprintBoxes.push(placement.sourceFootprintBox);
placements.push(placement);
const resizedCropBuffer = await sharp(sourceBuffer, { failOn: "none" })
@ -1989,6 +2047,7 @@ export async function POST(request: NextRequest) {
: undefined,
sourceImageUrl,
regions: regions.map((region) => region.rect),
regionPrompts: regions.map((region) => region.prompt),
...(typeof scalePercent === "number"
? { scalePercent: scalePercent as ScaleAdjustPercent }
: {}),

View File

@ -56,6 +56,7 @@ export interface CanvasScaleAdjustVerificationConfig {
allowOutsideChangedWhenChangedInside?: boolean;
protectedRegions?: CanvasLocalEditCompositeRegion[];
rejectFailedCandidates?: boolean;
regionPrompts?: string[];
sourceImageUrl: string;
regions: CanvasLocalEditCompositeRegion[];
targetRegions?: CanvasLocalEditCompositeRegion[];
@ -1405,6 +1406,63 @@ function getRawPixelIndex(width: number, x: number, y: number) {
return (y * width + x) * 3;
}
function normalizeScaleAdjustFocusText(input: unknown) {
return String(input || "")
.trim()
.toLowerCase()
.normalize("NFKC")
.replace(/[「」『』【】[\]()(){}]/g, " ")
.replace(/[_/|::,,.。+]+/g, " ")
.replace(/\s+/g, " ");
}
function getScaleAdjustFallbackFocusDirection(prompt: unknown) {
const text = normalizeScaleAdjustFocusText(prompt);
const compact = text.replace(/[^a-z0-9\u4e00-\u9fa5]+/g, "");
if (text.includes("left") || compact.includes("左")) return "left";
if (text.includes("right") || compact.includes("右")) return "right";
if (text.includes("top") || compact.includes("上")) return "top";
if (text.includes("bottom") || compact.includes("下")) return "bottom";
return "";
}
function getScaleAdjustFallbackFocusedSourceBox(params: {
prompt?: string;
sourceBox: ReturnType<typeof rectToPixelBox>;
}) {
const { prompt, sourceBox } = params;
const direction = getScaleAdjustFallbackFocusDirection(prompt);
if (!direction) return sourceBox;
const minWidth = Math.max(1, Math.round(sourceBox.width * 0.42));
const minHeight = Math.max(1, Math.round(sourceBox.height * 0.42));
let left = sourceBox.left;
let top = sourceBox.top;
let width = sourceBox.width;
let height = sourceBox.height;
if (direction === "left") {
width = Math.max(minWidth, Math.round(sourceBox.width * 0.68));
} else if (direction === "right") {
const nextWidth = Math.max(minWidth, Math.round(sourceBox.width * 0.68));
left = sourceBox.left + sourceBox.width - nextWidth;
width = nextWidth;
} else if (direction === "top") {
height = Math.max(minHeight, Math.round(sourceBox.height * 0.72));
} else if (direction === "bottom") {
const nextHeight = Math.max(minHeight, Math.round(sourceBox.height * 0.72));
top = sourceBox.top + sourceBox.height - nextHeight;
height = nextHeight;
}
return {
left,
top,
width,
height,
};
}
function getLuma(r: number, g: number, b: number) {
return r * 0.299 + g * 0.587 + b * 0.114;
}
@ -1662,13 +1720,21 @@ async function buildScaleAdjustDeterministicFallbackImages(params: {
region: targetRegion,
width: sourceImage.sourceWidth,
});
const prompt = verification.regionPrompts?.[index] || "";
const focusedSourceBox = getScaleAdjustFallbackFocusedSourceBox({
prompt,
sourceBox,
});
const mask = buildScaleAdjustFallbackSubjectMask({
box: sourceBox,
box: focusedSourceBox,
raw: baseRaw,
width: sourceImage.sourceWidth,
});
const maskedPixels = mask.reduce((sum, value) => sum + (value ? 1 : 0), 0);
if (maskedPixels < Math.max(24, sourceBox.width * sourceBox.height * 0.015)) {
if (
maskedPixels <
Math.max(24, focusedSourceBox.width * focusedSourceBox.height * 0.015)
) {
console.warn("[Canvas Generate Image] Scale adjust deterministic fallback skipped sparse mask:", {
debugLabel,
maskedPixels,
@ -1677,19 +1743,19 @@ async function buildScaleAdjustDeterministicFallbackImages(params: {
continue;
}
const background = estimateScaleAdjustFallbackBackground({
box: sourceBox,
box: focusedSourceBox,
raw: baseRaw,
width: sourceImage.sourceWidth,
});
const objectBuffer = await buildScaleAdjustFallbackObjectPng({
box: sourceBox,
box: focusedSourceBox,
mask,
sourceBuffer: sourceImage.sourceBuffer,
targetBox,
});
baseRaw = inpaintScaleAdjustMaskedRegion({
background,
box: sourceBox,
box: focusedSourceBox,
mask,
raw: baseRaw,
width: sourceImage.sourceWidth,