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),