fix(image): prevent NaN in spectral_angle_mapper when a pixel has zero norm#3424
fix(image): prevent NaN in spectral_angle_mapper when a pixel has zero norm#3424Teja1631 wants to merge 3 commits into
Conversation
…norm When preds or target contains a pixel with all-zero channels, its L2 norm is zero, causing division by zero (NaN) before torch.clamp().acos(). Fix: clamp the denominator (preds_norm * target_norm) to dtype's machine epsilon before dividing. Fixes both the functional and class interfaces since SpectralAngleMapper delegates to _sam_compute. Adds regression test reproducing the exact input from issue Lightning-AI#3322. Fixes Lightning-AI#3322
Pixels where all channels are zero produce a zero L2 norm, causing 0/0 = NaN in spectral_angle_mapper. Clamp the denominator by torch.finfo(dtype).eps before dividing. Fixes Lightning-AI#3322
There was a problem hiding this comment.
Pull request overview
This PR fixes spectral_angle_mapper / SpectralAngleMapper returning NaN when any pixel has zero L2 norm by stabilizing the normalization denominator in the SAM computation.
Changes:
- Clamp the SAM normalization denominator to an epsilon to avoid
0/0 -> NaNbeforeacos. - Add a regression test to ensure both functional and class interfaces do not produce
NaNfor zero-norm pixels.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/torchmetrics/functional/image/sam.py |
Stabilizes SAM computation by clamping the denominator to avoid NaNs on zero-norm pixels. |
tests/unittests/image/test_sam.py |
Adds a regression test covering the zero-norm pixel case for both functional and class APIs. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| a, b = torch.ones(2, 1, 3, 8, 8) | ||
| a[:, :, 5, 3] = 0 # zero-norm pixel — exact reproducer from the issue | ||
|
|
| # Clamp denominator to avoid NaN when a pixel has zero norm (all-zero channels) | ||
| denom = (preds_norm * target_norm).clamp(min=torch.finfo(preds.dtype).eps) | ||
| sam_score = torch.clamp(dot_product / denom, -1, 1).acos() |
| # functional interface | ||
| result = spectral_angle_mapper(a, b) | ||
| assert not torch.isnan(result), f"spectral_angle_mapper returned NaN: {result}" | ||
|
|
||
| # class interface | ||
| metric = SpectralAngleMapper() | ||
| result_cls = metric(a, b) | ||
| assert not torch.isnan(result_cls), f"SpectralAngleMapper returned NaN: {result_cls}" |
| # Result should be a valid angle in [0, pi/2] | ||
| assert result >= 0, f"result is negative: {result}" | ||
| assert result <= torch.pi / 2, f"result exceeds pi/2: {result}" |
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #3424 +/- ##
========================================
- Coverage 37% 31% -5%
========================================
Files 349 349
Lines 19901 19906 +5
========================================
- Hits 7264 6197 -1067
- Misses 12637 13709 +1072 🚀 New features to boost your workflow:
|
What does this PR do?
Fixes #3322 —
spectral_angle_mapper(andSpectralAngleMapper) returnedNaNwhenever any pixel in the input had all-zero channels.Root cause
In
_sam_compute, the denominatorpreds_norm * target_normis zero for any pixel whose L2 norm is zero. Dividing by zero before.acos()producesNaN, which propagates through the reduction to the final scalar.Fix
Clamp the denominator to the dtype's machine epsilon before dividing:
This matches the behaviour of
F.cosine_similarity(which useseps=1e-8internally) and returns a valid angle instead ofNaNfor zero-norm pixels. The fix is in_sam_compute, so both the functional API and theSpectralAngleMapperclass are covered.Reproducer (from the issue)
Tests
test_no_nan_on_zero_pixel— reproduces the exact input from the issue, asserts noNaNand result is in[0, π/2]for both the functional and class interfaces.Before submitting
🤖 Generated with Claude Code