NC Imago · Docs

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). Imago reads them and turns distortion into a DNG WarpRectilinear opcode, so it 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 (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 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.

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, when the CA planes are on: 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. By default one plane, G, applied to all three channels: lens_opcodes calls warp_opcode(…, lateral_ca: false). With lateral_ca: true (a code change) the planes are [R, G, B], unless 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: then G alone again. The G plane is the same either way. 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, 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); lateral CA parsed, off by default; 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

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 (Imago 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 (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 three of them: the APS-C lossless file as arw-sony-ilce7rm6-apsc-land-lossless.arw, and the ARW 6.0 decoder's full-frame Compressed and APS-C Compressed HQ files (arw6-sony-ilce7rm6-ff-land-c.arw, arw6-sony-ilce7rm6-apsc-land-hq.arw). The corpus test corpus_sony_lens_tables_become_opcodes checks all three, and the full-frame lossless file where it is present. 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.

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.

Ruling: the lateral CA planes are off by default. lens_opcodes emits the G plane alone; the conversion stays in the code and its unit tests (warp_opcode(…, lateral_ca: true)), so it can be turned on once CA is corrected where the table applies. Follow-up: warp the raw CFA's red and blue sites before demosaic (the table halves the raw CA, rms 0.062 → 0.035 px), then turn the planes on and repeat this check. Optics ▸ Remove Chromatic Aberration (optics.remove_ca, an automatic estimate from the image) remains available for visible fringes. The distortion numbers above were measured with the three-plane warp, whose G plane is the one the default emits.

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 passes: with the CA planes on (the verification renders) each got the three-plane warp, and the corrected and uncorrected renders differ on all 54; by default each gets the one-plane distortion warp.

Performance

IMAGO_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 One plane (the default; measured with manual Distortion 1) Three planes (CA planes on)
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 default one-plane warp takes the sample stage from 24 to 45 ms at 1920 px (inferred from the manual Distortion run: the same single-plane path, one plane for all three channels). The three colour planes cost about 2.5–3× that, as for an RW2 or DNG with three planes (the same path): the price of turning the CA planes on. 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

Sources