mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-13 09:20:07 -06:00
bb414d220a52a45cbf7cf5578858a0b55cbe21d8
16
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bb414d220a |
LR-EDIT-LIGHT-SHADOWS: fix Highlights/Shadows block artefacts in fine detail; 4× faster GPU median
Fixes #3460 Related: #2936 (slow GPU kernel compiles on Windows/DX12). This PR builds the GPU kernels without wgpu's workgroup-memory zeroing, which took a DX12 release start from ~87 s to 1.4 s on a GTX 1070 with the new median kernels; it may shorten the compiles there too, but does not close it. ## Problem Raising Shadows on a photo with dark lettering on a bright panel makes bright blocks grow beside the letters. On a Canon CR3 of a bus (6960×4640) with a black fleet number on a yellow panel, Shadows +75 puts bright yellow rectangles in the gaps where the digits pinch together and inside the 4's counter. At Shadows +5 there is none of this; at +75 it shows at any zoom. Narrow gaps between dark objects generally can do it: the pinned corpus has the same thing in the gap between two posts under a machine (Hasselblad, `coverage-6568.fff`). ## Cause From process V2 the Highlights/Shadows base (`local::tone_base`) is the edge-aware filter of a separable median of log luminance, at a radius of 0.15 % of the long edge (10 px here), so that thin detail (twigs, lines, #632) takes its surroundings' shift. Text strokes and gaps are about that wide, so each row and column window is close to half letter and half panel, and the median picks one tone or the other in rectangles: it fills the gaps between the digits and the 4's counter with the letters' tone. The guided filter keeps those blocks, so panel pixels whose base reads as deep shadow get Shadows' full lift (`2^(1.7·s·w^1.5)`, up to ~1.3 EV at +75). ## Examples Top: before, bottom: after. The fleet number on the bus photo, Whites +54, Blacks +31, Shadows +75, at 300 %: <img width="1020" height="976" alt="example_photo_pair" src="https://github.com/user-attachments/assets/cc5b164f-612d-4d7d-bda3-db6afd8b177f" /> The gap between two posts in the corpus' Hasselblad `coverage-6568.fff` (CC0, raw.pixls.us), Shadows +100, at 300 %: <!-- image: example_corpus_pair.png --> <img width="900" height="1216" alt="example_corpus_pair" src="https://github.com/user-attachments/assets/feb4b240-194e-40b1-b5b9-c4419f840df6" /> Synthetic lettering at the scale of a bus number in a 7000 px photo, Shadows +75, at 200 %: <!-- image: example_pair.png --> <img width="880" height="820" alt="example_pair" src="https://github.com/user-attachments/assets/bd02e600-dda0-4648-85e9-f89ce046eb08" /> ## Change `local::detail_free` (CPU) and its GPU twin: each value of log luminance is clamped between its separable median at r (as before) and the median of a 3× wider neighbourhood (the median at r of a box-downscaled copy, enlarged bilinearly, like `guided_fast`'s coefficients). The base is the guided filter of that plane. - A twig or speck in an even surround has both medians on the surround's side and takes the nearer, as under the median alone (#632 unchanged). - The gap between two strokes sits in a wider neighbourhood that is mostly panel: the medians disagree and the gap keeps its own tone. The strokes themselves count as fine dark detail on the panel (both medians agree) and keep their contrast, as twigs do. - Steps between regions stay exact (the fine median keeps them). - GPU: the coarse median reuses the box/bilinear resize, plus one `clamp_between_k` map kernel. CPU and GPU match (max 1 level). - GPU median kernels rewritten (`median_h` / `median_v`): each workgroup loads its taps into workgroup memory and selects by rank, 4 neighbouring outputs per thread. The old partial selection sort over a private 49-tap array sat in scratch memory. Same selection, so bit for bit the CPU's (`median_matches_cpu_bit_for_bit`, every r 1–24); renders are byte-identical to the old kernels. On 6960×4640 at r = 10 (M2 Max) the median takes 191 → 47 ms. - GPU kernels are built without wgpu's zeroing of workgroup memory (as `nn.rs` and `faces.rs` already are): on DX12 it made FXC take ~87 s over the new median kernels at every start of a release build (GTX 1070, 1.4 s without it). Every element of `med_tile` the kernels read is loaded before the barrier; renders are byte-identical. - Cost on the 6960×4640 bus CR3 with Whites +54, Blacks +31, Shadows +75 (M2 Max): GPU planes 231 ms on `main` → 113 ms here (257 ms with this PR's first kernels); CPU base ~388 → ~453 ms. The CPU path enlarges the coarse median while it clamps, into the fine median in place (`detail_free_matches_its_reference`, bit for bit). - New process V8 (`Process::LATEST`). Existing edits keep their process and render as before, so `RENDER_CACHE_VERSION` is unchanged (it is 76 on `main`). - Docs: `process-versions.md` (V8 row, a paragraph on the lettering fix, revision history), `parity-checklist.md` (LR-EDIT-LIGHT-HIGHLIGHTS / -SHADOWS notes). An earlier revision of this PR clamped each value to the quartiles of its window instead. It fixed the lettering but stopped treating isolated lines between r/2 and r wide as detail (twigs of 3–6 px against a dark wall were lifted less than the wall, as under V1), which the corpus run below caught. A fixed window can't tell an isolated line from a gap between strokes; a wider neighbourhood can. ## Process version The new base applies from a new process, **V8** (V7 plus `local::detail_free`; `local::base_keeps_gaps` is true from V8, CPU and GPU). v0.19.0 shipped V3 to V6, and crystal grain (#3630) took V7 on `main`, so V1 to V7 render exactly as before: `tests_legacy` still locks V1 and V2, and `only_process_v8_on_takes_the_detail_free_base` checks that every earlier process keeps the plain median. New photos get V8; photos already edited keep the blocks until Update to Current Process. The faster median kernels and the workgroup-memory change don't change pixels, so they apply to every process. They stay in this PR: the zeroing only costs time on the new median kernels (on `main` the kernels compile in 0.8 s on the GTX 1070), so on its own it would speed up nothing. ## How it was checked - [x] New test `gaps_between_letters_keep_their_own_tone_in_the_base`: a row of synthetic "8"s sized like the bus number. With the median, 360 panel pixels sit more than 1 EV above their base (worst 2.25 EV); now none do (worst 0.8 EV). It fails with the median alone. - [x] New test `isolated_lines_up_to_the_detail_radius_stay_out_of_the_base`: light lines 1–9 px wide on a dark ground (r = 10) stay out of the base, as under the median. It fails with the quartile clamp (a 6 px line 0.31 of the way into the base). - [x] New test `detail_free_keeps_steps_and_regions`: steps and flat fields pass through unchanged; tiny, empty and NaN planes don't panic. - [x] The #632 tests (twig/stalk contrast, local contrast, no halo at a strong edge, V1 unchanged) and the median reference tests pass unchanged. - [x] `lightcraft-gpu` integration tests on a real adapter, including `highlights_shadows_base_matches_on_every_process` (max difference 1 level). - [x] Corpus A/B: all 451 pinned raws (`cargo xtask corpus --download`) rendered at full size before and after, with Shadows +100 and with Highlights −100 (902 pairs, GPU). No failures. RMSE median 0.18 % (Shadows) / 0.10 % (Highlights), max 0.86 %; pixels changed by more than 3 % at most 0.07 % of a photo; worst 8×8-block change median 10 / 7 levels, 99th percentile 44 / 41, max 61. I looked at the 22 largest local changes (old over new, at the worst block): no new artefacts. Most are a slightly different tone in small gaps among foliage or branches; one is the Hasselblad gap above. - [x] User-visible change: rendered the bus photo at full size and at 2400 px with Whites +54, Blacks +31 and Shadows +5 / +75, and looked at the number, the plate, foliage against the sky and a brick lattice fence. - [x] New tests `median_matches_cpu_bit_for_bit` (GPU, M2 Max) and `detail_free_matches_its_reference` (CPU). - [x] After merging `main` and moving the base to V7 (Linux, Ryzen 5 7545U, RADV): `cargo test --release` of `lightcraft-develop`, `-pipeline` (including `tests_legacy`), `-catalog`, `-engine`, and `-gpu` on the real adapter (skipping `running_out_of_device_memory_falls_back_without_stopping_the_gpu`, which fills RAM on an APU; see above); `cargo fmt`, `cargo clippy --workspace --all-targets -D warnings`, `cargo xtask layers`, `roots`, `assets`, `parity` and `wasm`. Not run: the full `cargo xtask ci`, and the macOS-only `tests_light::v1_and_v2_render_as_before_v3` (V1 and V2 don't change, so it shouldn't need re-capturing). - [x] After merging `main` again (with crystal grain as V7) and moving the base to V8 (Linux, Ryzen 5 7545U, RADV): `cargo test --release` of `lightcraft-develop`, `-pipeline` (including `tests_legacy` and the grain tests), `-catalog`, `-engine`, and `-gpu` on the real adapter (skipping `running_out_of_device_memory_falls_back_without_stopping_the_gpu`); `cargo fmt`, `cargo clippy --workspace --all-targets -D warnings`, `cargo xtask layers`, `roots`, `assets` and `parity`. Not run: `wasm` and the full `cargo xtask ci`. - [x] `cargo xtask ci --quick` (with `CRAFT_FONTS_DIR`): all steps pass. After the median change: all steps pass again (without `CRAFT_FONTS_DIR`), and the CR3 renders byte-identical to the first kernels on GPU and CPU. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Highlights and Shadows adjustments better preserve clean gaps in fine details such as lettering while maintaining the existing treatment of broader image features. * Tone adjustments are less likely to include isolated, narrow lines in the base used for processing. * The updated detail handling helps retain fine structure while reducing unwanted influence from closely packed details. * **Documentation** * Updated process notes to explain how fine-detail handling affects Highlights and Shadows adjustments. <!-- end of auto-generated comment: release notes by coderabbit.ai --> |
||
|
|
ed10d4486c |
Merge main into crystal-grain; the crystal grain moves to process V7
v0.19.0 shipped V4 to V6 with value-noise grain, so those stay frozen and the crystal grain becomes a process of its own. V7 is V6 plus the grain: without grain it renders as V6 (`without_grain_v7_renders_as_v6`), and the GPU equivalence cases now cover V6's value noise. Merged with main's float output (`deep`, `hdr`): the value noise keeps its clamped luminance; the crystal grain already clamps, and fades out at and above SDR white. The finish kernel's failed-render check stays ahead of the aux upload. The test of unknown process numbers now takes one past the latest: 7 was hard-coded, and is a known process now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
8f638b33bd |
Treat a first intersect mask component as its base selection (#3854)
* Treat a first intersect mask component as its base selection A mask created with Intersect as its first component currently renders as empty, so the overlay and local adjustments never appear. Using that component as the base matches how Add and Subtract first components already behave. Change evaluate_one so the first component always sets the base alpha regardless of its combine op. Add a regression test with a single Intersect linear-gradient component asserting the start side is selected. Test: intersect_first_component_selects_its_shape failed before the change and passes after. * Masks: the GPU treats a first Intersect as the base too, and the process doc says so Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs: what changes for masks that start with Intersect Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3943279105 |
Colour noise reduction: joint linear chroma filter with speck brake, CPU and GPU (#815) (#1348)
* WIP: colour NR v2 prototype (env LIGHTCRAFT_NR_V2, CPU only) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on by default (LIGHTCRAFT_NR_V2=0 turns it off), GPU renders read back for it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: cache renders with the new colour NR apart from older ones Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on the GPU (fused down / apply kernels), coefficient grid sigma / 2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 on the CPU without full-size intermediates (row-streamed passes) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: report peak device-buffer use per GPU stage with LIGHTCRAFT_PROFILE * WIP: colour NR v2 linear mode (NRV2_LINEAR=1: c - Y, additive, noise model), Jensen term, mean-drift probe Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 noise model with equal-count luminance bands and a non-negative fit; probe asserts the mean over 3 noise realisations Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 linear mode by default (per channel, K 64, coarse pass smooths only the coarse part); CPU on GPU renders; real-colour probe Known: tests_window::noise_reduction_does_not_depend_on_the_window fails (the noise is measured in the rendered window; the square-root mode failed it too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 joint guide (NRV2_JOINT=1, opt-in), luminance guide knobs, leaf-next-to-black probe, camera-look guard from 858 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: colour NR v2 reads its env knobs once (per-pixel env reads made the linear mode 11x slower); joint stage timings with LIGHTCRAFT_PROFILE Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * WIP: joint colour NR without redundant passes (fused paths + next weights, none after the last scale, delta-1 weights from the path), two-component buffers, parallel reconstruction Bit-identical to |
||
|
|
baf079f659 |
LR-BEHAV-RENDER-FIDELITY: process V5 rolls raw tone curves off into white in stops, so raising Exposure keeps highlights (#1632) (#3783)
* LR-BEHAV-RENDER-FIDELITY: V3 camera tone curve rolls off in stops past its last knot (#1632) A raw's camera tone curve is fitted only to camera JPEG tones below 0.85, so its last knot sits 1 to 3 stops below the sensor's white. Past it, CameraTone::apply closes on white exponentially in linear light: the sensor's white renders at 8-bit 246-254 at Exposure 0 and 254-255 from +1, and raising Exposure turns the top stop the sensor recorded white. Process V3 extends the curve with CameraTone::apply_rolloff: same value and slope at the knot, then gap / (1 + c (2^t - 1)) at t stops past it, the gap halving every stop in the end. On 11 CC0 corpus files with a fitted look, unclipped pixels rendering white drop from up to 22.5% to 0 at +1 EV and from up to 35% to 0 at +2 EV; Exposure 0 renders change by at most 1 code on average. V1 and V2, the fit itself, JPEGs and raws without a fitted look are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: V3 generic raw curve rolls off in stops too; DRO-off, DNG and GPU checks (#1632) Raws without a camera curve take ToneMap::hdr, which reached white about 1/4 stop past the sensor's white and clipped there (o.min(1.0)). Process V3 now uses ToneMap::rolloff_hdr: from half a stop below the white point (tone::GENERIC_KNEE_STOPS) the extended Reinhard curve hands over to the same stop-based shoulder as the camera curve (tone::rolloff_past, shared with CameraTone::apply_rolloff), with the same value and slope at the join. The sensor's white renders at about 235 / 248 / 252 at Exposure 0 / +1 / +2 where V1 and V2 render 239 / 255 / 255. On 6 CC0 DNGs on the generic curve, unclipped pixels rendering white drop from up to 8.4% to 0 at +1 EV and from up to 17.2% to 0 at +2 EV. The DRO-off gate (look_fit::at_most) also checks the V3 shoulder; the rate bound already implies it, so no fit changes and LOOK_VERSION stays. Tests: the generic shoulder (join, slope, monotone, below white, no kink), a regression test that fails with the clipping curve, a DNG ProfileToneCurve under V3, the DRO-off property tests on the V3 shoulder, and a per-process camera-curve GPU check. Docs: process-versions, parity checklist (LR-BEHAV-RENDER-FIDELITY, LRC-DEV-PROCESS), gap 3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: what's new: process 3 keeps raw highlights when Exposure is raised (#1632) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: a flat last camera-curve segment joins the V3 shoulder with its own slope (#1632) The V3 camera shoulder took the last segment's slope with a floor of 0.1, so a flat or shallow segment (valid for a fitted, DNG or saved curve) joined with a kink: slope 0 on the curve, about 0.1 just past it. The shoulder now keeps the segment's own slope at the knot. Below 0.1 a quadratic term in 2^t - 1 joins the denominator: it adds no slope at the knot and makes up the 0.1 pace further up, so a flat curve still closes on white. Segments at 0.1 or steeper are byte-identical, and the generic curve passes no floor. Comments now say the shoulder approaches white in real numbers and can round to it in f32. Found in an independent review of the branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: V4's GPU test pins V4; the corpus DRO photos keep their knots under the V5 bound (#1632) The GPU light-operator test assumed the default process is V4; it now sets V4, since V5 is the default. The corpus DRO test checks that dro_off's V5 bound leaves the second-last knot where the rate bound puts it on the Sony DRO samples, so their V1 to V4 renders don't change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: the DRO-off curve is main's again, so V1 to V4 keep it (#1632) CodeRabbit found that the V5 bound in `dro_off` could move the second-last knot of a DRO photo whose fitted last segment is flat, and V1 to V4 read that curve too. `dro_off` and `at_most` are back to main's, so the camera-look fit is unchanged and LOOK_VERSION stays. The DRO tests check the V5 shoulder where the lowered segment's slope over its squared gap is at most the fitted one's (then it stays below), and on the Sony DRO corpus samples. Also: `rolloff_hdr`'s doc names the HDR lift, and the release note names DNGs whose curve already reaches white. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
eab130ecee |
Docs: drop the Last updated / Change header line that every PR conflicted on (#3790)
* Docs: drop the Last updated / Change header line that every PR conflicted on * xtask parity: the header's Changes field holds only the revision-history link; name the kept fields * xtask parity: reject a field after the header's Changes link |
||
|
|
3b23f25c1c |
Merge main into crystal-grain; the crystal grain moves to process V4
Main took V3 (sharpening) and V4 (calibrated Contrast and Blacks) while this was open. Neither is in a release yet (v0.18.1's latest is V2), so the grain joins V4 rather than adding a process: V1 to V3 keep the value noise, V4 draws crystals. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
fc97c72864 |
LR-BEHAV-RENDER-FIDELITY: calibrated Contrast and Blacks in process V3 (#523) (#1383)
* LR-BEHAV-RENDER-FIDELITY: split V3 Contrast and Blacks Port calibrated global Contrast and Blacks with shared CPU and GPU tables. Preserve V2 Whites, local adjustments, other source paths, and all V1 and V2 renders and persistent cache keys. Generate the constants from public Contrast and Blacks fits. Cover numeric references, legacy renders, keys that include the resolved process, windows and GPU parity. Document the measurements, limits and provisional number. Co-Authored-By: Codex <noreply@openai.com> * LR-BEHAV-RENDER-FIDELITY: reduce V3 preview cost and sanitize fits Compose calibrated CPU preview operators over linear input and skip unused mix endpoints. Keep high-bit-depth exports on the original operator path. Require three distinct photos per published error group and band, regenerate the fit hashes, and cover the sanitizer with synthetic tests. Document release measurements and identify the separate measurement tool proposed in #1181. Co-Authored-By: Codex <noreply@openai.com> * LR-BEHAV-RENDER-FIDELITY: preserve V3 luminance evaluation Retain the original operator evaluation for luminance mixes, including preceding Contrast, so preview interpolation cannot change the cutoff. Keep the fused per-channel path and exact deep exports. Add CPU and GPU regressions for both uniform cutoff fixtures. Document the measured CPU cost of positive Blacks and the retained fused path. Co-Authored-By: Codex <noreply@openai.com> * LR-BEHAV-RENDER-FIDELITY: finish split-module integration Import the persistent process-key helper directly in the session module. Keep the existing V3 operator selection and evaluation order in an inline helper so the combined finish-stage closure stays within the complexity limit. Refresh the V1/V2 golden hashes only after the exact fixture produces identical hashes on plain current main. Keep the cross-platform legacy samples unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * LR-BEHAV-RENDER-FIDELITY: date the calibrated Light gap summary Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Codex <noreply@openai.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
d977d3e1a4 |
LR-EDIT-DETAIL-SHARPEN: Radius and Detail work, and sharpening acts at pixel scale (#757) (#762)
* LR-EDIT-DETAIL-SHARPEN: Radius and Detail work, and sharpening acts at pixel scale (#757) * LR-EDIT-DETAIL-SHARPEN: an enlarging export keeps every Radius step (#757) The sharpening blur's sigma was capped at 4 px, so a 2x export rendered Radius 2 and 3 alike (both scaled past 4 px). The cap is now the largest sigma the Gaussian taps support, GAUSS_SIGMA_MAX (64), which the CPU and GPU blurs share. Test: enlarged_export_keeps_radius_steps_apart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM * LR-EDIT-DETAIL-SHARPEN: preserve process-aware previews after rebase (#757) Use the V3 pixel-scale blur and Masking weight for the Radius, Detail and Masking previews on both renderers. Preserve the legacy texture-band previews for V1/V2 and compute the same Gaussian when sharpening is disabled. Cover process selection, radius changes, and prepared/fresh plane equivalence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014ti8KemZbjE2ZgHgV1q1BM --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com> |
||
|
|
c81368470b |
LR-EDIT-EFFECTS-GRAIN: film grain from crystal layers (process V3)
Grain under the new process V3 is drawn from a simulated emulsion of crystal layers, after Aurélien Pierre's crystallographic grain model (implemented from the article's description). It fades out towards black and white as on a print, is applied symmetrically in the display-encoded domain (no shadow darkening, no grey specks in blown highlights), and matches Lightroom Classic's strength and grain width at mid grey. The emulsion is drawn once per Size / Roughness / seed on two periodic tiles in frame coordinates and kept as summed-area tables, so each output pixel takes the exact mean of the emulsion under its footprint: a preview is the export's grain box-downscaled. CPU and GPU read the same tables (the GPU from the finish kernel's aux buffer). V1 and V2 keep their value-noise grain exactly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
f14d166fdf |
Issue #3272: AI masks are enlarged along the photo's edges, no outline under a raised Exposure
An Object / Describe mask is stored as a model's logit grid (288² over the whole photo, ~20 px cells on a 24 MP export) and was enlarged by bilinear sampling alone, so its edge spilled 1–2 px onto the background; raising the mask's Exposure turned that strip into a bright outline in previews and exports (CPU and GPU: the GPU renderer evaluates these shapes with the same CPU `shape_alpha`). `sample_seg` now guided-filters the sampled logits by the photo's log luminance where the grid is coarser than the output: the fast (subsampled) guided filter, a window of 1.5 grid cells, so a preview and an export put the edge in the same place. Filtering logits, not coverage, lets the confident side win (the spill goes to ≈ 0, not to a mean of ≈ 0.1). Pixels whose window is all inside or all outside, and edges the photo has no contrast across, keep the plain sample exactly. Refine Edges still runs afterwards on the combined mask and finds the edges in place: no second halo, no tightening. One implementation: `guided_cross` (Refine Edges, Auto Mask) and the new `guided_cross_fast` share `guided_coeffs`; the subsampled form uses `local::guided_fast_step` and a box `resize`, like `local::guided_fast`. `guided_cross`'s output is unchanged. Existing AI masks become 1–2 px tighter at a subject's edges, on every process; their inside, outside and contrast-less edges are unchanged. This lands in place under the bug-fix rule of docs/process-versions.md (output that is in neither the photo nor the edit), noted there. Cost per AI selection (M2 Pro, 12 cores; one shape_alpha, before → after): 24 MP 25.7 → 48.2 ms, heap peak 96.5 → 108.8 MB; 60 MP 64.1 → 109.9 ms, 239.9 → 256.7 MB; 2.7 MP preview 3.1 → 10.6 ms, 11.2 → 26.5 MB. Tests (a dark disc on a light background, its mask on a 32² grid): ≈ 0 one and two px outside and ≈ 1 inside, all around (1 px outside was 0.66), bit-identical away from the edge; with Refine Edges 50 / 100 (was 0.29 outside) and the edge within 1 px of the unrefined one; the same edge position at 256, 512 and 1024 px; a Prompt mask's window matches the whole render. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
c5cebdef39 |
Tone: neutral display tone map is the identity up to white (#1945)
The SDR shoulder in ToneMap::display_hdr ran at neutral Contrast, Whites and Blacks too, so an unedited JPEG or PNG exported neutral 255, 254 and 253 one level darker and a white background never came out as 255. The shoulder now shapes edited curves only; neutral sliders are the identity up to white and clip above it. Edited curves render as before (V1). Tests: the identity test runs to 1.0, a new pipeline test renders the issue's neutral greys back exactly, the sRGB ramp round trip is exact, and a guard keeps the shoulder on edited curves. |
||
|
|
8c4898ec04 |
Merge PR #645
# Conflicts: # crates/engine/src/media.rs # crates/pipeline/src/finish.rs # crates/pipeline/src/local.rs |
||
|
|
964d9338ba |
Progress docs: target-app parity, RAW coverage vs Adobe, gaps, alpha gate, readiness by audience (#788)
Progress docs per craftrules standards/progress-docs.md: target-app parity, gaps, roadmap with alpha gate, readiness by audience (full / mainstream / essentials) with Opus 5.5 hours, model-by-model RAW coverage against Adobe's camera list (docs/raw/), and format, hardware, UI and localization parity docs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
70103a7e55 |
LR-EDIT-LIGHT-HIGHLIGHTS: keep local contrast under Highlights/Shadows, as process V2 (#632)
The highlights/shadows base (fast guided filter of log luminance, eps 0.35) followed thin high-contrast detail, so the per-pixel shift flattened twigs, lines and texture in bright areas (and dark ones under Shadows); Exposure kept them. This changes how every existing edit with Highlights or Shadows renders, so it ships as a new rendering process (docs/process-versions.md): - Process::V2 (version 2) is added and made LATEST. New photos and Reset get it; existing edits stay on V1 until Update to Current Process. Settings saved without the field still load as V1. - From V2 the base is the same guided filter of a separable median of log luminance (radius 0.15 % of the long edge, <= 24 px): fine detail takes its surroundings' shift and keeps its contrast, region steps stay put (no new halos). V1's base is untouched: local::tone_base_detail is 0 under V1 and tone_base then is exactly guided_fast(l, sigma, BASE_EPS), as before. - CPU (local::prepare) and GPU prepare (median_h / median_v kernels, bit for bit with the CPU median) both branch on it via PlaneSigmas::base_detail. - The base plane's cache key is now (sigma, process, median radius) (local::BaseKey), so a warm cache never serves one process the other's base. plane_at is shared with the GPU crate. - finish::base_tone: V2 uses V1's tone map. Tests: V2 keeps twig/local contrast (>= 0.9 of Exposure's) with no halo; V1 plane sigmas have no median and its base equals guided_fast bit for bit; V1 and V2 render alike without Highlights/Shadows; warm-cache renders across V1/V2 switches equal fresh ones (fails if the key drops the process); GPU matches CPU on both processes at 720 and 2400 px, cached and not. Pentax 645D 2401 at 4000 px: main and V1 renders bit-identical (plain, Highlights -100, Shadows +100). Highlights -100 bright rel. detail 0.095 (V1) -> 0.147 (V2), Exposure -1: 0.124; Shadows +100 dark rel. detail 0.126 -> 0.148. RENDER_CACHE_VERSION 54 (adjust at merge). |
||
|
|
2e817a45f4 |
LRC-DEV-PROCESS: process versions, so a look change can ship without changing existing edits (#146)
Each photo's develop settings record the rendering process they are interpreted by (`DevelopSettings::process`). There is one process, V1, the pipeline as it is; nothing renders differently (180 CPU 8/16-bit and GPU renders of procedural scenes, and the saved settings JSON and hash64, are byte-identical to main). Settings saved without the field load as V1 (a field-level serde default), new settings get the latest (`Default`), and V1 is left out when saving, so files and preview-cache keys stay as they were. A damaged value (anything but a whole number up to 2^32 - 1) reads as V1 too, so it never costs the rest of the settings (an unparsable `lc:settings` sidecar is ignored). Reset moves a photo to the latest process, section and slider resets don't; virtual copies inherit it; copy / paste / sync / Auto Sync / presets don't carry it; a number from a newer LightCraft is kept and renders with the newest known process. The process is not an edit (Edited badges, import-look checks). The base tone map is chosen per process in one place (`finish::base_tone`, shared by CPU and GPU). Photo > Update to Current Process (`develop.updateProcess`) moves selected photos on an older process to the latest as one undo step. Its menu enablement follows the selection; a call that names photos (`ids` / `id`) is validated on those instead, through a new opt-in `CommandSpec::explicit_targets` (execute checks enablement before the handler sees `ids`). `develop.get` reports the process. The rule and how to add V2: docs/process-versions.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |