LR-EDIT-OPTICS-PROFILE: Sony lens corrections verified on the ILCE-7RM6; docs and tracker

docs/sony-ilce7rm6-lens-corrections.md records the three Sony tables, the
measured conventions with their residuals, the conversion to
WarpRectilinear and its fit gate, the model allowlist and how to extend
it, the measurement method, the verification and the limits.

Verified locally on 54 ILCE-7RM6 photos with four lenses. The corrected
render lines up with the camera JPEG within 0.679 px at 1440 px at the
corners and edge midpoints of the 33 measurable photos shot with
in-camera correction on (up to 17.3 px uncorrected). The 13 controls show
the model's own shift. Every fit passes the 0.25 px gate (margin 0.075).
Lateral CA stays within 0.3 px, but applied after demosaic it raises the
render's corner CA (rms 0.032 -> 0.052 px); the doc and tracker say so.

Its "Two conventions" section sets these conventions next to the
ILCE-7RM4A's (vendor::arw_lens, docs/sony-lens-corrections.md, #498):
on the 34 measurable ILCE-7RM6 photos that formula gives 1.429 / 0.319 px
(max / rms) against 0.596 / 0.143, failing only on the 24-70 GM II and
fitting the 100 mm and 200-600 better; no single unit fits every lens
under either, and what would settle it (Sony On/Off exports of a wide
zoom, more lenses). docs/sony-lens-corrections.md gains a pointer to it.

Tracker rows LR-EDIT-OPTICS-PROFILE (one note for both bodies) and
LR-IMP-CAMERA-COVERAGE in docs/parity-checklist.md, gap 8 in
docs/gaps.md, the lens-profile section of docs/raw-parity.md (lens score
unchanged), whats-new (lossless ARWs now; Compressed / Compressed HQ once
the ARW 6.0 decoder, #625, lands) and the colour-fit follow-up note are
updated.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
dev-null-undefined
2026-10-13 13:09:40 +02:00
co-authored by Claude
parent d982a073c9
commit 33c362a7e4
7 changed files with 326 additions and 5 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ Matrix-only and matrix + table looks are each completed (tone curve, then chroma
The matrix is applied to scene-linear sensor pixels. The curve is carried in `SourceInfo` and applied in the pipeline finish stage, after exposure/WB/local edits. It is never baked into decoded luminance or clipped to JPEG precision. CPU and GPU use the same tone LUT. Contrast, whites and blacks adjust the starting look through the display-domain controls. JPEG neutral rendering and the existing DNG colour model are unchanged.
One third of the pixels are held out from both fits. The combined model must reduce linear-display squared error by at least 30% and have per-channel RMS error ≤0.10 (0.055 rejected half of the public raw.pixls.us samples tried — ILCE-6400, -6700, -7M4 — whose fits were still 1.5–3× closer to the camera JPEG than the fallback and visibly better). When the fit on all pixels fails these gates, it is tried once more, with the same gates, on the pixels away from edges (where the 3×3 neighbourhood in either proxy spans a luminance ratio above 3). Many camera JPEGs are lens-corrected while the raw is not (Sony "Distortion Comp.: Auto", compacts and kit zooms), and on a detailed scene the misaligned edges alone fail the gate although the colours agree everywhere else (issue #232). The public ILCE-7RM2 samples (raw.pixls.us, one façade shot in seven compression/correction variants, 24–70 mm at 51 mm) are up to 15 px of 1440 off their JPEG; the six with distortion correction on missed the gate at held-out RMS 0.108–0.115 and opened in the grey neutral fallback, 13 L* too dark (ΔE76 16.6–16.9 against the JPEG after registering its geometry). Away from edges they pass at 0.049–0.052 and land at ΔE 3.7–3.8 with lightness within 0.6 L*, like the variant shot with distortion correction off (0.059 on all pixels, ΔE 3.9). The corpus GX80 kit-zoom sample (up to 63 px of 1440 off) passes too: 0.107 → 0.081, ΔE 19.1 → 7.9. Fits accepted on all pixels are unchanged. These are provisional acceptance gates, not a colour-accuracy certification. Singular, unrelated and aspect-mismatched references retain the documented fallback; poor-fitting ones get the partial look below when it is clearly closer to the camera JPEG, and black-and-white ones only their tone curve (below). Both fits use the same fixed proxy at every decode resolution.
One third of the pixels are held out from both fits. The combined model must reduce linear-display squared error by at least 30% and have per-channel RMS error ≤0.10 (0.055 rejected half of the public raw.pixls.us samples tried — ILCE-6400, -6700, -7M4 — whose fits were still 1.5–3× closer to the camera JPEG than the fallback and visibly better). When the fit on all pixels fails these gates, it is tried once more, with the same gates, on the pixels away from edges (where the 3×3 neighbourhood in either proxy spans a luminance ratio above 3). Many camera JPEGs are lens-corrected while the raw is not (Sony "Distortion Comp.: Auto", compacts and kit zooms), and on a detailed scene the misaligned edges alone fail the gate although the colours agree everywhere else (issue #232). Sony's embedded lens tables now correct ILCE-7RM6 renders under Enable Lens Corrections ([Sony ARW lens corrections](sony-ilce7rm6-lens-corrections.md)), but this fit still matches its pairs without them (the sensor proxy is uncorrected); aligning the pairs first is a follow-up. The public ILCE-7RM2 samples (raw.pixls.us, one façade shot in seven compression/correction variants, 24–70 mm at 51 mm) are up to 15 px of 1440 off their JPEG; the six with distortion correction on missed the gate at held-out RMS 0.108–0.115 and opened in the grey neutral fallback, 13 L* too dark (ΔE76 16.6–16.9 against the JPEG after registering its geometry). Away from edges they pass at 0.049–0.052 and land at ΔE 3.7–3.8 with lightness within 0.6 L*, like the variant shot with distortion correction off (0.059 on all pixels, ΔE 3.9). The corpus GX80 kit-zoom sample (up to 63 px of 1440 off) passes too: 0.107 → 0.081, ΔE 19.1 → 7.9. Fits accepted on all pixels are unchanged. These are provisional acceptance gates, not a colour-accuracy certification. Singular, unrelated and aspect-mismatched references retain the documented fallback; poor-fitting ones get the partial look below when it is clearly closer to the camera JPEG, and black-and-white ones only their tone curve (below). Both fits use the same fixed proxy at every decode resolution.
**A partial look when the full look misses the gates.** The neutral fallback (camera RGB taken as sRGB, the default tone map) is far from the camera JPEG. Rendered for every photo on this path in the CC0 archive (1,150 files with a camera JPEG), it is 10.7 L\* too dark (median; about one stop), keeps 0.36 of the camera's colourfulness and has hues 7° off on the coloured pixels (median CIEDE2000 13.6). Its white balance is right: on the pixels the camera renders near-neutral the cast is under 1 a\*/b\* (median, every maker). The "blue" fallback on newer Sony bodies (Δb\* −22 over the whole frame on the ILCE-7RM5 sample) is a warm scene losing its colour, not a cast: its near-neutral pixels are within −1.6 b\*. So before falling back, a partial look is fitted (`fit_partial`): the quantile tone curve (registration-free, the largest part of the error), the chroma curve, and, when the scene has enough colour to learn a matrix (the full look's condition), the photo's own colour matrix at half weight, the other half the identity (`PARTIAL_MATRIX_WEIGHT`). No hue/saturation table. Half weight because, on the held-out proxy pixels of the 23 colourful photos without a look, the full matrix left 2 of them more than 2° further from the camera's hues than the neutral fallback and half weight none (median hue error 9.6° with the generic colour, 5.1° at half weight, 4.2° at full weight; on the 1,080 photos with a look 3.9° and 2.7°). Without a matrix (tone and chroma curve alone, or with one global saturation scale) about a third of all photos ended up more than 2° further from the camera's hues than the fallback, as the added colour makes the generic matrix's hue errors visible. The partial look is tried on all pixels, then away from edges, and used only when it cuts the held-out squared error below 0.7 of the neutral fallback's in linear and in gamma-encoded display values, and when the JPEG's luminance ranks the pixels like the scene's (Spearman ≥ 0.5, `PARTIAL_MIN_RANK_CORRELATION`): a curve matched to the brightness of an unrelated picture also beats the dark fallback pixel for pixel. The accepted full looks have 0.55 or more; three old Coolpix files whose raws decode wrongly have 0.07–0.27 and keep the fallback. There is no limit on the partial look's own error. On the archive (1,783 renders): all 1,087 full looks are byte-identical; of the 55 photos that reach the partial fit, 49 get it (40 on all pixels, 9 away from edges; 13 with the damped matrix, 41 with a chroma curve) and 6 keep the fallback (lens-corrected compacts: RX10, TZ80 1:1, D-LUX 4, G10 16:9, GF2 ×2). On the 49, median CIEDE2000 to the camera JPEG 14.0 → 2.1, ΔL\* −11.5 → +0.6, colourfulness 0.36 → 0.87, hue error 10.1° → 7.1°; every one is closer, by at least 1.3. Most of the others without a look never reach a fit: on 48 files (Nikon D4, Df, D300/D300S, D7000, D5100 and others) the camera JPEG proxy comes out a pixel narrower than the sensor proxy and no pairs are collected (the resize in PR #531 fixes this; with it 46 of them pass the full look's gates, CIEDE2000 17.9 → 2.9).
+2 -1
View File
@@ -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 and Sony ILCE-7RM4A distortion | 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 and Sony in-file distortion on two bodies (ILCE-7RM4A; ILCE-7RM6, also lateral CA), each with its own conventions; other Sony bodies' tables not applied until verified | 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**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12, 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 / F828 regression (#702), 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) |
| 10 B | **Per-model verification thin, plus cheap per-body decode fixes**: 236 of 1,446 Adobe models verified (16%), ~42% of photographers on a verified body; Nikon 5 bodies verified, Canon 12. Cheap fixes: Z 6II packed 14-bit NEF (#769, PR #671), Sony A100 / F828 regression (#702), 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) |
@@ -89,6 +89,7 @@ Beta-blocking gaps (B) total **~230–430 h**; with the stability and platform w
| 2026-10-10 | minor | Compressed ORF entry links the two Olympus patents that describe the method |
| 2026-10-11 | minor | Gap 14: `crs:HDREditMode` is read from XMP (#663); the headroom limit and SDR rendition still aren't |
| 2026-10-11 | minor | Gaps 2 and 10 shrink: EOS R5 Mark II CR3 decodes and is verified (fourth CR3 body, Canon 12 verified, 235 models, ~42% on a verified body); CR3 gain +6.5 → +6.2 points |
| 2026-10-10 | minor | Gap 8: Sony ILCE-7RM6 in-file distortion and lateral CA added next to the ILCE-7RM4A's (docs/sony-ilce7rm6-lens-corrections.md); the gap stays open (no lens database), hours unchanged |
| 2026-10-10 | minor | RAW gaps re-measured model by model (raw-parity.md): CR3, compressed ORF and verification entries carry measured shares; HE NEF no longer a beta blocker (+1.1 points for 20–40 h); verification entry absorbs the cheap per-body fixes (#769, #702, A200 series) |
| 2026-10-10 | minor | Beta total raised to 340–600 h to include the ~92 open core-path user issues |
| 2026-10-10 | major | Created from the checklist's Top gaps (whose items 1–2 had been duplicated by a merge) and ROADMAP's Where we stand; re-ranked; added evidence, impact, hours and beta flags; added platform-CI, lens-database, HE NEF and localization gaps |
File diff suppressed because one or more lines are too long
+3 -1
View File
@@ -7,6 +7,7 @@ does not decode, or decodes with the wrong colour, cannot switch. This document
model. The numbers feed [target-app-parity.md](target-app-parity.md); individual shortfalls are
ranked in [gaps.md](gaps.md). Details per format: [cr3.md](cr3.md),
[raf-compression.md](raf-compression.md), [sony-lens-corrections.md](sony-lens-corrections.md),
[sony-ilce7rm6-lens-corrections.md](sony-ilce7rm6-lens-corrections.md),
[camera-preview-colour.md](camera-preview-colour.md), [denoise.md](denoise.md).
## Headline
@@ -219,7 +220,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) |
| 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, checked against Sony's exports) and the ILCE-7RM6 (three lenses, a fourth, the 16–35 mm, only as a control with correction off; checked against the camera JPEG), with different conventions ([sony-ilce7rm6-lens-corrections.md](sony-ilce7rm6-lens-corrections.md#two-conventions)); the ILCE-7RM6's raw-IFD lateral CA table too (provisional unit, applied after demosaic), not its raw-IFD vignetting table |
| Vignetting / lateral CA from maker notes | ✅ | ⬜ |
| Manual distortion, vignetting, CA, defringe | ✅ | ✅ |
@@ -281,5 +282,6 @@ Beyond decoding:
| 2026-10-10 | minor | Compressed ORF: pointer to the two Olympus patents that describe the method, as a clean-room starting point |
| 2026-10-12 | minor | PEF without area tags: masked border rows and columns trimmed against the BlackPoint (issue #1271; KP, KF, K-3 Mark III, K-70, K-1, K-1 Mark II, K-S1, K-S2, 645Z); counts unchanged |
| 2026-10-11 | minor | EOS R5 Mark II verified (CRX QP-map first literal and limited-length escape, odd-height Bayer planes; 4 CC0 files pinned, QP maps of 33,665 private files): 235 verified, 942 unverified; use-weighted 85% (42% on verified bodies); decoding 77%, RAW readiness unchanged at ~56% |
| 2026-10-10 (later) | minor | Lens profiles: Sony ILCE-7RM6 in-file distortion and lateral CA next to the ILCE-7RM4A's distortion; lens score unchanged (~10%: one more body) |
| 2026-10-10 (later) | major | Model-by-model measurement against Adobe's published list (1,446 models: 234 verified, 943 unverified, 233 preview, 36 unsupported); `docs/raw/` data and re-measure script; use-weighted share 84% (40% on verified bodies); variant matrix; colour and lens sections with scores; ranked next cameras; RAW readiness ~56% from written parts |
| 2026-10-10 | major | Created: formats, makers, demosaic, colour and lens checklists; target counts from the installed Lightroom 9.6 bundle listing |
+301
View File
@@ -0,0 +1,301 @@
# Sony ARW lens corrections
Sony cameras record three lens-correction tables in the raw IFD of every ARW: distortion, vignetting and lateral
chromatic aberration (CA). LightCraft reads them and turns distortion and lateral CA into a DNG `WarpRectilinear`
opcode, so they reach the same **Optics ▸ Enable Lens Corrections** (`optics.lens_profile`) as a DNG's embedded warp
and Panasonic's RW2 distortion data. 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 ([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`).
Photos imported before this change keep their settings: **Photo → Reload from Disk** adds the lens data and, when the
photo is still at its import settings, turns the correction on as one undoable step; the automatic reload after an
external editor saves the file does the same. Edited photos keep their settings. The Profile Distortion slider
scales the warp (all three colour planes) as for DNG and RW2.
## The tables
ExifTool's names (EXIF table, `SubIFD` group); each table is a count-prefixed array of signed 16-bit values
(`SSHORT`, 17 / 17 / 33 values), and each has an in-camera on/off tag:
| Table | Tag | Flag tag | Count (full frame / APS-C crop mode) | Flag values seen |
|---|---|---|---|---|
| Distortion (`DistortionCorrParams`) | `0x7037` | `0x7036` | 16 / 11 | 0 (off) or 1 (on) |
| Vignetting (`VignettingCorrParams`) | `0x7032` | `0x7031` | 16 / 11 | 2 on all 54 photos |
| Lateral CA (`ChromaticAberrationCorrParams`) | `0x7035` | `0x7034` | 32 / 22 (red, then blue) | 1 on all 54 photos |
The first value is the knot count; only the first `count` values are used. Full-frame files carry 16 / 16 / 32 knots;
APS-C crop-mode files 11 / 11 / 22, padded to the same array length (distortion with its last value repeated,
vignetting and CA with zeros). The CA table holds red's `count / 2` knots, then blue's. The corrections are built
whatever the flags say: the flag only records what the camera did to its own JPEG. Tables with a count outside
2–16 (CA: an odd count or outside 4–32), a count that doesn't fit the array, or non-integer values are ignored.
## Measured conventions
None of this is documented; it was measured against the camera's own JPEGs (method below). Residuals are in px at a
1440 px long edge.
- **Knots and centre.** Knot `i` sits at sensor radius `i · s` from the default-crop centre, with
`s = hypot(9984, 6656) / 30` = 399.98 px: the full-frame default crop's half diagonal divided by 15, so knot 15 is
the full-frame crop corner. The spacing is fixed on the sensor: APS-C files use the same pixel spacing (verified on
CA, rms 0.004–0.010 px against 0.011–0.014 px for `i/15` of the APS-C half diagonal; `i/10` of it, 392 px, fits as
well). Values are interpolated linearly between
knots; beyond the last knot the last segment is extended (it matters only in the outermost ~0.4 knot spacing at
the corners of a zoomed-out barrel correction). The centre is the default-crop centre: the measured translation
between our render and the camera JPEG has median 0.013 px, max 0.216 px.
- **Distortion** is relative and indexed at the corrected radius, in units of 1/15000: a corrected radius `r`
reads the source radius `S(r) = r · (1 + v(r) / 15000)` (positive values: pincushion; negative: barrel). The camera
then zooms so the output fills the default crop: output radius = `z` · corrected radius, with `z` chosen so that
the largest `S(ρ / z) / ρ` over the crop's border radii `ρ` is 1 (binding at the corner for pincushion, at the
short-edge midpoint for barrel). `z` was 0.971–1.035 on these photos. Without the zoom every hypothesis fails
(24.6 px); with it, the worst residual over the 34 measurable corrected photos was 0.596 px (rms 0.143; corners and
edge midpoints 0.570 px), within the 0.75 px budget for a unit between about 14700 and 15350. A global
least-squares fit gives 15000.7.
- **Lateral CA** is relative to the green source radius, at that radius: red's source radius is
`g · (1 + c_R(g) / 3.36·10⁶)`, with `g` the green source radius (after the distortion correction above) and `c_R`
the red half on the same knots; blue likewise with the second half. Positive values: that colour's image is larger
than green's. **The unit is provisional.** The measurement is borderline: on the 15 photos with CA above 0.15 px
that a single unit explains, the best (minimax) unit 2.26·10⁶ leaves 0.305 px against a 0.3 px budget; per-photo
best units scatter from 1.5·10⁶ to 7.2·10⁶ (about ±30 % scene bias: the RawDB landscape and portrait shots of one
scene, with nearly identical tables, measure 3.4–3.5·10⁶ and 4.5·10⁶). The chosen 3.36·10⁶ is the median of the 20
cleanest photos: worst 0.399 px on the 15, and the rms raw CA over all 46 measured photos falls from 0.079 to
0.052 px. Swapping the halves or an absolute form (`v · norm / U`) is clearly worse.
- **Vignetting: not applied.** The camera JPEGs show no vignetting compensation relative to our uncorrected render:
over the 39 photos whose measurement reaches the corners, the corner/centre ratio JPEG / render has median 0.996
(p10 0.907, p90 1.087), while the tables would predict a corner gain with median +32 % for `1 + v / 2¹⁴` (+16 %
for `v / 2¹⁵`, +63 % for `v / 2¹³`). Regressing the measured corner EV on the predicted one gives slope 0.009 ±
0.063. No model (`1 + v/U`, `U/(U − v)`, `2^(v/U)`) at any unit from 2¹³ to 2¹⁸ reaches 3 %; the identity is the
best of all. The raw data isn't pre-compensated either: the read noise of three dark high-ISO frames is flat from
centre to corner, where a digital gain from these tables would raise it by 60–80 %. So the table probably describes
the lens's natural falloff, but this camera's JPEGs don't apply it, and nothing shows its unit or model. Options:
(a) skip it, so renders match the camera JPEG (the current ruling); (b) apply it with an unverified model for a
Lightroom-like look; (c) shoot flat fields (a white wall at several apertures, one lens) and measure the model.
## Conversion to `WarpRectilinear`
`crates/raw/src/vendor/arw_lens_rm6.rs` builds one `WarpRectilinear` in `OpcodeList3`, centred on the default-crop centre
(DNG pixel-index units relative to the active area; radius 1 = the farthest active-area corner, `m`):
1. `S` from the distortion table and the zoom `z` (bisection on [0.5, 2], 4097 border radii); the green map is
`G(r) = S(r / z)`. Red and blue: `G(r) · (1 + c(G(r)) / 3.36·10⁶)`.
2. Each map is sampled at `u = i / 64`, i = 0…64 (65 samples of `G(u · m) / m`) and fitted by least squares with
DNG's 4-term odd polynomial `u · (k0 + k1 u² + k2 u⁴ + k3 u⁶)` (`vendor::fit_radial_warp`, shared with RW2).
3. **Fit gate:** a plane is refused when its fit is worse than 0.25 px at a 1440 px long edge at a sample the crop
shows: the samples up to the first one at or beyond the crop corner. The lossless files' padded active area
(10240 × 7168 for a 9984 × 6656 crop) reaches radii that are never shown (`u` up to 0.925 is shown there).
Over all 54 photos the fit's worst error at all 65 samples was 0.220 px (a lossless file; ARW6 files at most
0.175 px). Between samples the error is somewhat larger: 0.071 against 0.049 px for the full-frame 16–35 mm
example in the tests.
4. Planes `[R, G, B]`; when the CA table is absent or malformed, a CA value is implausible (|c| above 2·10⁻³ of the
radius, i.e. 6720; the largest value in the 54 photos' tables is 1724) or a colour plane fails the gate, G alone is applied to all
three channels. No opcode when the distortion table is absent or all zero (CA alone is never applied: it is
relative to green), the zoom has no solution in [0.5, 2] or the G fit fails. A refused table leaves the photo
uncorrected rather than wrongly warped.
The opcode then goes through the same pipeline stage as a DNG's (`crates/pipeline/src/optics.rs`): it rotates with
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
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.
## Two conventions
A second Sony decoder, `crates/raw/src/vendor/arw_lens.rs`
([Sony embedded distortion corrections](sony-lens-corrections.md), PR #498), reads the same `0x7037` table on the
**ILCE-7RM4A**, validated against Sony Imaging Edge exports (distortion On/Off pairs at 200 and 600 mm with the
FE 200–600 mm; corrected TIFFs at 24 mm with the FE 24–105 mm). Its conventions differ from the ones above
(`r_o` = output radius about the crop centre, `v` = the table, linear between knots):
| | ILCE-7RM6 (this page, `arw_lens_rm6.rs`) | ILCE-7RM4A (`arw_lens.rs`) |
|---|---|---|
| Knots | `i · 399.98` px, fixed on the sensor; last segment extended | `i / 15` of the crop's half diagonal; radius clamped to 1 |
| Table indexed at | the corrected radius `r_o / z` | the output radius `r_o` |
| Unit | 15000 | 16384 |
| Zoom | fill: the largest `S(ρ / z) / ρ` over the border is 1 | `1 / max f` over the border radii |
| Aspect crops | applied; the zoom for the recorded crop is unverified | refused (native 3:2 only) |
| Tables | distortion (16 knots; 11 in APS-C) and lateral CA; vignetting read, not applied | distortion, 16 entries only |
`arw::decode` passes both the same active area and crop (the DNG-style default crop, on the ILCE-7RM6 the same as
Sony's crop tags); their model lists don't overlap, so a photo gets at most one of them: an ILCE-7RM6 this page's
conversion, an ILCE-7RM4A the other, every other body neither.
**Cross-check.** The ILCE-7RM4A formula, scored on the 34 measurable corrected ILCE-7RM6 photos exactly as in
Measured conventions (radial bins, px at 1440, max / rms; "c" = the worst corner or edge-midpoint point):
| Variant | Unit | All photos | Corners max / median | 24–70 GM II (23) | 100 mm Macro (8) | 200–600 G (3) |
|---|---|---|---|---|---|---|
| **A**: this page | 15000 | **0.596 / 0.143** | 0.570 / 0.099 | 0.596 / 0.100, c 0.474 | 0.361 / 0.135, c 0.336 | 0.588 / 0.325, c 0.570 |
| **B1**: the ILCE-7RM4A formula | 16384 | **1.429 / 0.319** | 1.913 / 0.204 | 1.429 / 0.385, c 1.913 | 0.289 / 0.049, c 0.234 | 0.264 / 0.115, c 0.320 |
| **B2**: B1 at its best single unit | 15680 | 0.940 / 0.223 | 1.202 / 0.098 | 0.940 / 0.215, c 1.202 | 0.327 / 0.100, c 0.278 | 0.683 / 0.428, c 0.637 |
| **B3**: A's indexing and zoom at 16384 | 16384 | 1.450 / 0.455 | 1.845 / 0.350 | 1.450 / 0.539, c 1.845 | 0.284 / 0.062, c 0.238 | 0.654 / 0.329, c 0.586 |
- B1 misses the 0.75 px budget on these photos, but only on the 24–70 GM II: at 24–28 mm it under-corrects the barrel
in the outer half (up to 1.43 px at 0.7–0.85 of the half diagonal), at 45–70 mm the pincushion by a nearly uniform
0.2–0.4 px. On the 100 mm and the 200–600 it fits better than A, and on the 200–600, the one lens both validations
share, its unit agrees with this body's (best unit under its indexing 16524–16602). So the data does not show the
two bodies differing.
- The zoom rules agree (within 0.0035 on every photo); the unit and the table index make the difference. Under A's
indexing the 24–70 GM II's barrel and pincushion ends want the same unit (14749 / 14860, 0.7 % apart); under B's
they need 14792 / 15705 (6 % apart).
- APS-C: B refuses 11-entry tables. Its knots read literally on the APS-C crop (`i / 15` of its half diagonal,
261.5 px) fit the APS-C RawDB files' lateral CA 4 × worse on 3 of 6 photos than the sensor-fixed 400 px; `i / 10`
(392 px) can't be told from 400 px.
**Open question: no single unit fits every lens under either convention.** The 200–600 wants a 5–7 % larger unit (a
weaker correction) than the 24–70 GM II's pincushion end with similar table values (+409 … +561 against +406 … +454
at the corner), under both indexings and every knot spacing tried (0.94–1.12 ×: the ratio of the two lenses' best
units stays 1.053–1.073). A's 15000 is the compromise that keeps every photo within 0.6 px; 16384 is the 200–600's
value. That rests on 3 measurable 200–600 photos (594–600 mm). The ILCE-7RM4A's own wide-angle evidence is its
weakest: its On/Off pairs are all 200–600, and its 24 mm check (LightCraft export against Sony's TIFF) is median 0.35 /
95th percentile 1.04 px at 2376 px. On its `WIDE` unit-test table the two formulas differ by up to 2.49 px at 1440 at
the corner (computed from the formulas, not measured).
**What would settle it:** Sony Imaging Edge On/Off exports of the 24–70 GM II (or any wide zoom's wide end) on either
body, which would show whether the wide-end disagreement is the body or the formula; and more lenses, and more
200–600 photos, on the ILCE-7RM6, to tell a per-lens unit from a convention. Until then each body keeps the
conventions it was validated with.
## Method
Photos: 54 ILCE-7RM6 ARWs: 42 private, and the 12 CC0 samples of the
[dnglab RawDB ILCE-7RM6 set](https://rawdb.dnglab.org/sets/Sony/ILCE-7RM6) (`ILCE-7RM6_{FF,APSC}_{land,port}_RAW{,-C,-C-HD}.ARW`:
full frame and APS-C, landscape and portrait, lossless, Compressed and Compressed HQ). `cargo xtask corpus --download`
fetches one of them, the APS-C lossless file, as `arw-sony-ilce7rm6-apsc-land-lossless.arw`, for the corpus test
`corpus_sony_lens_tables_become_opcodes` (its full-frame lossless case runs only where that file is present); the ARW
6.0 decoder (#625) pins one Compressed and one Compressed HQ file.
Four lenses: FE 24–70 mm F2.8 GM II (27 photos), FE 16–35 mm F2.8 GM II (12, all RawDB), FE 100 mm F2.8 Macro GM OSS
(9), FE 200–600 mm F5.6–6.3 G OSS (6). 41 photos have in-camera distortion correction on (all private); 13 have it
off (the 12 RawDB files and one private photo). Only aggregate numbers per lens are reported. The scripts are local
(the gitignored `plan/arw6/lens/`), not in the repository; the steps are described so they can be redone.
- **Oracle:** the camera JPEG inside each ARW (`JpgFromRaw`, 9984 × 6656 / 6528 × 4352, stored unrotated, turned to
the photo's orientation). Tag values were read with ExifTool used as a tool. Clean room: no lens-correction code of
any other project and no Adobe data was read.
- **Distortion (camera-JPEG block registration).** Our render at half the JPEG size and the JPEG (2 × 2 box-reduced)
are both reduced to a quarter of the JPEG size; on the green channel, every 40 px a 64 px JPEG block is located in
the render by normalised cross-correlation with parabolic sub-pixel refinement. Pass 1 searches coarse-to-fine
(±28 px at 1/16 size, then ±6 px); pass 2 (used for every number) searches ±6 px around pass 1's smooth radial fit
(data-derived, needed for a macro of a periodic screen). Blocks with low contrast (std < 2), NCC < 0.8 or a
tangential displacement above 4 px are dropped. A robust fit of `p_render = g(r) · p_jpeg + t` (`g` an even
4-term polynomial) gives the translation `t`; the measured radius ratio is the median of 32 radial bins (≥ 10
blocks each). A photo is measurable with ≥ 300 blocks and ≥ 15 bins reaching 0.7 of the half diagonal: 34 of the
41 corrected photos (two black frames, a near-black night frame, two low-detail frames and two whose blocks reach
only 0.54 of the half diagonal are not). 108 hypotheses (radius norm, knot placement, relative or absolute, table
indexed at the corrected or the source radius, zoom rule), each with its best unit, were compared.
- **Lateral CA (raw CFA).** The demosaiced render shows only 25–60 % of the lens's CA and the JPEG's 4:2:2 chroma
and noise reduction hide it, so CA was measured on the raw: from LightCraft's uncompressed CFA DNG export, the
red (blue) plane against green interpolated at the same sites (4-neighbour mean), square-root encoded, 2 × 2
binned; 40 px blocks every 24 px, ±3 px NCC search, radial component, median per 16 radial bins.
- **Vignetting (exposure brackets).** The JPEG is resampled into the render's geometry; per pixel and channel, the
exposure at which eight renders bracketed −0.3 … +1.8 EV equal the JPEG value is interpolated (smooth areas, JPEG
values 12–240, monotone brackets). That cancels the fitted tone curve, since exposure is a scene-linear gain before
it. Medians in 16 annuli give the JPEG's falloff relative to our uncorrected render.
## Verification
Every photo rendered with Enable Lens Corrections on and off at half the JPEG size (50 of the 54 are Compressed or
Compressed HQ ARW 6.0 files, so the renders used a local build of this code merged with the ARW6 decoder), then the same block
registration (passes 1 and 2) against the camera JPEG, and R and B against G inside each render (80 px blocks every
128 full-resolution px, ±4 px search; per corner the median radial shift of the 12 nearest blocks). px at a 1440 px
long edge; aggregates only.
**Distortion, photos corrected in camera** (our render with the correction on vs the camera JPEG; budget 0.75 px).
"Corners + midpoints" is the 2-D displacement at the four corners and four edge midpoints, global translation
included; "bins" the radial residual over 32 radial bins. A photo counts as measurable as in the method above.
| Lens | Photos (measurable) | Focal lengths | Corners + midpoints max / rms | Bins max / rms | Uncorrected, corners (per-photo max) |
|---|---|---|---|---|---|
| FE 24–70 mm F2.8 GM II | 26 (23) | 24–70 mm | 0.553 / 0.125 | 0.503 / 0.066 | 1.1–17.3 |
| FE 100 mm F2.8 Macro GM OSS | 9 (7) | 100 mm | 0.409 / 0.111 | 0.317 / 0.097 | 0.6–4.8 |
| FE 200–600 mm F5.6–6.3 G OSS | 6 (3) | 594–600 mm | 0.679 / 0.321 | 0.498 / 0.274 | 7.5–10.8 |
| **All** | **41 (33)** | | **0.679** / 0.150 (206 points) | 0.503 / 0.109 | 0.6–17.3 |
No photo is over budget; the global translation is at most 0.056 px. The 8 not measurable are black, near-black or
low-detail frames, or frames whose blocks don't reach 0.7 of the half diagonal. That is one more than in the
measurement (33 measurable against 34): one 100 mm photo keeps only 255 of the 300 blocks needed when it is matched on
the corrected render, as block coverage differs slightly between the two renders. The 200–600 mm keeps the systematic
residual the measurement found for it (its own best unit is 15659, see Limits).
**Controls, correction off in camera** (13 photos: the 12 RawDB files, full frame and APS-C, and one private 24–70 mm
at 58 mm). Their JPEG keeps the lens's distortion, so our corrected render must differ from it by the model's own shift:
it does, by up to 10.5 px (full frame) and 11.4 px (APS-C), and matches that shift within 0.639 px at the corners and
midpoints (bins 0.259 px; full-frame RawDB files ≤ 0.218 px, APS-C ≤ 0.639 px). This checks that the pipeline renders
the model (also for APS-C, barrel, zoom 0.984), not that the APS-C model is right.
**Portrait photos:** 8 corrected (orientation 6 and 8) within 0.232 px, 6 controls within 0.369 px of the model's
shift: the warp turns with the photo.
**Lateral CA in the render** (radial R-vs-G and B-vs-G shift, median of the 12 blocks nearest each corner; 43 photos
with enough detail at all four corners; budget 0.3 px):
| Lens | Photos | Off: max R / B | Off: rms | On: max R / B | On: rms | Camera JPEG: max / rms |
|---|---|---|---|---|---|---|
| FE 24–70 mm F2.8 GM II | 23 | 0.089 / 0.071 | 0.028 | 0.219 / 0.148 | 0.069 | 0.150 / 0.036 |
| FE 16–35 mm F2.8 GM II | 12 | 0.125 / 0.054 | 0.041 | 0.030 / 0.051 | 0.020 | 0.058 / 0.016 |
| FE 100 mm F2.8 Macro GM OSS | 6 | 0.020 / 0.050 | 0.013 | 0.061 / 0.157 | 0.044 | 0.021 / 0.004 |
| FE 200–600 mm F5.6–6.3 G OSS | 2 | 0.033 / 0.101 | 0.032 | 0.146 / 0.075 | 0.070 | 0.113 / 0.039 |
| **All** | **43** | 0.125 / 0.101 | 0.032 | **0.219** / 0.157 | 0.052 | 0.150 / 0.028 |
Within budget everywhere, but **the correction makes the render's own CA larger on average** (rms 0.032 → 0.052 px;
smaller only for the 16–35 mm). The reason is where it runs: over the radial bins beyond 0.3 of the half diagonal
(418 bins), our demosaiced render shows only 0.29 × the lens CA measured on the raw CFA (correlation 0.85), while the
warp after demosaic moves R and B by 0.84 × the table (correlation 0.96). On the raw data the table halves the CA (rms
0.062 → 0.035 px); applied after a demosaic that already hid most of it, it overshoots (rms 0.021 → 0.035 px over the
same bins). All of this is far below one pixel at 1440 px.
**Fit gate margins** (worst of R, G and B at the gated samples; gate 0.25 px): the 4 lossless files 0.160 px (margin
0.090; 0.220 px at all 65 samples, the padded radii never shown), the 46 full-frame ARW6 files 0.175 px (margin 0.075),
the 4 APS-C ARW6 files 0.035 px. The largest CA value in the 54 tables is 1724 (FE 24–70 mm), a quarter of the 6720
bound. So every photo gets the three-plane warp: the corrected and uncorrected renders differ on all 54.
## Performance
`LIGHTCRAFT_PROFILE=1` renders of the full-frame lossless file and the full-frame Compressed HQ file (corpus
`arw-sony-ilce7rm6-ff-land-lossless.arw`, `arw6-sony-ilce7rm6-ff-land-hq.arw`), on a 4-core laptop shared with other
builds (so absolute times are high); the fastest of 3–5 runs. The warp runs in the `sample` stage:
| Render | Off | Single-plane warp (manual Distortion 1) | On (three planes) |
|---|---|---|---|
| Full size 9984 × 6656, lossless | 267 ms | — | 3098 ms |
| Full size 9984 × 6656, Compressed HQ | 403 ms | 1187 ms | 4014 ms |
| 1920 px long edge (2496 × 1664 source), Compressed HQ | 23.7 ms | 45.4 ms | 112.0 ms |
The three colour planes cost about 2.5–3× a single-plane warp, as for an RW2 or DNG with three planes (the same
path). The stage cache keys the resampled source by the source, framing and output size, so tone, colour and
exposure drags don't re-run it.
## 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
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
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),
15659 (200–600 mm, 3 photos) and 16017 (100 mm, tiny tables); with the single 15000 the 200–600 mm keeps a systematic
residual of up to 0.59 px (within budget, above the method's 0.03 px noise floor). Something lens-dependent is not
captured; per-lens units would need per-lens verification.
- **Lateral CA unit provisional** (above), and the CA warp runs after demosaic, as for RW2 and DNG. Our demosaic
already hides about 70 % of the lens CA, so the full table overshoots in the render: within budget, but the rms
corner CA rises from 0.032 to 0.052 px (Verification). Correcting CA before demosaic would be the principled fix;
scaling the colour planes down or dropping them are the cheap alternatives.
- **Vignetting not applied** (above).
- **DNG export** carries the warp in `OpcodeList3`, as for RW2: a DNG reader applies it like any embedded lens
correction.
- **Existing imports** need Photo → Reload from Disk to pick up the lens data.
- The camera-JPEG colour fit still matches its pairs without the correction (docs/camera-preview-colour.md).
## Sources
- [ExifTool EXIF tag names](https://exiftool.org/TagNames/EXIF.html): the names and types of tags `0x7031`–`0x7037`
(ExifTool 13.59 was used to read the tag values). Their meaning, units and conventions were measured as above.
- [dnglab RawDB, Sony ILCE-7RM6](https://rawdb.dnglab.org/sets/Sony/ILCE-7RM6): the 12 CC0 samples (data only;
dnglab's code was not consulted).
+5
View File
@@ -1,5 +1,10 @@
# Sony embedded distortion corrections
The **ILCE-7RM6** (A7R VI) has its own conversion, with other conventions measured against its camera JPEGs:
[Sony ARW lens corrections](sony-ilce7rm6-lens-corrections.md). On ILCE-7RM6 photos the formula below misses the 24–70 GM II by
up to 1.43 px at 1440 px while fitting the 100 mm and 200–600 better, and no single unit fits every lens under either
convention; that page's *Two conventions* section has the comparison and what would settle it.
LightCraft reads the signed 16-sample distortion table in the raw image IFD (`0x7037`) of
Sony **ILCE-7RM4A (A7R IVA)** Bayer ARWs. Other camera models are deliberately left uncorrected,
even when they contain a table with the same layout.
+12
View File
@@ -102,6 +102,18 @@
- Compare and Reference View panes say "Rendering…" (or that the photo can't be opened) the same way, so a
magnified preview isn't taken for the photo when checking focus at 1:1.
### Lens corrections
- Sony A7R VI (ILCE-7RM6) raws are corrected for lens distortion and lateral chromatic aberration from the tables the
camera records in each file, under Optics ▸ Enable Lens Corrections, on by default for newly imported photos. On 33
photos shot with the camera's correction on (24–70 mm, 100 mm macro, 200–600 mm) the render lines up with the
camera's JPEG within 0.68 px at 1440 px at the corners, where it was up to 17 px off before. The camera's vignetting
table is read but not applied: its own JPEGs don't apply it either. This reaches lossless ARWs now; Compressed and
Compressed HQ ARWs (ARW 6.0) get it once their decoder (#625) lands, and until then open from their embedded JPEG.
The A7R IVA (ILCE-7RM4A) has its own distortion correction, checked against Sony's exports; other Sony bodies record
the same tables, but they stay uncorrected until they are checked the same way. Photos imported earlier get the
correction with Photo ▸ Reload from Disk (when still at their import settings). See
`docs/sony-ilce7rm6-lens-corrections.md`.
### JPEG XL export
- Export as JPEG XL (issue #247): Format ▸ JPEG XL in the Export dialog, `format: "jxl"` for agents and the CLI
(`render in.raw -o out.jxl`). Quality as for JPEG, or Lossless; Effort (Fast / Normal) trades time for a