Source code for nitransforms.io.fsl
"""Read/write FSL's transforms."""
import os
import warnings
import numpy as np
from numpy.linalg import inv
from pathlib import Path
from nibabel.affines import voxel_sizes
from .base import (
BaseLinearTransformList,
LinearParameters,
DisplacementsField,
TransformIOError,
TransformFileError,
_ensure_image,
)
[docs]
class FSLLinearTransform(LinearParameters):
"""A string-based structure for FSL linear transforms."""
def __str__(self):
"""Generate a string representation."""
lines = [
" ".join("%.08f" % col for col in row)
for row in self.structarr["parameters"]
]
return "\n".join(lines + [""])
[docs]
def to_string(self):
"""Convert to a string directly writeable to file."""
return self.__str__()
[docs]
@classmethod
def from_ras(cls, ras, moving=None, reference=None):
"""Create an FSL affine from a nitransform's RAS+ matrix."""
if moving is None:
warnings.warn(
"[Converting FSL to RAS] moving not provided, using reference as moving"
)
moving = reference
if reference is None:
raise TransformIOError("Cannot build FSL linear transform without a reference")
reference = _ensure_image(reference)
moving = _ensure_image(moving)
# Adjust for reference image offset and orientation
refswp, refspc = _fsl_aff_adapt(reference)
pre = reference.affine @ inv(refswp @ refspc)
# Adjust for moving image offset and orientation
movswp, movspc = _fsl_aff_adapt(moving)
post = movswp @ movspc @ inv(moving.affine)
# Compose FSL transform
mat = inv(np.swapaxes(post @ ras @ pre, 0, 1))
tf = cls()
tf.structarr["parameters"] = mat.T
return tf
[docs]
@classmethod
def from_string(cls, string):
"""Read the struct from string."""
tf = cls()
sa = tf.structarr
lines = [line.strip() for line in string.splitlines() if line.strip()]
if not lines or len(lines) < 4:
raise TransformFileError
sa["parameters"] = np.genfromtxt(
["\n".join(lines)], dtype=cls.dtype["parameters"]
)
return tf
[docs]
def to_ras(self, moving=None, reference=None):
"""Return a nitransforms internal RAS+ matrix."""
if reference is None:
raise TransformIOError("Cannot build FSL linear transform without a reference")
if moving is None:
warnings.warn(
"Converting FSL to RAS: moving image not provided, using reference."
)
moving = reference
reference = _ensure_image(reference)
moving = _ensure_image(moving)
refswp, refspc = _fsl_aff_adapt(reference)
pre = refswp @ refspc @ inv(reference.affine)
# Adjust for moving image offset and orientation
movswp, movspc = _fsl_aff_adapt(moving)
post = moving.affine @ inv(movswp @ movspc)
mat = self.structarr["parameters"].T
return post @ np.swapaxes(inv(mat), 0, 1) @ pre
[docs]
class FSLLinearTransformArray(BaseLinearTransformList):
"""A string-based structure for series of FSL linear transforms."""
_inner_type = FSLLinearTransform
[docs]
def to_filename(self, filename):
"""Store this transform to a file with the appropriate format."""
output_dir = Path(filename).parent
output_dir.mkdir(exist_ok=True, parents=True)
for i, xfm in enumerate(self.xforms):
(output_dir / f"{filename}.{i:03d}").write_text(str(xfm))
[docs]
def to_ras(self, moving=None, reference=None):
"""Return a nitransforms' internal RAS matrix."""
return np.stack(
[xfm.to_ras(moving=moving, reference=reference) for xfm in self.xforms]
)
[docs]
def to_string(self):
"""Convert to a string directly writeable to file."""
return "\n\n".join([xfm.to_string() for xfm in self.xforms])
[docs]
@classmethod
def from_fileobj(cls, fileobj, check=True):
"""Read the struct from a file object."""
return cls.from_string(fileobj.read())
[docs]
@classmethod
def from_ras(cls, ras, moving=None, reference=None):
"""Create an ITK affine from a nitransform's RAS+ matrix."""
_self = cls()
_self.xforms = [
cls._inner_type.from_ras(ras[i, ...], moving=moving, reference=reference)
for i in range(ras.shape[0])
]
return _self
[docs]
@classmethod
def from_string(cls, string):
"""Read the struct from string."""
_self = cls()
_self.xforms = [cls._inner_type.from_string(string)]
return _self
[docs]
@classmethod
def from_filename(cls, filename):
"""
Read the struct from a file given its path.
If the file does not exist, then indexed names
with the zero-padded suffix ``.NNN`` are
attempted, following FSL's MCFLIRT conventions.
"""
if os.path.exists(str(filename)):
return super().from_filename(filename)
_xforms = []
index = 0
while os.path.exists("%s.%03d" % (filename, index)):
with open("%s.%03d" % (filename, index)) as f:
string = f.read()
_xforms.append(cls._inner_type.from_string(string))
index += 1
if index == 0:
raise FileNotFoundError(str(filename))
_self = cls()
_self.xforms = _xforms
return _self
[docs]
class FSLDisplacementsField(DisplacementsField):
"""A data structure representing displacements fields."""
[docs]
@classmethod
def from_image(cls, imgobj):
"""Import a displacements field from a NIfTI file."""
hdr = imgobj.header.copy()
shape = hdr.get_data_shape()
if len(shape) != 4 or not shape[-1] in (2, 3):
raise TransformFileError(
'Displacements field "%s" does not come from FSL.' %
imgobj.file_map['image'].filename)
field = np.squeeze(np.asanyarray(imgobj.dataobj))
field[..., 0] *= -1.0
return imgobj.__class__(field, imgobj.affine, hdr)
[docs]
@classmethod
def to_image(cls, imgobj):
"""Export a displacements field from a nibabel object."""
hdr = imgobj.header.copy()
warp_data = imgobj.get_fdata()
warp_data[..., 0] *= -1
return imgobj.__class__(warp_data, imgobj.affine, hdr)
def _fsl_aff_adapt(space):
"""
Adapt FSL affines.
Calculates a matrix to convert from the original RAS image
coordinates to FSL's internal coordinate system of transforms
"""
aff = space.affine
zooms = list(voxel_sizes(aff)) + [1]
swp = np.eye(4)
if np.linalg.det(aff) > 0:
swp[0, 0] = -1.0
swp[0, 3] = (space.shape[0] - 1) * zooms[0]
return swp, np.diag(zooms)