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
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-20 10:58 +0200
1"""Utilities to resample a Niimg-like object.
3See http://nilearn.github.io/stable/manipulating_images/input_output.html
4"""
6import numbers
7import warnings
9import numpy as np
10from scipy import linalg
11from scipy.ndimage import affine_transform, find_objects
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
20###############################################################################
21# Affine utils
24def to_matrix_vector(transform):
25 """Split an homogeneous transform into its matrix and vector components.
27 The transformation must be represented in homogeneous coordinates.
28 It is split into its linear transformation matrix and translation vector
29 components.
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.
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.
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,).
48 See Also
49 --------
50 from_matrix_vector
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
60def from_matrix_vector(matrix, vector):
61 """Combine a matrix and vector into a homogeneous transform.
63 Combine a rotation matrix and translation vector into a transform
64 in homogeneous coordinates.
66 Parameters
67 ----------
68 matrix : numpy.ndarray
69 An (N, N) array representing the rotation matrix.
71 vector : numpy.ndarray
72 A (1, N) array representing the translation.
74 Returns
75 -------
76 xform : numpy.ndarray
77 An (N+1, N+1) transform matrix.
79 See Also
80 --------
81 nilearn.resampling.to_matrix_vector
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
92def coord_transform(x, y, z, affine):
93 """Convert the x, y, z coordinates from one image space to another space.
95 Parameters
96 ----------
97 x : number or ndarray (any shape)
98 The x coordinates in the input space.
100 y : number or ndarray (same shape as x)
101 The y coordinates in the input space.
103 z : number or ndarray
104 The z coordinates in the input space.
106 affine : 2D 4x4 ndarray
107 Affine that maps from input to output space.
109 Returns
110 -------
111 x : number or ndarray (same shape as input)
112 The x coordinates in the output space.
114 y : number or ndarray (same shape as input)
115 The y coordinates in the output space.
117 z : number or ndarray (same shape as input)
118 The z coordinates in the output space.
120 .. warning::
122 The x, y and z have their output space (e.g. MNI) coordinate ordering,
123 not 3D numpy image ordering.
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::
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)
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)
156def get_bounds(shape, affine):
157 """Return the world-space bounds occupied by an array given an affine.
159 The coordinates returned correspond to the **center** of the corner voxels.
161 Parameters
162 ----------
163 shape : :obj:`tuple`
164 shape of the array. Must have 3 integer values.
166 affine : numpy.ndarray
167 affine giving the linear transformation
168 between :term:`voxel` coordinates
169 and world-space coordinates.
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.
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)))
199def get_mask_bounds(img):
200 """Return the world-space bounds occupied by a mask.
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.
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
215 Notes
216 -----
217 The image should have only one connect component.
219 The affine should be diagonal or diagonal-permuted, use
220 reorder_img to ensure that it is the case.
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 )
248 return xmin, xmax, ymin, ymax, zmin, zmax
251class BoundingBoxError(ValueError):
252 """Raise error when resampling transformation is incompatible with data.
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 """
258 pass
261###############################################################################
262# Resampling
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
290 data = extrapolate_out_mask(
291 data, np.logical_not(not_finite), iterations=2
292 )[0]
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 )
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 )
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
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
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.
369 Parameters
370 ----------
371 img : Niimg-like object
372 See :ref:`extracting_data`.
373 Image(s) to resample.
375 %(target_affine)s
376 See notes.
378 %(target_shape)s
379 See notes.
381 interpolation : :obj:`str`, default='continuous'
382 Can be 'continuous', 'linear', or 'nearest'. Indicates the resample
383 method.
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.
390 order : "F" or "C", default='F'
391 Data ordering in output array. This function is slightly faster with
392 Fortran ordering.
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.
401 fill_value : :obj:`float`, default=0
402 Use a fill value for points outside of input volume.
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.
410 copy_header : :obj:`bool`, default=False
411 Whether to copy the header of the input image to the output.
413 .. versionadded:: 0.11.0
415 This parameter will be set to True by default in 0.13.0.
417 Returns
418 -------
419 resampled : nibabel.Nifti1Image
420 input image, resampled to have respectively target_shape and
421 target_affine as shape and affine.
423 See Also
424 --------
425 nilearn.image.resample_to_img
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.
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.
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.
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.
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.
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.
467 """
468 from .image import new_img_like # avoid circular imports
470 force_resample = _check_force_resample(force_resample)
471 # TODO: remove this warning in 0.13.0
472 check_copy_header(copy_header)
474 _check_resample_img_inputs(target_shape, target_affine, interpolation)
476 img = stringify_path(img)
477 img = _utils.check_niimg(img)
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 )
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
498 if target_affine is not None:
499 target_affine = np.asarray(target_affine)
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)
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()
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 )
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 )
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 )
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 )
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)
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)
568 resampled_data_dtype = _get_resampled_data_dtype(data, interpolation, A)
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 )
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
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 )
594 # TODO: flip axes that are flipped
595 # TODO: un-shuffle permuted dimensions
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 ]
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)
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
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
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 )
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)
652 return new_img_like(
653 img, resampled_data, target_affine, copy_header=copy_header
654 )
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
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
669 if target_affine is not None:
670 target_affine = np.asarray(target_affine)
672 return np.all(np.array(target_shape) == shape[:3]) and np.allclose(
673 target_affine, affine
674 )
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 )
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 )
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 )
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 )
707def _get_resampled_data_dtype(data, interpolation, A):
708 """Get the datat type of the resampled data.
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)
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.
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")
740 return resampled_data_dtype
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.
756 No registration is performed: the image should already be aligned.
758 .. versionadded:: 0.2.4
760 Parameters
761 ----------
762 source_img : Niimg-like object
763 See :ref:`extracting_data`.
764 Image(s) to resample.
766 target_img : Niimg-like object
767 See :ref:`extracting_data`.
768 Reference image taken for resampling.
770 interpolation : :obj:`str`, default='continuous'
771 Can be 'continuous', 'linear', or 'nearest'. Indicates the resample
772 method.
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.
779 order : "F" or "C", default="F"
780 Data ordering in output array. This function is slightly faster with
781 Fortran ordering.
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).
788 fill_value : :obj:`float`, default=0
789 Use a fill value for points outside of input volume.
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.
797 copy_header : :obj:`bool`, default=False
798 Whether to copy the header of the input image to the output.
800 .. versionadded:: 0.11.0
802 This parameter will be set to True by default in 0.13.0.
804 Returns
805 -------
806 resampled : nibabel.Nifti1Image
807 input image, resampled to have respectively target image shape and
808 affine as shape and affine.
810 See Also
811 --------
812 nilearn.image.resample_img
814 """
815 force_resample = _check_force_resample(force_resample)
817 target = _utils.check_niimg(target_img)
818 target_shape = target.shape
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]
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 )
839def reorder_img(img, resample=None, copy_header=False):
840 """Return an image with the affine diagonal (by permuting axes).
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.
846 Parameters
847 ----------
848 img : Niimg-like object
849 See :ref:`extracting_data`.
850 Image to reorder.
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.
860 copy_header : :obj:`bool`, default=False
861 Whether to copy the header of the input image to the output.
863 .. versionadded:: 0.11.0
865 This parameter will be set to True by default in 0.13.0.
866 """
867 from .image import new_img_like
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)
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 )
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 )
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)
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)
932 return new_img_like(img, data, affine, copy_header=copy_header)