fix: exclude protected people from furniture scale masks

This commit is contained in:
134 2026-07-04 18:58:30 +08:00
parent 970bd89638
commit fa7d9d81ff

View File

@ -1356,6 +1356,24 @@ function isScaleAdjustFurnitureProductLikePrompt(prompt: string) {
); );
} }
type ScaleAdjustProtectedPersonSide = "auto" | "left" | "right";
function getScaleAdjustProtectedPersonSide(
prompt: string
): ScaleAdjustProtectedPersonSide | null {
const text = normalizeScaleAdjustModelText(prompt);
if (
!/person|people|human|woman|man|model|girl|boy|face|body|人物|人像|人|模特|女人|男人|女孩|男孩|脸|身体/.test(
text
)
) {
return null;
}
if (/right|右侧|右边|右方|右/.test(text)) return "right";
if (/left|左侧|左边|左方|左/.test(text)) return "left";
return "auto";
}
function hasScaleAdjustPersonPixelSignal(params: { function hasScaleAdjustPersonPixelSignal(params: {
box: { left: number; top: number; width: number; height: number }; box: { left: number; top: number; width: number; height: number };
raw: Buffer; raw: Buffer;
@ -1550,11 +1568,61 @@ function buildScaleAdjustStructureInitialMask(params: {
}); });
} }
function buildScaleAdjustProtectedPersonSeedMask(params: {
box: { left: number; top: number; width: number; height: number };
raw: Buffer;
side: ScaleAdjustProtectedPersonSide;
width: number;
}) {
const { box, raw, side, width } = params;
const seed = new Uint8Array(box.width * box.height);
let seedCount = 0;
const sideAllowsX = (localX: number) => {
if (side === "right") return localX >= box.width * 0.52;
if (side === "left") return localX <= box.width * 0.48;
return true;
};
for (let localY = 0; localY < box.height; localY += 1) {
const y = box.top + localY;
for (let localX = 0; localX < box.width; localX += 1) {
if (!sideAllowsX(localX)) continue;
const x = box.left + localX;
const rawIndex = getScaleAdjustRawPixelIndex(width, x, y);
const r = raw[rawIndex] || 0;
const g = raw[rawIndex + 1] || 0;
const b = raw[rawIndex + 2] || 0;
const { luma, saturation } = getScaleAdjustPixelStats(r, g, b);
const likelySkin = isScaleAdjustLikelySkinPixel(r, g, b);
const likelyWhiteFabric =
isScaleAdjustLikelyWhiteFabricPixel(r, g, b, luma, saturation) &&
luma < 246 &&
saturation < 0.22;
if (!likelySkin && !likelyWhiteFabric) continue;
seed[localY * box.width + localX] = 255;
seedCount += 1;
}
}
const seedRatio = seedCount / Math.max(1, box.width * box.height);
if (seedCount < 24 || seedRatio < 0.0006) return null;
return dilateScaleAdjustStructureMask({
height: box.height,
mask: seed,
radius: Math.max(4, Math.round(Math.min(box.width, box.height) * 0.032)),
width: box.width,
});
}
function filterScaleAdjustStructureComponents(params: { function filterScaleAdjustStructureComponents(params: {
box: { width: number; height: number }; box: { width: number; height: number };
mask: Uint8Array; mask: Uint8Array;
protectedPersonMask?: Uint8Array | null;
protectedPersonSide?: ScaleAdjustProtectedPersonSide | null;
}) { }) {
const { box, mask } = params; const { box, mask, protectedPersonMask = null, protectedPersonSide = null } =
params;
const width = box.width; const width = box.width;
const height = box.height; const height = box.height;
const visited = new Uint8Array(mask.length); const visited = new Uint8Array(mask.length);
@ -1582,6 +1650,7 @@ function filterScaleAdjustStructureComponents(params: {
let minY = y; let minY = y;
let maxX = x; let maxX = x;
let maxY = y; let maxY = y;
let protectedPersonOverlap = 0;
while (head < queueX.length) { while (head < queueX.length) {
const cx = queueX[head] || 0; const cx = queueX[head] || 0;
@ -1592,6 +1661,9 @@ function filterScaleAdjustStructureComponents(params: {
minY = Math.min(minY, cy); minY = Math.min(minY, cy);
maxX = Math.max(maxX, cx); maxX = Math.max(maxX, cx);
maxY = Math.max(maxY, cy); maxY = Math.max(maxY, cy);
if (protectedPersonMask?.[cy * width + cx]) {
protectedPersonOverlap += 1;
}
for (let dy = -1; dy <= 1; dy += 1) { for (let dy = -1; dy <= 1; dy += 1) {
for (let dx = -1; dx <= 1; dx += 1) { for (let dx = -1; dx <= 1; dx += 1) {
@ -1616,7 +1688,27 @@ function filterScaleAdjustStructureComponents(params: {
const centerYRatio = (minY + componentHeight / 2) / height; const centerYRatio = (minY + componentHeight / 2) / height;
const touchesTop = minY <= height * 0.04; const touchesTop = minY <= height * 0.04;
const touchesRight = maxX >= width * 0.88; const touchesRight = maxX >= width * 0.88;
const touchesLeft = minX <= width * 0.12;
const edgeSliver = touchesRight && componentWidth < width * 0.18; const edgeSliver = touchesRight && componentWidth < width * 0.18;
const touchesProtectedPersonSide =
protectedPersonSide === "right"
? maxX >= width * 0.68
: protectedPersonSide === "left"
? minX <= width * 0.32
: protectedPersonSide === "auto"
? touchesLeft || touchesRight
: false;
const protectedPersonOverlapRatio =
protectedPersonOverlap / Math.max(1, area);
const likelyProtectedPersonComponent =
Boolean(protectedPersonMask) &&
protectedPersonOverlap > 0 &&
(protectedPersonOverlapRatio >= 0.012 ||
(touchesProtectedPersonSide &&
protectedPersonOverlap >= Math.max(6, area * 0.002))) &&
(touchesProtectedPersonSide ||
componentHeight >= height * 0.16 ||
componentWidth >= width * 0.16);
const broadTopBackground = const broadTopBackground =
touchesTop && touchesTop &&
centerYRatio <= 0.45 && centerYRatio <= 0.45 &&
@ -1642,7 +1734,8 @@ function filterScaleAdjustStructureComponents(params: {
likelyStructure && likelyStructure &&
centeredEnough && centeredEnough &&
!edgeSliver && !edgeSliver &&
!broadTopBackground !broadTopBackground &&
!likelyProtectedPersonComponent
) { ) {
if (fillSurfaceComponent) { if (fillSurfaceComponent) {
for (let fillY = minY; fillY <= maxY; fillY += 1) { for (let fillY = minY; fillY <= maxY; fillY += 1) {
@ -2446,6 +2539,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
includePersonPixels?: boolean; includePersonPixels?: boolean;
placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>; placement: ReturnType<typeof getScaleAdjustVisualGuidePlacement>;
preferPixelStructure?: boolean; preferPixelStructure?: boolean;
protectedPersonSide?: ScaleAdjustProtectedPersonSide | null;
raw: Buffer; raw: Buffer;
sourceBuffer: Buffer; sourceBuffer: Buffer;
sourceImageUrl?: string; sourceImageUrl?: string;
@ -2456,6 +2550,7 @@ async function buildScaleAdjustStructureLockedComposite(params: {
includePersonPixels = false, includePersonPixels = false,
placement, placement,
preferPixelStructure = false, preferPixelStructure = false,
protectedPersonSide = null,
raw, raw,
sourceBuffer, sourceBuffer,
sourceImageUrl, sourceImageUrl,
@ -2487,9 +2582,20 @@ async function buildScaleAdjustStructureLockedComposite(params: {
raw, raw,
width: sourceWidth, width: sourceWidth,
}); });
const protectedPersonMask =
preferPixelStructure && protectedPersonSide
? buildScaleAdjustProtectedPersonSeedMask({
box: placement.sourceBox,
raw,
side: protectedPersonSide,
width: sourceWidth,
})
: null;
const mask = filterScaleAdjustStructureComponents({ const mask = filterScaleAdjustStructureComponents({
box: placement.sourceBox, box: placement.sourceBox,
mask: initialMask, mask: initialMask,
protectedPersonMask,
protectedPersonSide,
}); });
const structureBbox = getScaleAdjustMaskBoundingBox({ const structureBbox = getScaleAdjustMaskBoundingBox({
height: placement.sourceBox.height, height: placement.sourceBox.height,
@ -2781,6 +2887,9 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
const preferPixelStructure = isScaleAdjustFurnitureProductLikePrompt( const preferPixelStructure = isScaleAdjustFurnitureProductLikePrompt(
region.prompt region.prompt
); );
const protectedPersonSide = preferPixelStructure
? getScaleAdjustProtectedPersonSide(region.prompt)
: null;
const hasPersonPixels = const hasPersonPixels =
!preferPixelStructure && !preferPixelStructure &&
!!sourceRaw && !!sourceRaw &&
@ -2797,6 +2906,7 @@ async function buildScaleAdjustVisualLayoutGuideBuffer(params: {
includePersonPixels, includePersonPixels,
placement, placement,
preferPixelStructure, preferPixelStructure,
protectedPersonSide,
raw: sourceRaw, raw: sourceRaw,
sourceBuffer, sourceBuffer,
sourceImageUrl, sourceImageUrl,