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

1"""Transformer used to apply basic transformations on MRI data.""" 

2 

3import warnings 

4from copy import copy as copy_object 

5from functools import partial 

6 

7import numpy as np 

8from joblib import Memory 

9from sklearn.utils.estimator_checks import check_is_fitted 

10 

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) 

26 

27 

28class _ExtractionFunctor: 

29 func_name = "nifti_masker_extractor" 

30 

31 def __init__(self, mask_img_): 

32 self.mask_img_ = mask_img_ 

33 

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 ) 

43 

44 

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 ) 

72 

73 

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. 

87 

88 Parameters 

89 ---------- 

90 imgs : 3D/4D Niimg-like object 

91 Images to be masked. Can be 3-dimensional or 4-dimensional. 

92 

93 For all other parameters refer to NiftiMasker documentation. 

94 

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. 

100 

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) 

108 

109 imgs = _utils.check_niimg(imgs, atleast_4d=True, ensure_ndim=4) 

110 

111 # Check whether resampling is truly necessary. If so, crop mask 

112 # as small as possible in order to speed up the process 

113 

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 

129 

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 

150 

151 

152@fill_doc 

153class NiftiMasker(BaseMasker): 

154 """Applying a mask to extract time-series from Niimg-like objects. 

155 

156 NiftiMasker is useful when preprocessing (detrending, standardization, 

157 resampling, etc.) of in-mask :term:`voxels<voxel>` is necessary. 

158 

159 Use case: 

160 working with time series of :term:`resting-state` or task maps. 

161 

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. 

174 

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. 

178 

179 %(smoothing_fwhm)s 

180 

181 %(standardize_maskers)s 

182 

183 %(standardize_confounds)s 

184 

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. 

189 

190 %(detrend)s 

191 

192 %(low_pass)s 

193 

194 %(high_pass)s 

195 

196 %(t_r)s 

197 

198 %(target_affine)s 

199 

200 .. note:: 

201 This parameter is passed to :func:`nilearn.image.resample_img`. 

202 

203 %(target_shape)s 

204 

205 .. note:: 

206 This parameter is passed to :func:`nilearn.image.resample_img`. 

207 

208 %(mask_strategy)s 

209 

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`. 

215 

216 Default='background'. 

217 

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. 

224 

225 %(dtype)s 

226 

227 %(memory)s 

228 

229 %(memory_level1)s 

230 

231 %(verbose0)s 

232 

233 reports : :obj:`bool`, default=True 

234 If set to True, data is saved in order to produce a report. 

235 

236 %(cmap)s 

237 default="gray" 

238 Only relevant for the report figures. 

239 

240 %(clean_args)s 

241 .. versionadded:: 0.11.2dev 

242 

243 %(masker_kwargs)s 

244 

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. 

253 

254 affine_ : 4x4 :obj:`numpy.ndarray` 

255 Affine of the transformed image. 

256 

257 n_elements_ : :obj:`int` 

258 The number of voxels in the mask. 

259 

260 .. versionadded:: 0.9.2 

261 

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 

270 

271 """ 

272 

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 

320 

321 # TODO remove when bumping to nilearn >0.13 

322 self.clean_kwargs = kwargs 

323 

324 def generate_report(self): 

325 """Generate a report of the masker.""" 

326 from nilearn.reporting.html_report import generate_report 

327 

328 return generate_report(self) 

329 

330 def _reporting(self): 

331 """Load displays needed for report. 

332 

333 Returns 

334 ------- 

335 displays : list 

336 A list of all displays to be rendered. 

337 

338 """ 

339 import matplotlib.pyplot as plt 

340 

341 from nilearn import plotting 

342 

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] 

347 

348 img = self._reporting_data["images"] 

349 mask = self._reporting_data["mask"] 

350 

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 ) 

381 

382 if "transform" not in self._reporting_data: 

383 return [init_display] 

384 

385 # if resampling was performed 

386 self._report_content["description"] += self._overlay_text 

387 

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 

392 

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 ) 

405 

406 return [init_display, final_display] 

407 

408 def __sklearn_is_fitted__(self): 

409 return hasattr(self, "mask_img_") 

410 

411 @fill_doc 

412 def fit(self, imgs=None, y=None): 

413 """Compute the mask corresponding to the data. 

414 

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. 

421 

422 %(y_dummy)s 

423 """ 

424 del y 

425 check_params(self.__dict__) 

426 

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 ) 

442 

443 if getattr(self, "_shelving", None) is None: 

444 self._shelving = False 

445 

446 self._sanitize_cleaning_parameters() 

447 self.clean_args_ = {} if self.clean_args is None else self.clean_args 

448 

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 ) 

454 

455 self.mask_img_ = self._load_mask(imgs) 

456 

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 {} 

466 

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 ) 

480 

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 

493 

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) 

497 

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 ) 

509 

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 

514 

515 # Load data in memory, while also checking that mask is binary/valid 

516 data, _ = load_mask_img(self.mask_img_, allow_empty=False) 

517 

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 ) 

524 

525 logger.log("Finished fit", verbose=self.verbose) 

526 

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 

544 

545 self._reporting_data["transform"] = [resampl_imgs, self.mask_img_] 

546 

547 return self 

548 

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. 

558 

559 Parameters 

560 ---------- 

561 imgs : 3D/4D Niimg-like object 

562 See :ref:`extracting_data`. 

563 Images to process. 

564 

565 %(confounds)s 

566 

567 %(sample_mask)s 

568 

569 copy : :obj:`bool`, default=True 

570 Indicates whether a copy is returned or not. 

571 

572 Returns 

573 ------- 

574 %(signals_transform_nifti)s 

575 

576 """ 

577 check_is_fitted(self) 

578 

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_ 

597 

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 ) 

619 

620 return data