Skip to content

Commit e0cee14

Browse files
Fix unspecified spatial size for rank-one resampling and add NIfTI regression tests (#9191)
Related to #9166. ### Description The current dev branch already includes the image writer spatial-rank fix. This PR handles the remaining rank-one case in spatial_resample: when spatial_size is None, use the input spatial size instead of passing None to the size validation predicate. Regression tests cover: - Writing channel-less 2D and 3D NIfTI images without an original affine or output spatial shape, checking shape, voxel values, and affine. - Eager and lazy rank-one resampling with spatial_size=None and -1. ### Types of changes - [x] Non-breaking bug fix. - [x] New tests added to cover the changes. ### Validation Ran the following related test modules: python -B -m unittest tests.data.test_nifti_rw tests.transforms.test_spatial_resample tests.transforms.spatial.test_spatial_resampled tests.transforms.test_resample_to_match tests.transforms.test_resample_to_matchd Result: 195 tests run, OK (6 skipped). Full integration and quick test suites were not run. AI assistance: OpenAI Codex assisted with implementation, testing, and preparation of this description. --------- Signed-off-by: moonbirdyang <kirito168888@gmail.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 08dedf9 commit e0cee14

3 files changed

Lines changed: 39 additions & 2 deletions

File tree

‎monai/transforms/spatial/functional.py‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,8 +162,11 @@ def spatial_resample(
162162
in_spatial_size = torch.tensor(original_spatial_shape[:spatial_rank])
163163
if isinstance(spatial_size, int) and (spatial_size == -1): # using the input spatial size
164164
spatial_size = in_spatial_size
165-
elif spatial_size is None and spatial_rank > 1: # auto spatial size
166-
spatial_size, _ = compute_shape_offset(in_spatial_size, src_affine, dst_affine) # type: ignore
165+
elif spatial_size is None:
166+
if spatial_rank > 1: # auto spatial size
167+
spatial_size, _ = compute_shape_offset(in_spatial_size, src_affine, dst_affine) # type: ignore
168+
else:
169+
spatial_size = in_spatial_size
167170
spatial_size = torch.tensor(
168171
fall_back_tuple(ensure_tuple(spatial_size)[:spatial_rank], in_spatial_size, lambda x: x is not None and x >= 0)
169172
)

‎tests/data/test_nifti_rw.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,26 @@ def test_write_5d(self):
249249
)
250250
np.testing.assert_allclose(out.affine, np.diag([1.4, 2, 2, 1]), atol=1e-4, rtol=1e-4)
251251

252+
@parameterized.expand([((64, 64),), ((8, 64, 64),)])
253+
def test_write_without_original_affine_and_spatial_shape(self, shape):
254+
"""Verify channel-less NIfTI writing preserves shape, voxel values, and affine without target metadata.
255+
256+
Args:
257+
shape: Shape of the channel-less 2D or 3D input array.
258+
"""
259+
data = np.arange(np.prod(shape), dtype=np.float32).reshape(shape)
260+
with tempfile.TemporaryDirectory() as out_dir:
261+
image_name = os.path.join(out_dir, "test.nii")
262+
writer_obj = NibabelWriter(output_dtype=np.float32)
263+
writer_obj.set_data_array(data, channel_dim=None, spatial_ndim=None)
264+
writer_obj.set_metadata({"affine": np.eye(4), "original_affine": None})
265+
writer_obj.write(image_name)
266+
267+
saved = nib.load(image_name)
268+
self.assertEqual(saved.shape, data.shape)
269+
np.testing.assert_array_equal(saved.get_fdata(), data)
270+
np.testing.assert_allclose(saved.affine, np.eye(4))
271+
252272

253273
if __name__ == "__main__":
254274
unittest.main()

‎tests/transforms/test_spatial_resample.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,20 @@ def test_inverse(self, img, device, data_param, expected_output):
222222
expected_affine = to_affine_nd(len(out.affine) - 1, torch.eye(4))
223223
assert_allclose(out.affine, expected_affine)
224224

225+
@parameterized.expand([(False, None), (False, -1), (True, None), (True, -1)])
226+
def test_1d_unchanged(self, lazy, spatial_size):
227+
"""Verify rank-one no-op resampling preserves values, pending shape, and affine.
228+
229+
Args:
230+
lazy: Whether to record the resampling lazily instead of executing eagerly.
231+
spatial_size: Output size setting, either None or -1, expected to preserve the input size.
232+
"""
233+
img = MetaTensor(torch.arange(8, dtype=torch.float32).reshape(1, 8))
234+
result = SpatialResample(lazy=lazy)(img, spatial_size=spatial_size)
235+
assert_allclose(result, img)
236+
assert_allclose(result.peek_pending_affine(), img.affine)
237+
self.assertEqual(tuple(result.peek_pending_shape()), (8,))
238+
225239
def test_unchange(self):
226240
for i, p in enumerate(TEST_NDARRAYS_ALL):
227241
set_track_meta(i % 2)

0 commit comments

Comments
 (0)