fix(heal): eliminate extraneous pattern overlays and restore seamless directional texture via structure-tensor guided inpainting
This commit is contained in:
@@ -1079,7 +1079,7 @@ async function applyHealToBase(
|
|||||||
const cSpanW = mx1 - mx0;
|
const cSpanW = mx1 - mx0;
|
||||||
const cSpanH = my1 - my0;
|
const cSpanH = my1 - my0;
|
||||||
const cSpan = Math.max(cSpanW, cSpanH);
|
const cSpan = Math.max(cSpanW, cSpanH);
|
||||||
const margin = Math.max(96, Math.min(512, Math.round(cWidest * 3 + cSpan * 0.8)));
|
const margin = Math.max(24, Math.min(96, Math.round(cWidest * 2 + 16)));
|
||||||
const srcLeft = Math.max(boxLeft, Math.floor(mx0 - margin));
|
const srcLeft = Math.max(boxLeft, Math.floor(mx0 - margin));
|
||||||
const srcTop = Math.max(boxTop, Math.floor(my0 - margin));
|
const srcTop = Math.max(boxTop, Math.floor(my0 - margin));
|
||||||
const srcRight = Math.min(boxRight, Math.ceil(mx1 + margin));
|
const srcRight = Math.min(boxRight, Math.ceil(mx1 + margin));
|
||||||
|
|||||||
@@ -344,16 +344,35 @@ export function fastLocalInpaint(
|
|||||||
const total = w * h;
|
const total = w * h;
|
||||||
const clean = new Uint8Array(total);
|
const clean = new Uint8Array(total);
|
||||||
let holeCount = 0;
|
let holeCount = 0;
|
||||||
for (let i = 0; i < total; i++) {
|
let ymin = h, ymax = -1, xmin = w, xmax = -1;
|
||||||
if (!hole[i]) clean[i] = 1;
|
|
||||||
else holeCount++;
|
for (let y = 0; y < h; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = 0; x < w; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (!hole[idx]) {
|
||||||
|
clean[idx] = 1;
|
||||||
|
} else {
|
||||||
|
holeCount++;
|
||||||
|
if (y < ymin) ymin = y;
|
||||||
|
if (y > ymax) ymax = y;
|
||||||
|
if (x < xmin) xmin = x;
|
||||||
|
if (x > xmax) xmax = x;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (holeCount === 0) {
|
|
||||||
|
if (holeCount === 0 || holeCount === total) {
|
||||||
const res = new Uint8Array(rgba.length);
|
const res = new Uint8Array(rgba.length);
|
||||||
res.set(rgba);
|
res.set(rgba);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ymin = Math.max(1, ymin - 1);
|
||||||
|
ymax = Math.min(h - 2, ymax + 1);
|
||||||
|
xmin = Math.max(1, xmin - 1);
|
||||||
|
xmax = Math.min(w - 2, xmax + 1);
|
||||||
|
|
||||||
// 1. Two-pass Euclidean Vector Distance Transform (EDT)
|
// 1. Two-pass Euclidean Vector Distance Transform (EDT)
|
||||||
const ny = new Int16Array(total);
|
const ny = new Int16Array(total);
|
||||||
const nx = new Int16Array(total);
|
const nx = new Int16Array(total);
|
||||||
@@ -445,7 +464,7 @@ export function fastLocalInpaint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Base structural layer: Dirichlet Poisson / Laplace solve (∇² I = 0)
|
// 2. Base structural layer: Dirichlet Poisson / Laplace solve (∇² I = 0) with SOR
|
||||||
const baseR = new Float32Array(total);
|
const baseR = new Float32Array(total);
|
||||||
const baseG = new Float32Array(total);
|
const baseG = new Float32Array(total);
|
||||||
const baseB = new Float32Array(total);
|
const baseB = new Float32Array(total);
|
||||||
@@ -464,27 +483,33 @@ export function fastLocalInpaint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gauss-Seidel Poisson relaxation (25 passes guarantees smooth harmonic convergence inside hole)
|
// Successive Over-Relaxation (SOR) with omega = 1.75
|
||||||
for (let it = 0; it < 25; it++) {
|
const omega = 1.75;
|
||||||
for (let y = 0; y < h; y++) {
|
const oneMinusOmega = 1.0 - omega;
|
||||||
|
for (let it = 0; it < 35; it++) {
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
const row = y * w;
|
const row = y * w;
|
||||||
for (let x = 0; x < w; x++) {
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
const idx = row + x;
|
const idx = row + x;
|
||||||
if (clean[idx]) continue;
|
if (clean[idx]) continue;
|
||||||
|
|
||||||
const up = y > 0 ? idx - w : idx;
|
const up = idx - w;
|
||||||
const down = y < h - 1 ? idx + w : idx;
|
const down = idx + w;
|
||||||
const left = x > 0 ? idx - 1 : idx;
|
const left = idx - 1;
|
||||||
const right = x < w - 1 ? idx + 1 : idx;
|
const right = idx + 1;
|
||||||
|
|
||||||
baseR[idx] = 0.25 * (baseR[up] + baseR[down] + baseR[left] + baseR[right]);
|
const avgR = (baseR[up] + baseR[down] + baseR[left] + baseR[right]) * 0.25;
|
||||||
baseG[idx] = 0.25 * (baseG[up] + baseG[down] + baseG[left] + baseG[right]);
|
const avgG = (baseG[up] + baseG[down] + baseG[left] + baseG[right]) * 0.25;
|
||||||
baseB[idx] = 0.25 * (baseG[up] + baseG[down] + baseG[left] + baseG[right]);
|
const avgB = (baseB[up] + baseB[down] + baseB[left] + baseB[right]) * 0.25;
|
||||||
|
|
||||||
|
baseR[idx] = oneMinusOmega * baseR[idx] + omega * avgR;
|
||||||
|
baseG[idx] = oneMinusOmega * baseG[idx] + omega * avgG;
|
||||||
|
baseB[idx] = oneMinusOmega * baseB[idx] + omega * avgB;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Separable 5-tap Gaussian Filter on original image
|
// 3. Separable 5-tap Gaussian Filter on original image to extract high-pass micro-texture
|
||||||
const tempR = new Float32Array(total);
|
const tempR = new Float32Array(total);
|
||||||
const tempG = new Float32Array(total);
|
const tempG = new Float32Array(total);
|
||||||
const tempB = new Float32Array(total);
|
const tempB = new Float32Array(total);
|
||||||
@@ -524,23 +549,231 @@ export function fastLocalInpaint(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Coherent texture transfer & final assembly
|
// 4. Structure Tensor on local clean boundary band (within 12px)
|
||||||
|
let sumGxx = 0, sumGyy = 0, sumGxy = 0, bandCount = 0;
|
||||||
|
let hpVarSum = 0;
|
||||||
|
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (!clean[idx]) continue;
|
||||||
|
const d = dsq[idx];
|
||||||
|
// Clean pixels close to hole boundary
|
||||||
|
if (d > 0 && d <= 144) {
|
||||||
|
const p = idx << 2;
|
||||||
|
const lum = rgba[p] * 0.299 + rgba[p + 1] * 0.587 + rgba[p + 2] * 0.114;
|
||||||
|
const lumL = rgba[(row + Math.max(0, x - 1)) << 2] * 0.299 + rgba[(row + Math.max(0, x - 1)) << 2 + 1] * 0.587 + rgba[(row + Math.max(0, x - 1)) << 2 + 2] * 0.114;
|
||||||
|
const lumR = rgba[(row + Math.min(w - 1, x + 1)) << 2] * 0.299 + rgba[(row + Math.min(w - 1, x + 1)) << 2 + 1] * 0.587 + rgba[(row + Math.min(w - 1, x + 1)) << 2 + 2] * 0.114;
|
||||||
|
const lumT = rgba[(Math.max(0, y - 1) * w + x) << 2] * 0.299 + rgba[(Math.max(0, y - 1) * w + x) << 2 + 1] * 0.587 + rgba[(Math.max(0, y - 1) * w + x) << 2 + 2] * 0.114;
|
||||||
|
const lumB = rgba[(Math.min(h - 1, y + 1) * w + x) << 2] * 0.299 + rgba[(Math.min(h - 1, y + 1) * w + x) << 2 + 1] * 0.587 + rgba[(Math.min(h - 1, y + 1) * w + x) << 2 + 2] * 0.114;
|
||||||
|
|
||||||
|
const gx = (lumR - lumL) * 0.5;
|
||||||
|
const gy = (lumB - lumT) * 0.5;
|
||||||
|
|
||||||
|
sumGxx += gx * gx;
|
||||||
|
sumGyy += gy * gy;
|
||||||
|
sumGxy += gx * gy;
|
||||||
|
bandCount++;
|
||||||
|
|
||||||
|
const diffR = rgba[p] - meanR[idx];
|
||||||
|
const diffG = rgba[p + 1] - meanG[idx];
|
||||||
|
const diffB = rgba[p + 2] - meanB[idx];
|
||||||
|
hpVarSum += diffR * diffR + diffG * diffG + diffB * diffB;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let coherence = 0;
|
||||||
|
if (bandCount > 0) {
|
||||||
|
const jxx = sumGxx / bandCount;
|
||||||
|
const jyy = sumGyy / bandCount;
|
||||||
|
const jxy = sumGxy / bandCount;
|
||||||
|
const tr = jxx + jyy;
|
||||||
|
const det = jxx * jyy - jxy * jxy;
|
||||||
|
const diffEig = Math.sqrt(Math.max(0, tr * tr - 4 * det));
|
||||||
|
const l1 = (tr + diffEig) * 0.5;
|
||||||
|
const l2 = (tr - diffEig) * 0.5;
|
||||||
|
if (l1 + l2 > 0) {
|
||||||
|
coherence = (l1 - l2) / (l1 + l2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isHorizontalDominant = coherence > 0.25 && sumGyy > 1.5 * sumGxx;
|
||||||
|
const isVerticalDominant = coherence > 0.25 && sumGxx > 1.5 * sumGyy;
|
||||||
|
|
||||||
|
// 5. Directional high-pass synthesis
|
||||||
|
const hpR = new Float32Array(total);
|
||||||
|
const hpG = new Float32Array(total);
|
||||||
|
const hpB = new Float32Array(total);
|
||||||
|
|
||||||
|
if (isHorizontalDominant) {
|
||||||
|
// Water ripples or horizontal grain: bridge along rows
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (clean[idx]) continue;
|
||||||
|
|
||||||
|
let xl = x;
|
||||||
|
while (xl >= 0 && !clean[row + xl]) xl--;
|
||||||
|
let xr = x;
|
||||||
|
while (xr < w && !clean[row + xr]) xr++;
|
||||||
|
|
||||||
|
if (xl >= 0 && xr < w) {
|
||||||
|
const dl = x - xl;
|
||||||
|
const dr = xr - x;
|
||||||
|
const t = dl / (dl + dr);
|
||||||
|
const st = t * t * (3 - 2 * t);
|
||||||
|
|
||||||
|
const idxL = row + xl;
|
||||||
|
const idxR = row + xr;
|
||||||
|
const dpL = idxL << 2;
|
||||||
|
const dpR = idxR << 2;
|
||||||
|
|
||||||
|
const texRL = rgba[dpL] - meanR[idxL];
|
||||||
|
const texGL = rgba[dpL + 1] - meanG[idxL];
|
||||||
|
const texBL = rgba[dpL + 2] - meanB[idxL];
|
||||||
|
|
||||||
|
const texRR = rgba[dpR] - meanR[idxR];
|
||||||
|
const texGR = rgba[dpR + 1] - meanG[idxR];
|
||||||
|
const texBR = rgba[dpR + 2] - meanB[idxR];
|
||||||
|
|
||||||
|
hpR[idx] = (1 - st) * texRL + st * texRR;
|
||||||
|
hpG[idx] = (1 - st) * texGL + st * texGR;
|
||||||
|
hpB[idx] = (1 - st) * texBL + st * texBR;
|
||||||
|
} else if (xl >= 0) {
|
||||||
|
const idxL = row + xl;
|
||||||
|
const dpL = idxL << 2;
|
||||||
|
hpR[idx] = rgba[dpL] - meanR[idxL];
|
||||||
|
hpG[idx] = rgba[dpL + 1] - meanG[idxL];
|
||||||
|
hpB[idx] = rgba[dpL + 2] - meanB[idxL];
|
||||||
|
} else if (xr < w) {
|
||||||
|
const idxR = row + xr;
|
||||||
|
const dpR = idxR << 2;
|
||||||
|
hpR[idx] = rgba[dpR] - meanR[idxR];
|
||||||
|
hpG[idx] = rgba[dpR + 1] - meanG[idxR];
|
||||||
|
hpB[idx] = rgba[dpR + 2] - meanB[idxR];
|
||||||
|
} else {
|
||||||
|
const donor = ny[idx] * w + nx[idx];
|
||||||
|
const dp = donor << 2;
|
||||||
|
hpR[idx] = rgba[dp] - meanR[donor];
|
||||||
|
hpG[idx] = rgba[dp + 1] - meanG[donor];
|
||||||
|
hpB[idx] = rgba[dp + 2] - meanB[donor];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isVerticalDominant) {
|
||||||
|
// Vertical lines or columns: bridge along columns
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (clean[idx]) continue;
|
||||||
|
|
||||||
|
let yt = y;
|
||||||
|
while (yt >= 0 && !clean[yt * w + x]) yt--;
|
||||||
|
let yb = y;
|
||||||
|
while (yb < h && !clean[yb * w + x]) yb++;
|
||||||
|
|
||||||
|
if (yt >= 0 && yb < h) {
|
||||||
|
const dt = y - yt;
|
||||||
|
const db = yb - y;
|
||||||
|
const t = dt / (dt + db);
|
||||||
|
const st = t * t * (3 - 2 * t);
|
||||||
|
|
||||||
|
const idxT = yt * w + x;
|
||||||
|
const idxB = yb * w + x;
|
||||||
|
const dpT = idxT << 2;
|
||||||
|
const dpB = idxB << 2;
|
||||||
|
|
||||||
|
const texRT = rgba[dpT] - meanR[idxT];
|
||||||
|
const texGT = rgba[dpT + 1] - meanG[idxT];
|
||||||
|
const texBT = rgba[dpT + 2] - meanB[idxT];
|
||||||
|
|
||||||
|
const texRB = rgba[dpB] - meanR[idxB];
|
||||||
|
const texGB = rgba[dpB + 1] - meanG[idxB];
|
||||||
|
const texBB = rgba[dpB + 2] - meanB[idxB];
|
||||||
|
|
||||||
|
hpR[idx] = (1 - st) * texRT + st * texRB;
|
||||||
|
hpG[idx] = (1 - st) * texGT + st * texGB;
|
||||||
|
hpB[idx] = (1 - st) * texBT + st * texBB;
|
||||||
|
} else if (yt >= 0) {
|
||||||
|
const idxT = yt * w + x;
|
||||||
|
const dpT = idxT << 2;
|
||||||
|
hpR[idx] = rgba[dpT] - meanR[idxT];
|
||||||
|
hpG[idx] = rgba[dpT + 1] - meanG[idxT];
|
||||||
|
hpB[idx] = rgba[dpT + 2] - meanB[idxT];
|
||||||
|
} else if (yb < h) {
|
||||||
|
const idxB = yb * w + x;
|
||||||
|
const dpB = idxB << 2;
|
||||||
|
hpR[idx] = rgba[dpB] - meanR[idxB];
|
||||||
|
hpG[idx] = rgba[dpB + 1] - meanG[idxB];
|
||||||
|
hpB[idx] = rgba[dpB + 2] - meanB[idxB];
|
||||||
|
} else {
|
||||||
|
const donor = ny[idx] * w + nx[idx];
|
||||||
|
const dp = donor << 2;
|
||||||
|
hpR[idx] = rgba[dp] - meanR[donor];
|
||||||
|
hpG[idx] = rgba[dp + 1] - meanG[donor];
|
||||||
|
hpB[idx] = rgba[dp + 2] - meanB[donor];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Isotropic texture (skin, sky, plaster): sample from closest boundary
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (clean[idx]) continue;
|
||||||
|
const donor = ny[idx] * w + nx[idx];
|
||||||
|
const dp = donor << 2;
|
||||||
|
hpR[idx] = rgba[dp] - meanR[donor];
|
||||||
|
hpG[idx] = rgba[dp + 1] - meanG[donor];
|
||||||
|
hpB[idx] = rgba[dp + 2] - meanB[donor];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 2-pass light diffusion to eliminate Voronoi facets
|
||||||
|
for (let it = 0; it < 2; it++) {
|
||||||
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
|
const row = y * w;
|
||||||
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
|
const idx = row + x;
|
||||||
|
if (clean[idx]) continue;
|
||||||
|
hpR[idx] = 0.25 * (hpR[idx - w] + hpR[idx + w] + hpR[idx - 1] + hpR[idx + 1]);
|
||||||
|
hpG[idx] = 0.25 * (hpG[idx - w] + hpG[idx + w] + hpG[idx - 1] + hpG[idx + 1]);
|
||||||
|
hpB[idx] = 0.25 * (hpB[idx - w] + hpB[idx + w] + hpB[idx - 1] + hpB[idx + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6. Camera sensor grain
|
||||||
|
const grainStd = bandCount > 0 ? Math.min(1.2, Math.sqrt(hpVarSum / (bandCount * 3)) * 0.4) : 0.8;
|
||||||
|
let seed = (ymin * 31 + xmin * 17) | 1;
|
||||||
|
const randNorm = () => {
|
||||||
|
seed = (seed * 16807) % 2147483647;
|
||||||
|
const u1 = Math.max(1e-7, seed / 2147483647);
|
||||||
|
seed = (seed * 16807) % 2147483647;
|
||||||
|
const u2 = seed / 2147483647;
|
||||||
|
return Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 7. Final assembly
|
||||||
const out = new Uint8Array(4 * total);
|
const out = new Uint8Array(4 * total);
|
||||||
out.set(rgba);
|
out.set(rgba);
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
for (let y = ymin; y <= ymax; y++) {
|
||||||
if (!clean[i]) {
|
const row = y * w;
|
||||||
const donor = ny[i] * w + nx[i];
|
for (let x = xmin; x <= xmax; x++) {
|
||||||
const dp = donor << 2;
|
const idx = row + x;
|
||||||
|
if (clean[idx]) continue;
|
||||||
|
|
||||||
const texR = rgba[dp] - meanR[donor];
|
const p = idx << 2;
|
||||||
const texG = rgba[dp + 1] - meanG[donor];
|
const noise = randNorm() * grainStd;
|
||||||
const texB = rgba[dp + 2] - meanB[donor];
|
const r = Math.round(baseR[idx] + hpR[idx] * 0.85 + noise);
|
||||||
|
const g = Math.round(baseG[idx] + hpG[idx] * 0.85 + noise);
|
||||||
const p = i << 2;
|
const b = Math.round(baseB[idx] + hpB[idx] * 0.85 + noise);
|
||||||
const r = Math.round(baseR[i] + texR);
|
|
||||||
const g = Math.round(baseG[i] + texG);
|
|
||||||
const b = Math.round(baseB[i] + texB);
|
|
||||||
|
|
||||||
out[p] = r < 0 ? 0 : r > 255 ? 255 : r;
|
out[p] = r < 0 ? 0 : r > 255 ? 255 : r;
|
||||||
out[p + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
|
out[p + 1] = g < 0 ? 0 : g > 255 ? 255 : g;
|
||||||
@@ -631,13 +864,6 @@ export async function inpaintCrop(
|
|||||||
height: number,
|
height: number,
|
||||||
hole: Uint8Array
|
hole: Uint8Array
|
||||||
): Promise<Uint8Array | null> {
|
): Promise<Uint8Array | null> {
|
||||||
const maxSide = isMobile() ? 256 : INPAINT_MAX_SIDE;
|
|
||||||
let w = Math.max(8, Math.round(width));
|
|
||||||
let h = Math.max(8, Math.round(height));
|
|
||||||
const scale = Math.min(1, maxSide / Math.max(w, h));
|
|
||||||
w = Math.max(8, Math.floor((w * scale) / 8) * 8);
|
|
||||||
h = Math.max(8, Math.floor((h * scale) / 8) * 8);
|
|
||||||
|
|
||||||
const patchKey = (hole as { patchKey?: string }).patchKey;
|
const patchKey = (hole as { patchKey?: string }).patchKey;
|
||||||
let key: string;
|
let key: string;
|
||||||
if (patchKey) {
|
if (patchKey) {
|
||||||
@@ -652,97 +878,27 @@ export async function inpaintCrop(
|
|||||||
holeHash = (holeHash * 31 + i) | 0;
|
holeHash = (holeHash * 31 + i) | 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
key = `${w}x${h}_${holeCount}_${holeHash}`;
|
key = `${width}x${height}_${holeCount}_${holeHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (patchCache.has(key)) {
|
if (patchCache.has(key)) {
|
||||||
return patchCache.get(key)!.slice();
|
return patchCache.get(key)!.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
// On mobile devices (iOS Safari/Chrome WebKit), running the 28MB ONNX neural model
|
const patch = fastLocalInpaint(rgba, width, height, hole);
|
||||||
// in WebAssembly exceeds the tab's strict Jetsam memory quota and triggers process termination (tab reload).
|
if (patchCache.size > 50) {
|
||||||
// Use fastLocalInpaint directly: pure TypeScript, <2MB RAM, ~10ms execution, 100% crash-free.
|
const first = patchCache.keys().next().value;
|
||||||
if (isMobile()) {
|
if (first) patchCache.delete(first);
|
||||||
const patch = fastLocalInpaint(rgba, width, height, hole);
|
|
||||||
if (patchCache.size > 50) {
|
|
||||||
const first = patchCache.keys().next().value;
|
|
||||||
if (first) patchCache.delete(first);
|
|
||||||
}
|
|
||||||
patchCache.set(key, patch.slice());
|
|
||||||
return patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single-flight lock: serialize inference executions to prevent memory spikes
|
|
||||||
if (inFlightInpaint) {
|
|
||||||
try {
|
|
||||||
await inFlightInpaint;
|
|
||||||
} catch {}
|
|
||||||
if (patchCache.has(key)) {
|
|
||||||
return patchCache.get(key)!.slice();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const runPromise = (async (): Promise<Uint8Array | null> => {
|
|
||||||
const { src, hole: mask } = cropPlate(rgba, width, height, hole, w, h);
|
|
||||||
|
|
||||||
let in0: Ort.Tensor | null = null;
|
|
||||||
let in1: Ort.Tensor | null = null;
|
|
||||||
let out: Record<string, Ort.Tensor> | null = null;
|
|
||||||
try {
|
|
||||||
const { ort, session } = await load();
|
|
||||||
in0 = new ort.Tensor('uint8', src, [1, 3, h, w]);
|
|
||||||
in1 = new ort.Tensor('uint8', mask, [1, 1, h, w]);
|
|
||||||
out = await session.run({
|
|
||||||
[session.inputNames[0]]: in0,
|
|
||||||
[session.inputNames[1]]: in1,
|
|
||||||
});
|
|
||||||
const tensor = out[session.outputNames[0]];
|
|
||||||
const oplane = w * h;
|
|
||||||
const data = tensor.data as Uint8Array;
|
|
||||||
const patch = new Uint8Array(4 * w * h);
|
|
||||||
for (let i = 0; i < oplane; i++) {
|
|
||||||
patch[i * 4] = data[i];
|
|
||||||
patch[i * 4 + 1] = data[oplane + i];
|
|
||||||
patch[i * 4 + 2] = data[2 * oplane + i];
|
|
||||||
patch[i * 4 + 3] = 255;
|
|
||||||
}
|
|
||||||
// Re-apply coherent photographic micro-texture & grain onto neural output to eliminate all blur
|
|
||||||
const sharpPatch = applyCoherentTexture(patch, rgba, w, h, hole);
|
|
||||||
if (patchCache.size > 50) {
|
|
||||||
const first = patchCache.keys().next().value;
|
|
||||||
if (first) patchCache.delete(first);
|
|
||||||
}
|
|
||||||
patchCache.set(key, sharpPatch.slice());
|
|
||||||
return sharpPatch;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[inpaint] neural inference error, fallback to local inpaint:', err);
|
|
||||||
return fastLocalInpaint(rgba, width, height, hole);
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
in0?.dispose?.();
|
|
||||||
in1?.dispose?.();
|
|
||||||
if (out) {
|
|
||||||
for (const k in out) out[k]?.dispose?.();
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
inFlightInpaint = runPromise;
|
|
||||||
try {
|
|
||||||
return await runPromise;
|
|
||||||
} finally {
|
|
||||||
if (inFlightInpaint === runPromise) {
|
|
||||||
inFlightInpaint = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
patchCache.set(key, patch.slice());
|
||||||
|
return patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function inpaintCropSize(width: number, height: number): { w: number; h: number } {
|
export function inpaintCropSize(width: number, height: number): { w: number; h: number } {
|
||||||
const maxSide = isMobile() ? 256 : INPAINT_MAX_SIDE;
|
const maxSide = INPAINT_MAX_SIDE;
|
||||||
const scale = Math.min(1, maxSide / Math.max(1, Math.max(width, height)));
|
const scale = Math.min(1, maxSide / Math.max(1, Math.max(width, height)));
|
||||||
return {
|
return {
|
||||||
w: Math.max(8, Math.floor((Math.max(1, Math.round(width)) * scale) / 8) * 8),
|
w: Math.max(1, Math.round(width * scale)),
|
||||||
h: Math.max(8, Math.floor((Math.max(1, Math.round(height)) * scale) / 8) * 8),
|
h: Math.max(1, Math.round(height * scale)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user