Coverage for nilearn/maskers/nifti_masker.py: 14%
165 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"""Transformer used to apply basic transformations on MRI data."""
3import warnings
4from copy import copy as copy_object
5from functools import partial
7import numpy as np
8from joblib import Memory
9from sklearn.utils.estimator_checks import check_is_fitted
11from nilearn import _utils
12from nilearn._utils import logger
13from nilearn._utils.docs import fill_doc
14from nilearn._utils.logger import find_stack_level
15from nilearn._utils.param_validation import check_params
16from nilearn.image import crop_img, resample_img
17from nilearn.maskers._utils import compute_middle_image
18from nilearn.maskers.base_masker import BaseMasker, filter_and_extract
19from nilearn.masking import (
20 apply_mask,
21 compute_background_mask,
22 compute_brain_mask,
23 compute_epi_mask,
24 load_mask_img,
25)
28class _ExtractionFunctor:
29 func_name = "nifti_masker_extractor"
31 def __init__(self, mask_img_):
32 self.mask_img_ = mask_img_
34 def __call__(self, imgs):
35 return (
36 apply_mask(
37 imgs,
38 self.mask_img_,
39 dtype=_utils.niimg.img_data_dtype(imgs),
40 ),
41 imgs.affine,
42 )
45def _get_mask_strategy(strategy):
46 """Return the mask computing method based on a provided strategy."""
47 if strategy == "background":
48 return compute_background_mask
49 elif strategy == "epi":
50 return compute_epi_mask
51 elif strategy == "whole-brain-template":
52 return partial(compute_brain_mask, mask_type="whole-brain")
53 elif strategy == "gm-template":
54 return partial(compute_brain_mask, mask_type="gm")
55 elif strategy == "wm-template":
56 return partial(compute_brain_mask, mask_type="wm")
57 elif strategy == "template":
58 warnings.warn(
59 "Masking strategy 'template' is deprecated."
60 "Please use 'whole-brain-template' instead.",
61 stacklevel=find_stack_level(),
62 )
63 return partial(compute_brain_mask, mask_type="whole-brain")
64 else:
65 raise ValueError(
66 f"Unknown value of mask_strategy '{strategy}'. "
67 "Acceptable values are 'background', "
68 "'epi', 'whole-brain-template', "
69 "'gm-template', and "
70 "'wm-template'."
71 )
74def filter_and_mask(
75 imgs,
76 mask_img_,
77 parameters,
78 memory_level=0,
79 memory=None,
80 verbose=0,
81 confounds=None,
82 sample_mask=None,
83 copy=True,
84 dtype=None,
85):
86 """Extract representative time series using given mask.
88 Parameters
89 ----------
90 imgs : 3D/4D Niimg-like object
91 Images to be masked. Can be 3-dimensional or 4-dimensional.
93 For all other parameters refer to NiftiMasker documentation.
95 Returns
96 -------
97 signals : 2D numpy array
98 Signals extracted using the provided mask. It is a scikit-learn
99 friendly 2D array with shape n_sample x n_features.
101 """
102 if memory is None:
103 memory = Memory(location=None)
104 # Convert input to niimg to check shape.
105 # This must be repeated after the shape check because check_niimg will
106 # coerce 5D data to 4D, which we don't want.
107 temp_imgs = _utils.check_niimg(imgs)
109 imgs = _utils.check_niimg(imgs, atleast_4d=True, ensure_ndim=4)
111 # Check whether resampling is truly necessary. If so, crop mask
112 # as small as possible in order to speed up the process
114 if not _utils.niimg_conversions.check_same_fov(imgs, mask_img_):
115 warnings.warn(
116 "imgs are being resampled to the mask_img resolution. "
117 "This process is memory intensive. You might want to provide "
118 "a target_affine that is equal to the affine of the imgs "
119 "or resample the mask beforehand "
120 "to save memory and computation time.",
121 UserWarning,
122 stacklevel=find_stack_level(),
123 )
124 parameters = copy_object(parameters)
125 # now we can crop
126 mask_img_ = crop_img(mask_img_, copy=False, copy_header=True)
127 parameters["target_shape"] = mask_img_.shape
128 parameters["target_affine"] = mask_img_.affine
130 data, _ = filter_and_extract(
131 imgs,
132 _ExtractionFunctor(mask_img_),
133 parameters,
134 memory_level=memory_level,
135 memory=memory,
136 verbose=verbose,
137 confounds=confounds,
138 sample_mask=sample_mask,
139 copy=copy,
140 dtype=dtype,
141 )
142 # For _later_: missing value removal or imputing of missing data
143 # (i.e. we want to get rid of NaNs, if smoothing must be done
144 # earlier)
145 # Optionally: 'doctor_nan', remove voxels with NaNs, other option
146 # for later: some form of imputation
147 if temp_imgs.ndim == 3:
148 data = data.squeeze()
149 return data
152@fill_doc
153class NiftiMasker(BaseMasker):
154 """Applying a mask to extract time-series from Niimg-like objects.
156 NiftiMasker is useful when preprocessing (detrending, standardization,
157 resampling, etc.) of in-mask :term:`voxels<voxel>` is necessary.
159 Use case:
160 working with time series of :term:`resting-state` or task maps.
162 Parameters
163 ----------
164 mask_img : Niimg-like object, optional
165 See :ref:`extracting_data`.
166 Mask for the data. If not given, a mask is computed in the fit step.
167 Optional parameters (mask_args and mask_strategy) can be set to
168 fine tune the mask extraction.
169 If the mask and the images have different resolutions, the images
170 are resampled to the mask resolution.
171 If target_shape and/or target_affine are provided, the mask is
172 resampled first. After this, the images are resampled to the
173 resampled mask.
175 runs : :obj:`numpy.ndarray`, optional
176 Add a run level to the preprocessing. Each run will be
177 detrended independently. Must be a 1D array of n_samples elements.
179 %(smoothing_fwhm)s
181 %(standardize_maskers)s
183 %(standardize_confounds)s
185 high_variance_confounds : :obj:`bool`, default=False
186 If True, high variance confounds are computed on provided image with
187 :func:`nilearn.image.high_variance_confounds` and default parameters
188 and regressed out.
190 %(detrend)s
192 %(low_pass)s
194 %(high_pass)s
196 %(t_r)s
198 %(target_affine)s
200 .. note::
201 This parameter is passed to :func:`nilearn.image.resample_img`.
203 %(target_shape)s
205 .. note::
206 This parameter is passed to :func:`nilearn.image.resample_img`.
208 %(mask_strategy)s
210 .. note::
211 Depending on this value, the mask will be computed from
212 :func:`nilearn.masking.compute_background_mask`,
213 :func:`nilearn.masking.compute_epi_mask`, or
214 :func:`nilearn.masking.compute_brain_mask`.
216 Default='background'.
218 mask_args : :obj:`dict`, optional
219 If mask is None, these are additional parameters passed to
220 :func:`nilearn.masking.compute_background_mask`,
221 or :func:`nilearn.masking.compute_epi_mask`
222 to fine-tune mask computation.
223 Please see the related documentation for details.
225 %(dtype)s
227 %(memory)s
229 %(memory_level1)s
231 %(verbose0)s
233 reports : :obj:`bool`, default=True
234 If set to True, data is saved in order to produce a report.
236 %(cmap)s
237 default="gray"
238 Only relevant for the report figures.
240 %(clean_args)s
241 .. versionadded:: 0.11.2dev
243 %(masker_kwargs)s
245 Attributes
246 ----------
247 mask_img_ : A 3D binary :obj:`nibabel.nifti1.Nifti1Image`
248 The mask of the data, or the one computed from ``imgs`` passed to fit.
249 If a ``mask_img`` is passed at masker construction,
250 then ``mask_img_`` is the resulting binarized version of it
251 where each voxel is ``True`` if all values across samples
252 (for example across timepoints) is finite value different from 0.
254 affine_ : 4x4 :obj:`numpy.ndarray`
255 Affine of the transformed image.
257 n_elements_ : :obj:`int`
258 The number of voxels in the mask.
260 .. versionadded:: 0.9.2
262 See Also
263 --------
264 nilearn.masking.compute_background_mask
265 nilearn.masking.compute_epi_mask
266 nilearn.image.resample_img
267 nilearn.image.high_variance_confounds
268 nilearn.masking.apply_mask
269 nilearn.signal.clean
271 """
273 def __init__(
274 self,
275 mask_img=None,
276 runs=None,
277 smoothing_fwhm=None,
278 standardize=False,
279 standardize_confounds=True,
280 detrend=False,
281 high_variance_confounds=False,
282 low_pass=None,
283 high_pass=None,
284 t_r=None,
285 target_affine=None,
286 target_shape=None,
287 mask_strategy="background",
288 mask_args=None,
289 dtype=None,
290 memory_level=1,
291 memory=None,
292 verbose=0,
293 reports=True,
294 cmap="gray",
295 clean_args=None,
296 **kwargs, # TODO remove when bumping to nilearn >0.13
297 ):
298 # Mask is provided or computed
299 self.mask_img = mask_img
300 self.runs = runs
301 self.smoothing_fwhm = smoothing_fwhm
302 self.standardize = standardize
303 self.standardize_confounds = standardize_confounds
304 self.high_variance_confounds = high_variance_confounds
305 self.detrend = detrend
306 self.low_pass = low_pass
307 self.high_pass = high_pass
308 self.t_r = t_r
309 self.target_affine = target_affine
310 self.target_shape = target_shape
311 self.mask_strategy = mask_strategy
312 self.mask_args = mask_args
313 self.dtype = dtype
314 self.memory = memory
315 self.memory_level = memory_level
316 self.verbose = verbose
317 self.reports = reports
318 self.cmap = cmap
319 self.clean_args = clean_args
321 # TODO remove when bumping to nilearn >0.13
322 self.clean_kwargs = kwargs
324 def generate_report(self):
325 """Generate a report of the masker."""
326 from nilearn.reporting.html_report import generate_report
328 return generate_report(self)
330 def _reporting(self):
331 """Load displays needed for report.
333 Returns
334 -------
335 displays : list
336 A list of all displays to be rendered.
338 """
339 import matplotlib.pyplot as plt
341 from nilearn import plotting
343 # Handle the edge case where this function is
344 # called with a masker having report capabilities disabled
345 if self._reporting_data is None:
346 return [None]
348 img = self._reporting_data["images"]
349 mask = self._reporting_data["mask"]
351 if img is None: # images were not provided to fit
352 msg = (
353 "No image provided to fit in NiftiMasker. "
354 "Setting image to mask for reporting."
355 )
356 warnings.warn(msg, stacklevel=find_stack_level())
357 self._report_content["warning_message"] = msg
358 img = mask
359 if self._reporting_data["dim"] == 5:
360 msg = (
361 "A list of 4D subject images were provided to fit. "
362 "Only first subject is shown in the report."
363 )
364 warnings.warn(msg, stacklevel=find_stack_level())
365 self._report_content["warning_message"] = msg
366 # create display of retained input mask, image
367 # for visual comparison
368 init_display = plotting.plot_img(
369 img,
370 black_bg=False,
371 cmap=self.cmap,
372 )
373 plt.close()
374 if mask is not None:
375 init_display.add_contours(
376 mask,
377 levels=[0.5],
378 colors="g",
379 linewidths=2.5,
380 )
382 if "transform" not in self._reporting_data:
383 return [init_display]
385 # if resampling was performed
386 self._report_content["description"] += self._overlay_text
388 # create display of resampled NiftiImage and mask
389 resampl_img, resampl_mask = self._reporting_data["transform"]
390 if resampl_img is None: # images were not provided to fit
391 resampl_img = resampl_mask
393 final_display = plotting.plot_img(
394 resampl_img,
395 black_bg=False,
396 cmap=self.cmap,
397 )
398 plt.close()
399 final_display.add_contours(
400 resampl_mask,
401 levels=[0.5],
402 colors="g",
403 linewidths=2.5,
404 )
406 return [init_display, final_display]
408 def __sklearn_is_fitted__(self):
409 return hasattr(self, "mask_img_")
411 @fill_doc
412 def fit(self, imgs=None, y=None):
413 """Compute the mask corresponding to the data.
415 Parameters
416 ----------
417 imgs : :obj:`list` of Niimg-like objects or None, default=None
418 See :ref:`extracting_data`.
419 Data on which the mask must be calculated. If this is a list,
420 the affine is considered the same for all.
422 %(y_dummy)s
423 """
424 del y
425 check_params(self.__dict__)
427 self._report_content = {
428 "description": (
429 "This report shows the input Nifti image overlaid "
430 "with the outlines of the mask (in green). We "
431 "recommend to inspect the report for the overlap "
432 "between the mask and its input image. "
433 ),
434 "warning_message": None,
435 "n_elements": 0,
436 "coverage": 0,
437 }
438 self._overlay_text = (
439 "\n To see the input Nifti image before resampling, "
440 "hover over the displayed image."
441 )
443 if getattr(self, "_shelving", None) is None:
444 self._shelving = False
446 self._sanitize_cleaning_parameters()
447 self.clean_args_ = {} if self.clean_args is None else self.clean_args
449 # Load data (if filenames are given, load them)
450 logger.log(
451 f"Loading data from {_utils.repr_niimgs(imgs, shorten=False)}",
452 verbose=self.verbose,
453 )
455 self.mask_img_ = self._load_mask(imgs)
457 # Compute the mask if not given by the user
458 if self.mask_img_ is None:
459 if imgs is None:
460 raise ValueError(
461 "Parameter 'imgs' must be provided to "
462 f"{self.__class__.__name__}.fit() "
463 "if no mask is passed to mask_img."
464 )
465 mask_args = self.mask_args if self.mask_args is not None else {}
467 logger.log("Computing the mask", verbose=self.verbose)
468 compute_mask = _get_mask_strategy(self.mask_strategy)
469 self.mask_img_ = self._cache(compute_mask, ignore=["verbose"])(
470 imgs, verbose=max(0, self.verbose - 1), **mask_args
471 )
472 elif imgs is not None:
473 warnings.warn(
474 f"[{self.__class__.__name__}.fit] "
475 "Generation of a mask has been requested (imgs != None) "
476 "while a mask was given at masker creation. "
477 "Given mask will be used.",
478 stacklevel=find_stack_level(),
479 )
481 if self.reports: # save inputs for reporting
482 self._reporting_data = {
483 "mask": self.mask_img_,
484 "dim": None,
485 "images": imgs,
486 }
487 if imgs is not None:
488 imgs, dims = compute_middle_image(imgs)
489 self._reporting_data["images"] = imgs
490 self._reporting_data["dim"] = dims
491 else:
492 self._reporting_data = None
494 # If resampling is requested, resample also the mask
495 # Resampling: allows the user to change the affine, the shape or both
496 logger.log("Resampling mask", verbose=self.verbose)
498 # TODO switch to force_resample=True
499 # when bumping to version > 0.13
500 self.mask_img_ = self._cache(resample_img)(
501 self.mask_img_,
502 target_affine=self.target_affine,
503 target_shape=self.target_shape,
504 copy=False,
505 interpolation="nearest",
506 copy_header=True,
507 force_resample=False,
508 )
510 if self.target_affine is not None: # resample image to target affine
511 self.affine_ = self.target_affine
512 else: # resample image to mask affine
513 self.affine_ = self.mask_img_.affine
515 # Load data in memory, while also checking that mask is binary/valid
516 data, _ = load_mask_img(self.mask_img_, allow_empty=False)
518 # Infer the number of elements (voxels) in the mask
519 self.n_elements_ = int(data.sum())
520 self._report_content["n_elements"] = self.n_elements_
521 self._report_content["coverage"] = (
522 self.n_elements_ / np.prod(data.shape) * 100
523 )
525 logger.log("Finished fit", verbose=self.verbose)
527 if (self.target_shape is not None) or (
528 (self.target_affine is not None) and self.reports
529 ):
530 if imgs is not None:
531 # TODO switch to force_resample=True
532 # when bumping to version > 0.13
533 resampl_imgs = self._cache(resample_img)(
534 imgs,
535 target_affine=self.affine_,
536 copy=False,
537 interpolation="nearest",
538 copy_header=True,
539 force_resample=False,
540 )
541 resampl_imgs, _ = compute_middle_image(resampl_imgs)
542 else: # imgs not provided to fit
543 resampl_imgs = None
545 self._reporting_data["transform"] = [resampl_imgs, self.mask_img_]
547 return self
549 @fill_doc
550 def transform_single_imgs(
551 self,
552 imgs,
553 confounds=None,
554 sample_mask=None,
555 copy=True,
556 ):
557 """Apply mask, spatial and temporal preprocessing.
559 Parameters
560 ----------
561 imgs : 3D/4D Niimg-like object
562 See :ref:`extracting_data`.
563 Images to process.
565 %(confounds)s
567 %(sample_mask)s
569 copy : :obj:`bool`, default=True
570 Indicates whether a copy is returned or not.
572 Returns
573 -------
574 %(signals_transform_nifti)s
576 """
577 check_is_fitted(self)
579 # Ignore the mask-computing params: they are not useful and will
580 # just invalid the cache for no good reason
581 # target_shape and target_affine are conveyed implicitly in mask_img
582 params = _utils.class_inspect.get_params(
583 self.__class__,
584 self,
585 ignore=[
586 "mask_img",
587 "mask_args",
588 "mask_strategy",
589 "_sample_mask",
590 "sample_mask",
591 ],
592 )
593 params["clean_kwargs"] = self.clean_args_
594 # TODO remove in 0.13.2
595 if self.clean_kwargs:
596 params["clean_kwargs"] = self.clean_kwargs_
598 data = self._cache(
599 filter_and_mask,
600 ignore=[
601 "verbose",
602 "memory",
603 "memory_level",
604 "copy",
605 ],
606 shelve=self._shelving,
607 )(
608 imgs,
609 self.mask_img_,
610 params,
611 memory_level=self.memory_level,
612 memory=self.memory,
613 verbose=self.verbose,
614 confounds=confounds,
615 sample_mask=sample_mask,
616 copy=copy,
617 dtype=self.dtype,
618 )
620 return data