From b9fb51f4e074a73073a2d50bba40a7907fd25a06 Mon Sep 17 00:00:00 2001 From: Teja Chunchu Date: Fri, 26 Jun 2026 12:13:32 -0400 Subject: [PATCH 1/5] fix(image): prevent NaN in spectral_angle_mapper when pixel has zero 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 #3322. Fixes #3322 --- src/torchmetrics/functional/image/sam.py | 4 +++- tests/unittests/image/test_sam.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/torchmetrics/functional/image/sam.py b/src/torchmetrics/functional/image/sam.py index af5edb5f41e..15e986f9eaf 100644 --- a/src/torchmetrics/functional/image/sam.py +++ b/src/torchmetrics/functional/image/sam.py @@ -74,7 +74,9 @@ def _sam_compute( dot_product = (preds * target).sum(dim=1) preds_norm = preds.norm(dim=1) target_norm = target.norm(dim=1) - sam_score = torch.clamp(dot_product / (preds_norm * target_norm), -1, 1).acos() + # 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() return reduce(sam_score, reduction) diff --git a/tests/unittests/image/test_sam.py b/tests/unittests/image/test_sam.py index 57a4dee1b95..45888009a24 100644 --- a/tests/unittests/image/test_sam.py +++ b/tests/unittests/image/test_sam.py @@ -130,3 +130,25 @@ def test_error_on_grayscale_image(metric_class=SpectralAngleMapper): metric = metric_class() with pytest.raises(ValueError, match="Expected channel dimension of `preds` and `target` to be larger than 1.*"): metric(torch.randn([16, 1, 16, 16]), torch.randn([16, 1, 16, 16])) + + +def test_no_nan_on_zero_pixel(): + """Regression test for https://github.com/Lightning-AI/torchmetrics/issues/3322. + + spectral_angle_mapper should not produce NaN when a pixel has zero norm + (all channels zero). Previously, the zero-norm denominator caused 0/0 = NaN. + """ + a, b = torch.ones(2, 1, 3, 8, 8) + a[:, :, 5, 3] = 0 # zero-norm pixel — exact reproducer from the issue + + # 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 and result <= torch.pi / 2, f"result out of range: {result}" From 37677508f7cb2a3c5344db84f9c7926e0cdc56ba Mon Sep 17 00:00:00 2001 From: Teja Chunchu Date: Fri, 26 Jun 2026 12:22:02 -0400 Subject: [PATCH 2/5] fix(sam): clamp denominator to avoid NaN on zero-norm pixels 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 #3322 --- tests/unittests/image/test_sam.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unittests/image/test_sam.py b/tests/unittests/image/test_sam.py index 45888009a24..96f593b8435 100644 --- a/tests/unittests/image/test_sam.py +++ b/tests/unittests/image/test_sam.py @@ -151,4 +151,5 @@ def test_no_nan_on_zero_pixel(): 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 and result <= torch.pi / 2, f"result out of range: {result}" + assert result >= 0, f"result is negative: {result}" + assert result <= torch.pi / 2, f"result exceeds pi/2: {result}" From 41aab779d1dc54936798ab70ee3c098019a1cc9a Mon Sep 17 00:00:00 2001 From: Teja Chunchu Date: Fri, 26 Jun 2026 21:48:45 -0400 Subject: [PATCH 3/5] ci: retrigger unit tests From d93ee2473d7b72fb475389bca3f1989853be19bf Mon Sep 17 00:00:00 2001 From: Teja Chunchu Date: Thu, 23 Jul 2026 21:43:01 -0400 Subject: [PATCH 4/5] test(sam): clarify zero-norm regression test per review --- tests/unittests/image/test_sam.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/unittests/image/test_sam.py b/tests/unittests/image/test_sam.py index 96f593b8435..982b71e6c7b 100644 --- a/tests/unittests/image/test_sam.py +++ b/tests/unittests/image/test_sam.py @@ -138,18 +138,19 @@ def test_no_nan_on_zero_pixel(): spectral_angle_mapper should not produce NaN when a pixel has zero norm (all channels zero). Previously, the zero-norm denominator caused 0/0 = NaN. """ - a, b = torch.ones(2, 1, 3, 8, 8) - a[:, :, 5, 3] = 0 # zero-norm pixel — exact reproducer from the issue + preds = torch.ones(1, 3, 8, 8) # N, C, H, W + target = torch.ones(1, 3, 8, 8) + preds[:, :, 5, 3] = 0 # zero-norm pixel — exact reproducer from the issue # functional interface - result = spectral_angle_mapper(a, b) - assert not torch.isnan(result), f"spectral_angle_mapper returned NaN: {result}" + result = spectral_angle_mapper(preds, target) + assert torch.isfinite(result).all(), f"spectral_angle_mapper returned non-finite value: {result}" # class interface metric = SpectralAngleMapper() - result_cls = metric(a, b) - assert not torch.isnan(result_cls), f"SpectralAngleMapper returned NaN: {result_cls}" + result_cls = metric(preds, target) + assert torch.isfinite(result_cls).all(), f"SpectralAngleMapper returned non-finite value: {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}" + assert (result >= 0).all(), f"result is negative: {result}" + assert (result <= torch.pi / 2).all(), f"result exceeds pi/2: {result}" From dbbde2c7b08b2149b67cb6e682e187d6242fe18d Mon Sep 17 00:00:00 2001 From: Teja Chunchu Date: Thu, 23 Jul 2026 21:46:28 -0400 Subject: [PATCH 5/5] docs: add CHANGELOG entry for SAM zero-norm fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9518750113a..1ff42986da3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - Fixed malformed LaTeX in `CLIPScore` and `HausdorffDistance` docstring math so it renders correctly ([#3427](https://github.com/Lightning-AI/torchmetrics/pull/3427)) +- Fixed `NaN` in `SpectralAngleMapper` / `spectral_angle_mapper` when a pixel has zero norm ([#3424](https://github.com/Lightning-AI/torchmetrics/pull/3424)) ---