Coverage for nilearn/_utils/niimg_conversions.py: 13%
127 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Conversion utilities."""
3import glob
4import itertools
5import warnings
6from pathlib import Path
8import numpy as np
9from joblib import Memory
10from nibabel.spatialimages import SpatialImage
11from numpy.testing import assert_array_equal
13import nilearn as ni
14from nilearn._utils.cache_mixin import cache
15from nilearn._utils.exceptions import DimensionError
16from nilearn._utils.helpers import stringify_path
17from nilearn._utils.logger import find_stack_level
18from nilearn._utils.niimg import _get_data, load_niimg, safe_get_data
19from nilearn._utils.path_finding import resolve_globbing
20from nilearn.typing import NiimgLike
23def _check_fov(img, affine, shape):
24 """Return True if img's field of view correspond to given \
25 shape and affine, False elsewhere.
26 """
27 img = check_niimg(img)
28 return img.shape[:3] == shape and np.allclose(img.affine, affine)
31def check_same_fov(*args, **kwargs) -> bool:
32 """Return True if provided images have the same field of view (shape and \
33 affine) and return False or raise an error elsewhere, depending on the \
34 `raise_error` argument.
36 This function can take an unlimited number of
37 images as arguments or keyword arguments and raise a user-friendly
38 ValueError if asked.
40 Parameters
41 ----------
42 args : images
43 Images to be checked. Images passed without keywords will be labeled
44 as img_#1 in the error message (replace 1 with the appropriate index).
46 kwargs : images
47 Images to be checked. In case of error, images will be reference by
48 their keyword name in the error message.
50 raise_error : boolean, optional
51 If True, an error will be raised in case of error.
53 """
54 raise_error = kwargs.pop("raise_error", False)
55 for i, arg in enumerate(args):
56 kwargs[f"img_#{i}"] = arg
57 errors = []
58 for (a_name, a_img), (b_name, b_img) in itertools.combinations(
59 kwargs.items(), 2
60 ):
61 if a_img.shape[:3] != b_img.shape[:3]:
62 errors.append((a_name, b_name, "shape"))
63 if not np.allclose(a_img.affine, b_img.affine):
64 errors.append((a_name, b_name, "affine"))
65 if errors and raise_error:
66 raise ValueError(
67 "Following field of view errors were detected:\n"
68 + "\n".join(
69 [
70 f"- {e[0]} and {e[1]} do not have the same {e[2]}"
71 for e in errors
72 ]
73 )
74 )
75 return not errors
78def check_imgs_equal(img1, img2) -> bool:
79 """Check if 2 NiftiImages have same fov and data."""
80 if not check_same_fov(img1, img2, raise_error=False):
81 return False
83 data_img1 = safe_get_data(img1)
84 data_img2 = safe_get_data(img2)
86 try:
87 assert_array_equal(data_img1, data_img2)
88 return True
89 except AssertionError:
90 return False
91 except Exception as e:
92 raise e
95def _index_img(img, index):
96 """Helper function for check_niimg_4d.""" # noqa: D401
97 from ..image import new_img_like # avoid circular imports
99 return new_img_like(
100 img, _get_data(img)[:, :, :, index], img.affine, copy_header=True
101 )
104def iter_check_niimg(
105 niimgs,
106 ensure_ndim=None,
107 atleast_4d=False,
108 target_fov=None,
109 dtype=None,
110 memory=None,
111 memory_level=0,
112):
113 """Iterate over a list of niimgs and do sanity checks and resampling.
115 Parameters
116 ----------
117 niimgs : list of niimg or glob pattern
118 Image to iterate over.
120 ensure_ndim : integer, optional
121 If specified, an error is raised if the data does not have the
122 required dimension.
124 atleast_4d : boolean, default=False
125 If True, any 3D image is converted to a 4D single scan.
127 target_fov : tuple of affine and shape, optional
128 If specified, images are resampled to this field of view.
130 %(dtype)s
132 memory : instance of joblib.Memory or string, default=None
133 Used to cache the masking process.
134 By default, no caching is done.
135 If a string is given, it is the path to the caching directory.
136 If ``None`` is passed will default to ``Memory(location=None)``.
138 memory_level : integer, default=0
139 Rough estimator of the amount of memory used by caching. Higher value
140 means more memory for caching.
142 See Also
143 --------
144 check_niimg, check_niimg_3d, check_niimg_4d
146 """
147 if memory is None:
148 memory = Memory(location=None)
149 # If niimgs is a string, use glob to expand it to the matching filenames.
150 niimgs = resolve_globbing(niimgs)
152 ref_fov = None
153 resample_to_first_img = False
154 ndim_minus_one = ensure_ndim - 1 if ensure_ndim is not None else None
155 if target_fov is not None and target_fov != "first":
156 ref_fov = target_fov
157 i = -1
158 for i, niimg in enumerate(niimgs):
159 try:
160 niimg = check_niimg(
161 niimg,
162 ensure_ndim=ndim_minus_one,
163 atleast_4d=atleast_4d,
164 dtype=dtype,
165 )
166 if i == 0:
167 ndim_minus_one = len(niimg.shape)
168 if ref_fov is None:
169 ref_fov = (niimg.affine, niimg.shape[:3])
170 resample_to_first_img = True
172 if not _check_fov(niimg, ref_fov[0], ref_fov[1]):
173 if target_fov is None:
174 raise ValueError(
175 f"Field of view of image #{i} is different from "
176 "reference FOV.\n"
177 f"Reference affine:\n{ref_fov[0]!r}\n"
178 f"Image affine:\n{niimg.affine!r}\n"
179 f"Reference shape:\n{ref_fov[1]!r}\n"
180 f"Image shape:\n{niimg.shape!r}\n"
181 )
182 from nilearn import image # we avoid a circular import
184 if resample_to_first_img:
185 warnings.warn(
186 "Affine is different across subjects."
187 " Realignment on first subject "
188 "affine forced",
189 stacklevel=find_stack_level(),
190 )
191 niimg = cache(
192 image.resample_img,
193 memory,
194 func_memory_level=2,
195 memory_level=memory_level,
196 )(
197 niimg,
198 target_affine=ref_fov[0],
199 target_shape=ref_fov[1],
200 copy_header=True,
201 force_resample=False, # TODO update to True in 0.13.0
202 )
203 yield niimg
204 except DimensionError as exc:
205 # Keep track of the additional dimension in the error
206 exc.increment_stack_counter()
207 raise
208 except TypeError as exc:
209 img_name = f" ({niimg}) " if isinstance(niimg, (str, Path)) else ""
211 exc.args = (
212 f"Error encountered while loading image #{i}{img_name}",
213 *exc.args,
214 )
215 raise
217 # Raising an error if input generator is empty.
218 if i == -1:
219 raise ValueError("Input niimgs list is empty.")
222def check_niimg(
223 niimg,
224 ensure_ndim=None,
225 atleast_4d=False,
226 dtype=None,
227 return_iterator=False,
228 wildcards=True,
229):
230 """Check that niimg is a proper 3D/4D niimg.
232 Turn filenames into objects.
234 Parameters
235 ----------
236 niimg : Niimg-like object
237 See :ref:`extracting_data`.
238 If niimg is a string or pathlib.Path, consider it as a path to
239 Nifti image and call nibabel.load on it. The '~' symbol is expanded to
240 the user home folder.
241 If it is an object, check if the affine attribute present and that
242 nilearn.image.get_data returns a result, raise TypeError otherwise.
244 ensure_ndim : integer {3, 4}, optional
245 Indicate the dimensionality of the expected niimg. An
246 error is raised if the niimg is of another dimensionality.
248 atleast_4d : boolean, default=False
249 Indicates if a 3d image should be turned into a single-scan 4d niimg.
251 %(dtype)s
252 If None, data will not be converted to a new data type.
254 return_iterator : boolean, default=False
255 Returns an iterator on the content of the niimg file input.
257 wildcards : boolean, default=True
258 Use niimg as a regular expression to get a list of matching input
259 filenames.
260 If multiple files match, the returned list is sorted using an ascending
261 order.
262 If no file matches the regular expression, a ValueError exception is
263 raised.
265 Returns
266 -------
267 result : 3D/4D Niimg-like object
268 Result can be nibabel.Nifti1Image or the input, as-is. It is guaranteed
269 that the returned object has an affine attribute and that its data can
270 be retrieved with nilearn.image.get_data.
272 Notes
273 -----
274 In nilearn, special care has been taken to make image manipulation easy.
275 This method is a kind of pre-requisite for any data processing method in
276 nilearn because it checks if data have a correct format and loads them if
277 necessary.
279 Its application is idempotent.
281 See Also
282 --------
283 iter_check_niimg, check_niimg_3d, check_niimg_4d
285 """
286 from ..image import new_img_like # avoid circular imports
288 if not (
289 isinstance(niimg, (NiimgLike, SpatialImage))
290 or (hasattr(niimg, "__iter__"))
291 ):
292 raise TypeError(
293 "input should be a NiftiLike object "
294 "or an iterable of NiftiLike object. "
295 f"Got: {niimg.__class__.__name__}"
296 )
298 if hasattr(niimg, "__iter__"):
299 for x in niimg:
300 if not (
301 isinstance(x, (NiimgLike, SpatialImage))
302 or hasattr(x, "__iter__")
303 ):
304 raise TypeError(
305 "iterable inputs should contain "
306 "NiftiLike objects or iterables. "
307 f"Got: {x.__class__.__name__}"
308 )
310 niimg = stringify_path(niimg)
312 if isinstance(niimg, str):
313 if wildcards and ni.EXPAND_PATH_WILDCARDS:
314 # Expand user path
315 expanded_niimg = str(Path(niimg).expanduser())
316 # Ascending sorting
317 filenames = sorted(glob.glob(expanded_niimg))
319 # processing filenames matching globbing expression
320 if len(filenames) >= 1 and glob.has_magic(niimg):
321 niimg = filenames # iterable case
322 # niimg is an existing filename
323 elif [expanded_niimg] == filenames:
324 niimg = filenames[0]
325 # No files found by glob
326 elif glob.has_magic(niimg):
327 # No files matching the glob expression, warn the user
328 message = (
329 "No files matching the entered niimg expression: "
330 f"'{niimg}'.\n"
331 "You may have left wildcards usage activated: "
332 "please set the global constant "
333 "'nilearn.EXPAND_PATH_WILDCARDS' to False "
334 "to deactivate this behavior."
335 )
336 raise ValueError(message)
337 else:
338 raise ValueError(f"File not found: '{niimg}'")
339 elif not Path(niimg).exists():
340 raise ValueError(f"File not found: '{niimg}'")
342 # in case of an iterable
343 if hasattr(niimg, "__iter__") and not isinstance(niimg, str):
344 if return_iterator:
345 return iter_check_niimg(
346 niimg, ensure_ndim=ensure_ndim, dtype=dtype
347 )
348 return ni.image.concat_imgs(
349 niimg, ensure_ndim=ensure_ndim, dtype=dtype
350 )
352 # Otherwise, it should be a filename or a SpatialImage, we load it
353 niimg = load_niimg(niimg, dtype=dtype)
355 if ensure_ndim == 3 and len(niimg.shape) == 4 and niimg.shape[3] == 1:
356 # "squeeze" the image.
357 data = safe_get_data(niimg)
358 affine = niimg.affine
359 niimg = new_img_like(niimg, data[:, :, :, 0], affine)
360 if atleast_4d and len(niimg.shape) == 3:
361 data = _get_data(niimg).view()
362 data.shape = (*data.shape, 1)
363 niimg = new_img_like(niimg, data, niimg.affine)
365 if ensure_ndim is not None and len(niimg.shape) != ensure_ndim:
366 raise DimensionError(len(niimg.shape), ensure_ndim)
368 if return_iterator:
369 return (_index_img(niimg, i) for i in range(niimg.shape[3]))
371 return niimg
374def check_niimg_3d(niimg, dtype=None):
375 """Check that niimg is a proper 3D niimg-like object and load it.
377 Parameters
378 ----------
379 niimg : Niimg-like object
380 See :ref:`extracting_data`.
381 If niimg is a string, consider it as a path to Nifti image and
382 call nibabel.load on it.
383 If it is an object, check if the affine attribute present and that
384 nilearn.image.get_data returns a result, raise TypeError otherwise.
386 %(dtype)s
388 Returns
389 -------
390 result : 3D Niimg-like object
391 Result can be nibabel.Nifti1Image or the input, as-is. It is guaranteed
392 that the returned object has an affine attribute and that its data can
393 be retrieved with nilearn.image.get_data.
395 Notes
396 -----
397 In nilearn, special care has been taken to make image manipulation easy.
398 This method is a kind of pre-requisite for any data processing method in
399 nilearn because it checks if data have a correct format and loads them if
400 necessary.
402 Its application is idempotent.
404 """
405 return check_niimg(niimg, ensure_ndim=3, dtype=dtype)
408def check_niimg_4d(niimg, return_iterator=False, dtype=None):
409 """Check that niimg is a proper 4D niimg-like object and load it.
411 Parameters
412 ----------
413 niimg : 4D Niimg-like object
414 See :ref:`extracting_data`.
415 If niimgs is an iterable, checks if data is really 4D. Then,
416 considering that it is a list of niimg and load them one by one.
417 If niimg is a string, consider it as a path to Nifti image and
418 call nibabel.load on it.
419 If it is an object, check if the affine attribute present and that
420 nilearn.image.get_data returns a result, raise TypeError otherwise.
422 dtype : {dtype, "auto"}, optional
423 Data type toward which the data should be converted. If "auto", the
424 data will be converted to int32 if dtype is discrete and float32 if it
425 is continuous.
427 return_iterator : boolean, default=False
428 If True, an iterator of 3D images is returned. This reduces the memory
429 usage when `niimgs` contains 3D images.
430 If False, a single 4D image is returned. When `niimgs` contains 3D
431 images they are concatenated together.
433 Returns
434 -------
435 niimg: 4D nibabel.Nifti1Image or iterator of 3D nibabel.Nifti1Image
437 Notes
438 -----
439 This function is the equivalent to check_niimg_3d() for Niimg-like objects
440 with a run level.
442 Its application is idempotent.
444 """
445 return check_niimg(
446 niimg, ensure_ndim=4, return_iterator=return_iterator, dtype=dtype
447 )