| 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/components/ |
Upload File : |
/**
* Postprocessing für Occluder-Maske:
* - optional vorab weichzeichnen (anti-alias)
* - binär-thresholden
* - N Schritte 8-Nachbarschaft-DILATION (wächst Maske um ~growPx)
* - leichter Abschluss-Blur (weicher Rand)
* Rückgabe: Data-URL PNG (grau=alpha)
*/
export async function processMask(
maskDataUrl: string,
growPx = 6, // wie stark erweitern (≈ Pixel)
preBlurPx = 1, // kleiner Vorab-Blur glättet Löcher
thresh = 0.45, // 0..1: ab wann ein Pixel als "Maske=1" gilt
finalBlurPx = 1 // weiche Kante am Ende
): Promise<string> {
const img = await loadImg(maskDataUrl)
const w = (img as any).naturalWidth || img.width
const h = (img as any).naturalHeight || img.height
const c = document.createElement('canvas')
c.width = w; c.height = h
const ctx = c.getContext('2d', { willReadFrequently: true })!
// 1) Maske zeichnen (+ kleiner Vorab-Blur)
if (preBlurPx > 0) (ctx as any).filter = `blur(${preBlurPx}px)`
ctx.drawImage(img, 0, 0, w, h)
;(ctx as any).filter = 'none'
// 2) ImageData holen und in Float-Alpha (0..1) konvertieren
let id = ctx.getImageData(0, 0, w, h)
const total = w * h
const a = new Float32Array(total)
let nonZero = 0
for (let i = 0, j = 3; i < total; i++, j += 4) {
const raw = id.data[j] / 255
a[i] = raw
if (raw > 0) nonZero += 1
}
let coverage = nonZero / Math.max(1, total)
const originalCoverage = coverage
let gamma = 0.65
if (coverage < 0.03) gamma = 0.52
else if (coverage < 0.08) gamma = 0.58
else if (coverage < 0.18) gamma = 0.64
else gamma = 0.72
let inverted = false
if (coverage > 0.55) {
let invNonZero = 0
for (let i = 0; i < total; i++) {
const inv = 1 - a[i]
if (inv > 0) invNonZero += 1
}
const invCoverage = invNonZero / Math.max(1, total)
if (invCoverage < coverage * 0.6 && invCoverage + 0.02 < coverage) {
for (let i = 0; i < total; i++) a[i] = 1 - a[i]
coverage = invCoverage
nonZero = invNonZero
inverted = true
gamma = coverage < 0.03 ? 0.52 : coverage < 0.08 ? 0.58 : coverage < 0.18 ? 0.64 : 0.72
console.info('[mask] inverted high-coverage mask', {
original: Number(originalCoverage.toFixed(3)),
inverted: Number(coverage.toFixed(3)),
})
}
}
let boostedSum = 0
let boostedMax = 0
if (nonZero > 0) {
for (let i = 0; i < total; i++) {
const v = a[i]
if (v <= 0) {
a[i] = 0
continue
}
const boosted = Math.pow(v, gamma)
a[i] = boosted
boostedSum += boosted
if (boosted > boostedMax) boostedMax = boosted
}
}
// If the mask is almost completely white, treat it as an inverted foreground mask.
if (!inverted && coverage > 0.98) {
nonZero = 0
for (let i = 0; i < total; i++) {
const inv = 1 - a[i]
a[i] = inv
if (inv > 0) nonZero += 1
}
coverage = nonZero / Math.max(1, total)
gamma = coverage < 0.03 ? 0.52 : coverage < 0.08 ? 0.58 : coverage < 0.18 ? 0.64 : 0.72
inverted = true
if (coverage < 0.03) console.info('[mask] inverted near-full mask', { coverage: Number(coverage.toFixed(3)) })
}
let effectiveThresh = Math.max(0, Math.min(0.98, thresh))
if (nonZero >= 64 && boostedMax > 0) {
const mean = boostedSum / nonZero
const tail = Math.max(0, boostedMax - mean)
const minClamp = coverage < 0.03 ? 0.16 : coverage < 0.08 ? 0.2 : coverage < 0.18 ? 0.26 : 0.3
const autoCandidate = Math.max(minClamp, Math.min(0.98, mean + tail * 0.12))
const requestedBase = Math.max(minClamp, Math.min(0.98, thresh))
const coverageClamp = coverage >= 0.7 ? 0.58
: coverage >= 0.55 ? 0.5
: coverage >= 0.4 ? 0.38
: coverage >= 0.28 ? 0.3
: minClamp
const requested = Math.max(coverageClamp, requestedBase)
if (requested <= autoCandidate) {
effectiveThresh = requested
} else if (coverage < 0.05) {
effectiveThresh = requested
} else {
const slack = Math.max(0, Math.min(1, (0.25 - coverage) / 0.25))
const blend = slack * slack
effectiveThresh = requested * blend + autoCandidate * (1 - blend)
}
if (effectiveThresh < minClamp) effectiveThresh = minClamp
if (Math.abs(effectiveThresh - requestedBase) > 0.015) {
console.info('[mask] adaptive threshold', {
requested: Number(thresh.toFixed(3)),
requestedClamped: Number(requested.toFixed(3)),
auto: Number(autoCandidate.toFixed(3)),
used: Number(effectiveThresh.toFixed(3)),
coverage: Number(coverage.toFixed(3)),
gamma: Number(gamma.toFixed(2)),
})
}
}
// Kern (hoch-konfident) -> nutzen wir später zum Bereinigen der Maske.
const core = new Uint8Array(total)
let coreCount = 0
const ensureCore = (cut: number) => {
cut = Math.max(0, Math.min(0.98, cut))
for (let i = 0; i < total; i++) {
if (core[i]) continue
if (a[i] >= cut) {
core[i] = 1
coreCount += 1
}
}
}
const initialCoreCut = Math.min(0.92, Math.max(0.52, effectiveThresh + 0.2))
ensureCore(initialCoreCut)
if (coreCount === 0 && nonZero > 0) ensureCore(Math.max(0.35, effectiveThresh + 0.1))
if (coreCount === 0 && nonZero > 0) ensureCore(Math.max(0.25, effectiveThresh))
if (coreCount === 0 && nonZero > 0) {
let maxBoost = 0
for (let i = 0; i < total; i++) if (a[i] > maxBoost) maxBoost = a[i]
if (maxBoost > 0) ensureCore(Math.max(0.2, maxBoost * 0.72))
}
// 3) Threshold -> binary
const bins = new Uint32Array(256)
for (let i = 0; i < total; i++) {
const v = Math.max(0, Math.min(1, a[i]))
bins[Math.min(255, Math.round(v * 255))] += 1
}
const targetCoverage = Math.min(0.4, Math.max(coverage * 3, coverage + 0.12))
const targetPixels = Math.max(1, Math.round(targetCoverage * total))
let cumulative = 0
let quantileBin = 255
for (let b = 255; b >= 0; b--) {
cumulative += bins[b]
if (cumulative >= targetPixels) {
quantileBin = b
break
}
}
const quantileThresh = quantileBin / 255
const finalThresh = inverted ? quantileThresh : Math.min(effectiveThresh, quantileThresh)
const bin = new Uint8Array(total)
let binActive = 0
for (let i = 0; i < total; i++) {
if (a[i] >= finalThresh) {
bin[i] = 1
binActive += 1
}
}
const postThreshCoverage = binActive / Math.max(1, total)
// Entferne Komponenten, die nicht mit einem Kern verbunden sind
if (coreCount > 0) {
const queue = new Uint32Array(total)
const localVisited = new Uint8Array(total)
const component: number[] = []
const sharedVisited = new Uint8Array(total)
const componentCoreCut = Math.max(0.3, Math.min(0.9, effectiveThresh + 0.08))
const flood = (start: number, visitMap: Uint8Array) => {
let qh = 0
let qt = 0
queue[qt++] = start
visitMap[start] = 1
component.length = 0
let hasCore = !!core[start]
while (qh < qt) {
const idx = queue[qh++]!
component.push(idx)
if (core[idx] || a[idx] >= componentCoreCut) hasCore = true
const x = idx % w
const y = (idx - x) / w
const tryPush = (nx: number, ny: number) => {
if (nx < 0 || ny < 0 || nx >= w || ny >= h) return
const ni = ny * w + nx
if (visitMap[ni] || !bin[ni]) return
visitMap[ni] = 1
queue[qt++] = ni
}
tryPush(x - 1, y)
tryPush(x + 1, y)
tryPush(x, y - 1)
tryPush(x, y + 1)
}
return hasCore
}
for (let i = 0; i < total; i++) {
if (!bin[i] || sharedVisited[i]) continue
const hasCore = flood(i, sharedVisited)
if (!hasCore) {
component.length = 0
const hasWeak = flood(i, localVisited)
if (!hasWeak) {
for (const idx of component) bin[idx] = 0
} else {
for (const idx of component) {
if (a[idx] < componentCoreCut) bin[idx] = 0
}
}
component.length = 0
}
}
}
// 4) Dilation-Schritte (8-neighborhood)
let steps = Math.max(0, Math.round(growPx))
if (postThreshCoverage < 0.06) {
steps = Math.max(steps, Math.round(growPx * 1.5) || 2)
} else if (postThreshCoverage < 0.12) {
steps = Math.max(steps, Math.round(growPx * 1.2) || 1)
}
if (steps > 0) {
const tmp = new Uint8Array(w * h)
const growFloor = Math.max(0.1, Math.min(0.35, finalThresh * 0.75))
const allowAggressive = coverage < 0.03
for (let s = 0; s < steps; s++) {
tmp.set(bin)
for (let y = 0; y < h; y++) {
const yMin = Math.max(0, y - 1)
const yMax = Math.min(h - 1, y + 1)
for (let x = 0; x < w; x++) {
const idx = y * w + x
if (bin[idx]) continue
const xMin = Math.max(0, x - 1)
const xMax = Math.min(w - 1, x + 1)
let grow = false
for (let ny = yMin; ny <= yMax && !grow; ny++) {
const rowOffset = ny * w
for (let nx = xMin; nx <= xMax; nx++) {
if (bin[rowOffset + nx]) {
grow = true
break
}
}
}
if (grow) {
if (a[idx] >= growFloor || (allowAggressive && s === 0)) {
tmp[idx] = 1
}
}
}
}
bin.set(tmp)
}
}
// 5) Zurück in ImageData (Alpha-Kanal setzen)
for (let i=0, j=3; i<w*h; i++, j+=4) {
id.data[j] = bin[i] ? 255 : 0
// Kanäle R,G,B egal – setzen wir neutral grau (optional)
id.data[j-1] = id.data[j-2] = id.data[j-3] = 128
}
ctx.putImageData(id, 0, 0)
// 6) Abschluss-Blur für weiche Kante
if (finalBlurPx > 0) {
const c2 = document.createElement('canvas')
c2.width = w; c2.height = h
const k = c2.getContext('2d')!
k.drawImage(c, 0, 0)
k.globalCompositeOperation = 'copy' // Alpha erhalten
;(k as any).filter = `blur(${finalBlurPx}px)`
k.drawImage(c2, 0, 0) // self-blur
return c2.toDataURL('image/png')
}
return c.toDataURL('image/png')
}
async function loadImg(src: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = () => resolve(img)
img.onerror = reject
img.src = src
})
}