Skip to content

spatial_resample raises TypeError when spatial_size is None and spatial_rank is 1 #9068

Description

@hjmjohnson

Describe the bug

spatial_resample passes lambda x: x >= 0 to fall_back_tuple, but that helper's default predicate is lambda x: x and x > 0, whose x and short-circuits on None. The override does not, so a None element raises TypeError instead of falling back to the default — which is the documented behaviour of fall_back_tuple, and its own docstring says so:

>>> fall_back_tuple((-1, None), (32, 32))
(32, 32)

monai/transforms/spatial/functional.py:159:

spatial_size = torch.tensor(
    fall_back_tuple(ensure_tuple(spatial_size)[:spatial_rank], in_spatial_size, lambda x: x >= 0)
)

spatial_size reaches that line as None whenever the caller did not supply one and spatial_rank <= 1: the branch above it, elif spatial_size is None and spatial_rank > 1, is the only thing that replaces None, so with a rank of 1 the None survives into ensure_tuple(None) → (None,) → None >= 0.

To Reproduce

import numpy as np
from monai.data.image_writer import NibabelWriter

w = NibabelWriter()
w.set_data_array(np.random.rand(3, 5), channel_dim=None)
w.set_metadata({"affine": np.diag([1, 1, 1]), "original_affine": np.diag([1.4, 1, 1])})
File "monai/data/image_writer.py", line 604, in set_metadata
File "monai/data/image_writer.py", line 273, in resample_if_needed
File "monai/transforms/spatial/array.py", line 229, in __call__
File "monai/transforms/spatial/functional.py", line 159, in <lambda>
    fall_back_tuple(ensure_tuple(spatial_size)[:spatial_rank], in_spatial_size, lambda x: x >= 0)
TypeError: '>=' not supported between instances of 'NoneType' and 'int'

Instrumenting spatial_resample confirms it is entered with spatial_size=None and img.shape=(1, 3, 5).

Expected behavior

None means "no size given for this axis" and should fall back to the corresponding in_spatial_size entry, exactly as fall_back_tuple's docstring describes.

Screenshots / test impact

This is not a corner case reachable only by hand — it fails 8 tests on current dev:

  • tests/data/test_nifti_rw.py — test_write_2d, test_write_3d
  • tests/data/test_image_rw.py — 4 failures + 4 errors across TestRegRes / writer round-trips

Reproduced on unmodified dev (c1240a2d4) in two independent environments:

Python 3.12 / torch 2.13.0 / numpy 2.x fails
Python 3.10 / torch 2.11.0 (highest version CI tests) / numpy 2.2.6 fails identically

So it is not a new-dependency artifact. git log -L159,159 dates that line to #6068 (Feb 2023).

Environment

Ensuring you use the relevant python executable, please paste the output of:
MONAI version: 1.6.0rc1+58.gc1240a2d4
Python: 3.10 and 3.12 (both affected)
PyTorch: 2.11.0 and 2.13.0 (both affected)
numpy: 2.2.6
nibabel: 5.4.2

Additional context

The narrowest fix is to make the predicate None-safe at the call site, matching the helper's default:

lambda x: x is not None and x >= 0

Worth checking the other fall_back_tuple callers that pass an explicit func for the same hazard — any predicate that does not short-circuit on None inherits it.

Activity

  1. MDSALMANSHAMS commented on Aug 27, 2026

    @MDSALMANSHAMS
    Contributor

    Nice catch — the fix you already pinned in the issue body (lambda x: x is not None and x >= 0 at functional.py:159) matches fall_back_tuple's own default short-circuit exactly, so it's a drop-in. I'll put up a PR with that change plus regression tests covering the spatial_rank == 1/spatial_size=None path and the writer round-trip from your repro. I'll also sweep the other fall_back_tuple call sites for the same non-short-circuiting-predicate hazard you flagged and report back what I find.

  2. markov12 commented on Sep 12, 2026

    @markov12

    Some extra findings on the root cause, in case they are useful for the fix already in progress (#9080). I'm not planning to open a competing PR.

    Why spatial_rank is 1 here: this looks like a regression from #8765 (spatial_ndim tracking in MetaTensor). In ImageWriter.resample_if_needed (monai/data/image_writer.py), the affine is set on data_array before the channel dim is added:

    data_array = convert_to_tensor(data_array, track_meta=True)        # shape (2, 3), no channel yet
    data_array.affine = ...                                             # setter clamps spatial_ndim to ndim - 1 = 1
    output_array = resampler(data_array[None], ...)                     # now (1, 2, 3), but cached spatial_ndim stays 1

    So spatial_resample sees spatial_rank == 1 for a 2D image (debug print inside spatial_resample: shape (1, 2, 3), affine (3, 3), get_spatial_ndim == 1), skips the compute_shape_offset branch, and reaches fall_back_tuple with None.

    Bisect (test_nifti_rw.py -k "write_2d or write_3d"):

    Consequence for the None-safe predicate alone: it removes the TypeError, but with spatial_rank == 1 no resampling happens, so the output keeps the input affine (test_write_2d then fails with affine 1.0 vs expected 1.4), and test_write_3d still fails (shape (1, 1, 5) vs expected (1, 1, 3)).

    Possible fix (add the channel dim before setting the affine):

    -        data_array = convert_to_tensor(data_array, track_meta=True)
    +        # add the channel dim before setting the affine, so that the cached spatial_ndim is not clamped
    +        data_array = convert_to_tensor(data_array, track_meta=True)[None]
             if affine is not None:
                 data_array.affine = convert_to_tensor(affine, track_meta=False)  # type: ignore
             resampler = SpatialResample(mode=mode, padding_mode=padding_mode, align_corners=align_corners, dtype=dtype)
             output_array = resampler(
    -            data_array[None], dst_affine=target_affine, spatial_size=output_spatial_shape  # type: ignore
    +            data_array, dst_affine=target_affine, spatial_size=output_spatial_shape  # type: ignore
             )

    Results on current dev (macOS arm64, Python 3.12.14, torch 2.14.0, nibabel 5.4.2, itk 5.4.7):

    • test_nifti_rw.py: 39 passed / 2 failed → 41 passed
    • test_image_rw.py (TestLoadSaveNifti, 8 cases): all failing → all passing
    • whole tests/data: 10 more tests pass, no new failures (the only remaining failure, test_lmdbdataset_dist, also fails before Add explicit spatial_ndim tracking to MetaTensor #8765 on my machine and looks macOS-multiprocessing related)
    • test_save_image.py, test_save_imaged.py, test_itk_writer.py, test_png_rw.py: unchanged, all passing

    Other code paths that set an affine on a tensor before adding a channel dim might be affected the same way, but I only checked the writer.

    Happy for this to be folded into #9080 or used however is most convenient.

    (Investigation done with the help of an AI assistant; I reviewed the results.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions