fix(heal): eliminate grey wash artifacts by restoring opaque patches and progressive fillSurface cropping
This commit is contained in:
@@ -1318,11 +1318,7 @@ function clusterHealSpots(
|
|||||||
const xj = sj.x * width;
|
const xj = sj.x * width;
|
||||||
const yj = sj.y * height;
|
const yj = sj.y * height;
|
||||||
const rj = sj.r * width;
|
const rj = sj.r * width;
|
||||||
// Connect spots that are close enough that their inpainting crops interact
|
const maxDist = (ri + rj) * 1.5;
|
||||||
// (e.g. parallel wires, nearby scratches or dust within margin reach).
|
|
||||||
// Clustering nearby objects together ensures they are inpainted in one unified
|
|
||||||
// pass where neither object is sampled as "clean background texture" for the other.
|
|
||||||
const maxDist = Math.max((ri + rj) * 2.5, ri + rj + 48);
|
|
||||||
const distSq = (xi - xj) ** 2 + (yi - yj) ** 2;
|
const distSq = (xi - xj) ** 2 + (yi - yj) ** 2;
|
||||||
if (distSq <= maxDist * maxDist) {
|
if (distSq <= maxDist * maxDist) {
|
||||||
adj[i].push(j);
|
adj[i].push(j);
|
||||||
@@ -1496,18 +1492,6 @@ function clusterHealSpots(
|
|||||||
// Strokes are clustered so that separate brush repairs (e.g. Brush 1 and Brush 2)
|
// Strokes are clustered so that separate brush repairs (e.g. Brush 1 and Brush 2)
|
||||||
// are solved independently at their own high resolution rather than lumped into
|
// are solved independently at their own high resolution rather than lumped into
|
||||||
// one downscaled bounding box that would degrade previous repairs.
|
// one downscaled bounding box that would degrade previous repairs.
|
||||||
// Progressive working surface for inpainting:
|
|
||||||
// When multiple strokes/clusters exist (such as parallel wires or adjacent spots),
|
|
||||||
// each cluster reads from the surface that already has previous clusters repaired,
|
|
||||||
// and writes ONLY to its own hole (alpha 0 outside the hole).
|
|
||||||
// This prevents earlier repaired wires from reappearing and prevents dirty neighboring
|
|
||||||
// textures from contaminating subsequent inpainting.
|
|
||||||
const workSurface = own(createSurface(width, height));
|
|
||||||
const workCanvas = workSurface.getCanvas();
|
|
||||||
if (snap) {
|
|
||||||
workCanvas.drawImage(snap, 0, 0, own(Skia.Paint()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const clusters = clusterHealSpots(heal, width, height);
|
const clusters = clusterHealSpots(heal, width, height);
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
const cWidest = Math.max(...cluster.map((s) => s.r * width));
|
const cWidest = Math.max(...cluster.map((s) => s.r * width));
|
||||||
@@ -1532,16 +1516,20 @@ function clusterHealSpots(
|
|||||||
const { w: mw, h: mh } = inpaintCropSize(srcW, srcH);
|
const { w: mw, h: mh } = inpaintCropSize(srcW, srcH);
|
||||||
const cropSurface = own(createSurface(mw, mh));
|
const cropSurface = own(createSurface(mw, mh));
|
||||||
if (cropSurface) {
|
if (cropSurface) {
|
||||||
flush(workSurface);
|
// Progressive inpainting: read the crop from fillSurface, which already
|
||||||
const workSnap = workSurface.makeImageSnapshot();
|
// carries any previous clusters' repaired patches. When repairing adjacent
|
||||||
if (workSnap) {
|
// 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(
|
cropSurface.getCanvas().drawImageRect(
|
||||||
workSnap,
|
fillSnap,
|
||||||
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
|
Skia.XYWHRect(srcLeft - boxLeft, srcTop - boxTop, srcW, srcH),
|
||||||
Skia.XYWHRect(0, 0, mw, mh),
|
Skia.XYWHRect(0, 0, mw, mh),
|
||||||
own(Skia.Paint())
|
own(Skia.Paint())
|
||||||
);
|
);
|
||||||
try { workSnap.dispose(); } catch {}
|
|
||||||
}
|
}
|
||||||
flush(cropSurface);
|
flush(cropSurface);
|
||||||
const cropPixels = cropSurface.getCanvas().readPixels(0, 0, {
|
const cropPixels = cropSurface.getCanvas().readPixels(0, 0, {
|
||||||
@@ -1555,7 +1543,6 @@ function clusterHealSpots(
|
|||||||
const kx = mw / srcW;
|
const kx = mw / srcW;
|
||||||
const ky = mh / srcH;
|
const ky = mh / srcH;
|
||||||
const hole = new Uint8Array(mw * mh);
|
const hole = new Uint8Array(mw * mh);
|
||||||
// Mark all spots from the current cluster
|
|
||||||
for (const s of cluster) {
|
for (const s of cluster) {
|
||||||
const cx = (s.x * width - srcLeft) * kx;
|
const cx = (s.x * width - srcLeft) * kx;
|
||||||
const cy = (s.y * height - srcTop) * ky;
|
const cy = (s.y * height - srcTop) * ky;
|
||||||
@@ -1573,35 +1560,8 @@ function clusterHealSpots(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Also mark any other heal spots falling into this crop box as holes,
|
|
||||||
// so they can never be sampled as "clean background texture"
|
|
||||||
for (const s of heal) {
|
|
||||||
const cx = (s.x * width - srcLeft) * kx;
|
|
||||||
const cy = (s.y * height - srcTop) * ky;
|
|
||||||
const rad = s.r * width * kx;
|
|
||||||
if (cx + rad < 0 || cx - rad >= mw || cy + rad < 0 || cy - rad >= mh) continue;
|
|
||||||
const x0 = Math.max(0, Math.floor(cx - rad));
|
|
||||||
const x1 = Math.min(mw - 1, Math.ceil(cx + rad));
|
|
||||||
const y0 = Math.max(0, Math.floor(cy - rad));
|
|
||||||
const y1 = Math.min(mh - 1, Math.ceil(cy + rad));
|
|
||||||
for (let y = y0; y <= y1; y++) {
|
|
||||||
const dy = y - cy;
|
|
||||||
const row = y * mw;
|
|
||||||
for (let x = x0; x <= x1; x++) {
|
|
||||||
const dx = x - cx;
|
|
||||||
if (dx * dx + dy * dy <= rad * rad) hole[row + x] = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const patch = await inpaintCrop(cropPixels, mw, mh, hole).catch(() => null);
|
const patch = await inpaintCrop(cropPixels, mw, mh, hole).catch(() => null);
|
||||||
if (patch) {
|
if (patch) {
|
||||||
// Ensure pixels outside hole are 100% transparent so drawing never
|
|
||||||
// clobbers neighboring clusters or clean background
|
|
||||||
for (let i = 0; i < mw * mh; i++) {
|
|
||||||
if (!hole[i]) {
|
|
||||||
patch[i * 4 + 3] = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const patchSurface = own(createSurface(mw, mh));
|
const patchSurface = own(createSurface(mw, mh));
|
||||||
if (patchSurface && patchSurface.getCanvas().writePixels(patch, mw, mh, 0, 0)) {
|
if (patchSurface && patchSurface.getCanvas().writePixels(patch, mw, mh, 0, 0)) {
|
||||||
flush(patchSurface);
|
flush(patchSurface);
|
||||||
@@ -1615,12 +1575,6 @@ function clusterHealSpots(
|
|||||||
Skia.XYWHRect(srcLeft - boxLeft, srcTop - boxTop, srcW, srcH),
|
Skia.XYWHRect(srcLeft - boxLeft, srcTop - boxTop, srcW, srcH),
|
||||||
patchPaint
|
patchPaint
|
||||||
);
|
);
|
||||||
workCanvas.drawImageRect(
|
|
||||||
patchImage,
|
|
||||||
Skia.XYWHRect(0, 0, mw, mh),
|
|
||||||
Skia.XYWHRect(srcLeft, srcTop, srcW, srcH),
|
|
||||||
patchPaint
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,8 +238,6 @@ export function fastLocalInpaint(
|
|||||||
out[p + 1] = cg;
|
out[p + 1] = cg;
|
||||||
out[p + 2] = cb;
|
out[p + 2] = cb;
|
||||||
out[p + 3] = 255;
|
out[p + 3] = 255;
|
||||||
} else {
|
|
||||||
out[i * 4 + 3] = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -325,20 +323,18 @@ export function fastLocalInpaint(
|
|||||||
return (seed / 0x7fffffff) * 2 - 1;
|
return (seed / 0x7fffffff) * 2 - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Output: only hole pixels are opaque; outside hole is transparent (alpha = 0)
|
// 5. Output
|
||||||
for (let y = 0; y < h; y++) {
|
for (let y = 0; y < h; y++) {
|
||||||
const row = y * w;
|
const row = y * w;
|
||||||
for (let x = 0; x < w; x++) {
|
for (let x = 0; x < w; x++) {
|
||||||
const idx = row + x;
|
const idx = row + x;
|
||||||
const p = idx * 4;
|
|
||||||
if (hole[idx]) {
|
if (hole[idx]) {
|
||||||
|
const p = idx * 4;
|
||||||
const grain = noiseAmp > 0.5 ? prng() * noiseAmp : 0;
|
const grain = noiseAmp > 0.5 ? prng() * noiseAmp : 0;
|
||||||
out[p] = Math.max(0, Math.min(255, Math.round(fR[idx] + grain)));
|
out[p] = Math.max(0, Math.min(255, Math.round(fR[idx] + grain)));
|
||||||
out[p + 1] = Math.max(0, Math.min(255, Math.round(fG[idx] + grain)));
|
out[p + 1] = Math.max(0, Math.min(255, Math.round(fG[idx] + grain)));
|
||||||
out[p + 2] = Math.max(0, Math.min(255, Math.round(fB[idx] + grain)));
|
out[p + 2] = Math.max(0, Math.min(255, Math.round(fB[idx] + grain)));
|
||||||
out[p + 3] = 255;
|
out[p + 3] = 255;
|
||||||
} else {
|
|
||||||
out[p + 3] = 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -475,14 +471,14 @@ export async function inpaintCrop(
|
|||||||
const tensor = out[session.outputNames[0]];
|
const tensor = out[session.outputNames[0]];
|
||||||
const oplane = w * h;
|
const oplane = w * h;
|
||||||
const data = tensor.data as Uint8Array;
|
const data = tensor.data as Uint8Array;
|
||||||
// Back to RGBA: only pixels inside the hole (mask[i] === 0) are opaque;
|
// Back to RGBA, opaque: the crop covers the frame, which has no transparency
|
||||||
// pixels outside the hole are transparent so drawing the patch never overwrites adjacent clusters.
|
// of its own to carry.
|
||||||
const patch = new Uint8Array(4 * w * h);
|
const patch = new Uint8Array(4 * w * h);
|
||||||
for (let i = 0; i < oplane; i++) {
|
for (let i = 0; i < oplane; i++) {
|
||||||
patch[i * 4] = data[i];
|
patch[i * 4] = data[i];
|
||||||
patch[i * 4 + 1] = data[oplane + i];
|
patch[i * 4 + 1] = data[oplane + i];
|
||||||
patch[i * 4 + 2] = data[2 * oplane + i];
|
patch[i * 4 + 2] = data[2 * oplane + i];
|
||||||
patch[i * 4 + 3] = mask[i] === 0 ? 255 : 0;
|
patch[i * 4 + 3] = 255;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
in0.dispose?.();
|
in0.dispose?.();
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1591f02043e723d73336f51f03f96ca086bed72ed41ef4176c9191af0356920a recipescam-images.tar.gz
|
21471af2e7d44d8f6d6de6e0a98b2659fc9aa4e7df2cc6e23983e86652a216df recipescam-images.tar.gz
|
||||||
|
|||||||
Reference in New Issue
Block a user