From 5678af49e9d0cf3acaf0dd9ec590c9874cbfffc8 Mon Sep 17 00:00:00 2001 From: Soumya Snigdha Kundu Date: Mon, 13 Apr 2026 10:58:54 +0100 Subject: [PATCH 1/2] Avoid eager C-order copies in NibabelReader (Fixes: #8107) Nibabel exposes NIfTI voxel buffers in their native Fortran layout, but MONAI was forcing np.asanyarray(img.dataobj, order="C") in NibabelReader._get_array_data(). For compressed .nii.gz inputs that adds a full dense memory reorder on top of the file read/decompression step, which is the hot path reported in issue #8107. Drop the forced C-order conversion and keep nibabel's native array layout instead. Downstream MONAI conversion paths already handle contiguity when they actually need it, so the reader does not need to pay that cost eagerly at load time. Add a regression test that loads a small NIfTI image through NibabelReader and asserts the returned data is still correct while preserving the native F-contiguous layout. This guards against reintroducing the eager copy in the reader path. Signed-off-by: Soumya Snigdha Kundu --- monai/data/image_reader.py | 2 +- tests/data/test_init_reader.py | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index c300476a6b..de02eb0593 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1217,7 +1217,7 @@ def _get_array_data(self, img, filename): data_offset = img.dataobj.offset data_dtype = img.dataobj.dtype return image[data_offset:].view(data_dtype).reshape(data_shape, order="F") - return np.asanyarray(img.dataobj, order="C") + return np.asanyarray(img.dataobj) class NumpyReader(ImageReader): diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 4170412207..8a46b5d45c 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -11,8 +11,12 @@ from __future__ import annotations +import os +import tempfile import unittest +import numpy as np + from monai.data import ITKReader, NibabelReader, NrrdReader, NumpyReader, PILReader, PydicomReader from monai.transforms import LoadImage, LoadImaged from tests.test_utils import SkipIfNoModule @@ -76,6 +80,23 @@ def test_readers_to_gpu(self): inst = NibabelReader(to_gpu=to_gpu) self.assertIsInstance(inst, NibabelReader) + @SkipIfNoModule("nibabel") + def test_nibabel_reader_avoids_eager_c_order_copy(self): + import nibabel as nib + + test_image = np.arange(2 * 3 * 4, dtype=np.int16).reshape(2, 3, 4) + with tempfile.TemporaryDirectory() as tempdir: + filename = os.path.join(tempdir, "test_image.nii.gz") + nib.save(nib.Nifti1Image(test_image, np.eye(4)), filename) + + reader = NibabelReader(mmap=False) + img = reader.read(filename) + data, _ = reader.get_data(img) + + np.testing.assert_array_equal(data, test_image) + self.assertTrue(data.flags.f_contiguous) + self.assertFalse(data.flags.c_contiguous) + if __name__ == "__main__": unittest.main() From e7412d502a68340992fd043dba02972eec032cd2 Mon Sep 17 00:00:00 2001 From: Soumya Snigdha Kundu Date: Mon, 13 Apr 2026 11:13:52 +0100 Subject: [PATCH 2/2] Broaden NibabelReader layout regression coverage Exercise both .nii and .nii.gz inputs in the tiny layout regression test so the reader path stays covered without adding a benchmark or a heavier fixture. Signed-off-by: Soumya Snigdha Kundu --- tests/data/test_init_reader.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/tests/data/test_init_reader.py b/tests/data/test_init_reader.py index 8a46b5d45c..169fd20a5f 100644 --- a/tests/data/test_init_reader.py +++ b/tests/data/test_init_reader.py @@ -86,16 +86,19 @@ def test_nibabel_reader_avoids_eager_c_order_copy(self): test_image = np.arange(2 * 3 * 4, dtype=np.int16).reshape(2, 3, 4) with tempfile.TemporaryDirectory() as tempdir: - filename = os.path.join(tempdir, "test_image.nii.gz") - nib.save(nib.Nifti1Image(test_image, np.eye(4)), filename) - - reader = NibabelReader(mmap=False) - img = reader.read(filename) - data, _ = reader.get_data(img) - - np.testing.assert_array_equal(data, test_image) - self.assertTrue(data.flags.f_contiguous) - self.assertFalse(data.flags.c_contiguous) + for suffix in (".nii", ".nii.gz"): + with self.subTest(suffix=suffix): + filename = os.path.join(tempdir, f"test_image{suffix}") + nib.save(nib.Nifti1Image(test_image, np.eye(4)), filename) + + reader = NibabelReader(mmap=False) + img = reader.read(filename) + data, _ = reader.get_data(img) + + np.testing.assert_array_equal(data, test_image) + # The reader must not force an eager C-order copy; the native + # (F-order) layout from nibabel should be preserved here. + self.assertFalse(data.flags.c_contiguous) if __name__ == "__main__":