|
1 | 1 | """Read/write AFNI's transforms."""
|
2 | 2 | from math import pi
|
3 | 3 | import numpy as np
|
4 |
| -from nibabel.affines import obliquity, voxel_sizes |
| 4 | +from nibabel.affines import obliquity, voxel_sizes, from_matvec |
5 | 5 |
|
6 | 6 | from ..patched import shape_zoom_affine
|
7 | 7 | from .base import (
|
@@ -44,22 +44,14 @@ def from_ras(cls, ras, moving=None, reference=None):
|
44 | 44 |
|
45 | 45 | if reference is not None and _is_oblique(reference.affine):
|
46 | 46 | print("Reference affine axes are oblique.")
|
47 |
| - M = reference.affine |
48 |
| - A = shape_zoom_affine( |
49 |
| - reference.shape, voxel_sizes(M), x_flip=False, y_flip=False |
50 |
| - ) |
51 |
| - pre = M.dot(np.linalg.inv(A)).dot(LPS) |
| 47 | + ras = ras @ _afni_warpdrive(reference.affine, ras=True, forward=False) |
52 | 48 |
|
53 | 49 | if moving is not None:
|
54 | 50 | moving = _ensure_image(moving)
|
55 | 51 |
|
56 | 52 | if moving is not None and _is_oblique(moving.affine):
|
57 | 53 | print("Moving affine axes are oblique.")
|
58 |
| - M2 = moving.affine |
59 |
| - A2 = shape_zoom_affine( |
60 |
| - moving.shape, voxel_sizes(M2), x_flip=True, y_flip=True |
61 |
| - ) |
62 |
| - post = A2.dot(np.linalg.inv(M2)) |
| 54 | + ras = _afni_warpdrive(reference.affine, ras=True) @ ras |
63 | 55 |
|
64 | 56 | # swapaxes is necessary, as axis 0 encodes series of transforms
|
65 | 57 | parameters = np.swapaxes(post @ ras @ pre, 0, 1)
|
@@ -185,3 +177,38 @@ def from_image(cls, imgobj):
|
185 | 177 |
|
186 | 178 | def _is_oblique(affine, thres=OBLIQUITY_THRESHOLD_DEG):
|
187 | 179 | return (obliquity(affine).min() * 180 / pi) > thres
|
| 180 | + |
| 181 | + |
| 182 | +def _afni_warpdrive(oblique, forward=True, ras=False): |
| 183 | + """ |
| 184 | + Calculate AFNI's ``WARPDRIVE_MATVEC_FOR_000000`` (de)obliquing affine. |
| 185 | +
|
| 186 | + Parameters |
| 187 | + ---------- |
| 188 | + oblique : 4x4 numpy.array |
| 189 | + affine that is not aligned to the cardinal axes. |
| 190 | + plumb : 4x4 numpy.array |
| 191 | + corresponding affine that is aligned to the cardinal axes. |
| 192 | + forward : :obj:`bool` |
| 193 | + Transforms the affine of oblique into an AFNI's plumb (if ``True``) |
| 194 | + or viceversa plumb -> oblique (if ``false``). |
| 195 | +
|
| 196 | + Returns |
| 197 | + ------- |
| 198 | + plumb_to_oblique : 4x4 numpy.array |
| 199 | + the matrix that pre-pended to the plumb affine rotates it |
| 200 | + to be oblique. |
| 201 | +
|
| 202 | + """ |
| 203 | + plumb = oblique[:3, :3] / np.abs(oblique[:3, :3]).max(0) |
| 204 | + plumb[np.abs(plumb) < 1.0] = 0 |
| 205 | + plumb *= voxel_sizes(oblique) |
| 206 | + |
| 207 | + R = from_matvec(plumb @ np.linalg.inv(oblique[:3, :3]), (0, 0, 0)) |
| 208 | + R[:3, 3] = oblique[:3, 3] - (R[:3, :3] @ oblique[:3, 3]) |
| 209 | + if not ras: |
| 210 | + # Change sign to match AFNI's warpdrive_matvec signs |
| 211 | + B = np.ones((2, 2)) |
| 212 | + R *= np.block([[B, -1.0 * B], [-1.0 * B, B]]) |
| 213 | + |
| 214 | + return R if forward else np.linalg.inv(R) |
0 commit comments