feat(it8): average the white balance over the grey wedge's midtone band - #124
Open
toonoumi wants to merge 1 commit into
Open
feat(it8): average the white balance over the grey wedge's midtone band#124toonoumi wants to merge 1 commit into
toonoumi wants to merge 1 commit into
Conversation
The camera fit read its white balance off ONE patch — `_pick_wb_id`'s lightest near-neutral. Since profiles started baking their calibration neutral, that single measurement's noise, dust or specular sheen rides into every frame the profile is applied to. On the maintainer's chart (LaserSoft ISO 12641-2 advanced, block A49-L72) the patch it picked was `C49`: L*87.25, chroma 1.40 — the near-white left end of a dye ramp, not a designed neutral, and the one most likely to clip a channel. The chart's 96-step grey wedge in rows G-J contributed nothing at all. `fit_camera_matrix` now averages over the midtone band of the ramp (`WB_L_RANGE`, L* 24-64: rows H-I of that wedge, roughly GS16-GS22 of a classic strip) — below the highlight end where a channel clips, above the shadows where veiling flare and black-level error bend the channel ratios, which is the very quantity being estimated. Each patch is corrected against the colour the REFERENCE says it is, not forced to equal-RGB. That is not an optimisation: the wedge carries a systematic cast (chroma 3.0-4.8 across H49-I72, mean a* -3.51, b* -2.09), and a systematic cast does not average out — a naive ratio average would bake it into the neutral with the sign flipped. Because `wb` enters the fit only through the D50 pin, the correction is a short fixed point that settles in one or two passes, with MAD outlier rejection so one bad patch cannot pull it. Measured on the real chart shot: the grey wedge's mean chroma error against its reference drops 3.548 -> 1.367 (max 4.370 -> 2.345). The anchor patch is unchanged — it still sets the exposure/white-relative normalisation and the cLUT residual anchor; only the channel ratios move. Charts with fewer than three usable candidates keep the old single-patch behaviour exactly. Spec: spec/wb-neutral-range-average.md Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
The camera fit read its white balance off one patch —
_pick_wb_id's lightest near-neutral. Since #123 made profiles bake their calibration neutral, that single measurement's noise, dust or specular sheen rides into every frame the profile is applied to.On the maintainer's chart — LaserSoft ISO 12641-2 advanced, Provia 100F, block
A49–L72— the patch it picked wasC49: L*87.25, chroma 1.40. That's the near-white left end of a dye ramp, not a designed neutral, and the patch most likely to clip a channel. The chart's real neutrals, a 96-step grey wedge in rows G–J, contributed nothing.The change
fit_camera_matrixnow averages the WB over the midtone band of the grey ramp —WB_L_RANGE, L* 24–64, which is rows H–I of that wedge and roughlyGS16–GS22of a classic 24-step strip. The band avoids both ends deliberately: above it a channel clips and the chart's Dmin flattens; below it veiling flare and black-level error bend the channel ratios, which is exactly the quantity being estimated.Each patch is corrected against its reference Lab, not forced to equal-RGB. This is not an optimisation. The wedge is not colorimetrically neutral — across H49–I72 its chroma runs 3.0–4.8 with mean a* −3.51, b* −2.09 — and a systematic cast does not average out. Forcing those patches to grey would bake the cast into the neutral with the sign flipped. So each patch is measured against
M⁻¹·X_ref, the device value that would actually produce its measured colour, which is what makes non-neutral candidates safe to average in the first place.Because balancing is diagonal and
Mis a full 3×3 least squares,wbenters the render only through the D50 pin — thediag(wb)factors cancel out of the correction (spec §4.3). So this is a short fixed point that settles in one or two passes rather than fighting the matrix fit, with per-channel MAD outlier rejection so one dusty patch can't pull it.The anchor patch is unchanged: it still sets the exposure scale, the white-relative reference normalisation, and the cLUT residual anchor. Only the channel ratios move.
Measured on the real chart
Sampling
DSC07095.ARWagainstE210616.cxfthrough the real sampling + fit path. The block quad was aligned by minimising within-patch variance (an objective that uses no reference data, so it cannot bias the comparison), and the ΔE recomputation was cross-checked against the fit's ownavg_deto the third decimal.This is a trade, not a free win. The neutral axis improves a lot — including the greys that were not in the average, which is genuine generalisation and not just fitting the patches we optimised. Overall ΔE improves 8.2%. But the saturated colour patches, which are held out of the WB estimate entirely, get 10% worse. That follows from the algebra in §4.3:
wbacts only through the D50 pin, so it is a diagonal in XYZ that trades error between the neutral axis and everything else. Pinning white where the greys say it is moves error onto the chromatic patches.Which band is best (same shot, same alignment)
Every band beats the single patch overall. But the estimate drifts monotonically as the band moves into the shadows (
wb_mult[0]0.865 → 0.781), exactly the veiling-flare/black-level signature §2 predicts — and accuracy degrades with it.G+His the optimum: essentially the same neutral accuracy as the shippedH+I(2.618 vs 2.647) with markedly better colour (3.331 vs 3.914) and the best overall (3.094 vs 3.492).The shipped default is
H+Ibecause that is what was specified. ChangingWB_L_RANGEto(44.0, 88.0)selectsG+Hinstead; the sweep above is the evidence for doing so.Compatibility
wb_ids == [wb_id].CameraFitgainswb_ids; it's transient (built by the wizard, consumed by the builders), so no persistence or migration.color_management.pyanddcp_profile.pyare untouched — they consumefit.wb_mult, so the better neutral flows into the ICCCCRntag and the DCPAsShotNeutralautomatically.wb_ids=[...]for an explicit list (skips the neutrality/band gates),wb_l_range=(lo,hi)for a different band.wb_ids=[]disables averaging.Tests
1076 passed, 9 skipped, 0 failed (suite run in chunks; the full run's pre-existing order-dependent hang is unrelated).
10 new tests in
tests/test_it8_profile.py: exact-neutral parity, the systematic-cast regression (asserts the naive average is biased and the corrected one isn't), noise averaging over a seed sweep, outlier rejection, band selection and both overrides, single-patch fallback, invalid/noise-floor exclusion, anchor-independence + convergence, and that the averaged neutral reaches both baked containers.One fixture note worth flagging for review: a pure grey ramp is rank-deficient for a 3×3 fit, so the wedge fixtures include colour patches — with any accidentally near-neutral one filtered out, so the wedge stays the only candidate set.
🤖 Generated with Claude Code