Skip to content

fix(flatfield): store the gain in sensor layout and turn it by each frame's EXIF orientation - #1359

Merged
marcinz606 merged 2 commits into
marcinz606:mainfrom
thetalkingdrum:fix/flatfield-exif-orientation
Oct 11, 2026
Merged

marcinz606 merged 2 commits into
marcinz606:mainfrom
thetalkingdrum:fix/flatfield-exif-orientation

Conversation

@thetalkingdrum

@thetalkingdrum thetalkingdrum commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The problem

A Flat Field profile only corrects frames whose EXIF orientation tag matches the reference's. Cameras on a copy stand point straight down, so their tilt sensor tags frames close to randomly. In one sample set, a single Canon session wrote tag 1 on 7 frames and tag 8 on the other 6. With a mismatched tag, the gain is applied upside down (tag 3), or squashed across the turn by cv2.resize (tags 6 and 8). The frame then ends up more uneven than with no correction at all. On a synthetic rig the spread goes from 73% uncorrected to 103–104%.

The cause: the bake decodes the reference with its EXIF orientation already applied, and every apply site multiplies the frame after its own orientation is applied. The embedded lens correction already does this the right way: it turns back to sensor layout before applying its maps.

The fix

  • The gain and the check copy are stored in sensor layout. The bake undoes the reference's tag.
  • Every apply site turns the gain by the decode's own tag first: preview and test-strip renders, rendered thumbnails, export, Linear Output, Roll Analysis, Auto-crop all frames, Auto Skew, stitch parts and the lens path. orientation is a required argument of apply_flatfield, so a new call site can't leave it out.
  • A startup migration converts existing profiles. It works out the tag the bake applied rather than the one read today. Earlier builds read CR3 and RAF tags as 1, so a tag only LibRaw sees is trusted only when the gain's shape shows the bake made that turn. A profile whose reference can't be read, or whose bake tag can't be told, stays unconverted, applies exactly as before, and is retried on the next launch. On a real profile folder with upright references, all 9 come out unchanged.
  • Check Flat Field sits the way the frame on the canvas does: the frame's EXIF tag, then its quarter turns and flips. Fine rotation, keystone and crop are left out, so the edges, where falloff shows, stay in view.

Trade-offs

  • THUMBNAIL_RENDER_VERSION goes 2 → 3, as CLAUDE.md requires for changed pixels. main has already moved it from 0.63.0's 1, so users updating to 0.64 re-check their thumbnails once either way, as long as this lands before 0.64.
  • A profile whose reference was deleted before upgrading keeps the old behaviour: correct for frames tagged like the reference, not for the others.

Testing

  • Unit tests cover:
    • every EXIF tag from 1 to 8, and half frames on a turned scan;
    • the bake;
    • the migration: a turned reference, an upright one, one offline and then back, an interrupted write, a LibRaw-only tag in each case, a JXL reference and an unreadable file;
    • the hand-off from the decode worker to the canvas render;
    • the Check Flat Field view under the frame's tag, quarter turns and flips.
  • End to end in the app, offscreen: one flat and frames tagged 1, 6 and 8 come out even in both preview and export, and the tag-1 result is identical to the old apply.
  • make all passes.

…rame's EXIF orientation

A profile was baked from its reference after the decode had applied the
reference's EXIF orientation, and applied to each frame after the frame's
own orientation, with cv2.resize stretching the gain to fit. A camera on a
copy stand tags frames close to randomly (one Canon session here wrote tag 1
on 7 frames and tag 8 on 6), so a frame tagged differently from its flat got
the gain upside down or squashed across the turn. On a synthetic rig that
leaves a 103-104% spread on tag 6/8 frames, worse than the 73% uncorrected.

The bake now undoes the reference's orientation, and every apply site takes
the decode's own tag first: preview and test strip renders, rendered
thumbnails, export, Linear Output, Roll Analysis, Auto-crop all frames,
Auto Skew, stitch parts and the lens path. The tag reaches the canvas render
with the decode metadata, beside the camera matrix. `orientation` is a
required argument of apply_flatfield, so a new call site cannot leave it out.

A startup migration moves existing profiles to sensor layout. It works out
the tag the bake applied, not the one read today: a JXL decode applies
none, and a tag only LibRaw reads (CR3, RAF, read since fb1d733) is
trusted only when the gain's aspect shows the bake made that quarter turn.
A profile whose reference cannot be read, or whose bake tag cannot be told,
is left unmarked, applies as baked, and is tried again on the next launch.
Check Flat Field takes the open frame's tag.

THUMBNAIL_RENDER_VERSION is bumped: frames with a Flat Field and a tag
different from their reference render differently.
The check sat in the camera's orientation while the frame on the canvas was
turned or flipped, so a portrait frame showed a landscape check and a dark
corner in the check did not match the same corner in the photo. It now takes
the frame's quarter turns and flips, after its EXIF orientation. Fine
rotation, keystone and crop are left out, so the edges, where falloff shows,
stay in view.
@thetalkingdrum
thetalkingdrum force-pushed the fix/flatfield-exif-orientation branch from 58d201f to 2afa59f Compare October 11, 2026 14:38
@marcinz606
marcinz606 merged commit bc66bf8 into marcinz606:main Oct 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants