Commit Graph
6 Commits
Author SHA1 Message Date
Brain WangandClaude Opus 5.5 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>
2026-10-29 11:13:04 +08:00
Gert Pellin 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.
2026-10-16 04:27:35 +02:00
Claude 8c4898ec04 Merge PR #645
# Conflicts:
#	crates/engine/src/media.rs
#	crates/pipeline/src/finish.rs
#	crates/pipeline/src/local.rs
2026-10-11 12:03:53 +00:00
Brandon ThomasandClaude Opus 5.5 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>
2026-10-10 14:25:15 -04:00
radialmonster 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).
2026-10-10 13:46:14 -04:00
Alex ChambersandClaude Opus 5.5 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>
2026-10-09 17:03:45 +02:00