diff --git a/contributors/people.toml b/contributors/people.toml index 7518acd38..76c7fcf1c 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -73,6 +73,9 @@ sync_github_name = true [people.dotCooCoo] real_name = "Robert Lee" + +[people.samuelili] +real_name = "Samuel Li" sync_github_name = true [people.dylanmtaylor] diff --git a/crates/raw/src/spectral.rs b/crates/raw/src/spectral.rs index b80442c02..3a3e74262 100644 --- a/crates/raw/src/spectral.rs +++ b/crates/raw/src/spectral.rs @@ -43,15 +43,17 @@ impl Camera { /// `(make, model as sold, table model)`: bodies sharing a table camera's sensor that rawtoaces-data's alias list /// doesn't name, kept here because the table is generated. Fujifilm's 26.1 MP X-Trans CMOS 4 bodies use the X-T3's -/// sensor (X-T30 issue: a black-and-white Acros camera JPEG gives no colour to fit, and without matrices the photo +/// or X-T4's sensor (X-T30 issue: a black-and-white Acros camera JPEG gives no colour to fit, and without matrices the photo /// opened in the muted neutral fallback). const SAME_SENSOR: &[(&str, &str, &str)] = &[ ("Fujifilm", "X-T30", "X-T3"), ("Fujifilm", "X-T30 II", "X-T3"), ("Fujifilm", "X-Pro3", "X-T3"), ("Fujifilm", "X100V", "X-T3"), - ("Fujifilm", "X-S10", "X-T3"), + ("Fujifilm", "X-S10", "X-T4"), ("Fujifilm", "X-E4", "X-T3"), + ("Fujifilm", "X-S20", "X-T4"), + ("Fujifilm", "X-M5", "X-T3"), ]; /// Model names compared without case, the brand, a leading `EOS` or a trailing `DIGITAL` @@ -164,6 +166,7 @@ mod tests { ("FUJIFILM", "X-T30", "X-T3"), ("FUJIFILM", "X-T30 II", "X-T3"), ("FUJIFILM", "X100V", "X-T3"), + ("FUJIFILM", "X-S20", "X-T4"), ] { assert_eq!(find(Some(make), model).map(|c| c.model), Some(row), "{make} {model}"); } diff --git a/crates/raw/src/vendor/raf_lens.rs b/crates/raw/src/vendor/raf_lens.rs index a976751d2..283a3d0ea 100644 --- a/crates/raw/src/vendor/raf_lens.rs +++ b/crates/raw/src/vendor/raf_lens.rs @@ -15,7 +15,7 @@ //! - distortion: percent, the bracket `1 + D/100` multiplies the radius; the camera's JPEG samples the raw at //! `source = z · r · (1 + D(source)/100)`, radii in half diagonals, with the zoom `z` that keeps the output //! rectangle inside the source (`1 / max` of the bracket over the frame's border radii). -//! - vignetting: relative illumination in percent at the source radius; the correction is the gain `100 / V`. +//! - vignette: relative illumination in percent at the source radius; the correction is the gain `100 / V`. use crate::{Opcode, Rect}; use lightcraft_tiff::Ifd; @@ -34,6 +34,14 @@ const FIT_ROUNDS: usize = 40; /// understood). const SCALE_TOLERANCE: f64 = 0.005; +/// Dimensions and half diagonals of the 26.1 MP (`6240 × 4160`) and 40 MP (`7728 × 5152`) active areas in pixels. +/// The X-S20 writes `scale × n = 4643` (the 40 MP half diagonal) on its uncropped 26.1 MP frame (`3749` px +/// half diagonal), while indexing the 9 knots `√(k/8)` over the 26.1 MP active area. +const XTRANS4_26MP_WIDTH: usize = 6240; +const XTRANS4_26MP_HEIGHT: usize = 4160; +const XTRANS4_26MP_HALF_DIAGONAL: f64 = 3749.0; +const XTRANS5_40MP_HALF_DIAGONAL: f64 = 4643.0; + /// Camera models whose tables were checked against the camera's own JPEG (distortion within 0.01 of the best free /// radial fit's correlation on at least one lens, see docs/fuji-lens-corrections.md). Other bodies stay uncorrected. /// The nine-knot bodies other than the X-T4 were checked with their tables read at `knot_last · r`; the plain radius @@ -106,9 +114,22 @@ struct Table { values: Vec, } +/// Whether `scale × n` matches the recorded image's `half_diagonal` (or, on an uncropped 26.1 MP X-S20 frame, +/// the 40 MP reference half diagonal that its firmware writes). +fn scale_matches(model: &str, scale_times_n: f64, half_diagonal: f64, w: usize, h: usize) -> bool { + if (scale_times_n / half_diagonal - 1.0).abs() <= SCALE_TOLERANCE { + return true; + } + model == "X-S20" + && w == XTRANS4_26MP_WIDTH + && h == XTRANS4_26MP_HEIGHT + && (half_diagonal / XTRANS4_26MP_HALF_DIAGONAL - 1.0).abs() <= SCALE_TOLERANCE + && (scale_times_n / XTRANS5_40MP_HALF_DIAGONAL - 1.0).abs() <= SCALE_TOLERANCE +} + /// Read and sanity-check a table. `half_diagonal` is the recorded image's half diagonal in pixels, which /// `scale × n` must reproduce. -fn read(ifd: &Ifd, tag: u16, half_diagonal: f64) -> Option { +fn read(model: &str, ifd: &Ifd, tag: u16, half_diagonal: f64, w: usize, h: usize) -> Option
{ let v = ifd.f64s(tag)?; if v.is_empty() || v.iter().any(|x| !x.is_finite()) { return None; @@ -118,7 +139,7 @@ fn read(ifd: &Ifd, tag: u16, half_diagonal: f64) -> Option
{ return None; } let scale = v[0]; - if scale <= 0.0 || (scale * n as f64 / half_diagonal - 1.0).abs() > SCALE_TOLERANCE { + if scale <= 0.0 || !scale_matches(model, scale * n as f64, half_diagonal, w, h) { return None; } let knots = v[1..1 + n].to_vec(); @@ -261,9 +282,9 @@ fn solve(mut a: [[f64; N]; N], mut b: [f64; N]) -> Option<[f64; } /// The warp (distortion with the frame-filling zoom) for a recorded image of `w × h` pixels. -fn warp(ifd: &Ifd, w: usize, h: usize) -> Option { +fn warp(model: &str, ifd: &Ifd, w: usize, h: usize) -> Option { let half = (w as f64).hypot(h as f64) / 2.0; - let table = read(ifd, DISTORTION, half)?; + let table = read(model, ifd, DISTORTION, half, w, h)?; // Percent: anything beyond a third is not distortion; an all-zero table has nothing to correct. if table.values.iter().any(|d| d.abs() > 30.0) || table.values.iter().all(|d| *d == 0.0) { return None; @@ -278,9 +299,9 @@ fn warp(ifd: &Ifd, w: usize, h: usize) -> Option { } /// Radial illumination gain `100 / V` as DNG `FixVignetteRadial` (`1 + k0 r² + … + k4 r¹⁰`). -fn vignette(ifd: &Ifd, w: usize, h: usize) -> Option { +fn vignette(model: &str, ifd: &Ifd, w: usize, h: usize) -> Option { let half = (w as f64).hypot(h as f64) / 2.0; - let t = read(ifd, VIGNETTING, half)?; + let t = read(model, ifd, VIGNETTING, half, w, h)?; if t.values.iter().any(|v| !(10.0..=130.0).contains(v)) { return None; } @@ -303,10 +324,11 @@ fn vignette(ifd: &Ifd, w: usize, h: usize) -> Option { /// The lens corrections of a RAF whose camera `model` was validated (anything else, and any table that is not in /// the understood layout and scale, is left uncorrected). `active` is the recorded image (the raw crop). pub(super) fn corrections(model: &str, raw: &Ifd, active: Rect) -> Vec { - if !VALIDATED.contains(&model.trim()) || active.width < 2 || active.height < 2 { + let model = model.trim(); + if !VALIDATED.contains(&model) || active.width < 2 || active.height < 2 { return Vec::new(); } - warp(raw, active.width, active.height).into_iter().chain(vignette(raw, active.width, active.height)).collect() + warp(model, raw, active.width, active.height).into_iter().chain(vignette(model, raw, active.width, active.height)).collect() } #[cfg(test)] @@ -401,8 +423,20 @@ mod tests { fn rejects_unknown_scale_layout_and_values() { let scale = 3749.0 / 9.0; let bad = |ifd: Ifd| corrections("X-T4", &ifd, XT4_AREA); - // another normalisation (X-S20 stores the 40 MP half diagonal), non-increasing knots, wrong lengths, garbage + // another normalisation (ignoring camera exceptions like the X-S20 which writes the 40 MP half diagonal), non-increasing knots, wrong lengths, garbage assert!(bad(only(DISTORTION, table(4643.0 / 9.0, &KNOTS9, &XT4_DIST))).is_empty()); + // X-S20 accepts the 40 MP normalization on its uncropped 26 MP sensor, but rejects in-camera crops + let xs20_ops = corrections("X-S20", &only(DISTORTION, table(4643.0 / 9.0, &KNOTS9, &XT4_DIST)), XT4_AREA); + assert!(matches!(xs20_ops.as_slice(), [Opcode::WarpRectilinear { .. }]), "{xs20_ops:?}"); + // Nonzero active-frame offsets are permitted as long as dimensions are 6240 × 4160 + let offset_area = Rect::new(32, 20, 6240, 4160); + let xs20_offset_ops = corrections("X-S20", &only(DISTORTION, table(4643.0 / 9.0, &KNOTS9, &XT4_DIST)), offset_area); + assert!(matches!(xs20_offset_ops.as_slice(), [Opcode::WarpRectilinear { .. }]), "{xs20_offset_ops:?}"); + let cropped = Rect::new(0, 0, 4992, 3328); + assert!(corrections("X-S20", &only(DISTORTION, table(4643.0 / 9.0, &KNOTS9, &XT4_DIST)), cropped).is_empty()); + // Differing active dimensions are rejected even if hypothetically close in half-diagonal + let wrong_dim = Rect::new(0, 0, 6242, 4157); + assert!(corrections("X-S20", &only(DISTORTION, table(4643.0 / 9.0, &KNOTS9, &XT4_DIST)), wrong_dim).is_empty()); let mut flat = KNOTS9; flat[3] = flat[2]; assert!(bad(only(DISTORTION, table(scale, &flat, &XT4_DIST))).is_empty()); @@ -447,6 +481,27 @@ mod tests { assert!((at_edge - planes[0][0]).abs() < 0.004); } + #[test] + fn xs20_40mp_scale_table_produces_warp_and_vignette() { + let scale = 4643.0 / 9.0; + let ifd = Ifd { + entries: vec![ + Entry { tag: DISTORTION, value: table(scale, &KNOTS9, &XT4_DIST), offset: 0 }, + Entry { tag: VIGNETTING, value: table(scale, &KNOTS9, &XT4_VIG), offset: 0 }, + ], + ..Default::default() + }; + // X-S20 on its uncropped 26 MP sensor emits both WarpRectilinear and FixVignetteRadial + let ops = corrections("X-S20", &ifd, XT4_AREA); + let [Opcode::WarpRectilinear { planes, center: wc }, Opcode::FixVignetteRadial { k, center: vc }] = ops.as_slice() else { + panic!("expected warp and vignette for X-S20: {ops:?}"); + }; + assert_eq!((*wc, *vc), ([0.5, 0.5], [0.5, 0.5])); + assert_eq!(planes.len(), 1); + assert!((planes[0][0] - 1.03).abs() < 0.02); + assert!((k[0]).abs() > 0.0); + } + #[test] fn a_vignette_table_works_without_a_distortion_table() { let scale = 3749.0 / 9.0; diff --git a/crates/raw/tests/it/corpus.rs b/crates/raw/tests/it/corpus.rs index 7f7074f91..85792c79a 100644 --- a/crates/raw/tests/it/corpus.rs +++ b/crates/raw/tests/it/corpus.rs @@ -1443,7 +1443,7 @@ fn corpus_kodak_dcs520c() { /// Fujifilm RAF lens tables (`0xf00b` distortion, `0xf010` vignetting) become a warp and a vignette gain for the /// cameras whose geometry was checked against the camera JPEG (docs/fuji-lens-corrections.md). Reads `LIGHTCRAFT_FUJI` (a folder -/// of RAFs or of per-model folders, e.g. the CC0 pixls set) or the corpus' `raf-fuji-*` files; skips when absent. +/// of RAFs or of per-model folders, e.g. the CC0 pixls set) or the corpus' `raf-fuji*` files; skips when absent. #[test] fn fuji_embedded_lens_tables_follow_the_validated_models() { fn collect(dir: &Path, depth: usize, out: &mut Vec) { @@ -1462,7 +1462,7 @@ fn fuji_embedded_lens_tables_follow_the_validated_models() { Some(dir) => collect(Path::new(&dir), 0, &mut files), None => { collect(&corpus_root().join("raw"), 1, &mut files); - files.retain(|p| p.file_name().is_some_and(|n| n.to_string_lossy().starts_with("raf-fuji-"))); + files.retain(|p| p.file_name().is_some_and(|n| n.to_string_lossy().starts_with("raf-fuji"))); } } files.sort(); diff --git a/docs/camera-preview-colour.md b/docs/camera-preview-colour.md index 7a1235ab3..a526f585a 100644 --- a/docs/camera-preview-colour.md +++ b/docs/camera-preview-colour.md @@ -36,7 +36,7 @@ If the original curve cannot be fitted, the wider curve can still be accepted af A near-black frame can have fewer than 256 colour pairs, so none of those attempts reaches tone fitting (#1587). After all existing looks fail, a tone-only fallback uses the wider signal floors above. At least 90% of the JPEG proxy must be below luminance 0.015. It excludes clipped sensor pixels from training and refuses invalid samples and JPEG highlights at or above 0.85. Every clipped pixel must stay at least as close to its JPEG luminance as the default curve leaves it. At least 256 usable pairs must show luminance rank correlation of 0.5 or more. Two thirds train a quantile curve; the held-out third must reduce squared luminance error by 30% in both linear and gamma-encoded values. This fallback keeps the source's spectral or neutral colour and learns no matrix, hue/saturation table or chroma curve. Earlier accepted looks keep their result. -The extra dark knots move the straight-line extension down toward black without a new serialized curve format or a different CPU/GPU interpolation rule. `LOOK_VERSION` 12 refreshes older fitted smart previews when their originals are online; `RENDER_CACHE_VERSION` 69 redraws cached renders. +The extra dark knots move the straight-line extension down toward black without a new serialized curve format or a different CPU/GPU interpolation rule. `LOOK_VERSION` 12 refreshes older fitted smart previews when their originals are online; `RENDER_CACHE_VERSION` 70 redraws cached renders. No process-version bump is needed under the [rule for changes to source data](process-versions.md#the-rule). The decoded source carries the fitted camera tone in `SourceInfo`; both V1 and V2 use it in `finish::base_tone`. Existing raws edited under V1 or V2 can therefore change when decoded again. Offline smart previews keep their stored pixels and tone until rebuilt from the original. The scan on library opening runs once per look version for each smart previews folder. If an original was offline at that scan, run Build Smart Previews after reconnecting it. @@ -192,8 +192,8 @@ For 52 camera models LightCraft has colour matrices fitted to the camera's measu [rawtoaces-data](https://github.com/AcademySoftwareFoundation/rawtoaces-data) v1.1.0, Apache-2.0): 22 Canon, 11 Nikon, 11 Sony, 3 Fujifilm and 5 others, plus the other names rawtoaces-data lists for them (EOS Rebel T3i and Kiss X5 for the EOS 600D; bodies it lists as sharing a sensor, such as the GFX100S with the GFX 100), and the bodies `SAME_SENSOR` -in `spectral.rs` adds by hand because the table is generated: Fujifilm's 26.1 MP X-Trans CMOS 4 bodies (X-T30, X-T30 II, -X-Pro3, X100V, X-S10, X-E4) take the X-T3's matrices. Without them an X-T30 raw whose camera JPEG was shot in Acros +in `spectral.rs` adds by hand because the table is generated: Fujifilm's 26.1 MP X-Trans CMOS 4 bodies take the X-T3's +(X-T30, X-T30 II, X-Pro3, X100V, X-E4, X-M5) or X-T4's (X-S10, X-S20) matrices. Without them an X-T30 raw whose camera JPEG was shot in Acros (black and white) got its tone curve on the muted neutral fallback, with a grey sky where the scene had a deep blue one. Per model the generator fits, under CIE Standard Illuminant A and under D65, a white-preserving 3×3 from white-balanced camera RGB to XYZ by the diff --git a/docs/fuji-lens-corrections.md b/docs/fuji-lens-corrections.md index 2546ef2cf..0a02545fb 100644 --- a/docs/fuji-lens-corrections.md +++ b/docs/fuji-lens-corrections.md @@ -28,8 +28,9 @@ Every table is an array of signed rationals with `n` knots: ``` - `scale × n` is the half diagonal of the recorded image (the RAF crop) in pixels: 3749 for 6240×4160, 4643 for - 7728×5152, 2942 for 4896×3264, 7280 for 11648×8736, on 61 of the 65 bodies. Files that store another number (X-S20, - X-T30 III and X-M5 write the 40 MP value, 4643; an X-T5 in an in-camera crop) are left uncorrected. + 7728×5152, 2942 for 4896×3264, 7280 for 11648×8736, on 61 of the 65 bodies. Files that store another number (X-T30 III and X-M5 write the 40 MP value, 4643; an X-T5 in an in-camera crop) + are left uncorrected. X-S20 writes the 40 MP reference value 4643 despite its 26 MP sensor, but indexes its 9 knots + over the 26 MP active frame; LightCraft accepts this scale for X-S20. - Knots are **radii in half diagonals**: the farthest corner is at 1, and a table is read at `ρ = r`, the distance from the centre in half diagonals. Older bodies use 11 knots `0, 0.1 … 1`, newer ones 9 knots evenly spaced in `r²` (`√(k/8)`), GFX 50 bodies 8 (`√(k/8)`, last 1). In the nine-knot tables the eighth knot is the corner and the ninth, diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index fc15d26e0..f90b9da21 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -212,7 +212,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-EDIT-DETAIL-SUPERRES | Super resolution | P2 | ⬜ | | | | LR-EDIT-DETAIL-AISHARPEN | AI sharpen | OOS | 🚫 | | | | LR-EDIT-OPTICS-CA | Remove chromatic aberration | P1 | ✅ | `crates/ui-egui/src/panels/edit.rs` (checkbox), `ctl:optics.caRed`, `ctl:optics.caBlue` | | -| LR-EDIT-OPTICS-PROFILE | Lens profile corrections | P1 | 🟡 | `ctl:optics.profileDistortion`, `ctl:optics.profileVignetting`, `crates/pipeline/src/optics.rs`, `crates/raw/src/vendor/rw2.rs`, `crates/raw/src/vendor/raf_lens.rs`, `crates/engine/src/crs.rs` | uses corrections embedded in the file: DNG `WarpRectilinear` / `WarpRectilinear2` (opcode 14, DNG 1.6: degree-14 radial polynomial, valid-radius clamp, reciprocal option, applied in list order with the spec's skip rule for a `WarpRectilinear` after an optional one, so Pixel DNGs' two-stage warp is corrected; up to 3 chained stages, sampled on the CPU because the polynomials need `f64`; issue #635) / `FixVignetteRadial`, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag `0x0119`, read as the equivalent `WarpRectilinear`; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's `LensProfileEnable=0` leaves them on, as Lightroom applies built-in corrections regardless (inferred); Sony ILCE-7RM4A Bayer ARW raw-IFD distortion tables (`0x7037`, 16-sample layout, native 3:2 framing) use the same warp path, enabled for the independently validated ILCE-7RM4A with FE 24–105 and FE 200–600 lenses, and ILCE-7CR full-frame Bayer 3:2 files with correction Auto or forced Auto (a separate corrected-radius conversion validated against camera JPEGs with FE 24-50mm, FE 16-25mm and FE 50mm lenses); A7CR Off, APS-C, aspect crops and lossless M/S remain uncorrected; ILCE-7RM6 verified against the camera JPEG on 54 photos with three lenses (`docs/sony-ilce7rm6-lens-corrections.md`); other Sony models remain uncorrected (see `docs/sony-lens-corrections.md`); Nikon NEF distortion from NikonNEFInfo (`0xc7d5`: polynomial, frame-filling zoom and the optical-centre offset left by in-body stabilisation; `crates/raw/src/vendor/nef_lens.rs`, `docs/nikon-lens-corrections.md`) for the Z 6II, Z 8 and Z f, full-frame image area only: F-mount lenses on the FTZ match the camera JPEG within 0.2 px median, Z lenses within 0.5–1.5 px except the 24-70 f/4's wide end, where the camera applies a further correction that is not in the readable data (7 px median at 24 mm, from 16 px / 106 px p95 uncorrected); Fujifilm RAF raw-IFD tables (`0xf00b` distortion, `0xf010` vignetting; `crates/raw/src/vendor/raf_lens.rs`, `docs/fuji-lens-corrections.md`) become a `WarpRectilinear` with the camera's frame-filling zoom and a `FixVignetteRadial` gain for 59 body models with sample files checked against the camera JPEG (the table indexed by the source radius with last knot at the corner; on 67 body/lens combinations the corrected render's radial shape error against the camera JPEG falls from a median of 0.72 % to 0.05 % of the half diagonal, largest 0.42 %, and the vignetting gain of the JPEG tracks the table at a median correlation of 0.987); files whose table scale is not the recorded image's half diagonal (X-S20, X-T30 III, X-M5, X-T5 crops) and six unmatched bodies stay uncorrected; Fujifilm's lateral CA table (`0xf00f`) is real but is not applied, because our demosaic already registers the planes and the stored shift over-corrects; vignetting and lateral CA data of Olympus raws not read yet; Sony ARW shading, lateral CA and distortion tables (raw IFD `0x7031`–`0x7037`, with the encrypted `SR2SubIFD` copies as a checked fallback) are decoded with their camera settings, provenance and framing evidence for every body that records them (`crates/raw/src/lens_metadata.rs`), but only the ILCE-7RM4A, ILCE-7RM6 and ILCE-7CR use the separate distortion conversions above; Sony shading and CA semantics are not validated beyond the ILCE-7RM6's (lateral CA off) (`docs/sony-lens-corrections.md`); Reload from Disk reads the file's lens data for photos catalogued without it and turns the correction on for those still at import settings (issue #314); lens-profile database: a lensfun-format XML database in `LIGHTCRAFT_LENSFUN_DIR` (none is bundled) supplies distortion (poly3 / poly5 / PTLens), TCA and vignetting for lenses it lists (`crates/lensdb`, `crates/engine/src/lens_profiles.rs`, `docs/lens-profiles.md`); raws that name no lens model (Nikon DSLRs: maker-note lens specification only) look it up by a name built from the specification (`24-70mm f/2.8`) and take only a profile named exactly that; no settings switch or bundled database yet | +| LR-EDIT-OPTICS-PROFILE | Lens profile corrections | P1 | 🟡 | `ctl:optics.profileDistortion`, `ctl:optics.profileVignetting`, `crates/pipeline/src/optics.rs`, `crates/raw/src/vendor/rw2.rs`, `crates/raw/src/vendor/raf_lens.rs`, `crates/engine/src/crs.rs` | uses corrections embedded in the file: DNG `WarpRectilinear` / `WarpRectilinear2` (opcode 14, DNG 1.6: degree-14 radial polynomial, valid-radius clamp, reciprocal option, applied in list order with the spec's skip rule for a `WarpRectilinear` after an optional one, so Pixel DNGs' two-stage warp is corrected; up to 3 chained stages, sampled on the CPU because the polynomials need `f64`; issue #635) / `FixVignetteRadial`, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag `0x0119`, read as the equivalent `WarpRectilinear`; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's `LensProfileEnable=0` leaves them on, as Lightroom applies built-in corrections regardless (inferred); Sony ILCE-7RM4A Bayer ARW raw-IFD distortion tables (`0x7037`, 16-sample layout, native 3:2 framing) use the same warp path, enabled for the independently validated ILCE-7RM4A with FE 24–105 and FE 200–600 lenses, and ILCE-7CR full-frame Bayer 3:2 files with correction Auto or forced Auto (a separate corrected-radius conversion validated against camera JPEGs with FE 24-50mm, FE 16-25mm and FE 50mm lenses); A7CR Off, APS-C, aspect crops and lossless M/S remain uncorrected; ILCE-7RM6 verified against the camera JPEG on 54 photos with three lenses (`docs/sony-ilce7rm6-lens-corrections.md`); other Sony models remain uncorrected (see `docs/sony-lens-corrections.md`); Nikon NEF distortion from NikonNEFInfo (`0xc7d5`: polynomial, frame-filling zoom and the optical-centre offset left by in-body stabilisation; `crates/raw/src/vendor/nef_lens.rs`, `docs/nikon-lens-corrections.md`) for the Z 6II, Z 8 and Z f, full-frame image area only: F-mount lenses on the FTZ match the camera JPEG within 0.2 px median, Z lenses within 0.5–1.5 px except the 24-70 f/4's wide end, where the camera applies a further correction that is not in the readable data (7 px median at 24 mm, from 16 px / 106 px p95 uncorrected); Fujifilm RAF raw-IFD tables (`0xf00b` distortion, `0xf010` vignetting; `crates/raw/src/vendor/raf_lens.rs`, `docs/fuji-lens-corrections.md`) become a `WarpRectilinear` with the camera's frame-filling zoom and a `FixVignetteRadial` gain for 59 body models with sample files checked against the camera JPEG (the table indexed by the source radius with last knot at the corner; on 67 body/lens combinations the corrected render's radial shape error against the camera JPEG falls from a median of 0.72 % to 0.05 % of the half diagonal, largest 0.42 %, and the vignetting gain of the JPEG tracks the table at a median correlation of 0.987); files whose table scale is not the recorded image's half diagonal (X-T30 III, X-M5, X-T5 crops; X-S20 supported) and six unmatched bodies stay uncorrected; Fujifilm's lateral CA table (`0xf00f`) is real but is not applied, because our demosaic already registers the planes and the stored shift over-corrects; vignetting and lateral CA data of Olympus raws not read yet; Sony ARW shading, lateral CA and distortion tables (raw IFD `0x7031`–`0x7037`, with the encrypted `SR2SubIFD` copies as a checked fallback) are decoded with their camera settings, provenance and framing evidence for every body that records them (`crates/raw/src/lens_metadata.rs`), but only the ILCE-7RM4A, ILCE-7RM6 and ILCE-7CR use the separate distortion conversions above; Sony shading and CA semantics are not validated beyond the ILCE-7RM6's (lateral CA off) (`docs/sony-lens-corrections.md`); Reload from Disk reads the file's lens data for photos catalogued without it and turns the correction on for those still at import settings (issue #314); lens-profile database: a lensfun-format XML database in `LIGHTCRAFT_LENSFUN_DIR` (none is bundled) supplies distortion (poly3 / poly5 / PTLens), TCA and vignetting for lenses it lists (`crates/lensdb`, `crates/engine/src/lens_profiles.rs`, `docs/lens-profiles.md`); raws that name no lens model (Nikon DSLRs: maker-note lens specification only) look it up by a name built from the specification (`24-70mm f/2.8`) and take only a profile named exactly that; no settings switch or bundled database yet | ) | LR-EDIT-OPTICS-DEFRINGE | Defringe | P1 | ✅ | `ctl:optics.defringe*` | no fringe eyedropper | | LR-EDIT-OPTICS-MANUAL | Manual distortion / vignetting | P1 | ✅ | `ctl:optics.distortion`, `ctl:optics.vignetting`, `ctl:optics.vignettingMidpoint` | | diff --git a/docs/raw-parity.md b/docs/raw-parity.md index 0dd11e205..7f047e599 100644 --- a/docs/raw-parity.md +++ b/docs/raw-parity.md @@ -214,7 +214,7 @@ sample files. A pointer for contributors, not legal advice. | Item | Adobe | LightCraft | |---|---|---| -| Default profile per model | Adobe Standard for every model (1,500 profile names in the bundle) | the DNG's own matrices; else a pooled profile (**5 bundled**: ILCA-99M2, ILCE-7CR, ILCE-7M4, X-H2S, X-T4); else a guarded per-file fit to the camera's embedded JPEG; else spectral matrices (**52 models**, rawtoaces-data, and the bodies sharing their sensors: 6 X-Trans CMOS 4 Fujifilm bodies take the X-T3's); else a neutral matrix | +| Default profile per model | Adobe Standard for every model (1,500 profile names in the bundle) | the DNG's own matrices; else a pooled profile (**5 bundled**: ILCA-99M2, ILCE-7CR, ILCE-7M4, X-H2S, X-T4); else a guarded per-file fit to the camera's embedded JPEG; else spectral matrices (**52 models**, rawtoaces-data, and the bodies sharing their sensors: 6 Fujifilm bodies take the X-T3's (X-T30, X-T30 II, X-Pro3, X100V, X-E4, X-M5) and 2 take the X-T4's (X-S10, X-S20)); else a neutral matrix | | Camera Matching looks | 481 models (plus phone-maker looks) | none (the per-file JPEG fit imitates the camera's own look) | | Creative profiles | Adobe Color, Portrait, Landscape, Vivid, Monochrome… | our own looks (Color, Neutral, Vivid, Landscape, Portrait, Monochrome) | | DCP / dual-illuminant model | ✅ | dual-illuminant DNG colour (ColorMatrix / ForwardMatrix 1-2, CameraCalibration, AnalogBalance), HueSatMap, LookTable, ProfileToneCurve; external `.dcp` never loaded (clean-room) |