Skip to content

fix(image): prevent NaN in spectral_angle_mapper when a pixel has zero norm#3424

Open
Teja1631 wants to merge 3 commits into
Lightning-AI:masterfrom
Teja1631:fix/spectral-angle-mapper-nan-zero-pixel
Open

fix(image): prevent NaN in spectral_angle_mapper when a pixel has zero norm#3424
Teja1631 wants to merge 3 commits into
Lightning-AI:masterfrom
Teja1631:fix/spectral-angle-mapper-nan-zero-pixel

Conversation

@Teja1631

Copy link
Copy Markdown

What does this PR do?

Fixes #3322spectral_angle_mapper (and SpectralAngleMapper) returned NaN whenever any pixel in the input had all-zero channels.

Root cause

In _sam_compute, the denominator preds_norm * target_norm is zero for any pixel whose L2 norm is zero. Dividing by zero before .acos() produces NaN, which propagates through the reduction to the final scalar.

# Before — NaN when any pixel norm is zero
sam_score = torch.clamp(dot_product / (preds_norm * target_norm), -1, 1).acos()

Fix

Clamp the denominator to the dtype's machine epsilon before dividing:

# After
denom = (preds_norm * target_norm).clamp(min=torch.finfo(preds.dtype).eps)
sam_score = torch.clamp(dot_product / denom, -1, 1).acos()

This matches the behaviour of F.cosine_similarity (which uses eps=1e-8 internally) and returns a valid angle instead of NaN for zero-norm pixels. The fix is in _sam_compute, so both the functional API and the SpectralAngleMapper class are covered.

Reproducer (from the issue)

import torch
from torchmetrics.functional.image import spectral_angle_mapper

a, b = torch.ones(2, 1, 3, 8, 8)
a[:, :, 5, 3] = 0  # zero-norm pixel
print(spectral_angle_mapper(a, b))  # was NaN, now a valid scalar

Tests

  • Added test_no_nan_on_zero_pixel — reproduces the exact input from the issue, asserts no NaN and result is in [0, π/2] for both the functional and class interfaces.
  • All 45 existing SAM tests pass.

Before submitting

  • Did you read the contributor guideline?
  • Did you make sure your PR addresses only one bug/feature?
  • Did you add/update tests for the bug fix?
  • Did you verify all existing tests still pass?

🤖 Generated with Claude Code

…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
Teja1631 added 2 commits June 26, 2026 12:22
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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 -> NaN before acos.
  • Add a regression test to ensure both functional and class interfaces do not produce NaN for 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.

Comment on lines +141 to +143
a, b = torch.ones(2, 1, 3, 8, 8)
a[:, :, 5, 3] = 0 # zero-norm pixel — exact reproducer from the issue

Comment on lines +77 to +79
# 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()
Comment on lines +144 to +151
# 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}"
Comment on lines +153 to +155
# 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-commenter

codecov-commenter commented Jun 28, 2026

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 0% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 31%. Comparing base (d184220) to head (41aab77).
⚠️ Report is 7 commits behind head on master.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

❗ There is a different number of reports uploaded between BASE (d184220) and HEAD (41aab77). Click for more details.

HEAD has 784 uploads less than BASE
Flag BASE (d184220) HEAD (41aab77)
torch2.0.1+cpu 20 2
python3.10 129 13
Windows 30 3
cpu 218 22
torch2.9.1 9 1
macOS 39 4
python3.12 89 9
torch2.9.1+cpu 30 3
torch2.8.0+cpu 30 3
torch2.8.0 10 1
Linux 149 15
torch2.10.0 10 1
torch2.7.1+cpu 20 2
torch2.10.0+cpu 20 2
torch2.4.1+cpu 10 1
torch2.2.2+cpu 10 1
torch2.1.2+cpu 10 1
torch2.5.1+cpu 10 1
torch2.0.1 10 1
torch2.3.1+cpu 9 1
torch2.6.0+cpu 10 1
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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@mergify mergify Bot added the ready label Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spectral_angle_mapper NaNs even with one zero pixel

4 participants