Coverage for nilearn/image/image.py: 8%
484 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"""
2Preprocessing functions for images.
4See also nilearn.signal.
5"""
7import collections.abc
8import itertools
9import warnings
10from copy import deepcopy
12import numpy as np
13from joblib import Memory, Parallel, delayed
14from nibabel import Nifti1Image, Nifti1Pair, load, spatialimages
15from scipy.ndimage import gaussian_filter1d, generate_binary_structure, label
16from scipy.stats import scoreatpercentile
18from nilearn import signal
19from nilearn._utils import (
20 as_ndarray,
21 check_niimg,
22 check_niimg_3d,
23 check_niimg_4d,
24 fill_doc,
25 logger,
26 repr_niimgs,
27)
28from nilearn._utils.exceptions import DimensionError
29from nilearn._utils.helpers import (
30 check_copy_header,
31 stringify_path,
32)
33from nilearn._utils.logger import find_stack_level
34from nilearn._utils.masker_validation import (
35 check_compatibility_mask_and_images,
36)
37from nilearn._utils.niimg import _get_data, safe_get_data
38from nilearn._utils.niimg_conversions import (
39 _index_img,
40 check_same_fov,
41 iter_check_niimg,
42)
43from nilearn._utils.param_validation import check_params, check_threshold
44from nilearn._utils.path_finding import resolve_globbing
45from nilearn.surface.surface import (
46 SurfaceImage,
47 at_least_2d,
48 extract_data,
49)
50from nilearn.surface.surface import get_data as get_surface_data
51from nilearn.surface.utils import assert_polymesh_equal, check_polymesh_equal
52from nilearn.typing import NiimgLike
55def get_data(img):
56 """Get the image data as a :class:`numpy.ndarray`.
58 Parameters
59 ----------
60 img : Niimg-like object or iterable of Niimg-like objects
61 See :ref:`extracting_data`.
63 Returns
64 -------
65 :class:`numpy.ndarray`
66 3D or 4D numpy array depending on the shape of `img`. This function
67 preserves the type of the image data.
68 If `img` is an in-memory Nifti image
69 it returns the image data array itself -- not a copy.
71 """
72 img = check_niimg(img)
73 return _get_data(img)
76def high_variance_confounds(
77 imgs, n_confounds=5, percentile=2.0, detrend=True, mask_img=None
78) -> np.ndarray:
79 """Return confounds extracted from input signals with highest variance.
81 Parameters
82 ----------
83 imgs : 4D Niimg-like or 2D SurfaceImage object
84 See :ref:`extracting_data`.
86 mask_img : Niimg-like or SurfaceImage object, or None, default=None
87 If not provided, all voxels / vertices are used.
88 If provided, confounds are extracted
89 from voxels / vertices inside the mask.
90 See :ref:`extracting_data`.
92 n_confounds : :obj:`int`, default=5
93 Number of confounds to return.
95 percentile : :obj:`float`, default=2.0
96 Highest-variance signals percentile to keep before computing the
97 singular value decomposition, 0. <= `percentile` <= 100.
98 `mask_img.sum() * percentile / 100` must be greater than `n_confounds`.
100 detrend : :obj:`bool`, default=True
101 If True, detrend signals before processing.
103 Returns
104 -------
105 :class:`numpy.ndarray`
106 Highest variance confounds. Shape: *(number_of_scans, n_confounds)*.
108 Notes
109 -----
110 This method is related to what has been published in the literature
111 as 'CompCor' (Behzadi NeuroImage 2007).
113 The implemented algorithm does the following:
115 - Computes the sum of squares for each signal (no mean removal).
116 - Keeps a given percentile of signals with highest variance (percentile).
117 - Computes an SVD of the extracted signals.
118 - Returns a given number (n_confounds) of signals from the SVD with
119 highest singular values.
121 See Also
122 --------
123 nilearn.signal.high_variance_confounds
125 """
126 from .. import masking
128 check_compatibility_mask_and_images(mask_img, imgs)
130 if mask_img is not None:
131 if isinstance(imgs, SurfaceImage):
132 check_polymesh_equal(mask_img.mesh, imgs.mesh)
133 sigs = masking.apply_mask(imgs, mask_img)
135 # Load the data only if it doesn't need to be masked
136 # Not using apply_mask here saves memory in most cases.
137 elif isinstance(imgs, SurfaceImage):
138 sigs = as_ndarray(get_surface_data(imgs))
139 sigs = np.reshape(sigs, (-1, sigs.shape[-1])).T
140 else:
141 imgs = check_niimg_4d(imgs)
142 sigs = as_ndarray(get_data(imgs))
143 sigs = np.reshape(sigs, (-1, sigs.shape[-1])).T
145 del imgs # help reduce memory consumption
147 return signal.high_variance_confounds(
148 sigs, n_confounds=n_confounds, percentile=percentile, detrend=detrend
149 )
152def _fast_smooth_array(arr):
153 """Perform simple smoothing.
155 Less computationally expensive than applying a Gaussian filter.
157 Only the first three dimensions of the array will be smoothed. The
158 filter uses [0.2, 1, 0.2] weights in each direction and use a
159 normalization to preserve the local average value.
161 Parameters
162 ----------
163 arr : :class:`numpy.ndarray`
164 4D array, with image number as last dimension. 3D arrays are
165 also accepted.
167 Returns
168 -------
169 :class:`numpy.ndarray`
170 Smoothed array.
172 Notes
173 -----
174 Rather than calling this function directly, users are encouraged
175 to call the high-level function :func:`smooth_img` with
176 `fwhm='fast'`.
178 """
179 neighbor_weight = 0.2
180 # 6 neighbors in 3D if not on an edge
181 n_neighbors = 6
182 # This scale ensures that a uniform array stays uniform
183 # except on the array edges
184 scale = 1 + n_neighbors * neighbor_weight
186 # Need to copy because the smoothing is done in multiple statements
187 # and there does not seem to be an easy way to do it in place
188 smoothed_arr = arr.copy()
189 weighted_arr = neighbor_weight * arr
191 smoothed_arr[:-1] += weighted_arr[1:]
192 smoothed_arr[1:] += weighted_arr[:-1]
193 smoothed_arr[:, :-1] += weighted_arr[:, 1:]
194 smoothed_arr[:, 1:] += weighted_arr[:, :-1]
195 smoothed_arr[:, :, :-1] += weighted_arr[:, :, 1:]
196 smoothed_arr[:, :, 1:] += weighted_arr[:, :, :-1]
197 smoothed_arr /= scale
199 return smoothed_arr
202@fill_doc
203def smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True):
204 """Smooth images by applying a Gaussian filter.
206 Apply a Gaussian filter along the three first dimensions of `arr`.
208 Parameters
209 ----------
210 arr : :class:`numpy.ndarray`
211 4D array, with image number as last dimension. 3D arrays are also
212 accepted.
214 affine : :class:`numpy.ndarray`
215 (4, 4) matrix, giving affine transformation for image. (3, 3) matrices
216 are also accepted (only these coefficients are used).
217 If `fwhm='fast'`, the affine is not used and can be None.
218 %(fwhm)s
219 ensure_finite : :obj:`bool`, default=True
220 If True, replace every non-finite values (like NaNs) by zero before
221 filtering.
223 copy : :obj:`bool`, default=True
224 If True, input array is not modified. True by default: the filtering
225 is not performed in-place.
227 Returns
228 -------
229 :class:`numpy.ndarray`
230 Filtered `arr`.
232 Notes
233 -----
234 This function is most efficient with arr in C order.
236 """
237 # Here, we have to investigate use cases of fwhm. Particularly, if fwhm=0.
238 # See issue #1537
239 if isinstance(fwhm, (int, float)) and (fwhm == 0.0):
240 warnings.warn(
241 f"The parameter 'fwhm' for smoothing is specified as {fwhm}. "
242 "Setting it to None (no smoothing will be performed)",
243 stacklevel=find_stack_level(),
244 )
245 fwhm = None
246 if arr.dtype.kind == "i":
247 if arr.dtype == np.int64:
248 arr = arr.astype(np.float64)
249 else:
250 arr = arr.astype(np.float32) # We don't need crazy precision.
251 if copy:
252 arr = arr.copy()
253 if ensure_finite:
254 # SPM tends to put NaNs in the data outside the brain
255 arr[np.logical_not(np.isfinite(arr))] = 0
256 if isinstance(fwhm, str) and (fwhm == "fast"):
257 arr = _fast_smooth_array(arr)
258 elif fwhm is not None:
259 fwhm = np.asarray([fwhm]).ravel()
260 fwhm = np.asarray([0.0 if elem is None else elem for elem in fwhm])
261 affine = affine[:3, :3] # Keep only the scale part.
262 fwhm_over_sigma_ratio = np.sqrt(8 * np.log(2)) # FWHM to sigma.
263 vox_size = np.sqrt(np.sum(affine**2, axis=0))
264 sigma = fwhm / (fwhm_over_sigma_ratio * vox_size)
265 for n, s in enumerate(sigma):
266 if s > 0.0:
267 gaussian_filter1d(arr, s, output=arr, axis=n)
268 return arr
271@fill_doc
272def smooth_img(imgs, fwhm):
273 """Smooth images by applying a Gaussian filter.
275 Apply a Gaussian filter along the three first dimensions of `arr`.
276 In all cases, non-finite values in input image are replaced by zeros.
278 Parameters
279 ----------
280 imgs : Niimg-like object or iterable of Niimg-like objects
281 Image(s) to smooth (see :ref:`extracting_data`
282 for a detailed description of the valid input types).
283 %(fwhm)s
285 Returns
286 -------
287 :class:`nibabel.nifti1.Nifti1Image` or list of
288 Filtered input image. If `imgs` is an iterable,
289 then `filtered_img` is a list.
291 """
292 # Use hasattr() instead of isinstance to workaround a Python 2.6/2.7 bug
293 # See http://bugs.python.org/issue7624
294 imgs = stringify_path(imgs)
295 if hasattr(imgs, "__iter__") and not isinstance(imgs, str):
296 single_img = False
297 else:
298 single_img = True
299 imgs = [imgs]
301 ret = []
302 for img in imgs:
303 img = check_niimg(img)
304 affine = img.affine
305 filtered = smooth_array(
306 get_data(img), affine, fwhm=fwhm, ensure_finite=True, copy=True
307 )
308 ret.append(new_img_like(img, filtered, affine, copy_header=True))
310 return ret[0] if single_img else ret
313def _crop_img_to(img, slices, copy=True, copy_header=False):
314 """Crops an image to a smaller size.
316 Crop `img` to size indicated by slices and adjust affine accordingly.
318 Parameters
319 ----------
320 img : Niimg-like object
321 Image to be cropped.
322 If slices has less entries than `img` has dimensions,
323 the slices will be applied to the first `len(slices)` dimensions
324 (See :ref:`extracting_data`).
326 slices : list of slices
327 Defines the range of the crop.
328 E.g. [slice(20, 200), slice(40, 150), slice(0, 100)] defines a cube.
330 copy : :obj:`bool`, default=True
331 Specifies whether cropped data is to be copied or not.
333 copy_header : :obj:`bool`
334 Whether to copy the header of the input image to the output.
335 If None, the default behavior is to not copy the header.
337 .. versionadded:: 0.11.0
339 This parameter will be set to True by default in 0.13.0.
341 Returns
342 -------
343 Niimg-like object
344 Cropped version of the input image.
346 offset : :obj:`list`, optional
347 List of tuples representing the number of voxels removed
348 (before, after) the cropped volumes, i.e.:
349 *[(x1_pre, x1_post), (x2_pre, x2_post), ..., (xN_pre, xN_post)]*
351 """
352 img = check_niimg(img)
354 data = get_data(img)
355 affine = img.affine
357 cropped_data = data[tuple(slices)]
358 if copy:
359 cropped_data = cropped_data.copy()
361 linear_part = affine[:3, :3]
362 old_origin = affine[:3, 3]
363 new_origin_voxel = np.array([s.start for s in slices])
364 new_origin = old_origin + linear_part.dot(new_origin_voxel)
366 new_affine = np.eye(4)
367 new_affine[:3, :3] = linear_part
368 new_affine[:3, 3] = new_origin
370 return new_img_like(img, cropped_data, new_affine, copy_header=copy_header)
373def crop_img(
374 img, rtol=1e-8, copy=True, pad=True, return_offset=False, copy_header=False
375):
376 """Crops an image as much as possible.
378 Will crop `img`, removing as many zero entries as possible without
379 touching non-zero entries.
380 Will leave one :term:`voxel` of zero padding
381 around the obtained non-zero area in order
382 to avoid sampling issues later on.
384 Parameters
385 ----------
386 img : Niimg-like object
387 Image to be cropped (see :ref:`extracting_data` for a detailed
388 description of the valid input types).
390 rtol : :obj:`float`, default=1e-08
391 relative tolerance (with respect to maximal absolute value of the
392 image), under which values are considered negligible and thus
393 croppable.
395 copy : :obj:`bool`, default=True
396 Specifies whether cropped data is copied or not.
398 pad : :obj:`bool`, default=True
399 Toggles adding 1-voxel of 0s around the border.
401 return_offset : :obj:`bool`, default=False
402 Specifies whether to return a tuple of the removed padding.
404 copy_header : :obj:`bool`, default=False
405 Whether to copy the header of the input image to the output.
407 .. versionadded:: 0.11.0
409 This parameter will be set to True by default in 0.13.0.
411 Returns
412 -------
413 Niimg-like object or :obj:`tuple`
414 Cropped version of the input image and, if `return_offset=True`,
415 a tuple of tuples representing the number of voxels
416 removed (before, after) the cropped volumes, i.e.:
417 *[(x1_pre, x1_post), (x2_pre, x2_post), ..., (xN_pre, xN_post)]*
419 """
420 # TODO: remove this warning in 0.13.0
421 check_copy_header(copy_header)
423 img = check_niimg(img)
424 data = get_data(img)
425 infinity_norm = max(-data.min(), data.max())
426 passes_threshold = np.logical_or(
427 data < -rtol * infinity_norm, data > rtol * infinity_norm
428 )
430 if data.ndim == 4:
431 passes_threshold = np.any(passes_threshold, axis=-1)
432 coords = np.array(np.where(passes_threshold))
434 # Sets full range if no data are found along the axis
435 if coords.shape[1] == 0:
436 start, end = [0, 0, 0], list(data.shape)
437 else:
438 start = coords.min(axis=1)
439 end = coords.max(axis=1) + 1
441 # pad with one voxel to avoid resampling problems
442 if pad:
443 start = np.maximum(start - 1, 0)
444 end = np.minimum(end + 1, data.shape[:3])
446 slices = [slice(s, e) for s, e in zip(start, end)][:3]
447 cropped_im = _crop_img_to(img, slices, copy=copy, copy_header=copy_header)
448 return (cropped_im, tuple(slices)) if return_offset else cropped_im
451def compute_mean(imgs, target_affine=None, target_shape=None, smooth=False):
452 """Compute the mean of the images over time or the 4th dimension.
454 See mean_img for details about the API.
455 """
456 from . import resampling
458 input_repr = repr_niimgs(imgs, shorten=True)
460 imgs = check_niimg(imgs)
461 mean_data = safe_get_data(imgs)
462 affine = imgs.affine
463 # Free memory ASAP
464 del imgs
465 if mean_data.ndim not in (3, 4):
466 raise ValueError(
467 "Computation expects 3D or 4D images, "
468 f"but {mean_data.ndim} dimensions were given ({input_repr})"
469 )
470 if mean_data.ndim == 4:
471 mean_data = mean_data.mean(axis=-1)
472 else:
473 mean_data = mean_data.copy()
474 # TODO switch to force_resample=True
475 # when bumping to version > 0.13
476 mean_data = resampling.resample_img(
477 Nifti1Image(mean_data, affine),
478 target_affine=target_affine,
479 target_shape=target_shape,
480 copy=False,
481 copy_header=True,
482 force_resample=False,
483 )
484 affine = mean_data.affine
485 mean_data = get_data(mean_data)
487 if smooth:
488 nan_mask = np.isnan(mean_data)
489 mean_data = smooth_array(
490 mean_data,
491 affine=np.eye(4),
492 fwhm=smooth,
493 ensure_finite=True,
494 copy=False,
495 )
496 mean_data[nan_mask] = np.nan
498 return mean_data, affine
501def _compute_surface_mean(imgs: SurfaceImage) -> SurfaceImage:
502 """Compute mean of a single surface image over its 2nd dimension."""
503 if len(imgs.shape) < 2 or imgs.shape[1] < 2:
504 data = imgs.data
505 else:
506 data = {
507 part: np.mean(value, axis=1).astype(float)
508 for part, value in imgs.data.parts.items()
509 }
510 return new_img_like(imgs, data=data)
513@fill_doc
514def mean_img(
515 imgs,
516 target_affine=None,
517 target_shape=None,
518 verbose=0,
519 n_jobs=1,
520 copy_header=False,
521):
522 """Compute the mean over images.
524 This can be a mean over time or the 4th dimension for a volume,
525 or the 2nd dimension for a surface image.
527 Note that if list of 4D volume images (or 2D surface images)
528 are given,
529 the mean of each image is computed separately,
530 and the resulting mean is computed after.
532 Parameters
533 ----------
534 imgs : Niimg-like or or :obj:`~nilearn.surface.SurfaceImage` object, or \
535 iterable of Niimg-like or :obj:`~nilearn.surface.SurfaceImage`.
536 Images to be averaged over 'time'
537 (see :ref:`extracting_data`
538 for a detailed description of the valid input types).
540 %(target_affine)s
541 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
543 %(target_shape)s
544 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
546 %(verbose0)s
548 n_jobs : :obj:`int`, default=1
549 The number of CPUs to use to do the computation (-1 means
550 'all CPUs').
551 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
553 copy_header : :obj:`bool`, default=False
554 Whether to copy the header of the input image to the output.
555 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
557 .. versionadded:: 0.11.0
559 This parameter will be set to True by default in 0.13.0.
561 Returns
562 -------
563 :obj:`~nibabel.nifti1.Nifti1Image` or :obj:`~nilearn.surface.SurfaceImage`
564 Mean image.
566 See Also
567 --------
568 nilearn.image.math_img : For more general operations on images.
570 """
571 is_iterable = isinstance(imgs, collections.abc.Iterable)
572 is_surface_img = isinstance(imgs, SurfaceImage) or (
573 is_iterable and all(isinstance(x, SurfaceImage) for x in imgs)
574 )
575 if is_surface_img:
576 if not is_iterable:
577 imgs = [imgs]
578 all_means = concat_imgs([_compute_surface_mean(x) for x in imgs])
579 return _compute_surface_mean(all_means)
581 # TODO: remove this warning in 0.13.0
582 check_copy_header(copy_header)
584 imgs = stringify_path(imgs)
585 is_str = isinstance(imgs, str)
586 is_iterable = isinstance(imgs, collections.abc.Iterable)
587 if is_str or not is_iterable:
588 imgs = [imgs]
590 imgs_iter = iter(imgs)
591 first_img = check_niimg(next(imgs_iter))
593 # Compute the first mean to retrieve the reference
594 # target_affine and target_shape if_needed
595 n_imgs = 1
596 running_mean, first_affine = compute_mean(
597 first_img, target_affine=target_affine, target_shape=target_shape
598 )
600 if target_affine is None or target_shape is None:
601 target_affine = first_affine
602 target_shape = running_mean.shape[:3]
604 for this_mean in Parallel(n_jobs=n_jobs, verbose=verbose)(
605 delayed(compute_mean)(
606 n, target_affine=target_affine, target_shape=target_shape
607 )
608 for n in imgs_iter
609 ):
610 n_imgs += 1
611 # compute_mean returns (mean_img, affine)
612 this_mean = this_mean[0]
613 running_mean += this_mean
615 running_mean = running_mean / float(n_imgs)
616 return new_img_like(
617 first_img, running_mean, target_affine, copy_header=copy_header
618 )
621def swap_img_hemispheres(img):
622 """Perform swapping of hemispheres in the indicated NIfTI image.
624 Use case: synchronizing ROIs across hemispheres.
626 Parameters
627 ----------
628 img : Niimg-like object
629 Images to swap (see :ref:`extracting_data` for a detailed description
630 of the valid input types).
632 Returns
633 -------
634 :class:`~nibabel.nifti1.Nifti1Image`
635 Hemispherically swapped image.
637 Notes
638 -----
639 Assumes that the image is sagitally aligned.
641 Should be used with caution (confusion might be caused with
642 radio/neuro conventions)
644 Note that this does not require a change of the affine matrix.
646 """
647 from .resampling import reorder_img
649 # Check input is really a path to a nifti file or a nifti object
650 img = check_niimg_3d(img)
652 # get nifti in x-y-z order
653 img = reorder_img(img, copy_header=True)
655 # create swapped nifti object
656 out_img = new_img_like(
657 img, get_data(img)[::-1], img.affine, copy_header=True
658 )
660 return out_img
663def index_img(imgs, index):
664 """Indexes into a image in the last dimension.
666 Common use cases include extracting an image out of `img` or
667 creating a 4D (or 2D for surface) image
668 whose data is a subset of `img` data.
670 Parameters
671 ----------
672 imgs : 4D Niimg-like object or 2D :obj:`~nilearn.surface.SurfaceImage`
673 See :ref:`extracting_data`.
675 index : Any type compatible with numpy array indexing
676 Used for indexing the data array in the last dimension.
678 Returns
679 -------
680 :obj:`~nibabel.nifti1.Nifti1Image` or :obj:`~nilearn.surface.SurfaceImage`
681 Indexed image.
683 See Also
684 --------
685 nilearn.image.concat_imgs
686 nilearn.image.iter_img
688 Examples
689 --------
690 First we concatenate two MNI152 images to create a 4D-image::
692 >>> from nilearn import datasets
693 >>> from nilearn.image import concat_imgs, index_img
694 >>> joint_mni_image = concat_imgs([datasets.load_mni152_template(),
695 ... datasets.load_mni152_template()])
696 >>> print(joint_mni_image.shape)
697 (197, 233, 189, 2)
699 We can now select one slice from the last dimension of this 4D-image::
701 >>> single_mni_image = index_img(joint_mni_image, 1)
702 >>> print(single_mni_image.shape)
703 (197, 233, 189)
705 We can also select multiple frames using the `slice` constructor::
707 >>> five_mni_images = concat_imgs([datasets.load_mni152_template()] * 5)
708 >>> print(five_mni_images.shape)
709 (197, 233, 189, 5)
711 >>> first_three_images = index_img(five_mni_images,
712 ... slice(0, 3))
713 >>> print(first_three_images.shape)
714 (197, 233, 189, 3)
716 """
717 if isinstance(imgs, SurfaceImage):
718 imgs = at_least_2d(imgs)
719 return new_img_like(imgs, data=extract_data(imgs, index))
721 imgs = check_niimg_4d(imgs)
722 # duck-type for pandas arrays, and select the 'values' attr
723 if hasattr(index, "values") and hasattr(index, "iloc"):
724 index = index.to_numpy().flatten()
725 return _index_img(imgs, index)
728def iter_img(imgs):
729 """Iterate over images.
731 Could be along the the 4th dimension for 4D Niimg-like object
732 or the 2nd dimension for 2D Surface images..
734 Parameters
735 ----------
736 imgs : 4D Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`
737 See :ref:`extracting_data`.
739 Returns
740 -------
741 Iterator of :class:`~nibabel.nifti1.Nifti1Image` \
742 or :obj:`~nilearn.surface.SurfaceImage`
744 See Also
745 --------
746 nilearn.image.index_img
748 """
749 if isinstance(imgs, SurfaceImage):
750 output = (
751 index_img(imgs, i) for i in range(at_least_2d(imgs).shape[1])
752 )
753 return output
754 return check_niimg_4d(imgs, return_iterator=True)
757def _downcast_from_int64_if_possible(data):
758 """Try to downcast to int32 if possible.
760 If `data` is 64-bit ints and can be converted to (signed) int 32,
761 return an int32 copy, otherwise return `data` itself.
762 """
763 if data.dtype not in (np.int64, np.uint64):
764 return data
765 img_min, img_max = np.min(data), np.max(data)
766 type_info = np.iinfo(np.int32)
767 can_cast = type_info.min <= img_min and type_info.max >= img_max
768 if can_cast:
769 warnings.warn(
770 "Data array used to create a new image contains 64-bit ints. "
771 "This is likely due to creating the array with numpy and "
772 "passing `int` as the `dtype`. Many tools such as FSL and SPM "
773 "cannot deal with int64 in Nifti images, so for compatibility the "
774 "data has been converted to int32.",
775 stacklevel=find_stack_level(),
776 )
777 return data.astype("int32")
778 warnings.warn(
779 "Data array used to create a new image contains 64-bit ints, and "
780 "some values too large to store in 32-bit ints. The resulting image "
781 "thus contains 64-bit ints, which may cause some compatibility issues "
782 "with some other tools or an error when saving the image to a "
783 "Nifti file.",
784 stacklevel=find_stack_level(),
785 )
786 return data
789def new_img_like(ref_niimg, data, affine=None, copy_header=False):
790 """Create a new image of the same class as the reference image.
792 Parameters
793 ----------
794 ref_niimg : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`
795 Reference image. The new image will be of the same type.
797 data : :class:`numpy.ndarray`
798 Data to be stored in the image. If data dtype is a boolean, then data
799 is cast to 'uint8' by default.
801 .. versionchanged:: 0.9.2
802 Changed default dtype casting of booleans from 'int8' to 'uint8'.
804 affine : 4x4 :class:`numpy.ndarray`, default=None
805 Transformation matrix.
806 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
808 copy_header : :obj:`bool`, default=False
809 Indicated if the header of the reference image should be used to
810 create the new image.
811 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
813 Returns
814 -------
815 Niimg-like or :obj:`~nilearn.surface.SurfaceImage` object
816 A loaded image with the same file type (and, optionally, header)
817 as the reference image.
819 """
820 if isinstance(ref_niimg, SurfaceImage):
821 mesh = ref_niimg.mesh
822 return SurfaceImage(
823 mesh=deepcopy(mesh),
824 data=data,
825 )
826 # Hand-written loading code to avoid too much memory consumption
827 orig_ref_niimg = ref_niimg
828 ref_niimg = stringify_path(ref_niimg)
829 is_str = isinstance(ref_niimg, str)
830 has_get_fdata = hasattr(ref_niimg, "get_fdata")
831 has_iter = hasattr(ref_niimg, "__iter__")
832 has_affine = hasattr(ref_niimg, "affine")
833 if has_iter and not any([is_str, has_get_fdata]):
834 ref_niimg = ref_niimg[0]
835 ref_niimg = stringify_path(ref_niimg)
836 is_str = isinstance(ref_niimg, str)
837 has_get_fdata = hasattr(ref_niimg, "get_fdata")
838 has_affine = hasattr(ref_niimg, "affine")
839 if not (has_get_fdata and has_affine):
840 if is_str:
841 ref_niimg = load(ref_niimg)
842 else:
843 raise TypeError(
844 "The reference image should be a niimg."
845 f" {orig_ref_niimg!r} was passed"
846 )
848 if affine is None:
849 affine = ref_niimg.affine
850 if data.dtype == bool:
851 data = as_ndarray(data, dtype=np.uint8)
852 data = _downcast_from_int64_if_possible(data)
853 header = None
854 if copy_header and ref_niimg.header is not None:
855 header = ref_niimg.header.copy()
856 try:
857 "something" in header # noqa: B015
858 except TypeError:
859 pass
860 else:
861 if "scl_slope" in header:
862 header["scl_slope"] = 0.0
863 if "scl_inter" in header:
864 header["scl_inter"] = 0.0
865 # 'glmax' is removed for Nifti2Image. Modify only if 'glmax' is
866 # available in header. See issue #1611
867 if "glmax" in header:
868 header["glmax"] = 0.0
869 if "cal_max" in header:
870 header["cal_max"] = np.max(data) if data.size > 0 else 0.0
871 if "cal_min" in header:
872 header["cal_min"] = np.min(data) if data.size > 0 else 0.0
873 klass = ref_niimg.__class__
874 if klass is Nifti1Pair:
875 # Nifti1Pair is an internal class, without a to_filename,
876 # we shouldn't return it
877 klass = Nifti1Image
878 return klass(data, affine, header=header)
881def _apply_cluster_size_threshold(arr, cluster_threshold, copy=True):
882 """Apply cluster-extent thresholding to voxel-wise thresholded array.
884 Parameters
885 ----------
886 arr : :obj:`numpy.ndarray` of shape (X, Y, Z)
887 3D array that has been thresholded at the voxel level.
888 cluster_threshold : :obj:`float`
889 Cluster-size threshold, in voxels, to apply to ``arr``.
890 copy : :obj:`bool`, default=True
891 Whether to copy the array before modifying it or not.
893 Returns
894 -------
895 arr : :obj:`numpy.ndarray` of shape (X, Y, Z)
896 Cluster-extent thresholded array.
898 Notes
899 -----
900 Clusters are defined in a bi-sided manner;
901 both negative and positive clusters are evaluated,
902 but this is done separately for each sign.
904 Clusters are defined using 6-connectivity, also known as NN1 (in AFNI) or
905 "faces" connectivity.
906 """
907 assert arr.ndim == 3
909 if copy:
910 arr = arr.copy()
912 # Define array for 6-connectivity, aka NN1 or "faces"
913 bin_struct = generate_binary_structure(3, 1)
915 for sign in np.unique(np.sign(arr)):
916 # Binarize using one-sided cluster-defining threshold
917 binarized = ((arr * sign) > 0).astype(int)
919 # Apply cluster threshold
920 label_map = label(binarized, bin_struct)[0]
921 clust_ids = sorted(np.unique(label_map)[1:])
922 for c_val in clust_ids:
923 if np.sum(label_map == c_val) < cluster_threshold:
924 arr[label_map == c_val] = 0
926 return arr
929def threshold_img(
930 img,
931 threshold,
932 cluster_threshold=0,
933 two_sided=True,
934 mask_img=None,
935 copy=True,
936 copy_header=False,
937):
938 """Threshold the given input image, mostly statistical or atlas images.
940 Thresholding can be done based on direct image intensities or selection
941 threshold with given percentile.
943 - If ``threshold`` is a :obj:`float`:
945 we threshold the image based on image intensities.
947 - When ``two_sided`` is True:
949 The given value should be within the range of minimum and maximum
950 intensity of the input image.
951 All instensities in the interval ``[-threshold, threshold]`` will be
952 set to zero.
954 - When ``two_sided`` is False:
956 - If the threshold is negative:
958 It should be greater than the minimum intensity of the input data.
959 All intensities greater than or equal to the specified threshold will
960 be set to zero.
961 All other instensities keep their original values.
963 - If the threshold is positive:
965 then it should be less than the maximum intensity of the input data.
966 All intensities less than or equal to the specified threshold will be
967 set to zero.
968 All other instensities keep their original values.
970 - If threshold is :obj:`str`:
972 The number part should be in interval ``[0, 100]``.
973 We threshold the image based on the score obtained using this percentile
974 on the image data.
975 The percentile rank is computed using
976 :func:`scipy.stats.scoreatpercentile`.
978 - When ``two_sided`` is True:
980 The score is calculated on the absolute values of data.
982 - When ``two_sided`` is False:
984 The score is calculated only on the non-negative values of data.
986 .. versionadded:: 0.2
988 .. versionchanged:: 0.9.0
989 New ``cluster_threshold`` and ``two_sided`` parameters added.
991 .. versionchanged:: 0.11.2dev
992 Add support for SurfaceImage.
994 Parameters
995 ----------
996 img : a 3D/4D Niimg-like object or a :obj:`~nilearn.surface.SurfaceImage`
997 Image containing statistical or atlas maps which should be thresholded.
999 threshold : :obj:`float` or :obj:`str`
1000 Threshold that is used to set certain voxel intensities to zero.
1001 If threshold is float, it should be within the range of minimum and the
1002 maximum intensity of the data.
1003 If `two_sided` is True, threshold cannot be negative.
1004 If threshold is :obj:`str`,
1005 the given string should be within the range of "0%" to "100%".
1007 cluster_threshold : :obj:`float`, default=0
1008 Cluster size threshold, in voxels. In the returned thresholded map,
1009 sets of connected voxels (``clusters``) with size smaller than this
1010 number will be removed.
1012 Not implemented for SurfaceImage.
1014 .. versionadded:: 0.9.0
1016 two_sided : :obj:`bool`, default=True
1017 Whether the thresholding should yield both positive and negative
1018 part of the maps.
1020 .. versionadded:: 0.9.0
1022 mask_img : Niimg-like object or a :obj:`~nilearn.surface.SurfaceImage` \
1023 or None, default=None
1024 Mask image applied to mask the input data.
1025 If None, no masking will be applied.
1027 copy : :obj:`bool`, default=True
1028 If True, input array is not modified. True by default: the filtering
1029 is not performed in-place.
1031 copy_header : :obj:`bool`, default=False
1032 Whether to copy the header of the input image to the output.
1034 Not applicable for SurfaceImage.
1036 .. versionadded:: 0.11.0
1038 This parameter will be set to True by default in 0.13.0.
1040 Returns
1041 -------
1042 :obj:`~nibabel.nifti1.Nifti1Image` \
1043 or a :obj:`~nilearn.surface.SurfaceImage`
1044 Thresholded image of the given input image.
1046 Raises
1047 ------
1048 ValueError
1049 If threshold is of type str but is not a non-negative number followed
1050 by the percent sign.
1051 If threshold is a negative float and `two_sided` is True.
1052 TypeError
1053 If threshold is neither float nor a string in correct percentile
1054 format.
1056 See Also
1057 --------
1058 nilearn.glm.threshold_stats_img :
1059 Threshold a statistical image using the alpha value, optionally with
1060 false positive control.
1062 """
1063 from nilearn.image.resampling import resample_img
1064 from nilearn.masking import load_mask_img
1066 if not isinstance(img, (*NiimgLike, SurfaceImage)):
1067 raise TypeError(
1068 "'img' should be a 3D/4D Niimg-like object or a SurfaceImage. "
1069 f"Got {type(img)=}."
1070 )
1072 if mask_img is not None:
1073 check_compatibility_mask_and_images(mask_img, img)
1075 if isinstance(img, SurfaceImage) and isinstance(mask_img, SurfaceImage):
1076 check_polymesh_equal(mask_img.mesh, img.mesh)
1078 if isinstance(img, SurfaceImage) and cluster_threshold > 0:
1079 warnings.warn(
1080 "Cluster thresholding not implemented for SurfaceImage. "
1081 "Setting 'cluster_threshold' to 0.",
1082 stacklevel=find_stack_level(),
1083 )
1084 cluster_threshold = 0
1086 if isinstance(img, NiimgLike):
1087 # TODO: remove this warning in 0.13.0
1088 check_copy_header(copy_header)
1090 img = check_niimg(img)
1091 img_data = safe_get_data(img, ensure_finite=True, copy_data=copy)
1092 affine = img.affine
1093 else:
1094 if copy:
1095 img = deepcopy(img)
1096 img_data = get_surface_data(img, ensure_finite=True)
1098 img_data_for_cutoff = img_data
1100 if mask_img is not None:
1101 # Set as 0 for the values which are outside of the mask
1102 if isinstance(mask_img, NiimgLike):
1103 mask_img = check_niimg_3d(mask_img)
1104 if not check_same_fov(img, mask_img):
1105 # TODO switch to force_resample=True
1106 # when bumping to version > 0.13
1107 mask_img = resample_img(
1108 mask_img,
1109 target_affine=affine,
1110 target_shape=img.shape[:3],
1111 interpolation="nearest",
1112 copy_header=True,
1113 force_resample=False,
1114 )
1115 mask_data, _ = load_mask_img(mask_img)
1117 # Take only points that are within the mask to check for threshold
1118 img_data_for_cutoff = img_data_for_cutoff[mask_data != 0.0]
1120 img_data[mask_data == 0.0] = 0.0
1122 else:
1123 mask_img, _ = load_mask_img(mask_img)
1125 mask_data = get_surface_data(mask_img)
1127 # Take only points that are within the mask to check for threshold
1128 img_data_for_cutoff = img_data_for_cutoff[mask_data != 0.0]
1130 for hemi in mask_img.data.parts:
1131 mask = mask_img.data.parts[hemi]
1132 img.data.parts[hemi][mask == 0.0] = 0.0
1134 cutoff_threshold = check_threshold(
1135 threshold,
1136 img_data_for_cutoff,
1137 percentile_func=scoreatpercentile,
1138 name="threshold",
1139 two_sided=two_sided,
1140 )
1142 # Apply threshold
1143 if isinstance(img, NiimgLike):
1144 img_data = _apply_threshold(img_data, two_sided, cutoff_threshold)
1145 else:
1146 img_data = _apply_threshold(img, two_sided, cutoff_threshold)
1148 # Perform cluster thresholding, if requested
1150 # Expand to 4D to support both 3D and 4D nifti
1151 expand = isinstance(img, NiimgLike) and img_data.ndim == 3
1152 if expand:
1153 img_data = img_data[:, :, :, None]
1154 if cluster_threshold > 0:
1155 for i_vol in range(img_data.shape[3]):
1156 img_data[..., i_vol] = _apply_cluster_size_threshold(
1157 img_data[..., i_vol],
1158 cluster_threshold,
1159 )
1160 if expand:
1161 # Reduce back to 3D
1162 img_data = img_data[:, :, :, 0]
1164 # Reconstitute img object
1165 if isinstance(img, NiimgLike):
1166 return new_img_like(img, img_data, affine, copy_header=copy_header)
1168 return new_img_like(img, img_data.data)
1171def _apply_threshold(img_data, two_sided, cutoff_threshold):
1172 """Apply a given threshold to an 'image'.
1174 If the image is a Surface applies to each part.
1176 Parameters
1177 ----------
1178 img_data: np.ndarray or SurfaceImage
1180 two_sided : :obj:`bool`, default=True
1181 Whether the thresholding should yield both positive and negative
1182 part of the maps.
1184 cutoff_threshold: :obj:`int`
1185 Effective threshold returned by check_threshold.
1187 Returns
1188 -------
1189 np.ndarray or SurfaceImage
1190 """
1191 if isinstance(img_data, SurfaceImage):
1192 for hemi, value in img_data.data.parts.items():
1193 img_data.data.parts[hemi] = _apply_threshold(
1194 value, two_sided, cutoff_threshold
1195 )
1196 return img_data
1198 if two_sided:
1199 mask = (-cutoff_threshold <= img_data) & (img_data <= cutoff_threshold)
1200 elif cutoff_threshold >= 0:
1201 mask = img_data <= cutoff_threshold
1202 else:
1203 mask = img_data >= cutoff_threshold
1205 img_data[mask] = 0.0
1207 return img_data
1210def math_img(formula, copy_header_from=None, **imgs):
1211 """Interpret a numpy based string formula using niimg in named parameters.
1213 .. versionadded:: 0.2.3
1215 Parameters
1216 ----------
1217 formula : :obj:`str`
1218 The mathematical formula to apply to image internal data. It can use
1219 numpy imported as 'np'.
1221 copy_header_from : :obj:`str`, default=None
1222 Takes the variable name of one of the images in the formula.
1223 The header of this image will be copied to the result of the formula.
1224 Note that the result image and the image to copy the header from,
1225 should have the same number of dimensions. If None, the default
1226 :class:`~nibabel.nifti1.Nifti1Header` is used.
1228 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1230 .. versionadded:: 0.10.4
1232 imgs : images (:class:`~nibabel.nifti1.Nifti1Image` or file names \
1233 or :obj:`~nilearn.surface.SurfaceImage` object)
1234 Keyword arguments corresponding to the variables in the formula as
1235 images.
1236 All input images should have the same 'geometry':
1238 - shape and affine for volume data
1239 - mesh (coordinates and faces) for surface data
1241 Returns
1242 -------
1243 :class:`~nibabel.nifti1.Nifti1Image` \
1244 or :obj:`~nilearn.surface.SurfaceImage` object
1245 Result of the formula as an image.
1246 Note that the dimension of the result image
1247 can be smaller than the input image.
1248 For volume image input, the affine is the same as the input images.
1249 For surface image input, the mesh is the same as the input images.
1251 See Also
1252 --------
1253 nilearn.image.mean_img : To simply compute the mean of multiple images
1255 Examples
1256 --------
1257 Let's load an image using nilearn datasets module::
1259 >>> from nilearn import datasets
1260 >>> anatomical_image = datasets.load_mni152_template()
1262 Now we can use any numpy function on this image::
1264 >>> from nilearn.image import math_img
1265 >>> log_img = math_img("np.log(img)", img=anatomical_image)
1267 We can also apply mathematical operations on several images::
1269 >>> result_img = math_img("img1 + img2",
1270 ... img1=anatomical_image, img2=log_img)
1272 The result image will have the same shape and affine as the input images;
1273 but might have different header information, specifically the TR value,
1274 see :gh:`2645`.
1276 .. versionadded:: 0.10.4
1278 We can also copy the header from one of the input images using
1279 ``copy_header_from``::
1281 >>> result_img_with_header = math_img("img1 + img2",
1282 ... img1=anatomical_image, img2=log_img,
1283 ... copy_header_from="img1")
1285 Notes
1286 -----
1287 This function is the Python equivalent of ImCal in SPM or fslmaths
1288 in FSL.
1290 """
1291 is_surface = all(isinstance(x, SurfaceImage) for x in imgs.values())
1293 if is_surface:
1294 first_img = next(iter(imgs.values()))
1295 for image in imgs.values():
1296 assert_polymesh_equal(first_img.mesh, image.mesh)
1298 # Computing input data as a dictionary of numpy arrays.
1299 data_dict = {k: {} for k in first_img.data.parts}
1300 for key, img in imgs.items():
1301 for k, v in img.data.parts.items():
1302 data_dict[k][key] = v
1304 # Add a reference to numpy in the kwargs of eval
1305 # so that numpy functions can be called from there.
1306 result = {}
1307 try:
1308 for k in data_dict:
1309 data_dict[k]["np"] = np
1310 result[k] = eval(formula, data_dict[k])
1311 except Exception as exc:
1312 exc.args = (
1313 "Input formula couldn't be processed, "
1314 f"you provided '{formula}',",
1315 *exc.args,
1316 )
1317 raise
1319 return new_img_like(first_img, result)
1321 try:
1322 niimgs = [check_niimg(image) for image in imgs.values()]
1323 check_same_fov(*niimgs, raise_error=True)
1324 except Exception as exc:
1325 exc.args = (
1326 "Input images cannot be compared, "
1327 f"you provided '{imgs.values()}',",
1328 *exc.args,
1329 )
1330 raise
1332 # Computing input data as a dictionary of numpy arrays. Keep a reference
1333 # niimg for building the result as a new niimg.
1334 niimg = None
1335 data_dict = {}
1336 for key, img in imgs.items():
1337 niimg = check_niimg(img)
1338 data_dict[key] = safe_get_data(niimg)
1340 # Add a reference to numpy in the kwargs of eval so that numpy functions
1341 # can be called from there.
1342 data_dict["np"] = np
1343 try:
1344 result = eval(formula, data_dict)
1345 except Exception as exc:
1346 exc.args = (
1347 f"Input formula couldn't be processed, you provided '{formula}',",
1348 *exc.args,
1349 )
1350 raise
1352 if copy_header_from is None:
1353 return new_img_like(niimg, result, niimg.affine)
1354 niimg = check_niimg(imgs[copy_header_from])
1355 # only copy the header if the result and the input image to copy the
1356 # header from have the same shape
1357 if result.ndim != niimg.ndim:
1358 raise ValueError(
1359 "Cannot copy the header. "
1360 "The result of the formula has a different number of "
1361 "dimensions than the image to copy the header from."
1362 )
1363 return new_img_like(niimg, result, niimg.affine, copy_header=True)
1366def binarize_img(
1367 img, threshold=0.0, mask_img=None, two_sided=True, copy_header=False
1368):
1369 """Binarize an image such that its values are either 0 or 1.
1371 .. versionadded:: 0.8.1
1373 Parameters
1374 ----------
1375 img : a 3D/4D Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`
1376 Image which should be binarized.
1378 threshold : :obj:`float` or :obj:`str`, default=0.0
1379 If float, we threshold the image based on image intensities meaning
1380 voxels which have intensities greater than this value will be kept.
1381 The given value should be within the range of minimum and
1382 maximum intensity of the input image.
1383 If string, it should finish with percent sign e.g. "80%" and we
1384 threshold based on the score obtained using this percentile on
1385 the image data. The voxels which have intensities greater than
1386 this score will be kept. The given string should be
1387 within the range of "0%" to "100%".
1389 mask_img : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`, \
1390 default=None
1391 Mask image applied to mask the input data.
1392 If None, no masking will be applied.
1394 two_sided : :obj:`bool`, default=True
1395 If `True`, threshold is applied to the absolute value of the image.
1396 If `False`, threshold is applied to the original value of the image.
1398 .. versionadded:: 0.10.3
1400 copy_header : :obj:`bool`, default=False
1401 Whether to copy the header of the input image to the output.
1403 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1405 .. versionadded:: 0.11.0
1407 This parameter will be set to True by default in 0.13.0.
1409 Returns
1410 -------
1411 :class:`~nibabel.nifti1.Nifti1Image`
1412 or :obj:`~nilearn.surface.SurfaceImage`
1413 Binarized version of the given input image. Output dtype is int8.
1415 See Also
1416 --------
1417 nilearn.image.threshold_img : To simply threshold but not binarize images.
1419 Examples
1420 --------
1421 Let's load an image using nilearn datasets module::
1423 >>> from nilearn import datasets
1424 >>> anatomical_image = datasets.load_mni152_template()
1426 Now we binarize it, generating a pseudo brainmask::
1428 >>> from nilearn.image import binarize_img
1429 >>> img = binarize_img(anatomical_image, copy_header=True)
1431 """
1432 warnings.warn(
1433 'The current default behavior for the "two_sided" argument '
1434 'is "True". This behavior will be changed to "False" in '
1435 "version 0.13.",
1436 DeprecationWarning,
1437 stacklevel=find_stack_level(),
1438 )
1440 return math_img(
1441 "img.astype(bool).astype('int8')",
1442 img=threshold_img(
1443 img,
1444 threshold,
1445 mask_img=mask_img,
1446 two_sided=two_sided,
1447 copy_header=copy_header,
1448 ),
1449 copy_header_from="img",
1450 )
1453@fill_doc
1454def clean_img(
1455 imgs,
1456 runs=None,
1457 detrend=True,
1458 standardize=True,
1459 confounds=None,
1460 low_pass=None,
1461 high_pass=None,
1462 t_r=None,
1463 ensure_finite=False,
1464 mask_img=None,
1465 **kwargs,
1466):
1467 """Improve :term:`SNR` on masked :term:`fMRI` signals.
1469 This function can do several things on the input signals, in
1470 the following order:
1472 - detrend
1473 - low- and high-pass filter
1474 - remove confounds
1475 - standardize
1477 Low-pass filtering improves specificity.
1479 High-pass filtering should be kept small, to keep some sensitivity.
1481 Filtering is only meaningful on evenly-sampled signals.
1483 According to Lindquist et al. (2018), removal of confounds will be done
1484 orthogonally to temporal filters (low- and/or high-pass filters), if both
1485 are specified.
1487 .. versionadded:: 0.2.5
1489 Parameters
1490 ----------
1491 imgs : 4D image Niimg-like object or \
1492 2D :obj:`~nilearn.surface.SurfaceImage`
1493 The signals in the last dimension are filtered (see
1494 :ref:`extracting_data` for a detailed description of the valid input
1495 types).
1497 runs : :class:`numpy.ndarray`, default=``None``
1498 Add a run level to the cleaning process. Each run will be
1499 cleaned independently. Must be a 1D array of n_samples elements.
1501 .. warning::
1503 'runs' replaces 'sessions' after release 0.10.0.
1504 Using 'session' will result in an error after release 0.10.0.
1507 detrend : :obj:`bool`, default=True
1508 If detrending should be applied on timeseries
1509 (before confound removal).
1511 standardize : :obj:`bool`, default=True
1512 If True, returned signals are set to unit variance.
1514 confounds : :class:`numpy.ndarray`, :obj:`str` or :obj:`list` of \
1515 Confounds timeseries. default=None
1516 Shape must be (instant number, confound number),
1517 or just (instant number,)
1518 The number of time instants in signals and confounds must be
1519 identical (i.e. signals.shape[0] == confounds.shape[0]).
1520 If a string is provided, it is assumed to be the name of a csv file
1521 containing signals as columns, with an optional one-line header.
1522 If a list is provided, all confounds are removed from the input
1523 signal, as if all were in the same array.
1525 %(low_pass)s
1527 %(high_pass)s
1529 t_r : :obj:`float`, default=None
1530 Repetition time, in second (sampling period). Set to None if not
1531 specified. Mandatory if used together with `low_pass` or `high_pass`.
1533 ensure_finite : :obj:`bool`, default=False
1534 If True, the non-finite values (NaNs and infs) found in the images
1535 will be replaced by zeros.
1537 mask_img : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`,\
1538 default=None
1539 If provided, signal is only cleaned from voxels inside the mask.
1540 If mask is provided, it should have same shape and affine as imgs.
1541 If not provided, all voxels / verrices are used.
1542 See :ref:`extracting_data`.
1544 kwargs : :obj:`dict`
1545 Keyword arguments to be passed to functions called
1546 within this function.
1547 Kwargs prefixed with ``'clean__'`` will be passed to
1548 :func:`~nilearn.signal.clean`.
1549 Within :func:`~nilearn.signal.clean`, kwargs prefixed with
1550 ``'butterworth__'`` will be passed to the Butterworth filter
1551 (i.e., ``clean__butterworth__``).
1553 Returns
1554 -------
1555 Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`
1556 Input images, cleaned. Same shape as `imgs`.
1558 Notes
1559 -----
1560 Confounds removal is based on a projection on the orthogonal
1561 of the signal space from :footcite:t:`Friston1994`.
1563 Orthogonalization between temporal filters and confound removal is based on
1564 suggestions in :footcite:t:`Lindquist2018`.
1566 References
1567 ----------
1568 .. footbibliography::
1570 See Also
1571 --------
1572 nilearn.signal.clean
1574 """
1575 # Avoid circular import
1576 from .. import masking
1578 # Check if t_r is set, otherwise propose t_r from imgs header
1579 if (low_pass is not None or high_pass is not None) and t_r is None:
1580 # We raise an error, instead of using the header's t_r as this
1581 # value is considered to be non-reliable
1582 raise ValueError(
1583 "Repetition time (t_r) must be specified for filtering. "
1584 "You specified None. "
1585 f"imgs header suggest it to be {imgs.header.get_zooms()[3]}"
1586 )
1588 clean_kwargs = {
1589 k[7:]: v for k, v in kwargs.items() if k.startswith("clean__")
1590 }
1592 if isinstance(imgs, SurfaceImage):
1593 imgs.data._check_ndims(2, "imgs")
1595 data = {}
1596 # Clean signal
1597 for p, v in imgs.data.parts.items():
1598 data[p] = signal.clean(
1599 v.T,
1600 runs=runs,
1601 detrend=detrend,
1602 standardize=standardize,
1603 confounds=confounds,
1604 low_pass=low_pass,
1605 high_pass=high_pass,
1606 t_r=t_r,
1607 ensure_finite=ensure_finite,
1608 **clean_kwargs,
1609 )
1610 data[p] = data[p].T
1612 if mask_img is not None:
1613 mask_img = masking.load_mask_img(mask_img)[0]
1614 for hemi in mask_img.data.parts:
1615 mask = mask_img.data.parts[hemi]
1616 data[hemi][mask == 0.0, ...] = 0.0
1618 return new_img_like(imgs, data)
1620 imgs_ = check_niimg_4d(imgs)
1622 # Prepare signal for cleaning
1623 if mask_img is not None:
1624 signals = masking.apply_mask(imgs_, mask_img)
1625 else:
1626 signals = get_data(imgs_).reshape(-1, imgs_.shape[-1]).T
1628 # Clean signal
1629 data = signal.clean(
1630 signals,
1631 runs=runs,
1632 detrend=detrend,
1633 standardize=standardize,
1634 confounds=confounds,
1635 low_pass=low_pass,
1636 high_pass=high_pass,
1637 t_r=t_r,
1638 ensure_finite=ensure_finite,
1639 **clean_kwargs,
1640 )
1642 # Put results back into Niimg-like object
1643 if mask_img is not None:
1644 imgs_ = masking.unmask(data, mask_img)
1645 elif "sample_mask" in clean_kwargs:
1646 sample_shape = imgs_.shape[:3] + clean_kwargs["sample_mask"].shape
1647 imgs_ = new_img_like(
1648 imgs_, data.T.reshape(sample_shape), copy_header=True
1649 )
1650 else:
1651 imgs_ = new_img_like(
1652 imgs_, data.T.reshape(imgs_.shape), copy_header=True
1653 )
1655 return imgs_
1658@fill_doc
1659def load_img(img, wildcards=True, dtype=None):
1660 """Load a Niimg-like object from filenames or list of filenames.
1662 .. versionadded:: 0.2.5
1664 Parameters
1665 ----------
1666 img : Niimg-like object
1667 If string, consider it as a path to NIfTI image and
1668 call `nibabel.load()`on it.
1669 The '~' symbol is expanded to the user home folder.
1670 If it is an object, check if affine attribute is present, raise
1671 `TypeError` otherwise.
1672 See :ref:`extracting_data`.
1674 wildcards : :obj:`bool`, default=True
1675 Use `img` as a regular expression to get a list of matching input
1676 filenames.
1677 If multiple files match, the returned list is sorted using an ascending
1678 order.
1679 If no file matches the regular expression, a `ValueError` exception is
1680 raised.
1682 %(dtype)s
1684 Returns
1685 -------
1686 3D/4D Niimg-like object
1687 Result can be :class:`~nibabel.nifti1.Nifti1Image` or the input, as-is.
1688 It is guaranteed that
1689 the returned object has an affine attributes and that
1690 nilearn.image.get_data returns its data.
1692 """
1693 return check_niimg(img, wildcards=wildcards, dtype=dtype)
1696@fill_doc
1697def concat_imgs(
1698 niimgs,
1699 dtype=np.float32,
1700 ensure_ndim=None,
1701 memory=None,
1702 memory_level=0,
1703 auto_resample=False,
1704 verbose=0,
1705):
1706 """Concatenate a list of images of varying lengths.
1708 The image list can contain:
1710 - Niimg-like objects of varying dimensions (i.e., 3D or 4D)
1711 as well as different 3D shapes and affines,
1712 as they will be matched to the first image in the list
1713 if ``auto_resample=True``.
1715 - surface images of varying dimensions (i.e., 1D or 2D)
1716 but with same number of vertices
1718 Parameters
1719 ----------
1720 niimgs : iterable of Niimg-like objects, or glob pattern, \
1721 or :obj:`list` or :obj:`tuple` \
1722 of :obj:`~nilearn.surface.SurfaceImage` object
1723 See :ref:`extracting_data`.
1724 Images to concatenate.
1726 dtype : numpy dtype, default=np.float32
1727 The dtype of the returned image.
1729 ensure_ndim : :obj:`int`, default=None
1730 Indicate the dimensionality of the expected niimg. An
1731 error is raised if the niimg is of another dimensionality.
1732 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1734 auto_resample : :obj:`bool`, default=False
1735 Converts all images to the space of the first one.
1736 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1738 %(verbose0)s
1740 %(memory)s
1741 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1743 %(memory_level)s
1744 Ignored for :obj:`~nilearn.surface.SurfaceImage`.
1746 Returns
1747 -------
1748 concatenated : :obj:`~nibabel.nifti1.Nifti1Image` \
1749 or :obj:`~nilearn.surface.SurfaceImage`
1750 A single image.
1752 See Also
1753 --------
1754 nilearn.image.index_img
1756 """
1757 check_params(locals())
1759 if (
1760 isinstance(niimgs, (tuple, list))
1761 and len(niimgs) > 0
1762 and all(isinstance(x, SurfaceImage) for x in niimgs)
1763 ):
1764 if len(niimgs) == 1:
1765 return niimgs[0]
1767 for i, img in enumerate(niimgs):
1768 check_polymesh_equal(img.mesh, niimgs[0].mesh)
1769 niimgs[i] = at_least_2d(img)
1771 if dtype is None:
1772 dtype = extract_data(niimgs[0]).dtype
1774 output_data = {}
1775 for part in niimgs[0].data.parts:
1776 tmp = [img.data.parts[part] for img in niimgs]
1777 output_data[part] = np.concatenate(tmp, axis=1).astype(dtype)
1779 return new_img_like(niimgs[0], data=output_data)
1781 if memory is None:
1782 memory = Memory(location=None)
1784 target_fov = "first" if auto_resample else None
1786 # We remove one to the dimensionality because of the list is one dimension.
1787 ndim = None
1788 if ensure_ndim is not None:
1789 ndim = ensure_ndim - 1
1791 # If niimgs is a string, use glob to expand it to the matching filenames.
1792 niimgs = resolve_globbing(niimgs)
1794 # First niimg is extracted to get information and for new_img_like
1795 first_niimg = None
1797 iterator, literator = itertools.tee(iter(niimgs))
1798 try:
1799 first_niimg = check_niimg(next(literator), ensure_ndim=ndim)
1800 except StopIteration:
1801 raise TypeError("Cannot concatenate empty objects")
1802 except DimensionError as exc:
1803 # Keep track of the additional dimension in the error
1804 exc.increment_stack_counter()
1805 raise
1807 # If no particular dimensionality is asked, we force consistency wrt the
1808 # first image
1809 if ndim is None:
1810 ndim = len(first_niimg.shape)
1812 if ndim not in [3, 4]:
1813 raise TypeError(
1814 "Concatenated images must be 3D or 4D. You gave a "
1815 f"list of {ndim}D images"
1816 )
1818 lengths = [first_niimg.shape[-1] if ndim == 4 else 1]
1819 for niimg in literator:
1820 # We check the dimensionality of the niimg
1821 try:
1822 niimg = check_niimg(niimg, ensure_ndim=ndim)
1823 except DimensionError as exc:
1824 # Keep track of the additional dimension in the error
1825 exc.increment_stack_counter()
1826 raise
1827 lengths.append(niimg.shape[-1] if ndim == 4 else 1)
1829 target_shape = first_niimg.shape[:3]
1830 if dtype is None:
1831 dtype = _get_data(first_niimg).dtype
1832 data = np.ndarray((*target_shape, sum(lengths)), order="F", dtype=dtype)
1833 cur_4d_index = 0
1834 for index, (size, niimg) in enumerate(
1835 zip(
1836 lengths,
1837 iter_check_niimg(
1838 iterator,
1839 atleast_4d=True,
1840 target_fov=target_fov,
1841 memory=memory,
1842 memory_level=memory_level,
1843 ),
1844 )
1845 ):
1846 nii_str = (
1847 f"image {niimg}" if isinstance(niimg, str) else f"image #{index}"
1848 )
1849 logger.log(f"Concatenating {index + 1}: {nii_str}", verbose)
1851 data[..., cur_4d_index : cur_4d_index + size] = _get_data(niimg)
1852 cur_4d_index += size
1854 return new_img_like(
1855 first_niimg, data, first_niimg.affine, copy_header=True
1856 )
1859def largest_connected_component_img(imgs):
1860 """Return the largest connected component of an image or list of images.
1862 .. versionadded:: 0.3.1
1864 Parameters
1865 ----------
1866 imgs : Niimg-like object or iterable of Niimg-like objects (3D)
1867 Image(s) to extract the largest connected component from.
1868 See :ref:`extracting_data`.
1870 Returns
1871 -------
1872 3D Niimg-like object or list of
1873 Image or list of images containing the largest connected component.
1875 Notes
1876 -----
1877 **Handling big-endian in given Nifti image**
1878 This function changes the existing byte-ordering information to new byte
1879 order, if the dtype in given Nifti image has non-native data type.
1880 This operation is done internally to avoid big-endian issues with
1881 scipy ndimage module.
1883 """
1884 from .._utils.ndimage import largest_connected_component
1886 imgs = stringify_path(imgs)
1887 if hasattr(imgs, "__iter__") and not isinstance(imgs, str):
1888 single_img = False
1889 else:
1890 single_img = True
1891 imgs = [imgs]
1893 ret = []
1894 for img in imgs:
1895 img = check_niimg_3d(img)
1896 affine = img.affine
1897 largest_component = largest_connected_component(safe_get_data(img))
1898 ret.append(
1899 new_img_like(img, largest_component, affine, copy_header=True)
1900 )
1902 return ret[0] if single_img else ret
1905def copy_img(img):
1906 """Copy an image to a nibabel.Nifti1Image.
1908 Parameters
1909 ----------
1910 img : image
1911 nibabel SpatialImage object to copy.
1913 Returns
1914 -------
1915 img_copy : image
1916 copy of input (data, affine and header)
1917 """
1918 if not isinstance(img, spatialimages.SpatialImage):
1919 raise ValueError("Input value is not an image")
1920 return new_img_like(
1921 img,
1922 safe_get_data(img, copy_data=True),
1923 img.affine.copy(),
1924 copy_header=True,
1925 )