Stacked on rb/1592: LOOK_VERSION 8 -> 9; the PR's corpus-profile helper is the base's corpus_uses_the_bundled_profile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolved against rb-base-36: LOOK_VERSION 6 -> 7; starting_colour_tags passes no lens (merge frames arrive without it).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fitted with `lightcraft-cli calibrate` to 144 usable ILCA-99M2 ARWs (of
150, seven shooting days, Standard creative style, DRO Auto) and their
in-camera JPEGs; aggregate colour statistics only.
On 30 withheld photos (profile fitted to the other 120) mean dE76 against
the camera JPEG goes 5.11 -> 4.87; 25 of 30 improve, the worst
regression is 0.24. All 150 photos stay accepted. The public raw.pixls.us
ILCA-99M2 file, not used for fitting, stays accepted (5.57 -> 5.73).
A corpus test checks that this public sample's fit keeps exactly the
built-in profile's matrix and table; the ILCE-7CR test now shares its
body. LOOK_VERSION 5 -> 6, as the profile changes the fit of this model.
docs/sony-ilce7rm6-lens-corrections.md records the three Sony tables, the
measured conventions with their residuals, the conversion to
WarpRectilinear and its fit gate, the model allowlist and how to extend
it, the measurement method, the verification and the limits.
Verified locally on 54 ILCE-7RM6 photos with four lenses. The corrected
render lines up with the camera JPEG within 0.679 px at 1440 px at the
corners and edge midpoints of the 33 measurable photos shot with
in-camera correction on (up to 17.3 px uncorrected). The 13 controls show
the model's own shift. Every fit passes the 0.25 px gate (margin 0.075).
Lateral CA stays within 0.3 px, but applied after demosaic it raises the
render's corner CA (rms 0.032 -> 0.052 px); the doc and tracker say so.
Its "Two conventions" section sets these conventions next to the
ILCE-7RM4A's (vendor::arw_lens, docs/sony-lens-corrections.md, #498):
on the 34 measurable ILCE-7RM6 photos that formula gives 1.429 / 0.319 px
(max / rms) against 0.596 / 0.143, failing only on the 24-70 GM II and
fitting the 100 mm and 200-600 better; no single unit fits every lens
under either, and what would settle it (Sony On/Off exports of a wide
zoom, more lenses). docs/sony-lens-corrections.md gains a pointer to it.
Tracker rows LR-EDIT-OPTICS-PROFILE (one note for both bodies) and
LR-IMP-CAMERA-COVERAGE in docs/parity-checklist.md, gap 8 in
docs/gaps.md, the lens-profile section of docs/raw-parity.md (lens score
unchanged), whats-new (lossless ARWs now; Compressed / Compressed HQ once
the ARW 6.0 decoder, #625, lands) and the colour-fit follow-up note are
updated.
Co-Authored-By: Claude <noreply@anthropic.com>
The HDR merge has the same cause as the panorama: its result took the
reference frame's colour tags, and a raw whose white balance is only in the
vendor's multipliers (CR2, NEF, ARW...) has no AsShotNeutral in them.
merge_hdr now takes the median multipliers of all its inputs (the rule of
consensus_multipliers: the mean of the middle two for an even count; missing,
non-finite and non-positive values do not vote) and sets the result's
AsShotNeutral to their reciprocal, as stitch does. With no multipliers (DNG
sources) the reference frame's tags are kept, as before. HdrResult carries the
multipliers so that the brackets of an HDR panorama vote as frames of their own
when the merged brackets are stitched (the engine passes them on).
In the app and the CLI the neutral from starting_colour_tags (#965) is already
there for frames balanced alike, so those renders do not change.
Tests: an HDR merge of raw-style frames gets the median AsShotNeutral (odd and
even counts, missing and unusable multipliers ignored) and the written DNG says
it; without multipliers the reference frame's colour is unchanged.
A raw whose white balance is only in the vendor's multipliers (CR2, NEF, ARW...)
has no AsShotNeutral of its own. Loading a frame dropped the multipliers and
stitch copied the first frame's colour tags, so a panorama of frames that reach
the stitcher without a neutral wrote a DNG with neither AsShotNeutral nor
AsShotWhiteXY, and a viewer had no white to start from (a strong colour cast
until White Balance > Auto).
Frames now carry the multipliers (Frame::wb_multipliers, filled from the decoded
raw; load_frame_with is split so a decoded raw can be tested without a file).
stitch takes the median R/G and B/G of the multipliers of the frames that are in
the panorama (the mean of the middle two for an even count; frames without
multipliers, or with non-finite or non-positive ones, do not vote) and sets the
result's AsShotNeutral to their reciprocal, green = 1. The source's
AsShotWhiteXY is dropped with it: a reader takes it before the neutral and the
DNG spec makes the two exclusive. With no multipliers anywhere (DNG sources) the
first frame's tags are kept, as before.
In the app and the CLI a raw without colour matrices already gets a neutral from
starting_colour_tags (#965). For frames balanced alike the new neutral renders
the same (checked in the engine crate); it differs when the camera balanced the
frames differently: the panorama follows the median instead of the first frame.
Tests: the frame carries the multipliers; the consensus for odd and even counts,
with missing and unusable values; stitching raw-style frames gives the median
AsShotNeutral, ignores frames outside the panorama and leaves the colour as it
was without multipliers; the written DNG, read back with the raw reader, has the
reciprocal AsShotNeutral and the white balance the source's multipliers give.
Held-out and library-wide calibrate runs on real CR3s (calibrate's CR3 /
CR2 input is #1073): profiles change little (R7 2.52 -> 2.57, R6 Mark II
6.12 -> 6.09 mean dE), so none is bundled; notes the CRX adaptive QP
172/175 files that stay preview-only. The catalog test pins that an EOS
R6 Mark II (Exif "Canon EOS R6m2") edits in Kelvin through its measured
matrices and an EOS R7 stays relative.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CzWyUn77ZWLe3q3qMiQbVt
#1123 gives HDR and panorama merges of raws without colour matrices the colour they open with, but writes
ColorMatrix1 from the forward matrix, so the as-shot white the DNG declares lands wherever the fitted look and the
generic camera model take it. On real ILCE-7M3 brackets the HDRs opened at As Shot 6113–7307 K / +115 (Duv +0.038)
where the ARWs show 3932–5268 K / +7, and any white-balance edit turned them green (CIEDE2000 11–13 against the ARW
at the same Temp/Tint). The render at As Shot was right (0.4–0.8).
The colour matrices now declare the white balance the source shows, decided by files::WbScaleOf (new dng_white):
the camera's measured matrices when they give the source's scale (lightcraft_raw::spectral; the DNG then reads every
white as the source does), else a matrix under which the as-shot neutral is the white the scale shows (Sony's
presets' reading, 6500 K / 0 on the relative scale, the file's own white otherwise). Forward matrix and neutral are
unchanged, so is the render at As Shot.
Measured on the same brackets with this change on a fork build: As Shot 4813 / 5268 / 3932 K, +8 / +8 / +7 as the
ARWs (declared white Duv +0.003); same Temp/Tint within CIEDE2000 0.8–1.7 of the ARW for most changes, about 5 for
the largest (5268 → 3200 K, 3932 → 6500 K: the ARW adapts its look with Bradford, the DNG rebalances in camera space).
Tests: a merged DNG shows its source's As Shot on all four scales (measured matrices, presets, relative, own); with a
look that tints white the DNG declares the shown white, gives the measured model's multipliers at other whites and
renders at As Shot as the source does. Both fail on main (ILCE-7M3 case: (7342, 115) for (5268, 8)).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
Sony ARWs of cameras without measured colour matrices were edited on the
relative scale (6500 K / 0 = as shot). Their SR2SubIFD stores the
multipliers of the camera's presets at 2500, 3200, 4500, 6000 and 8500 K;
lightcraft_raw::kelvin_presets reads the as-shot multipliers on the curve
through them and they take the spectral matrices' place in the #730
design (as-shot Kelvin and tint only; look, Bradford adaptation and the
pixel-identical legacy upgrade unchanged).
Covered: ILCA-99M2, ILCA-77M2, SLT-A33, DSC-RX10, ILCE-3500, ILCE-7S.
Kelvin-dial shots read back 3400 -> 3396, 5200 -> 5193, 4700 -> 4708,
5600 -> 5594 K; Shade 7493 K. Off-curve constants (isotherm tilt 0.74,
tint spacing 1) measured from the 14 Sony bodies with both spectral
matrices and presets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add correlated dark holdouts that reject misleading training and accept
matching training. Check finite, monotone curves in both cases. Confirm
the regression fails with the error gate removed and passes when restored.
Qualify the improvement claim when no original curve can be fitted, and
explain the source-look process policy and offline smart preview refresh.
Co-Authored-By: Codex <noreply@openai.com>
Use separate luminance pairs for tone fitting so the camera look follows
its JPEG into deep shadows. Keep colour pairs and holdouts unchanged.
Require enough correlated shadow samples and lower held-out error, with
noise and compression floors and the existing median curve fit.
Add synthetic regressions, bump look and render cache versions, and
record the public multi-make and private numeric validation in the docs.
Fixes#1307
Co-Authored-By: Codex <noreply@openai.com>
Photo Merge decoded each raw itself and wrote the reference frame's own colour
tags into the merged DNG. A CR2 (and NEF, ARW and the other raws whose colour is
fitted from the camera JPEG) has none, so an HDR merge of CR2s rendered with no
camera colour: green and about 16 L* dark.
`lightcraft_merge::load_frame_with` takes the raw's colour tags from the caller,
and the engine passes `starting_colour_tags`: the colour the raw opens with
(`starting_colour`) as DNG tags. That is one forward matrix with a colour matrix
and AsShotNeutral that agree with it at the as-shot white, the look's hue/sat
table as ProfileHueSatMap and its tone curve as ProfileToneCurve. Raws with
their own matrices (DNG) keep their tags. The look's chroma curve has no DNG
tag and is left out.
On two CC0 CR2s (EOS 550D, 40D) an HDR merge of three copies against the photo
alone: mean CIE76 dE 26.15 -> 1.07 and 22.89 -> 0.47.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sources: TIFF 6.0 and own black-box analysis of the three CC0 samples;
white-balance estimate from the embedded preview: own design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A raw shot in a monochrome picture style records full colour, but its
camera JPEG is grey: neither the full look nor the partial look has
colour to fit, so the photo started from the spectral matrices or the
neutral fallback with the default tone curve, about 1.3 EV darker than
the same body's colour-shot photos. The grey JPEG still shows the
camera's tone curve.
When no look was fitted and the JPEG is black and white (`monochrome`),
fit the quantile tone curve alone on top of the colour the photo gets
otherwise: the camera profile's (in `fit_preview`), else the spectral
matrices or the fallback (`monochrome_look`, after the spectral step).
The chroma curve stays neutral; the partial look's checks apply. A
tone-only look keeps the file's colour model for white balance.
LOOK_VERSION 2 -> 3, RENDER_CACHE_VERSION 42 -> 43.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raws without matrices in the file (ARW, NEF, RW2, RAF, CR2, CR3, PEF, SRW,
ORF) were all edited on the relative white-balance scale (6500 K / 0 = as
shot), even when lightcraft_raw::spectral has measured matrices for the
camera. Those 52 bodies (ILCE-7M3, ILCE-7M4, D850, EOS 5D Mark III...) now
edit in Kelvin like a DNG and like Lightroom.
Design:
- One decision on both sides: spectral::find_camera("Make Model") backs
catalog::relative_wb_camera (Photo::relative_wb); files::WbScaleOf decides
the same from the decoded file. A corpus test asserts they agree.
Photo::fitted_look keeps the old predicate for smart previews.
- The matrices only convert: As Shot is the file's multipliers read through
the model's ColorMatrix1/2 (spectral::as_shot_white_through). The look
(JPEG fit / profile / spectral fallback) is unchanged, and the slider
adapts from that white with Bradford. The camera-space rebalance stays
reserved for files with their own matrices.
- Presets resolve to their Kelvin values (WbMode::preset, sources cited).
- The Color panel shows the white balance in effect (effective_wb).
- XMP: crs::Target::for_file takes the camera; matrix cameras read
crs:Temperature as Kelvin (marked "scale": "kelvin"), others keep the
#510 as-shot shift.
Migration (no look changes):
- WhiteBalance.scale: WbScale { Legacy (absent in JSON), Kelvin }. A
white balance without a scale keeps its old meaning. On a spectral-Kelvin
raw (SourceInfo.legacy_relative_wb) the pipeline composes the old
adaptation (6500 -> value) with the new one (as shot -> ...) in Bradford
cone space (local::legacy_white). Von Kries scalings compose exactly, so
the matrix matches to f32 and the pixels are identical
(pipeline legacy_relative_white_balance_renders_as_before_on_a_kelvin_raw,
media old_relative_white_balance_on_a_kelvin_camera_keeps_its_look...).
- Every white-balance writer (develop.set/adjust/quickAdjust/wb/wbPick/
reset) first runs local::normalize_wb: a legacy value becomes the Custom
Kelvin value with the same look and is marked Kelvin. Partials without a
scale (old presets, develop.merge) stay legacy.
- Catalog format 7: Op::SetAsShotWb replaces the 6500/0 as-shot recorded
by older imports when the file is next decoded or reloaded (system op,
not an undo step). Format-version test updated.
- The web wire carries legacy_relative_wb.
Docs: parity (LR-EDIT-COLOR-TEMP), camera-preview-colour, xmp-interop,
lightroom-catalog-import, control-protocol, mcp, whats-new.
Merge origin/main into #560 (fit_preview / camera-look refactors of #528, #583, #568, #616).
`starting_colour` now tries the per-file camera-JPEG fit (with the camera profile when there is
one) first and uses the spectral-sensitivity matrices only when that fit fails, before the neutral
fallback, so no photo with an accepted JPEG fit changes. Tests pin the order through an injectable
fit; docs, parity, ROADMAP and what's new describe it. RENDER_CACHE_VERSION 37 -> 38.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only proxies of raws that take the per-file camera look (Photo::relative_wb) go stale: JPEG/PNG/DNG proxies are never rebuilt for the look. The opening scan runs once per LOOK_VERSION (a look-version marker in the smart previews folder), not at every library open; Build Smart Previews still refreshes stale proxies. Each proxy is checked and written under one lock, so the background refresh and library.smartPreviews never write the same file at once. LOOK_VERSION 2: main's DRO / ILCE-7CR camera-look changes (#528, #583, #568, #616).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ORF decoder supplies the as-shot white balance (ImageProcessing
WB_RBLevels) and no colour matrix, like ARW, NEF, RW2, RAF, CR2/CR3, PEF
and SRW, but ORF was left out of camera_preview::file_local_look and the
catalog's relative_wb, so it rendered with the neutral fallback matrix:
flat, grey and dark. It now gets the same guarded fit to its camera JPEG
and white balance relative to the as-shot look. Corpus (held-out RMS
against the fallback, all accepted): E-1 0.126 -> 0.014, E-400 0.087 ->
0.022, XZ-2 0.061 -> 0.023; compressed ORFs still open from their preview.
Against Lightroom Classic (CC0 E-1 kit, 2000 px, ΔE00): unedited render
11.25 -> 8.30; mean over 86 variants abs 12.10 -> 9.48, effect 3.44 ->
2.80. Every other kit's unedited render is byte-identical.
RENDER_CACHE_VERSION 23.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A smart preview keeps the colour and tone curves the camera-look fit gave it, so a
fit change (#499, #528, #583) left existing previews with the old look. They now
record LOOK_VERSION in the header; an older one is rebuilt from its original when the
library is opened or Build Smart Previews runs, and kept as it is while the original
is offline.
Fitted with `lightcraft-cli calibrate` to 125 usable ILCE-7CR ARWs (of 131,
September 2025 to September 2026, Standard creative style) and their
in-camera JPEGs; aggregate colour statistics only.
Without it, 8 of the 131 photos fail the fit gates and open with the
neutral matrix (dE76 6-26 against the camera JPEG). On 26 withheld photos
(profile fitted to the other 105) mean dE76 goes 4.07 -> 3.29 and both
withheld rejections are accepted (26.0 -> 6.1, 6.2 -> 5.5); the 24
withheld photos whose own fit was accepted are about unchanged (12
improve, mean 3.06 -> 3.08, worst regression 1.8). With the final profile
all 131 are accepted. The nine public raw.pixls.us ILCE-7CR codings stay
accepted (mean 1.59 -> 1.99).
A corpus test checks the public lossless-L sample's fit keeps exactly the
built-in profile's matrix and table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sony's Dynamic Range Optimizer (factory default Auto) brightens the darker
regions of the camera JPEG, not the raw. The tone curve fitted to that JPEG
carried the brightening into the raw's starting look: on 8 ILCE-7CR photos
with DRO on, LightCraft rendered +6.4 L* from Lightroom's default (median
dE76 7.5), against -0.2 L* (3.6) for the 16 with DRO off.
- lightcraft_raw::embedded_preview_dynamic_range_optimized reads Sony
maker-note tag 0xb025 (every documented value: 0 off; 1-3, 8-12 and
16-23, i.e. up to Lv8, on). It was present on all 264 Sony raws checked
(98 bodies); 0xb04f only says on/off and is missing on older bodies.
- With DRO on, the look keeps the colour fitted to the JPEG and its tone
curve is lowered, knot by knot, to Sony's curve without DRO wherever that
is darker (assets/camera-tone/sony-dro-off.json, pooled from 12 CC0
raw.pixls.us ARWs of 10 interchangeable-lens bodies; sources and
derivation in the file and assets/ATTRIBUTION.md). The second-last knot
is lowered only as far as keeps the shoulder past the last knot at or
below the fitted curve, so the result is never brighter anywhere.
Compacts that map raw values darker keep their own curve.
- DRO off, unknown and other makers: unchanged (byte-identical renders on
71 + 35 photos). Render cache version 23.
DRO-on photos now land at +0.8 L* from Lightroom (median dE76 3.3), band by
band like the DRO-off ones. Docs: camera-preview-colour.md, parity row.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A raw without colour matrices of its own now gets DNG ColorMatrix1/2 and
ForwardMatrix1/2 for its model when the model is one of the 52 cameras
in rawtoaces-data (Academy Software Foundation, Apache-2.0): 22 Canon,
11 Nikon, 11 Sony, 3 Fujifilm and 5 others, plus the other names its
alias list gives (Rebel T3i and Kiss X5 for the EOS 600D; bodies it
lists as sharing a sensor). tools/spectral_camera_table.py fits, under
Standard Illuminant A and D65, a white-preserving 3x3 to the measured
spectral sensitivities over rawtoaces-data's 190 training reflectances
and writes crates/raw/src/spectral_table.rs. No number comes from Adobe
or from a raw decoder. lightcraft_raw::spectral looks a camera up by
Exif make and model; the DNG colour model does the rest.
camera_preview::starting_colour takes the first available of: the
file's own matrices, a camera profile for the model (fitted to the
JPEG for tone), the spectral matrices, the file-local JPEG fit, the
neutral fallback. A profiled model whose profile fit fails takes its
spectral matrices. White balance stays relative to the as-shot look for
every raw without matrices of its own, so Photo::relative_wb and the
controls are unchanged; calibrate still pools covered models.
Tests: one per precedence step (synthetic), the table's lookup, aliases
and DNG shape, and a corpus test of the order on ILCE-7M4 (profile),
EOS 5D Mark III and D5100 (spectral), EOS 7D (own fit) and a DNG. The
ILCE-7RM2 and GFX100S corpus tests now expect the spectral matrices.
Attribution in assets/ATTRIBUTION.md and NOTICE. RENDER_CACHE_VERSION 23.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>