Take the tone from the hole and the texture from beside it

A clone gets the texture right and the level wrong. Dust on a gradient sky
is copied from a patch a little lighter than the hole it fills, and the
repair reads as a disc even though every grain in it is correct — which is
why FR-DEV-8 asks for heal and not only for clone.

Heal adds the membrane: the difference between the two neighbourhoods,
sampled at twenty-four points around the rim and interpolated across the
disc by inverse square distance. Solving the Poisson problem properly is
tens of Jacobi iterations, and an iteration here is a dispatch — sixty
dispatches to remove a dust spot is not a frame budget. The closed form
costs one loop over the rim, no state, and no second pass.

The spec called for mean-value weights; inverse squares are two
transcendentals per sample cheaper and agree wherever the boundary
difference varies smoothly, which is every repair anyone makes. What
decides whether that trade holds is the measurement, so the measurement is
the test: on a ramp steep enough to leave a clone wrong by 38 levels out
of 255, the heal is wrong by 0. docs/spot-removal.md §6.1 records what
shipped and what it would take to go back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 20:22:10 +02:00
co-authored by Claude Opus 5
parent 5323608051
commit f00b3ae924
4 changed files with 233 additions and 32 deletions
+144
View File
@@ -243,3 +243,147 @@ fn placing_repairs_compiles_one_pipeline() {
"six repairs, one compiled pipeline"
);
}
// ---------------------------------------------------------------------------
// Heal (FR-DEV-8)
// ---------------------------------------------------------------------------
/// A frame whose brightness ramps across it, with one black mark on it.
///
/// This is the case that separates the two modes. A clone copies a patch from
/// somewhere else on the ramp, so it arrives at the wrong level and leaves a
/// disc of the right texture and the wrong tone; a heal carries the difference
/// across from the boundary and leaves nothing.
fn ramped_frame(ctx: &GpuContext) -> DemosaicedImage {
let data: Vec<u8> = (0..SIZE * SIZE)
.flat_map(|i| {
let (x, y) = ((i % SIZE) as f32, (i / SIZE) as f32);
let (cx, cy) = (MARK.0 * SIZE as f32, MARK.1 * SIZE as f32);
let r = MARK_RADIUS * SIZE as f32;
// 64 at the left edge to 192 at the right: a ramp steep enough that
// a clone from a third of a frame away is unmistakably wrong, and
// shallow enough to stay well inside the range.
let ramp = 64.0 + 128.0 * x / SIZE as f32;
let v = if (x - cx).hypot(y - cy) <= r {
0u8
} else {
ramp as u8
};
[v, v, v, 255]
})
.collect();
DemosaicedImage::from_rgba8(ctx, &data, SIZE, SIZE).expect("upload")
}
/// What the ramp says at a column, as the byte the renderer should produce.
fn ramp_at(x: u32) -> u8 {
(64.0 + 128.0 * x as f32 / SIZE as f32) as u8
}
/// The worst a repair is wrong by, over the disc it covers.
///
/// Measured against the ramp the photograph would have had if the mark had
/// never been there, which is the only definition of "repaired" worth
/// asserting: a repair that removes the mark and leaves the wrong tone has not
/// repaired anything, it has drawn a different mark.
fn worst_error(pixels: &[u8]) -> u8 {
let (cx, cy) = (MARK.0 * SIZE as f32, MARK.1 * SIZE as f32);
let r = MARK_RADIUS * SIZE as f32;
let mut worst = 0u8;
for y in 0..SIZE {
for x in 0..SIZE {
if (x as f32 - cx).hypot(y as f32 - cy) > r {
continue;
}
let got = pixels[((y * SIZE + x) * 4) as usize];
worst = worst.max(got.abs_diff(ramp_at(x)));
}
}
worst
}
/// The measurement the mode exists for, and the one that decides
/// `RIM_SAMPLES`: on a gradient, a heal is right and a clone is not.
#[test]
fn a_heal_takes_the_tone_from_the_hole_it_fills() {
let Some(ctx) = ctx() else { return };
let source = ramped_frame(&ctx);
let mut pass = AdjustPass::new(&ctx);
let mut cloned = EditGraph::default_chain();
cloned.spots_mut().place(repair(SpotMode::Clone));
let clone_error = worst_error(&render(&mut pass, &cloned, &source, SIZE));
let mut healed = EditGraph::default_chain();
healed.spots_mut().place(repair(SpotMode::Heal));
let heal_error = worst_error(&render(&mut pass, &healed, &source, SIZE));
// The clone is wrong by roughly the ramp across the source offset — about
// 38 levels here — and it is wrong across the whole disc.
assert!(
clone_error > 20,
"the clone was supposed to be visibly wrong, and is off by {clone_error}"
);
// Measured at 0 on the reference device — the ramp is linear, so the
// boundary difference is constant all the way round and the membrane
// reproduces it exactly. The tolerance is for the rounding a different
// driver may do, not for the method being approximate here.
assert!(
heal_error <= 1,
"the heal is off by {heal_error} levels; the clone it has to beat is off by {clone_error}"
);
}
/// And the repair still has to remove the mark: a membrane that matched the
/// boundary while leaving the black disc underneath would pass the measurement
/// above by averaging its way past it.
#[test]
fn a_heal_removes_the_mark_as_well_as_matching_the_tone() {
let Some(ctx) = ctx() else { return };
let source = ramped_frame(&ctx);
let mut pass = AdjustPass::new(&ctx);
let mut graph = EditGraph::default_chain();
graph.spots_mut().place(repair(SpotMode::Heal));
let pixels = render(&mut pass, &graph, &source, SIZE);
let mark = (MARK.0 * SIZE as f32) as u32;
for x in mark - 2..=mark + 2 {
let got = pixels[(((MARK.1 * SIZE as f32) as u32 * SIZE + x) * 4) as usize];
assert!(
got.abs_diff(ramp_at(x)) <= 3,
"column {x} reads {got}, the ramp says {}",
ramp_at(x)
);
}
}
/// A heal on a flat field is a clone: the boundary difference is zero all the
/// way round, so the membrane is zero and neither mode has anything to add.
/// Worth pinning, because a membrane that quietly tinted a flat repair would
/// be invisible in the gradient test above.
#[test]
fn a_heal_on_a_flat_field_changes_nothing_a_clone_would_not() {
let Some(ctx) = ctx() else { return };
let source = marked_frame(&ctx);
let mut pass = AdjustPass::new(&ctx);
let mut cloned = EditGraph::default_chain();
cloned.spots_mut().place(repair(SpotMode::Clone));
let a = render(&mut pass, &cloned, &source, SIZE);
let mut healed = EditGraph::default_chain();
healed.spots_mut().place(repair(SpotMode::Heal));
let b = render(&mut pass, &healed, &source, SIZE);
let worst = a
.iter()
.zip(&b)
.map(|(x, y)| x.abs_diff(*y))
.max()
.unwrap_or(0);
assert!(
worst <= 1,
"heal and clone differ by {worst} on a flat field"
);
}