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

238 statements  

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

1"""Utilities to resample a Niimg-like object. 

2 

3See http://nilearn.github.io/stable/manipulating_images/input_output.html 

4""" 

5 

6import numbers 

7import warnings 

8 

9import numpy as np 

10from scipy import linalg 

11from scipy.ndimage import affine_transform, find_objects 

12 

13from nilearn import _utils 

14from nilearn._utils import fill_doc, stringify_path 

15from nilearn._utils.helpers import check_copy_header 

16from nilearn._utils.logger import find_stack_level 

17from nilearn._utils.niimg import _get_data 

18from nilearn.image.image import copy_img, crop_img 

19 

20############################################################################### 

21# Affine utils 

22 

23 

24def to_matrix_vector(transform): 

25 """Split an homogeneous transform into its matrix and vector components. 

26 

27 The transformation must be represented in homogeneous coordinates. 

28 It is split into its linear transformation matrix and translation vector 

29 components. 

30 

31 This function does not normalize the matrix. This means that for it to be 

32 the inverse of from_matrix_vector, transform[-1, -1] must equal 1, and 

33 transform[-1, :-1] must equal 0. 

34 

35 Parameters 

36 ---------- 

37 transform : numpy.ndarray 

38 Homogeneous transform matrix. Example: a (4, 4) transform representing 

39 linear transformation and translation in 3 dimensions. 

40 

41 Returns 

42 ------- 

43 matrix, vector : numpy.ndarray 

44 The matrix and vector components of the transform matrix. For 

45 an (N, N) transform, matrix will be (N-1, N-1) and vector will be 

46 a 1D array of shape (N-1,). 

47 

48 See Also 

49 -------- 

50 from_matrix_vector 

51 

52 """ 

53 ndimin = transform.shape[0] - 1 

54 ndimout = transform.shape[1] - 1 

55 matrix = transform[0:ndimin, 0:ndimout] 

56 vector = transform[0:ndimin, ndimout] 

57 return matrix, vector 

58 

59 

60def from_matrix_vector(matrix, vector): 

61 """Combine a matrix and vector into a homogeneous transform. 

62 

63 Combine a rotation matrix and translation vector into a transform 

64 in homogeneous coordinates. 

65 

66 Parameters 

67 ---------- 

68 matrix : numpy.ndarray 

69 An (N, N) array representing the rotation matrix. 

70 

71 vector : numpy.ndarray 

72 A (1, N) array representing the translation. 

73 

74 Returns 

75 ------- 

76 xform : numpy.ndarray 

77 An (N+1, N+1) transform matrix. 

78 

79 See Also 

80 -------- 

81 nilearn.resampling.to_matrix_vector 

82 

83 """ 

84 nin, nout = matrix.shape 

85 t = np.zeros((nin + 1, nout + 1), matrix.dtype) 

86 t[0:nin, 0:nout] = matrix 

87 t[nin, nout] = 1.0 

88 t[0:nin, nout] = vector 

89 return t 

90 

91 

92def coord_transform(x, y, z, affine): 

93 """Convert the x, y, z coordinates from one image space to another space. 

94 

95 Parameters 

96 ---------- 

97 x : number or ndarray (any shape) 

98 The x coordinates in the input space. 

99 

100 y : number or ndarray (same shape as x) 

101 The y coordinates in the input space. 

102 

103 z : number or ndarray 

104 The z coordinates in the input space. 

105 

106 affine : 2D 4x4 ndarray 

107 Affine that maps from input to output space. 

108 

109 Returns 

110 ------- 

111 x : number or ndarray (same shape as input) 

112 The x coordinates in the output space. 

113 

114 y : number or ndarray (same shape as input) 

115 The y coordinates in the output space. 

116 

117 z : number or ndarray (same shape as input) 

118 The z coordinates in the output space. 

119 

120 .. warning:: 

121 

122 The x, y and z have their output space (e.g. MNI) coordinate ordering, 

123 not 3D numpy image ordering. 

124 

125 Examples 

126 -------- 

127 Transform data from coordinates to brain space. The "affine" matrix 

128 can be found as the ".affine" attribute of a nifti image, or using 

129 the "get_affine()" method for older nibabel installations:: 

130 

131 >>> from nilearn import datasets, image 

132 >>> niimg = datasets.load_mni152_template() 

133 >>> # Find the MNI coordinates of the voxel (50, 50, 50) 

134 >>> image.coord_transform(50, 50, 50, niimg.affine) 

135 (-48.0, -84.0, -22.0) 

136 

137 """ 

138 squeeze = not hasattr(x, "__iter__") 

139 return_number = isinstance(x, numbers.Number) 

140 x = np.asanyarray(x) 

141 shape = x.shape 

142 coords = np.c_[ 

143 np.atleast_1d(x).flat, 

144 np.atleast_1d(y).flat, 

145 np.atleast_1d(z).flat, 

146 np.ones_like(np.atleast_1d(z).flat), 

147 ].T 

148 x, y, z, _ = np.dot(affine, coords) 

149 if return_number: 

150 return x.item(), y.item(), z.item() 

151 if squeeze: 

152 return x.squeeze(), y.squeeze(), z.squeeze() 

153 return np.reshape(x, shape), np.reshape(y, shape), np.reshape(z, shape) 

154 

155 

156def get_bounds(shape, affine): 

157 """Return the world-space bounds occupied by an array given an affine. 

158 

159 The coordinates returned correspond to the **center** of the corner voxels. 

160 

161 Parameters 

162 ---------- 

163 shape : :obj:`tuple` 

164 shape of the array. Must have 3 integer values. 

165 

166 affine : numpy.ndarray 

167 affine giving the linear transformation 

168 between :term:`voxel` coordinates 

169 and world-space coordinates. 

170 

171 Returns 

172 ------- 

173 coord : list of tuples 

174 coord[i] is a 2-tuple giving minimal and maximal coordinates along 

175 i-th axis. 

176 

177 """ 

178 adim, bdim, cdim = shape 

179 adim -= 1 

180 bdim -= 1 

181 cdim -= 1 

182 # form a collection of vectors for each 8 corners of the box 

183 box = np.array( 

184 [ 

185 [0.0, 0, 0, 1], 

186 [adim, 0, 0, 1], 

187 [0, bdim, 0, 1], 

188 [0, 0, cdim, 1], 

189 [adim, bdim, 0, 1], 

190 [adim, 0, cdim, 1], 

191 [0, bdim, cdim, 1], 

192 [adim, bdim, cdim, 1], 

193 ] 

194 ).T 

195 box = np.dot(affine, box)[:3] 

196 return list(zip(box.min(axis=-1), box.max(axis=-1))) 

197 

198 

199def get_mask_bounds(img): 

200 """Return the world-space bounds occupied by a mask. 

201 

202 Parameters 

203 ---------- 

204 img : Niimg-like object 

205 See :ref:`extracting_data`. 

206 The image to inspect. Zero values are considered as 

207 background. 

208 

209 Returns 

210 ------- 

211 xmin, xmax, ymin, ymax, zmin, zmax : floats 

212 The world-space bounds (field of view) occupied by the 

213 non-zero values in the image 

214 

215 Notes 

216 ----- 

217 The image should have only one connect component. 

218 

219 The affine should be diagonal or diagonal-permuted, use 

220 reorder_img to ensure that it is the case. 

221 

222 """ 

223 img = _utils.check_niimg_3d(img) 

224 mask = _utils.numpy_conversions.as_ndarray( 

225 _get_data(img), dtype=bool, copy=False 

226 ) 

227 affine = img.affine 

228 (xmin, xmax), (ymin, ymax), (zmin, zmax) = get_bounds(mask.shape, affine) 

229 slices = find_objects(mask.astype(int)) 

230 if len(slices) == 0: 

231 warnings.warn("empty mask", stacklevel=find_stack_level()) 

232 else: 

233 x_slice, y_slice, z_slice = slices[0] 

234 x_width, y_width, z_width = mask.shape 

235 xmin, xmax = ( 

236 xmin + x_slice.start * (xmax - xmin) / x_width, 

237 xmin + x_slice.stop * (xmax - xmin) / x_width, 

238 ) 

239 ymin, ymax = ( 

240 ymin + y_slice.start * (ymax - ymin) / y_width, 

241 ymin + y_slice.stop * (ymax - ymin) / y_width, 

242 ) 

243 zmin, zmax = ( 

244 zmin + z_slice.start * (zmax - zmin) / z_width, 

245 zmin + z_slice.stop * (zmax - zmin) / z_width, 

246 ) 

247 

248 return xmin, xmax, ymin, ymax, zmin, zmax 

249 

250 

251class BoundingBoxError(ValueError): 

252 """Raise error when resampling transformation is incompatible with data. 

253 

254 This can happen, for example, if the field of view of a target affine 

255 matrix does not contain any of the original data. 

256 """ 

257 

258 pass 

259 

260 

261############################################################################### 

262# Resampling 

263 

264 

265def _resample_one_img( 

266 data, A, b, target_shape, interpolation_order, out, copy=True, fill_value=0 

267): 

268 """Do not use: internal function for resample_img.""" 

269 if data.dtype.kind in ("i", "u"): 

270 # Integers are always finite 

271 has_not_finite = False 

272 else: 

273 not_finite = np.logical_not(np.isfinite(data)) 

274 has_not_finite = np.any(not_finite) 

275 if has_not_finite: 

276 warnings.warn( 

277 "NaNs or infinite values are present in the data " 

278 "passed to resample. This is a bad thing as they " 

279 "make resampling ill-defined and much slower.", 

280 RuntimeWarning, 

281 stacklevel=find_stack_level(), 

282 ) 

283 if copy: 

284 # We need to do a copy to avoid modifying the input 

285 # array 

286 data = data.copy() 

287 # data[not_finite] = 0 

288 from ..masking import extrapolate_out_mask 

289 

290 data = extrapolate_out_mask( 

291 data, np.logical_not(not_finite), iterations=2 

292 )[0] 

293 

294 # If data is binary and interpolation is continuous or linear, 

295 # warn the user as this might be unintentional 

296 if interpolation_order != 0 and np.array_equal(np.unique(data), [0, 1]): 

297 warnings.warn( 

298 "Resampling binary images with continuous or " 

299 "linear interpolation. This might lead to " 

300 "unexpected results. You might consider using " 

301 "nearest interpolation instead.", 

302 stacklevel=find_stack_level(), 

303 ) 

304 

305 # Suppresses warnings in https://github.com/nilearn/nilearn/issues/1363 

306 with warnings.catch_warnings(): 

307 warnings.filterwarnings( 

308 "ignore", message=".*has changed in SciPy 0.18.*" 

309 ) 

310 # The resampling itself 

311 affine_transform( 

312 data, 

313 A, 

314 offset=b, 

315 output_shape=target_shape, 

316 output=out, 

317 cval=fill_value, 

318 order=interpolation_order, 

319 ) 

320 

321 if has_not_finite: 

322 # Suppresses warnings in https://github.com/nilearn/nilearn/issues/1363 

323 with warnings.catch_warnings(): 

324 warnings.filterwarnings( 

325 "ignore", message=".*has changed in SciPy 0.18.*" 

326 ) 

327 # We need to resample the mask of not_finite values 

328 not_finite = affine_transform( 

329 not_finite, 

330 A, 

331 offset=b, 

332 output_shape=target_shape, 

333 order=0, 

334 ) 

335 out[not_finite] = np.nan 

336 return out 

337 

338 

339def _check_force_resample(force_resample): 

340 if force_resample is None: 

341 force_resample = False 

342 warnings.warn( 

343 ( 

344 "'force_resample' will be set to 'True'" 

345 " by default in Nilearn 0.13.0.\n" 

346 "Use 'force_resample=True' to suppress this warning." 

347 ), 

348 FutureWarning, 

349 stacklevel=find_stack_level(), 

350 ) 

351 return force_resample 

352 

353 

354@fill_doc 

355def resample_img( 

356 img, 

357 target_affine=None, 

358 target_shape=None, 

359 interpolation="continuous", 

360 copy=True, 

361 order="F", 

362 clip=True, 

363 fill_value=0, 

364 force_resample=None, 

365 copy_header=False, 

366): 

367 """Resample a Niimg-like object. 

368 

369 Parameters 

370 ---------- 

371 img : Niimg-like object 

372 See :ref:`extracting_data`. 

373 Image(s) to resample. 

374 

375 %(target_affine)s 

376 See notes. 

377 

378 %(target_shape)s 

379 See notes. 

380 

381 interpolation : :obj:`str`, default='continuous' 

382 Can be 'continuous', 'linear', or 'nearest'. Indicates the resample 

383 method. 

384 

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

386 If True, guarantees that output array has no memory in common with 

387 input array. 

388 In all cases, input images are never modified by this function. 

389 

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

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

392 Fortran ordering. 

393 

394 clip : :obj:`bool`, default=True 

395 If True, all resampled image values above max(img) and 

396 under min(img) are clipped to min(img) and max(img). Note that 

397 0 is added as an image value for clipping, and it is the padding 

398 value when extrapolating out of field of view. 

399 If False no clip is performed. 

400 

401 fill_value : :obj:`float`, default=0 

402 Use a fill value for points outside of input volume. 

403 

404 force_resample : :obj:`bool`, default=None 

405 False is intended for testing, 

406 this prevents the use of a padding optimization. 

407 Will be set to ``False`` if ``None`` is passed. 

408 The default value will be set to ``True`` for Nilearn >=0.13.0. 

409 

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

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

412 

413 .. versionadded:: 0.11.0 

414 

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

416 

417 Returns 

418 ------- 

419 resampled : nibabel.Nifti1Image 

420 input image, resampled to have respectively target_shape and 

421 target_affine as shape and affine. 

422 

423 See Also 

424 -------- 

425 nilearn.image.resample_to_img 

426 

427 Notes 

428 ----- 

429 **BoundingBoxError** 

430 If a 4x4 transformation matrix (target_affine) is given and all of the 

431 transformed data points have a negative voxel index along one of the 

432 axis, then none of the data will be visible in the transformed image 

433 and a BoundingBoxError will be raised. 

434 

435 If a 4x4 transformation matrix (target_affine) is given and no target 

436 shape is provided, the resulting image will have voxel coordinate 

437 (0, 0, 0) in the affine offset (4th column of target affine) and will 

438 extend far enough to contain all the visible data and a margin of one 

439 voxel. 

440 

441 **3x3 transformation matrices** 

442 If a 3x3 transformation matrix is given as target_affine, it will be 

443 assumed to represent the three coordinate axes of the target space. In 

444 this case the affine offset (4th column of a 4x4 transformation matrix) 

445 as well as the target_shape will be inferred by resample_img, such that 

446 the resulting field of view is the closest possible (with a margin of 

447 1 voxel) bounding box around the transformed data. 

448 

449 In certain cases one may want to obtain a transformed image with the 

450 closest bounding box around the data, which at the same time respects 

451 a voxel grid defined by a 4x4 affine transformation matrix. In this 

452 case, one resamples the image using this function given the target 

453 affine and no target shape. One then uses crop_img on the result. 

454 

455 **NaNs and infinite values** 

456 This function handles gracefully NaNs and infinite values in the input 

457 data, however they make the execution of the function much slower. 

458 

459 **Handling non-native endian in given Nifti images** 

460 This function automatically changes the byte-ordering information 

461 in the image dtype to new byte order. From non-native to native, which 

462 implies that if the given image has non-native endianness then the output 

463 data in Nifti image will have native dtype. This is only the case when 

464 if the given target_affine (transformation matrix) is diagonal and 

465 homogeneous. 

466 

467 """ 

468 from .image import new_img_like # avoid circular imports 

469 

470 force_resample = _check_force_resample(force_resample) 

471 # TODO: remove this warning in 0.13.0 

472 check_copy_header(copy_header) 

473 

474 _check_resample_img_inputs(target_shape, target_affine, interpolation) 

475 

476 img = stringify_path(img) 

477 img = _utils.check_niimg(img) 

478 

479 # If later on we want to impute sform using qform add this condition 

480 # see : https://github.com/nilearn/nilearn/issues/3168#issuecomment-1159447771 # noqa: E501 

481 if hasattr(img, "get_sform"): # NIfTI images only 

482 _, sform_code = img.get_sform(coded=True) 

483 if not sform_code: 

484 warnings.warn( 

485 "The provided image has no sform in its header. " 

486 "Please check the provided file. " 

487 "Results may not be as expected.", 

488 stacklevel=find_stack_level(), 

489 ) 

490 

491 # noop cases 

492 input_img_is_string = isinstance(img, str) 

493 if _resampling_not_needed(img, target_affine, target_shape): 

494 if copy and not input_img_is_string: 

495 img = copy_img(img) 

496 return img 

497 

498 if target_affine is not None: 

499 target_affine = np.asarray(target_affine) 

500 

501 # We now know that some resampling must be done. 

502 # The value of "copy" is of no importance: 

503 # output is always a separate array. 

504 data = _get_data(img) 

505 

506 # Get a bounding box for the transformed data 

507 # Embed target_affine in 4x4 shape if necessary 

508 if target_affine.shape == (3, 3): 

509 missing_offset = True 

510 target_affine_tmp = np.eye(4) 

511 target_affine_tmp[:3, :3] = target_affine 

512 target_affine = target_affine_tmp 

513 else: 

514 missing_offset = False 

515 target_affine = target_affine.copy() 

516 

517 affine = img.affine 

518 transform_affine = np.linalg.inv(target_affine).dot(affine) 

519 (xmin, xmax), (ymin, ymax), (zmin, zmax) = get_bounds( 

520 data.shape[:3], transform_affine 

521 ) 

522 

523 # if target_affine is (3, 3), then calculate 

524 # offset from bounding box and update bounding box 

525 # to be in the voxel coordinates of the calculated 4x4 affine 

526 if missing_offset: 

527 offset = target_affine[:3, :3].dot([xmin, ymin, zmin]) 

528 target_affine[:3, 3] = offset 

529 (xmin, xmax), (ymin, ymax), (zmin, zmax) = ( 

530 (0, xmax - xmin), 

531 (0, ymax - ymin), 

532 (0, zmax - zmin), 

533 ) 

534 

535 # if target_shape is not given (always the case with 3x3 

536 # transformation matrix and sometimes the case with 4x4 

537 # transformation matrix), then set it to contain the bounding 

538 # box by a margin of 1 voxel 

539 if target_shape is None: 

540 target_shape = ( 

541 int(np.ceil(xmax)) + 1, 

542 int(np.ceil(ymax)) + 1, 

543 int(np.ceil(zmax)) + 1, 

544 ) 

545 

546 # Check whether transformed data is actually within the FOV 

547 # of the target affine 

548 if xmax < 0 or ymax < 0 or zmax < 0: 

549 raise BoundingBoxError( 

550 "The field of view given " 

551 "by the target affine does " 

552 "not contain any of the data" 

553 ) 

554 

555 if np.all(target_affine == affine): 

556 # Small trick to be more numerically stable 

557 transform_affine = np.eye(4) 

558 else: 

559 transform_affine = np.dot(linalg.inv(affine), target_affine) 

560 A, b = to_matrix_vector(transform_affine) 

561 

562 data_shape = list(data.shape) 

563 # Make sure that we have a list here 

564 if isinstance(target_shape, np.ndarray): 

565 target_shape = target_shape.tolist() 

566 target_shape = tuple(target_shape) 

567 

568 resampled_data_dtype = _get_resampled_data_dtype(data, interpolation, A) 

569 

570 # Code is generic enough to work for both 3D and 4D images 

571 other_shape = data_shape[3:] 

572 resampled_data = np.zeros( 

573 list(target_shape) + other_shape, 

574 order=order, 

575 dtype=resampled_data_dtype, 

576 ) 

577 

578 # if (A == I OR some combination of permutation(I) and sign-flipped(I)) AND 

579 # all(b == integers): 

580 if ( 

581 np.all(np.eye(3) == A) 

582 and all(bt == np.round(bt) for bt in b) 

583 and not force_resample 

584 ): 

585 # TODO: also check for sign flips 

586 # TODO: also check for permutations of I 

587 

588 # ... special case: can be solved with padding alone 

589 # crop source image and keep N voxels offset before/after volume 

590 cropped_img, offsets = crop_img( 

591 img, pad=False, return_offset=True, copy_header=True 

592 ) 

593 

594 # TODO: flip axes that are flipped 

595 # TODO: un-shuffle permuted dimensions 

596 

597 # offset the original un-cropped image indices by the relative 

598 # translation, b. 

599 indices = [ 

600 (int(off.start - dim_b), int(off.stop - dim_b)) 

601 for off, dim_b in zip(offsets[:3], b[:3]) 

602 ] 

603 

604 # If image are not fully overlapping, place only portion of image. 

605 slices = [ 

606 slice(np.max((0, index[0])), np.min((dimsize, index[1]))) 

607 for dimsize, index in zip(resampled_data.shape, indices) 

608 ] 

609 slices = tuple(slices) 

610 

611 # ensure the source image being placed isn't larger than the dest 

612 subset_indices = tuple(slice(0, s.stop - s.start) for s in slices) 

613 resampled_data[slices] = _get_data(cropped_img)[subset_indices] 

614 else: 

615 if interpolation == "continuous": 

616 interpolation_order = 3 

617 elif interpolation == "linear": 

618 interpolation_order = 1 

619 elif interpolation == "nearest": 

620 interpolation_order = 0 

621 

622 # If A is diagonal, ndimage.affine_transform is clever enough to use a 

623 # better algorithm. 

624 if np.all(np.diag(np.diag(A)) == A): 

625 A = np.diag(A) 

626 all_img = (slice(None),) * 3 

627 

628 # Iterate over a set of 3D volumes, as the interpolation problem is 

629 # separable in the extra dimensions. This reduces the 

630 # computational cost 

631 for ind in np.ndindex(*other_shape): 

632 _resample_one_img( 

633 data[all_img + ind], 

634 A, 

635 b, 

636 target_shape, 

637 interpolation_order, 

638 out=resampled_data[all_img + ind], 

639 copy=not input_img_is_string, 

640 fill_value=fill_value, 

641 ) 

642 

643 if clip: 

644 # force resampled data to have a range contained in the original data 

645 # preventing ringing artifact 

646 # We need to add zero as a value considered for clipping, as it 

647 # appears in padding images. 

648 vmin = min(np.nanmin(data), 0) 

649 vmax = max(np.nanmax(data), 0) 

650 resampled_data.clip(vmin, vmax, out=resampled_data) 

651 

652 return new_img_like( 

653 img, resampled_data, target_affine, copy_header=copy_header 

654 ) 

655 

656 

657def _resampling_not_needed(img, target_affine, target_shape): 

658 """Check if resampling needed based on input image and requested FOV.""" 

659 shape = img.shape 

660 affine = img.affine 

661 

662 if (target_affine is None and target_shape is None) or ( 

663 np.shape(target_affine) == np.shape(affine) 

664 and np.allclose(target_affine, affine) 

665 and np.array_equal(target_shape, shape) 

666 ): 

667 return True 

668 

669 if target_affine is not None: 

670 target_affine = np.asarray(target_affine) 

671 

672 return np.all(np.array(target_shape) == shape[:3]) and np.allclose( 

673 target_affine, affine 

674 ) 

675 

676 

677def _check_resample_img_inputs(target_shape, target_affine, interpolation): 

678 # Do as many checks as possible before loading data, to avoid potentially 

679 # costly calls before raising an exception. 

680 if target_shape is not None and target_affine is None: 

681 raise ValueError( 

682 "If target_shape is specified, target_affine should" 

683 " be specified too." 

684 ) 

685 

686 if target_shape is not None and len(target_shape) != 3: 

687 raise ValueError( 

688 "The shape specified should be the shape of " 

689 "the 3D grid, and thus of length 3. " 

690 f"{target_shape} was specified." 

691 ) 

692 

693 if target_shape is not None and target_affine.shape == (3, 3): 

694 raise ValueError( 

695 "Given target shape without anchor vector: " 

696 "Affine shape should be (4, 4) and not (3, 3)" 

697 ) 

698 

699 allowed_interpolations = ("continuous", "linear", "nearest") 

700 if interpolation not in allowed_interpolations: 

701 raise ValueError( 

702 f"interpolation must be one of {allowed_interpolations}.\n" 

703 f" Got '{interpolation}' instead." 

704 ) 

705 

706 

707def _get_resampled_data_dtype(data, interpolation, A): 

708 """Get the datat type of the resampled data. 

709 

710 Make sure to cast unsupported data types to the closest support ones. 

711 """ 

712 resampled_data_dtype = data.dtype 

713 if interpolation == "continuous" and data.dtype.kind == "i": 

714 # cast unsupported data types to closest support dtype 

715 aux = data.dtype.name.replace("int", "float") 

716 aux = aux.replace("ufloat", "float").replace("floatc", "float") 

717 if aux in ["float8", "float16"]: 

718 aux = "float32" 

719 warnings.warn( 

720 f"Casting data from {data.dtype.name} to {aux}", 

721 stacklevel=find_stack_level(), 

722 ) 

723 resampled_data_dtype = np.dtype(aux) 

724 

725 # Since the release of 0.17, resampling nifti images have some issues 

726 # when affine is passed as 1D array 

727 # and if data is of non-native endianness. 

728 # See issue https://github.com/nilearn/nilearn/issues/1445. 

729 # If affine is passed as 1D, scipy uses _nd_image.zoom_shift rather 

730 # than _geometric_transform (2D) where _geometric_transform is able 

731 # to swap byte order in scipy later than 0.15 for nonnative endianness. 

732 

733 # We convert to 'native' order to not have any issues either with 

734 # 'little' or 'big' endian data dtypes (non-native endians). 

735 if ( 

736 len(A.shape) == 1 and not resampled_data_dtype.isnative 

737 ): # pragma: no cover 

738 resampled_data_dtype = resampled_data_dtype.newbyteorder("N") 

739 

740 return resampled_data_dtype 

741 

742 

743def resample_to_img( 

744 source_img, 

745 target_img, 

746 interpolation="continuous", 

747 copy=True, 

748 order="F", 

749 clip=False, 

750 fill_value=0, 

751 force_resample=None, 

752 copy_header=False, 

753): 

754 """Resample a Niimg-like source image on a target Niimg-like image. 

755 

756 No registration is performed: the image should already be aligned. 

757 

758 .. versionadded:: 0.2.4 

759 

760 Parameters 

761 ---------- 

762 source_img : Niimg-like object 

763 See :ref:`extracting_data`. 

764 Image(s) to resample. 

765 

766 target_img : Niimg-like object 

767 See :ref:`extracting_data`. 

768 Reference image taken for resampling. 

769 

770 interpolation : :obj:`str`, default='continuous' 

771 Can be 'continuous', 'linear', or 'nearest'. Indicates the resample 

772 method. 

773 

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

775 If True, guarantees that output array has no memory in common with 

776 input array. 

777 In all cases, input images are never modified by this function. 

778 

779 order : "F" or "C", default="F" 

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

781 Fortran ordering. 

782 

783 clip : :obj:`bool`, default=False 

784 If False, no clip is performed. 

785 If True, all resampled image values above max(img) 

786 and under min(img) are cllipped to min(img) and max(img). 

787 

788 fill_value : :obj:`float`, default=0 

789 Use a fill value for points outside of input volume. 

790 

791 force_resample : :obj:`bool`, default=None 

792 False is intended for testing, 

793 this prevents the use of a padding optimization. 

794 Will be set to ``False`` if ``None`` is passed. 

795 The default value will be set to ``True`` for Nilearn >=0.13.0. 

796 

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

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

799 

800 .. versionadded:: 0.11.0 

801 

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

803 

804 Returns 

805 ------- 

806 resampled : nibabel.Nifti1Image 

807 input image, resampled to have respectively target image shape and 

808 affine as shape and affine. 

809 

810 See Also 

811 -------- 

812 nilearn.image.resample_img 

813 

814 """ 

815 force_resample = _check_force_resample(force_resample) 

816 

817 target = _utils.check_niimg(target_img) 

818 target_shape = target.shape 

819 

820 # When target shape is greater than 3, we reduce to 3, to be compatible 

821 # with underlying call to resample_img 

822 if len(target_shape) > 3: 

823 target_shape = target.shape[:3] 

824 

825 return resample_img( 

826 source_img, 

827 target_affine=target.affine, 

828 target_shape=target_shape, 

829 interpolation=interpolation, 

830 copy=copy, 

831 order=order, 

832 clip=clip, 

833 fill_value=fill_value, 

834 force_resample=force_resample, 

835 copy_header=copy_header, 

836 ) 

837 

838 

839def reorder_img(img, resample=None, copy_header=False): 

840 """Return an image with the affine diagonal (by permuting axes). 

841 

842 The orientation of the new image will be RAS (Right, Anterior, Superior). 

843 If it is impossible to get xyz ordering by permuting the axes, a 

844 'ValueError' is raised. 

845 

846 Parameters 

847 ---------- 

848 img : Niimg-like object 

849 See :ref:`extracting_data`. 

850 Image to reorder. 

851 

852 resample : None or :obj:`str` in {'continuous', 'linear', 'nearest'}, \ 

853 default=None 

854 If resample is None, no resampling is performed, 

855 the axes are only permuted. 

856 Otherwise resampling is performed and 'resample' will 

857 be passed as the 'interpolation' argument into 

858 resample_img. 

859 

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

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

862 

863 .. versionadded:: 0.11.0 

864 

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

866 """ 

867 from .image import new_img_like 

868 

869 check_copy_header(copy_header) 

870 img = _utils.check_niimg(img) 

871 # The copy is needed in order not to modify the input img affine 

872 # see https://github.com/nilearn/nilearn/issues/325 for a concrete bug 

873 affine = img.affine.copy() 

874 A, b = to_matrix_vector(affine) 

875 

876 if not np.all((np.abs(A) > 0.001).sum(axis=0) == 1): 

877 if resample is None: 

878 raise ValueError( 

879 "Cannot reorder the axes: the image affine contains rotations" 

880 ) 

881 

882 # Identify the voxel size using a QR decomposition of the affine 

883 Q, R = np.linalg.qr(affine[:3, :3]) 

884 target_affine = np.diag(np.abs(np.diag(R))[np.abs(Q).argmax(axis=1)]) 

885 # TODO switch to force_resample=True 

886 # when bumping to version > 0.13 

887 return resample_img( 

888 img, 

889 target_affine=target_affine, 

890 interpolation=resample, 

891 force_resample=False, 

892 copy_header=True, 

893 ) 

894 

895 axis_numbers = np.argmax(np.abs(A), axis=0) 

896 data = _get_data(img) 

897 while not np.all(np.sort(axis_numbers) == axis_numbers): 

898 first_inversion = np.argmax(np.diff(axis_numbers) < 0) 

899 axis1 = first_inversion + 1 

900 axis2 = first_inversion 

901 data = np.swapaxes(data, axis1, axis2) 

902 order = np.array((0, 1, 2, 3)) 

903 order[axis1] = axis2 

904 order[axis2] = axis1 

905 affine = affine.T[order].T 

906 A, b = to_matrix_vector(affine) 

907 axis_numbers = np.argmax(np.abs(A), axis=0) 

908 

909 # Now make sure the affine is positive 

910 pixdim = np.diag(A).copy() 

911 if pixdim[0] < 0: 

912 b[0] = b[0] + pixdim[0] * (data.shape[0] - 1) 

913 pixdim[0] = -pixdim[0] 

914 slice1 = slice(None, None, -1) 

915 else: 

916 slice1 = slice(None, None, None) 

917 if pixdim[1] < 0: 

918 b[1] = b[1] + pixdim[1] * (data.shape[1] - 1) 

919 pixdim[1] = -pixdim[1] 

920 slice2 = slice(None, None, -1) 

921 else: 

922 slice2 = slice(None, None, None) 

923 if pixdim[2] < 0: 

924 b[2] = b[2] + pixdim[2] * (data.shape[2] - 1) 

925 pixdim[2] = -pixdim[2] 

926 slice3 = slice(None, None, -1) 

927 else: 

928 slice3 = slice(None, None, None) 

929 data = data[slice1, slice2, slice3] 

930 affine = from_matrix_vector(np.diag(pixdim), b) 

931 

932 return new_img_like(img, data, affine, copy_header=copy_header)