fix(heal): feed crisp photo context to inpaint model and cache patch across zoom levels
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user