fix: use cutout drafts for scale adjustment
This commit is contained in:
parent
ce4e056f20
commit
ea52d65971
@ -2,7 +2,10 @@ import { NextRequest, NextResponse } from "next/server";
|
|||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
import { auth } from "@/app/(auth)/auth";
|
import { auth } from "@/app/(auth)/auth";
|
||||||
import { fetchImageBufferFromUrl } from "@/lib/ai/rmbg/service";
|
import {
|
||||||
|
executeRmbgJob,
|
||||||
|
fetchImageBufferFromUrl,
|
||||||
|
} from "@/lib/ai/rmbg/service";
|
||||||
import {
|
import {
|
||||||
createCanvasImageTask,
|
createCanvasImageTask,
|
||||||
enqueueCanvasImageTask,
|
enqueueCanvasImageTask,
|
||||||
@ -106,6 +109,8 @@ const SCALE_ADJUST_PATCH_WORKSPACE_MAX_MARGIN = 0.18;
|
|||||||
const SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR = "#f5f7fb";
|
const SCALE_ADJUST_LAYOUT_GUIDE_WASH_COLOR = "#f5f7fb";
|
||||||
const SCALE_ADJUST_STRUCTURE_LOCK_MIN_MASK_RATIO = 0.006;
|
const SCALE_ADJUST_STRUCTURE_LOCK_MIN_MASK_RATIO = 0.006;
|
||||||
const SCALE_ADJUST_STRUCTURE_LOCK_COMPONENT_MIN_RATIO = 0.00018;
|
const SCALE_ADJUST_STRUCTURE_LOCK_COMPONENT_MIN_RATIO = 0.00018;
|
||||||
|
const SCALE_ADJUST_RMBG_MIN_ALPHA_RATIO = 0.012;
|
||||||
|
const SCALE_ADJUST_RMBG_MAX_SOLID_ALPHA_RATIO = 0.92;
|
||||||
const MIN_REGION_EDGE_PX = 24;
|
const MIN_REGION_EDGE_PX = 24;
|
||||||
const SCALE_ADJUST_OPERATION = "scale-adjust";
|
const SCALE_ADJUST_OPERATION = "scale-adjust";
|
||||||
type LocalEditOperation = "local-edit" | typeof SCALE_ADJUST_OPERATION;
|
type LocalEditOperation = "local-edit" | typeof SCALE_ADJUST_OPERATION;
|
||||||
@ -128,6 +133,11 @@ type ScaleAdjustPatchReference = {
|
|||||||
height: number;
|
height: number;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
type ScaleAdjustStructureLockedComposite = {
|
||||||
|
method?: "rmbg-cutout" | "pixel-mask";
|
||||||
|
overlay: { input: Buffer; left: number; top: number };
|
||||||
|
wash: { input: Buffer; left: number; top: number };
|
||||||
|
};
|
||||||
type AiModelWithProviderForSelection = Awaited<
|
type AiModelWithProviderForSelection = Awaited<
|
||||||
ReturnType<typeof getEnabledAiModelsWithProvider>
|
ReturnType<typeof getEnabledAiModelsWithProvider>
|
||||||
>[number];
|
>[number];
|
||||||
@ -1663,20 +1673,310 @@ async function buildScaleAdjustMaskWashPatch(params: {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScaleAdjustAlphaBoundingBox(params: {
|
||||||
|
height: number;
|
||||||
|
rawRgba: Buffer;
|
||||||
|
threshold?: number;
|
||||||
|
width: number;
|
||||||
|
}) {
|
||||||
|
const { height, rawRgba, threshold = 18, width } = params;
|
||||||
|
let left = width;
|
||||||
|
let top = height;
|
||||||
|
let right = -1;
|
||||||
|
let bottom = -1;
|
||||||
|
let count = 0;
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y += 1) {
|
||||||
|
for (let x = 0; x < width; x += 1) {
|
||||||
|
const alpha = rawRgba[(y * width + x) * 4 + 3] || 0;
|
||||||
|
if (alpha < threshold) continue;
|
||||||
|
left = Math.min(left, x);
|
||||||
|
top = Math.min(top, y);
|
||||||
|
right = Math.max(right, x);
|
||||||
|
bottom = Math.max(bottom, y);
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (right < left || bottom < top) return null;
|
||||||
|
return {
|
||||||
|
count,
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
width: right - left + 1,
|
||||||
|
height: bottom - top + 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractScaleAdjustAlphaChannel(params: {
|
||||||
|
height: number;
|
||||||
|
rawRgba: Buffer;
|
||||||
|
width: number;
|
||||||
|
}) {
|
||||||
|
const { height, rawRgba, width } = params;
|
||||||
|
const alpha = Buffer.alloc(width * height);
|
||||||
|
for (let index = 0; index < width * height; index += 1) {
|
||||||
|
alpha[index] = rawRgba[index * 4 + 3] || 0;
|
||||||
|
}
|
||||||
|
return alpha;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resizeScaleAdjustAlphaMask(params: {
|
||||||
|
height: number;
|
||||||
|
maskBuffer: Buffer;
|
||||||
|
targetHeight: number;
|
||||||
|
targetWidth: number;
|
||||||
|
width: number;
|
||||||
|
}) {
|
||||||
|
const { height, maskBuffer, targetHeight, targetWidth, width } = params;
|
||||||
|
const { data, info } = await sharp(maskBuffer, {
|
||||||
|
raw: { channels: 1, height, width },
|
||||||
|
})
|
||||||
|
.resize(targetWidth, targetHeight, {
|
||||||
|
background: { alpha: 1, b: 0, g: 0, r: 0 },
|
||||||
|
fit: "contain",
|
||||||
|
kernel: sharp.kernel.lanczos3,
|
||||||
|
})
|
||||||
|
.greyscale()
|
||||||
|
.raw()
|
||||||
|
.toBuffer({ resolveWithObject: true });
|
||||||
|
if (info.channels === 1) return Buffer.from(data);
|
||||||
|
|
||||||
|
const output = Buffer.alloc(targetWidth * targetHeight);
|
||||||
|
for (let index = 0; index < targetWidth * targetHeight; index += 1) {
|
||||||
|
output[index] = data[index * info.channels] || 0;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildScaleAdjustRmbgStructureLockedComposite(params: {
|
||||||
|
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
|
||||||
|
raw: Buffer;
|
||||||
|
sourceBuffer: Buffer;
|
||||||
|
sourceImageUrl?: string;
|
||||||
|
sourceWidth: number;
|
||||||
|
userId?: string;
|
||||||
|
}): Promise<ScaleAdjustStructureLockedComposite | null> {
|
||||||
|
const {
|
||||||
|
placement,
|
||||||
|
raw,
|
||||||
|
sourceBuffer,
|
||||||
|
sourceImageUrl,
|
||||||
|
sourceWidth,
|
||||||
|
userId,
|
||||||
|
} = params;
|
||||||
|
if (!userId) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const sourceCropBuffer = await sharp(sourceBuffer, { failOn: "none" })
|
||||||
|
.extract(placement.sourceBox)
|
||||||
|
.png({ compressionLevel: 6 })
|
||||||
|
.toBuffer();
|
||||||
|
const cutoutResult = await executeRmbgJob({
|
||||||
|
userId,
|
||||||
|
imageBase64: sourceCropBuffer.toString("base64"),
|
||||||
|
imageBuffer: sourceCropBuffer,
|
||||||
|
prompt: "比例调整主体抠图:只保留框选的目标人物/家具/商品主体,输出透明背景 PNG",
|
||||||
|
skipGallerySave: true,
|
||||||
|
sourceImageUrl,
|
||||||
|
});
|
||||||
|
const cutoutBuffer = await fetchImageBufferFromUrl(cutoutResult.original_url);
|
||||||
|
const normalizedCutout = await sharp(cutoutBuffer, { failOn: "none" })
|
||||||
|
.rotate()
|
||||||
|
.ensureAlpha()
|
||||||
|
.png({ compressionLevel: 6 })
|
||||||
|
.toBuffer();
|
||||||
|
const cutoutMetadata = await sharp(normalizedCutout, { failOn: "none" }).metadata();
|
||||||
|
const cutoutWidth = Math.round(Number(cutoutMetadata.width || 0));
|
||||||
|
const cutoutHeight = Math.round(Number(cutoutMetadata.height || 0));
|
||||||
|
if (cutoutWidth <= 0 || cutoutHeight <= 0) return null;
|
||||||
|
|
||||||
|
const cutoutRaw = await sharp(normalizedCutout, { failOn: "none" })
|
||||||
|
.ensureAlpha()
|
||||||
|
.raw()
|
||||||
|
.toBuffer();
|
||||||
|
const alphaBbox = getScaleAdjustAlphaBoundingBox({
|
||||||
|
height: cutoutHeight,
|
||||||
|
rawRgba: cutoutRaw,
|
||||||
|
width: cutoutWidth,
|
||||||
|
});
|
||||||
|
if (!alphaBbox) return null;
|
||||||
|
|
||||||
|
const alphaRatio = alphaBbox.count / Math.max(1, cutoutWidth * cutoutHeight);
|
||||||
|
const bboxCoversCanvas =
|
||||||
|
alphaBbox.width >= cutoutWidth * 0.97 &&
|
||||||
|
alphaBbox.height >= cutoutHeight * 0.97;
|
||||||
|
if (
|
||||||
|
alphaRatio < SCALE_ADJUST_RMBG_MIN_ALPHA_RATIO ||
|
||||||
|
(alphaRatio > SCALE_ADJUST_RMBG_MAX_SOLID_ALPHA_RATIO && bboxCoversCanvas)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameSizeCutout =
|
||||||
|
Math.abs(cutoutWidth - placement.sourceBox.width) <= 2 &&
|
||||||
|
Math.abs(cutoutHeight - placement.sourceBox.height) <= 2;
|
||||||
|
const fullAlphaMask = extractScaleAdjustAlphaChannel({
|
||||||
|
height: cutoutHeight,
|
||||||
|
rawRgba: cutoutRaw,
|
||||||
|
width: cutoutWidth,
|
||||||
|
});
|
||||||
|
const sourceMaskBuffer = sameSizeCutout
|
||||||
|
? fullAlphaMask
|
||||||
|
: await resizeScaleAdjustAlphaMask({
|
||||||
|
height: cutoutHeight,
|
||||||
|
maskBuffer: fullAlphaMask,
|
||||||
|
targetHeight: placement.sourceBox.height,
|
||||||
|
targetWidth: placement.sourceBox.width,
|
||||||
|
width: cutoutWidth,
|
||||||
|
});
|
||||||
|
const background = estimateScaleAdjustBoxBackground({
|
||||||
|
box: placement.sourceBox,
|
||||||
|
raw,
|
||||||
|
width: sourceWidth,
|
||||||
|
});
|
||||||
|
const sourceWashMask = dilateScaleAdjustStructureMask({
|
||||||
|
height: placement.sourceBox.height,
|
||||||
|
mask: new Uint8Array(sourceMaskBuffer),
|
||||||
|
radius: Math.max(
|
||||||
|
2,
|
||||||
|
Math.round(
|
||||||
|
Math.min(placement.sourceBox.width, placement.sourceBox.height) * 0.014
|
||||||
|
)
|
||||||
|
),
|
||||||
|
width: placement.sourceBox.width,
|
||||||
|
});
|
||||||
|
const sourceWashPatch = await buildScaleAdjustMaskWashPatch({
|
||||||
|
background,
|
||||||
|
box: placement.sourceBox,
|
||||||
|
maskBuffer: Buffer.from(sourceWashMask),
|
||||||
|
});
|
||||||
|
|
||||||
|
const subjectCutout = await sharp(normalizedCutout, { failOn: "none" })
|
||||||
|
.extract(alphaBbox)
|
||||||
|
.png({ compressionLevel: 6 })
|
||||||
|
.toBuffer();
|
||||||
|
const widthFactor =
|
||||||
|
placement.targetBox.width / Math.max(1, placement.sourceFootprintBox.width);
|
||||||
|
const heightFactor =
|
||||||
|
placement.targetBox.height / Math.max(1, placement.sourceFootprintBox.height);
|
||||||
|
const structureScale = Math.max(
|
||||||
|
0.05,
|
||||||
|
Math.min(
|
||||||
|
3.5,
|
||||||
|
Math.sqrt(
|
||||||
|
Math.max(0.0025, widthFactor) * Math.max(0.0025, heightFactor)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let placedWidth = Math.max(1, Math.round(alphaBbox.width * structureScale));
|
||||||
|
let placedHeight = Math.max(1, Math.round(alphaBbox.height * structureScale));
|
||||||
|
if (!sameSizeCutout) {
|
||||||
|
const maxPlacedWidth = Math.max(1, Math.round(placement.targetBox.width * 1.06));
|
||||||
|
const maxPlacedHeight = Math.max(1, Math.round(placement.targetBox.height * 1.06));
|
||||||
|
const fitRatio = Math.min(
|
||||||
|
1,
|
||||||
|
maxPlacedWidth / Math.max(1, placedWidth),
|
||||||
|
maxPlacedHeight / Math.max(1, placedHeight)
|
||||||
|
);
|
||||||
|
placedWidth = Math.max(1, Math.round(placedWidth * fitRatio));
|
||||||
|
placedHeight = Math.max(1, Math.round(placedHeight * fitRatio));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceSubjectBox = sameSizeCutout
|
||||||
|
? {
|
||||||
|
left: placement.sourceBox.left + alphaBbox.left,
|
||||||
|
top: placement.sourceBox.top + alphaBbox.top,
|
||||||
|
width: alphaBbox.width,
|
||||||
|
height: alphaBbox.height,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
const sourceCenterX = sourceSubjectBox
|
||||||
|
? sourceSubjectBox.left + sourceSubjectBox.width / 2
|
||||||
|
: placement.sourceBox.left + placement.sourceBox.width / 2;
|
||||||
|
const sourceCenterY = sourceSubjectBox
|
||||||
|
? sourceSubjectBox.top + sourceSubjectBox.height / 2
|
||||||
|
: placement.sourceBox.top + placement.sourceBox.height / 2;
|
||||||
|
const targetCenterX = sourceSubjectBox
|
||||||
|
? placement.targetBox.left +
|
||||||
|
((sourceCenterX - placement.sourceFootprintBox.left) /
|
||||||
|
Math.max(1, placement.sourceFootprintBox.width)) *
|
||||||
|
placement.targetBox.width
|
||||||
|
: placement.targetBox.left + placement.targetBox.width / 2;
|
||||||
|
const targetCenterY = sourceSubjectBox
|
||||||
|
? placement.targetBox.top +
|
||||||
|
((sourceCenterY - placement.sourceFootprintBox.top) /
|
||||||
|
Math.max(1, placement.sourceFootprintBox.height)) *
|
||||||
|
placement.targetBox.height
|
||||||
|
: placement.targetBox.top + placement.targetBox.height / 2;
|
||||||
|
const structureOverlay = await sharp(subjectCutout, { failOn: "none" })
|
||||||
|
.resize(placedWidth, placedHeight, {
|
||||||
|
fit: "fill",
|
||||||
|
kernel: sharp.kernel.lanczos3,
|
||||||
|
})
|
||||||
|
.png({ compressionLevel: 6 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
console.info("[Canvas Local Edit] Scale adjust RMBG structure draft applied:", {
|
||||||
|
alphaRatio: Number(alphaRatio.toFixed(3)),
|
||||||
|
cutoutHeight,
|
||||||
|
cutoutWidth,
|
||||||
|
placedHeight,
|
||||||
|
placedWidth,
|
||||||
|
sameSizeCutout,
|
||||||
|
sourceHeight: placement.sourceBox.height,
|
||||||
|
sourceWidth: placement.sourceBox.width,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
method: "rmbg-cutout",
|
||||||
|
overlay: {
|
||||||
|
input: structureOverlay,
|
||||||
|
left: Math.round(targetCenterX - placedWidth / 2),
|
||||||
|
top: Math.round(targetCenterY - placedHeight / 2),
|
||||||
|
},
|
||||||
|
wash: {
|
||||||
|
input: sourceWashPatch,
|
||||||
|
left: placement.sourceBox.left,
|
||||||
|
top: placement.sourceBox.top,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Canvas Local Edit] Scale adjust RMBG structure draft failed; fallback to pixel mask:", {
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function buildScaleAdjustStructureLockedComposite(params: {
|
async function buildScaleAdjustStructureLockedComposite(params: {
|
||||||
includePersonPixels?: boolean;
|
includePersonPixels?: boolean;
|
||||||
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
|
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
|
||||||
raw: Buffer;
|
raw: Buffer;
|
||||||
sourceBuffer: Buffer;
|
sourceBuffer: Buffer;
|
||||||
|
sourceImageUrl?: string;
|
||||||
sourceWidth: number;
|
sourceWidth: number;
|
||||||
}) {
|
userId?: string;
|
||||||
|
}): Promise<ScaleAdjustStructureLockedComposite | null> {
|
||||||
const {
|
const {
|
||||||
includePersonPixels = false,
|
includePersonPixels = false,
|
||||||
placement,
|
placement,
|
||||||
raw,
|
raw,
|
||||||
sourceBuffer,
|
sourceBuffer,
|
||||||
|
sourceImageUrl,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
|
userId,
|
||||||
} = params;
|
} = params;
|
||||||
|
const rmbgComposite = await buildScaleAdjustRmbgStructureLockedComposite({
|
||||||
|
placement,
|
||||||
|
raw,
|
||||||
|
sourceBuffer,
|
||||||
|
sourceImageUrl,
|
||||||
|
sourceWidth,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
if (rmbgComposite) return rmbgComposite;
|
||||||
|
|
||||||
const initialMask = buildScaleAdjustStructureInitialMask({
|
const initialMask = buildScaleAdjustStructureInitialMask({
|
||||||
box: placement.sourceBox,
|
box: placement.sourceBox,
|
||||||
includePersonPixels,
|
includePersonPixels,
|
||||||
@ -1784,6 +2084,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
|
|||||||
.toBuffer();
|
.toBuffer();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
method: "pixel-mask",
|
||||||
overlay: {
|
overlay: {
|
||||||
input: structureOverlay,
|
input: structureOverlay,
|
||||||
left: Math.round(targetCenterX - placedWidth / 2),
|
left: Math.round(targetCenterX - placedWidth / 2),
|
||||||
@ -1885,16 +2186,20 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
|||||||
scalePercent?: ScaleAdjustPercent | null;
|
scalePercent?: ScaleAdjustPercent | null;
|
||||||
sourceBuffer: Buffer;
|
sourceBuffer: Buffer;
|
||||||
sourceHeight: number;
|
sourceHeight: number;
|
||||||
|
sourceImageUrl?: string;
|
||||||
sourceWidth: number;
|
sourceWidth: number;
|
||||||
structureLockedDraft?: boolean;
|
structureLockedDraft?: boolean;
|
||||||
|
userId?: string;
|
||||||
}) {
|
}) {
|
||||||
const {
|
const {
|
||||||
regions,
|
regions,
|
||||||
scalePercent,
|
scalePercent,
|
||||||
sourceBuffer,
|
sourceBuffer,
|
||||||
sourceHeight,
|
sourceHeight,
|
||||||
|
sourceImageUrl,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
structureLockedDraft = false,
|
structureLockedDraft = false,
|
||||||
|
userId,
|
||||||
} = params;
|
} = params;
|
||||||
const sourceRaw = structureLockedDraft
|
const sourceRaw = structureLockedDraft
|
||||||
? await sharp(sourceBuffer, { failOn: "none" })
|
? await sharp(sourceBuffer, { failOn: "none" })
|
||||||
@ -1946,7 +2251,9 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
|
|||||||
placement,
|
placement,
|
||||||
raw: sourceRaw,
|
raw: sourceRaw,
|
||||||
sourceBuffer,
|
sourceBuffer,
|
||||||
|
sourceImageUrl,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
|
userId,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@ -2030,10 +2337,19 @@ async function createScaleAdjustPatchReference(params: {
|
|||||||
scalePercent?: ScaleAdjustPercent | null;
|
scalePercent?: ScaleAdjustPercent | null;
|
||||||
sourceBuffer: Buffer;
|
sourceBuffer: Buffer;
|
||||||
sourceHeight: number;
|
sourceHeight: number;
|
||||||
|
sourceImageUrl?: string;
|
||||||
sourceWidth: number;
|
sourceWidth: number;
|
||||||
|
userId?: string;
|
||||||
}): Promise<ScaleAdjustPatchReference> {
|
}): Promise<ScaleAdjustPatchReference> {
|
||||||
const { regions, scalePercent, sourceBuffer, sourceHeight, sourceWidth } =
|
const {
|
||||||
params;
|
regions,
|
||||||
|
scalePercent,
|
||||||
|
sourceBuffer,
|
||||||
|
sourceHeight,
|
||||||
|
sourceImageUrl,
|
||||||
|
sourceWidth,
|
||||||
|
userId,
|
||||||
|
} = params;
|
||||||
const workspaceRect = getScaleAdjustPatchWorkspaceRect(regions, scalePercent);
|
const workspaceRect = getScaleAdjustPatchWorkspaceRect(regions, scalePercent);
|
||||||
const workspacePixelRect = rectToPixels(workspaceRect, sourceWidth, sourceHeight);
|
const workspacePixelRect = rectToPixels(workspaceRect, sourceWidth, sourceHeight);
|
||||||
const structureLockedDraft = hasScaleAdjustEnlargeTarget(regions, scalePercent);
|
const structureLockedDraft = hasScaleAdjustEnlargeTarget(regions, scalePercent);
|
||||||
@ -2042,8 +2358,10 @@ async function createScaleAdjustPatchReference(params: {
|
|||||||
scalePercent,
|
scalePercent,
|
||||||
sourceBuffer,
|
sourceBuffer,
|
||||||
sourceHeight,
|
sourceHeight,
|
||||||
|
sourceImageUrl,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
structureLockedDraft,
|
structureLockedDraft,
|
||||||
|
userId,
|
||||||
});
|
});
|
||||||
const [sourceCropBuffer, targetDraftCropBuffer] = await Promise.all([
|
const [sourceCropBuffer, targetDraftCropBuffer] = await Promise.all([
|
||||||
sharp(sourceBuffer, { failOn: "none" })
|
sharp(sourceBuffer, { failOn: "none" })
|
||||||
@ -3216,7 +3534,9 @@ export async function POST(request: NextRequest) {
|
|||||||
scalePercent,
|
scalePercent,
|
||||||
sourceBuffer,
|
sourceBuffer,
|
||||||
sourceHeight,
|
sourceHeight,
|
||||||
|
sourceImageUrl,
|
||||||
sourceWidth,
|
sourceWidth,
|
||||||
|
userId: session.user.id,
|
||||||
})
|
})
|
||||||
: null;
|
: null;
|
||||||
const shouldCreateScaleAdjustCropReferences =
|
const shouldCreateScaleAdjustCropReferences =
|
||||||
|
|||||||
@ -153,6 +153,8 @@ class ScaleAdjustOutputRejectedError extends Error {
|
|||||||
const message =
|
const message =
|
||||||
params.reason === "outside-frame-changed"
|
params.reason === "outside-frame-changed"
|
||||||
? "比例调整结果改变了框选区域外的画面,已拒绝该模型结果"
|
? "比例调整结果改变了框选区域外的画面,已拒绝该模型结果"
|
||||||
|
: params.reason === "rectangular-seam-detected"
|
||||||
|
? "比例调整结果出现明显矩形拼接硬边,已拒绝该模型结果"
|
||||||
: "比例调整结果没有产生有效变化,已拒绝该模型结果";
|
: "比例调整结果没有产生有效变化,已拒绝该模型结果";
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "ScaleAdjustOutputRejectedError";
|
this.name = "ScaleAdjustOutputRejectedError";
|
||||||
@ -190,6 +192,14 @@ const SCALE_ADJUST_SHRINK_OUTSIDE_FOOTPRINT_RELAXED_MAX_RATIO = 0.84;
|
|||||||
const SCALE_ADJUST_SHRINK_OUTSIDE_FOOTPRINT_MIN_REMOVED_RATIO = 0.14;
|
const SCALE_ADJUST_SHRINK_OUTSIDE_FOOTPRINT_MIN_REMOVED_RATIO = 0.14;
|
||||||
const SCALE_ADJUST_SHRINK_MIN_SOURCE_OUTSIDE_PIXELS = 60;
|
const SCALE_ADJUST_SHRINK_MIN_SOURCE_OUTSIDE_PIXELS = 60;
|
||||||
const SCALE_ADJUST_SHRINK_TARGET_FOOTPRINT_MARGIN = 0.018;
|
const SCALE_ADJUST_SHRINK_TARGET_FOOTPRINT_MARGIN = 0.018;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_CHANGE_THRESHOLD = 18;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_MIN_EDGE_DIFF = 5;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_MAX_EDGE_DIFF = 54;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_EDGE_INCREASE = 2.5;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_RUN_RATIO = 0.11;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_TRANSITION_RATIO = 0.18;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_RUN_RATIO = 0.1;
|
||||||
|
const SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_TRANSITION_RATIO = 0.26;
|
||||||
|
|
||||||
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
|
const SUPPORTED_CANVAS_PROVIDER_TYPES = new Set([
|
||||||
"gemini",
|
"gemini",
|
||||||
@ -869,6 +879,257 @@ function isScaleAdjustOutsidePixelChanged(params: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScaleAdjustRawRgbDiff(raw: Buffer, leftIndex: number, rightIndex: number) {
|
||||||
|
return (
|
||||||
|
Math.abs(raw[leftIndex] - raw[rightIndex]) * 0.299 +
|
||||||
|
Math.abs(raw[leftIndex + 1] - raw[rightIndex + 1]) * 0.587 +
|
||||||
|
Math.abs(raw[leftIndex + 2] - raw[rightIndex + 2]) * 0.114
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScaleAdjustPixelChanged(params: {
|
||||||
|
generatedRaw: Buffer;
|
||||||
|
index: number;
|
||||||
|
sourceRaw: Buffer;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
Math.abs(params.generatedRaw[params.index] - params.sourceRaw[params.index]) *
|
||||||
|
0.299 +
|
||||||
|
Math.abs(
|
||||||
|
params.generatedRaw[params.index + 1] -
|
||||||
|
params.sourceRaw[params.index + 1]
|
||||||
|
) *
|
||||||
|
0.587 +
|
||||||
|
Math.abs(
|
||||||
|
params.generatedRaw[params.index + 2] -
|
||||||
|
params.sourceRaw[params.index + 2]
|
||||||
|
) *
|
||||||
|
0.114
|
||||||
|
) >= SCALE_ADJUST_RECTANGULAR_SEAM_CHANGE_THRESHOLD;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeScaleAdjustSeamLine(params: {
|
||||||
|
axis: "horizontal" | "vertical";
|
||||||
|
boxIndex: number;
|
||||||
|
changeMask: Uint8Array;
|
||||||
|
generatedRaw: Buffer;
|
||||||
|
height: number;
|
||||||
|
position: number;
|
||||||
|
rangeEnd: number;
|
||||||
|
rangeStart: number;
|
||||||
|
sourceRaw: Buffer;
|
||||||
|
width: number;
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
axis,
|
||||||
|
boxIndex,
|
||||||
|
changeMask,
|
||||||
|
generatedRaw,
|
||||||
|
height,
|
||||||
|
position,
|
||||||
|
rangeEnd,
|
||||||
|
rangeStart,
|
||||||
|
sourceRaw,
|
||||||
|
width,
|
||||||
|
} = params;
|
||||||
|
let transitionCount = 0;
|
||||||
|
let currentRun = 0;
|
||||||
|
let maxRun = 0;
|
||||||
|
let edgeDiffSum = 0;
|
||||||
|
const sampleCount = Math.max(1, rangeEnd - rangeStart);
|
||||||
|
|
||||||
|
for (let offset = rangeStart; offset < rangeEnd; offset += 1) {
|
||||||
|
const leftX = axis === "vertical" ? position - 1 : offset;
|
||||||
|
const leftY = axis === "vertical" ? offset : position - 1;
|
||||||
|
const rightX = axis === "vertical" ? position : offset;
|
||||||
|
const rightY = axis === "vertical" ? offset : position;
|
||||||
|
if (
|
||||||
|
leftX < 0 ||
|
||||||
|
rightX < 0 ||
|
||||||
|
leftY < 0 ||
|
||||||
|
rightY < 0 ||
|
||||||
|
leftX >= width ||
|
||||||
|
rightX >= width ||
|
||||||
|
leftY >= height ||
|
||||||
|
rightY >= height
|
||||||
|
) {
|
||||||
|
currentRun = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leftPixel = leftY * width + leftX;
|
||||||
|
const rightPixel = rightY * width + rightX;
|
||||||
|
const changedBoundary =
|
||||||
|
changeMask[leftPixel] !== changeMask[rightPixel];
|
||||||
|
const leftRawIndex = leftPixel * 3;
|
||||||
|
const rightRawIndex = rightPixel * 3;
|
||||||
|
const generatedEdgeDiff = getScaleAdjustRawRgbDiff(
|
||||||
|
generatedRaw,
|
||||||
|
leftRawIndex,
|
||||||
|
rightRawIndex
|
||||||
|
);
|
||||||
|
const sourceEdgeDiff = getScaleAdjustRawRgbDiff(
|
||||||
|
sourceRaw,
|
||||||
|
leftRawIndex,
|
||||||
|
rightRawIndex
|
||||||
|
);
|
||||||
|
const likelyBackgroundSeam =
|
||||||
|
generatedEdgeDiff >= SCALE_ADJUST_RECTANGULAR_SEAM_MIN_EDGE_DIFF &&
|
||||||
|
generatedEdgeDiff <= SCALE_ADJUST_RECTANGULAR_SEAM_MAX_EDGE_DIFF &&
|
||||||
|
generatedEdgeDiff - sourceEdgeDiff >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_EDGE_INCREASE;
|
||||||
|
|
||||||
|
if (changedBoundary && likelyBackgroundSeam) {
|
||||||
|
transitionCount += 1;
|
||||||
|
currentRun += 1;
|
||||||
|
maxRun = Math.max(maxRun, currentRun);
|
||||||
|
edgeDiffSum += generatedEdgeDiff;
|
||||||
|
} else {
|
||||||
|
currentRun = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
axis,
|
||||||
|
boxIndex,
|
||||||
|
edgeDiffMean: transitionCount > 0 ? edgeDiffSum / transitionCount : 0,
|
||||||
|
maxRun,
|
||||||
|
maxRunRatio: maxRun / sampleCount,
|
||||||
|
position,
|
||||||
|
sampleCount,
|
||||||
|
transitionCount,
|
||||||
|
transitionRatio: transitionCount / sampleCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getScaleAdjustRectangularSeamStats(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 changeMask = new Uint8Array(sourceWidth * sourceHeight);
|
||||||
|
for (let pixel = 0; pixel < sourceWidth * sourceHeight; pixel += 1) {
|
||||||
|
const rawIndex = pixel * 3;
|
||||||
|
changeMask[pixel] = getScaleAdjustPixelChanged({
|
||||||
|
generatedRaw,
|
||||||
|
index: rawIndex,
|
||||||
|
sourceRaw,
|
||||||
|
})
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protectedBoxes = getScaleAdjustProtectedRegions(verification).map((region) =>
|
||||||
|
rectToPixelBox({
|
||||||
|
height: sourceHeight,
|
||||||
|
region,
|
||||||
|
width: sourceWidth,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const lineStep = Math.max(
|
||||||
|
1,
|
||||||
|
Math.floor(Math.min(sourceWidth, sourceHeight) / 1400)
|
||||||
|
);
|
||||||
|
let bestVertical: ReturnType<typeof summarizeScaleAdjustSeamLine> | null = null;
|
||||||
|
let bestHorizontal: ReturnType<typeof summarizeScaleAdjustSeamLine> | null = null;
|
||||||
|
|
||||||
|
for (const [boxIndex, box] of protectedBoxes.entries()) {
|
||||||
|
const left = Math.max(2, box.left + 2);
|
||||||
|
const top = Math.max(2, box.top + 2);
|
||||||
|
const right = Math.min(sourceWidth - 2, box.left + box.width - 2);
|
||||||
|
const bottom = Math.min(sourceHeight - 2, box.top + box.height - 2);
|
||||||
|
if (right <= left || bottom <= top) continue;
|
||||||
|
|
||||||
|
for (let x = left; x <= right; x += lineStep) {
|
||||||
|
const stats = summarizeScaleAdjustSeamLine({
|
||||||
|
axis: "vertical",
|
||||||
|
boxIndex,
|
||||||
|
changeMask,
|
||||||
|
generatedRaw,
|
||||||
|
height: sourceHeight,
|
||||||
|
position: x,
|
||||||
|
rangeEnd: bottom,
|
||||||
|
rangeStart: top,
|
||||||
|
sourceRaw,
|
||||||
|
width: sourceWidth,
|
||||||
|
});
|
||||||
|
if (!bestVertical || stats.maxRunRatio > bestVertical.maxRunRatio) {
|
||||||
|
bestVertical = stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let y = top; y <= bottom; y += lineStep) {
|
||||||
|
const stats = summarizeScaleAdjustSeamLine({
|
||||||
|
axis: "horizontal",
|
||||||
|
boxIndex,
|
||||||
|
changeMask,
|
||||||
|
generatedRaw,
|
||||||
|
height: sourceHeight,
|
||||||
|
position: y,
|
||||||
|
rangeEnd: right,
|
||||||
|
rangeStart: left,
|
||||||
|
sourceRaw,
|
||||||
|
width: sourceWidth,
|
||||||
|
});
|
||||||
|
if (!bestHorizontal || stats.maxRunRatio > bestHorizontal.maxRunRatio) {
|
||||||
|
bestHorizontal = stats;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verticalPair =
|
||||||
|
bestVertical &&
|
||||||
|
bestVertical.maxRunRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_RUN_RATIO &&
|
||||||
|
bestVertical.transitionRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_TRANSITION_RATIO;
|
||||||
|
const horizontalPair =
|
||||||
|
bestHorizontal &&
|
||||||
|
bestHorizontal.maxRunRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_RUN_RATIO &&
|
||||||
|
bestHorizontal.transitionRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_PAIR_TRANSITION_RATIO;
|
||||||
|
const verticalStrong =
|
||||||
|
bestVertical &&
|
||||||
|
bestVertical.maxRunRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_RUN_RATIO &&
|
||||||
|
bestVertical.transitionRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_TRANSITION_RATIO;
|
||||||
|
const horizontalStrong =
|
||||||
|
bestHorizontal &&
|
||||||
|
bestHorizontal.maxRunRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_RUN_RATIO &&
|
||||||
|
bestHorizontal.transitionRatio >=
|
||||||
|
SCALE_ADJUST_RECTANGULAR_SEAM_STRONG_TRANSITION_RATIO;
|
||||||
|
|
||||||
|
return {
|
||||||
|
detected: Boolean((verticalPair && horizontalPair) || verticalStrong || horizontalStrong),
|
||||||
|
horizontal: bestHorizontal,
|
||||||
|
lineStep,
|
||||||
|
protectedBoxCount: protectedBoxes.length,
|
||||||
|
vertical: bestVertical,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function getScaleAdjustFootprintChangeStats(params: {
|
async function getScaleAdjustFootprintChangeStats(params: {
|
||||||
generatedBuffer: Buffer;
|
generatedBuffer: Buffer;
|
||||||
sourceBuffer: Buffer;
|
sourceBuffer: Buffer;
|
||||||
@ -1232,6 +1493,10 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
outsidePixelStats: Awaited<
|
outsidePixelStats: Awaited<
|
||||||
ReturnType<typeof getScaleAdjustOutsidePixelChangeStats>
|
ReturnType<typeof getScaleAdjustOutsidePixelChangeStats>
|
||||||
>;
|
>;
|
||||||
|
rectangularSeamDetected: boolean;
|
||||||
|
rectangularSeamStats: Awaited<
|
||||||
|
ReturnType<typeof getScaleAdjustRectangularSeamStats>
|
||||||
|
>;
|
||||||
}> = [];
|
}> = [];
|
||||||
for (const [index, image] of images.entries()) {
|
for (const [index, image] of images.entries()) {
|
||||||
const generatedUrl = image.originalUrl || image.thumbnailUrl;
|
const generatedUrl = image.originalUrl || image.thumbnailUrl;
|
||||||
@ -1243,6 +1508,7 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
outsideDiffScores,
|
outsideDiffScores,
|
||||||
outsidePixelStats,
|
outsidePixelStats,
|
||||||
footprintStats,
|
footprintStats,
|
||||||
|
rectangularSeamStats,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getScaleAdjustDiffScores({
|
getScaleAdjustDiffScores({
|
||||||
generatedBuffer,
|
generatedBuffer,
|
||||||
@ -1279,6 +1545,13 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
sourceWidth: sourceImage.sourceWidth,
|
sourceWidth: sourceImage.sourceWidth,
|
||||||
verification,
|
verification,
|
||||||
}),
|
}),
|
||||||
|
getScaleAdjustRectangularSeamStats({
|
||||||
|
generatedBuffer,
|
||||||
|
sourceBuffer: sourceImage.sourceBuffer,
|
||||||
|
sourceHeight: sourceImage.sourceHeight,
|
||||||
|
sourceWidth: sourceImage.sourceWidth,
|
||||||
|
verification,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
const visuallyChangedInside = changeDiffScores.some(
|
const visuallyChangedInside = changeDiffScores.some(
|
||||||
(score) => score >= SCALE_ADJUST_VISUAL_DIFF_THRESHOLD
|
(score) => score >= SCALE_ADJUST_VISUAL_DIFF_THRESHOLD
|
||||||
@ -1287,6 +1560,7 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
const outsideChanged =
|
const outsideChanged =
|
||||||
isScaleAdjustOutsideChanged(outsideDiffScores) ||
|
isScaleAdjustOutsideChanged(outsideDiffScores) ||
|
||||||
isScaleAdjustOutsidePixelChanged(outsidePixelStats);
|
isScaleAdjustOutsidePixelChanged(outsidePixelStats);
|
||||||
|
const rectangularSeamDetected = rectangularSeamStats.detected;
|
||||||
|
|
||||||
console.info("[Canvas Generate Image] Scale adjust result guard:", {
|
console.info("[Canvas Generate Image] Scale adjust result guard:", {
|
||||||
debugLabel,
|
debugLabel,
|
||||||
@ -1320,6 +1594,38 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
Number(score.toFixed(2))
|
Number(score.toFixed(2))
|
||||||
),
|
),
|
||||||
protectedRegionCount: verification.protectedRegions?.length || 0,
|
protectedRegionCount: verification.protectedRegions?.length || 0,
|
||||||
|
rectangularSeamDetected,
|
||||||
|
rectangularSeamStats: {
|
||||||
|
horizontal: rectangularSeamStats.horizontal
|
||||||
|
? {
|
||||||
|
edgeDiffMean: Number(
|
||||||
|
rectangularSeamStats.horizontal.edgeDiffMean.toFixed(2)
|
||||||
|
),
|
||||||
|
maxRunRatio: Number(
|
||||||
|
rectangularSeamStats.horizontal.maxRunRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
position: rectangularSeamStats.horizontal.position,
|
||||||
|
transitionRatio: Number(
|
||||||
|
rectangularSeamStats.horizontal.transitionRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
lineStep: rectangularSeamStats.lineStep,
|
||||||
|
vertical: rectangularSeamStats.vertical
|
||||||
|
? {
|
||||||
|
edgeDiffMean: Number(
|
||||||
|
rectangularSeamStats.vertical.edgeDiffMean.toFixed(2)
|
||||||
|
),
|
||||||
|
maxRunRatio: Number(
|
||||||
|
rectangularSeamStats.vertical.maxRunRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
position: rectangularSeamStats.vertical.position,
|
||||||
|
transitionRatio: Number(
|
||||||
|
rectangularSeamStats.vertical.transitionRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
regionCount: verification.regions.length,
|
regionCount: verification.regions.length,
|
||||||
regionDiffScores: diffScores.map((score) => Number(score.toFixed(2))),
|
regionDiffScores: diffScores.map((score) => Number(score.toFixed(2))),
|
||||||
scalePercent: verification.scalePercent ?? null,
|
scalePercent: verification.scalePercent ?? null,
|
||||||
@ -1329,6 +1635,7 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
const guardWarnings = [
|
const guardWarnings = [
|
||||||
...(outsideChanged ? ["outside-frame-changed"] : []),
|
...(outsideChanged ? ["outside-frame-changed"] : []),
|
||||||
...(!changedInside ? ["no-effective-scale-change"] : []),
|
...(!changedInside ? ["no-effective-scale-change"] : []),
|
||||||
|
...(rectangularSeamDetected ? ["rectangular-seam-detected"] : []),
|
||||||
];
|
];
|
||||||
if (guardWarnings.length > 0) {
|
if (guardWarnings.length > 0) {
|
||||||
console.warn("[Canvas Generate Image] Scale adjust result guard warning; keeping result for preview:", {
|
console.warn("[Canvas Generate Image] Scale adjust result guard warning; keeping result for preview:", {
|
||||||
@ -1360,6 +1667,37 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
sampledPixelCount: outsidePixelStats.sampledPixelCount,
|
sampledPixelCount: outsidePixelStats.sampledPixelCount,
|
||||||
step: outsidePixelStats.step,
|
step: outsidePixelStats.step,
|
||||||
},
|
},
|
||||||
|
rectangularSeamDetected,
|
||||||
|
rectangularSeamStats: {
|
||||||
|
horizontal: rectangularSeamStats.horizontal
|
||||||
|
? {
|
||||||
|
edgeDiffMean: Number(
|
||||||
|
rectangularSeamStats.horizontal.edgeDiffMean.toFixed(2)
|
||||||
|
),
|
||||||
|
maxRunRatio: Number(
|
||||||
|
rectangularSeamStats.horizontal.maxRunRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
position: rectangularSeamStats.horizontal.position,
|
||||||
|
transitionRatio: Number(
|
||||||
|
rectangularSeamStats.horizontal.transitionRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
vertical: rectangularSeamStats.vertical
|
||||||
|
? {
|
||||||
|
edgeDiffMean: Number(
|
||||||
|
rectangularSeamStats.vertical.edgeDiffMean.toFixed(2)
|
||||||
|
),
|
||||||
|
maxRunRatio: Number(
|
||||||
|
rectangularSeamStats.vertical.maxRunRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
position: rectangularSeamStats.vertical.position,
|
||||||
|
transitionRatio: Number(
|
||||||
|
rectangularSeamStats.vertical.transitionRatio.toFixed(3)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
},
|
||||||
changeRegionDiffScores: changeDiffScores.map((score) =>
|
changeRegionDiffScores: changeDiffScores.map((score) =>
|
||||||
Number(score.toFixed(2))
|
Number(score.toFixed(2))
|
||||||
),
|
),
|
||||||
@ -1392,6 +1730,8 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
outsideChanged,
|
outsideChanged,
|
||||||
outsideDiffScores,
|
outsideDiffScores,
|
||||||
outsidePixelStats,
|
outsidePixelStats,
|
||||||
|
rectangularSeamDetected,
|
||||||
|
rectangularSeamStats,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1414,6 +1754,8 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
outsideChanged,
|
outsideChanged,
|
||||||
outsideDiffScores,
|
outsideDiffScores,
|
||||||
outsidePixelStats,
|
outsidePixelStats,
|
||||||
|
rectangularSeamDetected,
|
||||||
|
rectangularSeamStats,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1424,11 +1766,20 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
const outsideMean = item.outsidePixelStats.meanDiff;
|
const outsideMean = item.outsidePixelStats.meanDiff;
|
||||||
const outsidePenalty = item.outsideChanged ? 100 : 0;
|
const outsidePenalty = item.outsideChanged ? 100 : 0;
|
||||||
const noChangePenalty = item.changedInside ? 0 : 60;
|
const noChangePenalty = item.changedInside ? 0 : 60;
|
||||||
return insideScore - outsideMean * 0.35 - outsidePenalty - noChangePenalty;
|
const seamPenalty = item.rectangularSeamDetected ? 140 : 0;
|
||||||
|
return (
|
||||||
|
insideScore -
|
||||||
|
outsideMean * 0.35 -
|
||||||
|
outsidePenalty -
|
||||||
|
noChangePenalty -
|
||||||
|
seamPenalty
|
||||||
|
);
|
||||||
};
|
};
|
||||||
const rankedImages = [...evaluatedImages].sort((left, right) => {
|
const rankedImages = [...evaluatedImages].sort((left, right) => {
|
||||||
const leftPassed = left.changedInside && !left.outsideChanged;
|
const leftPassed =
|
||||||
const rightPassed = right.changedInside && !right.outsideChanged;
|
left.changedInside && !left.outsideChanged && !left.rectangularSeamDetected;
|
||||||
|
const rightPassed =
|
||||||
|
right.changedInside && !right.outsideChanged && !right.rectangularSeamDetected;
|
||||||
if (leftPassed !== rightPassed) return leftPassed ? -1 : 1;
|
if (leftPassed !== rightPassed) return leftPassed ? -1 : 1;
|
||||||
return scoreEvaluation(right) - scoreEvaluation(left);
|
return scoreEvaluation(right) - scoreEvaluation(left);
|
||||||
});
|
});
|
||||||
@ -1438,9 +1789,13 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
debugLabel,
|
debugLabel,
|
||||||
candidateCount: evaluatedImages.length,
|
candidateCount: evaluatedImages.length,
|
||||||
imageIndex: selected.index,
|
imageIndex: selected.index,
|
||||||
passed: selected.changedInside && !selected.outsideChanged,
|
passed:
|
||||||
|
selected.changedInside &&
|
||||||
|
!selected.outsideChanged &&
|
||||||
|
!selected.rectangularSeamDetected,
|
||||||
changedInside: selected.changedInside,
|
changedInside: selected.changedInside,
|
||||||
outsideChanged: selected.outsideChanged,
|
outsideChanged: selected.outsideChanged,
|
||||||
|
rectangularSeamDetected: selected.rectangularSeamDetected,
|
||||||
changeRegionDiffScores: selected.changeDiffScores.map((score) =>
|
changeRegionDiffScores: selected.changeDiffScores.map((score) =>
|
||||||
Number(score.toFixed(2))
|
Number(score.toFixed(2))
|
||||||
),
|
),
|
||||||
@ -1473,12 +1828,16 @@ async function applyScaleAdjustVerificationToImages(params: {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
verification.rejectFailedCandidates &&
|
verification.rejectFailedCandidates &&
|
||||||
(!selected.changedInside || (selected.outsideChanged && !outsideChangeAccepted))
|
(!selected.changedInside ||
|
||||||
|
selected.rectangularSeamDetected ||
|
||||||
|
(selected.outsideChanged && !outsideChangeAccepted))
|
||||||
) {
|
) {
|
||||||
throw new ScaleAdjustOutputRejectedError({
|
throw new ScaleAdjustOutputRejectedError({
|
||||||
reason: selected.outsideChanged
|
reason: selected.rectangularSeamDetected
|
||||||
? "outside-frame-changed"
|
? "rectangular-seam-detected"
|
||||||
: "no-effective-scale-change",
|
: selected.outsideChanged
|
||||||
|
? "outside-frame-changed"
|
||||||
|
: "no-effective-scale-change",
|
||||||
diffScores: selected.changeDiffScores,
|
diffScores: selected.changeDiffScores,
|
||||||
images: [selected.cleanedImage || selected.image],
|
images: [selected.cleanedImage || selected.image],
|
||||||
outsideDiffScores: selected.outsideDiffScores,
|
outsideDiffScores: selected.outsideDiffScores,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user