fix(heal): cut the whole brush out of the mask, so the repair takes its colour from the photo and not from the spot it is repairing
This commit is contained in:
@@ -449,4 +449,40 @@ assert.ok(worstBand <= 8, `a painted pixel at x=${bandAt} is ${worstBand} levels
|
|||||||
H = keptH;
|
H = keptH;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 10. The brush put ON the blemish, which is what it is for. Every case above
|
||||||
|
// paints its stroke well inside the brush, so the rim the fill reads is
|
||||||
|
// ground however the hole was cut — and a fill that takes its colour from
|
||||||
|
// the brush's OWN pixels is invisible there. Here the whole of what the
|
||||||
|
// brush covers is the blemish, a disc just smaller than the brush, and the
|
||||||
|
// repair has to come back the colour of the ground around it. This is the
|
||||||
|
// shape the cut's softness leaks in: with the cut written as the mask's fade
|
||||||
|
// — `a = 1 - mask.a` — the outer quarter of the spot stays in the frame at
|
||||||
|
// partial weight, the blemish's rim is exactly in that band, and it is
|
||||||
|
// weighed into the blur the fill is made of. The repair then lands in the
|
||||||
|
// colour of the thing it was repairing, 4 levels off a grey ground on a
|
||||||
|
// 226,34,40 blemish (measured; the cut variants in heal-colour-pass.mjs).
|
||||||
|
{
|
||||||
|
const BLEMISH_R = 0.045 * W; // 7.2px under this 8px brush
|
||||||
|
const shadeBlemish = (x, y) =>
|
||||||
|
Math.hypot(x - cx, y - cy) < BLEMISH_R ? [226, 34, 40] : [GREY, GREY, GREY];
|
||||||
|
const onBlemish = healPass(bytesOf(shadeBlemish), [{ x: cx / W, y: cy / H, r: R }]);
|
||||||
|
let worstOn = 0;
|
||||||
|
let onCount = 0;
|
||||||
|
for (let y = 0; y < H; y++) {
|
||||||
|
for (let x = 0; x < W; x++) {
|
||||||
|
if (onBlemish.mask[(y * W + x) * 4 + 3] < 250) continue;
|
||||||
|
if (Math.hypot(x + 0.5 - cx, y + 0.5 - cy) > 0.5 * R * W * 0.75) continue;
|
||||||
|
onCount++;
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
worstOn = Math.max(worstOn, Math.abs(onBlemish.pixels[(y * W + x) * 4 + c] - GREY));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.ok(onCount > 20, 'the brush painted too little to judge');
|
||||||
|
assert.ok(
|
||||||
|
worstOn <= 3,
|
||||||
|
`a brush aimed at a blemish came back ${worstOn} levels off the ground it sits in — the fill is reading the stroke's own pixels, so the cut is the mask's fade and not the mask`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
console.log('ok the stroke is one mask, the fill carries the light, and nothing outside it moved');
|
console.log('ok the stroke is one mask, the fill carries the light, and nothing outside it moved');
|
||||||
|
|||||||
@@ -67,19 +67,30 @@ function num(v: unknown, fallback: number): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The first of the fill's two passes: the photo with the painted pixels taken
|
// The first of the fill's two passes: the photo with the painted pixels taken
|
||||||
// out of it. The colour of every pixel is kept and its alpha drops to what the
|
// out of it. The colour of every pixel is kept and its alpha drops to nothing
|
||||||
// brush did not touch, which is what turns the mask into a hole the second pass
|
// over the brush, which is what turns the mask into a hole the second pass can
|
||||||
// can see the rim of. Written as premultiplied — rgb times that alpha — because
|
// see the rim of. Written as premultiplied — rgb times that alpha — because
|
||||||
// that is what every surface here holds, and the blur below is built on it: a
|
// that is what every surface here holds, and the blur below is built on it: a
|
||||||
// blur of premultiplied pixels is a weighted average of the pixels that are
|
// blur of premultiplied pixels is a weighted average of the pixels that are
|
||||||
// still there, with the hole weighing nothing.
|
// still there, with the hole weighing nothing.
|
||||||
|
//
|
||||||
|
// The cut is the WHOLE stroke, and it is a step rather than the mask's own fade.
|
||||||
|
// The fill below reads the rim of this hole, so whatever is left standing inside
|
||||||
|
// it is weighed into the repair. Cut at the mask's fade instead — `a = 1 -
|
||||||
|
// mask.a`, the obvious reading of "take the mask out" — and the outer quarter of
|
||||||
|
// every spot stays behind at partial weight. A brush aimed at a blemish has the
|
||||||
|
// blemish's own rim in exactly that band, so the repair came back in the colour
|
||||||
|
// of the thing being repaired: on a grey ground with a red blemish, the obvious
|
||||||
|
// cut is 4.1 levels off the ground and the step is 1.1 (measured, the cut
|
||||||
|
// variants in heal-colour-pass.mjs). The threshold is lightcraft's own: the
|
||||||
|
// pixels a heal may read its tone from are the ones at alpha < 0.02 (spots.rs).
|
||||||
export const HEAL_CUT_SKSL = `
|
export const HEAL_CUT_SKSL = `
|
||||||
uniform shader img;
|
uniform shader img;
|
||||||
uniform shader mask;
|
uniform shader mask;
|
||||||
|
|
||||||
half4 main(float2 pos) {
|
half4 main(float2 pos) {
|
||||||
half4 c = img.eval(pos);
|
half4 c = img.eval(pos);
|
||||||
half a = 1.0 - mask.eval(pos).a;
|
half a = mask.eval(pos).a < 0.02 ? 1.0 : 0.0;
|
||||||
half3 straight = c.a > 0.0001 ? c.rgb / c.a : c.rgb;
|
half3 straight = c.a > 0.0001 ? c.rgb / c.a : c.rgb;
|
||||||
return half4(straight * a, a);
|
return half4(straight * a, a);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user