fix(heal): feed crisp photo context to inpaint model and cache patch across zoom levels

This commit is contained in:
2026-10-16 07:58:38 +07:00
parent 21659f0d8a
commit 2dac868274
3 changed files with 32 additions and 38 deletions
+1
View File
@@ -1054,6 +1054,7 @@ export function Workspace() {
// compare copy is never a stale render of the last one.
useEffect(() => {
setComparing(false);
clearInpaintCache();
}, [source]);
// The geometry and the stamps a render carries, apart from its recipe: the
+11 -16
View File
@@ -1664,22 +1664,14 @@ function clusterHealSpots(
);
const cropSurface = own(createSurface(mw, mh));
if (cropSurface) {
// Progressive inpainting: read the crop from fillSurface, which already
// carries the patches of the clusters this pass solved before it, over a
// frame that carries every drag repaired before this one. When repairing
// adjacent objects (e.g. parallel wires), the second wire sees the first
// wire ALREADY cleanly healed, so it never samples the first wire's dirty
// texture or overwrites the first wire's repair.
flush(fillSurface);
const fillSnap = own(fillSurface.makeImageSnapshot());
if (fillSnap) {
cropSurface.getCanvas().drawImageRect(
fillSnap,
Skia.XYWHRect(srcLeft - boxLeft, srcTop - boxTop, srcW, srcH),
Skia.XYWHRect(0, 0, mw, mh),
own(Skia.Paint())
);
}
// Neural inpainting (inpaint-web): read the clean, unblurred original pixels
// from snap so the model is fed crisp scene context without Gaussian pre-blurring.
cropSurface.getCanvas().drawImageRect(
snap,
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
Skia.XYWHRect(0, 0, mw, mh),
own(Skia.Paint())
);
flush(cropSurface);
const cropPixels = cropSurface.getCanvas().readPixels(0, 0, {
width: mw,
@@ -1709,6 +1701,9 @@ function clusterHealSpots(
}
}
}
// Compute normalized patchKey so preview zoom in/out hits cache with 0ms overhead
const patchKey = `${mw}x${mh}_` + cluster.map((s) => `${s.x.toFixed(4)},${s.y.toFixed(4)},${s.r.toFixed(4)}`).join(';');
(hole as { patchKey?: string }).patchKey = patchKey;
const patch = await inpaintCrop(cropPixels, mw, mh, hole).catch(() => null);
if (patch) {
const patchSurface = own(createSurface(mw, mh));
+20 -22
View File
@@ -505,18 +505,13 @@ export function cropPlate(
const plane = w * h;
const src = new Uint8Array(3 * plane);
const mask = new Uint8Array(plane).fill(255);
const field = rimFill(small, w, h, smallHole, 0);
for (let d = 0; d < plane; d++) {
const s = d * 4;
src[d] = small[s];
src[plane + d] = small[s + 1];
src[2 * plane + d] = small[s + 2];
if (smallHole[d]) {
src[d] = byte(field.r[d]);
src[plane + d] = byte(field.g[d]);
src[2 * plane + d] = byte(field.b[d]);
mask[d] = 0;
} else {
src[d] = small[s];
src[plane + d] = small[s + 1];
src[2 * plane + d] = small[s + 2];
}
}
return { src, hole: mask };
@@ -571,22 +566,25 @@ export async function inpaintCrop(
w = Math.max(8, Math.floor((w * scale) / 8) * 8);
h = Math.max(8, Math.floor((h * scale) / 8) * 8);
// Quick signature to avoid redundant re-inferences on subsequent renders
let holeCount = 0;
let holeHash = 0;
const step = Math.max(1, Math.floor(hole.length / 64));
for (let i = 0; i < hole.length; i += step) {
if (hole[i]) {
holeCount++;
holeHash = (holeHash * 31 + i) | 0;
// Quick signature to avoid redundant re-inferences on subsequent renders.
// When patchKey is supplied via (hole as any).patchKey or parameter, it is based on
// normalized coordinates and is invariant to zoom in/out preview resamplings.
const patchKey = (hole as { patchKey?: string }).patchKey;
let key: string;
if (patchKey) {
key = patchKey;
} else {
let holeCount = 0;
let holeHash = 0;
const step = Math.max(1, Math.floor(hole.length / 64));
for (let i = 0; i < hole.length; i += step) {
if (hole[i]) {
holeCount++;
holeHash = (holeHash * 31 + i) | 0;
}
}
key = `${w}x${h}_${holeCount}_${holeHash}`;
}
let pixHash = 0;
const pixStep = Math.max(4, Math.floor(rgba.length / 64) * 4);
for (let i = 0; i < rgba.length; i += pixStep) {
pixHash = (pixHash * 33 + rgba[i] + rgba[i + 1]) | 0;
}
const key = `${w}x${h}_${holeCount}_${holeHash}_${pixHash}`;
if (patchCache.has(key)) {
return patchCache.get(key)!.slice();