Coverage for nilearn/masking.py: 12%

262 statements  

« 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.""" 

2 

3import numbers 

4import warnings 

5 

6import numpy as np 

7from joblib import Parallel, delayed 

8from scipy.ndimage import binary_dilation, binary_erosion 

9 

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 

34 

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] 

46 

47 

48class _MaskWarning(UserWarning): 

49 """A class to always raise warnings.""" 

50 

51 

52warnings.simplefilter("always", _MaskWarning) 

53 

54 

55def load_mask_img(mask_img, allow_empty=False): 

56 """Check that a mask is valid. 

57 

58 This checks if it contains two values including 0 and load it. 

59 

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. 

65 

66 allow_empty : :obj:`bool`, default=False 

67 Allow loading an empty mask (full of 0 values). 

68 

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 

77 

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 ) 

87 

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) 

94 

95 values = np.unique(mask) 

96 

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 ) 

116 

117 mask = as_ndarray(mask, dtype=bool) 

118 

119 if isinstance(mask_img, NiimgLike): 

120 return mask, mask_img.affine 

121 

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 ) 

126 

127 return mask_img, None 

128 

129 

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]) 

159 

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 

169 

170 

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. 

177 

178 Given a list of input mask images, generate the output image which 

179 is the threshold-level intersection of the inputs. 

180 

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. 

186 

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. 

193 

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) 

210 

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") 

217 

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 

227 

228 grp_mask = grp_mask > (threshold * len(list(mask_imgs))) 

229 

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) 

234 

235 

236def _post_process_mask( 

237 mask, affine, opening=2, connected=True, warning_msg="" 

238): 

239 """Perform post processing on mask. 

240 

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 

260 

261 

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

278 

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. 

282 

283 .. note:: 

284 

285 In case of failure, it is usually advisable to 

286 increase ``lower_cutoff``. 

287 

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. 

294 

295 .. note:: 

296 If a 3D image is given, we suggest to use the mean image. 

297 

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 

309 

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 

315 

316 .. note:: 

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

318 

319 %(target_shape)s 

320 

321 .. note:: 

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

323 

324 %(memory)s 

325 

326 %(verbose0)s 

327 

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) 

335 

336 # Delayed import to avoid circular imports 

337 from nilearn.image.image import compute_mean 

338 

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 ) 

345 

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 ) 

358 

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 ) 

367 

368 mask = mean_epi >= threshold 

369 

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) 

379 

380 

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. 

397 

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. 

401 

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. 

408 

409 .. note:: 

410 

411 If 3D images are given, we suggest to use the mean image 

412 of each run. 

413 

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. 

420 

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 

434 

435 .. note:: 

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

437 

438 %(target_shape)s 

439 

440 .. note:: 

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

442 

443 %(memory)s 

444 

445 %(n_jobs)s 

446 

447 %(verbose0)s 

448 

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 ) 

474 

475 mask = intersect_masks(masks, connected=connected, threshold=threshold) 

476 return mask 

477 

478 

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. 

492 

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. 

498 

499 .. note:: 

500 

501 If a 3D image is given, we suggest to use the mean image. 

502 

503 %(border_size)s 

504 Default=2. 

505 %(connected)s 

506 Default=False. 

507 %(opening)s 

508 Default=False. 

509 %(target_affine)s 

510 

511 .. note:: 

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

513 

514 %(target_shape)s 

515 

516 .. note:: 

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

518 

519 %(memory)s 

520 %(verbose0)s 

521 

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) 

529 

530 data_imgs = check_niimg(data_imgs) 

531 

532 # Delayed import to avoid circular imports 

533 from nilearn.image.image import compute_mean 

534 

535 data, affine = cache(compute_mean, memory)( 

536 data_imgs, 

537 target_affine=target_affine, 

538 target_shape=target_shape, 

539 smooth=False, 

540 ) 

541 

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 

549 

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) 

559 

560 

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. 

575 

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. 

579 

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. 

586 

587 .. note:: 

588 If 3D images are given, we suggest to use the mean image 

589 of each run. 

590 

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. 

597 

598 %(border_size)s 

599 Default=2. 

600 

601 %(connected)s 

602 Default=True. 

603 

604 %(opening)s 

605 

606 %(target_affine)s 

607 

608 .. note:: 

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

610 

611 %(target_shape)s 

612 

613 .. note:: 

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

615 

616 %(memory)s 

617 

618 %(n_jobs)s 

619 

620 %(verbose0)s 

621 

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 ) 

645 

646 mask = intersect_masks(masks, connected=connected, threshold=threshold) 

647 return mask 

648 

649 

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. 

661 

662 This mask is calculated using MNI152 1mm-resolution template mask onto the 

663 target image. 

664 

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. 

671 

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 

681 

682 .. versionadded:: 0.8.1 

683 

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) 

691 

692 target_img = check_niimg(target_img) 

693 

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 ) 

705 

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 ) 

712 

713 mask = (get_data(resampled_template) >= threshold).astype("int8") 

714 

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 ) 

726 

727 return new_img_like(target_img, mask, affine) 

728 

729 

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. 

743 

744 The mask is calculated through the resampling of the corresponding 

745 MNI152 template mask onto the target image. 

746 

747 .. versionadded:: 0.8.1 

748 

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. 

754 

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. 

760 

761 threshold : :obj:`float`, default=0.5 

762 The value under which the :term:`MNI` template is cut off. 

763 

764 %(connected)s 

765 Default=True. 

766 

767 %(opening)s 

768 Default=2. 

769 

770 %(mask_type)s 

771 

772 %(memory)s 

773 

774 %(verbose0)s 

775 

776 .. note:: 

777 Argument not used but kept to fit the API 

778 

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. 

783 

784 Returns 

785 ------- 

786 mask : :class:`nibabel.nifti1.Nifti1Image` 

787 The brain mask (3D image). 

788 

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 ) 

799 

800 # Check images in the list have the same FOV without loading them in memory 

801 _ = list(check_niimg(target_imgs, return_iterator=True)) 

802 

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 

813 

814 

815# 

816# Time series extraction 

817# 

818 

819 

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. 

825 

826 Read the time series from the given image object, using the mask. 

827 

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. 

834 

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. 

838 

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. 

843 

844 %(smoothing_fwhm)s 

845 

846 .. note:: 

847 

848 Implies ensure_finite=True. 

849 

850 .. warning:: 

851 

852 Not yet implemented for surface images 

853 

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. 

857 

858 Returns 

859 ------- 

860 run_series : :class:`numpy.ndarray` 

861 2D array of series with shape 

862 (image number, :term:`voxel` / vertex number) 

863 

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 ) 

883 

884 

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

889 

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) 

897 

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 

906 

907 mask_data = as_ndarray(get_surface_data(mask_img), dtype=bool) 

908 series = get_surface_data(imgs) 

909 

910 if dtype == "f": 

911 dtype = series.dtype if series.dtype.kind == "f" else np.float32 

912 

913 series = as_ndarray(series, dtype=dtype, order="C", copy=True) 

914 del imgs # frees a lot of memory 

915 

916 return series[mask_data].T 

917 

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) 

921 

922 if smoothing_fwhm is not None: 

923 ensure_finite = True 

924 

925 imgs_img = check_niimg(imgs) 

926 affine = imgs_img.affine[:3, :3] 

927 

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 ) 

933 

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 ) 

939 

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) 

944 

945 if dtype == "f": 

946 dtype = series.dtype if series.dtype.kind == "f" else np.float32 

947 

948 series = as_ndarray(series, dtype=dtype, order="C", copy=True) 

949 del imgs # frees a lot of memory 

950 

951 # Delayed import to avoid circular imports 

952 from nilearn.image.image import smooth_array 

953 

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 

962 

963 

964def _unmask_3d(X, mask, order="C"): 

965 """Take masked data and bring them back to 3D (space only). 

966 

967 Parameters 

968 ---------- 

969 X : :class:`numpy.ndarray` 

970 Masked data. shape: (features,) 

971 

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}).") 

983 

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 

991 

992 

993def _unmask_4d(X, mask, order="C"): 

994 """Take masked data and bring them back to 4D. 

995 

996 Parameters 

997 ---------- 

998 X : :class:`numpy.ndarray` 

999 Masked data. shape: (samples, features) 

1000 

1001 mask : :class:`numpy.ndarray` 

1002 Mask. mask.ndim must be equal to 4, and dtype *must* be bool. 

1003 

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}).") 

1017 

1018 data = np.zeros((*mask.shape, X.shape[0]), dtype=X.dtype, order=order) 

1019 data[mask, :] = X.T 

1020 return data 

1021 

1022 

1023def unmask(X, mask_img, order="F"): 

1024 """Take masked data and bring them back into 3D/4D. 

1025 

1026 This function can be applied to a list of masked data. 

1027 

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. 

1033 

1034 mask_img : Niimg-like object 

1035 See :ref:`extracting_data`. 

1036 Must be 3-dimensional. 

1037 

1038 order : "F" or "C", default='F' 

1039 Data ordering in output array. This function is slightly faster with 

1040 Fortran ordering. 

1041 

1042 Returns 

1043 ------- 

1044 data : :class:`nibabel.nifti1.Nifti1Image` 

1045 Unmasked data. Depending on the shape of X, data can have 

1046 different shapes: 

1047 

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] 

1057 

1058 # The code after this block assumes that X is an ndarray; ensure this 

1059 X = np.asanyarray(X) 

1060 

1061 mask_img = check_niimg_3d(mask_img) 

1062 mask, affine = load_mask_img(mask_img) 

1063 

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 ) 

1072 

1073 return new_img_like(mask_img, unmasked, affine) 

1074 

1075 

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. 

1079 

1080 Used as a stand-alone function in low-level decoding (SpaceNet) and 

1081 clustering (ReNA) functions. 

1082 

1083 Parameters 

1084 ---------- 

1085 w : :class:`numpy.ndarray`, shape (n_features,) 

1086 The image to be unmasked. 

1087 

1088 mask : :class:`numpy.ndarray` 

1089 The mask used in the unmasking operation. It is required that 

1090 ``mask.sum() == n_features``. 

1091 

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