852 lines
26 KiB
Python
852 lines
26 KiB
Python
"""Freesurfer handling functions."""
|
|
|
|
# Authors: The MNE-Python contributors.
|
|
# License: BSD-3-Clause
|
|
# Copyright the MNE-Python contributors.
|
|
|
|
import os.path as op
|
|
from gzip import GzipFile
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
|
|
from ._fiff.constants import FIFF
|
|
from ._fiff.meas_info import read_fiducials
|
|
from .surface import _read_mri_surface, read_surface
|
|
from .transforms import (
|
|
Transform,
|
|
_ensure_trans,
|
|
apply_trans,
|
|
combine_transforms,
|
|
invert_transform,
|
|
read_ras_mni_t,
|
|
)
|
|
from .utils import (
|
|
_check_fname,
|
|
_check_option,
|
|
_import_nibabel,
|
|
_validate_type,
|
|
get_subjects_dir,
|
|
logger,
|
|
verbose,
|
|
)
|
|
|
|
|
|
def _check_subject_dir(subject, subjects_dir):
|
|
"""Check that the Freesurfer subject directory is as expected."""
|
|
subjects_dir = Path(get_subjects_dir(subjects_dir, raise_error=True))
|
|
for img_name in ("T1", "brain", "aseg"):
|
|
if not (subjects_dir / subject / "mri" / f"{img_name}.mgz").is_file():
|
|
raise ValueError(
|
|
"Freesurfer recon-all subject folder "
|
|
"is incorrect or improperly formatted, "
|
|
f"got {subjects_dir / subject}"
|
|
)
|
|
return subjects_dir / subject
|
|
|
|
|
|
def _get_aseg(aseg, subject, subjects_dir):
|
|
"""Check that the anatomical segmentation file exists and load it."""
|
|
nib = _import_nibabel("load aseg")
|
|
subjects_dir = Path(get_subjects_dir(subjects_dir, raise_error=True))
|
|
if aseg == "auto": # use aparc+aseg if auto
|
|
aseg = _check_fname(
|
|
subjects_dir / subject / "mri" / "aparc+aseg.mgz",
|
|
overwrite="read",
|
|
must_exist=False,
|
|
)
|
|
if not aseg: # if doesn't exist use wmparc
|
|
aseg = subjects_dir / subject / "mri" / "wmparc.mgz"
|
|
else:
|
|
aseg = subjects_dir / subject / "mri" / f"{aseg}.mgz"
|
|
_check_fname(aseg, overwrite="read", must_exist=True)
|
|
aseg = nib.load(aseg)
|
|
aseg_data = np.array(aseg.dataobj)
|
|
return aseg, aseg_data
|
|
|
|
|
|
def _reorient_image(img, axcodes="RAS"):
|
|
"""Reorient an image to a given orientation.
|
|
|
|
Parameters
|
|
----------
|
|
img : instance of SpatialImage
|
|
The MRI image.
|
|
axcodes : tuple | str
|
|
The axis codes specifying the orientation, e.g. "RAS".
|
|
See :func:`nibabel.orientations.aff2axcodes`.
|
|
|
|
Returns
|
|
-------
|
|
img_data : ndarray
|
|
The reoriented image data.
|
|
vox_ras_t : ndarray
|
|
The new transform from the new voxels to surface RAS.
|
|
|
|
Notes
|
|
-----
|
|
.. versionadded:: 0.24
|
|
"""
|
|
nib = _import_nibabel("reorient MRI image")
|
|
orig_data = np.array(img.dataobj).astype(np.float32)
|
|
# reorient data to RAS
|
|
ornt = nib.orientations.axcodes2ornt(
|
|
nib.orientations.aff2axcodes(img.affine)
|
|
).astype(int)
|
|
ras_ornt = nib.orientations.axcodes2ornt(axcodes)
|
|
ornt_trans = nib.orientations.ornt_transform(ornt, ras_ornt)
|
|
img_data = nib.orientations.apply_orientation(orig_data, ornt_trans)
|
|
orig_mgh = nib.MGHImage(orig_data, img.affine)
|
|
aff_trans = nib.orientations.inv_ornt_aff(ornt_trans, img.shape)
|
|
vox_ras_t = np.dot(orig_mgh.header.get_vox2ras_tkr(), aff_trans)
|
|
return img_data, vox_ras_t
|
|
|
|
|
|
def _mri_orientation(orientation):
|
|
"""Get MRI orientation information from an image.
|
|
|
|
Parameters
|
|
----------
|
|
orientation : str
|
|
Orientation that you want. Can be "axial", "sagittal", or "coronal".
|
|
|
|
Returns
|
|
-------
|
|
axis : int
|
|
The dimension of the axis to take slices over when plotting.
|
|
x : int
|
|
The dimension of the x axis.
|
|
y : int
|
|
The dimension of the y axis.
|
|
|
|
Notes
|
|
-----
|
|
.. versionadded:: 0.21
|
|
.. versionchanged:: 0.24
|
|
"""
|
|
_check_option("orientation", orientation, ("coronal", "axial", "sagittal"))
|
|
axis = dict(coronal=1, axial=2, sagittal=0)[orientation]
|
|
x, y = sorted(set([0, 1, 2]).difference(set([axis])))
|
|
return axis, x, y
|
|
|
|
|
|
def _get_mri_info_data(mri, data):
|
|
# Read the segmentation data using nibabel
|
|
if data:
|
|
_import_nibabel("load MRI atlas data")
|
|
out = dict()
|
|
_, out["vox_mri_t"], out["mri_ras_t"], dims, _, mgz = _read_mri_info(
|
|
mri, return_img=True
|
|
)
|
|
out.update(
|
|
mri_width=dims[0], mri_height=dims[1], mri_depth=dims[1], mri_volume_name=mri
|
|
)
|
|
if data:
|
|
assert mgz is not None
|
|
out["mri_vox_t"] = invert_transform(out["vox_mri_t"])
|
|
out["data"] = np.asarray(mgz.dataobj)
|
|
return out
|
|
|
|
|
|
def _get_mgz_header(fname):
|
|
"""Adapted from nibabel to quickly extract header info."""
|
|
fname = _check_fname(fname, overwrite="read", must_exist=True, name="MRI image")
|
|
if fname.suffix != ".mgz":
|
|
raise OSError("Filename must end with .mgz")
|
|
header_dtd = [
|
|
("version", ">i4"),
|
|
("dims", ">i4", (4,)),
|
|
("type", ">i4"),
|
|
("dof", ">i4"),
|
|
("goodRASFlag", ">i2"),
|
|
("delta", ">f4", (3,)),
|
|
("Mdc", ">f4", (3, 3)),
|
|
("Pxyz_c", ">f4", (3,)),
|
|
]
|
|
header_dtype = np.dtype(header_dtd)
|
|
with GzipFile(fname, "rb") as fid:
|
|
hdr_str = fid.read(header_dtype.itemsize)
|
|
header = np.ndarray(shape=(), dtype=header_dtype, buffer=hdr_str)
|
|
# dims
|
|
dims = header["dims"].astype(int)
|
|
dims = dims[:3] if len(dims) == 4 else dims
|
|
# vox2ras_tkr
|
|
delta = header["delta"]
|
|
ds = np.array(delta, float)
|
|
ns = np.array(dims * ds) / 2.0
|
|
v2rtkr = np.array(
|
|
[
|
|
[-ds[0], 0, 0, ns[0]],
|
|
[0, 0, ds[2], -ns[2]],
|
|
[0, -ds[1], 0, ns[1]],
|
|
[0, 0, 0, 1],
|
|
],
|
|
dtype=np.float32,
|
|
)
|
|
# ras2vox
|
|
d = np.diag(delta)
|
|
pcrs_c = dims / 2.0
|
|
Mdc = header["Mdc"].T
|
|
pxyz_0 = header["Pxyz_c"] - np.dot(Mdc, np.dot(d, pcrs_c))
|
|
M = np.eye(4, 4)
|
|
M[0:3, 0:3] = np.dot(Mdc, d)
|
|
M[0:3, 3] = pxyz_0.T
|
|
header = dict(dims=dims, vox2ras_tkr=v2rtkr, vox2ras=M, zooms=header["delta"])
|
|
return header
|
|
|
|
|
|
def _get_atlas_values(vol_info, rr):
|
|
# Transform MRI coordinates (where our surfaces live) to voxels
|
|
rr_vox = apply_trans(vol_info["mri_vox_t"], rr)
|
|
good = (
|
|
(rr_vox >= -0.5) & (rr_vox < np.array(vol_info["data"].shape, int) - 0.5)
|
|
).all(-1)
|
|
idx = np.round(rr_vox[good].T).astype(np.int64)
|
|
values = np.full(rr.shape[0], np.nan)
|
|
values[good] = vol_info["data"][tuple(idx)]
|
|
return values
|
|
|
|
|
|
def get_volume_labels_from_aseg(mgz_fname, return_colors=False, atlas_ids=None):
|
|
"""Return a list of names and colors of segmented volumes.
|
|
|
|
Parameters
|
|
----------
|
|
mgz_fname : path-like
|
|
Filename to read. Typically ``aseg.mgz`` or some variant in the
|
|
freesurfer pipeline.
|
|
return_colors : bool
|
|
If True returns also the labels colors.
|
|
atlas_ids : dict | None
|
|
A lookup table providing a mapping from region names (str) to ID values
|
|
(int). Can be None to use the standard Freesurfer LUT.
|
|
|
|
.. versionadded:: 0.21.0
|
|
|
|
Returns
|
|
-------
|
|
label_names : list of str
|
|
The names of segmented volumes included in this mgz file.
|
|
label_colors : list of str
|
|
The RGB colors of the labels included in this mgz file.
|
|
|
|
See Also
|
|
--------
|
|
read_freesurfer_lut
|
|
|
|
Notes
|
|
-----
|
|
.. versionchanged:: 0.21.0
|
|
The label names are now sorted in the same order as their corresponding
|
|
values in the MRI file.
|
|
|
|
.. versionadded:: 0.9.0
|
|
"""
|
|
nib = _import_nibabel("load MRI atlas data")
|
|
mgz_fname = _check_fname(
|
|
mgz_fname, overwrite="read", must_exist=True, name="mgz_fname"
|
|
)
|
|
atlas = nib.load(mgz_fname)
|
|
data = np.asarray(atlas.dataobj) # don't need float here
|
|
want = np.unique(data)
|
|
if atlas_ids is None:
|
|
atlas_ids, colors = read_freesurfer_lut()
|
|
elif return_colors:
|
|
raise ValueError("return_colors must be False if atlas_ids are provided")
|
|
# restrict to the ones in the MRI, sorted by label name
|
|
keep = np.isin(list(atlas_ids.values()), want)
|
|
keys = sorted(
|
|
(key for ki, key in enumerate(atlas_ids.keys()) if keep[ki]),
|
|
key=lambda x: atlas_ids[x],
|
|
)
|
|
if return_colors:
|
|
colors = [colors[k] for k in keys]
|
|
out = keys, colors
|
|
else:
|
|
out = keys
|
|
return out
|
|
|
|
|
|
##############################################################################
|
|
# Head to MRI volume conversion
|
|
|
|
|
|
@verbose
|
|
def head_to_mri(
|
|
pos,
|
|
subject,
|
|
mri_head_t,
|
|
subjects_dir=None,
|
|
*,
|
|
kind="mri",
|
|
unscale=False,
|
|
verbose=None,
|
|
):
|
|
"""Convert pos from head coordinate system to MRI ones.
|
|
|
|
Parameters
|
|
----------
|
|
pos : array, shape (n_pos, 3)
|
|
The coordinates (in m) in head coordinate system.
|
|
%(subject)s
|
|
mri_head_t : instance of Transform
|
|
MRI<->Head coordinate transformation.
|
|
%(subjects_dir)s
|
|
kind : str
|
|
The MRI coordinate frame kind, can be ``'mri'`` (default) for
|
|
FreeSurfer surface RAS or ``'ras'`` (default in 1.2) to use MRI RAS
|
|
(scanner RAS).
|
|
|
|
.. versionadded:: 1.2
|
|
unscale : bool
|
|
For surrogate MRIs (e.g., scaled using ``mne coreg``), if True
|
|
(default False), use the MRI scaling parameters to obtain points in
|
|
the original/surrogate subject's MRI space.
|
|
|
|
.. versionadded:: 1.2
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
coordinates : array, shape (n_pos, 3)
|
|
The MRI RAS coordinates (in mm) of pos.
|
|
|
|
Notes
|
|
-----
|
|
This function requires nibabel.
|
|
"""
|
|
from .coreg import read_mri_cfg
|
|
|
|
_validate_type(kind, str, "kind")
|
|
_check_option("kind", kind, ("ras", "mri"))
|
|
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
|
t1_fname = subjects_dir / subject / "mri" / "T1.mgz"
|
|
head_mri_t = _ensure_trans(mri_head_t, "head", "mri")
|
|
if kind == "ras":
|
|
_, _, mri_ras_t, _, _ = _read_mri_info(t1_fname)
|
|
head_ras_t = combine_transforms(head_mri_t, mri_ras_t, "head", "ras")
|
|
head_dest_t = head_ras_t
|
|
else:
|
|
assert kind == "mri"
|
|
head_dest_t = head_mri_t
|
|
pos_dest = apply_trans(head_dest_t, pos)
|
|
# unscale if requested
|
|
if unscale:
|
|
params = read_mri_cfg(subject, subjects_dir)
|
|
pos_dest /= params["scale"]
|
|
pos_dest *= 1e3 # mm
|
|
return pos_dest
|
|
|
|
|
|
##############################################################################
|
|
# Surface to MNI conversion
|
|
|
|
|
|
@verbose
|
|
def vertex_to_mni(vertices, hemis, subject, subjects_dir=None, verbose=None):
|
|
"""Convert the array of vertices for a hemisphere to MNI coordinates.
|
|
|
|
Parameters
|
|
----------
|
|
vertices : int, or list of int
|
|
Vertex number(s) to convert.
|
|
hemis : int, or list of int
|
|
Hemisphere(s) the vertices belong to.
|
|
%(subject)s
|
|
subjects_dir : str, or None
|
|
Path to ``SUBJECTS_DIR`` if it is not set in the environment.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
coordinates : array, shape (n_vertices, 3)
|
|
The MNI coordinates (in mm) of the vertices.
|
|
"""
|
|
singleton = False
|
|
if not isinstance(vertices, list) and not isinstance(vertices, np.ndarray):
|
|
singleton = True
|
|
vertices = [vertices]
|
|
|
|
if not isinstance(hemis, list) and not isinstance(hemis, np.ndarray):
|
|
hemis = [hemis] * len(vertices)
|
|
|
|
if not len(hemis) == len(vertices):
|
|
raise ValueError("hemi and vertices must match in length")
|
|
|
|
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
|
|
|
surfs = [subjects_dir / subject / "surf" / f"{h}.white" for h in ["lh", "rh"]]
|
|
|
|
# read surface locations in MRI space
|
|
rr = [read_surface(s)[0] for s in surfs]
|
|
|
|
# take point locations in MRI space and convert to MNI coordinates
|
|
xfm = read_talxfm(subject, subjects_dir)
|
|
xfm["trans"][:3, 3] *= 1000.0 # m->mm
|
|
data = np.array([rr[h][v, :] for h, v in zip(hemis, vertices)])
|
|
if singleton:
|
|
data = data[0]
|
|
return apply_trans(xfm["trans"], data)
|
|
|
|
|
|
##############################################################################
|
|
# Volume to MNI conversion
|
|
|
|
|
|
@verbose
|
|
def head_to_mni(pos, subject, mri_head_t, subjects_dir=None, verbose=None):
|
|
"""Convert pos from head coordinate system to MNI ones.
|
|
|
|
Parameters
|
|
----------
|
|
pos : array, shape (n_pos, 3)
|
|
The coordinates (in m) in head coordinate system.
|
|
%(subject)s
|
|
mri_head_t : instance of Transform
|
|
MRI<->Head coordinate transformation.
|
|
%(subjects_dir)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
coordinates : array, shape (n_pos, 3)
|
|
The MNI coordinates (in mm) of pos.
|
|
|
|
Notes
|
|
-----
|
|
This function requires either nibabel.
|
|
"""
|
|
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
|
|
|
# before we go from head to MRI (surface RAS)
|
|
head_mni_t = combine_transforms(
|
|
_ensure_trans(mri_head_t, "head", "mri"),
|
|
read_talxfm(subject, subjects_dir),
|
|
"head",
|
|
"mni_tal",
|
|
)
|
|
return apply_trans(head_mni_t, pos) * 1000.0
|
|
|
|
|
|
@verbose
|
|
def get_mni_fiducials(subject, subjects_dir=None, verbose=None):
|
|
"""Estimate fiducials for a subject.
|
|
|
|
Parameters
|
|
----------
|
|
%(subject)s
|
|
%(subjects_dir)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
fids_mri : list
|
|
List of estimated fiducials (each point in a dict), in the order
|
|
LPA, nasion, RPA.
|
|
|
|
Notes
|
|
-----
|
|
This takes the ``fsaverage-fiducials.fif`` file included with MNE—which
|
|
contain the LPA, nasion, and RPA for the ``fsaverage`` subject—and
|
|
transforms them to the given FreeSurfer subject's MRI space.
|
|
The MRI of ``fsaverage`` is already in MNI Talairach space, so applying
|
|
the inverse of the given subject's MNI Talairach affine transformation
|
|
(``$SUBJECTS_DIR/$SUBJECT/mri/transforms/talairach.xfm``) is used
|
|
to estimate the subject's fiducial locations.
|
|
|
|
For more details about the coordinate systems and transformations involved,
|
|
see https://surfer.nmr.mgh.harvard.edu/fswiki/CoordinateSystems and
|
|
:ref:`tut-source-alignment`.
|
|
"""
|
|
# Eventually we might want to allow using the MNI Talairach with-skull
|
|
# transformation rather than the standard brain-based MNI Talaranch
|
|
# transformation, and/or project the points onto the head surface
|
|
# (if available).
|
|
fname_fids_fs = (
|
|
Path(__file__).parent / "data" / "fsaverage" / "fsaverage-fiducials.fif"
|
|
)
|
|
|
|
# Read fsaverage fiducials file and subject Talairach.
|
|
fids, coord_frame = read_fiducials(fname_fids_fs)
|
|
assert coord_frame == FIFF.FIFFV_COORD_MRI
|
|
if subject == "fsaverage":
|
|
return fids # special short-circuit for fsaverage
|
|
mni_mri_t = invert_transform(read_talxfm(subject, subjects_dir))
|
|
for f in fids:
|
|
f["r"] = apply_trans(mni_mri_t, f["r"])
|
|
return fids
|
|
|
|
|
|
@verbose
|
|
def estimate_head_mri_t(subject, subjects_dir=None, verbose=None):
|
|
"""Estimate the head->mri transform from fsaverage fiducials.
|
|
|
|
A subject's fiducials can be estimated given a Freesurfer ``recon-all``
|
|
by transforming ``fsaverage`` fiducials using the inverse Talairach
|
|
transform, see :func:`mne.coreg.get_mni_fiducials`.
|
|
|
|
Parameters
|
|
----------
|
|
%(subject)s
|
|
%(subjects_dir)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
%(trans_not_none)s
|
|
"""
|
|
from .channels.montage import compute_native_head_t, make_dig_montage
|
|
|
|
subjects_dir = get_subjects_dir(subjects_dir, raise_error=True)
|
|
lpa, nasion, rpa = get_mni_fiducials(subject, subjects_dir)
|
|
montage = make_dig_montage(
|
|
lpa=lpa["r"], nasion=nasion["r"], rpa=rpa["r"], coord_frame="mri"
|
|
)
|
|
return invert_transform(compute_native_head_t(montage))
|
|
|
|
|
|
def _get_affine_from_lta_info(lines):
|
|
"""Get the vox2ras affine from lta file info."""
|
|
volume_data = np.loadtxt([line.split("=")[1] for line in lines])
|
|
# get the size of the volume (number of voxels), slice resolution.
|
|
# the matrix of directional cosines and the ras at the center of the bore
|
|
dims, deltas, dir_cos, center_ras = (
|
|
volume_data[0],
|
|
volume_data[1],
|
|
volume_data[2:5],
|
|
volume_data[5],
|
|
)
|
|
dir_cos_delta = dir_cos.T * deltas
|
|
vol_center = (dir_cos_delta @ dims[:3]) / 2
|
|
affine = np.eye(4)
|
|
affine[:3, :3] = dir_cos_delta
|
|
affine[:3, 3] = center_ras - vol_center
|
|
return affine
|
|
|
|
|
|
@verbose
|
|
def read_lta(fname, verbose=None):
|
|
"""Read a Freesurfer linear transform array file.
|
|
|
|
Parameters
|
|
----------
|
|
fname : path-like
|
|
The transform filename.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
affine : ndarray
|
|
The affine transformation described by the lta file.
|
|
"""
|
|
_check_fname(fname, "read", must_exist=True)
|
|
with open(fname) as fid:
|
|
lines = fid.readlines()
|
|
# 0 is linear vox2vox, 1 is linear ras2ras
|
|
trans_type = int(lines[0].split("=")[1].strip()[0])
|
|
assert trans_type in (0, 1)
|
|
affine = np.loadtxt(lines[5:9])
|
|
if trans_type == 1:
|
|
return affine
|
|
|
|
src_affine = _get_affine_from_lta_info(lines[12:18])
|
|
dst_affine = _get_affine_from_lta_info(lines[21:27])
|
|
|
|
# don't compute if src and dst are already identical
|
|
if np.allclose(src_affine, dst_affine):
|
|
return affine
|
|
|
|
ras2ras = src_affine @ np.linalg.inv(affine) @ np.linalg.inv(dst_affine)
|
|
affine = np.linalg.inv(np.linalg.inv(src_affine) @ ras2ras @ src_affine)
|
|
return affine
|
|
|
|
|
|
@verbose
|
|
def read_talxfm(subject, subjects_dir=None, verbose=None):
|
|
"""Compute MRI-to-MNI transform from FreeSurfer talairach.xfm file.
|
|
|
|
Parameters
|
|
----------
|
|
%(subject)s
|
|
%(subjects_dir)s
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
mri_mni_t : instance of Transform
|
|
The affine transformation from MRI to MNI space for the subject.
|
|
"""
|
|
# Adapted from freesurfer m-files. Altered to deal with Norig
|
|
# and Torig correctly
|
|
subjects_dir = get_subjects_dir(subjects_dir)
|
|
# Setup the RAS to MNI transform
|
|
ras_mni_t = read_ras_mni_t(subject, subjects_dir)
|
|
ras_mni_t["trans"][:3, 3] /= 1000.0 # mm->m
|
|
|
|
# We want to get from Freesurfer surface RAS ('mri') to MNI ('mni_tal').
|
|
# This file only gives us RAS (non-zero origin) ('ras') to MNI ('mni_tal').
|
|
# Se we need to get the ras->mri transform from the MRI headers.
|
|
|
|
# To do this, we get Norig and Torig
|
|
# (i.e. vox_ras_t and vox_mri_t, respectively)
|
|
path = subjects_dir / subject / "mri" / "orig.mgz"
|
|
if not path.is_file():
|
|
path = subjects_dir / subject / "mri" / "T1.mgz"
|
|
if not path.is_file():
|
|
raise OSError(f"mri not found: {path}")
|
|
_, _, mri_ras_t, _, _ = _read_mri_info(path)
|
|
mri_mni_t = combine_transforms(mri_ras_t, ras_mni_t, "mri", "mni_tal")
|
|
return mri_mni_t
|
|
|
|
|
|
def _check_mri(mri, subject, subjects_dir):
|
|
"""Check whether an mri exists in the Freesurfer subject directory."""
|
|
_validate_type(mri, "path-like", "mri")
|
|
if op.isfile(mri) and op.basename(mri) != mri:
|
|
return mri
|
|
if not op.isfile(mri):
|
|
if subject is None:
|
|
raise FileNotFoundError(
|
|
f"MRI file {mri!r} not found and no subject provided"
|
|
)
|
|
subjects_dir = str(get_subjects_dir(subjects_dir, raise_error=True))
|
|
mri = op.join(subjects_dir, subject, "mri", mri)
|
|
if not op.isfile(mri):
|
|
raise FileNotFoundError(f"MRI file {mri!r} not found")
|
|
if op.basename(mri) == mri:
|
|
err = (
|
|
f"Ambiguous filename - found {mri!r} in current folder.\n"
|
|
"If this is correct prefix name with relative or absolute path"
|
|
)
|
|
raise OSError(err)
|
|
return mri
|
|
|
|
|
|
def _read_mri_info(path, units="m", return_img=False, use_nibabel=False):
|
|
# This is equivalent but 100x slower, so only use nibabel if we need to
|
|
# (later):
|
|
if use_nibabel:
|
|
nib = _import_nibabel()
|
|
hdr = nib.load(path).header
|
|
n_orig = hdr.get_vox2ras()
|
|
t_orig = hdr.get_vox2ras_tkr()
|
|
dims = hdr.get_data_shape()
|
|
zooms = hdr.get_zooms()[:3]
|
|
else:
|
|
hdr = _get_mgz_header(path)
|
|
n_orig = hdr["vox2ras"]
|
|
t_orig = hdr["vox2ras_tkr"]
|
|
dims = hdr["dims"]
|
|
zooms = hdr["zooms"]
|
|
|
|
# extract the MRI_VOXEL to RAS (non-zero origin) transform
|
|
vox_ras_t = Transform("mri_voxel", "ras", n_orig)
|
|
|
|
# extract the MRI_VOXEL to MRI transform
|
|
vox_mri_t = Transform("mri_voxel", "mri", t_orig)
|
|
|
|
# construct the MRI to RAS (non-zero origin) transform
|
|
mri_ras_t = combine_transforms(invert_transform(vox_mri_t), vox_ras_t, "mri", "ras")
|
|
|
|
assert units in ("m", "mm")
|
|
if units == "m":
|
|
conv = np.array([[1e-3, 1e-3, 1e-3, 1]]).T
|
|
# scaling and translation terms
|
|
vox_ras_t["trans"] *= conv
|
|
vox_mri_t["trans"] *= conv
|
|
# just the translation term
|
|
mri_ras_t["trans"][:, 3:4] *= conv
|
|
|
|
out = (vox_ras_t, vox_mri_t, mri_ras_t, dims, zooms)
|
|
if return_img:
|
|
nibabel = _import_nibabel()
|
|
out += (nibabel.load(path),)
|
|
return out
|
|
|
|
|
|
def read_freesurfer_lut(fname=None):
|
|
"""Read a Freesurfer-formatted LUT.
|
|
|
|
Parameters
|
|
----------
|
|
fname : path-like | None
|
|
The filename. Can be None to read the standard Freesurfer LUT.
|
|
|
|
Returns
|
|
-------
|
|
atlas_ids : dict
|
|
Mapping from label names to IDs.
|
|
colors : dict
|
|
Mapping from label names to colors.
|
|
"""
|
|
lut = _get_lut(fname)
|
|
names, ids = lut["name"], lut["id"]
|
|
colors = np.array([lut["R"], lut["G"], lut["B"], lut["A"]], float).T
|
|
atlas_ids = dict(zip(names, ids))
|
|
colors = dict(zip(names, colors))
|
|
return atlas_ids, colors
|
|
|
|
|
|
def _get_lut(fname=None):
|
|
"""Get a FreeSurfer LUT."""
|
|
if fname is None:
|
|
fname = Path(__file__).parent / "data" / "FreeSurferColorLUT.txt"
|
|
_check_fname(fname, "read", must_exist=True)
|
|
dtype = [
|
|
("id", "<i8"),
|
|
("name", "U"),
|
|
("R", "<i8"),
|
|
("G", "<i8"),
|
|
("B", "<i8"),
|
|
("A", "<i8"),
|
|
]
|
|
lut = {d[0]: list() for d in dtype}
|
|
with open(fname) as fid:
|
|
for line in fid:
|
|
line = line.strip()
|
|
if line.startswith("#") or not line:
|
|
continue
|
|
line = line.split()
|
|
if len(line) != len(dtype):
|
|
raise RuntimeError(f"LUT is improperly formatted: {fname}")
|
|
for d, part in zip(dtype, line):
|
|
lut[d[0]].append(part)
|
|
lut = {d[0]: np.array(lut[d[0]], dtype=d[1]) for d in dtype}
|
|
assert len(lut["name"]) > 0
|
|
lut["name"] = [str(name) for name in lut["name"]]
|
|
return lut
|
|
|
|
|
|
@verbose
|
|
def _get_head_surface(surf, subject, subjects_dir, bem=None, verbose=None):
|
|
"""Get a head surface from the Freesurfer subject directory.
|
|
|
|
Parameters
|
|
----------
|
|
surf : str
|
|
The name of the surface 'auto', 'head', 'outer_skin', 'head-dense'
|
|
or 'seghead'.
|
|
%(subject)s
|
|
%(subjects_dir)s
|
|
bem : mne.bem.ConductorModel | None
|
|
The conductor model that stores information about the head surface.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
head_surf : dict | None
|
|
A dictionary with keys 'rr', 'tris', 'ntri', 'use_tris', 'np'
|
|
and 'coord_frame' that store information for mesh plotting and other
|
|
useful information about the head surface.
|
|
|
|
Notes
|
|
-----
|
|
.. versionadded: 0.24
|
|
"""
|
|
from .bem import _bem_find_surface, read_bem_surfaces
|
|
|
|
_check_option("surf", surf, ("auto", "head", "outer_skin", "head-dense", "seghead"))
|
|
if surf in ("auto", "head", "outer_skin"):
|
|
if bem is not None:
|
|
try:
|
|
return _bem_find_surface(bem, "head")
|
|
except RuntimeError:
|
|
logger.info(
|
|
"Could not find the surface for "
|
|
"head in the provided BEM model, "
|
|
"looking in the subject directory."
|
|
)
|
|
if subject is None:
|
|
if surf == "auto":
|
|
return
|
|
raise ValueError(
|
|
"To plot the head surface, the BEM/sphere"
|
|
" model must contain a head surface "
|
|
'or "subject" must be provided (got '
|
|
"None)"
|
|
)
|
|
subject_dir = op.join(get_subjects_dir(subjects_dir, raise_error=True), subject)
|
|
if surf in ("head-dense", "seghead"):
|
|
try_fnames = [
|
|
op.join(subject_dir, "bem", f"{subject}-head-dense.fif"),
|
|
op.join(subject_dir, "surf", "lh.seghead"),
|
|
]
|
|
else:
|
|
try_fnames = [
|
|
op.join(subject_dir, "bem", "outer_skin.surf"),
|
|
op.join(subject_dir, "bem", "flash", "outer_skin.surf"),
|
|
op.join(subject_dir, "bem", f"{subject}-head-sparse.fif"),
|
|
op.join(subject_dir, "bem", f"{subject}-head.fif"),
|
|
]
|
|
for fname in try_fnames:
|
|
if op.exists(fname):
|
|
logger.info(f"Using {op.basename(fname)} for head surface.")
|
|
if op.splitext(fname)[-1] == ".fif":
|
|
return read_bem_surfaces(fname, on_defects="warn")[0]
|
|
else:
|
|
return _read_mri_surface(fname)
|
|
raise OSError(
|
|
"No head surface found for subject "
|
|
f"{subject} after trying:\n" + "\n".join(try_fnames)
|
|
)
|
|
|
|
|
|
@verbose
|
|
def _get_skull_surface(surf, subject, subjects_dir, bem=None, verbose=None):
|
|
"""Get a skull surface from the Freesurfer subject directory.
|
|
|
|
Parameters
|
|
----------
|
|
surf : str
|
|
The name of the surface 'outer' or 'inner'.
|
|
%(subject)s
|
|
%(subjects_dir)s
|
|
bem : mne.bem.ConductorModel | None
|
|
The conductor model that stores information about the skull surface.
|
|
%(verbose)s
|
|
|
|
Returns
|
|
-------
|
|
skull_surf : dict | None
|
|
A dictionary with keys 'rr', 'tris', 'ntri', 'use_tris', 'np'
|
|
and 'coord_frame' that store information for mesh plotting and other
|
|
useful information about the head surface.
|
|
|
|
Notes
|
|
-----
|
|
.. versionadded: 0.24
|
|
"""
|
|
from .bem import _bem_find_surface
|
|
|
|
if bem is not None:
|
|
try:
|
|
return _bem_find_surface(bem, surf + "_skull")
|
|
except RuntimeError:
|
|
logger.info(
|
|
"Could not find the surface for "
|
|
"skull in the provided BEM model, "
|
|
"looking in the subject directory."
|
|
)
|
|
subjects_dir = Path(get_subjects_dir(subjects_dir, raise_error=True))
|
|
fname = _check_fname(
|
|
subjects_dir / subject / "bem" / (surf + "_skull.surf"),
|
|
overwrite="read",
|
|
must_exist=True,
|
|
name=f"{surf} skull surface",
|
|
)
|
|
return _read_mri_surface(fname)
|
|
|
|
|
|
def _estimate_talxfm_rigid(subject, subjects_dir):
|
|
from .coreg import _trans_from_params, fit_matched_points
|
|
|
|
xfm = read_talxfm(subject, subjects_dir)
|
|
# XYZ+origin + halfway
|
|
pts_tal = np.concatenate([np.eye(4)[:, :3], np.eye(3) * 0.5])
|
|
pts_subj = apply_trans(invert_transform(xfm), pts_tal)
|
|
# we fit with scaling enabled, but then discard it (we just need
|
|
# the rigid-body components)
|
|
params = fit_matched_points(pts_subj, pts_tal, scale=3, out="params")
|
|
rigid = _trans_from_params((True, True, False), params[:6])
|
|
return rigid
|