fix(heal): restore sharp photorealistic textures and structural lines via 1:1 scale exemplar synthesis
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user