Files
lightcraft/docs/sony-lens-corrections.md
Brandon Thomas f613aeba59 Merge PR #1176
# Conflicts:
#	crates/raw/src/vendor/arw.rs
#	docs/parity-checklist.md
#	docs/sony-lens-corrections.md
2026-10-14 23:54:06 -04:00

278 lines
23 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Sony embedded lens 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.
Sony ARWs record three lens corrections per shot in the raw image IFD: shading (vignetting), lateral chromatic
aberration (CA) and distortion, each as a camera setting and a radial table, with copies in the encrypted
`SR2SubIFD`. LightCraft **decodes all three** wherever a file carries them and **applies one**: the distortion table
of the **ILCE-7RM4A (A7R IVA)** in Bayer ARWs with native 3:2 framing, independently validated with the
FE 24–105mm F4 G OSS and FE 200–600mm F5.6–6.3 G OSS (the ILCE-7RM6's distortion is applied too, by its own
conversion above). Every other model,
and shading and CA on every model, are decoded only: no warp, vignetting or CA correction is added for them. The decoded record stays with the image
(`RawInfo::lens_metadata` / `RawImage::lens_metadata`, types in `crates/raw/src/lens_metadata.rs`, decoder in
`crates/raw/src/vendor/arw_lens.rs`) for diagnostics and for the validation work described below.
The applied distortion is converted into `OpcodeList3` / `WarpRectilinear`, so the existing profile correction
control, CPU/GPU optics path, EXIF orientation handling, and DNG export use the same correction. Linear YCbCr ARWs
and non-3:2 camera crops are excluded pending validation. An aspect crop may retain the full-frame radial
normalization; the decoder does not guess that relationship. Tables of other lengths and rejected tables remain
uncorrected, and a table found only in the encrypted `SR2SubIFD` is never applied. The camera's distortion Off
setting does not erase its table. Newly imported photos use LightCraft's existing embedded-profile default
(enabled); the profile correction control can disable or adjust it.
## What is decoded
| Component | Raw-IFD setting / table | `SR2SubIFD` setting / table | Accepted layout |
|---|---|---|---|
| Shading | `0x7031` / `0x7032` | `0x797c` / `0x797d` | SHORT\[1\]; SSHORT\[17\]: 16, then 16 samples |
| Lateral CA | `0x7034` / `0x7035` | `0x797f` / `0x7980` | SHORT\[1\]; SSHORT\[33\]: 32, then two 16-sample curves |
| Distortion | `0x7036` / `0x7037` | `0x7981` / `0x7982` | SHORT\[1\]; SSHORT\[17\]: 16, then 16 samples |
- **Bounded, exact reading.** A raw-IFD field is the one the TIFF reader parsed, the copy the image itself uses:
its first occurrence, within the reader's limits (8,192 entries per directory, a total value budget). The directory
is also scanned, in either byte order and in BigTIFF, for diagnostics only: a tag that occurs more than once is
flagged (`raw_ifd_repeated`, `sr2_repeated`), and an entry the reader left out is reported with its reason (values
outside the file, an unknown type, or past the reader's limits) rather than treated as absent. `SR2SubIFD` fields
are read straight from their first entry. A setting may also be a nonnegative SSHORT (the documented editor type
differs from the type the cameras store). Anything else is refused with its reason (`SonyFieldError`: field type,
count, leading sample count, values outside the file or block, past the reader's limits, a negative setting).
Nothing is allocated from a count in the file.
- **Raw IFD first, `SR2SubIFD` as a checked fallback.** The raw IFD's copy wins. The `SR2SubIFD`'s copy is used only
when the raw IFD lacks the tag; a malformed raw-IFD copy is reported, never silently replaced. When both copies are
well-formed their agreement is recorded (`copies_agree`), and on disagreement the raw IFD's copy is used. The block
is decrypted (by the existing `vendor/sr2.rs`) for every ARW that has one, so the check always runs; a block that
can't be found or decrypted gives no copies. Locating it selects the `SR2Private` directory's offset, length and
key exactly as the TIFF reader would (the reader's own directory layout, 8,192-entry limit, readable-entry rules
and value budget, then the first readable occurrence of each), but decodes only each field's first value, so the
directory's other entries cost no memory however large they claim to be; the block itself is limited to 1 MiB. The
plain tag numbers are not looked up inside the `SR2SubIFD`.
- **Settings keep their number and a documented meaning:** shading 256 Off, 257 and 272 Auto, 511 unavailable; CA
0 Off, 1 Auto, 255 unavailable; distortion 0 Off, 1 Auto, 17 Auto fixed by the lens, 255 unavailable. Any other value
is `Unknown(value)`, never Off (the A7CR's Low shading setting has no documented number). Values are not read as
bit masks. Off does not mean absent: the table is still recorded.
- **Only the 16-sample (CA: 2 × 16) layout is interpreted.** Many bodies store 11 (CA: 22) samples padded into the
same 17 (33) values: older compacts and bodies such as the DSC-RX100 III and ILCE-3500, and the ILCE-7M4's APS-C
captures (its full-frame captures carry 16). Those tables are refused as `CountPrefix(11)` and stay
uninterpreted. An all-zero table is kept: it is a valid no-op, not a sign of an unsupported lens.
- **Framing evidence travels with the tables:** the stored frame (padding included), Sony's crop tags and the DNG
default crop as recorded, the image crop, the reference rectangle the distortion conversion is centred on and
normalized to (the default crop when present, else the image crop), compression, whether the data is linear
YCbCr (lossless M/S), and whether both crops are native 3:2.
- **Per-component status.** `Applied` (converted into an opcode), `NoTable`, `NotValidatedComponent` (shading and CA
on every body), `NotValidatedModel`, `NotValidatedMode` (linear YCbCr, non-3:2 framing, or a distortion table found
only in the `SR2SubIFD`), `Neutral` (all-zero table in the validated scope) and `Rejected` (the validated
conversion refused the table). A readable table never implies a usable correction; only `Applied` components
become opcodes.
The applied path is unchanged: the validated conversion still receives the raw IFD's `0x7037` table exactly as the
TIFF reader parsed it, with the same model, framing and fit checks, and produces the same warp. A repeated tag keeps
the reader's choice and is only flagged, and a table the reader never reads (past its entry limit) is reported but
never applied, so already-catalogued photos, their previews and their stored lens data stay consistent with a fresh
decode.
## What each component still needs
The following summarizes an investigation on the ILCE-7CR (A7CR): an owner's collection of 131 raws with four
lenses (Voigtländer APO-LANTHAR 28mm F2 and 50mm F2, both manual focus; Sony FE 24–50mm F2.8 G; Tamron 35–150mm
F2–2.8), plus the three public raw.pixls.us A7CR samples. No private photographs, names or derived images are
part of this repository.
- **Inventory.** All 131 raws carry all three tables in both the raw IFD and the `SR2SubIFD`, and the copies agree
in every file; all four lenses provide tables, the manual-focus ones included. Shading is 272 (Auto) and CA 1
(Auto) everywhere; distortion is 17 (Auto, fixed by the lens) on the nine FE 24–50 files and 0 (Off) on the rest.
Every image crop is 9504 × 6336 at (32, 20), in uncompressed, compressed and lossless-L (padded to 9728 × 6656)
storage. The decoder reproduces this inventory exactly (see *Local inventory check*).
- **Distortion.** The table layout and crop tags match the A7R IVA's, but its interpretation applied unchanged does
not reproduce the A7CR camera JPEG at 24 mm: at 2376 px wide, median / 95th-percentile feature residuals were
1.61 / 6.17 px (12.98 / 51.19 px uncorrected, 6,539 matches), with a signed radial pattern (about +1.7 px at
radii 0.25–0.5, −7.4 px at 0.8–1) that no translation removes. At 50 mm it agrees (0.23 / 1.05 px, from
2.21 / 12.25 px). Scaling the raw table by about 10/9, or using the MakerNote's distortion array instead, brings
24 mm to about 0.37 / 1.10 and 0.33 / 0.97 px. Those are exploratory comparisons on two files against the camera
JPEG, not a model: the MakerNote values are roughly 1.11 times the raw table without one exact ratio, and the
A7R IVA was validated against a different reference renderer (Imaging Edge exports). Needed: A7CR Imaging Edge
On/Off exports at 24 and 50 mm and with another lens, compared with the raw table at its fixed scale; a decision on
the reference (camera JPEG or Imaging Edge); then ILCE-7CR added to the validated models for the full-frame Bayer
3:2 mode only. Neither a 10/9 multiplier nor the MakerNote table is used.
- **Shading.** The layout decodes, but no public specification gives the sample-to-gain function, its radius
convention on the A7CR, or whether the raw samples already contain part of the correction. Brightness ratios on six
natural scenes show no consistent missing peripheral brightening. Needed: evenly lit flat fields with shading
Off / Auto / Low at several apertures and focal lengths, raw CFA ratios between them, and Imaging Edge On/Off
exports, to fix the gain function and processing stage and rule out double correction.
- **Lateral CA.** A public analysis reads the table as a red, then a blue curve relative to green, at a 2⁻²¹
relative radial scale. On three distortion-Off files that candidate reduced red and blue feature residuals
modestly (for example 0.35 → 0.29 px median for red with the 28 mm), which supports investigating it but certifies
neither scale, interpolation nor processing stage. Two product changes come before it: embedded CA must be
independent of the distortion amount (today `pipeline::optics` scales the whole embedded per-plane warp with the
profile distortion control), and the red and blue planes must be resampled in camera RGB, before the camera colour
model mixes them (`engine::files::load_bytes_now` applies the colour model before the optics stage). Then
validation on full-resolution achromatic edges.
- **Other modes.** APS-C, aspect crops and lossless M/S (linear YCbCr) captures were not measured and stay
excluded.
## Local inventory check
`crates/raw/tests/sony_lens_inventory.rs` is opt-in and does nothing in CI. With `LIGHTCRAFT_SONY_LENS_DIR` naming a
directory of ARWs it decodes each from its headers and prints its record (settings, sources, copies' agreement,
sample ranges, statuses); `LIGHTCRAFT_SONY_LENS_FULL=1` also compares header and full decode. With
`LIGHTCRAFT_SONY_LENS_INVENTORY` naming a CSV inventory it checks every file's table counts, sizes, sample counts,
minima and maxima, raw-IFD versus `SR2SubIFD` agreement, settings (numbers and labels), stored size, compression and
crop tags. A file with a row must decode, and every file must have a row, whether it decodes or not. Any decode
error fails the check except an unsupported variant, which is counted and must equal `LIGHTCRAFT_SONY_LENS_UNSUPPORTED`
(default 0). On the 131 A7CR raws above, all 131 inventory rows reproduce and every file decodes. On 133 public Sony
ARWs (127 raw.pixls.us samples from 100 bodies, four corpus copies and two files attached to a public issue), 110 carry
a record, 14 decode without one, and 9 are unsupported variants (eight packed ARWs of older bodies and one ARW
without a CFA image); header and full decode agree on the 124 that decode, and no record has disagreeing copies.
## Clean-room evidence
The [ExifTool EXIF tag-name documentation](https://exiftool.org/TagNames/EXIF.html) names the raw-IFD tags
`0x7031` to `0x7037` (VignettingCorrection / VignettingCorrParams, ChromaticAberrationCorrection /
ChromaticAberrationCorrParams, DistortionCorrection / DistortionCorrParams) with their setting values, and gives the
tables as 17 or 33 signed 16-bit integers whose first value is the count. The
[Sony SR2SubIFD documentation](https://exiftool.org/TagNames/Sony.html#SR2SubIFD) names the encrypted copies
`0x797d`, `0x7980` and `0x7982`; the setting tags next to them (`0x797c`, `0x797f`, `0x7981`) are unnamed there and
were identified by inspecting files, where they equal the raw-IFD settings. The corpus test's expected tables are the
values ExifTool, run as a black-box tool, prints for the public samples. No external photo decoder implementation,
lens database or Adobe lens profile was consulted. Sony Imaging Edge Edit 4.1 was used only as a black-box reference
on local scratch copies.
The observed inverse mapping, with radius `r` normalized to the default crop's half diagonal, is:
```
f(r) = 1 + interpolate(table, r) / 16384
source = center + (output - center) * zoom * f(r)
zoom = 1 / max(f(r)) over radii on the rectangular output boundary
```
The border radii range from the shorter half edge / half diagonal to 1. Table knots span 0 through 1.
Linear interpolation approximates the observed map closely; we do not claim to reproduce Sony's
internal interpolator. A fixed-size weighted least-squares fit expresses source radius as
`r * (k0 + k1*r² + k2*r⁴ + k3*r⁶)`. The fit is rejected if radial error exceeds 0.0005 of the
half diagonal (about 0.43 px at a 1440 px 3:2 preview), or the mapping folds. Table values are bounded
and the input must have precisely the supported signed-short layout. Crop offset and normalization
are converted into DNG's active-area coordinate system before creating the opcode. The warp's centre
and normalization come from the standard `DefaultCropOrigin` / `DefaultCropSize` when the file has them,
not from Sony's crop tags: on the tested A7R IVA files the former start at raw x=32 and the latter at
x=0, and the standard origin is the one that aligns with Sony's exports. The image itself keeps the
decoder's crop precedence for every Sony body (Sony's crop tags, then the default crop, then the
recorded image size), so the framing is unchanged; the opcode's centre is in active-area coordinates,
so the warp stays on the optical centre either way. On the A7R IVA that framing still sits 32 raw
pixels left of Sony's exports (eight pixels in a 2376-pixel-wide export).
The main raw-IFD table is important: the similarly named MakerNote table has different values and
must not be substituted into this formula. Main raw-IFD and SR2SubIFD values agreed in the test files, and the
`SR2SubIFD` copy is never converted.
## Validation
At 2376×1584, independent SIFT correspondences between Sony distortion On and Off TIFF exports gave:
| Lens / focal length | Median residual | 95th percentile | Maximum matched radius |
|---|---:|---:|---:|
| FE 200–600 / 200 mm | 0.043 px | 0.191 px | 0.97 half diagonals |
| FE 200–600 / 600 mm | 0.055 px | 0.231 px | 0.95 half diagonals |
These use the fixed 16384 scale and automatic border framing, not fitted per-image scale or zoom.
A median translation below 0.004 px was removed from the residuals. Correspondences do not cover
all pixels; these are geometric feature residuals, not photometric equality or a worst-case guarantee.
Across 20 sampled RAWs (16 at 24–105 mm, four at 200–600 mm), the polynomial's maximum deviation
from the interpolated file table was below 0.12 px at 1440 px. That measures approximation error,
not agreement with an independent renderer. Camera JPEG and Apple Core Image comparisons support
the wide-lens interpretation but are weaker evidence than same-renderer On/Off pairs.
Historical validation before restoring main's image crop precedence: LightCraft exports with the
standard default crop framing were compared directly with Sony's corrected
TIFFs. At 2376 px wide, median / 95th-percentile feature residuals were 0.35 / 1.04 px at 24 mm,
0.15 / 0.40 px at 200 mm, and 0.13 / 0.56 px at 600 mm (median translation below 0.024 px).
The uncorrected medians at 200 and 600 mm were 8.63 and 10.82 px respectively. Different colour,
sharpening, and demosaicing affect feature localization; these are geometry checks, not colour parity.
An isolated desktop run on Apple M1 Metal detected the profile, rendered without GPU fallback or
warnings, and exposed the existing Optics controls. Existing library edits are not reset: once the
source has loaded, enable lens corrections if they were previously disabled.
After restoring main's crop precedence, fresh 2376×1584 exports on 2026-10-10
(main `b70ae174` plus the aspect-framing guard) measured:
| Focal length | Corrected median / p95 residual | Uncorrected median | Removed translation x / y | Matches / maximum radius |
|---|---|---|---|---|
| 24 mm | 0.36 / 1.12 px | 5.62 px | 7.98 / −0.01 px | 1,442 / 0.95 |
| 200 mm | 0.13 / 0.35 px | 8.65 px | 8.01 / 0.02 px | 11,606 / 0.98 |
| 600 mm | 0.10 / 0.37 px | 10.33 px | 8.02 / 0.00 px | 12,268 / 0.97 |
Residuals subtract only the median feature translation; no scale or lens coefficients are fitted to
these reference pairs. The roughly eight-pixel horizontal translation is the retained crop-origin
difference described above, not a claim of exact Sony framing. Maximum radius is in half diagonals.
The distortion geometry remains aligned while every body's image framing follows main's crop order.
Tests cover signedness/length rejection, zero and malformed tables, independent 600 mm reference
geometry, barrel framing, offset crop normalization, both TIFF byte orders, header/full consistency,
and preservation through DNG export. The decoder's synthetic tests (`crates/raw/src/vendor/arw_lens.rs`) add both
byte orders and BigTIFF, exact SHORT / SSHORT types, negative samples, missing, short, long, huge and zero counts,
wrong leading counts, values outside the file, a table past the TIFF reader's entry limit (not applied), repeated
tags (the reader's copy kept, as before), unknown and negative settings, raw-IFD versus
`SR2SubIFD` precedence and agreement, the correct `SR2SubIFD` tag numbers, a wrong decryption key, hostile entries
inside the decrypted block, linear YCbCr, framing evidence (Sony's crop at x = 0 against the default crop, agreeing
crops, padded storage, aspect crops, crop tags outside the frame), all eight EXIF orientations leaving the record
unchanged, and neutral, out-of-range and folding tables. On a small classic-TIFF fixture in both byte orders, every
truncation and four mutations of each byte outside the pixel data (set to 0x00 and 0xff, top and low bit flipped)
decode or fail without a panic. `tests/sr2_heap.rs` measures the peak heap of decoding a 16 MiB file whose
`SR2Private` directory holds eight fields aliasing the whole file: 37 kB, against 134 MB when those fields were
decoded. No private photographs or reference TIFFs are committed.
## Camera coverage and regression boundary
The model check is exact: `ILCE-7RM4` (A7R IV without the A suffix), `ILCE-7M3`, unknown/missing
model names, and every other model remain unsupported by this distortion decoder. Matching tag
numbers and table lengths do not establish the scale, interpolation or crop normalization on another
body. Support can expand after independent geometry validation; do not add model-name prefix matching.
The current `cargo xtask corpus --download` manifest contains 25 Sony CC0 samples across 18 bodies,
including samples 4822 (A7R IVA / 24–105 mm) and 3989 (A9 II / 200–600 mm), each with a pinned SHA-256 that the
download checks. `sony_embedded_distortion_is_limited_to_validated_models` checks 23 of them (the two ILCE-7CM2
codings have their own test), and `sony_lens_records_of_the_public_samples` verifies all 25 against their pins
(`SONY_SAMPLES` in `crates/raw/tests/corpus.rs`, itself anchored by a pinned digest and compared with the manifest)
before decoding. **No CI job fetches the corpus**: these fixture tests run only where it has been downloaded, and
otherwise skip each absent file. `LIGHTCRAFT_CORPUS_REQUIRED=1` makes `sony_lens_records_of_the_public_samples`
fail on any absent pinned file instead.
| Camera | Corpus samples | Decoded record: tables, settings (shading / CA / distortion) | Applied |
|---|---|---|---|
| ILCE-7RM4A | Compressed (4822) | All three tables; 257 / 1 / 17 | One distortion warp |
| ILCE-9M2 | Compressed (3989) | All three; 257 / 1 / 0 | None |
| ILCE-7M3 | Compressed, uncompressed | All three; 257 / 1 / 0 | None |
| ILCE-7M4 | 14-bit, lossless L/M/S | L/M/S: all three, 272 / 1 / 17; the 14-bit file (APS-C framing, 4608 × 3072): settings only, its 11-sample tables are not interpreted | None |
| ILCE-7RM2 | 12-bit uncompressed | All three; 256 / 0 / 1 | None |
| DSC-RX100 | One | Shading, from the `SR2SubIFD` (the raw IFD has no tables); CA and distortion in the 11-sample layout | None |
| DSC-RX100M3 | One | Shading; CA and distortion in the 11-sample layout | None |
| ILCE-7CR | Compressed, lossless L/M | All three; 272 / 1 / 0 | None |
| ILCE-7RM4 | 14-bit compressed | All three; 257 / 1 / 17 | None |
| ILCE-3500 | 5600 K | Settings only (unavailable) | None |
| ILCE-7S | Shade | An all-zero CA table; settings unavailable | None |
| ILCE-7SM2 | 16:9 framing | All-zero shading and CA tables; settings unavailable | None |
| DSLR-A500, DSLR-A700, SLT-A33, DSLR-A580, DSLR-A900 | One each | No record | None |
These are **model-boundary regression checks, not new geometric validation of those models**.
`sony_lens_records_of_the_public_samples` also checks the exact tables, settings, provenance and framing evidence of
the three A7CR codings and the A7R IVA sample, and that across the Sony corpus only the A7R IVA's distortion is ever
applied. In particular,
refusing the A7 III tables resolves the unverified corner warp identified in review. The lossless M/S
files are linear YCbCr and remain excluded independently of the model check. Header and full decode
must agree for every sample. Synthetic tests also verify both TIFF byte orders, exact model matching,
that the warp is centred on the standard default crop while the image keeps Sony's crop, and that DNG export preserves an accepted warp without adding one to rejected
models. A non-3:2 Sony image crop is also rejected when the standard default crop still describes
3:2 geometry; the regression fails without the image-framing guard. The existing independent Sony
geometry tests continue to cover the enabled A7R IVA path.
Before restoring main's crop precedence, re-exporting the 24, 200 and 600 mm reference files after
the model restriction produced byte-identical corrected PNGs to the earlier validated exports. Isolated headless app checks showed the A7R IVA profile
controls present and the A7 III profile controls absent, with no notices and completed RAW renders.
Before enabling another body, compare corrected and uncorrected renders against independent references
across multiple lenses/focal lengths and photographs with features near the edges and corners. Record
residuals and spatial coverage, and check crop/aspect and compression variants. Embedded JPEGs are
useful when the camera actually applied distortion correction; an Off JPEG cannot certify an On warp.
Prefer same-renderer Sony On/Off exports when the camera setting or geometry is ambiguous. Table-fit
error alone only checks our polynomial approximation, not whether the table's interpretation is correct.