Skip to content

Commit 10674bc

Browse files
Fix division by zero in Warp for singleton spatial dimensions (#8946)
### Description `Warp.forward` (native `grid_sample` path) normalizes grid coordinates with: ```python grid[..., i] = grid[..., i] * 2 / (dim - 1) - 1 ``` When a spatial dimension has size 1 — a **single-slice volume** `(B, C, 1, H, W)` or a single-row/column image `(B, C, 1, W)` / `(B, C, H, 1)` — `dim - 1 == 0`, so the normalization divides by zero and produces `inf`/`nan` coordinates. With `padding_mode="zeros"`, even a **zero displacement field** then yields: - `nan` output for 2D single-row/column inputs, and - all-zeros output for 3D single-slice inputs, instead of returning the input image unchanged. #### Fix Clamp the denominator to 1 so the lone voxel maps to `-1`. This mirrors the guard MONAI already applies in `monai.networks.utils.normalize_transform`, which performs the identical `align_corners=True` normalization and clamps the size with `norm[norm <= 1.0] = 2.0` to avoid exactly this division by zero. Non-singleton dimensions are numerically unchanged. ```python grid[..., i] = grid[..., i] * 2 / max(dim - 1, 1) - 1 ``` #### Verification Added `test_singleton_spatial_dim` (2D single-row, 2D single-column, 3D single-slice). With a zero displacement field the warped output now equals the input and contains no `nan`. The test fails on the current code and passes with the fix; the existing `Warp` tests are unaffected. ### Types of changes <!--- Put an `x` in all the boxes that apply, and remove the not applicable items --> - [x] Non-breaking change (fix or new feature that would not break existing functionality). - [x] New tests added to cover the changes. - [x] Quick tests passed locally by running `./runtests.sh --quick --unittests --disttests`. - [x] In-line docstrings updated. --------- Signed-off-by: VenkateswarluNagineni <venkates2002@tamu.edu>
1 parent 36a6f0b commit 10674bc

2 files changed

Lines changed: 27 additions & 1 deletion

File tree

monai/networks/blocks/warp.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ def forward(self, image: torch.Tensor, ddf: torch.Tensor):
155155

156156
if not _use_compiled: # pytorch native grid_sample
157157
for i, dim in enumerate(grid.shape[1:-1]):
158-
grid[..., i] = grid[..., i] * 2 / (dim - 1) - 1
158+
# guard against a singleton spatial dim (e.g. a single-slice volume), where
159+
# ``dim - 1 == 0`` would divide by zero; clamp the denominator to 1 so the lone
160+
# voxel maps to -1, matching ``monai.networks.utils.normalize_transform``.
161+
grid[..., i] = grid[..., i] * 2 / max(dim - 1, 1) - 1
159162
index_ordering: list[int] = list(range(spatial_dims - 1, -1, -1))
160163
grid = grid[..., index_ordering] # z, y, x -> x, y, z
161164
return F.grid_sample(

tests/networks/blocks/warp/test_warp.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import unittest
1414
from pathlib import Path
15+
from unittest import mock
1516

1617
import numpy as np
1718
import torch
@@ -138,6 +139,28 @@ def test_ill_shape(self):
138139
with self.assertRaisesRegex(ValueError, ""):
139140
warp_layer(image=torch.arange(4).reshape((1, 1, 2, 2)).to(dtype=torch.float), ddf=torch.zeros(1, 2, 3, 3))
140141

142+
@mock.patch("monai.networks.blocks.warp.USE_COMPILED", False)
143+
def test_singleton_spatial_dim(self):
144+
"""
145+
Regression test for a singleton spatial dimension (a single-slice volume or a
146+
single-row/column image), where the grid normalization ``* 2 / (dim - 1)`` previously
147+
divided by zero.
148+
149+
The native ``grid_sample`` path is forced via ``USE_COMPILED=False`` because only that
150+
branch normalizes the grid; the csrc ``grid_pull`` path is unaffected. ``padding_mode``
151+
is ``"zeros"`` so an out-of-range (pre-fix ``nan``) coordinate maps to 0 and exposes the
152+
bug, whereas ``"border"``/``"reflection"`` would clamp onto the lone voxel and mask it.
153+
For a zero displacement field the warped output must contain no ``nan`` and must equal
154+
the input image.
155+
"""
156+
for shape, ndim in [((1, 1, 1, 4, 4), 3), ((1, 1, 1, 5), 2), ((1, 1, 5, 1), 2)]:
157+
image = torch.rand(*shape)
158+
ddf = torch.zeros(shape[0], ndim, *shape[2:])
159+
warp_layer = Warp(mode="bilinear", padding_mode="zeros")
160+
result = warp_layer(image, ddf)
161+
self.assertFalse(torch.isnan(result).any(), f"NaN in warp output for shape {shape}")
162+
np.testing.assert_allclose(result.cpu().numpy(), image.cpu().numpy(), rtol=1e-4, atol=1e-4)
163+
141164
def test_grad(self):
142165
for b in GridSampleMode:
143166
for p in GridSamplePadMode:

0 commit comments

Comments
 (0)