Coverage for nilearn/image/image.py: 8%

484 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-20 10:58 +0200

1""" 

2Preprocessing functions for images. 

3 

4See also nilearn.signal. 

5""" 

6 

7import collections.abc 

8import itertools 

9import warnings 

10from copy import deepcopy 

11 

12import numpy as np 

13from joblib import Memory, Parallel, delayed 

14from nibabel import Nifti1Image, Nifti1Pair, load, spatialimages 

15from scipy.ndimage import gaussian_filter1d, generate_binary_structure, label 

16from scipy.stats import scoreatpercentile 

17 

18from nilearn import signal 

19from nilearn._utils import ( 

20 as_ndarray, 

21 check_niimg, 

22 check_niimg_3d, 

23 check_niimg_4d, 

24 fill_doc, 

25 logger, 

26 repr_niimgs, 

27) 

28from nilearn._utils.exceptions import DimensionError 

29from nilearn._utils.helpers import ( 

30 check_copy_header, 

31 stringify_path, 

32) 

33from nilearn._utils.logger import find_stack_level 

34from nilearn._utils.masker_validation import ( 

35 check_compatibility_mask_and_images, 

36) 

37from nilearn._utils.niimg import _get_data, safe_get_data 

38from nilearn._utils.niimg_conversions import ( 

39 _index_img, 

40 check_same_fov, 

41 iter_check_niimg, 

42) 

43from nilearn._utils.param_validation import check_params, check_threshold 

44from nilearn._utils.path_finding import resolve_globbing 

45from nilearn.surface.surface import ( 

46 SurfaceImage, 

47 at_least_2d, 

48 extract_data, 

49) 

50from nilearn.surface.surface import get_data as get_surface_data 

51from nilearn.surface.utils import assert_polymesh_equal, check_polymesh_equal 

52from nilearn.typing import NiimgLike 

53 

54 

55def get_data(img): 

56 """Get the image data as a :class:`numpy.ndarray`. 

57 

58 Parameters 

59 ---------- 

60 img : Niimg-like object or iterable of Niimg-like objects 

61 See :ref:`extracting_data`. 

62 

63 Returns 

64 ------- 

65 :class:`numpy.ndarray` 

66 3D or 4D numpy array depending on the shape of `img`. This function 

67 preserves the type of the image data. 

68 If `img` is an in-memory Nifti image 

69 it returns the image data array itself -- not a copy. 

70 

71 """ 

72 img = check_niimg(img) 

73 return _get_data(img) 

74 

75 

76def high_variance_confounds( 

77 imgs, n_confounds=5, percentile=2.0, detrend=True, mask_img=None 

78) -> np.ndarray: 

79 """Return confounds extracted from input signals with highest variance. 

80 

81 Parameters 

82 ---------- 

83 imgs : 4D Niimg-like or 2D SurfaceImage object 

84 See :ref:`extracting_data`. 

85 

86 mask_img : Niimg-like or SurfaceImage object, or None, default=None 

87 If not provided, all voxels / vertices are used. 

88 If provided, confounds are extracted 

89 from voxels / vertices inside the mask. 

90 See :ref:`extracting_data`. 

91 

92 n_confounds : :obj:`int`, default=5 

93 Number of confounds to return. 

94 

95 percentile : :obj:`float`, default=2.0 

96 Highest-variance signals percentile to keep before computing the 

97 singular value decomposition, 0. <= `percentile` <= 100. 

98 `mask_img.sum() * percentile / 100` must be greater than `n_confounds`. 

99 

100 detrend : :obj:`bool`, default=True 

101 If True, detrend signals before processing. 

102 

103 Returns 

104 ------- 

105 :class:`numpy.ndarray` 

106 Highest variance confounds. Shape: *(number_of_scans, n_confounds)*. 

107 

108 Notes 

109 ----- 

110 This method is related to what has been published in the literature 

111 as 'CompCor' (Behzadi NeuroImage 2007). 

112 

113 The implemented algorithm does the following: 

114 

115 - Computes the sum of squares for each signal (no mean removal). 

116 - Keeps a given percentile of signals with highest variance (percentile). 

117 - Computes an SVD of the extracted signals. 

118 - Returns a given number (n_confounds) of signals from the SVD with 

119 highest singular values. 

120 

121 See Also 

122 -------- 

123 nilearn.signal.high_variance_confounds 

124 

125 """ 

126 from .. import masking 

127 

128 check_compatibility_mask_and_images(mask_img, imgs) 

129 

130 if mask_img is not None: 

131 if isinstance(imgs, SurfaceImage): 

132 check_polymesh_equal(mask_img.mesh, imgs.mesh) 

133 sigs = masking.apply_mask(imgs, mask_img) 

134 

135 # Load the data only if it doesn't need to be masked 

136 # Not using apply_mask here saves memory in most cases. 

137 elif isinstance(imgs, SurfaceImage): 

138 sigs = as_ndarray(get_surface_data(imgs)) 

139 sigs = np.reshape(sigs, (-1, sigs.shape[-1])).T 

140 else: 

141 imgs = check_niimg_4d(imgs) 

142 sigs = as_ndarray(get_data(imgs)) 

143 sigs = np.reshape(sigs, (-1, sigs.shape[-1])).T 

144 

145 del imgs # help reduce memory consumption 

146 

147 return signal.high_variance_confounds( 

148 sigs, n_confounds=n_confounds, percentile=percentile, detrend=detrend 

149 ) 

150 

151 

152def _fast_smooth_array(arr): 

153 """Perform simple smoothing. 

154 

155 Less computationally expensive than applying a Gaussian filter. 

156 

157 Only the first three dimensions of the array will be smoothed. The 

158 filter uses [0.2, 1, 0.2] weights in each direction and use a 

159 normalization to preserve the local average value. 

160 

161 Parameters 

162 ---------- 

163 arr : :class:`numpy.ndarray` 

164 4D array, with image number as last dimension. 3D arrays are 

165 also accepted. 

166 

167 Returns 

168 ------- 

169 :class:`numpy.ndarray` 

170 Smoothed array. 

171 

172 Notes 

173 ----- 

174 Rather than calling this function directly, users are encouraged 

175 to call the high-level function :func:`smooth_img` with 

176 `fwhm='fast'`. 

177 

178 """ 

179 neighbor_weight = 0.2 

180 # 6 neighbors in 3D if not on an edge 

181 n_neighbors = 6 

182 # This scale ensures that a uniform array stays uniform 

183 # except on the array edges 

184 scale = 1 + n_neighbors * neighbor_weight 

185 

186 # Need to copy because the smoothing is done in multiple statements 

187 # and there does not seem to be an easy way to do it in place 

188 smoothed_arr = arr.copy() 

189 weighted_arr = neighbor_weight * arr 

190 

191 smoothed_arr[:-1] += weighted_arr[1:] 

192 smoothed_arr[1:] += weighted_arr[:-1] 

193 smoothed_arr[:, :-1] += weighted_arr[:, 1:] 

194 smoothed_arr[:, 1:] += weighted_arr[:, :-1] 

195 smoothed_arr[:, :, :-1] += weighted_arr[:, :, 1:] 

196 smoothed_arr[:, :, 1:] += weighted_arr[:, :, :-1] 

197 smoothed_arr /= scale 

198 

199 return smoothed_arr 

200 

201 

202@fill_doc 

203def smooth_array(arr, affine, fwhm=None, ensure_finite=True, copy=True): 

204 """Smooth images by applying a Gaussian filter. 

205 

206 Apply a Gaussian filter along the three first dimensions of `arr`. 

207 

208 Parameters 

209 ---------- 

210 arr : :class:`numpy.ndarray` 

211 4D array, with image number as last dimension. 3D arrays are also 

212 accepted. 

213 

214 affine : :class:`numpy.ndarray` 

215 (4, 4) matrix, giving affine transformation for image. (3, 3) matrices 

216 are also accepted (only these coefficients are used). 

217 If `fwhm='fast'`, the affine is not used and can be None. 

218 %(fwhm)s 

219 ensure_finite : :obj:`bool`, default=True 

220 If True, replace every non-finite values (like NaNs) by zero before 

221 filtering. 

222 

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

224 If True, input array is not modified. True by default: the filtering 

225 is not performed in-place. 

226 

227 Returns 

228 ------- 

229 :class:`numpy.ndarray` 

230 Filtered `arr`. 

231 

232 Notes 

233 ----- 

234 This function is most efficient with arr in C order. 

235 

236 """ 

237 # Here, we have to investigate use cases of fwhm. Particularly, if fwhm=0. 

238 # See issue #1537 

239 if isinstance(fwhm, (int, float)) and (fwhm == 0.0): 

240 warnings.warn( 

241 f"The parameter 'fwhm' for smoothing is specified as {fwhm}. " 

242 "Setting it to None (no smoothing will be performed)", 

243 stacklevel=find_stack_level(), 

244 ) 

245 fwhm = None 

246 if arr.dtype.kind == "i": 

247 if arr.dtype == np.int64: 

248 arr = arr.astype(np.float64) 

249 else: 

250 arr = arr.astype(np.float32) # We don't need crazy precision. 

251 if copy: 

252 arr = arr.copy() 

253 if ensure_finite: 

254 # SPM tends to put NaNs in the data outside the brain 

255 arr[np.logical_not(np.isfinite(arr))] = 0 

256 if isinstance(fwhm, str) and (fwhm == "fast"): 

257 arr = _fast_smooth_array(arr) 

258 elif fwhm is not None: 

259 fwhm = np.asarray([fwhm]).ravel() 

260 fwhm = np.asarray([0.0 if elem is None else elem for elem in fwhm]) 

261 affine = affine[:3, :3] # Keep only the scale part. 

262 fwhm_over_sigma_ratio = np.sqrt(8 * np.log(2)) # FWHM to sigma. 

263 vox_size = np.sqrt(np.sum(affine**2, axis=0)) 

264 sigma = fwhm / (fwhm_over_sigma_ratio * vox_size) 

265 for n, s in enumerate(sigma): 

266 if s > 0.0: 

267 gaussian_filter1d(arr, s, output=arr, axis=n) 

268 return arr 

269 

270 

271@fill_doc 

272def smooth_img(imgs, fwhm): 

273 """Smooth images by applying a Gaussian filter. 

274 

275 Apply a Gaussian filter along the three first dimensions of `arr`. 

276 In all cases, non-finite values in input image are replaced by zeros. 

277 

278 Parameters 

279 ---------- 

280 imgs : Niimg-like object or iterable of Niimg-like objects 

281 Image(s) to smooth (see :ref:`extracting_data` 

282 for a detailed description of the valid input types). 

283 %(fwhm)s 

284 

285 Returns 

286 ------- 

287 :class:`nibabel.nifti1.Nifti1Image` or list of 

288 Filtered input image. If `imgs` is an iterable, 

289 then `filtered_img` is a list. 

290 

291 """ 

292 # Use hasattr() instead of isinstance to workaround a Python 2.6/2.7 bug 

293 # See http://bugs.python.org/issue7624 

294 imgs = stringify_path(imgs) 

295 if hasattr(imgs, "__iter__") and not isinstance(imgs, str): 

296 single_img = False 

297 else: 

298 single_img = True 

299 imgs = [imgs] 

300 

301 ret = [] 

302 for img in imgs: 

303 img = check_niimg(img) 

304 affine = img.affine 

305 filtered = smooth_array( 

306 get_data(img), affine, fwhm=fwhm, ensure_finite=True, copy=True 

307 ) 

308 ret.append(new_img_like(img, filtered, affine, copy_header=True)) 

309 

310 return ret[0] if single_img else ret 

311 

312 

313def _crop_img_to(img, slices, copy=True, copy_header=False): 

314 """Crops an image to a smaller size. 

315 

316 Crop `img` to size indicated by slices and adjust affine accordingly. 

317 

318 Parameters 

319 ---------- 

320 img : Niimg-like object 

321 Image to be cropped. 

322 If slices has less entries than `img` has dimensions, 

323 the slices will be applied to the first `len(slices)` dimensions 

324 (See :ref:`extracting_data`). 

325 

326 slices : list of slices 

327 Defines the range of the crop. 

328 E.g. [slice(20, 200), slice(40, 150), slice(0, 100)] defines a cube. 

329 

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

331 Specifies whether cropped data is to be copied or not. 

332 

333 copy_header : :obj:`bool` 

334 Whether to copy the header of the input image to the output. 

335 If None, the default behavior is to not copy the header. 

336 

337 .. versionadded:: 0.11.0 

338 

339 This parameter will be set to True by default in 0.13.0. 

340 

341 Returns 

342 ------- 

343 Niimg-like object 

344 Cropped version of the input image. 

345 

346 offset : :obj:`list`, optional 

347 List of tuples representing the number of voxels removed 

348 (before, after) the cropped volumes, i.e.: 

349 *[(x1_pre, x1_post), (x2_pre, x2_post), ..., (xN_pre, xN_post)]* 

350 

351 """ 

352 img = check_niimg(img) 

353 

354 data = get_data(img) 

355 affine = img.affine 

356 

357 cropped_data = data[tuple(slices)] 

358 if copy: 

359 cropped_data = cropped_data.copy() 

360 

361 linear_part = affine[:3, :3] 

362 old_origin = affine[:3, 3] 

363 new_origin_voxel = np.array([s.start for s in slices]) 

364 new_origin = old_origin + linear_part.dot(new_origin_voxel) 

365 

366 new_affine = np.eye(4) 

367 new_affine[:3, :3] = linear_part 

368 new_affine[:3, 3] = new_origin 

369 

370 return new_img_like(img, cropped_data, new_affine, copy_header=copy_header) 

371 

372 

373def crop_img( 

374 img, rtol=1e-8, copy=True, pad=True, return_offset=False, copy_header=False 

375): 

376 """Crops an image as much as possible. 

377 

378 Will crop `img`, removing as many zero entries as possible without 

379 touching non-zero entries. 

380 Will leave one :term:`voxel` of zero padding 

381 around the obtained non-zero area in order 

382 to avoid sampling issues later on. 

383 

384 Parameters 

385 ---------- 

386 img : Niimg-like object 

387 Image to be cropped (see :ref:`extracting_data` for a detailed 

388 description of the valid input types). 

389 

390 rtol : :obj:`float`, default=1e-08 

391 relative tolerance (with respect to maximal absolute value of the 

392 image), under which values are considered negligible and thus 

393 croppable. 

394 

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

396 Specifies whether cropped data is copied or not. 

397 

398 pad : :obj:`bool`, default=True 

399 Toggles adding 1-voxel of 0s around the border. 

400 

401 return_offset : :obj:`bool`, default=False 

402 Specifies whether to return a tuple of the removed padding. 

403 

404 copy_header : :obj:`bool`, default=False 

405 Whether to copy the header of the input image to the output. 

406 

407 .. versionadded:: 0.11.0 

408 

409 This parameter will be set to True by default in 0.13.0. 

410 

411 Returns 

412 ------- 

413 Niimg-like object or :obj:`tuple` 

414 Cropped version of the input image and, if `return_offset=True`, 

415 a tuple of tuples representing the number of voxels 

416 removed (before, after) the cropped volumes, i.e.: 

417 *[(x1_pre, x1_post), (x2_pre, x2_post), ..., (xN_pre, xN_post)]* 

418 

419 """ 

420 # TODO: remove this warning in 0.13.0 

421 check_copy_header(copy_header) 

422 

423 img = check_niimg(img) 

424 data = get_data(img) 

425 infinity_norm = max(-data.min(), data.max()) 

426 passes_threshold = np.logical_or( 

427 data < -rtol * infinity_norm, data > rtol * infinity_norm 

428 ) 

429 

430 if data.ndim == 4: 

431 passes_threshold = np.any(passes_threshold, axis=-1) 

432 coords = np.array(np.where(passes_threshold)) 

433 

434 # Sets full range if no data are found along the axis 

435 if coords.shape[1] == 0: 

436 start, end = [0, 0, 0], list(data.shape) 

437 else: 

438 start = coords.min(axis=1) 

439 end = coords.max(axis=1) + 1 

440 

441 # pad with one voxel to avoid resampling problems 

442 if pad: 

443 start = np.maximum(start - 1, 0) 

444 end = np.minimum(end + 1, data.shape[:3]) 

445 

446 slices = [slice(s, e) for s, e in zip(start, end)][:3] 

447 cropped_im = _crop_img_to(img, slices, copy=copy, copy_header=copy_header) 

448 return (cropped_im, tuple(slices)) if return_offset else cropped_im 

449 

450 

451def compute_mean(imgs, target_affine=None, target_shape=None, smooth=False): 

452 """Compute the mean of the images over time or the 4th dimension. 

453 

454 See mean_img for details about the API. 

455 """ 

456 from . import resampling 

457 

458 input_repr = repr_niimgs(imgs, shorten=True) 

459 

460 imgs = check_niimg(imgs) 

461 mean_data = safe_get_data(imgs) 

462 affine = imgs.affine 

463 # Free memory ASAP 

464 del imgs 

465 if mean_data.ndim not in (3, 4): 

466 raise ValueError( 

467 "Computation expects 3D or 4D images, " 

468 f"but {mean_data.ndim} dimensions were given ({input_repr})" 

469 ) 

470 if mean_data.ndim == 4: 

471 mean_data = mean_data.mean(axis=-1) 

472 else: 

473 mean_data = mean_data.copy() 

474 # TODO switch to force_resample=True 

475 # when bumping to version > 0.13 

476 mean_data = resampling.resample_img( 

477 Nifti1Image(mean_data, affine), 

478 target_affine=target_affine, 

479 target_shape=target_shape, 

480 copy=False, 

481 copy_header=True, 

482 force_resample=False, 

483 ) 

484 affine = mean_data.affine 

485 mean_data = get_data(mean_data) 

486 

487 if smooth: 

488 nan_mask = np.isnan(mean_data) 

489 mean_data = smooth_array( 

490 mean_data, 

491 affine=np.eye(4), 

492 fwhm=smooth, 

493 ensure_finite=True, 

494 copy=False, 

495 ) 

496 mean_data[nan_mask] = np.nan 

497 

498 return mean_data, affine 

499 

500 

501def _compute_surface_mean(imgs: SurfaceImage) -> SurfaceImage: 

502 """Compute mean of a single surface image over its 2nd dimension.""" 

503 if len(imgs.shape) < 2 or imgs.shape[1] < 2: 

504 data = imgs.data 

505 else: 

506 data = { 

507 part: np.mean(value, axis=1).astype(float) 

508 for part, value in imgs.data.parts.items() 

509 } 

510 return new_img_like(imgs, data=data) 

511 

512 

513@fill_doc 

514def mean_img( 

515 imgs, 

516 target_affine=None, 

517 target_shape=None, 

518 verbose=0, 

519 n_jobs=1, 

520 copy_header=False, 

521): 

522 """Compute the mean over images. 

523 

524 This can be a mean over time or the 4th dimension for a volume, 

525 or the 2nd dimension for a surface image. 

526 

527 Note that if list of 4D volume images (or 2D surface images) 

528 are given, 

529 the mean of each image is computed separately, 

530 and the resulting mean is computed after. 

531 

532 Parameters 

533 ---------- 

534 imgs : Niimg-like or or :obj:`~nilearn.surface.SurfaceImage` object, or \ 

535 iterable of Niimg-like or :obj:`~nilearn.surface.SurfaceImage`. 

536 Images to be averaged over 'time' 

537 (see :ref:`extracting_data` 

538 for a detailed description of the valid input types). 

539 

540 %(target_affine)s 

541 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

542 

543 %(target_shape)s 

544 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

545 

546 %(verbose0)s 

547 

548 n_jobs : :obj:`int`, default=1 

549 The number of CPUs to use to do the computation (-1 means 

550 'all CPUs'). 

551 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

552 

553 copy_header : :obj:`bool`, default=False 

554 Whether to copy the header of the input image to the output. 

555 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

556 

557 .. versionadded:: 0.11.0 

558 

559 This parameter will be set to True by default in 0.13.0. 

560 

561 Returns 

562 ------- 

563 :obj:`~nibabel.nifti1.Nifti1Image` or :obj:`~nilearn.surface.SurfaceImage` 

564 Mean image. 

565 

566 See Also 

567 -------- 

568 nilearn.image.math_img : For more general operations on images. 

569 

570 """ 

571 is_iterable = isinstance(imgs, collections.abc.Iterable) 

572 is_surface_img = isinstance(imgs, SurfaceImage) or ( 

573 is_iterable and all(isinstance(x, SurfaceImage) for x in imgs) 

574 ) 

575 if is_surface_img: 

576 if not is_iterable: 

577 imgs = [imgs] 

578 all_means = concat_imgs([_compute_surface_mean(x) for x in imgs]) 

579 return _compute_surface_mean(all_means) 

580 

581 # TODO: remove this warning in 0.13.0 

582 check_copy_header(copy_header) 

583 

584 imgs = stringify_path(imgs) 

585 is_str = isinstance(imgs, str) 

586 is_iterable = isinstance(imgs, collections.abc.Iterable) 

587 if is_str or not is_iterable: 

588 imgs = [imgs] 

589 

590 imgs_iter = iter(imgs) 

591 first_img = check_niimg(next(imgs_iter)) 

592 

593 # Compute the first mean to retrieve the reference 

594 # target_affine and target_shape if_needed 

595 n_imgs = 1 

596 running_mean, first_affine = compute_mean( 

597 first_img, target_affine=target_affine, target_shape=target_shape 

598 ) 

599 

600 if target_affine is None or target_shape is None: 

601 target_affine = first_affine 

602 target_shape = running_mean.shape[:3] 

603 

604 for this_mean in Parallel(n_jobs=n_jobs, verbose=verbose)( 

605 delayed(compute_mean)( 

606 n, target_affine=target_affine, target_shape=target_shape 

607 ) 

608 for n in imgs_iter 

609 ): 

610 n_imgs += 1 

611 # compute_mean returns (mean_img, affine) 

612 this_mean = this_mean[0] 

613 running_mean += this_mean 

614 

615 running_mean = running_mean / float(n_imgs) 

616 return new_img_like( 

617 first_img, running_mean, target_affine, copy_header=copy_header 

618 ) 

619 

620 

621def swap_img_hemispheres(img): 

622 """Perform swapping of hemispheres in the indicated NIfTI image. 

623 

624 Use case: synchronizing ROIs across hemispheres. 

625 

626 Parameters 

627 ---------- 

628 img : Niimg-like object 

629 Images to swap (see :ref:`extracting_data` for a detailed description 

630 of the valid input types). 

631 

632 Returns 

633 ------- 

634 :class:`~nibabel.nifti1.Nifti1Image` 

635 Hemispherically swapped image. 

636 

637 Notes 

638 ----- 

639 Assumes that the image is sagitally aligned. 

640 

641 Should be used with caution (confusion might be caused with 

642 radio/neuro conventions) 

643 

644 Note that this does not require a change of the affine matrix. 

645 

646 """ 

647 from .resampling import reorder_img 

648 

649 # Check input is really a path to a nifti file or a nifti object 

650 img = check_niimg_3d(img) 

651 

652 # get nifti in x-y-z order 

653 img = reorder_img(img, copy_header=True) 

654 

655 # create swapped nifti object 

656 out_img = new_img_like( 

657 img, get_data(img)[::-1], img.affine, copy_header=True 

658 ) 

659 

660 return out_img 

661 

662 

663def index_img(imgs, index): 

664 """Indexes into a image in the last dimension. 

665 

666 Common use cases include extracting an image out of `img` or 

667 creating a 4D (or 2D for surface) image 

668 whose data is a subset of `img` data. 

669 

670 Parameters 

671 ---------- 

672 imgs : 4D Niimg-like object or 2D :obj:`~nilearn.surface.SurfaceImage` 

673 See :ref:`extracting_data`. 

674 

675 index : Any type compatible with numpy array indexing 

676 Used for indexing the data array in the last dimension. 

677 

678 Returns 

679 ------- 

680 :obj:`~nibabel.nifti1.Nifti1Image` or :obj:`~nilearn.surface.SurfaceImage` 

681 Indexed image. 

682 

683 See Also 

684 -------- 

685 nilearn.image.concat_imgs 

686 nilearn.image.iter_img 

687 

688 Examples 

689 -------- 

690 First we concatenate two MNI152 images to create a 4D-image:: 

691 

692 >>> from nilearn import datasets 

693 >>> from nilearn.image import concat_imgs, index_img 

694 >>> joint_mni_image = concat_imgs([datasets.load_mni152_template(), 

695 ... datasets.load_mni152_template()]) 

696 >>> print(joint_mni_image.shape) 

697 (197, 233, 189, 2) 

698 

699 We can now select one slice from the last dimension of this 4D-image:: 

700 

701 >>> single_mni_image = index_img(joint_mni_image, 1) 

702 >>> print(single_mni_image.shape) 

703 (197, 233, 189) 

704 

705 We can also select multiple frames using the `slice` constructor:: 

706 

707 >>> five_mni_images = concat_imgs([datasets.load_mni152_template()] * 5) 

708 >>> print(five_mni_images.shape) 

709 (197, 233, 189, 5) 

710 

711 >>> first_three_images = index_img(five_mni_images, 

712 ... slice(0, 3)) 

713 >>> print(first_three_images.shape) 

714 (197, 233, 189, 3) 

715 

716 """ 

717 if isinstance(imgs, SurfaceImage): 

718 imgs = at_least_2d(imgs) 

719 return new_img_like(imgs, data=extract_data(imgs, index)) 

720 

721 imgs = check_niimg_4d(imgs) 

722 # duck-type for pandas arrays, and select the 'values' attr 

723 if hasattr(index, "values") and hasattr(index, "iloc"): 

724 index = index.to_numpy().flatten() 

725 return _index_img(imgs, index) 

726 

727 

728def iter_img(imgs): 

729 """Iterate over images. 

730 

731 Could be along the the 4th dimension for 4D Niimg-like object 

732 or the 2nd dimension for 2D Surface images.. 

733 

734 Parameters 

735 ---------- 

736 imgs : 4D Niimg-like object or :obj:`~nilearn.surface.SurfaceImage` 

737 See :ref:`extracting_data`. 

738 

739 Returns 

740 ------- 

741 Iterator of :class:`~nibabel.nifti1.Nifti1Image` \ 

742 or :obj:`~nilearn.surface.SurfaceImage` 

743 

744 See Also 

745 -------- 

746 nilearn.image.index_img 

747 

748 """ 

749 if isinstance(imgs, SurfaceImage): 

750 output = ( 

751 index_img(imgs, i) for i in range(at_least_2d(imgs).shape[1]) 

752 ) 

753 return output 

754 return check_niimg_4d(imgs, return_iterator=True) 

755 

756 

757def _downcast_from_int64_if_possible(data): 

758 """Try to downcast to int32 if possible. 

759 

760 If `data` is 64-bit ints and can be converted to (signed) int 32, 

761 return an int32 copy, otherwise return `data` itself. 

762 """ 

763 if data.dtype not in (np.int64, np.uint64): 

764 return data 

765 img_min, img_max = np.min(data), np.max(data) 

766 type_info = np.iinfo(np.int32) 

767 can_cast = type_info.min <= img_min and type_info.max >= img_max 

768 if can_cast: 

769 warnings.warn( 

770 "Data array used to create a new image contains 64-bit ints. " 

771 "This is likely due to creating the array with numpy and " 

772 "passing `int` as the `dtype`. Many tools such as FSL and SPM " 

773 "cannot deal with int64 in Nifti images, so for compatibility the " 

774 "data has been converted to int32.", 

775 stacklevel=find_stack_level(), 

776 ) 

777 return data.astype("int32") 

778 warnings.warn( 

779 "Data array used to create a new image contains 64-bit ints, and " 

780 "some values too large to store in 32-bit ints. The resulting image " 

781 "thus contains 64-bit ints, which may cause some compatibility issues " 

782 "with some other tools or an error when saving the image to a " 

783 "Nifti file.", 

784 stacklevel=find_stack_level(), 

785 ) 

786 return data 

787 

788 

789def new_img_like(ref_niimg, data, affine=None, copy_header=False): 

790 """Create a new image of the same class as the reference image. 

791 

792 Parameters 

793 ---------- 

794 ref_niimg : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage` 

795 Reference image. The new image will be of the same type. 

796 

797 data : :class:`numpy.ndarray` 

798 Data to be stored in the image. If data dtype is a boolean, then data 

799 is cast to 'uint8' by default. 

800 

801 .. versionchanged:: 0.9.2 

802 Changed default dtype casting of booleans from 'int8' to 'uint8'. 

803 

804 affine : 4x4 :class:`numpy.ndarray`, default=None 

805 Transformation matrix. 

806 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

807 

808 copy_header : :obj:`bool`, default=False 

809 Indicated if the header of the reference image should be used to 

810 create the new image. 

811 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

812 

813 Returns 

814 ------- 

815 Niimg-like or :obj:`~nilearn.surface.SurfaceImage` object 

816 A loaded image with the same file type (and, optionally, header) 

817 as the reference image. 

818 

819 """ 

820 if isinstance(ref_niimg, SurfaceImage): 

821 mesh = ref_niimg.mesh 

822 return SurfaceImage( 

823 mesh=deepcopy(mesh), 

824 data=data, 

825 ) 

826 # Hand-written loading code to avoid too much memory consumption 

827 orig_ref_niimg = ref_niimg 

828 ref_niimg = stringify_path(ref_niimg) 

829 is_str = isinstance(ref_niimg, str) 

830 has_get_fdata = hasattr(ref_niimg, "get_fdata") 

831 has_iter = hasattr(ref_niimg, "__iter__") 

832 has_affine = hasattr(ref_niimg, "affine") 

833 if has_iter and not any([is_str, has_get_fdata]): 

834 ref_niimg = ref_niimg[0] 

835 ref_niimg = stringify_path(ref_niimg) 

836 is_str = isinstance(ref_niimg, str) 

837 has_get_fdata = hasattr(ref_niimg, "get_fdata") 

838 has_affine = hasattr(ref_niimg, "affine") 

839 if not (has_get_fdata and has_affine): 

840 if is_str: 

841 ref_niimg = load(ref_niimg) 

842 else: 

843 raise TypeError( 

844 "The reference image should be a niimg." 

845 f" {orig_ref_niimg!r} was passed" 

846 ) 

847 

848 if affine is None: 

849 affine = ref_niimg.affine 

850 if data.dtype == bool: 

851 data = as_ndarray(data, dtype=np.uint8) 

852 data = _downcast_from_int64_if_possible(data) 

853 header = None 

854 if copy_header and ref_niimg.header is not None: 

855 header = ref_niimg.header.copy() 

856 try: 

857 "something" in header # noqa: B015 

858 except TypeError: 

859 pass 

860 else: 

861 if "scl_slope" in header: 

862 header["scl_slope"] = 0.0 

863 if "scl_inter" in header: 

864 header["scl_inter"] = 0.0 

865 # 'glmax' is removed for Nifti2Image. Modify only if 'glmax' is 

866 # available in header. See issue #1611 

867 if "glmax" in header: 

868 header["glmax"] = 0.0 

869 if "cal_max" in header: 

870 header["cal_max"] = np.max(data) if data.size > 0 else 0.0 

871 if "cal_min" in header: 

872 header["cal_min"] = np.min(data) if data.size > 0 else 0.0 

873 klass = ref_niimg.__class__ 

874 if klass is Nifti1Pair: 

875 # Nifti1Pair is an internal class, without a to_filename, 

876 # we shouldn't return it 

877 klass = Nifti1Image 

878 return klass(data, affine, header=header) 

879 

880 

881def _apply_cluster_size_threshold(arr, cluster_threshold, copy=True): 

882 """Apply cluster-extent thresholding to voxel-wise thresholded array. 

883 

884 Parameters 

885 ---------- 

886 arr : :obj:`numpy.ndarray` of shape (X, Y, Z) 

887 3D array that has been thresholded at the voxel level. 

888 cluster_threshold : :obj:`float` 

889 Cluster-size threshold, in voxels, to apply to ``arr``. 

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

891 Whether to copy the array before modifying it or not. 

892 

893 Returns 

894 ------- 

895 arr : :obj:`numpy.ndarray` of shape (X, Y, Z) 

896 Cluster-extent thresholded array. 

897 

898 Notes 

899 ----- 

900 Clusters are defined in a bi-sided manner; 

901 both negative and positive clusters are evaluated, 

902 but this is done separately for each sign. 

903 

904 Clusters are defined using 6-connectivity, also known as NN1 (in AFNI) or 

905 "faces" connectivity. 

906 """ 

907 assert arr.ndim == 3 

908 

909 if copy: 

910 arr = arr.copy() 

911 

912 # Define array for 6-connectivity, aka NN1 or "faces" 

913 bin_struct = generate_binary_structure(3, 1) 

914 

915 for sign in np.unique(np.sign(arr)): 

916 # Binarize using one-sided cluster-defining threshold 

917 binarized = ((arr * sign) > 0).astype(int) 

918 

919 # Apply cluster threshold 

920 label_map = label(binarized, bin_struct)[0] 

921 clust_ids = sorted(np.unique(label_map)[1:]) 

922 for c_val in clust_ids: 

923 if np.sum(label_map == c_val) < cluster_threshold: 

924 arr[label_map == c_val] = 0 

925 

926 return arr 

927 

928 

929def threshold_img( 

930 img, 

931 threshold, 

932 cluster_threshold=0, 

933 two_sided=True, 

934 mask_img=None, 

935 copy=True, 

936 copy_header=False, 

937): 

938 """Threshold the given input image, mostly statistical or atlas images. 

939 

940 Thresholding can be done based on direct image intensities or selection 

941 threshold with given percentile. 

942 

943 - If ``threshold`` is a :obj:`float`: 

944 

945 we threshold the image based on image intensities. 

946 

947 - When ``two_sided`` is True: 

948 

949 The given value should be within the range of minimum and maximum 

950 intensity of the input image. 

951 All instensities in the interval ``[-threshold, threshold]`` will be 

952 set to zero. 

953 

954 - When ``two_sided`` is False: 

955 

956 - If the threshold is negative: 

957 

958 It should be greater than the minimum intensity of the input data. 

959 All intensities greater than or equal to the specified threshold will 

960 be set to zero. 

961 All other instensities keep their original values. 

962 

963 - If the threshold is positive: 

964 

965 then it should be less than the maximum intensity of the input data. 

966 All intensities less than or equal to the specified threshold will be 

967 set to zero. 

968 All other instensities keep their original values. 

969 

970 - If threshold is :obj:`str`: 

971 

972 The number part should be in interval ``[0, 100]``. 

973 We threshold the image based on the score obtained using this percentile 

974 on the image data. 

975 The percentile rank is computed using 

976 :func:`scipy.stats.scoreatpercentile`. 

977 

978 - When ``two_sided`` is True: 

979 

980 The score is calculated on the absolute values of data. 

981 

982 - When ``two_sided`` is False: 

983 

984 The score is calculated only on the non-negative values of data. 

985 

986 .. versionadded:: 0.2 

987 

988 .. versionchanged:: 0.9.0 

989 New ``cluster_threshold`` and ``two_sided`` parameters added. 

990 

991 .. versionchanged:: 0.11.2dev 

992 Add support for SurfaceImage. 

993 

994 Parameters 

995 ---------- 

996 img : a 3D/4D Niimg-like object or a :obj:`~nilearn.surface.SurfaceImage` 

997 Image containing statistical or atlas maps which should be thresholded. 

998 

999 threshold : :obj:`float` or :obj:`str` 

1000 Threshold that is used to set certain voxel intensities to zero. 

1001 If threshold is float, it should be within the range of minimum and the 

1002 maximum intensity of the data. 

1003 If `two_sided` is True, threshold cannot be negative. 

1004 If threshold is :obj:`str`, 

1005 the given string should be within the range of "0%" to "100%". 

1006 

1007 cluster_threshold : :obj:`float`, default=0 

1008 Cluster size threshold, in voxels. In the returned thresholded map, 

1009 sets of connected voxels (``clusters``) with size smaller than this 

1010 number will be removed. 

1011 

1012 Not implemented for SurfaceImage. 

1013 

1014 .. versionadded:: 0.9.0 

1015 

1016 two_sided : :obj:`bool`, default=True 

1017 Whether the thresholding should yield both positive and negative 

1018 part of the maps. 

1019 

1020 .. versionadded:: 0.9.0 

1021 

1022 mask_img : Niimg-like object or a :obj:`~nilearn.surface.SurfaceImage` \ 

1023 or None, default=None 

1024 Mask image applied to mask the input data. 

1025 If None, no masking will be applied. 

1026 

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

1028 If True, input array is not modified. True by default: the filtering 

1029 is not performed in-place. 

1030 

1031 copy_header : :obj:`bool`, default=False 

1032 Whether to copy the header of the input image to the output. 

1033 

1034 Not applicable for SurfaceImage. 

1035 

1036 .. versionadded:: 0.11.0 

1037 

1038 This parameter will be set to True by default in 0.13.0. 

1039 

1040 Returns 

1041 ------- 

1042 :obj:`~nibabel.nifti1.Nifti1Image` \ 

1043 or a :obj:`~nilearn.surface.SurfaceImage` 

1044 Thresholded image of the given input image. 

1045 

1046 Raises 

1047 ------ 

1048 ValueError 

1049 If threshold is of type str but is not a non-negative number followed 

1050 by the percent sign. 

1051 If threshold is a negative float and `two_sided` is True. 

1052 TypeError 

1053 If threshold is neither float nor a string in correct percentile 

1054 format. 

1055 

1056 See Also 

1057 -------- 

1058 nilearn.glm.threshold_stats_img : 

1059 Threshold a statistical image using the alpha value, optionally with 

1060 false positive control. 

1061 

1062 """ 

1063 from nilearn.image.resampling import resample_img 

1064 from nilearn.masking import load_mask_img 

1065 

1066 if not isinstance(img, (*NiimgLike, SurfaceImage)): 

1067 raise TypeError( 

1068 "'img' should be a 3D/4D Niimg-like object or a SurfaceImage. " 

1069 f"Got {type(img)=}." 

1070 ) 

1071 

1072 if mask_img is not None: 

1073 check_compatibility_mask_and_images(mask_img, img) 

1074 

1075 if isinstance(img, SurfaceImage) and isinstance(mask_img, SurfaceImage): 

1076 check_polymesh_equal(mask_img.mesh, img.mesh) 

1077 

1078 if isinstance(img, SurfaceImage) and cluster_threshold > 0: 

1079 warnings.warn( 

1080 "Cluster thresholding not implemented for SurfaceImage. " 

1081 "Setting 'cluster_threshold' to 0.", 

1082 stacklevel=find_stack_level(), 

1083 ) 

1084 cluster_threshold = 0 

1085 

1086 if isinstance(img, NiimgLike): 

1087 # TODO: remove this warning in 0.13.0 

1088 check_copy_header(copy_header) 

1089 

1090 img = check_niimg(img) 

1091 img_data = safe_get_data(img, ensure_finite=True, copy_data=copy) 

1092 affine = img.affine 

1093 else: 

1094 if copy: 

1095 img = deepcopy(img) 

1096 img_data = get_surface_data(img, ensure_finite=True) 

1097 

1098 img_data_for_cutoff = img_data 

1099 

1100 if mask_img is not None: 

1101 # Set as 0 for the values which are outside of the mask 

1102 if isinstance(mask_img, NiimgLike): 

1103 mask_img = check_niimg_3d(mask_img) 

1104 if not check_same_fov(img, mask_img): 

1105 # TODO switch to force_resample=True 

1106 # when bumping to version > 0.13 

1107 mask_img = resample_img( 

1108 mask_img, 

1109 target_affine=affine, 

1110 target_shape=img.shape[:3], 

1111 interpolation="nearest", 

1112 copy_header=True, 

1113 force_resample=False, 

1114 ) 

1115 mask_data, _ = load_mask_img(mask_img) 

1116 

1117 # Take only points that are within the mask to check for threshold 

1118 img_data_for_cutoff = img_data_for_cutoff[mask_data != 0.0] 

1119 

1120 img_data[mask_data == 0.0] = 0.0 

1121 

1122 else: 

1123 mask_img, _ = load_mask_img(mask_img) 

1124 

1125 mask_data = get_surface_data(mask_img) 

1126 

1127 # Take only points that are within the mask to check for threshold 

1128 img_data_for_cutoff = img_data_for_cutoff[mask_data != 0.0] 

1129 

1130 for hemi in mask_img.data.parts: 

1131 mask = mask_img.data.parts[hemi] 

1132 img.data.parts[hemi][mask == 0.0] = 0.0 

1133 

1134 cutoff_threshold = check_threshold( 

1135 threshold, 

1136 img_data_for_cutoff, 

1137 percentile_func=scoreatpercentile, 

1138 name="threshold", 

1139 two_sided=two_sided, 

1140 ) 

1141 

1142 # Apply threshold 

1143 if isinstance(img, NiimgLike): 

1144 img_data = _apply_threshold(img_data, two_sided, cutoff_threshold) 

1145 else: 

1146 img_data = _apply_threshold(img, two_sided, cutoff_threshold) 

1147 

1148 # Perform cluster thresholding, if requested 

1149 

1150 # Expand to 4D to support both 3D and 4D nifti 

1151 expand = isinstance(img, NiimgLike) and img_data.ndim == 3 

1152 if expand: 

1153 img_data = img_data[:, :, :, None] 

1154 if cluster_threshold > 0: 

1155 for i_vol in range(img_data.shape[3]): 

1156 img_data[..., i_vol] = _apply_cluster_size_threshold( 

1157 img_data[..., i_vol], 

1158 cluster_threshold, 

1159 ) 

1160 if expand: 

1161 # Reduce back to 3D 

1162 img_data = img_data[:, :, :, 0] 

1163 

1164 # Reconstitute img object 

1165 if isinstance(img, NiimgLike): 

1166 return new_img_like(img, img_data, affine, copy_header=copy_header) 

1167 

1168 return new_img_like(img, img_data.data) 

1169 

1170 

1171def _apply_threshold(img_data, two_sided, cutoff_threshold): 

1172 """Apply a given threshold to an 'image'. 

1173 

1174 If the image is a Surface applies to each part. 

1175 

1176 Parameters 

1177 ---------- 

1178 img_data: np.ndarray or SurfaceImage 

1179 

1180 two_sided : :obj:`bool`, default=True 

1181 Whether the thresholding should yield both positive and negative 

1182 part of the maps. 

1183 

1184 cutoff_threshold: :obj:`int` 

1185 Effective threshold returned by check_threshold. 

1186 

1187 Returns 

1188 ------- 

1189 np.ndarray or SurfaceImage 

1190 """ 

1191 if isinstance(img_data, SurfaceImage): 

1192 for hemi, value in img_data.data.parts.items(): 

1193 img_data.data.parts[hemi] = _apply_threshold( 

1194 value, two_sided, cutoff_threshold 

1195 ) 

1196 return img_data 

1197 

1198 if two_sided: 

1199 mask = (-cutoff_threshold <= img_data) & (img_data <= cutoff_threshold) 

1200 elif cutoff_threshold >= 0: 

1201 mask = img_data <= cutoff_threshold 

1202 else: 

1203 mask = img_data >= cutoff_threshold 

1204 

1205 img_data[mask] = 0.0 

1206 

1207 return img_data 

1208 

1209 

1210def math_img(formula, copy_header_from=None, **imgs): 

1211 """Interpret a numpy based string formula using niimg in named parameters. 

1212 

1213 .. versionadded:: 0.2.3 

1214 

1215 Parameters 

1216 ---------- 

1217 formula : :obj:`str` 

1218 The mathematical formula to apply to image internal data. It can use 

1219 numpy imported as 'np'. 

1220 

1221 copy_header_from : :obj:`str`, default=None 

1222 Takes the variable name of one of the images in the formula. 

1223 The header of this image will be copied to the result of the formula. 

1224 Note that the result image and the image to copy the header from, 

1225 should have the same number of dimensions. If None, the default 

1226 :class:`~nibabel.nifti1.Nifti1Header` is used. 

1227 

1228 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1229 

1230 .. versionadded:: 0.10.4 

1231 

1232 imgs : images (:class:`~nibabel.nifti1.Nifti1Image` or file names \ 

1233 or :obj:`~nilearn.surface.SurfaceImage` object) 

1234 Keyword arguments corresponding to the variables in the formula as 

1235 images. 

1236 All input images should have the same 'geometry': 

1237 

1238 - shape and affine for volume data 

1239 - mesh (coordinates and faces) for surface data 

1240 

1241 Returns 

1242 ------- 

1243 :class:`~nibabel.nifti1.Nifti1Image` \ 

1244 or :obj:`~nilearn.surface.SurfaceImage` object 

1245 Result of the formula as an image. 

1246 Note that the dimension of the result image 

1247 can be smaller than the input image. 

1248 For volume image input, the affine is the same as the input images. 

1249 For surface image input, the mesh is the same as the input images. 

1250 

1251 See Also 

1252 -------- 

1253 nilearn.image.mean_img : To simply compute the mean of multiple images 

1254 

1255 Examples 

1256 -------- 

1257 Let's load an image using nilearn datasets module:: 

1258 

1259 >>> from nilearn import datasets 

1260 >>> anatomical_image = datasets.load_mni152_template() 

1261 

1262 Now we can use any numpy function on this image:: 

1263 

1264 >>> from nilearn.image import math_img 

1265 >>> log_img = math_img("np.log(img)", img=anatomical_image) 

1266 

1267 We can also apply mathematical operations on several images:: 

1268 

1269 >>> result_img = math_img("img1 + img2", 

1270 ... img1=anatomical_image, img2=log_img) 

1271 

1272 The result image will have the same shape and affine as the input images; 

1273 but might have different header information, specifically the TR value, 

1274 see :gh:`2645`. 

1275 

1276 .. versionadded:: 0.10.4 

1277 

1278 We can also copy the header from one of the input images using 

1279 ``copy_header_from``:: 

1280 

1281 >>> result_img_with_header = math_img("img1 + img2", 

1282 ... img1=anatomical_image, img2=log_img, 

1283 ... copy_header_from="img1") 

1284 

1285 Notes 

1286 ----- 

1287 This function is the Python equivalent of ImCal in SPM or fslmaths 

1288 in FSL. 

1289 

1290 """ 

1291 is_surface = all(isinstance(x, SurfaceImage) for x in imgs.values()) 

1292 

1293 if is_surface: 

1294 first_img = next(iter(imgs.values())) 

1295 for image in imgs.values(): 

1296 assert_polymesh_equal(first_img.mesh, image.mesh) 

1297 

1298 # Computing input data as a dictionary of numpy arrays. 

1299 data_dict = {k: {} for k in first_img.data.parts} 

1300 for key, img in imgs.items(): 

1301 for k, v in img.data.parts.items(): 

1302 data_dict[k][key] = v 

1303 

1304 # Add a reference to numpy in the kwargs of eval 

1305 # so that numpy functions can be called from there. 

1306 result = {} 

1307 try: 

1308 for k in data_dict: 

1309 data_dict[k]["np"] = np 

1310 result[k] = eval(formula, data_dict[k]) 

1311 except Exception as exc: 

1312 exc.args = ( 

1313 "Input formula couldn't be processed, " 

1314 f"you provided '{formula}',", 

1315 *exc.args, 

1316 ) 

1317 raise 

1318 

1319 return new_img_like(first_img, result) 

1320 

1321 try: 

1322 niimgs = [check_niimg(image) for image in imgs.values()] 

1323 check_same_fov(*niimgs, raise_error=True) 

1324 except Exception as exc: 

1325 exc.args = ( 

1326 "Input images cannot be compared, " 

1327 f"you provided '{imgs.values()}',", 

1328 *exc.args, 

1329 ) 

1330 raise 

1331 

1332 # Computing input data as a dictionary of numpy arrays. Keep a reference 

1333 # niimg for building the result as a new niimg. 

1334 niimg = None 

1335 data_dict = {} 

1336 for key, img in imgs.items(): 

1337 niimg = check_niimg(img) 

1338 data_dict[key] = safe_get_data(niimg) 

1339 

1340 # Add a reference to numpy in the kwargs of eval so that numpy functions 

1341 # can be called from there. 

1342 data_dict["np"] = np 

1343 try: 

1344 result = eval(formula, data_dict) 

1345 except Exception as exc: 

1346 exc.args = ( 

1347 f"Input formula couldn't be processed, you provided '{formula}',", 

1348 *exc.args, 

1349 ) 

1350 raise 

1351 

1352 if copy_header_from is None: 

1353 return new_img_like(niimg, result, niimg.affine) 

1354 niimg = check_niimg(imgs[copy_header_from]) 

1355 # only copy the header if the result and the input image to copy the 

1356 # header from have the same shape 

1357 if result.ndim != niimg.ndim: 

1358 raise ValueError( 

1359 "Cannot copy the header. " 

1360 "The result of the formula has a different number of " 

1361 "dimensions than the image to copy the header from." 

1362 ) 

1363 return new_img_like(niimg, result, niimg.affine, copy_header=True) 

1364 

1365 

1366def binarize_img( 

1367 img, threshold=0.0, mask_img=None, two_sided=True, copy_header=False 

1368): 

1369 """Binarize an image such that its values are either 0 or 1. 

1370 

1371 .. versionadded:: 0.8.1 

1372 

1373 Parameters 

1374 ---------- 

1375 img : a 3D/4D Niimg-like object or :obj:`~nilearn.surface.SurfaceImage` 

1376 Image which should be binarized. 

1377 

1378 threshold : :obj:`float` or :obj:`str`, default=0.0 

1379 If float, we threshold the image based on image intensities meaning 

1380 voxels which have intensities greater than this value will be kept. 

1381 The given value should be within the range of minimum and 

1382 maximum intensity of the input image. 

1383 If string, it should finish with percent sign e.g. "80%" and we 

1384 threshold based on the score obtained using this percentile on 

1385 the image data. The voxels which have intensities greater than 

1386 this score will be kept. The given string should be 

1387 within the range of "0%" to "100%". 

1388 

1389 mask_img : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`, \ 

1390 default=None 

1391 Mask image applied to mask the input data. 

1392 If None, no masking will be applied. 

1393 

1394 two_sided : :obj:`bool`, default=True 

1395 If `True`, threshold is applied to the absolute value of the image. 

1396 If `False`, threshold is applied to the original value of the image. 

1397 

1398 .. versionadded:: 0.10.3 

1399 

1400 copy_header : :obj:`bool`, default=False 

1401 Whether to copy the header of the input image to the output. 

1402 

1403 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1404 

1405 .. versionadded:: 0.11.0 

1406 

1407 This parameter will be set to True by default in 0.13.0. 

1408 

1409 Returns 

1410 ------- 

1411 :class:`~nibabel.nifti1.Nifti1Image` 

1412 or :obj:`~nilearn.surface.SurfaceImage` 

1413 Binarized version of the given input image. Output dtype is int8. 

1414 

1415 See Also 

1416 -------- 

1417 nilearn.image.threshold_img : To simply threshold but not binarize images. 

1418 

1419 Examples 

1420 -------- 

1421 Let's load an image using nilearn datasets module:: 

1422 

1423 >>> from nilearn import datasets 

1424 >>> anatomical_image = datasets.load_mni152_template() 

1425 

1426 Now we binarize it, generating a pseudo brainmask:: 

1427 

1428 >>> from nilearn.image import binarize_img 

1429 >>> img = binarize_img(anatomical_image, copy_header=True) 

1430 

1431 """ 

1432 warnings.warn( 

1433 'The current default behavior for the "two_sided" argument ' 

1434 'is "True". This behavior will be changed to "False" in ' 

1435 "version 0.13.", 

1436 DeprecationWarning, 

1437 stacklevel=find_stack_level(), 

1438 ) 

1439 

1440 return math_img( 

1441 "img.astype(bool).astype('int8')", 

1442 img=threshold_img( 

1443 img, 

1444 threshold, 

1445 mask_img=mask_img, 

1446 two_sided=two_sided, 

1447 copy_header=copy_header, 

1448 ), 

1449 copy_header_from="img", 

1450 ) 

1451 

1452 

1453@fill_doc 

1454def clean_img( 

1455 imgs, 

1456 runs=None, 

1457 detrend=True, 

1458 standardize=True, 

1459 confounds=None, 

1460 low_pass=None, 

1461 high_pass=None, 

1462 t_r=None, 

1463 ensure_finite=False, 

1464 mask_img=None, 

1465 **kwargs, 

1466): 

1467 """Improve :term:`SNR` on masked :term:`fMRI` signals. 

1468 

1469 This function can do several things on the input signals, in 

1470 the following order: 

1471 

1472 - detrend 

1473 - low- and high-pass filter 

1474 - remove confounds 

1475 - standardize 

1476 

1477 Low-pass filtering improves specificity. 

1478 

1479 High-pass filtering should be kept small, to keep some sensitivity. 

1480 

1481 Filtering is only meaningful on evenly-sampled signals. 

1482 

1483 According to Lindquist et al. (2018), removal of confounds will be done 

1484 orthogonally to temporal filters (low- and/or high-pass filters), if both 

1485 are specified. 

1486 

1487 .. versionadded:: 0.2.5 

1488 

1489 Parameters 

1490 ---------- 

1491 imgs : 4D image Niimg-like object or \ 

1492 2D :obj:`~nilearn.surface.SurfaceImage` 

1493 The signals in the last dimension are filtered (see 

1494 :ref:`extracting_data` for a detailed description of the valid input 

1495 types). 

1496 

1497 runs : :class:`numpy.ndarray`, default=``None`` 

1498 Add a run level to the cleaning process. Each run will be 

1499 cleaned independently. Must be a 1D array of n_samples elements. 

1500 

1501 .. warning:: 

1502 

1503 'runs' replaces 'sessions' after release 0.10.0. 

1504 Using 'session' will result in an error after release 0.10.0. 

1505 

1506 

1507 detrend : :obj:`bool`, default=True 

1508 If detrending should be applied on timeseries 

1509 (before confound removal). 

1510 

1511 standardize : :obj:`bool`, default=True 

1512 If True, returned signals are set to unit variance. 

1513 

1514 confounds : :class:`numpy.ndarray`, :obj:`str` or :obj:`list` of \ 

1515 Confounds timeseries. default=None 

1516 Shape must be (instant number, confound number), 

1517 or just (instant number,) 

1518 The number of time instants in signals and confounds must be 

1519 identical (i.e. signals.shape[0] == confounds.shape[0]). 

1520 If a string is provided, it is assumed to be the name of a csv file 

1521 containing signals as columns, with an optional one-line header. 

1522 If a list is provided, all confounds are removed from the input 

1523 signal, as if all were in the same array. 

1524 

1525 %(low_pass)s 

1526 

1527 %(high_pass)s 

1528 

1529 t_r : :obj:`float`, default=None 

1530 Repetition time, in second (sampling period). Set to None if not 

1531 specified. Mandatory if used together with `low_pass` or `high_pass`. 

1532 

1533 ensure_finite : :obj:`bool`, default=False 

1534 If True, the non-finite values (NaNs and infs) found in the images 

1535 will be replaced by zeros. 

1536 

1537 mask_img : Niimg-like object or :obj:`~nilearn.surface.SurfaceImage`,\ 

1538 default=None 

1539 If provided, signal is only cleaned from voxels inside the mask. 

1540 If mask is provided, it should have same shape and affine as imgs. 

1541 If not provided, all voxels / verrices are used. 

1542 See :ref:`extracting_data`. 

1543 

1544 kwargs : :obj:`dict` 

1545 Keyword arguments to be passed to functions called 

1546 within this function. 

1547 Kwargs prefixed with ``'clean__'`` will be passed to 

1548 :func:`~nilearn.signal.clean`. 

1549 Within :func:`~nilearn.signal.clean`, kwargs prefixed with 

1550 ``'butterworth__'`` will be passed to the Butterworth filter 

1551 (i.e., ``clean__butterworth__``). 

1552 

1553 Returns 

1554 ------- 

1555 Niimg-like object or :obj:`~nilearn.surface.SurfaceImage` 

1556 Input images, cleaned. Same shape as `imgs`. 

1557 

1558 Notes 

1559 ----- 

1560 Confounds removal is based on a projection on the orthogonal 

1561 of the signal space from :footcite:t:`Friston1994`. 

1562 

1563 Orthogonalization between temporal filters and confound removal is based on 

1564 suggestions in :footcite:t:`Lindquist2018`. 

1565 

1566 References 

1567 ---------- 

1568 .. footbibliography:: 

1569 

1570 See Also 

1571 -------- 

1572 nilearn.signal.clean 

1573 

1574 """ 

1575 # Avoid circular import 

1576 from .. import masking 

1577 

1578 # Check if t_r is set, otherwise propose t_r from imgs header 

1579 if (low_pass is not None or high_pass is not None) and t_r is None: 

1580 # We raise an error, instead of using the header's t_r as this 

1581 # value is considered to be non-reliable 

1582 raise ValueError( 

1583 "Repetition time (t_r) must be specified for filtering. " 

1584 "You specified None. " 

1585 f"imgs header suggest it to be {imgs.header.get_zooms()[3]}" 

1586 ) 

1587 

1588 clean_kwargs = { 

1589 k[7:]: v for k, v in kwargs.items() if k.startswith("clean__") 

1590 } 

1591 

1592 if isinstance(imgs, SurfaceImage): 

1593 imgs.data._check_ndims(2, "imgs") 

1594 

1595 data = {} 

1596 # Clean signal 

1597 for p, v in imgs.data.parts.items(): 

1598 data[p] = signal.clean( 

1599 v.T, 

1600 runs=runs, 

1601 detrend=detrend, 

1602 standardize=standardize, 

1603 confounds=confounds, 

1604 low_pass=low_pass, 

1605 high_pass=high_pass, 

1606 t_r=t_r, 

1607 ensure_finite=ensure_finite, 

1608 **clean_kwargs, 

1609 ) 

1610 data[p] = data[p].T 

1611 

1612 if mask_img is not None: 

1613 mask_img = masking.load_mask_img(mask_img)[0] 

1614 for hemi in mask_img.data.parts: 

1615 mask = mask_img.data.parts[hemi] 

1616 data[hemi][mask == 0.0, ...] = 0.0 

1617 

1618 return new_img_like(imgs, data) 

1619 

1620 imgs_ = check_niimg_4d(imgs) 

1621 

1622 # Prepare signal for cleaning 

1623 if mask_img is not None: 

1624 signals = masking.apply_mask(imgs_, mask_img) 

1625 else: 

1626 signals = get_data(imgs_).reshape(-1, imgs_.shape[-1]).T 

1627 

1628 # Clean signal 

1629 data = signal.clean( 

1630 signals, 

1631 runs=runs, 

1632 detrend=detrend, 

1633 standardize=standardize, 

1634 confounds=confounds, 

1635 low_pass=low_pass, 

1636 high_pass=high_pass, 

1637 t_r=t_r, 

1638 ensure_finite=ensure_finite, 

1639 **clean_kwargs, 

1640 ) 

1641 

1642 # Put results back into Niimg-like object 

1643 if mask_img is not None: 

1644 imgs_ = masking.unmask(data, mask_img) 

1645 elif "sample_mask" in clean_kwargs: 

1646 sample_shape = imgs_.shape[:3] + clean_kwargs["sample_mask"].shape 

1647 imgs_ = new_img_like( 

1648 imgs_, data.T.reshape(sample_shape), copy_header=True 

1649 ) 

1650 else: 

1651 imgs_ = new_img_like( 

1652 imgs_, data.T.reshape(imgs_.shape), copy_header=True 

1653 ) 

1654 

1655 return imgs_ 

1656 

1657 

1658@fill_doc 

1659def load_img(img, wildcards=True, dtype=None): 

1660 """Load a Niimg-like object from filenames or list of filenames. 

1661 

1662 .. versionadded:: 0.2.5 

1663 

1664 Parameters 

1665 ---------- 

1666 img : Niimg-like object 

1667 If string, consider it as a path to NIfTI image and 

1668 call `nibabel.load()`on it. 

1669 The '~' symbol is expanded to the user home folder. 

1670 If it is an object, check if affine attribute is present, raise 

1671 `TypeError` otherwise. 

1672 See :ref:`extracting_data`. 

1673 

1674 wildcards : :obj:`bool`, default=True 

1675 Use `img` as a regular expression to get a list of matching input 

1676 filenames. 

1677 If multiple files match, the returned list is sorted using an ascending 

1678 order. 

1679 If no file matches the regular expression, a `ValueError` exception is 

1680 raised. 

1681 

1682 %(dtype)s 

1683 

1684 Returns 

1685 ------- 

1686 3D/4D Niimg-like object 

1687 Result can be :class:`~nibabel.nifti1.Nifti1Image` or the input, as-is. 

1688 It is guaranteed that 

1689 the returned object has an affine attributes and that 

1690 nilearn.image.get_data returns its data. 

1691 

1692 """ 

1693 return check_niimg(img, wildcards=wildcards, dtype=dtype) 

1694 

1695 

1696@fill_doc 

1697def concat_imgs( 

1698 niimgs, 

1699 dtype=np.float32, 

1700 ensure_ndim=None, 

1701 memory=None, 

1702 memory_level=0, 

1703 auto_resample=False, 

1704 verbose=0, 

1705): 

1706 """Concatenate a list of images of varying lengths. 

1707 

1708 The image list can contain: 

1709 

1710 - Niimg-like objects of varying dimensions (i.e., 3D or 4D) 

1711 as well as different 3D shapes and affines, 

1712 as they will be matched to the first image in the list 

1713 if ``auto_resample=True``. 

1714 

1715 - surface images of varying dimensions (i.e., 1D or 2D) 

1716 but with same number of vertices 

1717 

1718 Parameters 

1719 ---------- 

1720 niimgs : iterable of Niimg-like objects, or glob pattern, \ 

1721 or :obj:`list` or :obj:`tuple` \ 

1722 of :obj:`~nilearn.surface.SurfaceImage` object 

1723 See :ref:`extracting_data`. 

1724 Images to concatenate. 

1725 

1726 dtype : numpy dtype, default=np.float32 

1727 The dtype of the returned image. 

1728 

1729 ensure_ndim : :obj:`int`, default=None 

1730 Indicate the dimensionality of the expected niimg. An 

1731 error is raised if the niimg is of another dimensionality. 

1732 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1733 

1734 auto_resample : :obj:`bool`, default=False 

1735 Converts all images to the space of the first one. 

1736 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1737 

1738 %(verbose0)s 

1739 

1740 %(memory)s 

1741 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1742 

1743 %(memory_level)s 

1744 Ignored for :obj:`~nilearn.surface.SurfaceImage`. 

1745 

1746 Returns 

1747 ------- 

1748 concatenated : :obj:`~nibabel.nifti1.Nifti1Image` \ 

1749 or :obj:`~nilearn.surface.SurfaceImage` 

1750 A single image. 

1751 

1752 See Also 

1753 -------- 

1754 nilearn.image.index_img 

1755 

1756 """ 

1757 check_params(locals()) 

1758 

1759 if ( 

1760 isinstance(niimgs, (tuple, list)) 

1761 and len(niimgs) > 0 

1762 and all(isinstance(x, SurfaceImage) for x in niimgs) 

1763 ): 

1764 if len(niimgs) == 1: 

1765 return niimgs[0] 

1766 

1767 for i, img in enumerate(niimgs): 

1768 check_polymesh_equal(img.mesh, niimgs[0].mesh) 

1769 niimgs[i] = at_least_2d(img) 

1770 

1771 if dtype is None: 

1772 dtype = extract_data(niimgs[0]).dtype 

1773 

1774 output_data = {} 

1775 for part in niimgs[0].data.parts: 

1776 tmp = [img.data.parts[part] for img in niimgs] 

1777 output_data[part] = np.concatenate(tmp, axis=1).astype(dtype) 

1778 

1779 return new_img_like(niimgs[0], data=output_data) 

1780 

1781 if memory is None: 

1782 memory = Memory(location=None) 

1783 

1784 target_fov = "first" if auto_resample else None 

1785 

1786 # We remove one to the dimensionality because of the list is one dimension. 

1787 ndim = None 

1788 if ensure_ndim is not None: 

1789 ndim = ensure_ndim - 1 

1790 

1791 # If niimgs is a string, use glob to expand it to the matching filenames. 

1792 niimgs = resolve_globbing(niimgs) 

1793 

1794 # First niimg is extracted to get information and for new_img_like 

1795 first_niimg = None 

1796 

1797 iterator, literator = itertools.tee(iter(niimgs)) 

1798 try: 

1799 first_niimg = check_niimg(next(literator), ensure_ndim=ndim) 

1800 except StopIteration: 

1801 raise TypeError("Cannot concatenate empty objects") 

1802 except DimensionError as exc: 

1803 # Keep track of the additional dimension in the error 

1804 exc.increment_stack_counter() 

1805 raise 

1806 

1807 # If no particular dimensionality is asked, we force consistency wrt the 

1808 # first image 

1809 if ndim is None: 

1810 ndim = len(first_niimg.shape) 

1811 

1812 if ndim not in [3, 4]: 

1813 raise TypeError( 

1814 "Concatenated images must be 3D or 4D. You gave a " 

1815 f"list of {ndim}D images" 

1816 ) 

1817 

1818 lengths = [first_niimg.shape[-1] if ndim == 4 else 1] 

1819 for niimg in literator: 

1820 # We check the dimensionality of the niimg 

1821 try: 

1822 niimg = check_niimg(niimg, ensure_ndim=ndim) 

1823 except DimensionError as exc: 

1824 # Keep track of the additional dimension in the error 

1825 exc.increment_stack_counter() 

1826 raise 

1827 lengths.append(niimg.shape[-1] if ndim == 4 else 1) 

1828 

1829 target_shape = first_niimg.shape[:3] 

1830 if dtype is None: 

1831 dtype = _get_data(first_niimg).dtype 

1832 data = np.ndarray((*target_shape, sum(lengths)), order="F", dtype=dtype) 

1833 cur_4d_index = 0 

1834 for index, (size, niimg) in enumerate( 

1835 zip( 

1836 lengths, 

1837 iter_check_niimg( 

1838 iterator, 

1839 atleast_4d=True, 

1840 target_fov=target_fov, 

1841 memory=memory, 

1842 memory_level=memory_level, 

1843 ), 

1844 ) 

1845 ): 

1846 nii_str = ( 

1847 f"image {niimg}" if isinstance(niimg, str) else f"image #{index}" 

1848 ) 

1849 logger.log(f"Concatenating {index + 1}: {nii_str}", verbose) 

1850 

1851 data[..., cur_4d_index : cur_4d_index + size] = _get_data(niimg) 

1852 cur_4d_index += size 

1853 

1854 return new_img_like( 

1855 first_niimg, data, first_niimg.affine, copy_header=True 

1856 ) 

1857 

1858 

1859def largest_connected_component_img(imgs): 

1860 """Return the largest connected component of an image or list of images. 

1861 

1862 .. versionadded:: 0.3.1 

1863 

1864 Parameters 

1865 ---------- 

1866 imgs : Niimg-like object or iterable of Niimg-like objects (3D) 

1867 Image(s) to extract the largest connected component from. 

1868 See :ref:`extracting_data`. 

1869 

1870 Returns 

1871 ------- 

1872 3D Niimg-like object or list of 

1873 Image or list of images containing the largest connected component. 

1874 

1875 Notes 

1876 ----- 

1877 **Handling big-endian in given Nifti image** 

1878 This function changes the existing byte-ordering information to new byte 

1879 order, if the dtype in given Nifti image has non-native data type. 

1880 This operation is done internally to avoid big-endian issues with 

1881 scipy ndimage module. 

1882 

1883 """ 

1884 from .._utils.ndimage import largest_connected_component 

1885 

1886 imgs = stringify_path(imgs) 

1887 if hasattr(imgs, "__iter__") and not isinstance(imgs, str): 

1888 single_img = False 

1889 else: 

1890 single_img = True 

1891 imgs = [imgs] 

1892 

1893 ret = [] 

1894 for img in imgs: 

1895 img = check_niimg_3d(img) 

1896 affine = img.affine 

1897 largest_component = largest_connected_component(safe_get_data(img)) 

1898 ret.append( 

1899 new_img_like(img, largest_component, affine, copy_header=True) 

1900 ) 

1901 

1902 return ret[0] if single_img else ret 

1903 

1904 

1905def copy_img(img): 

1906 """Copy an image to a nibabel.Nifti1Image. 

1907 

1908 Parameters 

1909 ---------- 

1910 img : image 

1911 nibabel SpatialImage object to copy. 

1912 

1913 Returns 

1914 ------- 

1915 img_copy : image 

1916 copy of input (data, affine and header) 

1917 """ 

1918 if not isinstance(img, spatialimages.SpatialImage): 

1919 raise ValueError("Input value is not an image") 

1920 return new_img_like( 

1921 img, 

1922 safe_get_data(img, copy_data=True), 

1923 img.affine.copy(), 

1924 copy_header=True, 

1925 )