From 105fc67c9bedaa0d847aa314299b40a724b2d321 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 16 Oct 2026 16:10:27 +0700 Subject: [PATCH] fix(heal): restore sharp photorealistic textures and structural lines via 1:1 scale exemplar synthesis --- docker/frontend/src/engine/exportEngine.ts | 5 +- docker/frontend/src/engine/inpaint.ts | 332 ++++++++------------- 2 files changed, 132 insertions(+), 205 deletions(-) diff --git a/docker/frontend/src/engine/exportEngine.ts b/docker/frontend/src/engine/exportEngine.ts index 5bb6bff..16fed42 100644 --- a/docker/frontend/src/engine/exportEngine.ts +++ b/docker/frontend/src/engine/exportEngine.ts @@ -1088,10 +1088,7 @@ async function applyHealToBase( const srcH = srcBottom - srcTop; if (srcW >= 8 && srcH >= 8) { - const { w: mw, h: mh } = inpaintCropSize( - (srcW / width) * INPAINT_REF_LONG, - (srcH / height) * INPAINT_REF_LONG - ); + const { w: mw, h: mh } = inpaintCropSize(srcW, srcH); const cropSurface = tempOwn(createSurface(mw, mh)); if (cropSurface) { cropSurface.getCanvas().drawImageRect( diff --git a/docker/frontend/src/engine/inpaint.ts b/docker/frontend/src/engine/inpaint.ts index 3fbb07d..e496a6b 100644 --- a/docker/frontend/src/engine/inpaint.ts +++ b/docker/frontend/src/engine/inpaint.ts @@ -155,39 +155,56 @@ export function fastLocalInpaint( const out = new Uint8Array(rgba.length); out.set(rgba); - const mask = new Uint8Array(total); + const clean = new Uint8Array(total); let holeCount = 0; for (let i = 0; i < total; i++) { - if (hole[i]) { - mask[i] = 1; - holeCount++; - } + if (!hole[i]) clean[i] = 1; + else holeCount++; } if (holeCount === 0) return out; - // 1. Distance transform from clean boundary into the hole - const dist = new Float32Array(total).fill(1e6); - const queue: number[] = []; - const boundaryDonors: number[] = []; + // 1. Measure vertical structure periodicity (e.g. brick courses, horizontal joints, slats) + const rowLum = new Float32Array(h); + for (let y = 0; y < h; y++) { + let sum = 0; + const row = y * w * 4; + for (let x = 0; x < w; x++) { + const p = row + (x << 2); + sum += (rgba[p] * 77 + rgba[p + 1] * 150 + rgba[p + 2] * 29) >> 8; + } + rowLum[y] = sum / w; + } + let lumMean = 0; + for (let y = 0; y < h; y++) lumMean += rowLum[y]; + lumMean /= h; + for (let y = 0; y < h; y++) rowLum[y] -= lumMean; + let period = 0; + let bestCorr = 0; + const maxLag = Math.min(60, Math.floor(h / 2)); + for (let lag = 12; lag < maxLag; lag++) { + let sum = 0; + for (let y = 0; y < h - lag; y++) sum += rowLum[y] * rowLum[y + lag]; + if (sum > bestCorr) { + bestCorr = sum; + period = lag; + } + } + + // 2. Distance transform for onion-peel layer-by-layer inward synthesis + const dist = new Int16Array(total).fill(30000); + const queue: number[] = []; for (let y = 0; y < h; y++) { const row = y * w; for (let x = 0; x < w; x++) { const idx = row + x; - if (!mask[idx]) { + if (clean[idx]) { dist[idx] = 0; - const up = y > 0 && mask[idx - w]; - const down = y < h - 1 && mask[idx + w]; - const left = x > 0 && mask[idx - 1]; - const right = x < w - 1 && mask[idx + 1]; - if (up || down || left || right) { - boundaryDonors.push(idx); - } } else { - const up = y > 0 && !mask[idx - w]; - const down = y < h - 1 && !mask[idx + w]; - const left = x > 0 && !mask[idx - 1]; - const right = x < w - 1 && !mask[idx + 1]; + const up = y > 0 && clean[idx - w]; + const down = y < h - 1 && clean[idx + w]; + const left = x > 0 && clean[idx - 1]; + const right = x < w - 1 && clean[idx + 1]; if (up || down || left || right) { dist[idx] = 1; queue.push(idx); @@ -196,211 +213,124 @@ export function fastLocalInpaint( } } - if (boundaryDonors.length === 0) return out; - let head = 0; while (head < queue.length) { const idx = queue[head++]; + const dCur = dist[idx]; const cx = idx % w; const cy = (idx / w) | 0; - const dCur = dist[idx]; - const neighbors = [ - cy > 0 ? idx - w : -1, - cy < h - 1 ? idx + w : -1, - cx > 0 ? idx - 1 : -1, - cx < w - 1 ? idx + 1 : -1, - ]; + const n1 = cy > 0 ? idx - w : -1; + const n2 = cy < h - 1 ? idx + w : -1; + const n3 = cx > 0 ? idx - 1 : -1; + const n4 = cx < w - 1 ? idx + 1 : -1; - for (let n = 0; n < 4; n++) { - const nIdx = neighbors[n]; - if (nIdx !== -1 && mask[nIdx] && dist[nIdx] > dCur + 1) { - dist[nIdx] = dCur + 1; - queue.push(nIdx); + if (n1 !== -1 && dist[n1] > dCur + 1) { dist[n1] = dCur + 1; queue.push(n1); } + if (n2 !== -1 && dist[n2] > dCur + 1) { dist[n2] = dCur + 1; queue.push(n2); } + if (n3 !== -1 && dist[n3] > dCur + 1) { dist[n3] = dCur + 1; queue.push(n3); } + if (n4 !== -1 && dist[n4] > dCur + 1) { dist[n4] = dCur + 1; queue.push(n4); } + } + + // 3. Candidate donor relative shifts (dy, dx) + const shiftsY: number[] = []; + const shiftsX: number[] = []; + + // Shifts based on periodic structure if detected + if (period >= 12) { + for (const mult of [-2, -1, 1, 2]) { + const dy = mult * period; + for (let dx = -40; dx <= 40; dx += 4) { + shiftsY.push(dy); + shiftsX.push(dx); } } } - - // Sort queue by distance: outer boundary to inner core - queue.sort((a, b) => dist[a] - dist[b]); - - let sumHx = 0, sumHy = 0; - for (let i = 0; i < queue.length; i++) { - const idx = queue[i]; - sumHx += idx % w; - sumHy += (idx / w) | 0; + // Local horizontal and diagonal shifts + for (let dy = -6; dy <= 6; dy += 2) { + for (const dx of [-36, -28, -20, -14, -8, 8, 14, 20, 28, 36]) { + shiftsY.push(dy); + shiftsX.push(dx); + } + } + // Vertical shifts + for (const dy of [-28, -20, -14, -7, 7, 14, 20, 28]) { + for (let dx = -10; dx <= 10; dx += 5) { + shiftsY.push(dy); + shiftsX.push(dx); + } } - const holeCx = sumHx / queue.length; - const holeCy = sumHy / queue.length; - // 2. Anisotropic gradient propagation along isophotes - const R = 7; + const shiftCount = shiftsY.length; + const filled = new Uint8Array(clean); + + // 4. Inward synthesis: process pixels from boundary inward (queue order) + const P = 3; // 7x7 patch for (let i = 0; i < queue.length; i++) { const idx = queue[i]; - const px = idx % w; - const py = (idx / w) | 0; + const cx = idx % w; + const cy = (idx / w) | 0; - let sumR = 0, sumG = 0, sumB = 0, sumW = 0; + let bestCost = 1e9; + let bestDonor = idx; - // Normal vector pointing inward - let nx = 0, ny = 0; - if (px > 0 && px < w - 1) nx = dist[idx + 1] - dist[idx - 1]; - if (py > 0 && py < h - 1) ny = dist[idx + w] - dist[idx - w]; - const nLen = Math.hypot(nx, ny); - if (nLen > 1e-4) { nx /= nLen; ny /= nLen; } + for (let s = 0; s < shiftCount; s++) { + const dy = shiftsY[s]; + const dx = shiftsX[s]; + const sy = cy + dy; + const sx = cx + dx; - for (let dy = -R; dy <= R; dy++) { - const sy = py + dy; - if (sy < 0 || sy >= h) continue; - const srow = sy * w; - for (let dx = -R; dx <= R; dx++) { - const sx = px + dx; - if (sx < 0 || sx >= w) continue; - const d2 = dx * dx + dy * dy; - if (d2 === 0 || d2 > R * R) continue; + if (sy < P || sy >= h - P || sx < P || sx >= w - P) continue; + const donorIdx = sy * w + sx; + if (!clean[donorIdx]) continue; - const sIdx = srow + sx; - if (!mask[sIdx] || dist[sIdx] < dist[idx]) { - const dDist = Math.sqrt(d2); - const dir = Math.abs((dx * (-ny) + dy * nx) / dDist) * 0.7 + 0.3; - const wSample = dir * Math.exp(-dDist / 3.0) / (1 + dist[sIdx] * 0.5); + let sumDiff = 0; + let count = 0; - const sp = sIdx * 4; - let gx = 0, gy = 0; - if (sx > 0 && sx < w - 1) gx = (out[sp + 4] - out[sp - 4]) * 0.5; - if (sy > 0 && sy < h - 1) gy = (out[(srow + w + sx) * 4] - out[(srow - w + sx) * 4]) * 0.5; + for (let py = -P; py <= P; py++) { + const ty = cy + py; + const s_y = sy + py; + if (ty < 0 || ty >= h || s_y < 0 || s_y >= h) continue; + const rowT = ty * w; + const rowS = s_y * w; - const rExt = out[sp] + gx * (-dx) * 0.3; - const gExt = out[sp + 1] + gy * (-dy) * 0.3; - const bExt = out[sp + 2] + (gx + gy) * 0.5 * (-dx - dy) * 0.15; + for (let px = -P; px <= P; px++) { + const tx = cx + px; + const s_x = sx + px; + if (tx < 0 || tx >= w || s_x < 0 || s_x >= w) continue; - sumR += Math.max(0, Math.min(255, rExt)) * wSample; - sumG += Math.max(0, Math.min(255, gExt)) * wSample; - sumB += Math.max(0, Math.min(255, bExt)) * wSample; - sumW += wSample; + const pTarget = rowT + tx; + if (filled[pTarget]) { + const pDonor = rowS + s_x; + const tIdx = pTarget << 2; + const dIdx = pDonor << 2; + + const dr = out[tIdx] - rgba[dIdx]; + const dg = out[tIdx + 1] - rgba[dIdx + 1]; + const db = out[tIdx + 2] - rgba[dIdx + 2]; + + sumDiff += dr * dr + dg * dg + db * db; + count++; + } + } + } + + if (count > 0) { + const cost = (sumDiff / count) + Math.abs(dy) * 0.4 + Math.abs(dx) * 0.08; + if (cost < bestCost) { + bestCost = cost; + bestDonor = donorIdx; + if (cost < 20.0) break; // early exit on near-perfect match } } } - const p = idx * 4; - if (sumW > 0) { - out[p] = Math.round(sumR / sumW); - out[p + 1] = Math.round(sumG / sumW); - out[p + 2] = Math.round(sumB / sumW); - } else { - let bestD2 = 1e9; - let bestP = boundaryDonors[0] * 4; - const bStep = Math.max(1, Math.floor(boundaryDonors.length / 32)); - for (let k = 0; k < boundaryDonors.length; k += bStep) { - const bIdx = boundaryDonors[k]; - const bx = bIdx % w; - const by = (bIdx / w) | 0; - const bd2 = (px - bx) ** 2 + (py - by) ** 2; - if (bd2 < bestD2) { - bestD2 = bd2; - bestP = bIdx * 4; - } - } - out[p] = out[bestP]; - out[p + 1] = out[bestP + 1]; - out[p + 2] = out[bestP + 2]; - } - } - - // 3. Multi-scale Poisson relaxation - for (let pass = 0; pass < 4; pass++) { - for (let i = 0; i < queue.length; i++) { - const idx = queue[i]; - const px = idx % w; - const py = (idx / w) | 0; - if (px === 0 || px === w - 1 || py === 0 || py === h - 1) continue; - - const p = idx * 4; - const l = (idx - 1) * 4; - const r = (idx + 1) * 4; - const u = (idx - w) * 4; - const d = (idx + w) * 4; - - for (let c = 0; c < 3; c++) { - const avg = (out[l + c] + out[r + c] + out[u + c] + out[d + c]) * 0.25; - out[p + c] = Math.round(out[p + c] * 0.35 + avg * 0.65); - } - } - } - - // 4. Authentic Exemplar Texture & Grain Transfer - const bCount = boundaryDonors.length; - const bStep = Math.max(1, Math.floor(bCount / 64)); - let noiseVarSum = 0; - let noiseVarCount = 0; - - for (let i = 0; i < bCount; i += bStep) { - const bIdx = boundaryDonors[i]; - const bx = bIdx % w; - const by = (bIdx / w) | 0; - if (bx > 0 && bx < w - 1 && by > 0 && by < h - 1) { - const bp = bIdx * 4; - const avgSurrR = (rgba[bp - 4] + rgba[bp + 4] + rgba[(by - 1) * w * 4 + bx * 4] + rgba[(by + 1) * w * 4 + bx * 4]) * 0.25; - const avgSurrG = (rgba[bp - 3] + rgba[bp + 5] + rgba[(by - 1) * w * 4 + bx * 4 + 1] + rgba[(by + 1) * w * 4 + bx * 4 + 1]) * 0.25; - const avgSurrB = (rgba[bp - 2] + rgba[bp + 6] + rgba[(by - 1) * w * 4 + bx * 4 + 2] + rgba[(by + 1) * w * 4 + bx * 4 + 2]) * 0.25; - const diffR = rgba[bp] - avgSurrR; - const diffG = rgba[bp + 1] - avgSurrG; - const diffB = rgba[bp + 2] - avgSurrB; - noiseVarSum += Math.abs(diffR) + Math.abs(diffG) + Math.abs(diffB); - noiseVarCount++; - } - } - const avgTextureEnergy = noiseVarCount > 0 ? (noiseVarSum / noiseVarCount) / 3 : 1.5; - - let seed = 1234567; - const rand = () => { - seed = (seed * 16807) % 2147483647; - return (seed / 2147483647) - 0.5; - }; - - for (let i = 0; i < queue.length; i++) { - const idx = queue[i]; - const px = idx % w; - const py = (idx / w) | 0; - - const mx = Math.max(0, Math.min(w - 1, Math.round(2 * holeCx - px))); - const my = Math.max(0, Math.min(h - 1, Math.round(2 * holeCy - py))); - const mIdx = my * w + mx; - - let donorIdx = -1; - if (!mask[mIdx]) { - donorIdx = mIdx; - } else { - let bestDist = 1e9; - for (let k = 0; k < bCount; k += bStep) { - const bp = boundaryDonors[k]; - const bbx = bp % w; - const bby = (bp / w) | 0; - const d = (px - bbx) ** 2 + (py - bby) ** 2; - if (d < bestDist) { - bestDist = d; - donorIdx = bp; - } - } - } - - const p = idx * 4; - if (donorIdx >= 0) { - const dp = donorIdx * 4; - const dx = donorIdx % w; - const dy = (donorIdx / w) | 0; - let highR = 0, highG = 0, highB = 0; - if (dx > 0 && dx < w - 1 && dy > 0 && dy < h - 1) { - highR = rgba[dp] - (rgba[dp - 4] + rgba[dp + 4] + rgba[(dy - 1) * w * 4 + dx * 4] + rgba[(dy + 1) * w * 4 + dx * 4]) * 0.25; - highG = rgba[dp + 1] - (rgba[dp - 3] + rgba[dp + 5] + rgba[(dy - 1) * w * 4 + dx * 4 + 1] + rgba[(dy + 1) * w * 4 + dx * 4 + 1]) * 0.25; - highB = rgba[dp + 2] - (rgba[dp - 2] + rgba[dp + 6] + rgba[(dy - 1) * w * 4 + dx * 4 + 2] + rgba[(dy + 1) * w * 4 + dx * 4 + 2]) * 0.25; - } - const grain = rand() * Math.min(3.5, avgTextureEnergy * 0.5); - out[p] = Math.max(0, Math.min(255, Math.round(out[p] + highR * 0.75 + grain))); - out[p + 1] = Math.max(0, Math.min(255, Math.round(out[p + 1] + highG * 0.75 + grain))); - out[p + 2] = Math.max(0, Math.min(255, Math.round(out[p + 2] + highB * 0.75 + grain))); - } - out[p + 3] = 255; + const tIdx = idx << 2; + const dIdx = bestDonor << 2; + out[tIdx] = rgba[dIdx]; + out[tIdx + 1] = rgba[dIdx + 1]; + out[tIdx + 2] = rgba[dIdx + 2]; + out[tIdx + 3] = 255; + filled[idx] = 1; } return out;