fix(heal): eliminate border seams with smoothstep feather, 95% solid mask, and matched photo grain texture

This commit is contained in:
2026-10-14 16:45:15 +07:00
parent f6acf3ee6c
commit 8df0a03414
3 changed files with 41 additions and 32 deletions
+31 -14
View File
@@ -284,7 +284,7 @@ export function fastLocalInpaint(
}
// 3. Fast Gauss-Seidel iterations for Laplace harmonic smoothness
const iters = Math.min(24, Math.max(12, Math.round(Math.sqrt(holeCount) * 1.2)));
const iters = Math.min(48, Math.max(20, Math.round(Math.sqrt(holeCount) * 1.5)));
for (let iter = 0; iter < iters; iter++) {
for (let y = 0; y < h; y++) {
const row = y * w;
@@ -304,18 +304,32 @@ export function fastLocalInpaint(
}
}
// 4. Sample high-frequency texture noise from boundary to avoid plastic flatness
// 4. Sample high-frequency texture noise from surrounding photo to match grain and avoid plastic flatness
let noiseVariance = 0;
let count = 0;
for (let i = 0; i < bLen; i++) {
const bp = boundary[i];
const idx = bp.y * w + bp.x;
const diffR = bp.r - fR[idx];
const diffG = bp.g - fG[idx];
const diffB = bp.b - fB[idx];
noiseVariance += (diffR * diffR + diffG * diffG + diffB * diffB) / 3;
const p = (bp.y * w + bp.x) * 4;
if (bp.x + 1 < w && !hole[bp.y * w + bp.x + 1]) {
const p2 = (bp.y * w + bp.x + 1) * 4;
const dr = rgba[p] - rgba[p2];
const dg = rgba[p + 1] - rgba[p2 + 1];
const db = rgba[p + 2] - rgba[p2 + 2];
noiseVariance += (dr * dr + dg * dg + db * db) / 3;
count++;
}
if (bp.y + 1 < h && !hole[(bp.y + 1) * w + bp.x]) {
const p2 = ((bp.y + 1) * w + bp.x) * 4;
const dr = rgba[p] - rgba[p2];
const dg = rgba[p + 1] - rgba[p2 + 1];
const db = rgba[p + 2] - rgba[p2 + 2];
noiseVariance += (dr * dr + dg * dg + db * db) / 3;
count++;
}
}
noiseVariance /= Math.max(1, bLen);
const noiseAmp = Math.min(8, Math.sqrt(noiseVariance));
noiseVariance /= Math.max(1, count);
// High-frequency noise standard deviation (adjacent difference / sqrt(2))
const noiseAmp = Math.min(5, Math.max(0.6, Math.sqrt(noiseVariance * 0.5)));
let seed = (w * 31 + h * 17 + holeCount) | 0;
function prng(): number {
@@ -323,17 +337,20 @@ export function fastLocalInpaint(
return (seed / 0x7fffffff) * 2 - 1;
}
// 5. Output
// 5. Output: natural photographic grain matching the surrounding photo
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]) {
const p = idx * 4;
const grain = noiseAmp > 0.5 ? prng() * noiseAmp : 0;
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 + 2] = Math.max(0, Math.min(255, Math.round(fB[idx] + grain)));
const lum = prng() * noiseAmp;
const gr = lum + prng() * (noiseAmp * 0.2);
const gg = lum + prng() * (noiseAmp * 0.2);
const gb = lum + prng() * (noiseAmp * 0.2);
out[p] = Math.max(0, Math.min(255, Math.round(fR[idx] + gr)));
out[p + 1] = Math.max(0, Math.min(255, Math.round(fG[idx] + gg)));
out[p + 2] = Math.max(0, Math.min(255, Math.round(fB[idx] + gb)));
out[p + 3] = 255;
}
}