From e1ea92349a2c01c8fc03b9074de305134deae8d7 Mon Sep 17 00:00:00 2001 From: Julian Zahnow <54999052+Enthendor@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:05:43 +0200 Subject: [PATCH] LR-EDIT-OPTICS-PROFILE: Sony ILCE-6700 in-file distortion The ILCE-7RM6 conversion extends to the APS-C ILCE-6700: its 11 knots span the file's own default crop (half diagonal / (count - 1) = 372.09 px), with the same unit 15000, index and zoom rule. APS-C bodies use a 0.5 px fit gate: the polynomial matches the camera JPEG as well as the exact table, while the 0.25 px gate refused 13 of 30 tables. Measured on 30 photos with three lenses against the camera JPEG: p95 0.22-0.73 px on 27 photos as rendered, 1.36-1.38 px at the Sigma 10-18 mm's 10 mm end (uncorrected up to 5.6 px). Co-Authored-By: Claude Opus 5.5 --- crates/raw/src/vendor/arw_lens_rm6.rs | 94 ++++++++++++++++++++++---- docs/gaps.md | 5 +- docs/raw-parity.md | 5 +- docs/sony-ilce7rm6-lens-corrections.md | 45 ++++++++++-- 4 files changed, 129 insertions(+), 20 deletions(-) diff --git a/crates/raw/src/vendor/arw_lens_rm6.rs b/crates/raw/src/vendor/arw_lens_rm6.rs index cc3a23e61..9b8afd777 100644 --- a/crates/raw/src/vendor/arw_lens_rm6.rs +++ b/crates/raw/src/vendor/arw_lens_rm6.rs @@ -23,7 +23,7 @@ //! crop and reads only inside it: the largest `S(ρ/z)/ρ` over the crop border radii `ρ` is 1. 0.596 px over the 34 //! measurable corrected photos (budget 0.75 px; corners and edge midpoints 0.570 px). //! - `WarpRectilinear`'s 4-term odd polynomial fits that map within 2.35·10⁻⁴ of the DNG radius (0.220 px) on every -//! photo, at its 65 fit samples. A table it fits worse than 0.25 px at the samples the crop shows is refused (the +//! photo, at its 65 fit samples. A table it fits worse than 0.25 px (0.5 px on an APS-C body) at the samples the crop shows is refused (the //! photo stays uncorrected rather than wrongly warped). Between samples the error is somewhat larger: 0.071 against //! 0.049 px for the full-frame 16–35 mm example in the tests. //! - vignetting: read but never applied. The camera JPEGs keep the uncorrected render's radial falloff (median @@ -37,10 +37,12 @@ //! them on in code, pass `true` there. When on, a CA value beyond [`CA_MAX_RELATIVE`] or a colour plane that fails //! the fit gate above drops both colour planes, and G alone is applied to all three channels. //! -//! The correction is produced whatever the in-camera flag says. Only bodies listed in [`FULL_FRAME_CROP`] get it: the -//! unit and the knot spacing are verified on the ILCE-7RM6 alone. Another body is added by repeating the measurement -//! on its photos shot with distortion correction on and listing its full-frame default crop. The method and the -//! numbers: docs/sony-ilce7rm6-lens-corrections.md. +//! The correction is produced whatever the in-camera flag says. Only bodies listed in [`FULL_FRAME_CROP`] or +//! [`APSC_BODIES`] get it: the unit and the knot spacing are verified on the ILCE-7RM6 and, for distortion, on the +//! ILCE-6700 (APS-C, 11 knots spanning its own crop, the same unit 15000: 0.73 px worst over 27 photos with three +//! lenses as rendered, 1.38 px at the 10–18 mm's 10 mm end). Another body is added by repeating the measurement on its photos shot +//! with distortion correction on and listing its full-frame default crop (or, for an APS-C body, its model). The method +//! and the numbers: docs/sony-ilce7rm6-lens-corrections.md. use super::fit_radial_warp; use crate::opcodes::max_radius; @@ -57,6 +59,9 @@ pub(crate) const CA_FLAG: u16 = 0x7034; /// The bodies whose tables are verified (Exif `Model`), with their full-frame default crop (width, height) in px. const FULL_FRAME_CROP: &[(&str, (f64, f64))] = &[("ILCE-7RM6", (9984.0, 6656.0))]; +/// The verified APS-C bodies (Exif `Model`): their knots span the file's own default crop, the last knot at its corner. +/// Measured on the ILCE-6700 (docs/sony-ilce7rm6-lens-corrections.md → ILCE-6700). +const APSC_BODIES: &[&str] = &["ILCE-6700"]; /// The distortion table's value of 1 (relative radius); a global least-squares fit over the 34 photos gives 15000.7. const DISTORTION_UNIT: f64 = 15000.0; /// The CA table's value of 1 (relative to the green source radius). Provisional: the median best unit of the 20 @@ -70,6 +75,10 @@ const CA_MAX_RELATIVE: f64 = 2e-3; const ZOOM_SCAN: usize = 4096; /// Largest polynomial fit error accepted at the fit samples the crop shows, px at a 1440 px long edge. const FIT_ERROR_PX_AT_1440: f64 = 0.25; +/// The fit gate of [`APSC_BODIES`]: their 11 knots over a smaller crop bend more sharply than the polynomial follows +/// (worst 0.44 px over the 30 measured ILCE-6700 tables; 13 of them exceed 0.25 px), while the fitted polynomial +/// matches the camera JPEG as well as the exact table (p95 0.22–0.73 px against 0.23–0.68 px, 10 mm aside). +const APSC_FIT_ERROR_PX_AT_1440: f64 = 0.5; /// A file's lens tables: the first `count` values of each (CA: red, blue); `None` when absent or malformed. /// `vignetting` and `flags` are parsed for the tests and the docs only: don't build an opcode from them (the camera @@ -106,10 +115,19 @@ pub(crate) fn read_tables(raw: &Ifd) -> LensTables { } } -/// The knot spacing (px) of a verified body: its full-frame default-crop half diagonal / 15. -fn knot_spacing(model: &str) -> Option { +/// The knot spacing (px) of a verified body: a full-frame body's full-frame default-crop half diagonal / 15 (fixed on +/// the sensor, so in its APS-C crop mode too); an APS-C body's default-crop `crop` half diagonal / (`knots` − 1). Both +/// put the last knot at the corner of the crop the table was written for (the ILCE-7RM6's APS-C mode, 11 knots, gives +/// 392 px that way, which fits its files as well as the sensor-fixed 400 px). `None` for an unverified body, or an +/// APS-C body with fewer than two knots or an empty crop. +fn knot_spacing(model: &str, knots: usize, crop: Rect) -> Option { let model = model.trim(); - FULL_FRAME_CROP.iter().find(|(m, _)| *m == model).map(|&(_, (w, h))| w.hypot(h) / 30.0) + if let Some(&(_, (w, h))) = FULL_FRAME_CROP.iter().find(|(m, _)| *m == model) { + return Some(w.hypot(h) / 30.0); + } + let gaps = knots.checked_sub(1).filter(|&n| n > 0)?; + let half_diagonal = (crop.width as f64).hypot(crop.height as f64) / 2.0; + (APSC_BODIES.contains(&model) && half_diagonal > 0.0).then(|| half_diagonal / gaps as f64) } /// The knot table at radius `r` px: knot `i` at `i · spacing`, linear in between, the last segment extended beyond @@ -161,6 +179,11 @@ fn fill_zoom(source: &dyn Fn(f64) -> f64, x: f64, y: f64) -> Option { /// worse than 0.25 px at a 1440 px long edge at one of the fit samples within the crop (a padded active area, as in /// lossless files, reaches radii that are never shown). pub(crate) fn warp_opcode(t: &LensTables, knot_spacing: f64, active: Rect, crop: Rect, lateral_ca: bool) -> Option { + gated_warp_opcode(t, knot_spacing, active, crop, lateral_ca, FIT_ERROR_PX_AT_1440) +} + +/// [`warp_opcode`] with the fit gate `fit_error_px` (px at a 1440 px long edge) instead of 0.25 px. +fn gated_warp_opcode(t: &LensTables, knot_spacing: f64, active: Rect, crop: Rect, lateral_ca: bool, fit_error_px: f64) -> Option { let v = t.distortion.as_deref().filter(|v| v.iter().any(|&x| x != 0.0))?; let (aw, ah) = (active.width as f64, active.height as f64); if aw < 2.0 || ah < 2.0 || crop.width == 0 || crop.height == 0 { @@ -172,7 +195,7 @@ pub(crate) fn warp_opcode(t: &LensTables, knot_spacing: f64, active: Rect, crop: // DNG's normalisation: pixel-index units, radius 1 = the farthest active-area corner let (cx, cy) = (crop.x as f64 + (cw - 1.0) / 2.0, crop.y as f64 + (ch - 1.0) / 2.0); let m = max_radius(cx, cy, aw - 1.0, ah - 1.0); - let max_fit_error = FIT_ERROR_PX_AT_1440 * (cw.max(ch) / 1440.0) / m; + let max_fit_error = fit_error_px * (cw.max(ch) / 1440.0) / m; let shown = (cw / 2.0).hypot(ch / 2.0) / m; // output radius (px) → green source radius let green = |r: f64| source(r / z); @@ -204,8 +227,15 @@ pub(crate) fn warp_opcode(t: &LensTables, knot_spacing: f64, active: Rect, crop: /// CA went from rms 0.032 / max 0.125 px (off) to 0.052 / 0.219 px (on) at a 1440 px long edge; only the 16–35 mm /// improved. Follow-up: warp the CFA colour planes before demosaic, then turn the planes on here. pub(crate) fn lens_opcodes(raw: &Ifd, model: &str, active: Rect, crop: Rect) -> Vec { - let Some(spacing) = knot_spacing(model) else { return Vec::new() }; - warp_opcode(&read_tables(raw), spacing, active, crop, false).into_iter().collect() + let tables = read_tables(raw); + let knots = tables.distortion.as_ref().map_or(0, Vec::len); + let Some(spacing) = knot_spacing(model, knots, crop) else { return Vec::new() }; + let op = if APSC_BODIES.contains(&model.trim()) { + gated_warp_opcode(&tables, spacing, active, crop, false, APSC_FIT_ERROR_PX_AT_1440) + } else { + warp_opcode(&tables, spacing, active, crop, false) + }; + op.into_iter().collect() } #[cfg(test)] @@ -253,7 +283,7 @@ mod tests { const APSC_B: [f64; 4] = [1.016155702, -0.06162477627, 0.02753315326, -0.005554915901]; fn knots() -> f64 { - knot_spacing("ILCE-7RM6").unwrap() + knot_spacing("ILCE-7RM6", 16, FF_CROP).unwrap() } fn ifd(tags: Vec<(u16, Value)>) -> Ifd { @@ -525,6 +555,46 @@ mod tests { assert!(matches!(op, Some(Opcode::WarpRectilinear { .. })), "{op:?}"); } + /// An ILCE-6700 file (Sigma 10–18 mm at 10 mm): 11 knots, the strongest table of the measured set. + const A6700_DISTORTION: [i16; 17] = [11, 90, 11, -90, -205, -345, -500, -676, -852, -1031, -1190, -1333, -1333, -1333, -1333, -1333, -1333]; + const A6700_CROP: Rect = Rect { x: 0, y: 0, width: 6192, height: 4128 }; + /// An ILCE-6700 file (Tamron 17–70 mm at 32 mm), on the decoded geometry: the polynomial fits it within 0.44 px. + const A6700_32MM_DISTORTION: [i16; 17] = [11, 7, 0, 7, 29, 58, 101, 158, 230, 323, 446, 611, 611, 611, 611, 611, 611]; + const A6700_ACTIVE: Rect = Rect { x: 0, y: 0, width: 6656, height: 4608 }; + const A6700_DECODED_CROP: Rect = Rect { x: 26, y: 20, width: 6192, height: 4128 }; + + #[test] + fn apsc_body_knots_span_its_own_crop() { + // half diagonal 3720.9 px / 10 + let s = knot_spacing("ILCE-6700", 11, A6700_CROP).unwrap(); + assert!((s - 372.0929).abs() < 1e-3, "{s}"); + // the full-frame body keeps its sensor-fixed spacing whatever the crop and count + assert_eq!(knot_spacing("ILCE-7RM6", 11, APSC_CROP), knot_spacing("ILCE-7RM6", 16, FF_CROP)); + // degenerate counts and crops give no spacing instead of a division by zero + assert_eq!(knot_spacing("ILCE-6700", 1, A6700_CROP), None); + assert_eq!(knot_spacing("ILCE-6700", 0, A6700_CROP), None); + assert_eq!(knot_spacing("ILCE-6700", 11, Rect { x: 0, y: 0, width: 0, height: 0 }), None); + let ifd = sony_lens_ifd(&A6700_DISTORTION, &APSC_VIGNETTING, &APSC_CA, [1, 2, 1]); + let ops = lens_opcodes(&ifd, "ILCE-6700", A6700_CROP, A6700_CROP); + let gated = gated_warp_opcode(&read_tables(&ifd), s, A6700_CROP, A6700_CROP, false, APSC_FIT_ERROR_PX_AT_1440); + assert_eq!(ops, gated.into_iter().collect::>()); + assert_eq!(ops.len(), 1); + // other APS-C bodies stay unverified + assert!(lens_opcodes(&ifd, "ILCE-6600", A6700_CROP, A6700_CROP).is_empty()); + } + + /// The APS-C fit gate: a table the polynomial fits within 0.44 px is applied on the ILCE-6700, while the + /// full-frame 0.25 px gate refuses it. + #[test] + fn apsc_body_uses_its_wider_fit_gate() { + let ifd = sony_lens_ifd(&A6700_32MM_DISTORTION, &APSC_VIGNETTING, &APSC_CA, [1, 2, 1]); + let t = read_tables(&ifd); + let s = knot_spacing("ILCE-6700", 11, A6700_DECODED_CROP).unwrap(); + assert_eq!(warp_opcode(&t, s, A6700_ACTIVE, A6700_DECODED_CROP, false), None); + let ops = lens_opcodes(&ifd, "ILCE-6700", A6700_ACTIVE, A6700_DECODED_CROP); + assert!(matches!(ops.as_slice(), [Opcode::WarpRectilinear { planes, .. }] if planes.len() == 1), "{ops:?}"); + } + #[test] fn unknown_model_gets_no_opcodes() { let ifd = sony_lens_ifd(&FF_DISTORTION, &FF_VIGNETTING, &FF_CA, [0, 2, 1]); diff --git a/docs/gaps.md b/docs/gaps.md index be411a205..738979572 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Gaps: where LightCraft falls short of Lightroom -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-21 · **Change:** minor (gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains) +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-23 · **Change:** minor (gap 8: Sony ILCE-7RM6 in-file distortion conversion extends to the APS-C ILCE-6700; gap 28: F8 shows / hides the right panel; import review album hierarchy, #2362; gap 20: Minolta MRW decodes; gap 10: 551 models verified; gap 15: Print Page Options — page numbers, page info, crop marks; gap 15: Print Page Options — page numbers, page info, crop marks; gap 28: `\` shows / hides the Filter Bar in the grids; gap 21: Indonesian interface shipped, every message but What's New; gap 27: the Windows x64 installer verified on Windows 11; the Settings → Apps uninstall confirmation remains) Every known shortfall, one entry each, ranked by user impact (how many Lightroom users hit it, and how hard it blocks them), then by effort. **Agents pick work from the top** (the first entry nobody is @@ -22,7 +22,7 @@ Hours are Opus 5.5 agent wall-clock hours (see [target-app-parity.md](target-app | 5 B | **AI masks: Subject, Sky, People, Background, Landscape** are heuristics; Object / Describe run SAM 3 but its download has no mirror configured | `crates/segment`; ai-masks.md; MASK rows 67% | masking is how most Lightroom users edit today | 40–70 + model decision | [target-app-parity](target-app-parity.md#ai) | | 6 B | **Compressed ORF** (every Olympus / OM body since ~2008) | measured: 66 of 78 Olympus / OM models preview-only; `orf.rs` refuses; OM bucket 3% of photographers, 3% of them fully work | +2.6 points; best gain per hour of any raw gap (~0.2) | 8–15 | [raw-parity](raw-parity.md#compressed-orf-a-clean-room-starting-point) (the two Olympus patents that describe the method) | | 7 | **Nikon High Efficiency NEF** (HE / HE★ on Z 8, Z 9, Z 6III, Z f, Z50II, Z5II when set to HE) | named and preview only (#193); needs a pure-Rust JPEG XS decoder | +1.1 points (only shots taken in HE; lossless works on the same bodies); low gain per hour (~0.04), so not a beta blocker | 20–40 | [raw-parity](raw-parity.md) | -| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A, full-frame ILCE-7CR distortion (Auto only) and ILCE-7RM6 distortion (`docs/sony-ilce7rm6-lens-corrections.md`) and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) | +| 8 B | **No lens profile database** (LR-EDIT-OPTICS-PROFILE): only DNG opcodes, RW2 in-file distortion, Sony ILCE-7RM4A, full-frame ILCE-7CR distortion (Auto only), ILCE-7RM6 and ILCE-6700 distortion (`docs/sony-ilce7rm6-lens-corrections.md`) and Nikon Z 6II / Z 8 / Z f distortion (`docs/nikon-lens-corrections.md`) | Adobe ships 3,635 lens profiles; we never use them | distortion / vignetting correction is a default step for many | 40–80 + calibration shots | [raw-parity](raw-parity.md#lens-profiles) | | 9 B | **Tests don't run in CI on macOS, Windows or Linux x64**: only FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU | regressions reach users on the main platforms; `cargo xtask ci` runs only locally | 6–12 | [target-app-parity](target-app-parity.md#by-dimension) | | 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 551 of 1,446 Adobe models verified (38%), ~85% of photographers on a verified body; Nikon 5 bodies verified, Canon 34, Leica 14 (the Q3 by a recorded check of 38 private DNGs). Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 regression (#702); F828 SRF now discovered and imported as preview-only, sensor decoding still missing, DSLR-A200…A390 / A850 12-bit ARW (#535) | measured: [raw/coverage.tsv](raw/coverage.tsv) | per-body bugs (levels, crops, WB) surface only from users | 25–45 | [raw-parity](raw-parity.md#by-maker-measured) | | 11 | **Enhance: Raw Details, Super Resolution; AI denoise without shippable weights or X-Trans** | ENH rows 0%; denoise Bayer only, RawNIND weights GPL and opt-in | Fujifilm users get no AI denoise; nobody gets it out of the box | 30–60 + model decision | [raw-parity](raw-parity.md#demosaic-levels-crops-and-lens-metadata) | @@ -79,6 +79,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w | Date | Change | Summary | |---|---|---| +| 2026-10-23 | minor | Gap 8 narrows: Sony ILCE-6700 (APS-C) in-file distortion through the ILCE-7RM6 conversion, knots spanning its own crop (30 photos, 3 lenses; 0.73 px worst except 1.38 px at 10 mm); the gap stays open (no lens database), hours unchanged | | 2026-10-21 | minor | Gap 28 shrinks: F8 shows / hides the right panel (`view.rightPanel`); rating `[` `]`, flag cycling and F1 help were already bound, so the entry lists only filter-bar keys, Tab / ⇧Tab and F5; 2–4 h (was 4–8) | | 2026-10-20 | minor | #2362: import destination albums use the shared hierarchy/search picker instead of a flat name list; existing import workflows retained; ranked gaps/readiness counts unchanged | | 2026-10-19 | minor | Gap 21: Indonesian (`id`) shipped with every catalog message but What's New; four new languages left (Hindi, Arabic, Korean, Vietnamese), 135–239 h | diff --git a/docs/raw-parity.md b/docs/raw-parity.md index 2923ea880..f93f98e25 100644 --- a/docs/raw-parity.md +++ b/docs/raw-parity.md @@ -1,6 +1,6 @@ # RAW parity: cameras, formats, demosaic, colour and lens profiles -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-18 · **Change:** minor (Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-23 · **Change:** minor (Sony ILCE-6700 in-file distortion; Minolta MRW decoded, 6 bodies verified; lossy JPEG XL DNG tiles with a non-linear declared transfer curve decode; Canon ColorData Kelvin; X-Trans CMOS 4 bodies take the X-T3's spectral matrices) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 (Camera Raw 18.7) Raw processing is the deepest single dimension of Lightroom parity: a photographer whose camera does not decode, or decodes with the wrong colour, cannot switch. This document measures it model by @@ -243,7 +243,7 @@ essentials user (the default look is what they keep). | Item | Adobe | LightCraft | |---|---|---| | Profile database | 3,635 profile files, 56 makers, auto-selected | **0** (Adobe LCPs never used; no database of our own) | -| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses), full-frame Bayer 3:2 ILCE-7CR files with correction Auto or forced Auto (3 lenses; see sony-lens-corrections.md for residuals and limits) and the ILCE-7RM6 (3 lenses, 54 photos; `sony-ilce7rm6-lens-corrections.md`); Nikon NikonNEFInfo distortion (tag `0xc7d5`, about the sensor-shifted optical centre) for the Z 6II and Z f: F-mount lenses on the FTZ within 0.2 px of the camera JPEG, Z lenses within 0.5–1.5 px except the 24-70's wide end (7 px median; `docs/nikon-lens-corrections.md`) | +| In-file corrections | ✅ | DNG opcodes (WarpRectilinear, FixVignetteRadial, GainMap…) applied; Panasonic / Leica RW2 distortion (79 photos, 13 bodies, 11 lenses); Sony tag 0x7037 distortion for the ILCE-7RM4A (2 lenses), full-frame Bayer 3:2 ILCE-7CR files with correction Auto or forced Auto (3 lenses; see sony-lens-corrections.md for residuals and limits) the ILCE-7RM6 (3 lenses, 54 photos) and the APS-C ILCE-6700 (3 lenses, 30 photos; 1.38 px at a 10 mm wide end, 0.73 px elsewhere; `sony-ilce7rm6-lens-corrections.md`); Nikon NikonNEFInfo distortion (tag `0xc7d5`, about the sensor-shifted optical centre) for the Z 6II and Z f: F-mount lenses on the FTZ within 0.2 px of the camera JPEG, Z lenses within 0.5–1.5 px except the 24-70's wide end (7 px median; `docs/nikon-lens-corrections.md`) | | Vignetting / lateral CA from maker notes | ✅ | ⬜ | | Manual distortion, vignetting, CA, defringe | ✅ | ✅ | @@ -296,6 +296,7 @@ Beyond decoding: | Date | Change | Summary | |---|---|---| +| 2026-10-23 | minor | Lens profiles: Sony ILCE-6700 (APS-C) in-file distortion through the ILCE-7RM6 conversion, knots spanning its own crop; lens score unchanged (~10%: one more body) | | 2026-10-18 | patch | X-T30, X-T30 II, X-Pro3, X100V, X-S10 and X-E4 take the X-T3's spectral matrices (same 26.1 MP X-Trans CMOS 4 sensor): an X-T30 raw with a black-and-white (Acros) camera JPEG no longer opens in the muted neutral fallback | | 2026-10-16 | minor | Kelvin white balance from Canon CR2 `ColorData` presets (PowerShot G1 X Mark III) | | 2026-10-15 | minor | DNGs with a look table and no tone curve (Lightroom-written, Adobe Standard) fit their tone to the embedded preview, also Lightroom's 1024 px one: two lossy ILCE-7CM2 DNGs no longer render 0.8 EV dark and flat | diff --git a/docs/sony-ilce7rm6-lens-corrections.md b/docs/sony-ilce7rm6-lens-corrections.md index d59723ac4..73d11b025 100644 --- a/docs/sony-ilce7rm6-lens-corrections.md +++ b/docs/sony-ilce7rm6-lens-corrections.md @@ -5,7 +5,8 @@ chromatic aberration (CA). LightCraft reads them and turns distortion into a DNG reaches the same **Optics ▸ Enable Lens Corrections** (`optics.lens_profile`) as a DNG's embedded warp and Panasonic's RW2 distortion data. Lateral CA is parsed and converts to the warp's red and blue planes, but it is **off by default**: applied after demosaic it made the render's CA worse (Verification). Vignetting is read but not applied (below). The -conversion is verified on the **ILCE-7RM6** only; the ILCE-7RM4A has its own conversion, with other conventions +conversion is verified on the **ILCE-7RM6** and, for distortion, the **ILCE-6700** ([ILCE-6700](#ilce-6700)); the +ILCE-7RM4A has its own conversion, with other conventions ([Two conventions](#two-conventions)), and other Sony bodies' tables give no correction until they are verified. The correction is on by default for new imports, as for any file with embedded lens data (`Photo::camera_defaults`). @@ -109,6 +110,40 @@ correction on (several lenses and focal lengths), repeat the camera-JPEG registr render, confirm the unit, the knot spacing and the zoom rule (or measure the body's own), then add its full-frame default crop to the table with a test. +APS-C bodies are listed in `APSC_BODIES` instead: today `ILCE-6700`. Their knots span the file's own default crop +(spacing = its half diagonal / (knot count − 1)), the rule that also gives the ILCE-7RM6 its 400 px in full frame (16 +knots) and, within the measurement, in its APS-C mode (11 knots: 392 px, which fits as well as 400 px). + +## ILCE-6700 + +The ILCE-6700 (APS-C, 6192 × 4128 default crop) writes 11-knot tables like the ILCE-7RM6's APS-C mode. Measured as +above on 30 photos shot with distortion correction Auto: Sigma 10–18 mm F2.8 DC DN (9 photos, 10–18 mm), Tamron +17–70 mm F2.8 B070 (13, 17–70 mm) and Sony E 70–350 mm G (8, 70–350 mm), all private, only the summary numbers here. +The convention is the ILCE-7RM6's (corrected-radius index, unit 15000, frame-filling zoom), with the knot spacing +the APS-C half diagonal / 10 = 372.09 px: + +| | p95 residual at 1440 px | +|---|---| +| Uncorrected | 3.8–5.6 px (19 photos; on the other 11 the shift exceeds the method's ±20 px search) | +| Sensor-fixed 400 px (the ILCE-7RM6's pixel spacing) | 0.56–3.71 px (passes only at 10 mm) | +| **Half diagonal / 10 = 372 px, unit 15000** (exact table) | **0.23–0.68 px on 27 photos (median 0.43); 1.34–1.42 px on the three 10 mm photos** | +| The same as LightCraft renders it (`lightcraft-cli render`, Enable Lens Corrections on, fitted polynomial) | **0.22–0.73 px on 27 photos (median 0.44); 1.36–1.38 px at 10 mm** | + +Units 14000–17000 and a reading that drops the first table value as a non-knot (10 knots over the half diagonal, +413 px) were also scanned; none keeps all 30 within 0.75 px, and 15000 gives the lowest worst case at 372 px. The +residual is radial (tangential median ≤ 0.02 px, so the centre is right) and has the same shape on every lens, +positive at mid radii and negative at the corners, growing with the table's magnitude (10 mm: last knot −1333, 1.3 px +at the corners). Fitted per photo, the best spacing and unit trade off against each other and drift with the lens +(10 mm about 400 px, 18 mm about 365 px): the same lens-dependent effect as the per-lens units above. The 10 mm end +stays within the 0.5–1.5 px accepted for Nikon Z lenses (`docs/nikon-lens-corrections.md`) and removes about 75 % of +the uncorrected 5.6 px. + +**Fit gate.** The 11 knots over the smaller crop bend more sharply than `WarpRectilinear`'s polynomial follows: on the +decoded geometry (active area 6656 × 4608) it fits the 30 tables within 0.08–0.44 px, and 13 of them exceed the +full-frame 0.25 px gate. Measured against the camera JPEG, the fitted polynomial is as good as the exact table (p95 +0.22–0.73 px against 0.23–0.68 px on the 27 photos, 1.35–1.38 px at 10 mm), so APS-C bodies use a 0.5 px gate +(`APSC_FIT_ERROR_PX_AT_1440`). + ## Two conventions A second Sony decoder, `crates/raw/src/vendor/arw_lens.rs` @@ -287,10 +322,12 @@ the resampled source by the source, framing and output size, so tone, colour and ## Limits -- **ILCE-7RM6 only.** Other Sony bodies record the same tags; their knot spacing (defined here by the full-frame crop), - unit and zoom rule are unverified, so they get no correction from this conversion (the ILCE-7RM4A's distortion has +- **ILCE-7RM6 and ILCE-6700 only.** Other Sony bodies record the same tags; their knot spacing (defined here by the + full-frame crop, or an APS-C body's own crop), unit and zoom rule are unverified, so they get no correction from this conversion (the ILCE-7RM4A's distortion has its own: [Two conventions](#two-conventions)). -- **APS-C distortion is unverified in camera:** no APS-C photo with distortion correction on was available; the APS-C +- **ILCE-6700 at strong barrel distortion:** 1.36–1.38 px at the Sigma 10–18 mm's 10 mm end (above); its vignetting + and lateral CA tables are not measured. +- **APS-C distortion is unverified in camera on the ILCE-7RM6:** no APS-C photo with distortion correction on was available; the APS-C knot spacing is verified on CA only, and the APS-C zoom follows the full-frame rule. - **Aspect-ratio crops** (in-camera 4:3, 16:9, 1:1) were not measured; the correction uses the crop the file records. - **One distortion unit for every lens.** Fitted per lens the unit is 14826 (24–70 mm, 23 photos, very consistent),