Skip to content

Commit 862f3a6

Browse files
aymuos15ericspod
andauthored
Avoid eager C-order copy in NibabelReader (fixes #8107) (#8825)
## Summary - `NibabelReader._get_array_data` forced `np.asanyarray(img.dataobj, order="C")`, triggering a full dense memory reorder on every load on top of the file read/decompression step. This is the hot path reported in #8107. - Drop the forced C-order conversion and keep nibabel's native (F-order) layout. This aligns the CPU path with the existing GPU/cupy branch just above, which already returns F-order via `.reshape(data_shape, order="F")`. - Downstream MONAI conversion paths (`convert_to_tensor`/`convert_to_numpy` in `monai/utils/type_conversion.py`, `monai/data/image_writer.py`, recon utils) already call `ascontiguousarray` where they actually need C-contiguous memory, so the reader does not need to pay that cost eagerly at load time. Biggest wins are on uncompressed `.nii`, where nibabel's memmap view is returned lazily rather than being materialized by a forced reorder. Compressed `.nii.gz` still pays the decompression cost but skips the subsequent reorder pass, which matches the "twice as long" observation from @ericspod in the issue thread. ## Compatibility note The returned array's memory layout changes from C-contiguous to whatever nibabel provides (typically F-contiguous). Any external caller consuming `reader.get_data(...)[0]` directly via `.tobytes()` or a raw C-extension buffer without first calling `ascontiguousarray` would see a different byte order. All in-repo consumers already guard themselves. ## Test plan - [x] New regression test in `tests/data/test_init_reader.py` loads a small NIfTI through `NibabelReader` for both `.nii` and `.nii.gz`, asserts array equality, and asserts the returned data is not C-contiguous (i.e. no eager C copy). - [x] `pytest tests/data/test_init_reader.py` passes locally. - [x] `runtests.sh`-equivalent checks: `ruff`, `black --skip-magic-trailing-comma --check`, `isort --check`, `pycln`, pre-commit hooks — all clean on touched files. Fixes #8107 --------- Signed-off-by: Soumya Snigdha Kundu <soumya_snigdha.kundu@kcl.ac.uk> Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Co-authored-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com>
1 parent 098a443 commit 862f3a6

2 files changed

Lines changed: 27 additions & 2 deletions

File tree

monai/data/image_reader.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1102,7 +1102,8 @@ def get_data(self, img) -> tuple[np.ndarray, dict]:
11021102
This function returns two objects, first is numpy array of image data, second is dict of metadata.
11031103
It constructs `affine`, `original_affine`, and `spatial_shape` and stores them in meta dict.
11041104
When loading a list of files, they are stacked together at a new dimension as the first dimension,
1105-
and the metadata of the first image is used to present the output metadata.
1105+
and the metadata of the first image is used to present the output metadata. The returned arrays
1106+
preserve the ordering in the original data, typically this is F-ordering for NIfTI files.
11061107
11071108
Args:
11081109
img: a Nibabel image object loaded from an image file or a list of Nibabel image objects.
@@ -1217,7 +1218,7 @@ def _get_array_data(self, img, filename):
12171218
data_offset = img.dataobj.offset
12181219
data_dtype = img.dataobj.dtype
12191220
return image[data_offset:].view(data_dtype).reshape(data_shape, order="F")
1220-
return np.asanyarray(img.dataobj, order="C")
1221+
return np.asanyarray(img.dataobj)
12211222

12221223

12231224
class NumpyReader(ImageReader):

tests/data/test_init_reader.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
from __future__ import annotations
1313

14+
import os
15+
import tempfile
1416
import unittest
1517

18+
import numpy as np
19+
1620
from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader
1721
from monai.transforms import LoadImage, LoadImaged
1822
from tests.test_utils import SkipIfNoModule
@@ -76,6 +80,26 @@ def test_readers_to_gpu(self):
7680
inst = NibabelReader(to_gpu=to_gpu)
7781
self.assertIsInstance(inst, NibabelReader)
7882

83+
@SkipIfNoModule("nibabel")
84+
def test_nibabel_reader_avoids_eager_c_order_copy(self):
85+
import nibabel as nib
86+
87+
test_image = np.arange(2 * 3 * 4, dtype=np.int16).reshape(2, 3, 4)
88+
with tempfile.TemporaryDirectory() as tempdir:
89+
for suffix in (".nii", ".nii.gz"):
90+
with self.subTest(suffix=suffix):
91+
filename = os.path.join(tempdir, f"test_image{suffix}")
92+
nib.save(nib.Nifti1Image(test_image, np.eye(4)), filename)
93+
94+
reader = NibabelReader(mmap=False)
95+
img = reader.read(filename)
96+
data, _ = reader.get_data(img)
97+
98+
np.testing.assert_array_equal(data, test_image)
99+
# The reader must not force an eager C-order copy; the native
100+
# (F-order) layout from nibabel should be preserved here.
101+
self.assertFalse(data.flags.c_contiguous)
102+
79103

80104
if __name__ == "__main__":
81105
unittest.main()

0 commit comments

Comments
 (0)