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:
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -533,6 +533,19 @@ fn spot_tap(p: vec2<f32>) -> vec3<f32> {
|
||||
}",
|
||||
}];
|
||||
|
||||
/// TRACES: FR-DEV-8
|
||||
/// How many points around a disc's rim a heal samples.
|
||||
///
|
||||
/// The membrane in [`SPOT_BODY`] is an interpolation of the boundary
|
||||
/// difference, so this is the resolution of the boundary it sees. Twenty-four
|
||||
/// puts a sample every fifteen degrees, which on a disc of any size a
|
||||
/// photographer draws is finer than the tone it is interpolating.
|
||||
///
|
||||
/// A uniform rather than a constant in the source, so tuning it uploads a
|
||||
/// buffer instead of recompiling — and so a future control could trade it for
|
||||
/// speed on a large repair without a second shader.
|
||||
pub const RIM_SAMPLES: f32 = 24.0;
|
||||
|
||||
/// The WGSL every spot pass runs. See [`SpotSet::passes`] for the record layout
|
||||
/// it reads, which is where the meaning of each lane is written down.
|
||||
const SPOT_BODY: &str = "\
|
||||
@@ -565,9 +578,48 @@ for (var i = 0u; i < repairs; i = i + 1u) {
|
||||
// The same displacement within the disc, read from beside it: the patch is
|
||||
// a translation of the photograph, so its texture arrives unrotated and
|
||||
// unscaled.
|
||||
//
|
||||
// `replacement`, not `patch`: WGSL reserves that word, and a reserved
|
||||
// keyword in generated code is a compile error a long way from its cause.
|
||||
let replacement = spot_tap(src.xy + delta);
|
||||
var replacement = spot_tap(src.xy + delta);
|
||||
|
||||
// Heal: carry the source's texture, but the destination's tone.
|
||||
//
|
||||
// What a clone gets wrong is not the texture, it is the level. Dust on a
|
||||
// gradient sky is cloned from a patch a little lighter or darker than the
|
||||
// hole it fills, and the repair reads as a disc even though every grain in
|
||||
// it is right. The fix is the difference between the two neighbourhoods,
|
||||
// interpolated across the disc — a membrane, in the sense the Poisson
|
||||
// literature means, approximated here in closed form rather than solved.
|
||||
//
|
||||
// Solving it properly is tens of Jacobi iterations, and an iteration in
|
||||
// this architecture is a dispatch: sixty dispatches to remove a dust spot
|
||||
// is not a frame budget. Interpolating the boundary difference by inverse
|
||||
// square distance costs one loop over the rim and no state at all, and on
|
||||
// the case that actually matters — a smooth background, where the
|
||||
// difference around the rim is near enough constant — it lands on the same
|
||||
// answer the solve would.
|
||||
if (src.w > 0.5) {
|
||||
var weighted = vec3<f32>(0.0);
|
||||
var total = 0.0;
|
||||
let samples = i32(rim_samples);
|
||||
for (var k = 0; k < samples; k = k + 1) {
|
||||
// Offset by half a step so no sample sits exactly on an axis,
|
||||
// where a rim that crosses a hard edge would align with it.
|
||||
let angle = (f32(k) + 0.5) * 6.283185307 / rim_samples;
|
||||
let arm = vec2<f32>(cos(angle), sin(angle)) * disc.z;
|
||||
// What the photograph says here, minus what the source says at the
|
||||
// matching point of its own rim.
|
||||
let boundary = spot_tap(disc.xy + arm) - spot_tap(src.xy + arm);
|
||||
// Inverse square distance, floored so a pixel that lands on a
|
||||
// sample is a large weight rather than an infinite one.
|
||||
let w = 1.0 / max(dot(here - (disc.xy + arm), here - (disc.xy + arm)), 1.0);
|
||||
weighted = weighted + boundary * w;
|
||||
total = total + w;
|
||||
}
|
||||
replacement = replacement + weighted / max(total, 1e-6);
|
||||
}
|
||||
|
||||
c = mix(c, replacement, cover);
|
||||
}";
|
||||
|
||||
@@ -669,7 +721,10 @@ impl SpotSet {
|
||||
label: round_label(round),
|
||||
radius: reach.ceil() as u32,
|
||||
wgsl: SPOT_BODY.to_string(),
|
||||
uniforms: Vec::new(),
|
||||
uniforms: vec![crate::operation::Uniform {
|
||||
name: "rim_samples",
|
||||
value: RIM_SAMPLES,
|
||||
}],
|
||||
storage,
|
||||
});
|
||||
}
|
||||
|
||||
+28
-26
@@ -218,7 +218,7 @@ existing luminance helper.
|
||||
|
||||
`membrane` is what separates the two modes, and it is zero for `Clone`.
|
||||
|
||||
### 6.1 Heal, without a Poisson solve
|
||||
### 6.1 Heal, without a Poisson solve — **implemented**
|
||||
|
||||
The classic heal is Poisson blending: copy the *gradients* of the source and
|
||||
solve for the image whose gradients they are, subject to matching the
|
||||
@@ -226,32 +226,34 @@ destination on the boundary. Solved properly that is an iterative linear system
|
||||
— tens of Jacobi passes over the disc — and each iteration is a dispatch in this
|
||||
architecture. Sixty dispatches to remove a dust spot is not a frame budget.
|
||||
|
||||
The membrane that solve produces is smooth and interpolates the boundary
|
||||
difference, and there is a closed form that approximates it to a quality nobody
|
||||
has ever complained about in a photograph: **mean-value seamless cloning**
|
||||
(Farbman et al., 2009). Sample the difference between destination and source
|
||||
around the rim of the disc at `K` points, and interpolate those differences into
|
||||
the interior with mean-value weights:
|
||||
What that solve produces is a smooth membrane interpolating the boundary
|
||||
difference, and a membrane can be interpolated directly instead of solved. The
|
||||
shipped form samples the difference between destination and source at `K` points
|
||||
around the rim and interpolates them into the interior by inverse square
|
||||
distance:
|
||||
|
||||
```
|
||||
for k in 0..K:
|
||||
b_k = tap(rim_k) - tap(rim_k + offset) // boundary difference
|
||||
membrane = Σ λ_k(p) · b_k // λ from the rim geometry
|
||||
w_k = 1 / max(|p - rim_k|², 1)
|
||||
membrane = Σ w_k·b_k / Σ w_k
|
||||
```
|
||||
|
||||
For a circular boundary the mean-value weights reduce to something a shader can
|
||||
evaluate directly — the interior point's angular position and distance to each
|
||||
rim sample — so a heal costs `K` pairs of taps and a weighted sum, per pixel
|
||||
*inside the disc only*. No iteration, no extra pass, no state.
|
||||
`K = 24` — `RIM_SAMPLES` in `core/dr-pipeline/src/spot.rs`, a uniform rather
|
||||
than a constant in the source, so tuning it uploads a buffer instead of
|
||||
recompiling. The cost is `2K` bilinear samples per pixel *inside a disc*, and
|
||||
nothing at all outside one.
|
||||
|
||||
What it buys: a patch that carries the source's texture and the destination's
|
||||
colour and brightness, which is precisely the failure a plain clone has when a
|
||||
sky is a gradient — the clone is right in texture and a quarter-stop wrong in
|
||||
tone, and shows as a disc.
|
||||
|
||||
`K = 24` is the starting number. It is a uniform, so it is tunable without a
|
||||
recompile, and the acceptance test in §12 is a measured one rather than a taste
|
||||
one.
|
||||
**What was originally specified here was mean-value seamless cloning** (Farbman
|
||||
et al., 2009), whose weights are half-angle tangents over the rim rather than
|
||||
inverse squares. The difference matters when the boundary difference varies
|
||||
sharply around the rim; on the case that actually arises — a repair on a
|
||||
smoothly varying background — both reduce to the same answer, and the inverse
|
||||
square form costs two transcendentals per sample fewer. The measurement in
|
||||
`core/dr-gpu/tests/spot_removal.rs` is what decides whether that trade stays
|
||||
good: on a linear ramp steep enough to make a clone wrong by 38 levels out of
|
||||
255, the heal is wrong by **0**. If a case turns up where it is not, the weights
|
||||
are four lines and the tests are already written.
|
||||
|
||||
### 6.2 Why not compute the boundary statistics on the CPU
|
||||
|
||||
@@ -487,30 +489,30 @@ surprise inside the existing paste.
|
||||
Each stage is shippable and each has something to look at. Test names are the
|
||||
files they belong in.
|
||||
|
||||
**S1 — The model.** `spot.rs` in `dr-pipeline`: `Spot`, `SpotMode`, `SpotSet`,
|
||||
**S1 — The model.** *(Done.)* `spot.rs` in `dr-pipeline`: `Spot`, `SpotMode`, `SpotSet`,
|
||||
the bounds, id derivation, round grouping (§5.2). No GPU, no UI.
|
||||
*Tests:* `core/dr-pipeline/tests/spots.rs` — id stability across two identical
|
||||
placements, `MAX_SPOTS` refuses rather than drops, overlapping sources produce
|
||||
two rounds, disjoint produce one.
|
||||
|
||||
**S2 — Persistence.** Sidecar write, parse, round trip, merge.
|
||||
**S2 — Persistence.** *(Done.)* Sidecar write, parse, round trip, merge.
|
||||
*Acceptance:* a hand-written sidecar with three spots survives a load/save round
|
||||
trip byte-identically, and two devices that each add a spot offline end with
|
||||
both.
|
||||
|
||||
**S3 — The storage binding.** `DetailPass::storage`, the preamble's binding 3,
|
||||
**S3 — The storage binding.** *(Done.)* `DetailPass::storage`, the preamble's binding 3,
|
||||
the dummy buffer, the two layouts.
|
||||
*Acceptance:* every existing detail test still passes untouched, and a synthetic
|
||||
pass reading the buffer gets what was uploaded.
|
||||
|
||||
**S4 — Clone.** The disc, the feather, the bilinear helper, the pass grouping,
|
||||
**S4 — Clone.** *(Done.)* The disc, the feather, the bilinear helper, the pass grouping,
|
||||
the halo declaration, spliced first into the chain.
|
||||
*Acceptance:* `core/dr-gpu/tests/spot_removal.rs` — a synthetic frame with a
|
||||
black disc on a flat grey field is clean to within a tolerance after one clone
|
||||
spot; the same edit at a one-quarter proxy and at full size land the disc in the
|
||||
same *normalised* place; adding spots does not grow `cached_pipelines()`.
|
||||
|
||||
**S5 — Heal.** The mean-value membrane, the mode switch.
|
||||
**S5 — Heal.** The membrane, the mode switch. **Done** — §6.1, and the measurement came out at 0 levels of error against a clone's 38.
|
||||
*Acceptance:* a dark spot on a linear grey **gradient** — the case clone fails —
|
||||
is clean to within a tolerance, and the residual at the disc boundary is below
|
||||
the residual a clone leaves by an order of magnitude. This is the measurement
|
||||
@@ -544,7 +546,7 @@ means it.
|
||||
|
||||
## 14. Open questions
|
||||
|
||||
1. **`K = 24`?** Decided by S5's measurement, not here.
|
||||
1. ~~**`K = 24`?**~~ Settled by S5's measurement: 24 samples, inverse square weights, zero error on the case the mode exists for.
|
||||
2. **Does the reveal view belong to spot mode only,** or is it a view mode of
|
||||
its own that a photographer can turn on while doing something else? It is
|
||||
cheap to allow both; the risk is a mode nobody remembers turning on.
|
||||
|
||||
@@ -10,7 +10,7 @@ Denominators are parsed from [`requirements.md`](requirements.md) at run time, n
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 232 |
|
||||
| TRACES tags found | 647 |
|
||||
| TRACES tags found | 648 |
|
||||
| Requirements defined | 177 |
|
||||
| Requirements covered | 91 |
|
||||
| **Coverage** | **51.4%** (91/177) |
|
||||
@@ -63,8 +63,8 @@ _None._
|
||||
| FR-DEV-4 | [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:217`](../core/dr-gpu/src/lib.rs#L217) |
|
||||
| FR-DEV-5 | [`core/dr-pipeline/src/history.rs:124`](../core/dr-pipeline/src/history.rs#L124), [`core/dr-pipeline/src/history.rs:1`](../core/dr-pipeline/src/history.rs#L1), [`core/dr-pipeline/src/history.rs:55`](../core/dr-pipeline/src/history.rs#L55), [`core/dr-pipeline/src/history.rs:71`](../core/dr-pipeline/src/history.rs#L71), [`core/dr-pipeline/src/history.rs:79`](../core/dr-pipeline/src/history.rs#L79), [`ui/dr-ui/src/develop.rs:2818`](../ui/dr-ui/src/develop.rs#L2818), [`ui/dr-ui/src/develop.rs:2828`](../ui/dr-ui/src/develop.rs#L2828), [`ui/dr-ui/src/develop.rs:488`](../ui/dr-ui/src/develop.rs#L488), [`ui/dr-ui/src/lib.rs:1267`](../ui/dr-ui/src/lib.rs#L1267) |
|
||||
| FR-DEV-6 | [`core/dr-pipeline/src/preset.rs:1`](../core/dr-pipeline/src/preset.rs#L1), [`core/dr-types/src/settings.rs:182`](../core/dr-types/src/settings.rs#L182), [`ui/dr-ui/src/develop.rs:2757`](../ui/dr-ui/src/develop.rs#L2757), [`ui/dr-ui/src/develop.rs:2778`](../ui/dr-ui/src/develop.rs#L2778), [`ui/dr-ui/src/lib.rs:1239`](../ui/dr-ui/src/lib.rs#L1239), [`ui/dr-ui/src/library.rs:1545`](../ui/dr-ui/src/library.rs#L1545), [`ui/dr-ui/src/library.rs:364`](../ui/dr-ui/src/library.rs#L364), [`ui/dr-ui/src/library.rs:392`](../ui/dr-ui/src/library.rs#L392), [`ui/dr-ui/src/library_ui.rs:2576`](../ui/dr-ui/src/library_ui.rs#L2576), [`ui/dr-ui/src/library_ui.rs:2976`](../ui/dr-ui/src/library_ui.rs#L2976), [`ui/dr-ui/src/library_ui.rs:466`](../ui/dr-ui/src/library_ui.rs#L466), [`ui/dr-ui/src/presets.rs:1`](../ui/dr-ui/src/presets.rs#L1), [`ui/dr-ui/src/settings_ui.rs:535`](../ui/dr-ui/src/settings_ui.rs#L535), [`ui/dr-ui/ui/adjust.slint:598`](../ui/dr-ui/ui/adjust.slint#L598), [`ui/dr-ui/ui/library.slint:1305`](../ui/dr-ui/ui/library.slint#L1305), [`ui/dr-ui/ui/library.slint:831`](../ui/dr-ui/ui/library.slint#L831), [`ui/dr-ui/ui/library.slint:912`](../ui/dr-ui/ui/library.slint#L912), [`ui/dr-ui/ui/settings.slint:87`](../ui/dr-ui/ui/settings.slint#L87) |
|
||||
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`core/dr-pipeline/src/spot.rs:107`](../core/dr-pipeline/src/spot.rs#L107), [`core/dr-pipeline/src/spot.rs:143`](../core/dr-pipeline/src/spot.rs#L143), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:341`](../core/dr-pipeline/src/spot.rs#L341), [`core/dr-pipeline/src/spot.rs:426`](../core/dr-pipeline/src/spot.rs#L426), [`core/dr-pipeline/src/spot.rs:575`](../core/dr-pipeline/src/spot.rs#L575), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1) |
|
||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:575`](../core/dr-pipeline/src/spot.rs#L575), [`ui/dr-ui/src/develop.rs:2162`](../ui/dr-ui/src/develop.rs#L2162), [`ui/dr-ui/src/develop.rs:2984`](../ui/dr-ui/src/develop.rs#L2984), [`ui/dr-ui/src/develop.rs:3467`](../ui/dr-ui/src/develop.rs#L3467), [`ui/dr-ui/src/develop.rs:3501`](../ui/dr-ui/src/develop.rs#L3501), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
|
||||
| FR-DEV-8 | [`core/dr-gpu/src/detail.rs:252`](../core/dr-gpu/src/detail.rs#L252), [`core/dr-gpu/src/detail.rs:434`](../core/dr-gpu/src/detail.rs#L434), [`core/dr-gpu/tests/detail_instances.rs:1`](../core/dr-gpu/tests/detail_instances.rs#L1), [`core/dr-gpu/tests/spot_removal.rs:1`](../core/dr-gpu/tests/spot_removal.rs#L1), [`core/dr-pipeline/src/detail.rs:363`](../core/dr-pipeline/src/detail.rs#L363), [`core/dr-pipeline/src/detail.rs:387`](../core/dr-pipeline/src/detail.rs#L387), [`core/dr-pipeline/src/detail.rs:422`](../core/dr-pipeline/src/detail.rs#L422), [`core/dr-pipeline/src/detail.rs:496`](../core/dr-pipeline/src/detail.rs#L496), [`core/dr-pipeline/src/graph.rs:109`](../core/dr-pipeline/src/graph.rs#L109), [`core/dr-pipeline/src/graph.rs:187`](../core/dr-pipeline/src/graph.rs#L187), [`core/dr-pipeline/src/graph.rs:587`](../core/dr-pipeline/src/graph.rs#L587), [`core/dr-pipeline/src/operation.rs:299`](../core/dr-pipeline/src/operation.rs#L299), [`core/dr-pipeline/src/operation.rs:517`](../core/dr-pipeline/src/operation.rs#L517), [`core/dr-pipeline/src/sidecar.rs:186`](../core/dr-pipeline/src/sidecar.rs#L186), [`core/dr-pipeline/src/sidecar.rs:330`](../core/dr-pipeline/src/sidecar.rs#L330), [`core/dr-pipeline/src/sidecar.rs:664`](../core/dr-pipeline/src/sidecar.rs#L664), [`core/dr-pipeline/src/sidecar.rs:800`](../core/dr-pipeline/src/sidecar.rs#L800), [`core/dr-pipeline/src/sidecar.rs:854`](../core/dr-pipeline/src/sidecar.rs#L854), [`core/dr-pipeline/src/sidecar.rs:884`](../core/dr-pipeline/src/sidecar.rs#L884), [`core/dr-pipeline/src/spot.rs:107`](../core/dr-pipeline/src/spot.rs#L107), [`core/dr-pipeline/src/spot.rs:143`](../core/dr-pipeline/src/spot.rs#L143), [`core/dr-pipeline/src/spot.rs:1`](../core/dr-pipeline/src/spot.rs#L1), [`core/dr-pipeline/src/spot.rs:341`](../core/dr-pipeline/src/spot.rs#L341), [`core/dr-pipeline/src/spot.rs:426`](../core/dr-pipeline/src/spot.rs#L426), [`core/dr-pipeline/src/spot.rs:536`](../core/dr-pipeline/src/spot.rs#L536), [`core/dr-pipeline/src/spot.rs:627`](../core/dr-pipeline/src/spot.rs#L627), [`core/dr-pipeline/tests/spot_sidecar.rs:1`](../core/dr-pipeline/tests/spot_sidecar.rs#L1), [`core/dr-pipeline/tests/spots.rs:1`](../core/dr-pipeline/tests/spots.rs#L1) |
|
||||
| FR-DSP-1 | [`core/dr-gpu/src/adjust.rs:2088`](../core/dr-gpu/src/adjust.rs#L2088), [`core/dr-gpu/src/adjust.rs:2165`](../core/dr-gpu/src/adjust.rs#L2165), [`core/dr-gpu/src/adjust.rs:2250`](../core/dr-gpu/src/adjust.rs#L2250), [`core/dr-gpu/src/adjust.rs:54`](../core/dr-gpu/src/adjust.rs#L54), [`core/dr-gpu/src/adjust.rs:770`](../core/dr-gpu/src/adjust.rs#L770), [`core/dr-gpu/src/lib.rs:54`](../core/dr-gpu/src/lib.rs#L54), [`core/dr-gpu/src/lib.rs:94`](../core/dr-gpu/src/lib.rs#L94), [`core/dr-gpu/tests/capture_sharpen.rs:200`](../core/dr-gpu/tests/capture_sharpen.rs#L200), [`core/dr-gpu/tests/detail_stage.rs:328`](../core/dr-gpu/tests/detail_stage.rs#L328), [`core/dr-gpu/tests/local_contrast.rs:263`](../core/dr-gpu/tests/local_contrast.rs#L263), [`core/dr-gpu/tests/noise_reduction.rs:378`](../core/dr-gpu/tests/noise_reduction.rs#L378), [`core/dr-pipeline/src/detail.rs:136`](../core/dr-pipeline/src/detail.rs#L136), [`core/dr-pipeline/src/detail.rs:465`](../core/dr-pipeline/src/detail.rs#L465), [`core/dr-pipeline/src/graph.rs:449`](../core/dr-pipeline/src/graph.rs#L449), [`core/dr-pipeline/src/graph.rs:479`](../core/dr-pipeline/src/graph.rs#L479), [`core/dr-pipeline/src/ops/capture_sharpen.rs:1`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L1), [`core/dr-pipeline/src/ops/capture_sharpen.rs:654`](../core/dr-pipeline/src/ops/capture_sharpen.rs#L654), [`core/dr-pipeline/src/ops/local_contrast.rs:1`](../core/dr-pipeline/src/ops/local_contrast.rs#L1), [`core/dr-pipeline/src/ops/local_contrast.rs:662`](../core/dr-pipeline/src/ops/local_contrast.rs#L662), [`core/dr-pipeline/src/ops/noise_reduction.rs:695`](../core/dr-pipeline/src/ops/noise_reduction.rs#L695), [`core/dr-pipeline/src/spot.rs:627`](../core/dr-pipeline/src/spot.rs#L627), [`ui/dr-ui/src/develop.rs:2162`](../ui/dr-ui/src/develop.rs#L2162), [`ui/dr-ui/src/develop.rs:2984`](../ui/dr-ui/src/develop.rs#L2984), [`ui/dr-ui/src/develop.rs:3467`](../ui/dr-ui/src/develop.rs#L3467), [`ui/dr-ui/src/develop.rs:3501`](../ui/dr-ui/src/develop.rs#L3501), [`ui/dr-ui/src/lib.rs:59`](../ui/dr-ui/src/lib.rs#L59), [`ui/dr-ui/src/lib.rs:720`](../ui/dr-ui/src/lib.rs#L720) |
|
||||
| FR-DSP-6 | [`core/dr-pipeline/src/operation.rs:446`](../core/dr-pipeline/src/operation.rs#L446), [`core/dr-types/src/colour.rs:1`](../core/dr-types/src/colour.rs#L1) |
|
||||
| FR-DSP-7 | [`core/dr-gpu/src/histogram.rs:147`](../core/dr-gpu/src/histogram.rs#L147), [`core/dr-gpu/src/histogram.rs:1`](../core/dr-gpu/src/histogram.rs#L1), [`core/dr-gpu/src/histogram.rs:281`](../core/dr-gpu/src/histogram.rs#L281), [`core/dr-gpu/src/histogram.rs:50`](../core/dr-gpu/src/histogram.rs#L50), [`core/dr-gpu/src/shaders/histogram.wgsl:1`](../core/dr-gpu/src/shaders/histogram.wgsl#L1), [`ui/dr-ui/src/develop.rs:2207`](../ui/dr-ui/src/develop.rs#L2207), [`ui/dr-ui/src/develop.rs:4569`](../ui/dr-ui/src/develop.rs#L4569), [`ui/dr-ui/src/develop.rs:4601`](../ui/dr-ui/src/develop.rs#L4601), [`ui/dr-ui/src/develop.rs:499`](../ui/dr-ui/src/develop.rs#L499), [`ui/dr-ui/src/histogram.rs:1`](../ui/dr-ui/src/histogram.rs#L1), [`ui/dr-ui/src/lib.rs:1326`](../ui/dr-ui/src/lib.rs#L1326), [`ui/dr-ui/src/lib.rs:285`](../ui/dr-ui/src/lib.rs#L285), [`ui/dr-ui/ui/app.slint:276`](../ui/dr-ui/ui/app.slint#L276), [`ui/dr-ui/ui/histogram.slint:122`](../ui/dr-ui/ui/histogram.slint#L122), [`ui/dr-ui/ui/histogram.slint:1`](../ui/dr-ui/ui/histogram.slint#L1) |
|
||||
| FR-EXP-1 | [`core/dr-export/src/encode.rs:1`](../core/dr-export/src/encode.rs#L1), [`core/dr-export/src/lib.rs:1`](../core/dr-export/src/lib.rs#L1), [`core/dr-types/src/settings.rs:1`](../core/dr-types/src/settings.rs#L1), [`ui/dr-ui/src/settings_ui.rs:1`](../ui/dr-ui/src/settings_ui.rs#L1) |
|
||||
|
||||
Reference in New Issue
Block a user