mirror of
https://github.com/storytold/lightcraft.git
synced 2026-10-31 21:48:53 -06:00
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:
+85
-12
@@ -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
@@ -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
@@ -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 |
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user