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/3] 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/3] 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__": From 2ca1aa974f3433a2410dce7a5450665ee13aa945 Mon Sep 17 00:00:00 2001 From: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> Date: Tue, 5 May 2026 17:57:43 +0100 Subject: [PATCH 3/3] Apply suggestion from @ericspod Signed-off-by: Eric Kerfoot <17726042+ericspod@users.noreply.github.com> --- monai/data/image_reader.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index de02eb0593..9eb8967ad0 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -1102,7 +1102,8 @@ def get_data(self, img) -> tuple[np.ndarray, dict]: This function returns two objects, first is numpy array of image data, second is dict of metadata. It constructs `affine`, `original_affine`, and `spatial_shape` and stores them in meta dict. When loading a list of files, they are stacked together at a new dimension as the first dimension, - and the metadata of the first image is used to present the output metadata. + and the metadata of the first image is used to present the output metadata. The returned arrays + preserve the ordering in the original data, typically this is F-ordering for NIfTI files. Args: img: a Nibabel image object loaded from an image file or a list of Nibabel image objects.