Skip to content

feat(profile): build and apply camera profiles for trichrome captures - #125

Open
toonoumi wants to merge 1 commit into
mainfrom
feat/trichrome-camera-profile
Open

feat(profile): build and apply camera profiles for trichrome captures#125
toonoumi wants to merge 1 commit into
mainfrom
feat/trichrome-camera-profile

Conversation

@toonoumi

Copy link
Copy Markdown
Owner

Why

A trichrome capture is not the same device space as a normal Bayer shot. The merge takes the red plane shot under red light, the green under green, the blue under blue — so each channel's sensitivity is the sensor response × its own light, and the channel balance comes from three independent exposures. Two halves of that were missing:

  1. A trichrome profile couldn't be built. The IT8 wizard decoded exactly one file, so the space the user actually scans in was unprofileable. Profiling a white-light shot and applying it to trichrome frames is simply the wrong transform.
  2. A profile was never applied to a merged image. _read_merged did field correction and the slice chain, then returned — the DCP/ICC apply the RAW branch performs (ccr_image.py:748-750) had no counterpart. So even a correct profile did nothing on the frames it was built for.

Profiling from a triplet

Page 1 of the wizard gains "Trichrome: merge 3 RAWs (red, green, blue)":

  • Browse becomes a multi-select requiring exactly 3 RAWs, sorted by basename exactly as an import sorts them (ccr_merge.sort_for_merge), so the wizard and a 3-way import of the same files agree on which frame is R, G and B.
  • Use current image adopts the triplet directly when the loaded image is already a merge — main_window now hands the dialog the merge descriptor.
  • Each card of a multi-card session is itself a triplet; mixing a single shot in would combine samples from two device spaces.
  • The merge mode (single photosite / linear demosaic) is read from the global 3-way setting and shown in the label, not silently baked — it decides the space the fit happens in, so a hidden mode would be undiagnosable.

decode_target_merged sets is_merged/merge_sources on the same bare CCRImage that decode_target already builds. read_image dispatches on is_merged before it looks at the path or Positive mode, so the merged frame arrives in the identical bare device space (apply_input_icc=False → no field correction, no camera profile) and everything downstream — sampling, fit, quality report, ICC/DCP synthesis — is untouched. That's the "treat it as one raw" requirement, implemented as a decode-source change rather than new profiling maths.

Applying to merged images

_read_merged now applies the active profile in the same pipeline position as the RAW branch: field correction → slice ops → downsize → profile.

as_shot_wb is None on purpose. A merge has three source RAWs with three different camera_whitebalance values and none of them describes the merged channel balance — that's a property of the three lights and their exposures, so any single frame's metadata would be arbitrary. Since #123 a generated profile carries its own calibration neutral and ignores the as-shot value, which is exactly right here: a trichrome profile balances merged frames on the lighting rig it was calibrated on. A profile without a baked neutral takes the documented unbalanced path.

Device-space guard

Swapping the two kinds produces badly wrong colour with no symptom beyond "the colour looks off" — the exact failure this feature exists to remove, and now easy to hit with both kinds in one picker. So profiles record which space they describe: DCP private tag 52525 (LONG), ICC private tag CCRk (XYZType payload, mirroring CCRn). Both are absent on every existing profile, so "no tag" means normal and nothing already on disk changes meaning. A mismatch logs once per profile+kind pair rather than refusing — colour management is the user's call, and a hard block would be worse than a preview they can see is wrong. The wizard also defaults the name to "Camera trichrome …" so the two are distinguishable.

This guard is the one part of the PR the user didn't ask for. It's self-contained (the tag + is_trichrome + _warn_kind_mismatch) and can be dropped if you'd rather not carry it.

Verification

Full suite: 1112 passed, 9 skipped. Two order-dependent failures appear in one chunk (test_positive_mode, test_sprocket_mask) — I confirmed they reproduce identically on clean main at cca21b2, so they're pre-existing pollution, not this change. Both pass in isolation.

14 new tests in tests/test_trichrome_profile.py, following the merge tests' convention of monkeypatching the one rawpy-touching function: bare-device decode contract, triplet ordering, DCP and ICC apply on merged reads, apply_input_icc=False and the disable toggle, pipeline position asserted as an explicit order list, the baked-neutral claim, kind round-trip in all three containers (matrix ICC, cLUT ICC, DCP), tag well-formedness (ascending TIFF tags, LONG×1, neutral still round-trips beside it), and warn-once behaviour.

End-to-end on the real trichrome triplet in H:/TempPhoto/it8/3way — merge → decode → sample → fit → build both containers → apply on a merged read → is_trichrome round-trip all work, and the profiled read differs from the bare one as expected.

What is NOT verified: fit quality. That folder holds trichrome negatives, not a chart, and there's no trichrome shot of an IT8 anywhere on disk — so the ΔE from that run is meaningless and I'm not reporting it as a result. Shooting the IT8 under the three lights is the remaining validation step, and it's the one that will say whether the profile is actually good.

🤖 Generated with Claude Code

A trichrome capture is not the same device space as a normal Bayer shot: the
merge takes the red plane shot under red light, the green under green and the
blue under blue, so each channel's sensitivity is the sensor response times its
own light, and the channel balance comes from three independent exposures. Two
halves of that were missing.

Profiling. The IT8 wizard decoded exactly one file, so the space the user
actually scans in could not be profiled at all. Page 1 gains a "Trichrome: merge
3 RAWs" checkbox: Browse becomes a multi-select requiring exactly three RAWs,
sorted by basename exactly as an import sorts them so the wizard and a 3-way
import of the same files agree on which frame is red, green and blue. "Use
current image" adopts the triplet directly when the loaded image is already a
merge. decode_target_merged sets is_merged/merge_sources on the bare CCRImage
that decode_target already builds, and read_image dispatches on that before it
looks at the path - so the merged frame arrives in the same bare device space
(apply_input_icc=False: no field correction, no camera profile) and everything
downstream, sampling through ICC/DCP synthesis, treats it as one raw. The merge
mode is shown in the target label rather than silently baked, since it decides
the device space the fit happens in.

Applying. _read_merged applied field correction and the slice chain and then
returned - the DCP/ICC apply the RAW branch performs had no counterpart, so even
a correct profile did nothing on the frames it was built for. It now applies in
the same pipeline position (field correction -> slice ops -> downsize ->
profile). as_shot_wb is None on purpose: a merge has three source RAWs with three
different camera_whitebalance values and none describes the merged balance. Since
spec/camera-profile-calibration-wb.md a generated profile carries its own
calibration neutral and ignores the as-shot value, which is exactly right here.

Guard. Swapping the two kinds produces badly wrong colour with no symptom beyond
"the colour looks off", so profiles record which space they describe - DCP
private tag 52525, ICC private tag CCRk, both absent on every existing profile,
so "no tag" means normal. A mismatch logs once per profile+kind pair rather than
refusing: colour management is the user's call.

Verified end to end on the real trichrome triplet (merge, decode, fit, build,
apply, tag round-trip). Fit QUALITY is unverified - there is no trichrome shot of
an IT8 chart to test against, only negatives.

Spec: spec/trichrome-camera-profile.md

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant