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:
2026-10-15 00:55:01 +07:00
parent 678d3811fc
commit 8809eb84b0
2 changed files with 51 additions and 4 deletions
+36
View File
@@ -449,4 +449,40 @@ assert.ok(worstBand <= 8, `a painted pixel at x=${bandAt} is ${worstBand} levels
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');