| Server IP : 217.154.3.148 / Your IP : 216.73.216.90 Web Server : nginx/1.30.3 System : Linux blissful-wright.217-154-3-148.plesk.page 6.8.0-124-generic #124-Ubuntu SMP PREEMPT_DYNAMIC Tue May 26 13:00:45 UTC 2026 x86_64 User : gracious-boyd_hfhh1h8vo9n ( 10000) PHP Version : 8.3.31 Disable Function : opcache_get_status MySQL : OFF | cURL : ON | WGET : OFF | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/vhosts/gracious-boyd.217-154-3-148.plesk.page/nextjs/app/api/analyze/ |
Upload File : |
import { NextRequest, NextResponse } from "next/server";
export const runtime = "nodejs";
import sharp from "sharp";
import path from "node:path";
import crypto from "node:crypto";
import fs from "node:fs/promises";
import { spawn } from "node:child_process";
import { runPeriodicAnalysis } from "@/lib/analyzers";
import { refineGridRansac } from "@/lib/ransac_grid";
import { estimateDepth, type DepthMap } from "@/lib/depth/depth-anything";
import { refineOccluderMask, type RefinedOccluderResult } from "@/lib/segmentation/occluder";
import { getOccluderDefaults } from "@/lib/occluder/defaults";
import {
normalizeWithPreset,
resolvePresetName,
type MaskPresetName,
type NormalizeMaskResponse,
type NormalizeOptions,
} from "@/lib/mask/normalize";
import { runMaskPipeline } from "@/lib/masks/pipeline";
import type { GridContext, PipelineFlags } from "@/lib/masks/types";
import { thresholdMask } from "@/lib/masks/lattice";
import {
keepSeedDrivenComponents,
suppressTileLeak,
overlapWithSeeds,
connectedComponents,
distanceToNearestLattice,
} from "@/lib/masks/components";
import { morphDilate, morphErode, maskOr, maskAnd } from "@/utils/maskOps";
import type { TileSegmentationResult } from "@/lib/segmentation/tile-unet";
import { detectTileBand, detectTileBandFromTilesBinary, type TileBand } from "@/lib/segmentation/tile-mask";
// Lazy-load segmentation only when requested to avoid failing on hosts
// without onnxruntime CPU provider. We import inside the handler.
const OCCLUDER_ENV_ALLOWLIST = new Set<string>([
"OCCLUDER_METHOD",
"OCCLUDER_SAM_MAX_LEAK",
"OCCLUDER_SAM_LEAK_WEIGHT",
"OCCLUDER_SAM_BAND_RADIUS",
"OCCLUDER_SAM_NEG_ERODE",
"OCCLUDER_SAM_NEG_Q",
"OCCLUDER_SAM_POS_GRAD_Q",
"OCCLUDER_SAM_POS_BAND_ONLY",
"OCCLUDER_SAM_MIN_OVERLAP",
"OCCLUDER_SAM_MIN_OUTSIDE",
"OCCLUDER_SAM_BOX_MARGIN",
"OCCLUDER_SAM_MAX_TILE_RATIO",
"OCCLUDER_SAM_TOP_K",
"OCCLUDER_SAM_EDGE_WEIGHT",
"OCCLUDER_SAM_EDGE_RADIUS",
"OCCLUDER_SAM_DEPTH_CLIP",
"OCCLUDER_SAM_DEPTH_CLIP_DILATE",
"OCCLUDER_SAM_DEPTH_CLIP_MIN_FRAC",
"OCCLUDER_SAM_DEPTH_CLIP_MIN_COVERAGE",
"OCCLUDER_SAM_PATCH_FNS",
"OCCLUDER_SAM_PATCH_PY",
"OCCLUDER_SAM_PATCH_MODEL",
"OCCLUDER_SAM_PATCH_BOXES",
"OCCLUDER_SAM_PATCH_MARGIN",
"OCCLUDER_SAM_PATCH_DEVICE",
"OCCLUDER_SAM_PATCH_TIMEOUT_MS",
"OCCLUDER_SAM_PATCH_TILE_BAND_ONLY",
"OCCLUDER_SAM_PATCH_TILE_BAND_PAD",
"OCCLUDER_SAM_PATCH_TILE_DILATE",
"OCCLUDER_SAM_PATCH_MIN_FN_COVERAGE",
"OCCLUDER_SAM_PATCH_REQUIRE_TILE",
"OCCLUDER_SAM_PATCH_MIN_COMP",
"OCCLUDER_SAM_PATCH_MAX_COMP",
"OCCLUDER_SAM_PATCH_MAX_COMP_FRAC",
"OCCLUDER_SAM_PATCH_USE_BACK",
"OCCLUDER_SAM_PATCH_UNION_BACK",
"OCCLUDER_SAM_PATCH_GROW",
"OCCLUDER_SAM_PATCH_MAX_COVERAGE",
"OCCLUDER_SAM_PATCH_MAX_ADD_FRAC",
"OCCLUDER_SAM_PATCH_MAX_ADD_PIXELS",
"OCCLUDER_SAM_PATCH_MAX_ADD_PIXELS_7XM",
"OCCLUDER_SAM_PATCH_ADJ_PX",
"OCCLUDER_FN_ASSIST_UNION",
"OCCLUDER_FN_ASSIST_WITHOUT_SAM",
"OCCLUDER_FN_ASSIST_MIN_COMP",
"OCCLUDER_FN_ASSIST_MAX_COMP",
"OCCLUDER_FN_ASSIST_ADJ_PX",
"OCCLUDER_FN_ASSIST_TILE_ONLY",
"OCCLUDER_DISTILL_MIN_COVERAGE",
"OCCLUDER_DISTILL_MAX_COVERAGE",
"OCCLUDER_DISTILL_MAX_TRANS_DENSITY",
"OCCLUDER_DISTILL_MAX_TILE_LEAK",
"OCCLUDER_DISTILL_BOTTOM_STRIP_FRAC",
"OCCLUDER_DISTILL_MIN_COMP_FRAC",
"OCCLUDER_DISTILL_BIG_COMP_FRAC",
"OCCLUDER_DISTILL_MIN_PROXY",
"OCCLUDER_PATCH_REFINE",
"OCCLUDER_PATCH_TOP_N",
"OCCLUDER_PATCH_MODEL",
"OCCLUDER_PATCH_DEVICE",
"OCCLUDER_PIPELINE_FALLBACK_MODE",
]);
type OccluderEnvOverrides = Record<string, string>;
function parseOccluderEnvOverrides(raw: string | null): OccluderEnvOverrides | null {
if (!raw) return null;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
return null;
}
if (!parsed || typeof parsed !== "object") return null;
const env: OccluderEnvOverrides = {};
for (const [key, value] of Object.entries(parsed as Record<string, unknown>)) {
if (!OCCLUDER_ENV_ALLOWLIST.has(key)) continue;
if (value == null) continue;
env[key] = String(value);
}
return Object.keys(env).length ? env : null;
}
function applyOccluderEnvOverrides(overrides: OccluderEnvOverrides | null): (() => void) | null {
if (!overrides) return null;
const entries = Object.entries(overrides);
if (!entries.length) return null;
const previous = new Map<string, string | undefined>();
for (const [key, value] of entries) {
previous.set(key, process.env[key]);
process.env[key] = String(value);
}
return () => {
for (const [key, value] of previous.entries()) {
if (value == null) {
delete process.env[key];
} else {
process.env[key] = value;
}
}
};
}
function parsePositiveIntEnv(value: string | undefined | null): number | null {
const parsed = Number(value ?? "");
if (!Number.isFinite(parsed) || parsed <= 0) return null;
return Math.max(1, Math.floor(parsed));
}
function parseTimeoutMsEnv(
key: string,
fallbackMs: number,
minMs = 1_000,
maxMs = 10 * 60_000,
): number {
const raw = Number(process.env[key] ?? "");
if (!Number.isFinite(raw) || raw <= 0) return fallbackMs;
return Math.max(minMs, Math.min(maxMs, Math.round(raw)));
}
async function withStepTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
if (!(timeoutMs > 0)) return promise;
return await new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
}, timeoutMs);
promise.then(
(value) => {
clearTimeout(timer);
resolve(value);
},
(err) => {
clearTimeout(timer);
reject(err);
},
);
});
}
function resolveSamPatchMaxAddPixelsCap(sampleName: string | null | undefined): {
cap: number;
scope: "none" | "global" | "sample_7xm";
} {
const normalized = (sampleName ?? "").toLowerCase();
if (normalized.startsWith("7xm")) {
const sampleCap = parsePositiveIntEnv(process.env.OCCLUDER_SAM_PATCH_MAX_ADD_PIXELS_7XM);
if (sampleCap != null) {
return { cap: sampleCap, scope: "sample_7xm" };
}
}
const globalCap = parsePositiveIntEnv(process.env.OCCLUDER_SAM_PATCH_MAX_ADD_PIXELS);
if (globalCap != null) {
return { cap: globalCap, scope: "global" };
}
return { cap: Number.POSITIVE_INFINITY, scope: "none" };
}
function isComfyProfileUpload(name: string | null | undefined): boolean {
const normalized = (name ?? "").trim().toLowerCase();
return normalized === "comfyui_01234_.png";
}
function resolveOccluderPresetBackend(): string {
const value = (process.env.OCCLUDER_BACKEND ?? process.env.OCCLUDER_PRESET ?? "").trim().toLowerCase();
if (value === "rfdet_m3" || value === "comfy_rfdet_m3") return "rfdet_m3";
return "";
}
/** ---------- Utils ---------- */
function clamp(v:number, lo:number, hi:number){ return Math.max(lo, Math.min(hi, v)); }
function toGrayFloat32(rgba: Uint8Array, w: number, h: number): Float32Array {
const out = new Float32Array(w * h);
for (let i = 0, j = 0; i < rgba.length; i += 4, j++) {
out[j] = 0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2];
}
return out;
}
function sobelMag(gray: Float32Array, w: number, h: number): Float32Array {
const out = new Float32Array(w * h);
const gxk = [-1,0,1,-2,0,2,-1,0,1];
const gyk = [-1,-2,-1,0,0,0,1,2,1];
for (let y = 1; y < h - 1; y++) {
for (let x = 1; x < w - 1; x++) {
let gx = 0, gy = 0, k = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++, k++) {
const v = gray[(y + dy) * w + (x + dx)];
gx += gxk[k] * v;
gy += gyk[k] * v;
}
}
out[y * w + x] = Math.hypot(gx, gy);
}
}
return out;
}
function projectXWeighted(mag: Float32Array, weights: Float32Array|null, w: number, h: number): Float32Array {
const s = new Float32Array(w);
if (weights) {
for (let x = 0; x < w; x++) {
let acc = 0;
for (let y = 0; y < h; y++) acc += mag[y*w + x] * weights[y*w + x];
s[x] = acc;
}
} else {
for (let x = 0; x < w; x++) {
let acc = 0;
for (let y = 0; y < h; y++) acc += mag[y*w + x];
s[x] = acc;
}
}
return s;
}
function projectYWeighted(mag: Float32Array, weights: Float32Array|null, w: number, h: number): Float32Array {
const s = new Float32Array(h);
if (weights) {
for (let y = 0; y < h; y++) {
let acc = 0;
for (let x = 0; x < w; x++) acc += mag[y*w + x] * weights[y*w + x];
s[y] = acc;
}
} else {
for (let y = 0; y < h; y++) {
let acc = 0;
for (let x = 0; x < w; x++) acc += mag[y*w + x];
s[y] = acc;
}
}
return s;
}
function autoCorr1D(sig: Float32Array): Float32Array {
const n = sig.length;
const out = new Float32Array(n);
const mean = sig.reduce((a,b)=>a+b,0)/n;
for (let i=0;i<n;i++) sig[i] = sig[i] - mean;
for (let lag=1; lag<n; lag++){
let s=0;
for (let i=0; i+lag<n; i++) s += sig[i]*sig[i+lag];
out[lag] = s;
}
out[0]=0;
return out;
}
function buildTilePlaneMask(tile: TileSegmentationResult | null): Uint8Array | null {
if (!tile || !tile.ids || tile.ids.length === 0) return null;
const classes = tile.classes ?? [];
const byName = (name: string) =>
classes.find((cls) => String(cls.name).toLowerCase() === name.toLowerCase()) ?? null;
const tilesId = byName("tiles")?.id ?? (classes.length > 1 ? 1 : 0);
const groutId = byName("grout")?.id ?? null;
const out = new Uint8Array(tile.ids.length);
for (let i = 0; i < tile.ids.length; i++) {
const cls = tile.ids[i];
if (cls === tilesId || (groutId != null && cls === groutId)) {
out[i] = 255;
}
}
return out;
}
type BandSelection = {
band: TileBand;
source: string;
};
function applyBandToMask(
mask: Uint8Array,
width: number,
height: number,
yMin: number,
yMax: number,
): Uint8Array {
const out = new Uint8Array(mask);
for (let y = 0; y < height; y++) {
if (y >= yMin && y < yMax) continue;
out.fill(0, y * width, (y + 1) * width);
}
return out;
}
function normalizeBandRange(
yMin: number,
yMax: number,
height: number,
minHeight: number,
maxHeight: number,
): { yMin: number; yMax: number; adjusted: boolean } | null {
if (height <= 0) return null;
let y0 = Math.max(0, Math.min(height, Math.floor(yMin)));
let y1 = Math.max(0, Math.min(height, Math.floor(yMax)));
if (y1 <= y0) return null;
let adjusted = false;
let bandHeight = y1 - y0;
if (bandHeight < minHeight) {
const center = Math.round((y0 + y1) / 2);
y0 = Math.max(0, Math.min(height - minHeight, center - Math.floor(minHeight / 2)));
y1 = Math.min(height, y0 + minHeight);
adjusted = true;
bandHeight = y1 - y0;
}
if (bandHeight > maxHeight) {
const center = Math.round((y0 + y1) / 2);
y0 = Math.max(0, Math.min(height - maxHeight, center - Math.floor(maxHeight / 2)));
y1 = Math.min(height, y0 + maxHeight);
adjusted = true;
}
return { yMin: y0, yMax: y1, adjusted };
}
function bandFromNonZeroRows(
mask: Uint8Array,
width: number,
height: number,
): { yMin: number; yMax: number } | null {
if (!(width > 0 && height > 0) || mask.length !== width * height) return null;
let yMin = -1;
let yMax = -1;
for (let y = 0; y < height; y++) {
const row = y * width;
let hit = false;
for (let x = 0; x < width; x++) {
if (mask[row + x]) {
hit = true;
break;
}
}
if (hit) {
if (yMin < 0) yMin = y;
yMax = y;
}
}
if (yMin < 0 || yMax < yMin) return null;
return { yMin, yMax: yMax + 1 };
}
function pickTileBandFromSegmentation(
tile: TileSegmentationResult | null,
promptMask: Uint8Array | null,
width: number,
height: number,
): BandSelection | null {
if (!tile || !tile.ids || !(width > 0 && height > 0)) return null;
const classes = tile.classes ?? [];
const byName = (name: string) =>
classes.find((cls) => String(cls.name).toLowerCase() === name.toLowerCase()) ?? null;
const tilesId = byName("tiles")?.id ?? (classes.length > 1 ? 1 : 0);
const groutId = byName("grout")?.id ?? null;
if (groutId != null) {
const band = detectTileBand(tile.ids, width, height, {
minRowGrout: 0.002,
minRowTilesGrout: 0.01,
smoothWindow: 9,
excludeBottomFrac: 0.15,
groutId,
tilesId,
});
if (band) return { band, source: "tile_band_grout" };
}
if (promptMask) {
const band = detectTileBandFromTilesBinary(promptMask, null, width, height, {
smoothWindow: 11,
excludeBottomFrac: 0.15,
minStartFrac: 0.08,
minRowFracMin: 0.08,
maxRowFracToThreshFrac: 0.5,
peakThresholdFrac: 0.65,
minBandFrac: 0.12,
});
if (band) return { band, source: "tile_band_binary" };
const relaxed = detectTileBandFromTilesBinary(promptMask, null, width, height, {
smoothWindow: 9,
excludeBottomFrac: 0.15,
minStartFrac: 0.06,
minRowFracMin: 0.06,
maxRowFracToThreshFrac: 0.35,
peakThresholdFrac: 0.55,
minBandFrac: 0.1,
});
if (relaxed) return { band: relaxed, source: "tile_band_binary_relaxed" };
}
return null;
}
function bandMaskByRowDensity(
mask: Uint8Array,
width: number,
height: number,
): { mask: Uint8Array; applied: boolean; band: { yMin: number; yMax: number; threshold: number; p80: number; fallback?: boolean; } | null } {
if (width <= 0 || height <= 0 || mask.length !== width * height) {
return { mask, applied: false, band: null };
}
const rowCoverage = new Float32Array(height);
for (let y = 0; y < height; y++) {
const row = y * width;
let count = 0;
for (let x = 0; x < width; x++) {
if (mask[row + x]) count += 1;
}
rowCoverage[y] = count / Math.max(1, width);
}
const sorted = Array.from(rowCoverage).sort((a, b) => a - b);
const p80 = sorted[Math.floor(sorted.length * 0.8)] ?? 0;
const p60 = sorted[Math.floor(sorted.length * 0.6)] ?? 0;
const p50 = sorted[Math.floor(sorted.length * 0.5)] ?? 0;
const minFracEnv = Number(process.env.OCCLUDER_SAM_BAND_MIN_FRAC ?? "0.12");
const baseThreshold = Math.max(0.02, Math.min(0.6, Math.max(minFracEnv, p80 * 0.5)));
const relaxedThreshold = Math.max(0.01, Math.min(baseThreshold, Math.max(p60 * 0.35, p50 * 0.45)));
const relaxedThreshold2 = Math.max(0.005, Math.min(relaxedThreshold, p80 * 0.25));
const thresholdCandidates = [baseThreshold, relaxedThreshold, relaxedThreshold2]
.filter((value, index, arr) => value > 0 && arr.indexOf(value) === index);
let bestStart = -1;
let bestEnd = -1;
let thresholdUsed = baseThreshold;
const findLongestRun = (threshold: number) => {
let curStart = -1;
let runStart = -1;
let runEnd = -1;
for (let y = 0; y < height; y++) {
const keep = rowCoverage[y] >= threshold;
if (keep && curStart < 0) curStart = y;
if (!keep && curStart >= 0) {
const end = y - 1;
if (end - curStart > runEnd - runStart) {
runStart = curStart;
runEnd = end;
}
curStart = -1;
}
}
if (curStart >= 0) {
const end = height - 1;
if (end - curStart > runEnd - runStart) {
runStart = curStart;
runEnd = end;
}
}
if (runStart < 0 || runEnd < runStart) return null;
return { start: runStart, end: runEnd };
};
for (const candidate of thresholdCandidates) {
const run = findLongestRun(candidate);
if (run) {
bestStart = run.start;
bestEnd = run.end;
thresholdUsed = candidate;
break;
}
}
const minHeightFrac = Number(process.env.OCCLUDER_SAM_BAND_MIN_HEIGHT_FRAC ?? "0.04");
const minHeight = Math.max(4, Math.round(height * minHeightFrac));
const maxHeightFrac = Number(process.env.OCCLUDER_SAM_BAND_MAX_HEIGHT_FRAC ?? "0.6");
const maxHeight = Math.max(minHeight, Math.round(height * maxHeightFrac));
let fallbackUsed = false;
if (bestStart >= 0 && bestEnd >= bestStart) {
const bandHeight = bestEnd - bestStart + 1;
if (bandHeight < minHeight) {
const center = Math.round((bestStart + bestEnd) / 2);
const half = Math.floor(minHeight / 2);
bestStart = Math.max(0, Math.min(height - minHeight, center - half));
bestEnd = Math.min(height - 1, bestStart + minHeight - 1);
fallbackUsed = true;
}
}
if (bestStart < 0 || bestEnd < bestStart) {
const minOverall = Number(process.env.OCCLUDER_SAM_BAND_MIN_OVERALL ?? "0.01");
const overallCoverage = coverageOfMask(mask);
if (overallCoverage >= minOverall) {
let weightSum = 0;
let weightedY = 0;
for (let y = 0; y < height; y++) {
const weight = rowCoverage[y];
if (weight <= 0) continue;
weightSum += weight;
weightedY += weight * y;
}
const center = weightSum > 0 ? Math.round(weightedY / weightSum) : Math.floor(height / 2);
const targetHeightFrac = clamp(
Number(process.env.OCCLUDER_SAM_BAND_TARGET_HEIGHT_FRAC ?? "0.25"),
minHeightFrac,
maxHeightFrac,
);
const targetHeight = Math.round(height * targetHeightFrac);
const bandHeight = Math.max(minHeight, Math.min(maxHeight, targetHeight));
bestStart = Math.max(0, Math.min(height - bandHeight, center - Math.floor(bandHeight / 2)));
bestEnd = Math.min(height - 1, bestStart + bandHeight - 1);
fallbackUsed = true;
thresholdUsed = relaxedThreshold2;
}
}
if (bestStart < 0 || bestEnd < bestStart) {
return { mask, applied: false, band: null };
}
const expand = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_BAND_EXPAND ?? "6")));
let yMin = Math.max(0, bestStart - expand);
let yMax = Math.min(height - 1, bestEnd + expand);
const bandHeight = yMax - yMin + 1;
if (bandHeight > maxHeight) {
const center = Math.round((yMin + yMax) / 2);
yMin = Math.max(0, Math.min(height - maxHeight, center - Math.floor(maxHeight / 2)));
yMax = Math.min(height - 1, yMin + maxHeight - 1);
fallbackUsed = true;
}
const out = new Uint8Array(mask);
for (let y = 0; y < height; y++) {
if (y >= yMin && y <= yMax) continue;
out.fill(0, y * width, (y + 1) * width);
}
return {
mask: out,
applied: true,
band: {
yMin,
yMax: yMax + 1,
threshold: Number(thresholdUsed.toFixed(3)),
p80: Number(p80.toFixed(3)),
fallback: fallbackUsed,
},
};
}
function bestPeriod(ac: Float32Array, minP: number, maxP: number): number {
minP = Math.max(3, minP|0);
maxP = Math.max(minP+1, maxP|0);
let best = 0, bestVal = -Infinity;
for (let p=minP; p<=maxP && p<ac.length; p++) {
const v = ac[p];
if (v > bestVal) { bestVal = v; best = p; }
}
return best;
}
function phaseForPeriod(proj: Float32Array, period: number): number {
if (period <= 0) return 0;
let bestOff=0, bestScore=-Infinity;
for (let off=0; off<period; off++){
let score=0;
for (let i=off; i<proj.length; i+=period) score += proj[i];
if (score>bestScore){bestScore=score; bestOff=off;}
}
return bestOff;
}
function componentTouchesMask(
comp: { pixels: number[] },
mask: Uint8Array | null | undefined,
): boolean {
if (!mask) return false;
for (const idx of comp.pixels) {
if (mask[idx]) return true;
}
return false;
}
function componentOverlapFraction(
comp: { pixels: number[]; area: number },
mask: Uint8Array | null | undefined,
): number {
if (!mask || comp.area <= 0) return 0;
let hits = 0;
for (const idx of comp.pixels) {
if (mask[idx]) hits += 1;
}
return hits / comp.area;
}
function componentExtentX(comp: { minX: number; maxX: number }): number {
return comp.maxX - comp.minX + 1;
}
function componentExtentY(comp: { minY: number; maxY: number }): number {
return comp.maxY - comp.minY + 1;
}
function componentAspectRatio(comp: { minX: number; maxX: number; minY: number; maxY: number }): number {
const extX = componentExtentX(comp);
const extY = componentExtentY(comp);
return extX / Math.max(1, extY);
}
function componentVarianceY(
comp: { pixels: number[]; area: number; minY: number; maxY: number },
width: number,
): number {
if (comp.area <= 0 || width <= 0) return 0;
let sum = 0;
let sumSq = 0;
for (const idx of comp.pixels) {
const y = Math.floor(idx / width);
sum += y;
sumSq += y * y;
}
const mean = sum / comp.area;
const variance = Math.max(0, sumSq / comp.area - mean * mean);
const extent = Math.max(1, componentExtentY(comp));
const norm = variance / (extent * extent);
return Number.isFinite(norm) ? norm : 0;
}
function coverageOfMask(mask: Uint8Array | null | undefined): number {
if (!mask || !mask.length) return 0;
let filled = 0;
for (let i = 0; i < mask.length; i++) {
if (mask[i]) filled += 1;
}
return filled / mask.length;
}
function coverageWithinMask(mask: Uint8Array | null | undefined, region: Uint8Array | null | undefined): number {
if (!mask || !region) return 0;
const total = Math.min(mask.length, region.length);
let interior = 0;
let overlap = 0;
for (let i = 0; i < total; i++) {
if (!region[i]) continue;
interior += 1;
if (mask[i]) overlap += 1;
}
return interior > 0 ? overlap / interior : 0;
}
function sha1Hex(data: Uint8Array | Buffer | string): string {
const hash = crypto.createHash("sha1");
hash.update(data as any);
return hash.digest("hex");
}
function maskSha1(mask: Uint8Array, width: number, height: number): string {
const header = Buffer.from(`${width}x${height}\0`, "utf8");
const hash = crypto.createHash("sha1");
hash.update(header);
hash.update(Buffer.from(mask));
return hash.digest("hex");
}
function meanBinaryTransitionsPerRow(mask: Uint8Array, width: number, height: number): number {
if (!(width > 1 && height > 0) || mask.length < width * height) return 0;
let transitions = 0;
for (let y = 0; y < height; y++) {
const row = y * width;
let prev = mask[row] > 127;
for (let x = 1; x < width; x++) {
const v = mask[row + x] > 127;
if (v !== prev) transitions += 1;
prev = v;
}
}
return transitions / height;
}
function meanBinaryTransitionsPerCol(mask: Uint8Array, width: number, height: number): number {
if (!(width > 0 && height > 1) || mask.length < width * height) return 0;
let transitions = 0;
for (let x = 0; x < width; x++) {
let prev = mask[x] > 127;
for (let y = 1; y < height; y++) {
const v = mask[y * width + x] > 127;
if (v !== prev) transitions += 1;
prev = v;
}
}
return transitions / width;
}
function looksLikeGridBinaryMask(mask: Uint8Array, width: number, height: number): boolean {
const rowTransitions = meanBinaryTransitionsPerRow(mask, width, height);
const colTransitions = meanBinaryTransitionsPerCol(mask, width, height);
// Heuristic thresholds:
// - Real kitchen occluders are usually a few large blobs => low transition density.
// - Regular tile/checker patterns produce high-frequency transitions in both axes.
const rowThresh = Math.max(180, Math.round(width * 0.12));
const colThresh = Math.max(180, Math.round(height * 0.12));
return rowTransitions > rowThresh && colTransitions > colThresh;
}
async function dilateBinaryMaskViaBlur(
mask: Uint8Array,
width: number,
height: number,
radius: number,
threshold = 8,
): Promise<Uint8Array> {
const r = Math.max(0, Math.round(radius));
if (!r) return Uint8Array.from(mask);
const t = Math.max(1, Math.min(254, Math.round(threshold)));
const out = await sharp(Buffer.from(mask), { raw: { width, height, channels: 1 } })
.blur(r)
.threshold(t)
.raw()
.toBuffer();
return new Uint8Array(out);
}
type BinaryMask = { data: Uint8Array; width: number; height: number };
type DepthByteMap = { data: Uint8Array; width: number; height: number; min: number; max: number };
function normalizeDepthMap(depth: DepthMap): DepthMap {
const { data, width, height } = depth;
let min = Infinity;
let max = -Infinity;
for (const v of data) {
if (!Number.isFinite(v)) continue;
if (v < min) min = v;
if (v > max) max = v;
}
if (!Number.isFinite(min) || !Number.isFinite(max)) {
return { data: new Float32Array(data.length), width, height };
}
const range = max - min || 1;
const norm = new Float32Array(data.length);
for (let i = 0; i < data.length; i++) {
const v = Number.isFinite(data[i]) ? data[i] : min;
norm[i] = (v - min) / range;
}
return { data: norm, width, height };
}
function normalizeDepthFloatToByte(depth: DepthMap): DepthByteMap {
const { data, width, height } = depth;
let min = Infinity;
let max = -Infinity;
for (const v of data) {
if (!Number.isFinite(v)) continue;
min = Math.min(min, v);
max = Math.max(max, v);
}
const range = max - min || 1;
const out = new Uint8Array(width * height);
for (let i = 0; i < data.length; i++) {
const v = Number.isFinite(data[i]) ? data[i] : min;
out[i] = Math.max(0, Math.min(255, Math.round(((v - min) / range) * 255)));
}
return { data: out, width, height, min, max };
}
function cropDepthToAspect(
map: DepthByteMap,
targetW: number,
targetH: number,
): { map: DepthByteMap; crop: { x: number; y: number; width: number; height: number } | null } {
if (!(targetW > 0 && targetH > 0 && map.width > 0 && map.height > 0)) {
return { map, crop: null };
}
const targetRatio = targetW / targetH;
const currentRatio = map.width / map.height;
if (!Number.isFinite(targetRatio) || !Number.isFinite(currentRatio)) {
return { map, crop: null };
}
if (Math.abs(currentRatio - targetRatio) < 0.02) {
return { map, crop: null };
}
let cropW = map.width;
let cropH = map.height;
let cropX = 0;
let cropY = 0;
if (currentRatio > targetRatio) {
cropW = Math.max(1, Math.round(map.height * targetRatio));
cropX = Math.max(0, Math.floor((map.width - cropW) / 2));
} else {
cropH = Math.max(1, Math.round(map.width / targetRatio));
cropY = Math.max(0, Math.floor((map.height - cropH) / 2));
}
if (cropW === map.width && cropH === map.height) {
return { map, crop: null };
}
const cropped = new Uint8Array(cropW * cropH);
for (let y = 0; y < cropH; y++) {
const srcRow = (y + cropY) * map.width + cropX;
const dstRow = y * cropW;
cropped.set(map.data.subarray(srcRow, srcRow + cropW), dstRow);
}
return {
map: { data: cropped, width: cropW, height: cropH, min: map.min, max: map.max },
crop: { x: cropX, y: cropY, width: cropW, height: cropH },
};
}
function invertDepthBytes(map: DepthByteMap): DepthByteMap {
const out = new Uint8Array(map.data.length);
for (let i = 0; i < map.data.length; i++) out[i] = 255 - map.data[i];
return { data: out, width: map.width, height: map.height, min: 255 - map.max, max: 255 - map.min };
}
function histogram256(data: Uint8Array): Uint32Array {
const hist = new Uint32Array(256);
for (let i = 0; i < data.length; i++) hist[data[i]] += 1;
return hist;
}
function computeWallDepthFromHist(hist: Uint32Array): number {
// far range peak (0..100 bucket range) -> backsplash plane
const limit = Math.min(100, hist.length);
let peakIdx = 0;
let peakVal = -1;
for (let i = 0; i < limit; i++) {
const v = hist[i];
if (v > peakVal) {
peakVal = v;
peakIdx = i;
}
}
return peakIdx;
}
function buildDepthOccluderFromBytes(depth: DepthByteMap, cutoff: number): BinaryMask {
const { data, width, height } = depth;
const mask = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
mask[i] = data[i] > cutoff ? 255 : 0;
}
return { data: mask, width, height };
}
function buildDepthBackMaskFromBytes(depth: DepthByteMap, cutoff: number): BinaryMask {
const { data, width, height } = depth;
const mask = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
mask[i] = data[i] <= cutoff ? 255 : 0;
}
return { data: mask, width, height };
}
function morphErode(mask: Uint8Array, width: number, height: number, radius = 1): Uint8Array {
if (radius <= 0) return Uint8Array.from(mask);
const out = new Uint8Array(mask.length);
const r = Math.max(1, Math.floor(radius));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let keep = 255;
for (let dy = -r; dy <= r && keep; dy++) {
const yy = y + dy;
if (yy < 0 || yy >= height) { keep = 0; break; }
const row = yy * width;
for (let dx = -r; dx <= r; dx++) {
const xx = x + dx;
if (xx < 0 || xx >= width) { keep = 0; break; }
if (mask[row + xx] === 0) { keep = 0; break; }
}
}
out[y * width + x] = keep ? 255 : 0;
}
}
return out;
}
function morphDilate(mask: Uint8Array, width: number, height: number, radius = 1): Uint8Array {
if (radius <= 0) return Uint8Array.from(mask);
const out = new Uint8Array(mask.length);
const r = Math.max(1, Math.floor(radius));
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
let fill = 0;
for (let dy = -r; dy <= r && !fill; dy++) {
const yy = y + dy;
if (yy < 0 || yy >= height) continue;
const row = yy * width;
for (let dx = -r; dx <= r; dx++) {
const xx = x + dx;
if (xx < 0 || xx >= width) continue;
if (mask[row + xx]) { fill = 255; break; }
}
}
out[y * width + x] = fill;
}
}
return out;
}
function morphOpen(mask: Uint8Array, width: number, height: number, erodeR = 1, dilateR = 2): Uint8Array {
const eroded = morphErode(mask, width, height, erodeR);
return morphDilate(eroded, width, height, dilateR);
}
async function blurMask(mask: Uint8Array, width: number, height: number, sigma = 2): Promise<Uint8Array> {
if (sigma <= 0) return Uint8Array.from(mask);
const blurred = await sharp(Buffer.from(mask), {
raw: { width, height, channels: 1 },
})
.blur(sigma)
.toColorspace('b-w')
.raw()
.toBuffer();
return new Uint8Array(blurred);
}
function morphClose(mask: Uint8Array, width: number, height: number, radius = 3): Uint8Array {
const dilated = morphDilate(mask, width, height, radius);
return morphErode(dilated, width, height, radius);
}
function removeSmallComponents(mask: Uint8Array, width: number, height: number, minSize = 1000): Uint8Array {
const cleaned = Uint8Array.from(mask);
const components = connectedComponents(cleaned, width, height, 1);
const total = width * height;
const minPixels = Math.max(minSize, Math.floor(0.01 * total));
for (const comp of components) {
if (comp.area < minPixels) {
for (const idx of comp.pixels) cleaned[idx] = 0;
}
}
return cleaned;
}
async function smoothDepthEdges(map: DepthByteMap): Promise<DepthByteMap> {
try {
const smoothed = await sharp(Buffer.from(map.data), {
raw: { width: map.width, height: map.height, channels: 1 },
failOnError: false,
})
.median(3)
.blur(1)
.toColorspace('b-w')
.raw()
.toBuffer();
return { data: new Uint8Array(smoothed), width: map.width, height: map.height, min: map.min, max: map.max };
} catch {
return map;
}
}
function estimateWallDepth(normDepth: DepthMap): number {
const { data, width, height } = normDepth;
const x0 = Math.floor(width * 0.3);
const x1 = Math.floor(width * 0.7);
const samples: number[] = [];
for (let y = 0; y < height; y++) {
const row = y * width;
for (let x = x0; x < x1; x++) {
const v = data[row + x];
if (Number.isFinite(v)) samples.push(v);
}
}
samples.sort((a, b) => a - b);
const idx = Math.floor(samples.length * 0.8);
return samples[idx] ?? 0.5;
}
function buildDepthOccluderMask(normDepth: DepthMap, wallDepth: number, margin = 0.05): BinaryMask {
const { data, width, height } = normDepth;
const threshold = wallDepth - margin;
const mask = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
const v = data[i];
mask[i] = Number.isFinite(v) && v < threshold ? 255 : 0;
}
return { data: mask, width, height };
}
function buildDepthBackMask(normDepth: DepthMap, wallDepth: number, margin = 0.03): BinaryMask {
const { data, width, height } = normDepth;
const threshold = wallDepth - margin;
const mask = new Uint8Array(data.length);
for (let i = 0; i < data.length; i++) {
const v = data[i];
mask[i] = Number.isFinite(v) && v >= threshold ? 255 : 0;
}
return { data: mask, width, height };
}
async function resizeBinaryMask(mask: BinaryMask, width: number, height: number): Promise<BinaryMask> {
if (mask.width === width && mask.height === height) return mask;
const resized = await sharp(Buffer.from(mask.data), {
raw: { width: mask.width, height: mask.height, channels: 1 },
})
.resize(width, height, { kernel: "lanczos3" })
.toColorspace("b-w")
.raw()
.toBuffer();
return { data: new Uint8Array(resized), width, height };
}
function mergeOccluderMasks(base: BinaryMask, extra: BinaryMask): BinaryMask {
const merged = Uint8Array.from(base.data);
const usable = Math.min(merged.length, extra.data.length);
for (let i = 0; i < usable; i++) {
if (extra.data[i]) merged[i] = 255;
}
return { data: merged, width: base.width, height: base.height };
}
function countClassIds(ids: Uint8Array): Record<string, number> {
const counts: Record<string, number> = { "0": 0, "1": 0, "2": 0, "255": 0 };
for (let i = 0; i < ids.length; i++) {
const v = ids[i];
if (v === 0) counts["0"] += 1;
else if (v === 1) counts["1"] += 1;
else if (v === 2) counts["2"] += 1;
else if (v === 255) counts["255"] += 1;
}
return counts;
}
function buildYoloBoostMask(
base: Uint8Array,
probMap: Uint8Array | null,
width: number,
height: number,
meta: Record<string, unknown> | null,
): { mask: Uint8Array; coverage: number; applied: number } | null {
if (!meta) return null;
const yolo = (meta as any)?.yolo;
const instances = Array.isArray(yolo?.instances) ? yolo.instances as Array<any> : null;
if (!instances || instances.length === 0) return null;
const total = width * height;
const boxMask = new Uint8Array(total);
let applied = 0;
const marginBase = Math.max(3, Math.round(Math.max(width, height) * 0.004));
for (const entry of instances) {
const boxRaw = Array.isArray(entry?.bbox)
? entry.bbox
: Array.isArray(entry?.box)
? entry.box
: null;
if (!boxRaw || boxRaw.length < 4) continue;
const coords = boxRaw.map((value: unknown) => Number(value)).filter((value) => Number.isFinite(value));
if (coords.length < 4) continue;
const [bx1, by1, bx2, by2] = coords as number[];
const minX = clamp(Math.floor(Math.min(bx1, bx2)) - marginBase, 0, width - 1);
const maxX = clamp(Math.ceil(Math.max(bx1, bx2)) + marginBase, 0, width - 1);
const minY = clamp(Math.floor(Math.min(by1, by2)) - marginBase, 0, height - 1);
const maxY = clamp(Math.ceil(Math.max(by1, by2)) + marginBase, 0, height - 1);
if (maxX <= minX || maxY <= minY) continue;
applied += 1;
for (let y = minY; y <= maxY; y++) {
const row = y * width;
for (let x = minX; x <= maxX; x++) {
boxMask[row + x] = 255;
}
}
}
if (applied === 0) return null;
let support = boxMask;
if (probMap && probMap.length === total) {
const supportProb = thresholdMask(probMap, 112);
if (supportProb) {
support = maskAnd(support, supportProb);
}
}
support = morphDilate(support, width, height, 1);
const boosted = maskOr(base, support);
if (!boosted) return null;
const coverage = coverageOfMask(boosted);
return { mask: boosted, coverage, applied };
}
type GridMasks = {
interiorMask: Uint8Array;
lineMask: Uint8Array;
};
type FinalMaskMeta = {
polarity: 'white' | 'black';
coverage?: number;
preset?: MaskPresetName;
source?: string;
stats?: Record<string, unknown> | null;
};
type TileMaskMeta = {
classes: string[];
colorMap: Record<string, [number, number, number]>;
coverage?: Record<string, number>;
coverageAbsolute?: Record<string, number>;
rawCoverage?: Record<string, number>;
occluderCoverage?: number;
occludedPixels?: number;
occluderPixels?: number;
occludedCoverage?: number;
warnings?: string[];
classIdLegend?: Record<string, string>;
classIdLegendRaw?: Record<string, string>;
classPixelCountsRaw?: Record<string, number>;
classPixelCounts?: Record<string, number>;
unetDebug?: unknown;
tileBand?: { yMin: number; yMax: number; confidence: number } | null;
model?: string | null;
source?: string;
};
type TileMaskVersions = {
combined?: string | null;
raw?: string | null;
occluder?: string | null;
};
function buildGridMasks(width: number, height: number, vlines: number[], hlines: number[], bandPx: number): GridMasks {
const total = width * height;
const interior = new Uint8Array(total);
const lineMask = new Uint8Array(total);
if (!width || !height || (!vlines.length && !hlines.length) || bandPx <= 0) {
interior.fill(255);
return { interiorMask: interior, lineMask };
}
const band = Math.max(1, Math.round(bandPx));
const colFlags = new Uint8Array(width);
const rowFlags = new Uint8Array(height);
for (const raw of vlines) {
const v = Math.max(0, Math.min(width - 1, Math.round(raw)));
const start = Math.max(0, v - band);
const end = Math.min(width - 1, v + band);
for (let x = start; x <= end; x++) colFlags[x] = 1;
}
for (const raw of hlines) {
const v = Math.max(0, Math.min(height - 1, Math.round(raw)));
const start = Math.max(0, v - band);
const end = Math.min(height - 1, v + band);
for (let y = start; y <= end; y++) rowFlags[y] = 1;
}
interior.fill(255);
for (let y = 0; y < height; y++) {
const row = y * width;
const rowFlag = rowFlags[y] === 1;
for (let x = 0; x < width; x++) {
if (rowFlag || colFlags[x] === 1) {
interior[row + x] = 0;
lineMask[row + x] = 255;
}
}
}
return { interiorMask: interior, lineMask };
}
function leakRatio(mask: Uint8Array, lineMask: Uint8Array): number {
let onLine = 0;
let total = 0;
for (let i = 0; i < Math.min(mask.length, lineMask.length); i++) {
if (!lineMask[i]) continue;
total += 1;
if (mask[i]) onLine += 1;
}
if (!total) return 0;
return onLine / total;
}
type MaskBoostStats = {
changed: boolean;
coverage: number;
gamma: number;
lo: number;
hi: number;
};
type RawMaskDiagnostics = {
coverageArray: number;
coveragePng: number;
mismatch: number;
suspicious: boolean;
};
function coverageThreshold(mask: Uint8Array, threshold = 128): number {
const total = mask.length;
if (!total) return 0;
let on = 0;
for (let i = 0; i < total; i++) {
if (mask[i] >= threshold) on += 1;
}
return on / total;
}
async function maskToDataUrlWithCheck(
mask: Uint8Array,
width: number,
height: number,
): Promise<{ dataUrl: string; diagnostics: RawMaskDiagnostics }>
{
const pngBuffer = await sharp(Buffer.from(mask), {
raw: { width, height, channels: 1 },
})
.toColorspace('b-w')
.png()
.toBuffer();
const dataUrl = `data:image/png;base64,${pngBuffer.toString('base64')}`;
const arrayCoverage = coverageThreshold(mask);
const decoded = await sharp(pngBuffer)
.toColorspace('b-w')
.raw()
.toBuffer();
const pngCoverage = coverageThreshold(new Uint8Array(decoded));
const mismatch = Math.abs(pngCoverage - arrayCoverage);
return {
dataUrl,
diagnostics: {
coverageArray: Number(arrayCoverage.toFixed(6)),
coveragePng: Number(pngCoverage.toFixed(6)),
mismatch: Number(mismatch.toFixed(6)),
suspicious: mismatch > 0.02,
},
};
}
function boostSegMaskInPlace(mask: Uint8Array): MaskBoostStats {
const total = mask.length;
if (total === 0) return { changed: false, coverage: 0, gamma: 1, lo: 0, hi: 0 };
const hist = new Uint32Array(256);
let nonZero = 0;
for (let i = 0; i < total; i++) {
const v = mask[i];
hist[v] += 1;
if (v > 0) nonZero += 1;
}
if (nonZero < 32) {
return { changed: false, coverage: nonZero / total, gamma: 1, lo: 0, hi: 0 };
}
const coverage = nonZero / total;
if (coverage > 0.6) {
return { changed: false, coverage, gamma: 1, lo: 0, hi: 0 };
}
const percentile = (q: number): number => {
if (!Number.isFinite(q)) return 0;
const target = Math.max(0, Math.min(nonZero - 1, Math.round(q * (nonZero - 1))));
let acc = 0;
for (let value = 1; value < hist.length; value++) {
const count = hist[value];
if (count === 0) continue;
acc += count;
if (acc > target) return value;
}
return 255;
};
const lowQuantile = coverage < 0.05 ? 0.02 : 0.08;
const hiQuantile = coverage < 0.03 ? 0.995 : 0.97;
const lo = percentile(lowQuantile);
const hiRaw = percentile(hiQuantile);
const hi = Math.min(255, Math.max(lo + 8, hiRaw));
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) {
return { changed: false, coverage, gamma: 1, lo, hi };
}
const gamma = coverage < 0.03 ? 0.52 : coverage < 0.08 ? 0.6 : coverage < 0.18 ? 0.68 : 0.75;
const range = hi - lo;
const invRange = range > 0 ? 1 / range : 0;
let changed = false;
for (let i = 0; i < total; i++) {
const v = mask[i];
if (v <= lo) {
if (v !== 0) { mask[i] = 0; changed = true; }
continue;
}
if (v >= hi) {
if (v !== 255) { mask[i] = 255; changed = true; }
continue;
}
const norm = (v - lo) * invRange;
const boosted = Math.pow(Math.max(0, Math.min(1, norm)), gamma);
const mapped = Math.max(0, Math.min(255, Math.round(boosted * 255)));
if (mask[i] !== mapped) {
mask[i] = mapped;
changed = true;
}
}
return { changed, coverage, gamma, lo, hi };
}
async function maskToDataUrl(mask: Uint8Array, width: number, height: number): Promise<string> {
const buffer = await sharp(Buffer.from(mask), {
raw: { width, height, channels: 1 },
})
.toColorspace("b-w")
.png()
.toBuffer();
return `data:image/png;base64,${buffer.toString('base64')}`;
}
async function maskToPngBuffer(mask: Uint8Array, width: number, height: number): Promise<Buffer> {
return sharp(Buffer.from(mask), {
raw: { width, height, channels: 1 },
})
.toColorspace("b-w")
.png()
.toBuffer();
}
async function runSamHqPatchFromBuffer(
image: Buffer,
fnMask: Uint8Array,
width: number,
height: number,
opts?: {
python?: string;
model?: string;
maxBoxes?: string;
margin?: string;
maxBoxSpanFrac?: string;
device?: string;
timeoutMs?: number;
},
): Promise<Uint8Array | null> {
const python = opts?.python ?? process.env.OCCLUDER_SAM_PATCH_PY ?? "python3";
const model = opts?.model ?? process.env.OCCLUDER_SAM_PATCH_MODEL ?? "syscv-community/sam-hq-vit-base";
const maxBoxes = opts?.maxBoxes ?? process.env.OCCLUDER_SAM_PATCH_BOXES ?? "6";
const margin = opts?.margin ?? process.env.OCCLUDER_SAM_PATCH_MARGIN ?? "24";
const maxBoxSpanFrac = opts?.maxBoxSpanFrac ?? process.env.OCCLUDER_SAM_PATCH_MAX_BOX_SPAN_FRAC ?? "0.45";
const device = opts?.device ?? process.env.OCCLUDER_SAM_PATCH_DEVICE ?? "auto";
const timeoutMs = Math.max(
60000,
Number(opts?.timeoutMs ?? process.env.OCCLUDER_SAM_PATCH_TIMEOUT_MS ?? 180000),
);
const scriptPath = path.resolve(process.cwd(), "tools", "sam_hq_patch.py");
const patchRoot = path.join(process.cwd(), "runs");
await fs.mkdir(patchRoot, { recursive: true });
const tempDir = await fs.mkdtemp(path.join(patchRoot, "sam_patch_"));
const imagePath = path.join(tempDir, "image.png");
const fnPath = path.join(tempDir, "fn_mask.png");
const outPath = path.join(tempDir, "sam_patch.png");
await fs.writeFile(imagePath, await sharp(image).resize(width, height, { fit: "fill" }).png().toBuffer());
await fs.writeFile(fnPath, await maskToPngBuffer(fnMask, width, height));
await new Promise<void>((resolve, reject) => {
const proc = spawn(
python,
[
scriptPath,
"--image",
imagePath,
"--fn-mask",
fnPath,
"--out",
outPath,
"--model",
model,
"--max-boxes",
maxBoxes,
"--box-margin",
margin,
"--max-box-span-frac",
maxBoxSpanFrac,
"--device",
device,
],
{ stdio: ["ignore", "pipe", "pipe"] },
);
let stderr = "";
let stdout = "";
const timer = setTimeout(() => {
proc.kill("SIGKILL");
reject(new Error(`sam_hq_patch timed out after ${timeoutMs}ms`));
}, timeoutMs);
proc.stdout.on("data", (chunk) => {
stdout += chunk.toString();
});
proc.stderr.on("data", (chunk) => {
stderr += chunk.toString();
});
proc.on("error", (err) => {
clearTimeout(timer);
reject(err);
});
proc.on("close", (code) => {
clearTimeout(timer);
if (code === 0) return resolve();
reject(new Error(`sam_hq_patch failed (${code}): ${stderr || stdout}`));
});
}).catch((err) => {
console.warn(err);
});
try {
const { data } = await sharp(outPath)
.resize(width, height, { fit: "fill", kernel: "nearest" })
.threshold(128)
.raw()
.toBuffer({ resolveWithObject: true });
const mask = new Uint8Array(width * height);
for (let i = 0; i < mask.length; i++) {
mask[i] = data[i] > 0 ? 255 : 0;
}
return mask;
} catch {
return null;
}
}
async function cutoutToDataUrl(source: Buffer, mask: Uint8Array, width: number, height: number): Promise<string> {
const alpha = Buffer.from(mask);
const buffer = await sharp(source)
.ensureAlpha()
.composite([
{
input: alpha,
raw: { width, height, channels: 1 },
blend: 'dest-in',
},
])
.png()
.toBuffer();
return `data:image/png;base64,${buffer.toString('base64')}`;
}
async function overlayToDataUrl(source: Buffer, mask: Uint8Array, width: number, height: number): Promise<string> {
const overlay = Buffer.alloc(width * height * 4);
for (let i = 0; i < width * height; i++) {
const alpha = mask[i];
const offset = i * 4;
overlay[offset + 0] = 255;
overlay[offset + 1] = 85;
overlay[offset + 2] = 0;
overlay[offset + 3] = Math.min(255, Math.round(alpha * 0.6));
}
const buffer = await sharp(source)
.composite([
{
input: overlay,
raw: { width, height, channels: 4 },
blend: 'over',
},
])
.png()
.toBuffer();
return `data:image/png;base64,${buffer.toString('base64')}`;
}
export async function POST(req: NextRequest) {
let restoreOccluderEnv: (() => void) | null = null;
try {
const url = new URL(req.url);
const occluderEnvOverrides = parseOccluderEnvOverrides(url.searchParams.get("occluderEnv"));
const occluderDefaults = getOccluderDefaults(process.env.OCCLUDER_DEFAULTS);
const occluderEnvDefaults = occluderDefaults?.occluderEnv ?? {};
for (const [key, value] of Object.entries(occluderEnvDefaults)) {
if (process.env[key] == null) {
process.env[key] = String(value);
}
}
restoreOccluderEnv = applyOccluderEnvOverrides(occluderEnvOverrides);
const debug = url.searchParams.get("debug") === "1";
const useSeg = (url.searchParams.get("seg") === "1") || (process.env.SEGMENT_OCCLUDERS === "1");
let occluderMethod = (process.env.OCCLUDER_METHOD ?? "").trim().toLowerCase();
let preferSegOccluder = occluderMethod === "sam_hq"
|| occluderMethod === "sam-hq"
|| occluderMethod === "sam_hq_prompted"
|| occluderMethod === "distilled_unsup"
|| occluderMethod === "distilled-unsup"
|| occluderMethod === "distilled";
const preferTileBand = (process.env.OCCLUDER_SAM_DEPTH_BAND_PREFER_TILEBAND ?? "1") !== "0";
const ransacParam = url.searchParams.get("ransac") ?? "";
const ransacForce = ransacParam === "1" || ransacParam === "true";
const ransacAuto = ransacParam === "auto";
const maskWeight = clamp(Number(url.searchParams.get("maskw") ?? process.env.MASK_WEIGHT ?? "0.85"), 0, 1);
const maskBlur = clamp(Number(url.searchParams.get("maskblur") ?? process.env.MASK_BLUR ?? "1.5"), 0, 10);
const presetParamRaw = url.searchParams.get("maskPreset") ?? process.env.MASK_PRESET_DEFAULT ?? null;
const presetRequested = resolvePresetName(presetParamRaw);
const maskDebugEnabled = debug || url.searchParams.get("maskDebug") === "1";
const fallbackThresholdEnv = Number.isFinite(Number(process.env.MASK_PRESET_FALLBACK))
? Number(process.env.MASK_PRESET_FALLBACK)
: 0;
const fallbackThreshold = Math.max(0, Math.min(0.06, fallbackThresholdEnv));
const headerFlag = (key: string) => req.headers.get(key)?.toLowerCase() ?? null;
const parseFlag = (value: string | null | undefined, defaultOn = true) => {
if (value == null) return defaultOn;
if (value === "1" || value === "true" || value === "on") return true;
if (value === "0" || value === "false" || value === "off") return false;
return defaultOn;
};
const parseNumber = (value: string | null | undefined): number | undefined => {
if (value == null) return undefined;
const num = Number(value);
return Number.isFinite(num) ? num : undefined;
};
const presetDefaults = presetRequested === 'tiles-detail'
? { lattice: true, illumination: true, autotune: true, autotuneMs: 200, thinFrac: 0.04, minAreaFrac: 0.12 }
: { lattice: true, illumination: true, autotune: true, autotuneMs: 150, thinFrac: 0.04, minAreaFrac: 0.12 };
const normalizeModelName = (value: string | null | undefined): string => {
if (!value) return 'unet_latest';
const normalized = value.toLowerCase();
if (['unet_latest', 'unet_old', 'u2net', 'none'].includes(normalized)) {
return normalized;
}
return 'unet_latest';
};
const tileModelRequested = normalizeModelName(
headerFlag('x-tile-model') ?? url.searchParams.get('model'),
);
const flags: PipelineFlags = {
enableLattice: parseFlag(
headerFlag("x-mask-lattice") ?? url.searchParams.get("maskLattice"),
presetDefaults.lattice,
),
enableIllumination: parseFlag(
headerFlag("x-mask-illumination") ?? url.searchParams.get("maskIllumination"),
presetDefaults.illumination,
),
enableAutotune: parseFlag(
headerFlag("x-mask-autotune") ?? url.searchParams.get("maskAutotune"),
presetDefaults.autotune,
),
autotuneMs: clamp(
Number(
headerFlag("x-mask-autotune-ms") ??
url.searchParams.get("maskAutotuneMs") ??
presetDefaults.autotuneMs.toString(),
),
0,
1000,
),
thinWidthFrac: (() => {
const val = parseNumber(headerFlag("x-mask-thinwidthfrac") ?? url.searchParams.get("maskThinWidthFrac"));
return Number.isFinite(val as number) ? (val as number) : presetDefaults.thinFrac;
})(),
minAreaFrac: (() => {
const val = parseNumber(headerFlag("x-mask-minareafrac") ?? url.searchParams.get("maskMinAreaFrac"));
return Number.isFinite(val as number) ? (val as number) : presetDefaults.minAreaFrac;
})(),
};
flags.thinWidthFrac = Math.max(0.0, Math.min(0.5, flags.thinWidthFrac));
flags.minAreaFrac = Math.max(0.05, Math.min(0.8, flags.minAreaFrac));
const formDataTimeoutMs = parseTimeoutMsEnv("ANALYZE_FORMDATA_TIMEOUT_MS", 20_000);
const fileBufferTimeoutMs = parseTimeoutMsEnv("ANALYZE_FILE_BUFFER_TIMEOUT_MS", 20_000);
const depthStageTimeoutMs = parseTimeoutMsEnv("ANALYZE_DEPTH_STAGE_TIMEOUT_MS", 90_000);
const form = await withStepTimeout(req.formData(), formDataTimeoutMs, "request.formData");
const file = form.get("file") as File | null;
if (!file) return NextResponse.json({ ok:false, error:"missing file" }, { status: 400 });
const fileName = typeof (file as any)?.name === "string" ? String((file as any).name) : null;
const presetBackend = resolveOccluderPresetBackend();
const presetComfyRoute = presetBackend === "rfdet_m3" && isComfyProfileUpload(fileName);
if (presetComfyRoute) {
occluderMethod = "distilled_unsup";
preferSegOccluder = true;
}
const effectiveOccluderEnvOverrides: OccluderEnvOverrides | null = (() => {
const merged: OccluderEnvOverrides = { ...(occluderEnvOverrides ?? {}) };
if (presetComfyRoute) {
merged.OCCLUDER_BACKEND = "rfdet_m3";
if (!merged.OCCLUDER_METHOD) {
merged.OCCLUDER_METHOD = "distilled_unsup";
}
}
return Object.keys(merged).length ? merged : null;
})();
const raw = Buffer.from(
await withStepTimeout(
file.arrayBuffer(),
fileBufferTimeoutMs,
"file.arrayBuffer",
),
);
const meta0 = await sharp(raw).metadata();
let W = meta0.width ?? 0, H = meta0.height ?? 0;
let tileSegmentationResult: TileSegmentationResult | null = null;
if (!(W>0 && H>0)) {
return NextResponse.json({ ok:false, error:"Decode error: image width/height invalid (w and h must be numbers)." }, { status: 422 });
}
let depthByteMap: DepthByteMap | null = null;
let depthWallDepth: number | null = null;
let depthWallThreshold: number | null = null;
let depthCutoff: number | null = null;
let depthOccluderMask: BinaryMask | null = null;
let depthBackMask: BinaryMask | null = null;
let depthRawDataUrl: string | null = null;
let depthOccluderUrl: string | null = null;
let depthMaskRawUrl: string | null = null;
let depthHistogram: number[] | null = null;
let depthInverted = false;
let depthDebugPayload: any = null;
let depthCrop: { x: number; y: number; width: number; height: number } | null = null;
try {
const depthRaw = await withStepTimeout(
estimateDepth(raw),
depthStageTimeoutMs,
"estimateDepth",
);
depthByteMap = normalizeDepthFloatToByte(depthRaw);
const targetW = meta0.width ?? depthByteMap.width;
const targetH = meta0.height ?? depthByteMap.height;
const cropResult = cropDepthToAspect(depthByteMap, targetW, targetH);
depthByteMap = cropResult.map;
depthCrop = cropResult.crop;
const depthPng = await sharp(Buffer.from(depthByteMap.data), {
raw: { width: depthByteMap.width, height: depthByteMap.height, channels: 1 },
})
.png()
.toBuffer();
const runRefine = async (buffer: Buffer, sensitivity: number) =>
refineOccluderMask(buffer, targetW, targetH, sensitivity, raw);
const scoreCandidate = (candidate: RefinedOccluderResult) => {
if (candidate.rejected) {
return { cov: 0, rowT: 0, colT: 0, gridLike: true, score: 1e9 };
}
const cov = coverageOfMask(candidate.mask);
const rowT = meanBinaryTransitionsPerRow(candidate.mask, candidate.width, candidate.height);
const colT = meanBinaryTransitionsPerCol(candidate.mask, candidate.width, candidate.height);
const gridLike = looksLikeGridBinaryMask(candidate.mask, candidate.width, candidate.height);
// Penalize extreme coverage, but do not treat it as "grid-like" by itself.
const covPenalty = cov < 0.1 ? (0.1 - cov) * 6 : cov > 0.9 ? (cov - 0.9) * 10 : 0;
const score =
(rowT / Math.max(1, candidate.width)) +
(colT / Math.max(1, candidate.height)) +
covPenalty +
(gridLike ? 2 : 0);
return { cov, rowT, colT, gridLike, score };
};
const sensitivities = [18, 22, 26, 30];
const candidates: Array<{ label: string; inverted: boolean; png: Buffer }> = [
{ label: "normal", inverted: false, png: depthPng },
];
// Only pay the cost of inverted evaluation when the normal orientation looks suspicious.
const refinedNormal = await runRefine(depthPng, sensitivities[0]);
const scoreNormal = scoreCandidate(refinedNormal);
if (scoreNormal.gridLike || scoreNormal.cov < 0.02 || scoreNormal.cov > 0.9) {
const inverted = Buffer.from(depthByteMap.data.map((v) => 255 - v));
const invertedPng = await sharp(inverted, {
raw: { width: depthByteMap.width, height: depthByteMap.height, channels: 1 },
})
.png()
.toBuffer();
candidates.push({ label: "inverted", inverted: true, png: invertedPng });
}
let refined: RefinedOccluderResult | null = null;
let chosenScore: ReturnType<typeof scoreCandidate> | null = null;
let sensitivityUsed = sensitivities[0];
let candidateLabel = candidates[0].label;
for (const candidate of candidates) {
for (const sensitivity of sensitivities) {
const candidateRefined = await runRefine(candidate.png, sensitivity);
const candidateScore = scoreCandidate(candidateRefined);
if (!refined || !chosenScore || candidateScore.score < chosenScore.score) {
refined = candidateRefined;
chosenScore = candidateScore;
depthInverted = candidate.inverted;
sensitivityUsed = sensitivity;
candidateLabel = candidate.label;
}
// Early exit on a clearly good mask.
if (!candidateScore.gridLike && candidateScore.cov >= 0.08 && candidateScore.cov <= 0.9) {
break;
}
}
}
if (!refined || !chosenScore) {
throw new Error("[depth] failed to refine occluder mask");
}
if (refined.rejected) {
depthOccluderMask = null;
depthOccluderUrl = null;
depthMaskRawUrl = null;
depthRawDataUrl = await maskToDataUrl(depthByteMap.data, depthByteMap.width, depthByteMap.height);
depthHistogram = refined.histogram ?? null;
depthWallDepth = refined.wallDepth ?? null;
depthWallThreshold = refined.threshold ?? null;
depthCutoff = refined.occluderThreshold ?? null;
depthDebugPayload = {
source: "refineOccluderMask",
rejected: true,
rejectReason: refined.rejected.reason,
rejectMetrics: refined.rejected.metrics ?? null,
inverted: depthInverted,
rawUrl: depthRawDataUrl,
resizedUrl: refined.debug?.resizedDepthUrl ?? null,
};
} else {
// Polarity: The rest of the pipeline expects 255=occluder (blocks tiles), 0=free (wall).
// Depth thresholding can sometimes yield the inverse (255=wall plane). Detect and fix via coverage heuristic.
let polarityInverted = false;
let polarityFixedMask = refined.mask;
let occCoverage = chosenScore.cov;
let autoDilateRadius = 0;
if (occCoverage > 0.65) {
const inv = new Uint8Array(refined.mask.length);
for (let i = 0; i < refined.mask.length; i++) inv[i] = 255 - refined.mask[i];
const invCoverage = coverageOfMask(inv);
if (invCoverage < occCoverage) {
polarityInverted = true;
polarityFixedMask = inv;
}
} else if (occCoverage < 0.01) {
const inv = new Uint8Array(refined.mask.length);
for (let i = 0; i < refined.mask.length; i++) inv[i] = 255 - refined.mask[i];
const invCoverage = coverageOfMask(inv);
if (invCoverage > occCoverage) {
polarityInverted = true;
polarityFixedMask = inv;
}
}
// Recompute from the final mask (source of truth) so debug/metrics match what we actually return.
occCoverage = coverageOfMask(polarityFixedMask);
// Auto-dilate depth occluder if it is too thin/fragmented.
// This stabilizes wall-adjacent objects without requiring U2/heuristics.
if (occCoverage > 0 && occCoverage < 0.20) {
for (const r of [2, 3, 4]) {
const dilated = await dilateBinaryMaskViaBlur(polarityFixedMask, refined.width, refined.height, r, 8);
const cov = coverageOfMask(dilated);
if (cov > occCoverage) {
polarityFixedMask = dilated;
occCoverage = cov;
autoDilateRadius = r;
}
if (occCoverage >= 0.26) break;
}
}
// Optional: shrink the final occluder slightly to reduce visible "halo/cutout" margins.
// This must happen after solidify/dilate so we don’t reintroduce holes.
const shrinkParam = url.searchParams.get("occluder_shrink_px") ?? "";
const requestedShrinkPx = clamp(Number.parseInt(shrinkParam || "0", 10) || 0, 0, 6);
let occluderShrinkPxApplied = 0;
let occluderUrlPreShrink: string | null = null;
if (requestedShrinkPx > 0 && polarityFixedMask.length === refined.width * refined.height) {
const before = polarityFixedMask;
const beforeCov = occCoverage;
const eroded = morphErode(before, refined.width, refined.height, requestedShrinkPx);
const afterCov = coverageOfMask(eroded);
// Guard: don’t collapse masks (e.g., on very thin occluders).
if (afterCov > 0.005 || beforeCov <= 0.02) {
occluderShrinkPxApplied = requestedShrinkPx;
occluderUrlPreShrink = await maskToDataUrl(before, refined.width, refined.height).catch(() => null);
polarityFixedMask = eroded;
occCoverage = afterCov;
}
}
depthOccluderMask = { data: polarityFixedMask, width: refined.width, height: refined.height };
// Always encode from the final mask used by the pipeline, so debug can’t drift.
const depthOccluderUrlPrePolarity = refined.debug?.processedUrl ?? null;
depthOccluderUrl = await maskToDataUrl(polarityFixedMask, refined.width, refined.height);
const depthOccluderUrlCandidate = depthOccluderUrl;
depthMaskRawUrl = refined.debug?.rawUrl ?? null;
const depthResizedUrl = refined.debug?.resizedDepthUrl ?? null;
depthRawDataUrl = await maskToDataUrl(depthByteMap.data, depthByteMap.width, depthByteMap.height);
depthHistogram = refined.histogram;
depthWallDepth = refined.wallDepth;
depthWallThreshold = refined.threshold;
depthCutoff = refined.occluderThreshold;
const occluderMaskSha1 = maskSha1(polarityFixedMask, refined.width, refined.height);
const depthMapSha1 = maskSha1(depthByteMap.data, depthByteMap.width, depthByteMap.height);
const rowTransitions = meanBinaryTransitionsPerRow(polarityFixedMask, refined.width, refined.height);
const colTransitions = meanBinaryTransitionsPerCol(polarityFixedMask, refined.width, refined.height);
let occluderPixels = 0;
for (let i = 0; i < polarityFixedMask.length; i++) {
if (polarityFixedMask[i] > 127) occluderPixels += 1;
}
// Re-check grid-likeness on the final mask (after polarity correction).
// We treat this as a warning only (not a hard rejection), because noisy occluders can have
// high transition counts without being a true tile/checker grid.
const gridLikeWarning = looksLikeGridBinaryMask(polarityFixedMask, refined.width, refined.height);
const rejectedCoverage = occCoverage > 0.98 || occCoverage < 0.002;
const rejectedSolid = occCoverage === 0 || occCoverage === 1;
const rejected = rejectedCoverage || rejectedSolid;
if (rejected) {
console.warn("[depth] rejecting depth occluder", {
reason: rejectedSolid ? "solid-mask" : "coverage-out-of-range",
coverage: Number(occCoverage.toFixed(4)),
rowTransitions: Number(rowTransitions.toFixed(2)),
colTransitions: Number(colTransitions.toFixed(2)),
candidate: candidateLabel,
sensitivityUsed,
occluderMaskSha1,
});
depthOccluderMask = null;
depthOccluderUrl = null;
}
// Build back/plane mask at target size
const resizedDepth = await sharp(Buffer.from(depthByteMap.data), {
raw: { width: depthByteMap.width, height: depthByteMap.height, channels: 1 },
})
.resize(targetW, targetH, { kernel: "lanczos3" })
.toColorspace("b-w")
.raw()
.toBuffer();
const backMask = new Uint8Array(resizedDepth.length);
const backThresh = Math.min(255, refined.threshold + 5);
for (let i = 0; i < resizedDepth.length; i++) {
backMask[i] = resizedDepth[i] <= backThresh ? 255 : 0;
}
depthBackMask = { data: backMask, width: targetW, height: targetH };
const depthOccCov = occCoverage;
const depthBackCov = coverageOfMask(backMask);
depthDebugPayload = {
source: "refineOccluderMask",
candidate: candidateLabel,
sensitivityUsed,
polarityInverted,
autoDilateRadius,
occluderShrinkPx: occluderShrinkPxApplied,
rejected,
gridLikeWarning,
warnings: refined.warnings ?? null,
rejectReason: rejected
? rejectedSolid
? "occluder is solid (all 0 or all 255)"
: "occluder coverage out of range"
: null,
occluderMaskSha1,
depthMapSha1,
occluderTransitionsPerRow: Number(rowTransitions.toFixed(2)),
occluderTransitionsPerCol: Number(colTransitions.toFixed(2)),
wallDepth: depthWallDepth,
wallThreshold: depthWallThreshold,
occlusionThreshold: depthCutoff,
histogram: depthHistogram,
inverted: depthInverted,
occluderCoverage: depthOccCov != null ? Number(depthOccCov.toFixed(4)) : null,
occluderPixels,
occluderTotalPixels: refined.width * refined.height,
backCoverage: depthBackCov != null ? Number(depthBackCov.toFixed(4)) : null,
rawUrl: depthRawDataUrl,
resizedUrl: depthResizedUrl,
maskRawUrl: depthMaskRawUrl,
occluderUrl: depthOccluderUrl,
occluderUrlPreShrink,
occluderUrlRejected: rejected ? depthOccluderUrlCandidate : null,
occluderUrlPrePolarity: depthOccluderUrlPrePolarity,
depthCrop,
};
}
} catch (depthErr) {
if (debug) {
const message =
depthErr instanceof Error
? (depthErr.stack ?? depthErr.message)
: String(depthErr);
console.warn("[depth] failed to compute depth map", depthErr);
depthDebugPayload = {
error: message,
};
}
}
const enableTileSegmentation = tileModelRequested !== 'none';
let tileCombineResult: Awaited<ReturnType<typeof combineTileMaskWithOccluder>> | null = null;
if (meta0.width && meta0.height && enableTileSegmentation) {
try {
if (tileModelRequested === 'unet_latest') {
const tileModule = await import('@/lib/segmentation/tile-unet');
if (typeof tileModule.inferTileSegmentation === 'function') {
tileSegmentationResult = await tileModule.inferTileSegmentation(raw, { debug });
}
} else {
console.warn('[tile-seg] model not implemented, skipping', tileModelRequested);
}
} catch (tileError) {
console.warn('[tile-seg] failed to compute tile mask', tileError);
}
}
// (A) Segmentierung am Original
let maskOrig: Uint8Array | null = null;
let maskOriginSource: string | null = null;
let maskOriginMeta: Record<string, unknown> | null = null;
let maskBoostStats: MaskBoostStats | null = null;
let maskPresetApplied: MaskPresetName | null = null;
let maskNormalized: Uint8Array | null = null;
let maskForWeights: Uint8Array | null = null;
let maskDebugPayload: any = null;
let maskRawDataUrl: string | null = null;
let maskNormalizedDataUrl: string | null = null;
let maskOverlayDataUrl: string | null = null;
let maskCutoutDataUrl: string | null = null;
let maskCoverage: number | null = null;
let maskBackgroundCoverage: number | null = null;
let maskOccluderCoverage: number | null = null;
let maskSeedCoverage: number | null = null;
let finalMaskDataUrl: string | null = null;
let finalMaskMeta: FinalMaskMeta | null = null;
let intensityBytes: Uint8Array | null = null;
let gridContext: GridContext | null = null;
let latticeLeak: number | null = null;
let latticeInteriorCoverage: number | null = null;
let maskWidth: number | null = null;
let maskHeight: number | null = null;
let normalizationPrimary: NormalizeMaskResponse | null = null;
let normalizationChosen: NormalizeMaskResponse | null = null;
let maskNormalizedInitial: Uint8Array | null = null;
let maskNormalizedInitialCoverage: number | null = null;
let pipelineWarnings: string[] | undefined = undefined;
let tileMaskDataUrl: string | null = null;
let tileMaskMeta: TileMaskMeta | null = null;
let tileMaskVersions: TileMaskVersions | null = null;
let samPromptedDebug: Record<string, unknown> | null = null;
let promptTileMask: Uint8Array | null = null;
let promptTileWidth: number | null = null;
let promptTileHeight: number | null = null;
let promptTileSource: string | null = null;
if (useSeg) {
const occluderDebugDir = maskDebugEnabled
? path.join(process.cwd(), "runs", "occluder-debug")
: null;
const workerUrl = process.env.OCCLUDER_WORKER_URL ?? null;
const workerTimeoutMs = Math.max(1_000, Number(process.env.OCCLUDER_WORKER_TIMEOUT ?? "20000"));
const samPromptedFlag = (process.env.OCCLUDER_SAM_PROMPTED ?? "").trim().toLowerCase();
const samAutoEnabled = samPromptedFlag === ""
? (process.env.OCCLUDER_SAM_AUTO ?? "1") !== "0"
: false;
const samPromptedEnabled = samPromptedFlag === "1"
|| (samPromptedFlag !== "0" && (Boolean(process.env.OCCLUDER_SEG_SAM_CHECKPOINT) || samAutoEnabled));
const samTimeoutMs = Math.max(30_000, Number(process.env.OCCLUDER_SEG_SAM_TIMEOUT ?? "180000"));
promptTileMask = samPromptedEnabled ? buildTilePlaneMask(tileSegmentationResult ?? null) : null;
promptTileWidth = tileSegmentationResult?.width ?? null;
promptTileHeight = tileSegmentationResult?.height ?? null;
promptTileSource = promptTileMask ? "tile_unet" : null;
const promptCoverage = coverageOfMask(promptTileMask);
const promptTooWeak = promptCoverage > 0 && promptCoverage < 0.02;
const promptTooStrong = promptCoverage > 0.95;
samPromptedDebug = samPromptedEnabled
? {
enabled: samPromptedEnabled,
mode: samPromptedFlag === "1" ? "forced" : (samPromptedFlag === "0" ? "disabled" : "auto"),
autoEnabled: samAutoEnabled,
ready: false,
source: promptTileSource ?? null,
coverage: Number((promptCoverage || 0).toFixed(4)),
tooWeak: promptTooWeak,
tooStrong: promptTooStrong,
tileMaskSize: promptTileWidth && promptTileHeight ? `${promptTileWidth}x${promptTileHeight}` : null,
}
: null;
if ((samPromptedEnabled && (!promptTileMask || promptTooWeak || promptTooStrong)) && depthBackMask) {
const targetW = meta0.width ?? depthBackMask.width;
const targetH = meta0.height ?? depthBackMask.height;
const depthAligned = await resizeBinaryMask(depthBackMask, targetW, targetH);
promptTileMask = depthAligned.data;
promptTileWidth = depthAligned.width;
promptTileHeight = depthAligned.height;
promptTileSource = "depth_back";
if (samPromptedDebug) {
samPromptedDebug.source = promptTileSource;
samPromptedDebug.coverage = Number(coverageOfMask(promptTileMask).toFixed(4));
samPromptedDebug.tileMaskSize = `${promptTileWidth}x${promptTileHeight}`;
}
}
const promptBandEnabled = (process.env.OCCLUDER_SAM_PROMPT_BAND ?? "0") === "1";
if (samPromptedEnabled && promptTileMask && promptTileWidth && promptTileHeight && promptBandEnabled) {
const banded = bandMaskByRowDensity(promptTileMask, promptTileWidth, promptTileHeight);
if (banded.applied) {
promptTileMask = banded.mask;
promptTileSource = promptTileSource ? `${promptTileSource}+band` : "banded";
if (samPromptedDebug && banded.band) {
samPromptedDebug.source = promptTileSource;
samPromptedDebug.coverage = Number(coverageOfMask(promptTileMask).toFixed(4));
samPromptedDebug.tileMaskSize = `${promptTileWidth}x${promptTileHeight}`;
samPromptedDebug.band = banded.band;
}
}
}
if (samPromptedEnabled && (!promptTileMask || promptTooWeak || promptTooStrong) && meta0.width && meta0.height) {
const topFrac = clamp(Number(process.env.OCCLUDER_SAM_FALLBACK_TOP ?? "0.35"), 0, 0.9);
const bottomFrac = clamp(Number(process.env.OCCLUDER_SAM_FALLBACK_BOTTOM ?? "0.9"), 0.1, 1);
const y0 = Math.max(0, Math.floor(meta0.height * topFrac));
const y1 = Math.max(y0 + 1, Math.floor(meta0.height * bottomFrac));
const band = new Uint8Array(meta0.width * meta0.height);
for (let y = y0; y < y1; y++) {
const row = y * meta0.width;
band.fill(255, row, row + meta0.width);
}
promptTileMask = band;
promptTileWidth = meta0.width;
promptTileHeight = meta0.height;
promptTileSource = "fallback_band";
if (samPromptedDebug) {
samPromptedDebug.source = promptTileSource;
samPromptedDebug.coverage = Number(coverageOfMask(promptTileMask).toFixed(4));
samPromptedDebug.tileMaskSize = `${promptTileWidth}x${promptTileHeight}`;
}
}
const samPromptedReady = Boolean(
samPromptedEnabled &&
promptTileMask &&
promptTileWidth &&
promptTileHeight,
);
if (samPromptedDebug) samPromptedDebug.ready = samPromptedReady;
if (samPromptedEnabled && !samPromptedReady && debug) {
console.warn("[mask] sam_prompted requested but tile mask is missing");
}
const fetchWorkerMask = async () => {
if (!workerUrl) return null;
const form = new FormData();
const blob = new Blob([raw], { type: meta0?.format ? `image/${meta0.format}` : "application/octet-stream" });
form.append("file", blob, (file as any)?.name ?? "upload");
const response = await fetch(workerUrl, {
method: "POST",
body: form,
signal: AbortSignal.timeout(workerTimeoutMs),
});
if (!response.ok) {
if (debug) console.warn(`[mask] occluder-worker responded ${response.status}`);
return null;
}
const payload = await response.json() as any;
if (!payload?.ok || typeof payload.mask !== "string") {
if (debug) console.warn("[mask] occluder-worker invalid payload", payload);
return null;
}
const maskBuffer = Buffer.from(payload.mask, "base64");
let pipeline = sharp(maskBuffer, { failOnError: false })
.ensureAlpha()
.removeAlpha()
.greyscale();
const resizeW = meta0.width ?? payload.width ?? null;
const resizeH = meta0.height ?? payload.height ?? null;
if (resizeW && resizeH) {
pipeline = pipeline.resize(resizeW, resizeH, { fit: "fill" });
}
const { data: maskData, info } = await pipeline
.raw()
.toBuffer({ resolveWithObject: true });
return {
data: new Uint8Array(maskData),
width: info.width ?? resizeW ?? meta0.width ?? 0,
height: info.height ?? resizeH ?? meta0.height ?? 0,
source: payload.source ?? "occluder-worker",
meta: payload.meta ?? null,
coverage: typeof payload.coverage === "number" ? payload.coverage : null,
};
};
if (!maskOrig && workerUrl && !samPromptedReady) {
try {
const payload = await fetchWorkerMask();
if (payload?.data && payload.data.length) {
maskOrig = payload.data;
maskWidth = payload.width || maskWidth;
maskHeight = payload.height || maskHeight;
maskOriginSource = payload.source ?? "occluder-worker";
maskOriginMeta = (payload.meta ?? null) as Record<string, unknown> | null;
if (typeof payload.coverage === "number") {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
workerCoverage: payload.coverage,
};
}
}
} catch (err) {
if (debug) console.warn("[mask] occluder-worker failed", err);
}
}
let occluderSegError: string | null = null;
if (!maskOrig) {
try {
const mod = await import("@/lib/segmentation/occluder_best");
const infer = (mod as any).inferOccluderBest as (buf: Buffer, opts?: unknown) => Promise<{ mask: Uint8Array; source: string; meta?: Record<string, unknown> | null; } | null>;
if (typeof infer === "function") {
const result = await infer(raw, {
debugDir: occluderDebugDir ?? undefined,
tileMask: samPromptedReady ? promptTileMask : undefined,
tileMaskWidth: samPromptedReady ? promptTileWidth : undefined,
tileMaskHeight: samPromptedReady ? promptTileHeight : undefined,
samPrompted: samPromptedReady,
timeoutMs: samPromptedReady ? samTimeoutMs : undefined,
env: effectiveOccluderEnvOverrides ?? undefined,
});
if (result && result.mask && result.mask.length) {
maskOrig = result.mask;
maskOriginSource = result.source ?? "yolo";
maskOriginMeta = result.meta ?? null;
if (effectiveOccluderEnvOverrides) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
occluderEnvOverrides: effectiveOccluderEnvOverrides,
};
}
if (presetComfyRoute) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
occluderPresetBackend: presetBackend,
occluderPresetApplied: true,
occluderMethodEffective: occluderMethod,
uploadFileName: fileName ?? undefined,
};
}
if (samPromptedReady) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
promptTileSource: promptTileSource ?? undefined,
};
}
} else {
occluderSegError = "inferOccluderBest returned empty result";
}
} else {
occluderSegError = "inferOccluderBest export missing";
}
} catch (err) {
occluderSegError = err instanceof Error ? err.message : String(err);
if (debug) console.warn("[mask] occluder-seg failed", err);
}
}
if (!maskOrig && workerUrl && samPromptedReady) {
try {
const payload = await fetchWorkerMask();
if (payload?.data && payload.data.length) {
maskOrig = payload.data;
maskWidth = payload.width || maskWidth;
maskHeight = payload.height || maskHeight;
maskOriginSource = payload.source ?? "occluder-worker";
maskOriginMeta = (payload.meta ?? null) as Record<string, unknown> | null;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPromptedFallback: true,
occluderSegError: occluderSegError ?? undefined,
};
if (typeof payload.coverage === "number") {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
workerCoverage: payload.coverage,
};
}
}
} catch (err) {
if (debug) console.warn("[mask] occluder-worker fallback failed", err);
}
} else if (occluderSegError) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
occluderSegError,
};
}
maskOriginMeta = {
...(maskOriginMeta ?? {}),
occluderMethodResolved: occluderMethod,
};
if (maskOrig && samPromptedReady && workerUrl && !preferSegOccluder) {
try {
const rescueEnabled = (process.env.OCCLUDER_SAM_RESCUE ?? "1") !== "0";
if (rescueEnabled && promptTileMask && promptTileWidth && promptTileHeight) {
const payload = await fetchWorkerMask();
if (payload?.data && payload.data.length) {
let workerMask = payload.data;
if (payload.width !== promptTileWidth || payload.height !== promptTileHeight) {
const aligned = await resizeBinaryMask(
{ data: workerMask, width: payload.width, height: payload.height },
promptTileWidth,
promptTileHeight,
);
workerMask = aligned.data;
}
const rescueBandPx = Math.max(0, Number(process.env.OCCLUDER_SAM_RESCUE_BAND ?? "3"));
const rescueErodePx = Math.max(0, Number(process.env.OCCLUDER_SAM_RESCUE_ERODE ?? "1"));
const rescueAdjPx = Math.max(0, Number(process.env.OCCLUDER_SAM_RESCUE_ADJ ?? "6"));
const adjacency = rescueAdjPx > 0
? morphDilate(maskOrig, promptTileWidth, promptTileHeight, rescueAdjPx)
: maskOrig;
const tileRegion = rescueBandPx > 0
? morphDilate(promptTileMask, promptTileWidth, promptTileHeight, rescueBandPx)
: promptTileMask;
let rescueRegion = tileRegion ? maskOr(tileRegion, adjacency) : adjacency;
if (depthBackMask && depthBackMask.width && depthBackMask.height) {
const aligned = await resizeBinaryMask(
{ data: depthBackMask.data, width: depthBackMask.width, height: depthBackMask.height },
promptTileWidth,
promptTileHeight,
);
const rowThresh = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_RESCUE_ROW_THRESH ?? "0.08")));
const rowMargin = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_RESCUE_ROW_MARGIN ?? "6")));
let yMin = -1;
let yMax = -1;
for (let y = 0; y < aligned.height; y++) {
let count = 0;
const row = y * aligned.width;
for (let x = 0; x < aligned.width; x++) {
if (aligned.data[row + x]) count += 1;
}
if (count / Math.max(1, aligned.width) >= rowThresh) {
if (yMin < 0) yMin = y;
yMax = y;
}
}
if (yMin >= 0 && yMax >= yMin) {
const band = new Uint8Array(aligned.width * aligned.height);
const start = Math.max(0, yMin - rowMargin);
const end = Math.min(aligned.height - 1, yMax + rowMargin);
for (let y = start; y <= end; y++) {
const row = y * aligned.width;
for (let x = 0; x < aligned.width; x++) band[row + x] = 255;
}
rescueRegion = maskOr(rescueRegion, band);
}
}
const rescueSupport = rescueErodePx > 0
? morphErode(workerMask, promptTileWidth, promptTileHeight, rescueErodePx)
: workerMask;
const merged = new Uint8Array(maskOrig);
let rescuePixels = 0;
const limit = Math.min(merged.length, rescueSupport.length, rescueRegion.length);
const minArea = Math.max(1, Number(process.env.OCCLUDER_SAM_RESCUE_MIN_AREA ?? "200"));
const components = connectedComponents(rescueSupport, promptTileWidth, promptTileHeight, minArea);
let mergedComponents = 0;
for (const comp of components) {
let touches = false;
for (const idx of comp.pixels) {
if (idx < limit && rescueRegion[idx]) {
touches = true;
break;
}
}
if (!touches) continue;
mergedComponents += 1;
for (const idx of comp.pixels) {
if (idx < limit && !merged[idx]) {
merged[idx] = 255;
rescuePixels += 1;
}
}
}
if (rescuePixels > 0) {
maskOrig = merged;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
rescue: {
pixels: rescuePixels,
coverage: Number((rescuePixels / merged.length).toFixed(6)),
bandPx: rescueBandPx,
erodePx: rescueErodePx,
adjPx: rescueAdjPx,
minArea,
components: mergedComponents,
source: payload.source ?? "occluder-worker",
},
};
if (debug) {
console.info("[mask] sam rescue merged", {
pixels: rescuePixels,
coverage: Number((rescuePixels / merged.length).toFixed(6)),
components: mergedComponents,
});
}
}
}
}
} catch (err) {
if (debug) console.warn("[mask] sam rescue failed", err);
}
}
if (!maskOrig) {
try {
const mod = await import("@/lib/segmentation/u2net");
const fn = (mod as any).inferU2Mask as (buf: Buffer) => Promise<Uint8Array>;
if (typeof fn === "function") {
maskOrig = await fn(raw);
if (maskOrig) {
maskOriginSource = maskOriginSource ?? "u2net";
}
}
} catch {
maskOrig = null;
}
}
if (!maskOrig) {
try {
const origin = new URL(req.url).origin;
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "/nextjs";
const modelUrl = `${origin}${basePath}/models/u2netp.onnx`;
const wasmCdn = "https://cdn.jsdelivr.net/npm/onnxruntime-web@1.17.1/dist/";
const modW = await import("@/lib/segmentation/u2net-wasm");
const fnW = (modW as any).inferU2MaskWasm as (buf: Buffer, mu: string, wp?: string) => Promise<Uint8Array>;
maskOrig = await fnW(raw, modelUrl, wasmCdn);
if (maskOrig) {
maskOriginSource = maskOriginSource ?? "u2net-wasm";
}
} catch {
maskOrig = null;
}
}
const depthOccCoverage = depthOccluderMask
? (depthDebugPayload?.occluderCoverage ?? coverageOfMask(depthOccluderMask.data))
: null;
const depthColTransitions = depthDebugPayload?.occluderTransitionsPerCol ?? null;
const depthGridLike = depthDebugPayload?.gridLikeWarning ?? false;
const baseOccCoverage = maskOrig ? coverageOfMask(maskOrig) : null;
const depthMaxColTrans = Number(process.env.OCCLUDER_DEPTH_MAX_COL_TRANS ?? "160");
const depthMaxCoverage = Number(process.env.OCCLUDER_DEPTH_MAX_COVERAGE ?? "0.45");
const depthMaxDelta = Number(process.env.OCCLUDER_DEPTH_MAX_DELTA ?? "0.15");
let allowDepthOccluder = Boolean(depthOccluderMask);
let depthClipApplied = false;
const depthSuppressReasons: string[] = [];
if (allowDepthOccluder && preferSegOccluder) {
allowDepthOccluder = false;
depthSuppressReasons.push("method_override");
}
if (allowDepthOccluder && maskOrig) {
if (depthGridLike) {
allowDepthOccluder = false;
depthSuppressReasons.push("grid_like");
}
if (
depthColTransitions != null &&
depthColTransitions > depthMaxColTrans &&
depthOccCoverage != null &&
depthOccCoverage > 0.18
) {
allowDepthOccluder = false;
depthSuppressReasons.push("col_transitions");
}
if (depthOccCoverage != null && depthOccCoverage > depthMaxCoverage) {
allowDepthOccluder = false;
depthSuppressReasons.push("coverage_high");
}
if (
baseOccCoverage != null &&
depthOccCoverage != null &&
depthOccCoverage > baseOccCoverage + depthMaxDelta
) {
allowDepthOccluder = false;
depthSuppressReasons.push("larger_than_base");
}
}
if (!allowDepthOccluder && depthSuppressReasons.length) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthOccluderSuppressed: {
reasons: depthSuppressReasons,
coverage: depthOccCoverage != null ? Number(depthOccCoverage.toFixed(4)) : null,
baseCoverage: baseOccCoverage != null ? Number(baseOccCoverage.toFixed(4)) : null,
colTransitions: depthColTransitions,
gridLike: depthGridLike,
},
};
}
const depthClipEnabled = (process.env.OCCLUDER_SAM_DEPTH_CLIP ?? "0") === "1";
if (depthClipEnabled && preferSegOccluder && maskOrig && depthOccluderMask) {
const targetW = maskWidth ?? depthOccluderMask.width;
const targetH = maskHeight ?? depthOccluderMask.height;
const aligned = (targetW && targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const dilatePx = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_DEPTH_CLIP_DILATE ?? "2")));
const clipMask = (dilatePx > 0 && aligned.width && aligned.height)
? morphDilate(aligned.data, aligned.width, aligned.height, dilatePx)
: aligned.data;
const clipped = maskAnd(maskOrig, clipMask);
const beforeCoverage = coverageOfMask(maskOrig);
const afterCoverage = coverageOfMask(clipped);
const minFrac = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_DEPTH_CLIP_MIN_FRAC ?? "0.4")));
const minCoverage = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_DEPTH_CLIP_MIN_COVERAGE ?? "0.05")));
if (afterCoverage >= minCoverage && (beforeCoverage <= 0 || afterCoverage / beforeCoverage >= minFrac)) {
maskOrig = clipped;
maskWidth = aligned.width;
maskHeight = aligned.height;
depthClipApplied = true;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthClip: {
applied: true,
dilatePx,
beforeCoverage: Number(beforeCoverage.toFixed(4)),
afterCoverage: Number(afterCoverage.toFixed(4)),
minFrac,
minCoverage,
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthClip: {
applied: false,
reason: "coverage_too_small",
dilatePx,
beforeCoverage: Number(beforeCoverage.toFixed(4)),
afterCoverage: Number(afterCoverage.toFixed(4)),
minFrac,
minCoverage,
},
};
}
}
const promptClipEnabled = (process.env.OCCLUDER_SAM_PROMPT_CLIP ?? "0") === "1";
if (
promptClipEnabled &&
preferSegOccluder &&
maskOrig &&
promptTileMask &&
promptTileWidth &&
promptTileHeight
) {
const targetW = maskWidth ?? promptTileWidth;
const targetH = maskHeight ?? promptTileHeight;
const promptAligned = (targetW && targetH)
? await resizeBinaryMask(
{ data: promptTileMask, width: promptTileWidth, height: promptTileHeight },
targetW,
targetH,
)
: { data: promptTileMask, width: promptTileWidth, height: promptTileHeight };
const banded = bandMaskByRowDensity(promptAligned.data, promptAligned.width, promptAligned.height);
const band = banded.applied && banded.band
? { yMin: banded.band.yMin, yMax: banded.band.yMax }
: bandFromNonZeroRows(promptAligned.data, promptAligned.width, promptAligned.height);
const padFrac = Math.max(0, Number(process.env.OCCLUDER_SAM_PROMPT_CLIP_PAD_FRAC ?? "0.03"));
const padPx = Math.max(0, Math.round(promptAligned.height * padFrac));
let clipMask = promptAligned.data;
let clipBand: { yMin: number; yMax: number } | null = null;
if (band) {
const yMin = Math.max(0, band.yMin - padPx);
const yMax = Math.min(promptAligned.height, band.yMax + padPx);
const full = new Uint8Array(promptAligned.width * promptAligned.height);
full.fill(255);
clipMask = applyBandToMask(full, promptAligned.width, promptAligned.height, yMin, yMax);
clipBand = { yMin, yMax };
}
const dilatePx = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PROMPT_CLIP_DILATE ?? "0")));
const clipMaskDilated = dilatePx > 0
? morphDilate(clipMask, promptAligned.width, promptAligned.height, dilatePx)
: clipMask;
const clipped = maskAnd(maskOrig, clipMaskDilated);
const beforeCoverage = coverageOfMask(maskOrig);
const afterCoverage = coverageOfMask(clipped);
const minFrac = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_PROMPT_CLIP_MIN_FRAC ?? "0.2")));
const minCoverage = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_PROMPT_CLIP_MIN_COVERAGE ?? "0.02")));
if (afterCoverage >= minCoverage && (beforeCoverage <= 0 || afterCoverage / beforeCoverage >= minFrac)) {
maskOrig = clipped;
maskWidth = promptAligned.width;
maskHeight = promptAligned.height;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
promptClip: {
applied: true,
dilatePx,
beforeCoverage: Number(beforeCoverage.toFixed(4)),
afterCoverage: Number(afterCoverage.toFixed(4)),
minFrac,
minCoverage,
band: clipBand,
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
promptClip: {
applied: false,
reason: "coverage_too_small",
dilatePx,
beforeCoverage: Number(beforeCoverage.toFixed(4)),
afterCoverage: Number(afterCoverage.toFixed(4)),
minFrac,
minCoverage,
band: clipBand,
},
};
}
}
const promptRescueEnabled = (process.env.OCCLUDER_SAM_PROMPT_RESCUE ?? "1") !== "0";
if (
promptRescueEnabled &&
preferSegOccluder &&
maskOrig &&
depthOccluderMask &&
promptTileMask &&
promptTileWidth &&
promptTileHeight
) {
const baseCoverage = coverageOfMask(maskOrig);
const minRescueCoverage = Math.max(0, Math.min(1, Number(process.env.OCCLUDER_SAM_PROMPT_RESCUE_MIN_COVERAGE ?? "0.06")));
if (baseCoverage < minRescueCoverage) {
const targetW = maskWidth ?? promptTileWidth ?? depthOccluderMask.width;
const targetH = maskHeight ?? promptTileHeight ?? depthOccluderMask.height;
const promptAligned = (targetW && targetH)
? await resizeBinaryMask(
{ data: promptTileMask, width: promptTileWidth, height: promptTileHeight },
targetW,
targetH,
)
: { data: promptTileMask, width: promptTileWidth, height: promptTileHeight };
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const rowBanded = bandMaskByRowDensity(promptAligned.data, promptAligned.width, promptAligned.height);
let bandMask = rowBanded.applied ? rowBanded.mask : null;
let bandMeta: { yMin: number; yMax: number } | null = null;
if (!bandMask) {
const extent = bandFromNonZeroRows(promptAligned.data, promptAligned.width, promptAligned.height);
if (extent) {
bandMask = applyBandToMask(
new Uint8Array(promptAligned.width * promptAligned.height).fill(255),
promptAligned.width,
promptAligned.height,
extent.yMin,
extent.yMax,
);
bandMeta = { yMin: extent.yMin, yMax: extent.yMax };
}
} else if (rowBanded.band) {
bandMeta = { yMin: rowBanded.band.yMin, yMax: rowBanded.band.yMax };
}
if (bandMask) {
const bandDilate = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PROMPT_RESCUE_BAND_DILATE ?? "2")));
const bandMaskDilated = bandDilate > 0
? morphDilate(bandMask, promptAligned.width, promptAligned.height, bandDilate)
: bandMask;
const bandCoverage = coverageOfMask(bandMaskDilated);
const maxBandCoverage = clamp(
Number(process.env.OCCLUDER_SAM_PROMPT_RESCUE_MAX_BAND_COVERAGE ?? "0.5"),
0.1,
1,
);
if (bandCoverage <= maxBandCoverage) {
const clipped = maskAnd(depthAligned.data, bandMaskDilated);
const beforeCoverage = coverageOfMask(maskOrig);
const merged = maskOr(maskOrig, clipped);
const afterCoverage = coverageOfMask(merged);
maskOrig = merged;
maskWidth = targetW;
maskHeight = targetH;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
promptRescue: {
applied: true,
bandDilate,
bandCoverage: Number(bandCoverage.toFixed(4)),
maxBandCoverage: Number(maxBandCoverage.toFixed(3)),
band: bandMeta,
coverageBefore: Number(beforeCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
promptRescue: {
applied: false,
reason: "band_too_large",
bandCoverage: Number(bandCoverage.toFixed(4)),
maxBandCoverage: Number(maxBandCoverage.toFixed(3)),
band: bandMeta,
},
};
}
}
}
}
if (allowDepthOccluder && depthOccluderMask) {
if (depthClipApplied) {
// If we already clipped to depth, skip union to avoid re-expanding.
allowDepthOccluder = false;
}
}
const boardUnionMinCoverage = Math.max(
0,
Math.min(1, Number(process.env.OCCLUDER_SAM_BOARD_UNION_MIN_COVERAGE ?? "0.4")),
);
if (boardUnionMinCoverage > 0 && maskOrig && depthOccluderMask) {
const currentCoverage = coverageOfMask(maskOrig);
if (currentCoverage < boardUnionMinCoverage) {
const targetW = meta0.width ?? promptTileWidth ?? depthOccluderMask.width;
const targetH = meta0.height ?? promptTileHeight ?? depthOccluderMask.height;
if (targetW && targetH && maskOrig.length === targetW * targetH) {
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const dilatePx = Math.max(
0,
Math.round(Number(process.env.OCCLUDER_SAM_BOARD_UNION_DILATE ?? "6")),
);
const boardMask = dilatePx > 0
? morphDilate(depthAligned.data, targetW, targetH, dilatePx)
: depthAligned.data;
const merged = maskOr(maskOrig, boardMask);
const afterCoverage = coverageOfMask(merged);
const maxBoardCoverage = Math.min(
1,
Number(process.env.OCCLUDER_SAM_BOARD_UNION_MAX_COVERAGE ?? "0.9"),
);
if (afterCoverage <= maxBoardCoverage) {
maskOrig = merged;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBoardUnion: {
applied: true,
dilatePx,
coverageBefore: Number(currentCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
minCoverage: Number(boardUnionMinCoverage.toFixed(3)),
maxCoverage: Number(maxBoardCoverage.toFixed(3)),
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBoardUnion: {
applied: false,
reason: "union_too_large",
dilatePx,
coverageBefore: Number(currentCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
minCoverage: Number(boardUnionMinCoverage.toFixed(3)),
maxCoverage: Number(maxBoardCoverage.toFixed(3)),
},
};
}
}
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBoardUnion: {
applied: false,
reason: "coverage_ok",
coverage: Number(currentCoverage.toFixed(4)),
target: Number(boardUnionMinCoverage.toFixed(3)),
},
};
}
}
if (allowDepthOccluder && depthOccluderMask) {
const targetW = meta0.width ?? depthOccluderMask.width;
const targetH = meta0.height ?? depthOccluderMask.height;
const depthMaskAligned = targetW && targetH
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
if (maskOrig) {
const baseMask: BinaryMask = {
data: maskOrig,
width: maskWidth ?? depthMaskAligned.width,
height: maskHeight ?? depthMaskAligned.height,
};
const baseAligned = (baseMask.width !== depthMaskAligned.width || baseMask.height !== depthMaskAligned.height)
? await resizeBinaryMask(baseMask, depthMaskAligned.width, depthMaskAligned.height)
: baseMask;
const merged = mergeOccluderMasks(baseAligned, depthMaskAligned);
maskOrig = merged.data;
maskWidth = merged.width;
maskHeight = merged.height;
} else {
maskOrig = depthMaskAligned.data;
maskWidth = depthMaskAligned.width;
maskHeight = depthMaskAligned.height;
maskOriginSource = maskOriginSource ?? "depth";
}
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthOccluder: true,
wallDepth: depthWallDepth ?? undefined,
};
}
if (preferSegOccluder && maskOrig && depthOccluderMask && promptTileMask && promptTileWidth && promptTileHeight) {
const forceBandMinCoverage = Math.max(
0,
Math.min(1, Number(process.env.OCCLUDER_SAM_DEPTH_BAND_FORCE_MIN_COVERAGE ?? "0")),
);
const currentCoverage = coverageOfMask(maskOrig);
const forceDepthBand = forceBandMinCoverage > 0 && currentCoverage < forceBandMinCoverage;
if (preferTileBand && !forceDepthBand) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "deferred_tile_band",
...(forceBandMinCoverage > 0 ? {
forceBandMinCoverage: Number(forceBandMinCoverage.toFixed(3)),
currentCoverage: Number(currentCoverage.toFixed(4)),
} : {}),
},
};
} else {
const bandMinHeightFrac = Number(process.env.OCCLUDER_SAM_DEPTH_BAND_MIN_HEIGHT_FRAC ?? "0.25");
const bandMaxHeightFrac = Number(process.env.OCCLUDER_SAM_BAND_MAX_HEIGHT_FRAC ?? "0.6");
const bandMinConf = clamp(
Number(process.env.OCCLUDER_SAM_DEPTH_BAND_MIN_CONF ?? "0.4"),
0,
1,
);
const bandMaxCoverage = clamp(
Number(process.env.OCCLUDER_SAM_DEPTH_BAND_MAX_COVERAGE ?? "0.6"),
0.1,
1,
);
const minHeight = Math.max(4, Math.round(promptTileHeight * bandMinHeightFrac));
const maxHeight = Math.max(minHeight, Math.round(promptTileHeight * bandMaxHeightFrac));
let banded: { mask: Uint8Array; band: Record<string, unknown>; source: string } | null = null;
let rejectedBand: { source: string; confidence: number } | null = null;
const pick = pickTileBandFromSegmentation(
tileSegmentationResult,
promptTileMask,
promptTileWidth,
promptTileHeight,
);
if (pick) {
const pickConf = Number.isFinite(pick.band.confidence)
? Number(pick.band.confidence)
: 0;
if (pickConf >= bandMinConf) {
const normalized = normalizeBandRange(
pick.band.yMin,
pick.band.yMax,
promptTileHeight,
minHeight,
maxHeight,
);
if (normalized) {
banded = {
mask: applyBandToMask(
promptTileMask,
promptTileWidth,
promptTileHeight,
normalized.yMin,
normalized.yMax,
),
band: { ...pick.band, adjusted: normalized.adjusted },
source: pick.source,
};
}
} else {
rejectedBand = { source: pick.source, confidence: pickConf };
}
}
if (!banded) {
const extent = bandFromNonZeroRows(promptTileMask, promptTileWidth, promptTileHeight);
if (extent) {
const normalized = normalizeBandRange(
extent.yMin,
extent.yMax,
promptTileHeight,
minHeight,
maxHeight,
);
if (normalized) {
banded = {
mask: applyBandToMask(
promptTileMask,
promptTileWidth,
promptTileHeight,
normalized.yMin,
normalized.yMax,
),
band: {
yMin: normalized.yMin,
yMax: normalized.yMax,
adjusted: normalized.adjusted,
...(rejectedBand ? { confidence: rejectedBand.confidence } : {}),
},
source: rejectedBand ? "tile_extent_low_conf" : "tile_extent",
};
}
}
}
if (!banded) {
const rowBanded = bandMaskByRowDensity(promptTileMask, promptTileWidth, promptTileHeight);
if (rowBanded.applied && rowBanded.band) {
banded = { mask: rowBanded.mask, band: rowBanded.band, source: "row_density" };
}
}
if (!banded) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "band_unavailable",
bandRejected: rejectedBand ? {
source: rejectedBand.source,
confidence: Number(rejectedBand.confidence.toFixed(3)),
minConfidence: Number(bandMinConf.toFixed(3)),
} : null,
},
};
}
const bandBase = banded?.mask ?? null;
const targetW = meta0.width ?? promptTileWidth ?? depthOccluderMask.width;
const targetH = meta0.height ?? promptTileHeight ?? depthOccluderMask.height;
if (bandBase && targetW && targetH && maskOrig.length === targetW * targetH) {
const bandDilate = Math.max(
0,
Math.round(Number(process.env.OCCLUDER_TILE_BAND_DILATE ?? "6")),
);
const bandMask = bandDilate > 0
? morphDilate(bandBase, promptTileWidth, promptTileHeight, bandDilate)
: bandBase;
const bandCoverage = coverageOfMask(bandMask);
if (bandCoverage > bandMaxCoverage) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "band_too_large",
bandCoverage: Number(bandCoverage.toFixed(4)),
bandMaxCoverage: Number(bandMaxCoverage.toFixed(3)),
bandSource: banded?.source ?? null,
band: banded?.band ?? null,
bandRejected: rejectedBand ? {
source: rejectedBand.source,
confidence: Number(rejectedBand.confidence.toFixed(3)),
minConfidence: Number(bandMinConf.toFixed(3)),
} : null,
},
};
} else {
const depthAligned = (depthOccluderMask.width !== promptTileWidth || depthOccluderMask.height !== promptTileHeight)
? await resizeBinaryMask(depthOccluderMask, promptTileWidth, promptTileHeight)
: depthOccluderMask;
const clipped = maskAnd(depthAligned.data, bandMask);
const beforeCoverage = coverageOfMask(maskOrig);
const merged = maskOr(maskOrig, clipped);
const afterCoverage = coverageOfMask(merged);
maskOrig = merged;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: true,
bandApplied: true,
bandDilate,
bandCoverage: Number(bandCoverage.toFixed(4)),
bandMaxCoverage: Number(bandMaxCoverage.toFixed(3)),
bandSource: banded?.source ?? null,
band: banded?.band ?? null,
bandRejected: rejectedBand ? {
source: rejectedBand.source,
confidence: Number(rejectedBand.confidence.toFixed(3)),
minConfidence: Number(bandMinConf.toFixed(3)),
} : null,
coverageBefore: Number(beforeCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
},
};
if (maskDebugPayload?.originMeta) {
maskDebugPayload.originMeta = maskOriginMeta;
}
}
}
}
const tileUnionMinCoverage = Math.max(
0,
Math.min(1, Number(process.env.OCCLUDER_SAM_DEPTH_TILE_UNION_MIN_COVERAGE ?? "0")),
);
if (tileUnionMinCoverage > 0 && maskOrig && depthOccluderMask) {
const coverageNow = coverageOfMask(maskOrig);
if (coverageNow < tileUnionMinCoverage) {
const targetW = meta0.width ?? promptTileWidth ?? depthOccluderMask.width;
const targetH = meta0.height ?? promptTileHeight ?? depthOccluderMask.height;
if (targetW && targetH && maskOrig.length === targetW * targetH) {
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const tileAlignedMask = (promptTileWidth !== targetW || promptTileHeight !== targetH)
? await resizeBinaryMask(
{ data: promptTileMask, width: promptTileWidth, height: promptTileHeight },
targetW,
targetH,
)
: { data: promptTileMask, width: targetW, height: targetH };
const dilatePx = Math.max(
0,
Math.round(Number(process.env.OCCLUDER_SAM_DEPTH_TILE_UNION_DILATE ?? "2")),
);
const tileMaskDilated = dilatePx > 0
? morphDilate(tileAlignedMask.data, targetW, targetH, dilatePx)
: tileAlignedMask.data;
const clipped = maskAnd(depthAligned.data, tileMaskDilated);
const merged = maskOr(maskOrig, clipped);
const afterCoverage = coverageOfMask(merged);
const maxCoverage = Math.max(
0,
Math.min(1, Number(process.env.OCCLUDER_SAM_DEPTH_TILE_UNION_MAX_COVERAGE ?? "0.6")),
);
if (afterCoverage <= maxCoverage) {
maskOrig = merged;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthTileUnion: {
applied: true,
dilatePx,
coverageBefore: Number(coverageNow.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
minCoverage: Number(tileUnionMinCoverage.toFixed(3)),
maxCoverage: Number(maxCoverage.toFixed(3)),
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthTileUnion: {
applied: false,
reason: "union_too_large",
dilatePx,
coverageBefore: Number(coverageNow.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
minCoverage: Number(tileUnionMinCoverage.toFixed(3)),
maxCoverage: Number(maxCoverage.toFixed(3)),
},
};
}
}
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthTileUnion: {
applied: false,
reason: "coverage_ok",
coverage: Number(coverageNow.toFixed(4)),
minCoverage: Number(tileUnionMinCoverage.toFixed(3)),
},
};
}
}
}
const samPatchEnabled = Number(process.env.OCCLUDER_SAM_PATCH_FNS ?? "0") > 0;
const fnAssistEnabled = (process.env.OCCLUDER_FN_ASSIST_UNION ?? "1") !== "0";
const fnAssistWithoutSam = (process.env.OCCLUDER_FN_ASSIST_WITHOUT_SAM ?? "0") !== "0";
const shouldRunFnAssist = fnAssistEnabled && (samPatchEnabled || fnAssistWithoutSam);
if ((samPatchEnabled || shouldRunFnAssist) && maskOrig && depthOccluderMask) {
const targetW = maskWidth ?? meta0.width ?? depthOccluderMask.width;
const targetH = maskHeight ?? meta0.height ?? depthOccluderMask.height;
if (targetW && targetH && maskOrig.length === targetW * targetH) {
const baseMask: BinaryMask = { data: maskOrig, width: targetW, height: targetH };
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const tileBandOnly = (process.env.OCCLUDER_SAM_PATCH_TILE_BAND_ONLY ?? "1") !== "0";
const tileBandPad = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PATCH_TILE_BAND_PAD ?? "12")));
let tileBandMask: Uint8Array | null = null;
if (tileBandOnly && tileMaskMeta?.tileBand) {
const yMin = Math.max(0, Math.min(targetH - 1, tileMaskMeta.tileBand.yMin - tileBandPad));
const yMax = Math.max(0, Math.min(targetH - 1, tileMaskMeta.tileBand.yMax + tileBandPad));
if (yMax >= yMin) {
tileBandMask = new Uint8Array(targetW * targetH);
for (let y = yMin; y <= yMax; y++) {
const rowStart = y * targetW;
tileBandMask.fill(255, rowStart, rowStart + targetW);
}
}
}
const applyTileBandMask = (mask: Uint8Array) => (tileBandMask ? maskAnd(mask, tileBandMask) : mask);
const invertMask = (input: Uint8Array) => {
const out = new Uint8Array(input.length);
for (let i = 0; i < input.length; i++) out[i] = input[i] > 0 ? 0 : 255;
return out;
};
let fnMask = new Uint8Array(targetW * targetH);
let fnPixels = 0;
let fnSource = "depth";
for (let i = 0; i < fnMask.length; i++) {
if (depthAligned.data[i] > 0 && baseMask.data[i] === 0) {
fnMask[i] = 255;
fnPixels += 1;
}
}
if (tileBandMask) {
fnMask = applyTileBandMask(fnMask);
fnPixels = 0;
for (let i = 0; i < fnMask.length; i++) {
if (fnMask[i] > 0) fnPixels += 1;
}
}
const minFnCoverage = clamp(
Number(process.env.OCCLUDER_SAM_PATCH_MIN_FN_COVERAGE ?? "0.002"),
0,
1,
);
let fnCoverage = fnPixels / Math.max(1, fnMask.length);
const patchRequireTile = (process.env.OCCLUDER_SAM_PATCH_REQUIRE_TILE ?? "0") !== "0";
let tileDilatePx = 0;
let tileMaskDilated: Uint8Array | null = null;
if (promptTileMask && promptTileWidth && promptTileHeight) {
const tileAligned = (promptTileWidth !== targetW || promptTileHeight !== targetH)
? await resizeBinaryMask(
{ data: promptTileMask, width: promptTileWidth, height: promptTileHeight },
targetW,
targetH,
)
: { data: promptTileMask, width: targetW, height: targetH };
tileDilatePx = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PATCH_TILE_DILATE ?? "2")));
tileMaskDilated = tileDilatePx > 0
? morphDilate(tileAligned.data, targetW, targetH, tileDilatePx)
: tileAligned.data;
if (patchRequireTile) {
fnMask = maskAnd(fnMask, tileMaskDilated);
fnPixels = 0;
for (let i = 0; i < fnMask.length; i++) {
if (fnMask[i] > 0) fnPixels += 1;
}
fnCoverage = fnPixels / Math.max(1, fnMask.length);
}
}
const minCompPx = Math.max(50, Number(process.env.OCCLUDER_SAM_PATCH_MIN_COMP ?? "200"));
const maxComps = Math.max(1, Number(process.env.OCCLUDER_SAM_PATCH_MAX_COMP ?? "8"));
const maxCompFrac = clamp(
Number(process.env.OCCLUDER_SAM_PATCH_MAX_COMP_FRAC ?? "0.2"),
0,
1,
);
let baseArea = fnMask.length;
if (tileMaskDilated) {
let tileCount = 0;
for (let i = 0; i < tileMaskDilated.length; i++) {
if (tileMaskDilated[i] > 0) tileCount += 1;
}
if (tileCount > 0) baseArea = tileCount;
}
const maxCompPx = maxCompFrac > 0 ? Math.max(1, Math.round(maxCompFrac * baseArea)) : 0;
const limitFnComponents = (inputMask: Uint8Array) => {
const comps = connectedComponents(inputMask, targetW, targetH, minCompPx);
if (comps.length === 0) {
return new Uint8Array(inputMask);
}
let filtered = comps;
if (maxCompPx > 0) {
filtered = comps.filter((comp) => comp.pixels.length <= maxCompPx);
}
if (filtered.length === 0) {
return new Uint8Array(inputMask.length);
}
filtered.sort((a, b) => b.pixels.length - a.pixels.length);
const limited = filtered.slice(0, maxComps);
const limitedMask = new Uint8Array(inputMask.length);
for (const comp of limited) {
for (const idx of comp.pixels) {
limitedMask[idx] = 255;
}
}
return limitedMask;
};
fnMask = limitFnComponents(fnMask);
fnPixels = 0;
for (let i = 0; i < fnMask.length; i++) {
if (fnMask[i] > 0) fnPixels += 1;
}
fnCoverage = fnPixels / Math.max(1, fnMask.length);
const useBackFallback = (process.env.OCCLUDER_SAM_PATCH_USE_BACK ?? "1") !== "0";
const unionBack = (process.env.OCCLUDER_SAM_PATCH_UNION_BACK ?? "1") !== "0";
if (useBackFallback && depthBackMask) {
const backAligned = (depthBackMask.width !== targetW || depthBackMask.height !== targetH)
? await resizeBinaryMask(depthBackMask, targetW, targetH)
: depthBackMask;
let candidate = invertMask(backAligned.data);
if (patchRequireTile && tileMaskDilated) {
candidate = maskAnd(candidate, tileMaskDilated);
}
const baseInv = invertMask(baseMask.data);
const backCandidate = applyTileBandMask(maskAnd(candidate, baseInv));
if (unionBack) {
fnMask = maskOr(fnMask, backCandidate);
if (fnSource === "depth") fnSource = "depth+back";
} else if (fnCoverage < minFnCoverage) {
fnMask = backCandidate;
fnSource = "back";
}
fnMask = limitFnComponents(fnMask);
fnPixels = 0;
for (let i = 0; i < fnMask.length; i++) {
if (fnMask[i] > 0) fnPixels += 1;
}
fnCoverage = fnPixels / Math.max(1, fnMask.length);
}
if (shouldRunFnAssist) {
const assistMinComp = Math.max(8, Math.round(Number(process.env.OCCLUDER_FN_ASSIST_MIN_COMP ?? "30")));
const assistMaxComp = Math.max(assistMinComp, Math.round(Number(process.env.OCCLUDER_FN_ASSIST_MAX_COMP ?? "6000")));
const assistAdjPx = Math.max(0, Math.round(Number(process.env.OCCLUDER_FN_ASSIST_ADJ_PX ?? "8")));
const assistTileOnly = (process.env.OCCLUDER_FN_ASSIST_TILE_ONLY ?? "1") !== "0";
const baseAdj = assistAdjPx > 0
? morphDilate(maskOrig, targetW, targetH, assistAdjPx)
: maskOrig;
const fnComps = connectedComponents(fnMask, targetW, targetH, assistMinComp);
let assistAdded = 0;
for (const comp of fnComps) {
if (comp.pixels.length < assistMinComp || comp.pixels.length > assistMaxComp) continue;
let touchesAdj = false;
let touchesTile = false;
for (const idx of comp.pixels) {
if (!touchesAdj && baseAdj[idx] > 0) touchesAdj = true;
if (!touchesTile && tileMaskDilated && tileMaskDilated[idx] > 0) touchesTile = true;
if (touchesAdj && (touchesTile || !assistTileOnly)) break;
}
if (!touchesAdj) continue;
if (assistTileOnly && tileMaskDilated && !touchesTile) continue;
for (const idx of comp.pixels) {
if (maskOrig[idx] === 0) {
maskOrig[idx] = 255;
assistAdded += 1;
}
}
}
if (assistAdded > 0) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
fnAssist: {
applied: true,
addedPixels: assistAdded,
minComp: assistMinComp,
maxComp: assistMaxComp,
adjPx: assistAdjPx,
tileOnly: assistTileOnly,
},
};
}
}
if (samPatchEnabled && fnCoverage >= minFnCoverage) {
let patchMask = await runSamHqPatchFromBuffer(raw, fnMask, targetW, targetH);
if (patchMask && patchMask.length === maskOrig.length) {
const patchGrowPx = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PATCH_GROW ?? "10")));
if (patchGrowPx > 0) {
const fnGrow = morphDilate(fnMask, targetW, targetH, patchGrowPx);
patchMask = maskAnd(patchMask, fnGrow);
}
const adjPx = Math.max(0, Math.round(Number(process.env.OCCLUDER_SAM_PATCH_ADJ_PX ?? "12")));
if (adjPx > 0) {
const adjacency = morphDilate(maskOrig, targetW, targetH, adjPx);
const fnAdjacency = morphDilate(fnMask, targetW, targetH, Math.max(1, Math.floor(adjPx / 2)));
const allowed = patchRequireTile && tileMaskDilated
? maskOr(maskOr(adjacency, fnAdjacency), tileMaskDilated)
: maskOr(adjacency, fnAdjacency);
patchMask = maskAnd(patchMask, allowed);
}
const beforeCoverage = coverageOfMask(maskOrig);
const beforePixels = Math.round(beforeCoverage * patchMask.length);
const maxCoverage = clamp(
Number(process.env.OCCLUDER_SAM_PATCH_MAX_COVERAGE ?? "0.55"),
0.1,
1,
);
const maxAddFrac = clamp(
Number(process.env.OCCLUDER_SAM_PATCH_MAX_ADD_FRAC ?? "0.15"),
0,
1,
);
const maxAddedByCoverage = Math.max(0, Math.floor((maxCoverage * patchMask.length) - beforePixels));
const maxAddedByFrac = Math.max(0, Math.floor(maxAddFrac * patchMask.length));
const sampleName = typeof (file as any)?.name === "string"
? String((file as any).name)
: null;
const maxAddPixelsCap = resolveSamPatchMaxAddPixelsCap(sampleName);
const maxAddedPixels = Math.min(maxAddedByCoverage, maxAddedByFrac, maxAddPixelsCap.cap);
const maxAddPixelsCapValue = Number.isFinite(maxAddPixelsCap.cap)
? Math.max(0, Math.floor(maxAddPixelsCap.cap))
: null;
let addedPixels = 0;
for (let i = 0; i < patchMask.length; i++) {
if (patchMask[i] > 0 && maskOrig[i] === 0) addedPixels += 1;
}
let trimIterations = 0;
if (addedPixels > maxAddedPixels && maxAddedPixels > 0) {
let trimmed = patchMask;
let prevTrimmed = patchMask;
let nextAdded = addedPixels;
// Reduce oversized SAM patch gradually instead of hard reject.
while (nextAdded > maxAddedPixels && trimIterations < 10) {
prevTrimmed = trimmed;
trimmed = morphErode(trimmed, targetW, targetH, 1);
trimIterations += 1;
nextAdded = 0;
for (let i = 0; i < trimmed.length; i++) {
if (trimmed[i] > 0 && maskOrig[i] === 0) nextAdded += 1;
}
}
// If one erosion step undershoots heavily, add back a bounded ring so
// maxAddFrac/maxCoverage can steer the final patch size continuously.
if (nextAdded < maxAddedPixels && trimIterations > 0) {
const budget = maxAddedPixels - nextAdded;
if (budget > 0) {
const ring = new Uint8Array(trimmed.length);
for (let i = 0; i < trimmed.length; i++) {
if (prevTrimmed[i] > 0 && trimmed[i] === 0 && maskOrig[i] === 0) ring[i] = 255;
}
let restored = 0;
// Prefer ring pixels that touch trimmed mask to keep contour smooth.
for (let i = 0; i < ring.length && restored < budget; i++) {
if (ring[i] === 0) continue;
const x = i % targetW;
const y = (i / targetW) | 0;
const touching =
(x > 0 && trimmed[i - 1] > 0) ||
(x + 1 < targetW && trimmed[i + 1] > 0) ||
(y > 0 && trimmed[i - targetW] > 0) ||
(y + 1 < targetH && trimmed[i + targetW] > 0);
if (!touching) continue;
trimmed[i] = 255;
restored += 1;
}
// Fill remaining budget from ring if needed.
for (let i = 0; i < ring.length && restored < budget; i++) {
if (ring[i] === 0 || trimmed[i] > 0) continue;
trimmed[i] = 255;
restored += 1;
}
nextAdded += restored;
}
}
patchMask = trimmed;
addedPixels = nextAdded;
}
const merged = maskOr(maskOrig, patchMask);
const afterCoverage = coverageOfMask(merged);
const addFrac = Math.max(0, afterCoverage - beforeCoverage);
if (addedPixels > 0 && afterCoverage <= maxCoverage && addFrac <= maxAddFrac) {
maskOrig = merged;
maskWidth = targetW;
maskHeight = targetH;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPatch: {
applied: true,
fnSource,
fnCoverage: Number(fnCoverage.toFixed(4)),
fnPixels,
addedPixels,
coverageBefore: Number(beforeCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
tileDilatePx,
adjPx,
patchRequireTile,
tileBandOnly,
tileBandPad,
trimIterations,
maxAddedPixels,
maxAddPixelsCap: maxAddPixelsCapValue,
maxAddPixelsScope: maxAddPixelsCap.scope,
maxCoverage,
maxAddFrac: Number(maxAddFrac.toFixed(3)),
},
};
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPatch: {
applied: false,
reason: "patch_too_large",
fnSource,
fnCoverage: Number(fnCoverage.toFixed(4)),
fnPixels,
addedPixels,
coverageBefore: Number(beforeCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
tileDilatePx,
adjPx,
patchRequireTile,
tileBandOnly,
tileBandPad,
trimIterations,
maxAddedPixels,
maxAddPixelsCap: maxAddPixelsCapValue,
maxAddPixelsScope: maxAddPixelsCap.scope,
maxCoverage,
maxAddFrac: Number(maxAddFrac.toFixed(3)),
},
};
}
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPatch: {
applied: false,
reason: "patch_failed",
fnSource,
fnCoverage: Number(fnCoverage.toFixed(4)),
fnPixels,
tileDilatePx,
patchRequireTile,
tileBandOnly,
tileBandPad,
},
};
}
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPatch: {
applied: false,
reason: "fn_too_small",
fnSource,
fnCoverage: Number(fnCoverage.toFixed(4)),
fnPixels,
tileDilatePx,
patchRequireTile,
},
};
}
if (maskDebugPayload?.originMeta) {
maskDebugPayload.originMeta = maskOriginMeta;
}
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPatch: {
applied: false,
reason: "size_mismatch",
},
};
}
}
if (maskOrig && depthOccluderMask && !allowDepthOccluder && !preferSegOccluder) {
try {
const depthRescueEnabled = (process.env.OCCLUDER_DEPTH_RESCUE ?? "1") !== "0";
const onlyLargerThanBase = depthSuppressReasons.length > 0
&& depthSuppressReasons.every((reason) => reason === "larger_than_base");
const minCoverage = Math.max(0, Number(process.env.OCCLUDER_DEPTH_RESCUE_MIN_COVERAGE ?? "0.05"));
const maxCoverage = Math.min(1, Number(process.env.OCCLUDER_DEPTH_RESCUE_MAX_COVERAGE ?? "0.4"));
const depthCovOk = depthOccCoverage != null && depthOccCoverage >= minCoverage && depthOccCoverage <= maxCoverage;
if (depthRescueEnabled && onlyLargerThanBase && depthCovOk) {
const targetW = maskWidth ?? depthOccluderMask.width;
const targetH = maskHeight ?? depthOccluderMask.height;
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const rescueBandPx = Math.max(0, Number(process.env.OCCLUDER_DEPTH_RESCUE_BAND ?? "6"));
const rescueErodePx = Math.max(0, Number(process.env.OCCLUDER_DEPTH_RESCUE_ERODE ?? "1"));
const rescueAdjPx = Math.max(0, Number(process.env.OCCLUDER_DEPTH_RESCUE_ADJ ?? "8"));
const adjacency = rescueAdjPx > 0
? morphDilate(maskOrig, targetW, targetH, rescueAdjPx)
: maskOrig;
const tileRegion = promptTileMask && samPromptedReady
? morphDilate(promptTileMask, targetW, targetH, rescueBandPx)
: null;
const rescueRegion = tileRegion ? maskOr(tileRegion, adjacency) : adjacency;
const rescueSupport = rescueErodePx > 0
? morphErode(depthAligned.data, targetW, targetH, rescueErodePx)
: depthAligned.data;
const minArea = Math.max(200, Number(process.env.OCCLUDER_DEPTH_RESCUE_MIN_AREA ?? "1200"));
const components = connectedComponents(rescueSupport, targetW, targetH, minArea);
const merged = new Uint8Array(maskOrig);
let rescuePixels = 0;
let mergedComponents = 0;
const limit = Math.min(merged.length, rescueRegion.length);
for (const comp of components) {
let touches = false;
for (const idx of comp.pixels) {
if (idx < limit && rescueRegion[idx]) {
touches = true;
break;
}
}
if (!touches) continue;
mergedComponents += 1;
for (const idx of comp.pixels) {
if (idx < limit && !merged[idx]) {
merged[idx] = 255;
rescuePixels += 1;
}
}
}
if (rescuePixels > 0) {
maskOrig = merged;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthRescue: {
pixels: rescuePixels,
coverage: Number((rescuePixels / merged.length).toFixed(6)),
bandPx: rescueBandPx,
erodePx: rescueErodePx,
adjPx: rescueAdjPx,
minArea,
components: mergedComponents,
},
};
if (debug) {
console.info("[mask] depth rescue merged", {
pixels: rescuePixels,
coverage: Number((rescuePixels / merged.length).toFixed(6)),
components: mergedComponents,
});
}
}
}
} catch (err) {
if (debug) console.warn("[mask] depth rescue failed", err);
}
}
}
let rawMaskSnapshot: Uint8Array | null = null;
let rawMaskDiagnostics: RawMaskDiagnostics | null = null;
if (maskOrig) {
if (!maskOriginSource) {
maskOriginSource = 'segmentation';
}
maskBoostStats = boostSegMaskInPlace(maskOrig);
if (debug && maskBoostStats?.changed) {
console.info("[mask] boosted", {
coverage: maskBoostStats.coverage.toFixed(4),
gamma: maskBoostStats.gamma,
lo: maskBoostStats.lo,
hi: maskBoostStats.hi,
});
}
if (!maskOriginMeta && maskBoostStats) {
maskOriginMeta = {
boostCoverage: Number(maskBoostStats.coverage.toFixed(4)),
gamma: maskBoostStats.gamma,
lo: maskBoostStats.lo,
hi: maskBoostStats.hi,
};
}
maskForWeights = maskOrig;
rawMaskSnapshot = Uint8Array.from(maskOrig);
if (samPromptedDebug) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
samPrompted: samPromptedDebug,
};
}
}
let normalizationOverrides: Partial<NormalizeOptions> | undefined;
let segmentationCoverage: number | null = null;
if (maskForWeights && meta0.width && meta0.height) {
try {
const maskW = meta0.width;
const maskH = meta0.height;
const grayBytes = new Uint8Array(
await sharp(raw).removeAlpha().toColorspace('b-w').raw().toBuffer()
);
intensityBytes = grayBytes;
maskWidth = maskW;
maskHeight = maskH;
const segCoverage = coverageOfMask(maskForWeights);
segmentationCoverage = Number(segCoverage.toFixed(6));
if (maskOriginMeta) {
maskOriginMeta = {
...maskOriginMeta,
segCoverage: Number(segCoverage.toFixed(4)),
};
}
if (Number.isFinite(segCoverage) && segCoverage >= 0.02 && segCoverage <= 0.7) {
normalizationOverrides = {
invertMode: 'false',
removeBorder: false,
thinWidth: 0,
fallbackLow: 0.002,
};
}
const samSource = Boolean(
(maskOriginSource && maskOriginSource.includes("sam")) ||
(maskOriginMeta && (maskOriginMeta as any).sam)
);
if (samSource) {
normalizationOverrides = {
...(normalizationOverrides ?? {}),
invertMode: 'false',
thresholdMode: 'fixed',
thresholdValue: 1,
removeBorder: false,
thinWidth: 0,
minArea: 1,
keepLargest: 0,
gridKill: 'off',
edgeFill: { enabled: false, threshold: 0, dilate: 0 },
edgeGuard: { enabled: false, threshold: 0, dilate: 0 },
seedGrow: 0,
carveRadius: 0,
morphOps: [],
bgMode: 'classic',
fallbackLow: 0,
fallbackHigh: 1,
};
}
const primary = normalizeWithPreset({
mask: maskForWeights,
width: maskW,
height: maskH,
gray: grayBytes,
preset: presetRequested,
overrides: normalizationOverrides,
});
let chosen = primary;
const primaryInterior = primary.result.interiorCoverage ?? 0;
const primaryLeak = primary.result.leakPct ?? primary.result.leakAfter ?? 1;
const qualityGood = primaryInterior >= 0.90 && primaryLeak <= 0.10;
const primaryCov = primary.result.coverage ?? 0;
const shouldForceFallback = (fallbackThreshold > 0)
&& (primaryCov < fallbackThreshold)
&& (primaryInterior < 0.85);
if (shouldForceFallback && !qualityGood && primary.preset !== 'tiles-detail') {
const detail = normalizeWithPreset({
mask: maskForWeights,
width: maskW,
height: maskH,
gray: grayBytes,
preset: 'tiles-detail',
overrides: normalizationOverrides,
});
if (detail.result.coverage > primary.result.coverage + 0.005) {
chosen = detail;
}
}
maskPresetApplied = chosen.preset;
maskNormalized = Uint8Array.from(chosen.result.data);
maskForWeights = maskNormalized;
normalizationPrimary = primary;
normalizationChosen = chosen;
if (maskNormalized && maskWidth && maskHeight) {
const boost = buildYoloBoostMask(maskNormalized, maskOrig ?? null, maskWidth, maskHeight, maskOriginMeta);
if (boost) {
maskNormalized = boost.mask;
maskForWeights = maskNormalized;
maskOriginMeta = {
...(maskOriginMeta ?? {}),
yoloBoost: {
applied: boost.applied,
coverage: Number(boost.coverage.toFixed(6)),
},
};
if (normalizationChosen?.result) {
normalizationChosen.result.data = Uint8Array.from(maskNormalized);
normalizationChosen.result.coverage = boost.coverage;
if (normalizationChosen.result.stats?.background) {
normalizationChosen.result.stats.background.occluderCoverage = boost.coverage;
}
if (normalizationChosen.result.stats?.hybrid) {
normalizationChosen.result.stats.hybrid.seedCoverage = boost.coverage;
}
}
}
}
maskNormalizedInitial = Uint8Array.from(maskNormalized);
maskNormalizedInitialCoverage = coverageOfMask(maskNormalized);
const normalizedStats = chosen.result.stats;
const backgroundStats = normalizedStats.background ?? null;
if (maskNormalized && maskWidth && maskHeight) {
const meanAlpha = coverageOfMask(maskNormalized);
const stdEstimate = normalizedStats.hybrid?.std ?? normalizedStats.hybrid?.localStdThresh ?? 0;
if (meanAlpha < 0.22 && stdEstimate > 0 && stdEstimate < 25) {
maskNormalized = morphDilate(maskNormalized, maskWidth, maskHeight, 1);
maskForWeights = maskNormalized;
maskNormalizedInitial = Uint8Array.from(maskNormalized);
maskNormalizedInitialCoverage = coverageOfMask(maskNormalized);
}
}
maskCoverage = maskNormalized ? coverageOfMask(maskNormalized) : chosen.result.coverage;
maskBackgroundCoverage = backgroundStats?.coverage ?? null;
maskOccluderCoverage = backgroundStats?.occluderCoverage ?? null;
maskSeedCoverage = normalizedStats.hybrid?.seedCoverage ?? null;
} catch (normErr) {
if (debug) {
console.warn('[mask] normalization failed', normErr);
}
}
}
if (rawMaskSnapshot && meta0.width && meta0.height) {
const encodedRaw = await maskToDataUrlWithCheck(rawMaskSnapshot, meta0.width, meta0.height);
maskRawDataUrl = encodedRaw.dataUrl;
rawMaskDiagnostics = encodedRaw.diagnostics;
if (!finalMaskDataUrl || preferSegOccluder) {
finalMaskDataUrl = maskRawDataUrl;
finalMaskMeta = {
polarity: 'white',
source: maskOriginSource ?? undefined,
stats: maskOriginMeta ?? undefined,
};
}
}
// (B) Downscale Arbeitsbild
const maxSide = 1200;
let work = sharp(raw).removeAlpha();
if (Math.max(W,H) > maxSide) {
const scale = maxSide / Math.max(W,H);
W = Math.max(1, Math.round((meta0.width ?? W) * scale));
H = Math.max(1, Math.round((meta0.height ?? H) * scale));
work = work.resize(W, H, { fit:"inside" });
}
// (C) Maske auf Arbeitsauflösung + Feather -> Gewichte
let weights: Float32Array | null = null;
if (useSeg && (maskForWeights || maskOrig)) {
const maskSourceForWeights = maskForWeights ?? maskOrig!;
const maskResizedBuf = await sharp(Buffer.from(maskSourceForWeights), {
raw: { width: meta0.width!, height: meta0.height!, channels: 1 }
})
.resize(W, H, { kernel: "lanczos3" })
.blur(maskBlur)
.toColorspace("b-w")
.raw()
.toBuffer();
const m = new Uint8Array(maskResizedBuf);
weights = new Float32Array(W*H);
const a = maskWeight;
for (let i=0; i<m.length; i++){
const mi = m[i] / 255; // 0..1 (1=Occluder)
weights[i] = 1 - a * mi; // 1..(1-a)
}
}
// (D) Kanten + gewichtete Projektionen
const rgba = await work.raw().ensureAlpha().toBuffer({ resolveWithObject:true });
const w = rgba.info.width, h = rgba.info.height;
const gray = toGrayFloat32(rgba.data, w, h);
const mag = sobelMag(gray, w, h);
const projX = projectXWeighted(mag, weights, w, h);
const projY = projectYWeighted(mag, weights, w, h);
const acX = autoCorr1D(projX.slice());
const acY = autoCorr1D(projY.slice());
const perX = bestPeriod(acX, Math.max(12, Math.floor(w/100)), Math.max(24, Math.floor(w/3)));
const perY = bestPeriod(acY, Math.max(12, Math.floor(h/100)), Math.max(24, Math.floor(h/3)));
const phaseX = phaseForPeriod(projX, perX);
const phaseY = phaseForPeriod(projY, perY);
const grout = Math.max(1, Math.round(0.025 * ((perX + perY) / 2)));
const vlines:number[] = [];
for (let x = phaseX; x < w; x += perX || w) vlines.push(x);
const hlines:number[] = [];
for (let y = phaseY; y < h; y += perY || h) hlines.push(y);
// In Original-Pixel skalieren
const scaleBackX = (meta0.width ?? w) / w;
const scaleBackY = (meta0.height ?? h) / h;
const widthPx = meta0.width ?? w;
const heightPx = meta0.height ?? h;
const scaleLines = (values: number[], scale: number, extent: number) => {
const acc = new Set<number>();
const maxIdx = Math.max(0, extent - 1);
for (const v of values) {
const scaled = clamp(Math.round(v * scale), 0, maxIdx);
acc.add(scaled);
}
return Array.from(acc).sort((a, b) => a - b);
};
const tile_w_px = Math.round(perX * scaleBackX);
const tile_h_px = Math.round(perY * scaleBackY);
const grout_px = Math.round(grout * Math.sqrt(scaleBackX*scaleBackY));
let gridV = scaleLines(vlines, scaleBackX, widthPx);
let gridH = scaleLines(hlines, scaleBackY, heightPx);
const median = (values: number[]): number => {
if (!values.length) return 0;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
};
const consecutiveDiffs = (values: number[]): number[] => {
const diffs: number[] = [];
for (let i = 1; i < values.length; i++) {
diffs.push(values[i] - values[i - 1]);
}
return diffs;
};
const safePeriod = (fallback: number, values: number[]): number => {
if (fallback && fallback > 0) return fallback;
const clean = values.filter((v) => Number.isFinite(v)).sort((a, b) => a - b);
const med = median(consecutiveDiffs(clean));
return med > 0 ? med : 0;
};
const computeCoverage = (tileW: number, tileH: number, linesV: number[], linesH: number[]) => {
const TvLocal = safePeriod(tileW, linesV);
const ThLocal = safePeriod(tileH, linesH);
const expectedVLocal = TvLocal > 0 && widthPx > 0 ? Math.max(1, Math.floor(widthPx / TvLocal) + 1) : linesV.length || 1;
const expectedHLocal = ThLocal > 0 && heightPx > 0 ? Math.max(1, Math.floor(heightPx / ThLocal) + 1) : linesH.length || 1;
const covV = Math.min(1, expectedVLocal ? linesV.length / expectedVLocal : 0);
const covH = Math.min(1, expectedHLocal ? linesH.length / expectedHLocal : 0);
return { Tv: TvLocal, Th: ThLocal, coverage: Math.min(covV, covH) };
};
const primaryCoverage = computeCoverage(tile_w_px, tile_h_px, gridV, gridH);
const coverage = primaryCoverage.coverage;
const periodicRaw = runPeriodicAnalysis(gray, w, h, { debug, weights: weights ?? null });
let periodicCandidate: null | {
tile_w_px: number;
tile_h_px: number;
grout_px: number;
gridV: number[];
gridH: number[];
coverage: number;
confidence: number;
source: string;
raw: typeof periodicRaw;
} = null;
if (periodicRaw.ok) {
const altGridV = scaleLines(periodicRaw.vlines, scaleBackX, widthPx);
const altGridH = scaleLines(periodicRaw.hlines, scaleBackY, heightPx);
const tileWAlt = Math.max(1, Math.round(periodicRaw.tile_w_px * scaleBackX));
const tileHAlt = Math.max(1, Math.round(periodicRaw.tile_h_px * scaleBackY));
const groutAlt = Math.max(1, Math.round(periodicRaw.grout_px * Math.sqrt(scaleBackX * scaleBackY)));
const coverageAltStats = computeCoverage(tileWAlt, tileHAlt, altGridV, altGridH);
const coverageAlt = coverageAltStats.coverage;
periodicCandidate = {
tile_w_px: tileWAlt,
tile_h_px: tileHAlt,
grout_px: groutAlt,
gridV: altGridV,
gridH: altGridH,
coverage: coverageAlt,
confidence: periodicRaw.confidence ?? 0,
source: periodicRaw.source ?? "cv@fft-hough",
raw: periodicRaw,
};
if (debug) {
console.log("periodic-analysis", {
tile_w_px: tileWAlt,
tile_h_px: tileHAlt,
grout_px: groutAlt,
coverage: coverageAlt,
confidence: periodicCandidate.confidence,
});
}
}
type RansacMetaType = ReturnType<typeof refineGridRansac>["meta"];
type RansacInfo = {
mode: string;
coverage: number;
v?: RansacMetaType["v"];
h?: RansacMetaType["h"];
};
type ApplyRansacResult = {
vlines: number[];
hlines: number[];
meta?: RansacMetaType;
info: RansacInfo;
coverage: number;
};
const applyRansac = (
label: string,
tileW: number,
tileH: number,
linesV: number[],
linesH: number[],
coverageSeed: number,
): ApplyRansacResult => {
let v = [...linesV];
let h = [...linesH];
let meta: RansacMetaType | undefined;
let info: RansacInfo = { mode: "off", coverage: coverageSeed };
const shouldAutoCandidate = ransacAuto && coverageSeed < 0.6;
if ((ransacForce || shouldAutoCandidate) && widthPx > 0 && heightPx > 0) {
try {
const refined = refineGridRansac(widthPx, heightPx, v, h);
const confV = refined.meta?.v?.conf ?? 0;
const confH = refined.meta?.h?.conf ?? 0;
const confOK = confV >= 0.5 && confH >= 0.5;
if (ransacForce || confOK) {
v = refined.vlines;
h = refined.hlines;
meta = refined.meta;
info = { mode: ransacForce ? "force" : "auto", coverage: coverageSeed, ...refined.meta };
} else {
info = { mode: "auto-skip", coverage: coverageSeed, ...refined.meta };
}
} catch (refineErr) {
if (debug) {
console.warn(`RANSAC ${label} failed:`, refineErr);
}
info = { mode: ransacForce ? "force-error" : "auto-error", coverage: coverageSeed };
}
}
const postCoverage = computeCoverage(tileW, tileH, v, h);
info = { ...info, coverage: postCoverage.coverage };
return { vlines: v, hlines: h, meta, info, coverage: postCoverage.coverage };
};
const currentRansac = applyRansac("projection", tile_w_px, tile_h_px, gridV, gridH, coverage);
gridV = currentRansac.vlines;
gridH = currentRansac.hlines;
type RansacMeta = RansacMetaType;
type Candidate = {
label: 'projection' | 'periodic';
tile_w_px: number;
tile_h_px: number;
grout_px: number;
grid: { vlines: number[]; hlines: number[] };
coverage: number;
confidence: number | null;
source: string;
ransac: RansacInfo;
ransacMeta?: RansacMeta;
};
const projectionCandidate: Candidate = {
label: 'projection',
tile_w_px,
tile_h_px,
grout_px,
grid: { vlines: gridV, hlines: gridH },
coverage: currentRansac.coverage,
confidence: null,
source: `${(useSeg && maskOrig) ? "seg@u2net -> weighted sobel+projection" : "sobel+projection"}${currentRansac.meta ? " + ransac1d" : ""}`,
ransac: currentRansac.info,
ransacMeta: currentRansac.meta,
};
let periodicResult: Candidate | null = null;
if (periodicCandidate) {
const periodicRansac = applyRansac("periodic", periodicCandidate.tile_w_px, periodicCandidate.tile_h_px, periodicCandidate.gridV, periodicCandidate.gridH, periodicCandidate.coverage);
periodicResult = {
label: 'periodic',
tile_w_px: periodicCandidate.tile_w_px,
tile_h_px: periodicCandidate.tile_h_px,
grout_px: periodicCandidate.grout_px,
grid: { vlines: periodicRansac.vlines, hlines: periodicRansac.hlines },
coverage: periodicRansac.coverage,
confidence: periodicCandidate.confidence ?? null,
source: `${periodicCandidate.source}${periodicRansac.meta ? " + ransac1d" : ""}`,
ransac: periodicRansac.info,
ransacMeta: periodicRansac.meta,
};
}
let best: Candidate = projectionCandidate;
let method: "projection" | "periodic" = "projection";
if (periodicResult) {
const coverageGain = periodicResult.coverage - projectionCandidate.coverage;
const periodicConf = periodicResult.confidence ?? 0;
const projectionWeak = projectionCandidate.coverage < 0.45;
const periodicStrong = periodicResult.coverage >= 0.6 && periodicConf >= 0.35;
const confidenceStrong = periodicConf >= 0.55 && coverageGain > 0.05;
if ((periodicStrong && coverageGain > 0.12) || confidenceStrong || (projectionWeak && coverageGain > 0.05)) {
best = periodicResult;
method = "periodic";
}
}
const analysis = {
method,
candidates: {
projection: {
tile_w_px: projectionCandidate.tile_w_px,
tile_h_px: projectionCandidate.tile_h_px,
grout_px: projectionCandidate.grout_px,
coverage: projectionCandidate.coverage,
confidence: projectionCandidate.confidence,
source: projectionCandidate.source,
ransac: projectionCandidate.ransac,
},
...(periodicResult ? {
periodic: {
tile_w_px: periodicResult.tile_w_px,
tile_h_px: periodicResult.tile_h_px,
grout_px: periodicResult.grout_px,
coverage: periodicResult.coverage,
confidence: periodicResult.confidence,
source: periodicResult.source,
ransac: periodicResult.ransac,
},
} : {}),
},
};
const finalTileW = best.tile_w_px;
const finalTileH = best.tile_h_px;
const finalGrout = best.grout_px;
const finalGrid = best.grid;
const source = best.source;
const ransacInfo = best.ransac;
if (
maskNormalized &&
maskPresetApplied &&
intensityBytes &&
normalizationChosen &&
normalizationChosen.result?.stats
) {
gridContext = finalGrid.vlines.length || finalGrid.hlines.length ? {
width: maskWidth ?? meta0.width ?? intensityBytes.length,
height: maskHeight ?? meta0.height ?? intensityBytes.length,
vlines: finalGrid.vlines,
hlines: finalGrid.hlines,
groutPx: finalGrout ?? 0,
tileWidthPx: finalTileW ?? 0,
tileHeightPx: finalTileH ?? 0,
} : null;
const pipelineResult = runMaskPipeline({
normalizedMask: maskNormalized,
normalizedCoverage: maskNormalizedInitialCoverage ?? maskCoverage ?? 0,
stats: normalizationChosen.result.stats,
intensity: intensityBytes,
width: maskWidth ?? meta0.width ?? intensityBytes.length,
height: maskHeight ?? meta0.height ?? intensityBytes.length,
grid: gridContext ?? undefined,
preset: maskPresetApplied,
u2BoostMask: maskOrig ? thresholdMask(maskOrig, 230) ?? undefined : undefined,
flags,
});
pipelineWarnings = Array.isArray(pipelineResult.debug?.coverage?.warnings)
? pipelineResult.debug.coverage.warnings.filter((w): w is string => typeof w === 'string')
: undefined;
const pipelineMaskBase = Uint8Array.from(pipelineResult.mask);
const pipelineCoverageAfter = pipelineResult.coverage ?? 0;
const pipelineLeakAfter = pipelineResult.leak ?? 0;
const pipelineInteriorBefore = pipelineResult.interiorCoverage ?? 0;
const coverageBefore = maskNormalizedInitialCoverage ?? maskCoverage ?? pipelineCoverageAfter;
let interiorAfter = pipelineInteriorBefore;
const leakBefore = pipelineLeakAfter;
// Apply seed-driven component filtering and lattice-aware leak suppression
let filteredMask = Uint8Array.from(pipelineResult.mask);
let seedOverlap = 0;
let u2Seeds = maskOrig && gridContext ? thresholdMask(maskOrig, 150) : null;
if (u2Seeds && maskWidth && maskHeight && maskOccluderCoverage != null && maskOccluderCoverage > 0.25) {
u2Seeds = morphDilate(u2Seeds, maskWidth, maskHeight, 1);
}
const seedCoverage = u2Seeds ? coverageOfMask(u2Seeds) : 0;
if (u2Seeds && gridContext && maskWidth && maskHeight) {
// Step 1: Keep only seed-driven components (lenient parameters)
const compFiltered = keepSeedDrivenComponents(
pipelineResult.mask,
u2Seeds,
maskWidth,
maskHeight,
0.08, // minIoU
400 // minArea
);
const compFilteredCoverage = coverageOfMask(compFiltered);
// Airbag: If component filtering is too aggressive, union with seeds
if (compFilteredCoverage < 0.75 * coverageBefore) {
// Union compFiltered with seeds to prevent collapse
for (let i = 0; i < compFiltered.length; i++) {
if (u2Seeds[i]) compFiltered[i] = 255;
}
}
// Step 2: Suppress tile-interior leak with hysteresis
filteredMask = suppressTileLeak(
compFiltered,
maskOrig, // probability map
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
gridContext.groutPx,
148 // threshold ~0.58
);
// Airbag: Pipeline must never be thinner than 60% of seeds
const filteredCoverage = coverageOfMask(filteredMask);
if (filteredCoverage < 0.60 * seedCoverage) {
// Union with seeds to guarantee >= seed coverage
for (let i = 0; i < filteredMask.length; i++) {
if (u2Seeds[i]) filteredMask[i] = 255;
}
}
}
// Measure filtered mask metrics
const gridMasks = gridContext && maskWidth && maskHeight
? buildGridMasks(maskWidth, maskHeight, gridContext.vlines, gridContext.hlines, gridContext.groutPx)
: null;
const coreBandMask = gridContext && maskWidth && maskHeight
? buildGridMasks(
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
Math.max(1, Math.round((gridContext.groutPx ?? 3) / 2)),
).lineMask
: null;
let leakAfterClamp1: number | null = null;
let leakAfterReinject: number | null = null;
let leakAfterFinal: number | null = null;
let leakAfter = gridMasks ? leakRatio(filteredMask, gridMasks.lineMask) : leakBefore;
let coverageAfter = coverageOfMask(filteredMask);
interiorAfter = gridMasks
? coverageWithinMask(filteredMask, gridMasks.interiorMask)
: pipelineInteriorBefore;
let acceptanceOverride: string | undefined;
if (leakAfter > 0.09 && maskOrig && gridContext && maskWidth && maskHeight) {
const clampBandPx = Math.max(3, (gridContext.groutPx ?? 3) + 1);
const baseMask = Uint8Array.from(filteredMask);
const tightened = suppressTileLeak(
baseMask,
maskOrig,
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
clampBandPx,
168,
);
const lineMask = gridMasks?.lineMask ?? null;
if (lineMask) {
for (let i = 0; i < tightened.length; i++) {
if (lineMask[i]) tightened[i] = 0;
}
}
const bandMask = lineMask ? morphDilate(lineMask, maskWidth, maskHeight, 1) : null;
let clampDebug: {
lostPixels: number;
restoredPixels: number;
lostComponents: number;
restoredComponents: number;
samples: Array<{ area: number; meanDist: number; detFrac: number; seedFrac: number; touchesSafe: boolean; restored: boolean }>;
} | null = null;
if (bandMask) {
const lost = new Uint8Array(tightened.length);
let lostCount = 0;
for (let i = 0; i < tightened.length; i++) {
if (baseMask[i] && !tightened[i] && bandMask[i]) {
lost[i] = 255;
lostCount += 1;
}
}
if (lostCount > 0) {
clampDebug = {
lostPixels: lostCount,
restoredPixels: 0,
lostComponents: 0,
restoredComponents: 0,
samples: [],
};
const distMap = distanceToNearestLattice(
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
clampBandPx,
);
const adjacencyMask = Uint8Array.from(tightened);
const comps = connectedComponents(lost, maskWidth, maskHeight, 24);
clampDebug.lostComponents = comps.length;
for (const comp of comps) {
const area = comp.area;
if (area < 32) continue;
const aspect = componentAspectRatio(comp);
const extentX = componentExtentX(comp);
const varianceYNorm = componentVarianceY(comp, maskWidth);
const textileWidth = Math.max(180, Math.round(maskWidth * 0.18));
const isTextileShape = aspect > 2.2 && extentX >= textileWidth && varianceYNorm < 0.6;
const coreTouch = componentTouchesMask(comp, coreBandMask);
const coreFrac = componentOverlapFraction(comp, coreBandMask);
let sumDist = 0;
let detSupport = 0;
let seedSupport = 0;
let touchesSafe = false;
for (const idx of comp.pixels) {
sumDist += distMap[idx];
if (maskOrig[idx] >= 200) detSupport += 1;
if (u2Seeds && u2Seeds[idx]) seedSupport += 1;
if (!touchesSafe) {
const x = idx % maskWidth;
const y = (idx / maskWidth) | 0;
if (
(x > 0 && adjacencyMask[idx - 1]) ||
(x + 1 < maskWidth && adjacencyMask[idx + 1]) ||
(y > 0 && adjacencyMask[idx - maskWidth]) ||
(y + 1 < maskHeight && adjacencyMask[idx + maskWidth])
) {
touchesSafe = true;
}
}
}
const meanDist = sumDist / area;
const detFrac = detSupport / area;
const seedFrac = seedSupport / Math.max(1, area);
const hasStrongSupport = detFrac >= 0.45 || seedFrac >= 0.45;
if (coreTouch && meanDist < 0.4) continue;
if (coreTouch && meanDist < 0.9 && !hasStrongSupport) continue;
if (coreFrac > 0.05) continue;
const areaThreshold = isTextileShape ? 380 : 140;
if (area < areaThreshold && !hasStrongSupport) continue;
let shouldRestore = false;
if (touchesSafe && meanDist >= 1.0) shouldRestore = true;
if (!shouldRestore && detFrac >= 0.30 && area >= 150 && meanDist >= 0.8) shouldRestore = true;
if (!shouldRestore && seedFrac >= 0.25 && area >= 150 && meanDist >= 0.8) shouldRestore = true;
if (!shouldRestore && (detFrac >= 0.55 || seedFrac >= 0.55) && meanDist >= 0.6) shouldRestore = true;
if (!shouldRestore && hasStrongSupport && area >= 800 && coreFrac <= 0.04) shouldRestore = true;
if (clampDebug && clampDebug.samples.length < 25) {
clampDebug.samples.push({
area,
meanDist: Number(meanDist.toFixed(3)),
detFrac: Number(detFrac.toFixed(3)),
seedFrac: Number(seedFrac.toFixed(3)),
touchesSafe,
restored: shouldRestore,
});
}
if (!shouldRestore) continue;
for (const idx of comp.pixels) {
const allowSeed = u2Seeds && u2Seeds[idx];
const allowProb = maskOrig[idx] >= 215;
const allow = allowProb || allowSeed;
if (distMap[idx] < 1 && !allow) continue;
const onLine = lineMask ? lineMask[idx] !== 0 : false;
if (onLine && !allow) continue;
if (!allow && distMap[idx] < 2) continue;
tightened[idx] = 255;
}
if (clampDebug) {
clampDebug.restoredComponents += 1;
clampDebug.restoredPixels += area;
}
}
}
}
if (u2Seeds && maskOrig) {
for (let i = 0; i < tightened.length; i++) {
if (!u2Seeds[i]) continue;
const allowProb = maskOrig[i] >= 210;
if (lineMask && lineMask[i] && !allowProb) continue;
tightened[i] = 255;
}
}
const tightenedCoverage = coverageOfMask(tightened);
const tightenedLeak = gridMasks ? leakRatio(tightened, gridMasks.lineMask) : leakAfter;
const tightenedInterior = gridMasks ? coverageWithinMask(tightened, gridMasks.interiorMask) : interiorAfter;
const leakThreshold = Math.min(0.08, leakAfter - 0.01);
if (tightenedLeak <= leakThreshold && tightenedCoverage >= coverageAfter * 0.6) {
filteredMask = tightened;
coverageAfter = tightenedCoverage;
leakAfter = tightenedLeak;
interiorAfter = tightenedInterior;
leakAfterClamp1 = tightenedLeak;
if (clampDebug) {
if (pipelineResult.debug?.leakClamp && typeof pipelineResult.debug.leakClamp === 'object') {
(pipelineResult.debug.leakClamp as any).occluder = clampDebug;
} else {
pipelineResult.debug.leakClamp = { enabled: true, ...clampDebug };
}
}
}
}
// Occluder reinjection using seeds / detector when clamp clears grout
if (maskOrig && gridContext && maskWidth && maskHeight && gridMasks) {
const bandPx = Math.max(3, (gridContext.groutPx ?? 3) + 1);
const bandMask = morphDilate(gridMasks.lineMask, maskWidth, maskHeight, 1);
const lostBand = new Uint8Array(filteredMask.length);
let lostBandCount = 0;
for (let i = 0; i < filteredMask.length; i++) {
if ((pipelineMaskBase[i] && !filteredMask[i]) && bandMask[i]) {
lostBand[i] = 255;
lostBandCount += 1;
}
}
if (lostBandCount > 0) {
const distMap = distanceToNearestLattice(
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
bandPx,
);
const adjacencyMask = Uint8Array.from(filteredMask);
const comps = connectedComponents(lostBand, maskWidth, maskHeight, 24);
const reinjectBase = Uint8Array.from(filteredMask);
const reinject = new Uint8Array(filteredMask);
for (const comp of comps) {
const area = comp.area;
if (area < 120) continue;
const aspect = componentAspectRatio(comp);
const extentX = componentExtentX(comp);
const varianceYNorm = componentVarianceY(comp, maskWidth);
const textileWidth = Math.max(160, Math.round(maskWidth * 0.16));
const isTextileShape = aspect > 2.2 && extentX >= textileWidth && varianceYNorm < 0.55;
const coreTouch = componentTouchesMask(comp, coreBandMask);
const coreFrac = componentOverlapFraction(comp, coreBandMask);
let sumDist = 0;
let detSupport = 0;
let seedSupport = 0;
let touchesSafe = false;
for (const idx of comp.pixels) {
sumDist += distMap[idx];
if (maskOrig[idx] >= 200) detSupport += 1;
if (u2Seeds && u2Seeds[idx]) seedSupport += 1;
if (!touchesSafe) {
const x = idx % maskWidth;
const y = (idx / maskWidth) | 0;
if (
(x > 0 && adjacencyMask[idx - 1]) ||
(x + 1 < maskWidth && adjacencyMask[idx + 1]) ||
(y > 0 && adjacencyMask[idx - maskWidth]) ||
(y + 1 < maskHeight && adjacencyMask[idx + maskWidth])
) {
touchesSafe = true;
}
}
}
const meanDist = sumDist / area;
const detFrac = detSupport / Math.max(1, area);
const seedFrac = seedSupport / Math.max(1, area);
const hasStrongSupport = detFrac >= 0.5 || seedFrac >= 0.5;
if (coreTouch && meanDist < 0.45) continue;
if (coreTouch && meanDist < 0.9 && !hasStrongSupport) continue;
if (coreFrac > 0.05) continue;
let shouldRestore = false;
const areaThreshold = isTextileShape ? 420 : 180;
if (area < areaThreshold && !hasStrongSupport) continue;
if (seedFrac >= 0.30 && meanDist >= 0.85) shouldRestore = true;
if (!shouldRestore && detFrac >= 0.24 && area >= 170 && meanDist >= 0.85) shouldRestore = true;
if (!shouldRestore && touchesSafe && meanDist >= 1.2) shouldRestore = true;
if (!shouldRestore && hasStrongSupport && meanDist >= 0.9) shouldRestore = true;
if (!shouldRestore && hasStrongSupport && area >= 800 && coreFrac <= 0.04) shouldRestore = true;
if (!shouldRestore) continue;
for (const idx of comp.pixels) {
const allowSeed = u2Seeds && u2Seeds[idx];
const allowProb = maskOrig[idx] >= 215;
const allowClose = allowProb || allowSeed;
const onLine = gridMasks.lineMask[idx] !== 0;
if (distMap[idx] < 0.5 && !allowClose) continue;
if (onLine && !allowClose) continue;
if (!allowClose && maskOrig[idx] < 200) continue;
reinject[idx] = 255;
}
}
const priorCoverage = coverageAfter;
const priorLeak = leakAfter;
const priorInterior = interiorAfter;
filteredMask = reinject;
coverageAfter = coverageOfMask(filteredMask);
leakAfter = leakRatio(filteredMask, gridMasks.lineMask);
interiorAfter = coverageWithinMask(filteredMask, gridMasks.interiorMask);
const leakGuard = Math.min(0.09, priorLeak + 0.015);
if (leakAfter > leakGuard) {
filteredMask = reinjectBase;
coverageAfter = priorCoverage;
leakAfter = priorLeak;
interiorAfter = priorInterior;
} else {
leakAfterReinject = leakAfter;
}
}
}
if (u2Seeds && maskOrig && gridContext && maskWidth && maskHeight) {
const lineMask = gridMasks?.lineMask ?? null;
const distSeeds = distanceToNearestLattice(
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
Math.max(3, (gridContext.groutPx ?? 3) + 1),
);
const priorMask = Uint8Array.from(filteredMask);
const priorCoverage = coverageAfter;
const priorLeak = leakAfter;
const priorInterior = interiorAfter;
let touched = false;
for (let i = 0; i < filteredMask.length; i++) {
if (!u2Seeds[i]) continue;
if (filteredMask[i]) continue;
const probOk = maskOrig[i] >= 230;
const distOk = distSeeds[i] >= 0.8;
if (!probOk && !distOk) continue;
if (lineMask && lineMask[i] && !probOk) continue;
filteredMask[i] = 255;
touched = true;
}
if (touched && gridMasks) {
coverageAfter = coverageOfMask(filteredMask);
leakAfter = leakRatio(filteredMask, gridMasks.lineMask);
interiorAfter = coverageWithinMask(filteredMask, gridMasks.interiorMask);
const leakGuard = Math.min(0.12, priorLeak + 0.03);
if (leakAfter > leakGuard) {
filteredMask = priorMask;
coverageAfter = priorCoverage;
leakAfter = priorLeak;
interiorAfter = priorInterior;
}
leakAfterFinal = leakAfter;
}
}
if (maskOrig && gridContext && gridMasks && maskWidth && maskHeight) {
const miniBand = Math.max(2, Math.round(Math.max(1, (gridContext.groutPx ?? 3) * 0.5)));
const miniClamp = suppressTileLeak(
Uint8Array.from(filteredMask),
maskOrig,
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
miniBand,
168,
);
const lineMask = gridMasks.lineMask;
if (lineMask) {
for (let i = 0; i < miniClamp.length; i++) {
if (!lineMask[i]) continue;
const strongProb = maskOrig ? maskOrig[i] >= 220 : false;
const strongSeed = u2Seeds ? Boolean(u2Seeds[i]) : false;
if (!strongProb && !strongSeed) {
miniClamp[i] = 0;
}
}
}
if (u2Seeds) {
for (let i = 0; i < miniClamp.length; i++) {
if (!u2Seeds[i]) continue;
const onLine = lineMask ? Boolean(lineMask[i]) : false;
if (onLine && maskOrig && maskOrig[i] < 200) continue;
miniClamp[i] = 255;
}
}
const miniLeak = leakRatio(miniClamp, gridMasks.lineMask);
const miniCoverage = coverageOfMask(miniClamp);
const miniInterior = coverageWithinMask(miniClamp, gridMasks.interiorMask);
const leakTarget = Math.min(leakAfter, 0.12);
if (miniLeak <= leakTarget && miniCoverage >= coverageAfter * 0.6) {
filteredMask = miniClamp;
coverageAfter = miniCoverage;
leakAfter = miniLeak;
interiorAfter = miniInterior;
leakAfterFinal = miniLeak;
}
}
const seedGuardShared = u2Seeds && maskWidth && maskHeight
? morphDilate(u2Seeds, maskWidth, maskHeight, 2)
: null;
if (u2Seeds && gridMasks?.lineMask && maskWidth && maskHeight) {
const lineMask = gridMasks.lineMask;
const seedGuard = seedGuardShared;
let trimmed = false;
for (let i = 0; i < filteredMask.length; i++) {
if (!filteredMask[i]) continue;
if (!lineMask[i]) continue;
if (seedGuard && seedGuard[i]) continue;
filteredMask[i] = 0;
trimmed = true;
}
if (trimmed) {
coverageAfter = coverageOfMask(filteredMask);
leakAfter = leakRatio(filteredMask, gridMasks.lineMask);
interiorAfter = coverageWithinMask(filteredMask, gridMasks.interiorMask);
leakAfterFinal = leakAfter;
}
}
if (maskNormalizedInitial && gridMasks?.lineMask && maskWidth && maskHeight) {
const union = Uint8Array.from(filteredMask);
const lineMask = gridMasks.lineMask;
let unionChanged = false;
for (let i = 0; i < union.length; i++) {
if (union[i]) continue;
if (!maskNormalizedInitial[i]) continue;
if (lineMask[i]) {
if (seedGuardShared && seedGuardShared[i]) {
// ok
} else if (!maskOrig || maskOrig[i] < 210) {
continue;
}
}
union[i] = 255;
unionChanged = true;
}
if (unionChanged) {
const unionCoverage = coverageOfMask(union);
const unionLeak = leakRatio(union, gridMasks.lineMask);
const unionInterior = coverageWithinMask(union, gridMasks.interiorMask);
const drop = coverageBefore - unionCoverage;
if (unionLeak <= 0.14 || (unionLeak <= 0.16 && drop >= 0.08)) {
filteredMask = union;
coverageAfter = unionCoverage;
leakAfter = unionLeak;
interiorAfter = unionInterior;
leakAfterFinal = unionLeak;
if (pipelineResult.debug?.leakClamp && typeof pipelineResult.debug.leakClamp === 'object') {
(pipelineResult.debug.leakClamp as any).unionApplied = {
coverage: Number(unionCoverage.toFixed(6)),
leak: Number(unionLeak.toFixed(6)),
interior: Number(unionInterior.toFixed(6)),
drop: Number(drop.toFixed(6)),
};
}
} else if (pipelineResult.debug?.leakClamp && typeof pipelineResult.debug.leakClamp === 'object') {
(pipelineResult.debug.leakClamp as any).unionRejected = {
coverage: Number(unionCoverage.toFixed(6)),
leak: Number(unionLeak.toFixed(6)),
interior: Number(unionInterior.toFixed(6)),
drop: Number(drop.toFixed(6)),
};
}
}
}
if (maskNormalizedInitial && gridMasks && maskWidth && maskHeight) {
const diff = new Uint8Array(filteredMask.length);
let diffPixels = 0;
for (let i = 0; i < filteredMask.length; i++) {
if (filteredMask[i]) continue;
if (!maskNormalizedInitial[i]) continue;
diff[i] = 255;
diffPixels += 1;
}
if (diffPixels > 0) {
const distMap = distanceToNearestLattice(
maskWidth,
maskHeight,
gridContext?.vlines ?? [],
gridContext?.hlines ?? [],
Math.max(3, (gridContext?.groutPx ?? 3) + 1),
);
const comps = connectedComponents(diff, maskWidth, maskHeight, 64);
const lineMask = gridMasks.lineMask;
const reinjected: Array<Record<string, unknown>> = [];
let reinjectGain = 0;
for (const comp of comps) {
const area = comp.area;
if (area < 80) continue;
let sumDist = 0;
let lineCount = 0;
let seedSupport = 0;
let probSupport = 0;
for (const idx of comp.pixels) {
sumDist += distMap[idx];
if (lineMask[idx]) lineCount += 1;
if (seedGuardShared && seedGuardShared[idx]) seedSupport += 1;
if (maskOrig && maskOrig[idx] >= 210) probSupport += 1;
}
const meanDist = sumDist / area;
const lineFrac = lineCount / area;
const seedFrac = seedSupport / area;
const probFrac = probSupport / area;
if (lineFrac > 0.50 && seedFrac < 0.25 && probFrac < 0.60) continue;
const allowSeedBacked = seedFrac >= 0.60;
const allowProbBacked = probFrac >= 0.80 && meanDist >= (gridContext?.groutPx ?? 3) * 2.0;
if (!allowSeedBacked && !allowProbBacked) continue;
const candidate = Uint8Array.from(filteredMask);
let applied = false;
for (const idx of comp.pixels) {
if (candidate[idx]) continue;
if (lineMask[idx] && !((seedGuardShared && seedGuardShared[idx]) || (maskOrig && maskOrig[idx] >= 220))) continue;
candidate[idx] = 255;
applied = true;
}
if (!applied) continue;
const candCoverage = coverageOfMask(candidate);
const candLeak = leakRatio(candidate, gridMasks.lineMask);
const candInterior = coverageWithinMask(candidate, gridMasks.interiorMask);
if (candLeak <= 0.14) {
const gain = candCoverage - coverageAfter;
if (gain > 0 && reinjectGain + gain > 0.06) continue;
filteredMask = candidate;
coverageAfter = candCoverage;
leakAfter = candLeak;
interiorAfter = candInterior;
leakAfterFinal = candLeak;
reinjectGain += Math.max(0, gain);
reinjected.push({
area,
meanDist: Number(meanDist.toFixed(3)),
lineFrac: Number(lineFrac.toFixed(3)),
seedFrac: Number(seedFrac.toFixed(3)),
probFrac: Number(probFrac.toFixed(3)),
coverage: Number(candCoverage.toFixed(6)),
leak: Number(candLeak.toFixed(6)),
});
}
}
if (reinjected.length && pipelineResult.debug?.leakClamp && typeof pipelineResult.debug.leakClamp === 'object') {
(pipelineResult.debug.leakClamp as any).normalizedReinject = reinjected;
}
}
}
if (leakAfterFinal == null) leakAfterFinal = leakAfter;
if (
pipelineMaskBase &&
gridMasks &&
maskOrig &&
u2Seeds &&
seedOverlap >= 0.95 &&
coverageAfter < 0.22
) {
const relaxedMask = Uint8Array.from(pipelineMaskBase);
if (gridMasks.lineMask) {
const lineMask = gridMasks.lineMask;
for (let i = 0; i < relaxedMask.length; i++) {
if (!lineMask[i]) continue;
const strong = maskOrig[i] >= 208 || Boolean(u2Seeds[i]);
if (!strong) relaxedMask[i] = 0;
}
}
for (let i = 0; i < relaxedMask.length; i++) {
if (u2Seeds[i]) relaxedMask[i] = 255;
}
const relaxedLeak = leakRatio(relaxedMask, gridMasks.lineMask);
const relaxedCoverage = coverageOfMask(relaxedMask);
const relaxedInterior = coverageWithinMask(relaxedMask, gridMasks.interiorMask);
if (
relaxedLeak <= Math.min(0.16, leakAfterFinal + 0.02) &&
relaxedCoverage >= coverageAfter * 0.9
) {
filteredMask = relaxedMask;
coverageAfter = relaxedCoverage;
leakAfter = relaxedLeak;
interiorAfter = relaxedInterior;
leakAfterFinal = relaxedLeak;
}
}
leakAfter = leakAfterFinal;
if (leakAfter > 0.12 && maskOrig && gridContext && gridMasks && maskWidth && maskHeight) {
const clampStrict = suppressTileLeak(
Uint8Array.from(filteredMask),
maskOrig,
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
Math.max(2, (gridContext.groutPx ?? 3) + 1),
172,
);
const lineMask = gridMasks.lineMask;
if (lineMask) {
for (let i = 0; i < clampStrict.length; i++) {
if (lineMask[i]) clampStrict[i] = 0;
}
}
if (u2Seeds) {
for (let i = 0; i < clampStrict.length; i++) {
if (u2Seeds[i]) clampStrict[i] = 255;
}
}
const strictLeak = leakRatio(clampStrict, gridMasks.lineMask);
if (strictLeak <= Math.min(leakAfter, 0.12)) {
filteredMask = clampStrict;
coverageAfter = coverageOfMask(filteredMask);
leakAfter = strictLeak;
interiorAfter = coverageWithinMask(filteredMask, gridMasks.interiorMask);
leakAfterFinal = strictLeak;
}
}
const bigDropSoft = coverageBefore - coverageAfter > 0.08;
const leakSafeSoft = leakAfter <= 0.08;
const interiorGood = pipelineInteriorBefore >= 0.88;
if (pipelineResult.debug?.coverage) {
pipelineResult.debug.coverage.after = Number(coverageAfter.toFixed(6));
pipelineResult.debug.coverage.leak = Number(leakAfterFinal!.toFixed(6));
if (Array.isArray(pipelineResult.debug.coverage.warnings) && leakAfter <= 0.08) {
pipelineResult.debug.coverage.warnings = pipelineResult.debug.coverage.warnings.filter((w: unknown) => w !== 'leak_warn');
}
}
if (!pipelineResult.debug) pipelineResult.debug = {} as typeof pipelineResult.debug;
if (!pipelineResult.debug.leakClamp || typeof pipelineResult.debug.leakClamp !== 'object') {
pipelineResult.debug.leakClamp = { enabled: true } as any;
}
const leakClampDebug = pipelineResult.debug.leakClamp as Record<string, unknown>;
if (leakAfterClamp1 != null) leakClampDebug.leakAfterClamp1 = Number(leakAfterClamp1.toFixed(6));
if (leakAfterReinject != null) leakClampDebug.leakAfterReinject = Number(leakAfterReinject.toFixed(6));
leakClampDebug.leakAfterFinal = Number(leakAfterFinal!.toFixed(6));
leakClampDebug.leakAfter = Number(leakAfterFinal!.toFixed(6));
if (leakAfter <= 0.08 && pipelineWarnings && pipelineWarnings.length) {
pipelineWarnings = pipelineWarnings.filter((w) => w !== 'leak_warn');
}
if (bigDropSoft && leakSafeSoft && interiorGood && maskOrig && gridContext && maskWidth && maskHeight) {
let softCandidate = suppressTileLeak(
Uint8Array.from(pipelineMaskBase),
maskOrig,
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
gridContext.groutPx,
148,
);
if (u2Seeds) {
for (let i = 0; i < softCandidate.length; i++) {
if (u2Seeds[i]) softCandidate[i] = 255;
}
}
const softMasks = gridMasks ?? buildGridMasks(
maskWidth,
maskHeight,
gridContext.vlines,
gridContext.hlines,
gridContext.groutPx,
);
const softCoverage = coverageOfMask(softCandidate);
const softLeak = leakRatio(softCandidate, softMasks.lineMask);
const softInterior = coverageWithinMask(softCandidate, softMasks.interiorMask);
if (softCoverage > coverageAfter && softLeak <= 0.08) {
filteredMask = softCandidate;
coverageAfter = softCoverage;
leakAfter = softLeak;
interiorAfter = softInterior;
acceptanceOverride = 'soft-detail';
}
}
seedOverlap = u2Seeds ? overlapWithSeeds(filteredMask, u2Seeds) : 0;
// Conditional acceptance with relaxed rules for thin occluders
const allowRelaxed = pipelineInteriorBefore >= 0.85 && seedOverlap >= 0.90;
const occluderScene = allowRelaxed;
const bigDrop = (coverageBefore - coverageAfter) >= 0.08;
const leakSafe = leakAfter <= 0.08;
let leakOk: boolean;
if (allowRelaxed) {
const drop = coverageBefore - coverageAfter;
leakOk = leakAfter <= 0.09
|| (leakAfter <= 0.14 && drop >= 0.08);
} else {
leakOk = leakAfter <= 0.06 || (leakAfter <= 0.09 && interiorAfter >= 0.80);
}
let coverageTooHigh = allowRelaxed
? coverageAfter > coverageBefore + 0.25
: coverageAfter > coverageBefore + 0.15;
let covMin = Math.max(0.16, 0.45 * seedCoverage);
if (seedOverlap >= 0.98 && interiorAfter >= 0.18) {
covMin = Math.min(covMin, 0.12);
}
const covFloorRel = Number.isFinite(coverageBefore) ? 0.35 * coverageBefore : 0;
const covFloorAbs = allowRelaxed ? Math.min(covMin, 0.10 + 0.05 * seedCoverage) : covMin;
let coverageTooLow = coverageAfter < Math.max(covFloorRel, covFloorAbs);
if (interiorAfter >= 0.90 && leakAfter <= (allowRelaxed ? 0.12 : 0.05)) {
coverageTooLow = false;
}
if (occluderScene && bigDrop && leakSafe) {
coverageTooLow = false;
}
if (seedOverlap >= 0.97 && coverageAfter >= 0.12) {
coverageTooLow = false;
}
const invalidMask = !Number.isFinite(coverageAfter) || filteredMask.length !== (maskNormalized?.length ?? 0);
const fallbackMode = (process.env.OCCLUDER_PIPELINE_FALLBACK_MODE ?? "strict").trim().toLowerCase();
const fallbackDisabled = fallbackMode === "off" || fallbackMode === "disabled" || fallbackMode === "none";
let acceptPipeline = leakOk && !coverageTooHigh && !coverageTooLow && !invalidMask;
const samPatchMeta = (maskOriginMeta as { samPatch?: { applied?: boolean } } | null | undefined)?.samPatch;
const samPatchApplied = !!samPatchMeta?.applied;
const samPatchForceAccept = (process.env.OCCLUDER_SAM_PATCH_FORCE_ACCEPT ?? "1") !== "0";
const samPatchForceMaxCoverage = clamp(
Number(process.env.OCCLUDER_SAM_PATCH_FORCE_MAX_COVERAGE ?? "0.35"),
0.05,
1,
);
const allowSamPatchOverride = samPatchForceAccept
&& samPatchApplied
&& !invalidMask
&& coverageAfter <= samPatchForceMaxCoverage
&& coverageAfter >= Math.max(0.06, covFloorRel);
const allowOverride = (acceptanceOverride === 'soft-detail' && leakSafe)
|| (occluderScene && bigDrop && leakSafe && !coverageTooHigh && !invalidMask)
|| allowSamPatchOverride;
if (!acceptPipeline && allowOverride) {
filteredMask = Uint8Array.from(pipelineMaskBase);
coverageAfter = pipelineCoverageAfter;
leakAfter = pipelineLeakAfter;
interiorAfter = pipelineInteriorBefore;
coverageTooLow = false;
coverageTooHigh = false;
leakOk = true;
acceptPipeline = true;
if (!acceptanceOverride) acceptanceOverride = allowSamPatchOverride ? 'sam-patch-force' : 'occluder-relaxed';
}
if (!acceptPipeline && fallbackDisabled && !invalidMask) {
filteredMask = Uint8Array.from(pipelineMaskBase);
coverageAfter = pipelineCoverageAfter;
leakAfter = pipelineLeakAfter;
interiorAfter = pipelineInteriorBefore;
coverageTooLow = false;
coverageTooHigh = false;
leakOk = true;
acceptPipeline = true;
if (!acceptanceOverride) acceptanceOverride = 'fallback-disabled';
}
const acceptanceDebug: Record<string, unknown> = {
coverageBefore: Number(coverageBefore.toFixed(4)),
coverageAfter: Number(coverageAfter.toFixed(4)),
coverageDelta: Number((coverageAfter - coverageBefore).toFixed(4)),
interiorAfter: Number(interiorAfter.toFixed(4)),
leakBefore: Number(leakBefore.toFixed(6)),
leakAfter: Number(leakAfter.toFixed(6)),
seedOverlap: Number(seedOverlap.toFixed(4)),
allowRelaxed,
fallbackMode,
leakOk,
coverageTooHigh,
coverageTooLow,
invalidMask,
};
if (acceptanceOverride) {
acceptanceDebug.override = acceptanceOverride;
}
pipelineResult.debug.acceptance = acceptanceDebug;
if (acceptPipeline) {
maskNormalized = filteredMask;
maskCoverage = coverageAfter;
latticeLeak = leakAfter;
latticeInteriorCoverage = interiorAfter;
pipelineResult.debug.fallback = {
reason: {
leakTooHigh: false,
coverageTooHigh: false,
coverageTooLow: false,
invalidMask: false,
},
usedOriginal: false,
originalCoverage: coverageBefore,
pipelineCoverage: coverageAfter,
leakPct: leakAfter,
};
} else {
maskNormalized = maskNormalizedInitial ?? maskNormalized;
maskCoverage = maskNormalizedInitialCoverage ?? maskCoverage;
latticeLeak = leakAfter;
latticeInteriorCoverage = interiorAfter;
pipelineResult.debug.fallback = {
reason: {
leakTooHigh: !leakOk,
coverageTooHigh,
coverageTooLow,
invalidMask,
},
usedOriginal: true,
originalCoverage: coverageBefore,
pipelineCoverage: coverageAfter,
leakPct: leakAfter,
};
}
// Final airbag: Ensure mask never falls below 60% of seed coverage
if (u2Seeds && maskNormalized && seedCoverage > 0) {
const finalCoverage = coverageOfMask(maskNormalized);
const minFrac = 0.60;
if (finalCoverage < minFrac * seedCoverage) {
// Union with seeds to guarantee minimum coverage
for (let i = 0; i < maskNormalized.length; i++) {
if (u2Seeds[i]) maskNormalized[i] = 255;
}
maskCoverage = coverageOfMask(maskNormalized);
}
}
maskDebugPayload = {
preset: maskPresetApplied,
requestedPreset: presetRequested,
fallbackApplied: normalizationChosen.preset !== (normalizationPrimary?.preset ?? normalizationChosen.preset),
normalizedCoverage: maskCoverage,
originalCoverage: normalizationChosen.result.stats.originalCoverage,
background: normalizationChosen.result.stats.background ?? null,
seedCoverage: normalizationChosen.result.stats.hybrid?.seedCoverage ?? null,
pipeline: pipelineResult.debug,
raw: rawMaskDiagnostics ?? undefined,
origin: maskOriginSource ?? undefined,
originMeta: maskOriginMeta ?? undefined,
normalizeStats: normalizationChosen.result.stats,
normalizeOverrides: normalizationOverrides ?? null,
segCoverage: segmentationCoverage,
};
if (maskNormalizedDataUrl == null && maskWidth && maskHeight) {
maskNormalizedDataUrl = await maskToDataUrl(maskNormalized, maskWidth, maskHeight);
}
if (maskDebugEnabled && maskWidth && maskHeight) {
maskOverlayDataUrl = await overlayToDataUrl(raw, maskNormalized, maskWidth, maskHeight);
maskCutoutDataUrl = await cutoutToDataUrl(raw, maskNormalized, maskWidth, maskHeight);
}
finalMaskDataUrl = maskNormalizedDataUrl;
finalMaskMeta = {
polarity: 'white',
coverage: Number(maskCoverage.toFixed(3)),
preset: maskPresetApplied,
source: maskOriginSource ?? undefined,
stats: maskOriginMeta ?? undefined,
};
}
if (!finalMaskDataUrl && maskNormalized && maskWidth && maskHeight) {
maskNormalizedDataUrl = await maskToDataUrl(maskNormalized, maskWidth, maskHeight);
finalMaskDataUrl = maskNormalizedDataUrl;
finalMaskMeta = {
polarity: 'white',
coverage: maskCoverage != null ? Number(maskCoverage.toFixed(3)) : undefined,
preset: maskPresetApplied ?? undefined,
source: maskOriginSource ?? undefined,
stats: maskOriginMeta ?? undefined,
};
if (maskDebugEnabled) {
maskOverlayDataUrl = await overlayToDataUrl(raw, maskNormalized, maskWidth, maskHeight);
maskCutoutDataUrl = await cutoutToDataUrl(raw, maskNormalized, maskWidth, maskHeight);
}
}
if (!maskDebugPayload && normalizationChosen) {
maskDebugPayload = {
preset: maskPresetApplied,
requestedPreset: presetRequested,
fallbackApplied: normalizationChosen.preset !== (normalizationPrimary?.preset ?? normalizationChosen.preset),
normalizedCoverage: maskCoverage,
originalCoverage: normalizationChosen.result.stats.originalCoverage,
background: normalizationChosen.result.stats.background ?? null,
seedCoverage: normalizationChosen.result.stats.hybrid?.seedCoverage ?? null,
pipeline: { flags },
raw: rawMaskDiagnostics ?? undefined,
origin: maskOriginSource ?? undefined,
originMeta: maskOriginMeta ?? undefined,
};
}
if (tileMaskMeta?.tileBand) {
maskDebugPayload = {
...(maskDebugPayload ?? {}),
tileBandDebug: {
enabled: true,
yMin: tileMaskMeta.tileBand.yMin,
yMax: tileMaskMeta.tileBand.yMax,
confidence: tileMaskMeta.tileBand.confidence,
},
};
}
if (depthDebugPayload) {
maskDebugPayload = { ...(maskDebugPayload ?? {}), depth: depthDebugPayload };
}
if (tileSegmentationResult) {
// Prefer depth occluder but merge with segmentation masks so farther objects are not dropped.
const baseMaskData = rawMaskSnapshot ?? maskNormalized ?? null;
const baseMaskWidth = maskWidth ?? meta0.width ?? tileSegmentationResult.width;
const baseMaskHeight = maskHeight ?? meta0.height ?? tileSegmentationResult.height;
const useDepthOccluder = Boolean(depthOccluderMask) && !preferSegOccluder;
let occluderMaskForTiles = useDepthOccluder ? depthOccluderMask?.data ?? null : baseMaskData ?? null;
let occluderWidth = useDepthOccluder ? depthOccluderMask?.width ?? baseMaskWidth : baseMaskWidth;
let occluderHeight = useDepthOccluder ? depthOccluderMask?.height ?? baseMaskHeight : baseMaskHeight;
let occluderSource = useDepthOccluder ? 'depth-anything-v2' : (maskOriginSource ?? undefined);
let occluderDataUrl = useDepthOccluder
? (depthOccluderUrl ?? finalMaskDataUrl ?? maskNormalizedDataUrl ?? null)
: (finalMaskDataUrl ?? maskNormalizedDataUrl ?? null);
if (useDepthOccluder && depthOccluderMask && baseMaskData && baseMaskWidth && baseMaskHeight) {
const baseMask: BinaryMask = {
data: baseMaskData,
width: baseMaskWidth,
height: baseMaskHeight,
};
const depthAligned = await resizeBinaryMask(depthOccluderMask, baseMask.width, baseMask.height);
const merged = mergeOccluderMasks(baseMask, depthAligned);
occluderMaskForTiles = merged.data;
occluderWidth = merged.width;
occluderHeight = merged.height;
occluderSource = 'depth+seg';
if (maskDebugEnabled) {
occluderDataUrl = await maskToDataUrl(merged.data, merged.width, merged.height).catch(() => occluderDataUrl);
}
}
const colorEntries = tileSegmentationResult.classes.map((cls) => [String(cls.id), cls.color] as const);
const classPixelCountsRaw = countClassIds(tileSegmentationResult.ids);
const rawLegend = (() => {
const names = tileSegmentationResult.classes.map((cls) => String(cls.name).toLowerCase());
// 2-class tiles model: 0=background, 1=tiles
if (names.length === 2 && names[0] === 'background' && names[1] === 'tiles') {
return { "0": "background", "1": "tiles", "255": "ignore" };
}
return { "0": "background", "1": "grout", "2": "tiles", "255": "ignore" };
})();
const effectiveClasses = rawLegend["1"] === "tiles" && Object.keys(rawLegend).length === 3
? ['background', 'grout', 'tiles']
: tileSegmentationResult.classes.map((cls) => cls.name);
const effectiveColorMap = rawLegend["1"] === "tiles" && Object.keys(rawLegend).length === 3
? ({
"0": [0, 0, 0],
"1": [52, 152, 219],
"2": [46, 204, 113],
} satisfies Record<string, [number, number, number]>)
: (Object.fromEntries(colorEntries) as Record<string, [number, number, number]>);
const applyCombinedTileMask = (combined: any, occluderPayload: { dataUrl?: string | null } | null) => {
tileCombineResult = combined;
tileMaskDataUrl = combined.combined.dataUrl;
const classPixelCountsCombined = countClassIds(combined.combined.ids);
tileMaskMeta = {
// IMPORTANT: This metadata must match the *combined* mask image.
// For the 2-class tiles model we promote to an effective 3-class mask (bg/grout/tiles),
// so the frontend can reliably extract `tilesMask` and `groutMask`.
classes: effectiveClasses,
classIdLegend: { "0": "background", "1": "grout", "2": "tiles", "255": "ignore" },
classIdLegendRaw: rawLegend,
classPixelCountsRaw,
classPixelCounts: classPixelCountsCombined,
colorMap: effectiveColorMap,
coverage: combined.combined.coverage,
coverageAbsolute: combined.combined.coverageAbsolute,
rawCoverage: tileSegmentationResult.coverage,
occluderCoverage: combined.combined.occluderCoverage,
occludedPixels: combined.combined.occludedPixels,
occluderPixels: combined.combined.occluderPixels,
occludedCoverage: combined.combined.occludedCoverage,
warnings: combined.combined.warnings,
unetDebug: tileSegmentationResult.debug ?? null,
model: tileModelRequested,
tileBand: combined.combined.tileBand ? {
yMin: combined.combined.tileBand.yMin,
yMax: combined.combined.tileBand.yMax,
confidence: combined.combined.tileBand.confidence,
} : null,
tileMaskV3: (combined.combined as any).tileMaskV3 ?? null,
tileMaskV2: combined.combined.tileMaskV2 ?? null,
source: combined.combined.source,
};
tileMaskVersions = {
combined: combined.combined.dataUrl,
raw: combined.rawDataUrl,
occluder: combined.occluderDataUrl ?? (occluderPayload?.dataUrl ?? null),
};
};
try {
const { combineTileMaskWithOccluder } = await import('@/lib/segmentation/tile-mask');
const wallMaskPayload = depthBackMask
? { data: depthBackMask.data, width: depthBackMask.width, height: depthBackMask.height }
: null;
const bandOverrideParam = url.searchParams.get('tile_band_override');
const bandOverride = (() => {
if (!bandOverrideParam) return null;
const parts = bandOverrideParam.split(',').map((p) => Number(p));
if (parts.length === 2 && parts.every((v) => Number.isFinite(v) && v >= 0 && v <= 1)) {
return { yMinNorm: parts[0], yMaxNorm: parts[1] };
}
return null;
})();
// Enable depth wall-fill when we have a wall plane. The actual fill is still constrained by
// `tileBand`/grid ROI logic inside `combineTileMaskWithOccluder` so we don't leak into upper walls.
const enableDepthWallFill = Boolean(depthBackMask);
const tileMaskV3Enabled = process.env.TILEMASK_V3 === "1";
const tileMaskV2Enabled = process.env.TILEMASK_V2 === "1";
let sourceImageRgbaCache:
| { data: Uint8Array; width: number; height: number; channels: number }
| null
| undefined = undefined;
const getSourceImageRgba = async () => {
if (!tileMaskV2Enabled && !tileMaskV3Enabled) return null;
if (sourceImageRgbaCache !== undefined) return sourceImageRgbaCache;
try {
const rgbaRaw = await sharp(raw)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
sourceImageRgbaCache = {
data: new Uint8Array(rgbaRaw.data),
width: rgbaRaw.info.width,
height: rgbaRaw.info.height,
channels: rgbaRaw.info.channels,
};
} catch {
sourceImageRgbaCache = null;
}
return sourceImageRgbaCache;
};
const buildCombine = async (occluderPayloadOverride: {
data: Uint8Array;
width: number;
height: number;
dataUrl?: string | null;
source?: string | null;
} | null) => {
const combined = await combineTileMaskWithOccluder(
tileSegmentationResult,
occluderPayloadOverride,
{
bandOverride,
wallMask: wallMaskPayload,
enableDepthWallFill,
grid: { vlines: finalGrid.vlines, hlines: finalGrid.hlines },
tileMaskV3Enabled,
tileMaskV2Enabled,
sourceImageRgba: await getSourceImageRgba(),
},
);
applyCombinedTileMask(combined, occluderPayloadOverride);
return combined;
};
const occluderPayload = occluderMaskForTiles && occluderWidth && occluderHeight
? {
data: occluderMaskForTiles,
width: occluderWidth,
height: occluderHeight,
dataUrl: occluderDataUrl,
source: occluderSource,
}
: null;
const combined = await buildCombine(occluderPayload);
if (
preferTileBand &&
preferSegOccluder &&
maskOrig &&
depthOccluderMask &&
combined.combined.tileBand
) {
const bandMaxCoverage = clamp(
Number(process.env.OCCLUDER_SAM_DEPTH_BAND_MAX_COVERAGE ?? "0.6"),
0.1,
1,
);
const bandDilate = Math.max(
0,
Math.round(Number(process.env.OCCLUDER_TILE_BAND_DILATE ?? "6")),
);
const band = combined.combined.tileBand;
const targetW = meta0.width ?? tileSegmentationResult?.width ?? maskWidth ?? depthOccluderMask.width;
const targetH = meta0.height ?? tileSegmentationResult?.height ?? maskHeight ?? depthOccluderMask.height;
let skipBandUnion = false;
if (!targetW || !targetH) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "tile_band_missing_dims",
},
};
if (maskDebugPayload?.originMeta) {
maskDebugPayload.originMeta = maskOriginMeta;
}
skipBandUnion = true;
}
const yMin = skipBandUnion ? 0 : clamp(Math.floor(band.yMin), 0, targetH);
const yMax = skipBandUnion ? 0 : clamp(Math.floor(band.yMax), 0, targetH);
if (!skipBandUnion && yMax <= yMin) {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "tile_band_invalid",
bandSource: "tile_band_combined",
band: {
yMin: band.yMin,
yMax: band.yMax,
confidence: band.confidence,
},
},
};
if (maskDebugPayload?.originMeta) {
maskDebugPayload.originMeta = maskOriginMeta;
}
skipBandUnion = true;
}
if (!skipBandUnion) {
const bandBase = new Uint8Array(targetW * targetH);
for (let y = yMin; y < yMax; y++) {
const row = y * targetW;
bandBase.fill(255, row, row + targetW);
}
const bandMask = bandDilate > 0
? morphDilate(bandBase, targetW, targetH, bandDilate)
: bandBase;
const bandCoverage = coverageOfMask(bandMask);
if (bandCoverage <= bandMaxCoverage) {
const depthAligned = (depthOccluderMask.width !== targetW || depthOccluderMask.height !== targetH)
? await resizeBinaryMask(depthOccluderMask, targetW, targetH)
: depthOccluderMask;
const clipped = maskAnd(depthAligned.data, bandMask);
const baseMask = (maskWidth && maskHeight && (maskWidth !== targetW || maskHeight !== targetH))
? await resizeBinaryMask({ data: maskOrig, width: maskWidth, height: maskHeight }, targetW, targetH)
: { data: maskOrig, width: targetW, height: targetH };
const beforeCoverage = coverageOfMask(baseMask.data);
const merged = maskOr(baseMask.data, clipped);
const afterCoverage = coverageOfMask(merged);
maskOrig = merged;
maskWidth = targetW;
maskHeight = targetH;
rawMaskSnapshot = Uint8Array.from(maskOrig);
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: true,
bandApplied: true,
bandDilate,
bandCoverage: Number(bandCoverage.toFixed(4)),
bandMaxCoverage: Number(bandMaxCoverage.toFixed(3)),
bandSource: "tile_band_combined",
band: {
yMin: band.yMin,
yMax: band.yMax,
confidence: band.confidence,
},
coverageBefore: Number(beforeCoverage.toFixed(4)),
coverageAfter: Number(afterCoverage.toFixed(4)),
},
};
if (maskWidth && maskHeight) {
maskRawDataUrl = await maskToDataUrl(maskOrig, maskWidth, maskHeight);
finalMaskDataUrl = maskRawDataUrl;
finalMaskMeta = {
polarity: 'white',
source: maskOriginSource ?? undefined,
stats: maskOriginMeta ?? undefined,
};
}
const updatedPayload = {
data: maskOrig,
width: maskWidth ?? promptTileWidth,
height: maskHeight ?? promptTileHeight,
dataUrl: maskRawDataUrl,
source: maskOriginSource ?? undefined,
};
await buildCombine(updatedPayload);
} else {
maskOriginMeta = {
...(maskOriginMeta ?? {}),
depthBandUnion: {
applied: false,
reason: "band_too_large",
bandCoverage: Number(bandCoverage.toFixed(4)),
bandMaxCoverage: Number(bandMaxCoverage.toFixed(3)),
bandSource: "tile_band_combined",
band: {
yMin: band.yMin,
yMax: band.yMax,
confidence: band.confidence,
},
},
};
if (maskDebugPayload?.originMeta) {
maskDebugPayload.originMeta = maskOriginMeta;
}
}
}
}
} catch (tileCombineError) {
if (debug) {
console.warn('[tile-seg] failed to combine tile mask with occluder', tileCombineError);
}
tileMaskDataUrl = tileSegmentationResult.dataUrl;
tileMaskMeta = {
classes: tileSegmentationResult.classes.map((cls) => cls.name),
classIdLegend: { "0": "background", "1": "grout", "2": "tiles", "255": "ignore" },
classIdLegendRaw: rawLegend,
classPixelCountsRaw,
classPixelCounts: classPixelCountsRaw,
colorMap: Object.fromEntries(colorEntries) as Record<string, [number, number, number]>,
coverage: tileSegmentationResult.coverage,
coverageAbsolute: tileSegmentationResult.coverage,
unetDebug: tileSegmentationResult.debug ?? null,
source: 'unet_latest',
model: tileModelRequested,
};
tileMaskVersions = {
combined: tileSegmentationResult.dataUrl,
raw: tileSegmentationResult.dataUrl,
};
}
}
if (analysis.mask && normalizationChosen) {
analysis.mask = {
original_coverage: Number(normalizationChosen.result.stats.originalCoverage.toFixed(3)),
coverage: maskCoverage != null ? Number(maskCoverage.toFixed(3)) : 0,
threshold: Number((normalizationChosen.result.threshold / 255).toFixed(3)),
inverted: normalizationChosen.result.inverted,
source: maskOriginSource ?? undefined,
};
}
// Suppress heuristic warnings when only unet_latest is requested.
if (tileModelRequested === 'unet_latest') {
pipelineWarnings = undefined;
}
const fg_mask = finalMaskDataUrl;
const fg_mask_meta = finalMaskMeta;
if (maskDebugPayload?.originMeta && fg_mask_meta?.stats) {
maskDebugPayload.originMeta = fg_mask_meta.stats;
if (fg_mask_meta.source) {
maskDebugPayload.origin = fg_mask_meta.source;
}
}
const response = {
ok: true,
image: { width: meta0.width, height: meta0.height },
tile_w_px: finalTileW,
tile_h_px: finalTileH,
grout_px: finalGrout,
tile_w_mm: null, tile_h_mm: null, grout_mm: null,
grid: {
vlines: finalGrid.vlines,
hlines: finalGrid.hlines,
},
source,
tile_model: tileModelRequested,
ransac: ransacInfo,
fg_mask, // <<<<<<<<<< neu
...(fg_mask_meta ? { fg_mask_meta } : {}),
...(tileMaskDataUrl ? { tile_mask: tileMaskDataUrl } : {}),
...(tileMaskMeta ? { tile_mask_meta: tileMaskMeta } : {}),
...(tileMaskVersions ? { tile_mask_versions: tileMaskVersions } : {}),
...(maskPresetApplied ? { mask_preset: maskPresetApplied } : {}),
...(maskDebugPayload ? { mask_debug: maskDebugPayload } : {}),
...(depthDebugPayload ? { depth_debug: depthDebugPayload } : {}),
...(tileMaskMeta?.tileBand ? {
tile_band: {
yMin: tileMaskMeta.tileBand.yMin,
yMax: tileMaskMeta.tileBand.yMax,
confidence: tileMaskMeta.tileBand.confidence,
},
} : {}),
...((maskNormalizedDataUrl || maskRawDataUrl) ? {
mask_versions: {
normalized: maskNormalizedDataUrl,
raw: maskRawDataUrl,
overlay: maskOverlayDataUrl,
cutout: maskCutoutDataUrl,
},
} : {}),
...(maskCoverage !== null ? ({
mask_metrics: {
coverage: Number(maskCoverage.toFixed(3)),
coverageNorm: Number(maskCoverage.toFixed(3)),
coverageRaw: rawMaskDiagnostics
? Number(rawMaskDiagnostics.coverageArray.toFixed(3))
: (maskNormalizedInitialCoverage != null ? Number(maskNormalizedInitialCoverage.toFixed(3)) : undefined),
backgroundCoverage: maskBackgroundCoverage != null ? Number(maskBackgroundCoverage.toFixed(3)) : undefined,
occluderCoverage: maskOccluderCoverage != null ? Number(maskOccluderCoverage.toFixed(3)) : undefined,
seedCoverage: maskSeedCoverage != null ? Number(maskSeedCoverage.toFixed(3)) : undefined,
preset: maskPresetApplied ?? undefined,
source: maskOriginSource ?? undefined,
latticeLeak: latticeLeak ?? undefined,
latticeLeakPct: latticeLeak != null ? Number((latticeLeak * 100).toFixed(3)) : undefined,
latticeInteriorCoverage: latticeInteriorCoverage != null ? Number(latticeInteriorCoverage.toFixed(3)) : undefined,
warnings: pipelineWarnings && pipelineWarnings.length ? pipelineWarnings : undefined,
autotuneMs: flags.autotuneMs,
rawMismatch: rawMaskDiagnostics ? Number(rawMaskDiagnostics.mismatch.toFixed(3)) : undefined,
rawSuspicious: rawMaskDiagnostics ? rawMaskDiagnostics.suspicious : undefined,
},
}) : {}),
name: (file as any)?.name,
type: (file as any)?.type,
} as const;
return NextResponse.json(response);
} catch (err:any) {
console.error("API /api/analyze fatal:", err);
const msg = err?.message || String(err);
const code = /width\/height invalid|missing file/i.test(msg) ? 422 : 500;
return NextResponse.json({ ok:false, error: msg }, { status: code });
} finally {
restoreOccluderEnv?.();
}
}