Coverage for nilearn/masking.py: 12%
262 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"""Utilities to compute and operate on brain masks."""
3import numbers
4import warnings
6import numpy as np
7from joblib import Parallel, delayed
8from scipy.ndimage import binary_dilation, binary_erosion
10from nilearn._utils import (
11 as_ndarray,
12 check_niimg,
13 check_niimg_3d,
14 fill_doc,
15 logger,
16)
17from nilearn._utils.cache_mixin import cache
18from nilearn._utils.logger import find_stack_level
19from nilearn._utils.ndimage import get_border_data, largest_connected_component
20from nilearn._utils.niimg import safe_get_data
21from nilearn._utils.param_validation import check_params
22from nilearn.datasets import (
23 load_mni152_gm_template,
24 load_mni152_template,
25 load_mni152_wm_template,
26)
27from nilearn.image import get_data, new_img_like, resampling
28from nilearn.surface.surface import (
29 SurfaceImage,
30)
31from nilearn.surface.surface import get_data as get_surface_data
32from nilearn.surface.utils import check_polymesh_equal
33from nilearn.typing import NiimgLike
35__all__ = [
36 "apply_mask",
37 "compute_background_mask",
38 "compute_brain_mask",
39 "compute_epi_mask",
40 "compute_multi_background_mask",
41 "compute_multi_brain_mask",
42 "compute_multi_epi_mask",
43 "intersect_masks",
44 "unmask",
45]
48class _MaskWarning(UserWarning):
49 """A class to always raise warnings."""
52warnings.simplefilter("always", _MaskWarning)
55def load_mask_img(mask_img, allow_empty=False):
56 """Check that a mask is valid.
58 This checks if it contains two values including 0 and load it.
60 Parameters
61 ----------
62 mask_img : Niimg-like object or a :obj:`~nilearn.surface.SurfaceImage`
63 See :ref:`extracting_data`.
64 The mask to check.
66 allow_empty : :obj:`bool`, default=False
67 Allow loading an empty mask (full of 0 values).
69 Returns
70 -------
71 mask : :class:`numpy.ndarray` or :obj:`~nilearn.surface.SurfaceImage`
72 Boolean version of the input.
73 Returns a :class:`numpy.ndarray` if Niimg-like object
74 was passed as input
75 or :obj:`~nilearn.surface.SurfaceImage`
76 if :obj:`~nilearn.surface.SurfaceImage` was passed as input
78 mask_affine : None or (4,4) array-like
79 Affine of the mask if Niimg-like object was passed as input,
80 None otherwise.
81 """
82 if not isinstance(mask_img, (*NiimgLike, SurfaceImage)):
83 raise TypeError(
84 "'img' should be a 3D/4D Niimg-like object or a SurfaceImage. "
85 f"Got {type(mask_img)=}."
86 )
88 if isinstance(mask_img, NiimgLike):
89 mask_img = check_niimg_3d(mask_img)
90 mask = safe_get_data(mask_img, ensure_finite=True)
91 else:
92 mask_img.data._check_ndims(1)
93 mask = get_surface_data(mask_img, ensure_finite=True)
95 values = np.unique(mask)
97 if len(values) == 1:
98 # We accept a single value if it is not 0 (full true mask).
99 if values[0] == 0 and not allow_empty:
100 raise ValueError(
101 "The mask is invalid as it is empty: it masks all data."
102 )
103 elif len(values) == 2:
104 # If there are 2 different values, one of them must be 0 (background)
105 if 0 not in values:
106 raise ValueError(
107 "Background of the mask must be represented with 0. "
108 f"Given mask contains: {values}."
109 )
110 else:
111 # If there are more than 2 values, the mask is invalid
112 raise ValueError(
113 f"Given mask is not made of 2 values: {values}. "
114 "Cannot interpret as true or false."
115 )
117 mask = as_ndarray(mask, dtype=bool)
119 if isinstance(mask_img, NiimgLike):
120 return mask, mask_img.affine
122 for hemi in mask_img.data.parts:
123 mask_img.data.parts[hemi] = as_ndarray(
124 mask_img.data.parts[hemi], dtype=bool
125 )
127 return mask_img, None
130def extrapolate_out_mask(data, mask, iterations=1):
131 """Extrapolate values outside of the mask."""
132 if iterations > 1:
133 data, mask = extrapolate_out_mask(
134 data, mask, iterations=iterations - 1
135 )
136 new_mask = binary_dilation(mask)
137 larger_mask = np.zeros(np.array(mask.shape) + 2, dtype=bool)
138 larger_mask[1:-1, 1:-1, 1:-1] = mask
139 # Use nans as missing value: ugly
140 masked_data = np.zeros(larger_mask.shape + data.shape[3:])
141 masked_data[1:-1, 1:-1, 1:-1] = data.copy()
142 masked_data[np.logical_not(larger_mask)] = np.nan
143 outer_shell = larger_mask.copy()
144 outer_shell[1:-1, 1:-1, 1:-1] = np.logical_xor(new_mask, mask)
145 outer_shell_x, outer_shell_y, outer_shell_z = np.where(outer_shell)
146 extrapolation = []
147 for i, j, k in [
148 (1, 0, 0),
149 (-1, 0, 0),
150 (0, 1, 0),
151 (0, -1, 0),
152 (0, 0, 1),
153 (0, 0, -1),
154 ]:
155 this_x = outer_shell_x + i
156 this_y = outer_shell_y + j
157 this_z = outer_shell_z + k
158 extrapolation.append(masked_data[this_x, this_y, this_z])
160 extrapolation = np.array(extrapolation)
161 extrapolation = np.nansum(extrapolation, axis=0) / np.sum(
162 np.isfinite(extrapolation), axis=0
163 )
164 extrapolation[np.logical_not(np.isfinite(extrapolation))] = 0
165 new_data = np.zeros_like(masked_data)
166 new_data[outer_shell] = extrapolation
167 new_data[larger_mask] = masked_data[larger_mask]
168 return new_data[1:-1, 1:-1, 1:-1], new_mask
171#
172# Utilities to compute masks
173#
174@fill_doc
175def intersect_masks(mask_imgs, threshold=0.5, connected=True):
176 """Compute intersection of several masks.
178 Given a list of input mask images, generate the output image which
179 is the threshold-level intersection of the inputs.
181 Parameters
182 ----------
183 mask_imgs : :obj:`list` of Niimg-like objects
184 See :ref:`extracting_data`.
185 3D individual masks with same shape and affine.
187 threshold : :obj:`float`, default=0.5
188 Gives the level of the intersection, must be within [0, 1].
189 threshold=1 corresponds to keeping the intersection of all
190 masks, whereas threshold=0 is the union of all masks.
191 %(connected)s
192 Default=True.
194 Returns
195 -------
196 grp_mask : 3D :class:`nibabel.nifti1.Nifti1Image`
197 Intersection of all masks.
198 """
199 check_params(locals())
200 if len(mask_imgs) == 0:
201 raise ValueError("No mask provided for intersection")
202 grp_mask = None
203 first_mask, ref_affine = load_mask_img(mask_imgs[0], allow_empty=True)
204 ref_shape = first_mask.shape
205 if threshold > 1:
206 raise ValueError("The threshold should be smaller than 1")
207 if threshold < 0:
208 raise ValueError("The threshold should be greater than 0")
209 threshold = min(threshold, 1 - 1.0e-7)
211 for this_mask in mask_imgs:
212 mask, affine = load_mask_img(this_mask, allow_empty=True)
213 if np.any(affine != ref_affine):
214 raise ValueError("All masks should have the same affine")
215 if np.any(mask.shape != ref_shape):
216 raise ValueError("All masks should have the same shape")
218 if grp_mask is None:
219 # We use int here because there may be a lot of masks to merge
220 grp_mask = as_ndarray(mask, dtype=int)
221 else:
222 # If this_mask is floating point and grp_mask is integer, numpy 2
223 # casting rules raise an error for in-place addition. Hence we do
224 # it long-hand.
225 # XXX should the masks be coerced to int before addition?
226 grp_mask += mask
228 grp_mask = grp_mask > (threshold * len(list(mask_imgs)))
230 if np.any(grp_mask > 0) and connected:
231 grp_mask = largest_connected_component(grp_mask)
232 grp_mask = as_ndarray(grp_mask, dtype=np.int8)
233 return new_img_like(check_niimg_3d(mask_imgs[0]), grp_mask, ref_affine)
236def _post_process_mask(
237 mask, affine, opening=2, connected=True, warning_msg=""
238):
239 """Perform post processing on mask.
241 Performs opening and keep only largest connected component is
242 ``connected=True``.
243 """
244 if opening:
245 opening = int(opening)
246 mask = binary_erosion(mask, iterations=opening)
247 mask_any = mask.any()
248 if not mask_any:
249 warnings.warn(
250 f"Computed an empty mask. {warning_msg}",
251 _MaskWarning,
252 stacklevel=find_stack_level(),
253 )
254 if connected and mask_any:
255 mask = largest_connected_component(mask)
256 if opening:
257 mask = binary_dilation(mask, iterations=2 * opening)
258 mask = binary_erosion(mask, iterations=opening)
259 return mask, affine
262@fill_doc
263def compute_epi_mask(
264 epi_img,
265 lower_cutoff=0.2,
266 upper_cutoff=0.85,
267 connected=True,
268 opening=2,
269 exclude_zeros=False,
270 ensure_finite=True,
271 target_affine=None,
272 target_shape=None,
273 memory=None,
274 verbose=0,
275):
276 """Compute a brain mask from :term:`fMRI` data in 3D or \
277 4D :class:`numpy.ndarray`.
279 This is based on an heuristic proposed by T.Nichols:
280 find the least dense point of the histogram, between fractions
281 ``lower_cutoff`` and ``upper_cutoff`` of the total image histogram.
283 .. note::
285 In case of failure, it is usually advisable to
286 increase ``lower_cutoff``.
288 Parameters
289 ----------
290 epi_img : Niimg-like object
291 See :ref:`extracting_data`.
292 :term:`EPI` image, used to compute the mask.
293 3D and 4D images are accepted.
295 .. note::
296 If a 3D image is given, we suggest to use the mean image.
298 %(lower_cutoff)s
299 Default=0.2.
300 %(upper_cutoff)s
301 Default=0.85.
302 %(connected)s
303 Default=True.
304 %(opening)s
305 Default=2.
306 ensure_finite : :obj:`bool`, default=True
307 If ensure_finite is True, the non-finite values (NaNs and infs)
308 found in the images will be replaced by zeros
310 exclude_zeros : :obj:`bool`, default=False
311 Consider zeros as missing values for the computation of the
312 threshold. This option is useful if the images have been
313 resliced with a large padding of zeros.
314 %(target_affine)s
316 .. note::
317 This parameter is passed to :func:`nilearn.image.resample_img`.
319 %(target_shape)s
321 .. note::
322 This parameter is passed to :func:`nilearn.image.resample_img`.
324 %(memory)s
326 %(verbose0)s
328 Returns
329 -------
330 mask : :class:`nibabel.nifti1.Nifti1Image`
331 The brain mask (3D image).
332 """
333 check_params(locals())
334 logger.log("EPI mask computation", verbose)
336 # Delayed import to avoid circular imports
337 from nilearn.image.image import compute_mean
339 mean_epi, affine = cache(compute_mean, memory)(
340 epi_img,
341 target_affine=target_affine,
342 target_shape=target_shape,
343 smooth=(1 if opening else False),
344 )
346 if ensure_finite:
347 # Get rid of memmapping
348 mean_epi = as_ndarray(mean_epi)
349 # SPM tends to put NaNs in the data outside the brain
350 mean_epi[np.logical_not(np.isfinite(mean_epi))] = 0
351 sorted_input = np.sort(np.ravel(mean_epi))
352 if exclude_zeros:
353 sorted_input = sorted_input[sorted_input != 0]
354 lower_cutoff = int(np.floor(lower_cutoff * len(sorted_input)))
355 upper_cutoff = min(
356 int(np.floor(upper_cutoff * len(sorted_input))), len(sorted_input) - 1
357 )
359 delta = (
360 sorted_input[lower_cutoff + 1 : upper_cutoff + 1]
361 - sorted_input[lower_cutoff:upper_cutoff]
362 )
363 ia = delta.argmax()
364 threshold = 0.5 * (
365 sorted_input[ia + lower_cutoff] + sorted_input[ia + lower_cutoff + 1]
366 )
368 mask = mean_epi >= threshold
370 mask, affine = _post_process_mask(
371 mask,
372 affine,
373 opening=opening,
374 connected=connected,
375 warning_msg="Are you sure that input "
376 "data are EPI images not detrended. ",
377 )
378 return new_img_like(epi_img, mask, affine)
381@fill_doc
382def compute_multi_epi_mask(
383 epi_imgs,
384 lower_cutoff=0.2,
385 upper_cutoff=0.85,
386 connected=True,
387 opening=2,
388 threshold=0.5,
389 target_affine=None,
390 target_shape=None,
391 exclude_zeros=False,
392 n_jobs=1,
393 memory=None,
394 verbose=0,
395):
396 """Compute a common mask for several runs or subjects of :term:`fMRI` data.
398 Uses the mask-finding algorithms to extract masks for each run
399 or subject, and then keep only the main connected component of the
400 a given fraction of the intersection of all the masks.
402 Parameters
403 ----------
404 epi_imgs : :obj:`list` of Niimg-like objects
405 See :ref:`extracting_data`.
406 A list of arrays, each item being a subject or a run.
407 3D and 4D images are accepted.
409 .. note::
411 If 3D images are given, we suggest to use the mean image
412 of each run.
414 threshold : :obj:`float`, default=0.5
415 The inter-run threshold: the fraction of the
416 total number of runs in for which a :term:`voxel` must be
417 in the mask to be kept in the common mask.
418 threshold=1 corresponds to keeping the intersection of all
419 masks, whereas threshold=0 is the union of all masks.
421 %(lower_cutoff)s
422 Default=0.2.
423 %(upper_cutoff)s
424 Default=0.85.
425 %(connected)s
426 Default=True.
427 %(opening)s
428 Default=2.
429 exclude_zeros : :obj:`bool`, default=False
430 Consider zeros as missing values for the computation of the
431 threshold. This option is useful if the images have been
432 resliced with a large padding of zeros.
433 %(target_affine)s
435 .. note::
436 This parameter is passed to :func:`nilearn.image.resample_img`.
438 %(target_shape)s
440 .. note::
441 This parameter is passed to :func:`nilearn.image.resample_img`.
443 %(memory)s
445 %(n_jobs)s
447 %(verbose0)s
449 Returns
450 -------
451 mask : 3D :class:`nibabel.nifti1.Nifti1Image`
452 The brain mask.
453 """
454 check_params(locals())
455 if len(epi_imgs) == 0:
456 raise TypeError(
457 f"An empty object - {epi_imgs:r} - was passed instead of an "
458 "image or a list of images"
459 )
460 masks = Parallel(n_jobs=n_jobs, verbose=verbose)(
461 delayed(compute_epi_mask)(
462 epi_img,
463 lower_cutoff=lower_cutoff,
464 upper_cutoff=upper_cutoff,
465 connected=connected,
466 opening=opening,
467 exclude_zeros=exclude_zeros,
468 target_affine=target_affine,
469 target_shape=target_shape,
470 memory=memory,
471 )
472 for epi_img in epi_imgs
473 )
475 mask = intersect_masks(masks, connected=connected, threshold=threshold)
476 return mask
479@fill_doc
480def compute_background_mask(
481 data_imgs,
482 border_size=2,
483 connected=False,
484 opening=False,
485 target_affine=None,
486 target_shape=None,
487 memory=None,
488 verbose=0,
489):
490 """Compute a brain mask for the images by guessing \
491 the value of the background from the border of the image.
493 Parameters
494 ----------
495 data_imgs : Niimg-like object
496 See :ref:`extracting_data`.
497 Images used to compute the mask. 3D and 4D images are accepted.
499 .. note::
501 If a 3D image is given, we suggest to use the mean image.
503 %(border_size)s
504 Default=2.
505 %(connected)s
506 Default=False.
507 %(opening)s
508 Default=False.
509 %(target_affine)s
511 .. note::
512 This parameter is passed to :func:`nilearn.image.resample_img`.
514 %(target_shape)s
516 .. note::
517 This parameter is passed to :func:`nilearn.image.resample_img`.
519 %(memory)s
520 %(verbose0)s
522 Returns
523 -------
524 mask : :class:`nibabel.nifti1.Nifti1Image`
525 The brain mask (3D image).
526 """
527 check_params(locals())
528 logger.log("Background mask computation", verbose)
530 data_imgs = check_niimg(data_imgs)
532 # Delayed import to avoid circular imports
533 from nilearn.image.image import compute_mean
535 data, affine = cache(compute_mean, memory)(
536 data_imgs,
537 target_affine=target_affine,
538 target_shape=target_shape,
539 smooth=False,
540 )
542 if np.isnan(get_border_data(data, border_size)).any():
543 # We absolutely need to cater for NaNs as a background:
544 # SPM does that by default
545 mask = np.logical_not(np.isnan(data))
546 else:
547 background = np.median(get_border_data(data, border_size))
548 mask = data != background
550 mask, affine = _post_process_mask(
551 mask,
552 affine,
553 opening=opening,
554 connected=connected,
555 warning_msg="Are you sure that input "
556 "images have a homogeneous background.",
557 )
558 return new_img_like(data_imgs, mask, affine)
561@fill_doc
562def compute_multi_background_mask(
563 data_imgs,
564 border_size=2,
565 connected=True,
566 opening=2,
567 threshold=0.5,
568 target_affine=None,
569 target_shape=None,
570 n_jobs=1,
571 memory=None,
572 verbose=0,
573):
574 """Compute a common mask for several runs or subjects of data.
576 Uses the mask-finding algorithms to extract masks for each run
577 or subject, and then keep only the main connected component of the
578 a given fraction of the intersection of all the masks.
580 Parameters
581 ----------
582 data_imgs : :obj:`list` of Niimg-like objects
583 See :ref:`extracting_data`.
584 A list of arrays, each item being a subject or a run.
585 3D and 4D images are accepted.
587 .. note::
588 If 3D images are given, we suggest to use the mean image
589 of each run.
591 threshold : :obj:`float`, default=0.5
592 The inter-run threshold: the fraction of the
593 total number of run in for which a :term:`voxel` must be
594 in the mask to be kept in the common mask.
595 threshold=1 corresponds to keeping the intersection of all
596 masks, whereas threshold=0 is the union of all masks.
598 %(border_size)s
599 Default=2.
601 %(connected)s
602 Default=True.
604 %(opening)s
606 %(target_affine)s
608 .. note::
609 This parameter is passed to :func:`nilearn.image.resample_img`.
611 %(target_shape)s
613 .. note::
614 This parameter is passed to :func:`nilearn.image.resample_img`.
616 %(memory)s
618 %(n_jobs)s
620 %(verbose0)s
622 Returns
623 -------
624 mask : 3D :class:`nibabel.nifti1.Nifti1Image`
625 The brain mask.
626 """
627 check_params(locals())
628 if len(data_imgs) == 0:
629 raise TypeError(
630 f"An empty object - {data_imgs:r} - was passed instead of an "
631 "image or a list of images"
632 )
633 masks = Parallel(n_jobs=n_jobs, verbose=verbose)(
634 delayed(compute_background_mask)(
635 img,
636 border_size=border_size,
637 connected=connected,
638 opening=opening,
639 target_affine=target_affine,
640 target_shape=target_shape,
641 memory=memory,
642 )
643 for img in data_imgs
644 )
646 mask = intersect_masks(masks, connected=connected, threshold=threshold)
647 return mask
650@fill_doc
651def compute_brain_mask(
652 target_img,
653 threshold=0.5,
654 connected=True,
655 opening=2,
656 memory=None,
657 verbose=0,
658 mask_type="whole-brain",
659):
660 """Compute the whole-brain, grey-matter or white-matter mask.
662 This mask is calculated using MNI152 1mm-resolution template mask onto the
663 target image.
665 Parameters
666 ----------
667 target_img : Niimg-like object
668 See :ref:`extracting_data`.
669 Images used to compute the mask. 3D and 4D images are accepted.
670 Only the shape and affine of ``target_img`` will be used here.
672 threshold : :obj:`float`, default=0.5
673 The value under which the :term:`MNI` template is cut off.
674 %(connected)s
675 Default=True.
676 %(opening)s
677 Default=2.
678 %(memory)s
679 %(verbose0)s
680 %(mask_type)s
682 .. versionadded:: 0.8.1
684 Returns
685 -------
686 mask : :class:`nibabel.nifti1.Nifti1Image`
687 The whole-brain mask (3D image).
688 """
689 check_params(locals())
690 logger.log(f"Template {mask_type} mask computation", verbose)
692 target_img = check_niimg(target_img)
694 if mask_type == "whole-brain":
695 template = load_mni152_template(resolution=1)
696 elif mask_type == "gm":
697 template = load_mni152_gm_template(resolution=1)
698 elif mask_type == "wm":
699 template = load_mni152_wm_template(resolution=1)
700 else:
701 raise ValueError(
702 f"Unknown mask type {mask_type}. "
703 "Only 'whole-brain', 'gm' or 'wm' are accepted."
704 )
706 resampled_template = cache(resampling.resample_to_img, memory)(
707 template,
708 target_img,
709 copy_header=True,
710 force_resample=False, # TODO set to True in 0.13.0
711 )
713 mask = (get_data(resampled_template) >= threshold).astype("int8")
715 warning_message = (
716 f"{mask_type} mask is empty, "
717 "lower the threshold or check your input FOV"
718 )
719 mask, affine = _post_process_mask(
720 mask,
721 target_img.affine,
722 opening=opening,
723 connected=connected,
724 warning_msg=warning_message,
725 )
727 return new_img_like(target_img, mask, affine)
730@fill_doc
731def compute_multi_brain_mask(
732 target_imgs,
733 threshold=0.5,
734 connected=True,
735 opening=2,
736 memory=None,
737 verbose=0,
738 mask_type="whole-brain",
739 **kwargs,
740):
741 """Compute the whole-brain, grey-matter or white-matter mask \
742 for a list of images.
744 The mask is calculated through the resampling of the corresponding
745 MNI152 template mask onto the target image.
747 .. versionadded:: 0.8.1
749 Parameters
750 ----------
751 target_imgs : :obj:`list` of Niimg-like object
752 See :ref:`extracting_data`.
753 Images used to compute the mask. 3D and 4D images are accepted.
755 .. note::
756 The images in this list must be of same shape and affine.
757 The mask is calculated with the first element of the list
758 for only the shape/affine of the image is used for this
759 masking strategy.
761 threshold : :obj:`float`, default=0.5
762 The value under which the :term:`MNI` template is cut off.
764 %(connected)s
765 Default=True.
767 %(opening)s
768 Default=2.
770 %(mask_type)s
772 %(memory)s
774 %(verbose0)s
776 .. note::
777 Argument not used but kept to fit the API
779 **kwargs : optional arguments
780 Arguments such as 'target_affine' are used in the call of other
781 masking strategies, which then would raise an error for this function
782 which does not need such arguments.
784 Returns
785 -------
786 mask : :class:`nibabel.nifti1.Nifti1Image`
787 The brain mask (3D image).
789 See Also
790 --------
791 nilearn.masking.compute_brain_mask
792 """
793 check_params(locals())
794 if len(target_imgs) == 0:
795 raise TypeError(
796 f"An empty object - {target_imgs:r} - was passed instead of an "
797 "image or a list of images"
798 )
800 # Check images in the list have the same FOV without loading them in memory
801 _ = list(check_niimg(target_imgs, return_iterator=True))
803 mask = compute_brain_mask(
804 target_imgs[0],
805 threshold=threshold,
806 connected=connected,
807 opening=opening,
808 memory=memory,
809 verbose=verbose,
810 mask_type=mask_type,
811 )
812 return mask
815#
816# Time series extraction
817#
820@fill_doc
821def apply_mask(
822 imgs, mask_img, dtype="f", smoothing_fwhm=None, ensure_finite=True
823):
824 """Extract signals from images using specified mask.
826 Read the time series from the given image object, using the mask.
828 Parameters
829 ----------
830 imgs : :obj:`list` of 4D Niimg-like objects or 2D SurfaceImage
831 See :ref:`extracting_data`.
832 Images to be masked.
833 List of lists of 3D Niimg-like or 2D surface images are also accepted.
835 mask_img : Niimg-like or SurfaceImage object
836 See :ref:`extracting_data`.
837 Mask array with True value where a voxel / vertex should be used.
839 dtype : numpy dtype or 'f', default="f"
840 The dtype of the output, if 'f', any float output is acceptable
841 and if the data is stored on the disk as floats the data type
842 will not be changed.
844 %(smoothing_fwhm)s
846 .. note::
848 Implies ensure_finite=True.
850 .. warning::
852 Not yet implemented for surface images
854 ensure_finite : :obj:`bool`, default=True
855 If ensure_finite is True, the non-finite values (NaNs and
856 infs) found in the images will be replaced by zeros.
858 Returns
859 -------
860 run_series : :class:`numpy.ndarray`
861 2D array of series with shape
862 (image number, :term:`voxel` / vertex number)
864 Notes
865 -----
866 When using smoothing, ``ensure_finite`` is set to True, as non-finite
867 values would spread across the image.
868 """
869 if not isinstance(imgs, SurfaceImage):
870 mask_img = check_niimg_3d(mask_img)
871 mask, mask_affine = load_mask_img(mask_img)
872 mask_img = new_img_like(mask_img, mask, mask_affine)
873 else:
874 mask, mask_affine = load_mask_img(mask_img)
875 mask_img = mask
876 return apply_mask_fmri(
877 imgs,
878 mask_img,
879 dtype=dtype,
880 smoothing_fwhm=smoothing_fwhm,
881 ensure_finite=ensure_finite,
882 )
885def apply_mask_fmri(
886 imgs, mask_img, dtype="f", smoothing_fwhm=None, ensure_finite=True
887) -> np.ndarray:
888 """Perform similar action to :func:`nilearn.masking.apply_mask`.
890 The only difference with :func:`nilearn.masking.apply_mask` is that
891 some costly checks on ``mask_img`` are not performed: ``mask_img`` is
892 assumed to contain only two different values (this is checked for in
893 :func:`nilearn.masking.apply_mask`, not in this function).
894 """
895 if isinstance(imgs, SurfaceImage) and isinstance(mask_img, SurfaceImage):
896 check_polymesh_equal(mask_img.mesh, imgs.mesh)
898 if smoothing_fwhm is not None:
899 warnings.warn(
900 "Parameter smoothing_fwhm "
901 "is not yet supported for surface data",
902 UserWarning,
903 stacklevel=2,
904 )
905 smoothing_fwhm = True
907 mask_data = as_ndarray(get_surface_data(mask_img), dtype=bool)
908 series = get_surface_data(imgs)
910 if dtype == "f":
911 dtype = series.dtype if series.dtype.kind == "f" else np.float32
913 series = as_ndarray(series, dtype=dtype, order="C", copy=True)
914 del imgs # frees a lot of memory
916 return series[mask_data].T
918 mask_img = check_niimg_3d(mask_img)
919 mask_affine = mask_img.affine
920 mask_data = as_ndarray(get_data(mask_img), dtype=bool)
922 if smoothing_fwhm is not None:
923 ensure_finite = True
925 imgs_img = check_niimg(imgs)
926 affine = imgs_img.affine[:3, :3]
928 if not np.allclose(mask_affine, imgs_img.affine):
929 raise ValueError(
930 f"Mask affine:\n{mask_affine}\n is different from img affine:"
931 "\n{imgs_img.affine}"
932 )
934 if mask_data.shape != imgs_img.shape[:3]:
935 raise ValueError(
936 f"Mask shape: {mask_data.shape!s} is different "
937 f"from img shape:{imgs_img.shape[:3]!s}"
938 )
940 # All the following has been optimized for C order.
941 # Time that may be lost in conversion here is regained multiple times
942 # afterward, especially if smoothing is applied.
943 series = safe_get_data(imgs_img)
945 if dtype == "f":
946 dtype = series.dtype if series.dtype.kind == "f" else np.float32
948 series = as_ndarray(series, dtype=dtype, order="C", copy=True)
949 del imgs # frees a lot of memory
951 # Delayed import to avoid circular imports
952 from nilearn.image.image import smooth_array
954 smooth_array(
955 series,
956 affine,
957 fwhm=smoothing_fwhm,
958 ensure_finite=ensure_finite,
959 copy=False,
960 )
961 return series[mask_data].T
964def _unmask_3d(X, mask, order="C"):
965 """Take masked data and bring them back to 3D (space only).
967 Parameters
968 ----------
969 X : :class:`numpy.ndarray`
970 Masked data. shape: (features,)
972 mask : Niimg-like object
973 See :ref:`extracting_data`.
974 Mask. mask.ndim must be equal to 3, and dtype *must* be bool.
975 """
976 if mask.dtype != bool:
977 raise TypeError("mask must be a boolean array")
978 if X.ndim != 1:
979 raise TypeError("X must be a 1-dimensional array")
980 n_features = mask.sum()
981 if X.shape[0] != n_features:
982 raise TypeError(f"X must be of shape (samples, {n_features}).")
984 data = np.zeros(
985 (mask.shape[0], mask.shape[1], mask.shape[2]),
986 dtype=X.dtype,
987 order=order,
988 )
989 data[mask] = X
990 return data
993def _unmask_4d(X, mask, order="C"):
994 """Take masked data and bring them back to 4D.
996 Parameters
997 ----------
998 X : :class:`numpy.ndarray`
999 Masked data. shape: (samples, features)
1001 mask : :class:`numpy.ndarray`
1002 Mask. mask.ndim must be equal to 4, and dtype *must* be bool.
1004 Returns
1005 -------
1006 data : :class:`numpy.ndarray`
1007 Unmasked data.
1008 Shape: (mask.shape[0], mask.shape[1], mask.shape[2], X.shape[0])
1009 """
1010 if mask.dtype != bool:
1011 raise TypeError("mask must be a boolean array")
1012 if X.ndim != 2:
1013 raise TypeError("X must be a 2-dimensional array")
1014 n_features = mask.sum()
1015 if X.shape[1] != n_features:
1016 raise TypeError(f"X must be of shape (samples, {n_features}).")
1018 data = np.zeros((*mask.shape, X.shape[0]), dtype=X.dtype, order=order)
1019 data[mask, :] = X.T
1020 return data
1023def unmask(X, mask_img, order="F"):
1024 """Take masked data and bring them back into 3D/4D.
1026 This function can be applied to a list of masked data.
1028 Parameters
1029 ----------
1030 X : :class:`numpy.ndarray` (or :obj:`list` of)
1031 Masked data. shape: (samples #, features #).
1032 If X is one-dimensional, it is assumed that samples# == 1.
1034 mask_img : Niimg-like object
1035 See :ref:`extracting_data`.
1036 Must be 3-dimensional.
1038 order : "F" or "C", default='F'
1039 Data ordering in output array. This function is slightly faster with
1040 Fortran ordering.
1042 Returns
1043 -------
1044 data : :class:`nibabel.nifti1.Nifti1Image`
1045 Unmasked data. Depending on the shape of X, data can have
1046 different shapes:
1048 - X.ndim == 2:
1049 Shape: (mask.shape[0], mask.shape[1], mask.shape[2], X.shape[0])
1050 - X.ndim == 1:
1051 Shape: (mask.shape[0], mask.shape[1], mask.shape[2])
1052 """
1053 # Handle lists. This can be a list of other lists / arrays, or a list or
1054 # numbers. In the latter case skip.
1055 if isinstance(X, list) and not isinstance(X[0], numbers.Number):
1056 return [unmask(x, mask_img, order=order) for x in X]
1058 # The code after this block assumes that X is an ndarray; ensure this
1059 X = np.asanyarray(X)
1061 mask_img = check_niimg_3d(mask_img)
1062 mask, affine = load_mask_img(mask_img)
1064 if np.ndim(X) == 2:
1065 unmasked = _unmask_4d(X, mask, order=order)
1066 elif np.ndim(X) == 1:
1067 unmasked = _unmask_3d(X, mask, order=order)
1068 else:
1069 raise TypeError(
1070 f"Masked data X must be 2D or 1D array; got shape: {X.shape!s}"
1071 )
1073 return new_img_like(mask_img, unmasked, affine)
1076def unmask_from_to_3d_array(w, mask):
1077 """Unmask an image into whole brain, \
1078 with off-mask :term:`voxels<voxel>` set to 0.
1080 Used as a stand-alone function in low-level decoding (SpaceNet) and
1081 clustering (ReNA) functions.
1083 Parameters
1084 ----------
1085 w : :class:`numpy.ndarray`, shape (n_features,)
1086 The image to be unmasked.
1088 mask : :class:`numpy.ndarray`
1089 The mask used in the unmasking operation. It is required that
1090 ``mask.sum() == n_features``.
1092 Returns
1093 -------
1094 out : 3D :class:`numpy.ndarray` (same shape as `mask`)
1095 The unmasked version of `w`.
1096 """
1097 if mask.sum() != len(w):
1098 raise ValueError("Expecting mask.sum() == len(w).")
1099 out = np.zeros(mask.shape, dtype=w.dtype)
1100 out[mask] = w
1101 return out