Merge PR #2732: LR-EDIT-OPTICS-PROFILE: Sony ILCE-6700 in-file distortion

docs/gaps.md: main's header, gap 9 and revision rows kept; gap 8 row and header carry the ILCE-6700 mention; the PR's revision row added.
This commit is contained in:
aiongg
2026-10-31 02:08:57 +00:00
4 changed files with 145 additions and 26 deletions
+85 -12
View File
@@ -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<f64> {
/// 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 a crop that isn't 3:2 (the only shape measured; empty crops included).
fn knot_spacing(model: &str, knots: usize, crop: Rect) -> Option<f64> {
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) && super::arw_lens::is_3_2(crop)).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<f64> {
/// 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<Opcode> {
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<Opcode> {
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<Opcode> {
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,49 @@ 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);
// the spacing comes from the crop, so a crop of another shape than the measured 3:2 gets no correction
let square = Rect { width: A6700_CROP.height, ..A6700_CROP };
assert_eq!(knot_spacing("ILCE-6700", 11, square), 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::<Vec<_>>());
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]);
+3 -2
View File
@@ -1,6 +1,6 @@
# Gaps: where LightCraft falls short of Lightroom
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (LR-META-EXIF-FULL rejects extra EXIF clock components and preserves date fallbacks; gap 8: lens profile work is tracked in #329; gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; 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 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; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-25 · **Change:** minor (gap 8: Sony ILCE-7RM6 in-file distortion conversion extends to the APS-C ILCE-6700; LR-META-EXIF-FULL rejects extra EXIF clock components and preserves date fallbacks; gap 8: lens profile work is tracked in #329; gap 30: the Info panel shows the selected photos' shared metadata and marks mixed fields; gap 28: ⌘⌥⇧C / ⌘⌥⇧V copy and paste metadata; gap 13: before a Lightroom catalog import the app asks merge or new library, #2672; 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 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; gap 28: ⌘L Enable Filters switches the filter off and on; gap 15: Japanese photo and postcard paper sizes in the Print page setup and PDF)
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`); tracked in #329 | 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) and 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`); tracked in #329 | 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 or Windows**: Linux x64 runs `cargo xtask ci` on every PR and push to main (`.github/workflows/ci.yml`) and FreeBSD runs `cargo test`; the others build packages | `.github/workflows/` (ci, freebsd, packaging-lint, release, windows-arm64); the linux-riscv64 release job added in 0.6.0 (#844) runs only `lightcraft-cli --version` under QEMU; macOS / Windows jobs: #1148 | regressions in `cfg(target_os)` code reach macOS and Windows users | 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) |
@@ -94,6 +94,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| 2026-10-22 | minor | Gap 15: the Print page setup and PDF gain the Japanese photo and postcard paper sizes (L判 89×127, 2L判 127×178, KG判 102×152, はがき 100×148, 往復はがき 148×200, 六切 203×254, 四切 254×305, 名刺 55×91 mm), by their customary names; gap not closed |
| 2026-10-22 | minor | #2052: Thai glyphs added to shared export text; gap 21 retains Thai mark positioning, Arabic joining and bidi limits. No ranked gap closes; estimates unchanged |
| 2026-10-24 | minor | LR-LIB-SMARTALBUM: relative-date views and source totals expire on the session-clock minute, including nested smart albums; ranked gaps and readiness counts unchanged |
| 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-22 | patch | XMP namespace-prefix collisions no longer overwrite foreign metadata; ranked gaps and estimates unchanged |
| 2026-10-22 | patch | Issue #2519: PSD/PSB EXIF resource 1058 and XMP resource 1060 now use the common metadata extractor, and `.psb` files are accepted by import; no ranked gap or readiness count changes |
| 2026-10-22 | minor | Gap 30 added: multi-select metadata editing — Info panel and All Metadata dialog show one photo's values with no mixed-value indication |
+3 -2
View File
@@ -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 |
+54 -10
View File
@@ -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`).
@@ -101,13 +102,49 @@ the photo (portrait files are covered) and is carried by DNG export, as RW2's is
## Which bodies get it
Only bodies listed in `FULL_FRAME_CROP` (`arw_lens_rm6.rs`) get an opcode: today `ILCE-7RM6` (9984 × 6656). The unit and
the knot spacing are verified on that body alone; the spacing is defined by its full-frame crop, and a body with
another sensor size or another unit would be warped wrongly. Other bodies get no correction from this conversion; the
ILCE-7RM4A gets its own, with other conventions (below). To add a body: collect photos of it shot with distortion
Only bodies on one of two allowlists in `arw_lens_rm6.rs` get an opcode: `FULL_FRAME_CROP` (today `ILCE-7RM6`,
9984 × 6656) and `APSC_BODIES` (today `ILCE-6700`, below). The unit and the knot spacing are verified per body; a body
with another sensor size or another unit would be warped wrongly. Other bodies get no correction from this conversion;
the ILCE-7RM4A gets its own, with other conventions (below). To add a body: collect photos of it shot with distortion
correction on (several lenses and focal lengths), repeat the camera-JPEG registration below against the uncorrected
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.
render, confirm the unit, the knot spacing and the zoom rule (or measure the body's own). Then add a full-frame body's
default crop to `FULL_FRAME_CROP`, or an APS-C body whose knots span its own crop (as measured for the ILCE-6700) to
`APSC_BODIES`, with a test; an APS-C body also needs its fit gate checked against the camera JPEG (see *ILCE-6700*).
For bodies in `APSC_BODIES` (today `ILCE-6700`), the knots span the file's own default crop: spacing = its half
diagonal / (knot count − 1). This crop-based rule applies only to that list. The ILCE-7RM6 keeps its sensor-fixed
400 px in both modes; in full frame (16 knots) the crop-based rule gives the same 400 px, and in its APS-C mode
(11 knots) it would give 392 px, which was only an alternative measurement there (it fits as well, above).
## 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
@@ -287,10 +324,17 @@ 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). The two public
raw.pixls.us ILCE-6700 files (one scene, E 16–55 mm F2.8 G at 16 mm, not used for fitting) both carry the same
table (−1829 at the last knot). On the compressed file the correction leaves p95 3.1 px (uncorrected 5.1 px, exact
table 3.4 px); with the same spacing this lens fits at unit 16000 (0.43 px), another lens-dependent unit (below).
On the lossless file the table, on its padded 6656 × 4608 frame, misses the 0.5 px fit gate, so that photo stays
uncorrected (5.3 px). Its vignetting and lateral CA tables are read, not applied: measured in #2801 (the camera JPEG
doesn't compensate vignetting, as on the ILCE-7RM6).
- **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),