[fix] interpolate inside the bracket in the DNG solver (#325) - #327
Merged
antond-weta merged 1 commit intoSep 13, 2026
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #327 +/- ##
==========================================
+ Coverage 91.97% 92.01% +0.03%
==========================================
Files 17 17
Lines 3814 3830 +16
Branches 562 567 +5
==========================================
+ Hits 3508 3524 +16
Misses 306 306
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
thc1006
force-pushed
the
fix/dng-solver-bracket
branch
from
September 11, 2026 09:52
f7c589f to
a8c6148
Compare
5 tasks
thc1006
force-pushed
the
fix/dng-solver-bracket
branch
from
September 11, 2026 11:09
a8c6148 to
48dedda
Compare
…reFoundation#325) find_camera_to_XYZ_matrix sweeps the mired range between the two calibration illuminants and, once the error changes sign, interpolates between the last two samples. The interpolation added the correction where it should subtract it, so the estimate landed outside the bracket, as far past the current sample as the root is before it. On the test fixture the reported illuminant was 5317K, where the error is 8.4 mired, instead of 5566K, where it is 0.0001 mired. Two smaller problems in the same loop: a sample skipped because its matrix was not invertible left last_error at zero, so the next sample always looked like a sign change and produced twice its mired value; and the loop stopped before high_mired, so a neutral shot under the warmer calibration illuminant could not be matched exactly. Fix the sign, track whether a previous valid sample exists, and sample high_mired as the last step. A DNG with a single colour matrix arrives here with illuminant 0 and an all-zero second matrix. It only produced the right result because the zero matrix was skipped and the bogus sign change then pushed the estimate out of range, where the weight clamps back to the first matrix. With the sweep fixed it would interpolate towards the zero matrix instead, so, like the DNG SDK, the sweep now only runs when the second illuminant is known and its matrix is present; otherwise the first matrix is used as is. The DNG goldens recorded from the old estimate are updated, and the new tests fail on the previous code. Resolves AcademySoftwareFoundation#325. Assisted-by: Claude Code / Claude Opus 5 Claude-Session: https://claude.ai/code/session_01UUqNrkHJbRcyzA8NxvSsoQ Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
antond-weta
force-pushed
the
fix/dng-solver-bracket
branch
from
September 13, 2026 20:41
48dedda to
bbf03fe
Compare
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.
Description
This fixes the linear interpolation step in
find_camera_to_XYZ_matrixand two smaller problems in the same loop. The numbers and the reproduction are in #325.The interpolation between the two samples that bracket the root used
current_mired + ...where it needscurrent_mired - ..., so the estimate landed outside the bracket. On the test fixture the solver reported 5317K, where the error is 8.4 mired; the interpolated root is at 5566K, where it is 0.0001 mired.The loop now keeps track of whether a previous valid sample exists. Before, a sample skipped because its matrix was not invertible left
last_errorat 0, which made the next sample look like a sign change and produced twice its mired value. The sweep also ends with a sample athigh_miredinstead of stopping just below it, so a neutral under the warmer calibration illuminant is matched exactly. Identical illuminants still fall back to the first matrix, as before.Single colour matrix
A DNG with only ColorMatrix1 arrives with
calibration[1].illuminant == 0and an all-zero second matrix. It came out right on main by accident: the zero matrix at the first sample is skipped, the bogus sign change then returns twice the next sample's mired, that is out of range, and the weight clamps back to ColorMatrix1. With the sweep fixed it would interpolate towards the zero matrix and return the matrix scaled by 33x on the fixture (see the comment on #325). So the sweep now only runs when the second illuminant is known and its matrix is present and not all zeros, which is the condition the DNG SDK uses (HasColorMatrix2()plus both temperatures known); otherwise the first matrix is used as is. That also covers aMetadatabuilt by hand with one calibration, where the second matrix is empty and the old code read past its end, and the case of CalibrationIlluminant2 present without ColorMatrix2, where main returned a matrix off by 20% and the first version of this PR by 7x. An illuminant2 of 0 (EXIF "unknown") next to a valid ColorMatrix2 was interpolated as 5500K before and is ignored now, as in the SDK.An empty range (identical illuminants, or both beyond the same clamp) takes no samples and falls back to the first matrix as before.
neutral_RGBwith fewer than three values is rejected like an empty one instead of being read past its end.About the goldens
The DNG goldens in testDNGIdt.cpp, usage_example_core.cpp, python/test_core_bindings.py and python/test_image_converter.py were recorded from the old estimate (188.05 mired) and change to the values at 179.64 mired. I did not tune them by hand, they are what the fixed code produces; the residual of the solver at the new estimate is 1.5e-4 mired versus 8.4 mired before, which is the reason I think they are right to change.
The doc comment of the function said "binary search", which it never was; I changed it to describe the sweep.
Overlap with #277
Two lines in the same loop are touched by #277 too. A 3-way merge resolves it by itself (checked both orders); if it does not for some reason, whichever lands second I'll rebase.
Tests
testIDT_FindCameraToXYZMtx: golden updated, 5317k to 5566k.testIDT_FindCameraToXYZMtx_NonInvertibleFirstSample: singular ColorMatrix1. The first sample is skipped and the result is the closest valid sample (6295k) instead of twice its mired (3147k).testIDT_FindCameraToXYZMtx_MatchAtHighMired: neutral built for 2856K, the warmer calibration illuminant. Comes back as the first calibration matrix. Only the matrix is checked: the logged temperature is 2856.00002K, too close to theint()boundary to pin the string.testIDT_FindCameraToXYZMtx_SingleCalibration: second illuminant 0 with an all-zero and with an empty second matrix, and a known second illuminant with an all-zero matrix; the result is the inverse of ColorMatrix1 and nothing is interpolated. On main the three variants report 2676k, 2676k and 3147k.testIDT_FindCameraToXYZMtx_IdenticalIlluminants: both calibrations for 2856K; the first matrix comes back with no message, as before.testIDT_FindCameraToXYZMtx_KnownIlluminants: neutrals built from the interpolated matrix at 3000K, 3700.5K, 4500K, 5500K and 6200K; the solver has to return that matrix within 1e-5. All five fail on the previous code (off by several mired) and pass now within 3e-6.I checked that every new or updated assertion fails when the old rawtoaces_core.cpp is swapped back in.
I also ran the sweep on 20k generated fixtures (fixture's calibration matrices, random illuminant pairs, target CCT with a known answer). For targets inside the calibration range the estimate was off by 4.59 mired median / 8.96 p90 before and 0.0007 / 0.0145 after; 12294 of 14082 fixtures were off by more than 1 mired before, 18 after (those have more than one root in the range, the solver picks the first).
Full ctest 17/17, install and config_tests on Ubuntu 24.04 with gcc 13.3, also with C++20 and with Eigen off; ASan+UBSan with clang 18 on the tests, tests/run_valid.sh and 60k fuzzed metadata sets (random and degenerate illuminants, matrices and neutrals) clean; CodeChecker (clangsa + clang-tidy) and clang-format 16 clean.
Checklist:
Assisted-by: TOOL / MODELline in the pull request description above.Assisted-by: Claude Code / Claude Opus 5
(Used to write the probe that replicates the sweep, to run the fixture sweep above and the CI steps locally, and to draft the tests; I reviewed the change and the goldens myself.)