BUG: Fix JointHistogramMutualInformation metric marginals and derivative#6569
Conversation
There was a problem hiding this comment.
Thank you for contributing a pull request! 🙏
Welcome to the ITK community! 🤗👋☀️
We are glad you are here and appreciate your contribution. Please keep in mind our community participation guidelines. 📜
More support and guidance on the contribution process can be found in our contributing guide. 📖
This is an automatic message. Allow for time for the ITK community to be able to read the pull request and comment
on it.
1a85100 to
e4ba390
Compare
The ported ITK formula `ln2·dMmPDF·J/Pm − dJPDF·(lnJ − lnPm)` is not an
MI gradient: the marginal term's sign opposes the joint term's, the joint
term is weighted by a log ratio instead of 1/J, and ln2 multiplies one
term only. It admits no scalar potential at all (Clairaut: mixed partials
of the implied phi disagree by a factor of ln2), and this port reproduced
it deliberately, reasoning that no unambiguous fix existed.
That reasoning was wrong. The frozen-density MI gradient has exactly one
derivation: differentiate the sample's own log-likelihood ratio along the
moving axis, holding the density field fixed, giving
∂/∂m ln( J/(pf·pm) ) = dJPDF/J − dMmPDF/Pm
(pf drops — it does not depend on the moving intensity). Divided by ln2
for the bits `compute_value` reports, negated for this crate's +∇value
convention.
Measured at [1.3, -0.7] with 32 bins, the old formula returned
[1.29e-3, -5.32e-4] against a finite difference of its own value of
[0.140, -0.0802] — 108x and 151x too small. The new one returns
[0.134, -0.0698], ratios 0.96 and 0.87, the residual being the
structurally-excluded rebinning term of the hard-binned histogram.
Tests: `frozen_density_identity_matches_finite_difference` now pins the
weight against a finite difference of its exact potential `lnJ − ln mm`;
`derivative_matches_finite_difference_direction` gains a magnitude bound
the old formula could not pass; `gradient_descent_recovers_a_translated_blob`
tightens 0.5px → 0.05px and no longer needs lr=20 (the derivative's zero
now coincides with the value's argmin, so any lr in [0.5, 5] converges).
Submitted upstream as InsightSoftwareConsortium/ITK#6569.
|
Thanks for the exceptionally thorough write-up, @physwkim — the A few things to move this forward:
Code review — math & source verification (all three confirmed)1. 2. Marginal PDFs swapped — CORRECT. 3. Derivative — CORRECT in form and sign. The new weight Coupling. The derivative threader reads Code quality. The derivative rewrite also drops the old Downstream impact across the ITK forestNo public signature changes — every consumer compiles unchanged; only numerics shift (toward the true optimum). Only workflows that explicitly select the JointHistogram MI metric are affected; Mattes/CC/MSQ/Demons are untouched. SimpleITK —
BRAINSTools — BRAINSFit ANTs — Not affected — elastix / ITKElastix (own Baseline regeneration — checklist & how I can help
A companion impact report has been drafted for the migration-guide directory ( |
6618578 to
22191f3
Compare
|
@hjmjohnson Thanks for the independent verification and the downstream sweep.
|
…6569) New JointHistogramMutualInformation registration baselines required by the metric correction in InsightSoftwareConsortium/ITK#6569 (marginal-PDF pairing and MI-derivative fixes). The prior baselines encoded the pre-fix metric output and no longer match. primary (double): bafkreigqpynjmaslsa2ad5bvfvajnnpkbpzmjvwgb45ujq4tz6rokj2xo4 alternate (float): bafkreidario4d4czuebxmx2eqahh7enromv22i4c76buhgol2h5ysccofe Regenerated on macOS arm64 (single-threaded); Linux/Windows float alternates may be appended if CI on those platforms exceeds the comparison tolerance.
This comment was marked as resolved.
This comment was marked as resolved.
|
Pushed Diagnosis detail
|
|
@blowekamp SimpleITK — SetMetricAsJointHistogramMutualInformation() maps to the affected class. Testing/Unit/sitkImageRegistrationMethodTests.cxx pins EXPECT_NEAR(-0.52624100016564002, MetricEvaluate, 2e-6) for SetMetricAsJointHistogramMutualInformation(20,1.5). That pair is asymmetric (its JHMI value differs from its Mattes value), so the marginal fix moves the value past 2e-6 — the expected constant needs updating. @hjmjohnson BRAINSTools — BRAINSFit --costMetric MIH → the affected class. Three baseline-comparing tests: BRAINSFitTest_MIHAffineRotationMasks, BRAINSFitTest_MIHScaleSkewVersorRotationMasks, BRAINSFitTest_MIHMetricBrainToItself. They are mono-modal (near-symmetric marginals), so the pairing fix barely moves them but the derivative fix may shift convergence — run before/after to see if tolerance is breached. Default MMI (Mattes) and NMIH (a different, v3 class) are not affected. @ntustison ANTs — antsRegistration --metric MI[...] builds the affected class (the separate Mattes metric does not). ANTs' CTest suite only exercises the legacy ANTS binary (ANTS_MI_1/2), which uses ANTs' own internal MI — not affected. The real drift is in the pipeline scripts calling antsRegistration MI (antsRegistrationSyNQuick.sh, antsBrainExtraction.sh, antsCorticalThickness.sh, antsMultivariateTemplateConstruction2.sh, antsJointLabelFusion2.sh) — no pinned baselines, so no red CI, but user results change (improve). |
|
Thanks you very much, @hjmjohnson , for the heads-up. |
…ient ComputeJointPDFPoint() indexes the joint PDF as (fixed bin, moving bin). Summing along direction 0 therefore yields the moving marginal, but it was stored in m_FixedImageMarginalPDF, and the direction-1 sum in m_MovingImageMarginalPDF. GetValue() then divided p(a,b) by p_moving(a) * p_fixed(b), and the derivative threader interpolated the fixed marginal at a moving intensity. Fill each marginal from the axis it is named for. The derivative scaling factor log(2)*dMmPDF*J/Pm - dJPDF*(log J - log Pm) is not the differential of any MI estimate: the marginal term has the wrong sign and the joint term is weighted by a log ratio instead of 1/J, so the optimizer converges away from the truth that GetValue() is minimized at. Use the Parzen-window MI gradient (Viola-Wells) per sample, (dJPDF / J - dMmPDF / Pm) / log(2), in bits to match GetValue(). The fixes are coupled: the corrected gradient reads m_MovingImageMarginalPDF, which holds the moving marginal only after the pairing fix.
The JointHistogramMutualInformation metric correction changes the
registered output of itkSimpleImageRegistrationTest{Float,Double}; the
prior baselines encoded the old metric. Update the content links to the
regenerated data (primary = double, alternate .1 = float, alternate
.2 = float on x86_64) and drop the stale .3 alternate. Data uploaded to
ITKTestingData.
ComputeJointPDFPoint() maps intensities to a [0,1] bin axis, so the per-sample MI gradient (dJPDF/J - dMmPDF/Pm) differentiates with respect to the normalized coordinate. Apply the chain-rule factor d(bin)/d(intensity) = 1/(TrueMax - TrueMin) so GetDerivative() matches a finite difference of GetValue() independent of the moving image's intensity range; previously the magnitude scaled linearly with that range (e.g. 256x between 8-bit and 16-bit encodings of the same data). Clamp each log-slope term to |ln(eps)|/binwidth, the steepest slope the eps-guarded PDF can represent across one bin. The clamp does not engage on well-posed data (verified over benign, background-dominated, and engineered sparse-bin image pairs) and caps the worst-case per-sample weight at O(1/binwidth) instead of O(sample count). The derivative remains the gradient of the per-sample average estimator; it can differ from a finite difference of the plug-in histogram sum reported by GetValue() by the Parzen-smoothing mismatch (measured 0.68x-1.03x across histogram configurations).
The chain-rule rescaling of the JHMI derivative shifts the itkSimpleImageRegistrationTestFloat result on x86_64 slightly past the comparison tolerance; the double and arm64 float results remain within tolerance of the existing baselines. Replace the .2.nii alternate with the regenerated output. Data uploaded to ITKTestingData.
Never called; references m_ThreaderFixedImageMarginalPDFInterpolator and m_JointPDFSpacing, which do not exist on this threader. It compiled only because it was never instantiated.
The value-consistency and non-overlap checks printed failures but returned EXIT_SUCCESS; make them fail the test. Add a central finite-difference check of the analytic derivative at a detuned offset. Replace the raster-ramp test images, whose mutual information is translation-invariant up to boundary effects, with a smooth blob pair whose marginals differ; use enough histogram bins that the padded PDF has more than one interior interval, which otherwise degenerates the derivative stencils to zero.
a2f580a to
bfb0315
Compare
|
Branch updated (rebased onto current New history (6 commits):
Why commits 3–6 (review findings on top of the confirmed fixes)Chain-rule factor (commit 3). Sparse-bin clamp (commit 3). The Known property (documented, not changed): the derivative is the gradient of the per-sample average estimator; it can differ from a finite difference of the plug-in sum reported by Baselines (commits 2, 4). Only the x86_64 float result moves past tolerance under the rescale (error 3.0–17.9 vs 3.0/8px against the previous alternates); double and arm64-float remain within tolerance of the commit-2 baselines, so only the Dead helper (commit 5). Test enforcement (commit 6). The unit test's failure paths printed to stderr but returned Validation: full |
Companion to the ITK v6 migration guide documenting the behavior change in itk::JointHistogramMutualInformationImageToImageMetricv4 (marginal-PDF pairing and derivative) and the ScaleLogarithmicTransform Jacobian fix from InsightSoftwareConsortium#6569, plus the downstream consumers (SimpleITK, BRAINSTools BRAINSFit MIH, ANTs antsRegistration MI) whose regression baselines shift and need regeneration.
Two coupled bugs in
JointHistogramMutualInformationImageToImageMetricv4: each marginal PDF is paired with the wrong joint-PDF axis, and the derivative is not an MI gradient. The independentScaleLogarithmicTransformJacobian fix was split out to #6572 per review.itkSimpleImageRegistrationTest{Float,Double}baselines encode the old metric's output and need regeneration — @hjmjohnson has offered to push regenerated.cidlinks to this branch.1. Marginal PDFs are swapped — proven from GetJointPDF()
ComputeJointPDFPoint(.hxx:396-397) writes the fixed intensity to axis 0and the moving intensity to axis 1, and
itkJointHistogramMutualInformationComputeJointPDFThreaderBase.hxx:83indexesthe histogram directly with that point. So
m_JointPDFis(fixed bin, moving bin).InitializeForIterationwalked it withlinearIter.SetDirection(0), whichsums along axis 0 (over fixed bins) and indexes lines by axis 1. That sum is
p_moving, but it was stored intom_FixedImageMarginalPDF; theSetDirection(1)loop storedp_fixedintom_MovingImageMarginalPDF. Thecomment on the first loop — "Compute moving image marginal PDF by summing over
fixed image bins" — already describes what the code computes, and contradicts
the array it writes into.
GetValue()(.hxx:353-360) then readspx = m_FixedImageMarginalPDF[ii],py = m_MovingImageMarginalPDF[jj]againstm_JointPDF(ii, jj), i.e. itdivides
p(a,b)byp_moving(a) * p_fixed(b).Evidence. A standalone program builds the metric on an asymmetric pair
(moving intensities are the cube of the fixed ones, so the two marginals are
genuinely different), then recomputes both candidate values from
GetJointPDF()alone, summing each axis by hand:GetValue()onmainmatches the swapped pairing to 12 significant digits.Why no test ever caught it: the only numeric registration test of this metric
(
itkImageToImageMetricv4RegistrationTest2) registers an image against acyclic shift of itself, so
p_fixedandp_movingare near-identicaldistributions and the swap is invisible there by construction.
2. The derivative is not an MI gradient — evidence and fix
itkJointHistogramMutualInformationGetValueAndDerivativeThreader.hxx:147-151weighted each sample by
This is not the differential of any MI estimate: the marginal term enters
with the wrong sign, the joint term is weighted by a log ratio instead of
1/J, andlog(2)multiplies one term only. For the interpolated-PDFestimate this class uses, the standard Parzen-window MI gradient
(Viola & Wells) weight is
(the
pffactor drops because it does not depend on the moving intensity).The commit uses
(dJPDF / J - dMmPDF / Pm) / log(2), in bits to matchGetValue().This commit depends on the marginal-pairing fix: the threader reads
m_MovingImageMarginalPDFfor bothPmanddMmPDF, which only holdsp_movingafter that fix. The two commits must land together.Direction oracle. On the registration test's own image pair, the JH-MI
derivative should be parallel to the Mattes derivative — both estimate the
same MI, and Mattes' derivative is finite-difference-consistent with its
value (
analytic / d(value) = -0.99on this problem; the JH value itself ishard-binned and piecewise-constant, so it admits no finite-difference check).
Cosine between JH and Mattes derivatives, at off-diagonal offsets where the
test is informative (true shift
[2, 5]):(0, 0)(1, 3)(3, 5.14)(2.83, 5.28)With the old formula the derivative field vanishes near
(3, 5.14)— a fullpixel from the optimum — which is where gradient descent then converges.
Convergence.
itkImageToImageMetricv4RegistrationTest(true shift[2, 5]; endpoints identical at 500 and 2000 iterations, i.e. trueequilibria;
ITK_GLOBAL_DEFAULT_NUMBER_OF_THREADS=1, bit-reproducible):[3.076, 5.143]— x 35 % off[2.487, 5.195]— x 19.6 % off[2.101, 5.014]— x 4.8 %[2.014, 5.014]— x 0.7 %Note the shipped code only ever passed this test as an early-stopping
accident: at its converged equilibrium,
mainitself violates the test's11 % tolerance. The 47-iteration budget froze the trajectory while it
happened to be passing near the truth. A separate value-surface scan shows
the argmin of
GetValue()is exactly[2, 5]on every tree — the optimizerwas being led away from it by the derivative.
An earlier revision of this PR carried a different commit that merely
moved the
log(2)factor. It was withdrawn as unjustified; this commitreplaces the formula with a derivable one and the empirical record above.
Test impact — local run with ExternalData fetched
Local suite (
ctest -L 'ITKMetricsv4|ITKRegistrationMethodsv4', 61 tests,data fetched via the
ITKDatatarget):main: 0 failuresitkSimpleImageRegistrationTestFloat,itkSimpleImageRegistrationTestDoubleitkImageToImageMetricv4RegistrationTest2, which failed in this PR's firstCI run, passes unchanged with the derivative fix — no test retuning was
needed.
An earlier revision of this PR claimed no test encoded the old behavior; that
was wrong (I had audited only
Metricsv4/test/) and is retracted.Remaining red tests — baselines need regeneration
itkSimpleImageRegistrationTest{Float,Double}compare their registered outputagainst baseline images (
Baseline/itkSimpleImageRegistrationTest*.nii.cid)that were generated with the old metric. Any change to the metric changes the
output, so these comparisons fail by construction (local
ImageError13426against the old baseline; the test's own internal checks all pass).
The baselines are ExternalData content links; regenerating them requires
uploading new content to data.kitware.com, which I have no account for.
@hjmjohnson has offered to regenerate them from this branch and push the
.cidlinks here.AI assistance
of ITKv4 registration against this source; wrote the marginal-pairing probe,
the value-surface scan, the Mattes direction oracle, and the local bisects
ITK_BUILD_DEFAULT_MODULES=OFF,Module_ITKRegistrationMethodsv4=ON,Module_ITKMetricsv4=ON, Release,ExternalData fetched). Full
ITKMetricsv4+ITKRegistrationMethodsv4suites run on
mainand on this branch; per-commit bisects of the failingtest; all probe programs run per tree with threads pinned to 1.
log(2)commit) were retracted in place rather than quietly amended.