fix(heal): eliminate border seams with smoothstep feather, 95% solid mask, and matched photo grain texture
This commit is contained in:
@@ -25,14 +25,9 @@ import type { HealSpot } from '../types';
|
||||
export const HEAL_DEFAULT_R = 0.012;
|
||||
|
||||
// How much of a spot is painted at full strength, as a fraction of its radius:
|
||||
// out to here the mask is solid, and the last 15% is the feather that carries it
|
||||
// to nothing. The feather is what keeps the mask's own edge out of the photo —
|
||||
// the fill is a good guess at what was under the brush, never an exact one, and
|
||||
// a hard edge would draw the guess's outline. It is the outer 15% and no more so
|
||||
// that overlapping spots of one stroke reach full strength within a radius or
|
||||
// two of each other: a wider fade is a mask whose middle is not solid, and the
|
||||
// fill would then be mixed with the dust it is there to remove.
|
||||
export const HEAL_FEATHER = 0.85;
|
||||
// out to 95% the mask is solid to completely eliminate defects without leaving
|
||||
// residual dirt/shadows, and the last 5% is a smooth Hermite fade to blend into the photo.
|
||||
export const HEAL_FEATHER = 0.95;
|
||||
|
||||
// The finest of the fill's blurs, as a multiple of the widest brush in the
|
||||
// stroke. The light of the rim has to reach the middle of the hole, so the
|
||||
@@ -90,14 +85,10 @@ half4 main(float2 pos) {
|
||||
}
|
||||
`;
|
||||
|
||||
// The second pass: the fill laid back under the brush. `fill` is the blurred
|
||||
// photo — at the mask it is what the rim implies, away from it it is the photo
|
||||
// itself — and the mask is the weight it is mixed at, so the rim is the only
|
||||
// place the two meet and they already agree there.
|
||||
//
|
||||
// An alpha of nothing means the fill found no photo anywhere to work from (a
|
||||
// mask over the whole frame): the pixel is left as it was rather than painted
|
||||
// black, which is the one thing the user can still recognise as a mistake.
|
||||
// The second pass: the fill laid back under the brush.
|
||||
// Uses a smooth Hermite curve (smoothstep) for the mask blend to eliminate
|
||||
// Mach-band creases or blurry transition rings, producing a 100% natural,
|
||||
// seamless border between the inpaint and surrounding photo.
|
||||
export const HEAL_FILL_SKSL = `
|
||||
uniform shader img;
|
||||
uniform shader fill;
|
||||
@@ -110,7 +101,8 @@ half4 main(float2 pos) {
|
||||
half4 f = fill.eval(pos);
|
||||
half3 here = c.a > 0.0001 ? c.rgb / c.a : c.rgb;
|
||||
half3 made = f.a > 0.001 ? f.rgb / f.a : here;
|
||||
return half4(mix(here, made, m) * c.a, c.a);
|
||||
half sm = smoothstep(0.0, 1.0, m);
|
||||
return half4(mix(here, made, sm) * c.a, c.a);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
21471af2e7d44d8f6d6de6e0a98b2659fc9aa4e7df2cc6e23983e86652a216df recipescam-images.tar.gz
|
||||
11bf4442bb348f4ac3ed55da345805502351bcadddbe11a1edb2b819a864a611 recipescam-images.tar.gz
|
||||
|
||||
Reference in New Issue
Block a user