Newer
Older

Paul McCarthy
committed
#!/usr/bin/env python
#
# freesurfer.py - The FreesurferMesh class.

Paul McCarthy
committed
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`FreesurferMesh` class, which can be
used for loading Freesurfer geometry and vertex data files.
The following types of files generated by Freesurfer are recognised by this
module:
- Core geometry files, defining the cortical mesh. Each core geometry
file has the same number of vertices and same triangle definitions.
- Extra geometry files, defining the cortical mesh. Extra geometry
files may have a different number of vertices and/or triangle
definitions.
- Vertex data files (a.k.a. ``'curv'`` files), containing a scalar value for
every vertex in the mesh. This data may also be contained in
``mgz``/``mgh`` files.
- Label files, containing indices of a sub-set of vertices in the
mesh.
- Annotation files, containing a label value, and an RGBA colour for a
subset of vertices in the mesh.
The following functions are also available:
.. autosummary::
:nosignatures:
loadVertexDataFile
isCoreGeometryFile
isGeometryFile
isVertexDataFile
isVertexLabelFile
isVertexAnnotFile
relatedGeometryFiles
relatedVertexDataFiles
findReferenceImage

Paul McCarthy
committed
"""
import os.path as op
import itertools as it
import glob
import fnmatch
import collections

Paul McCarthy
committed
import numpy as np
import nibabel.freesurfer as nibfs

Paul McCarthy
committed
import fsl.utils.path as fslpath
import fsl.utils.memoize as memoize
import fsl.data.image as fslimage
import fsl.data.mghimage as fslmgh
import fsl.data.mesh as fslmesh

Paul McCarthy
committed
CORE_GEOMETRY_FILES = ['?h.orig',
'?h.pial',
'?h.white',
'?h.inflated',
'?h.sphere']
"""File patterns for identifying the core Freesurfer geometry files. """
CORE_GEOMETRY_DESCRIPTIONS = [
"Freesurfer surface (original)",
"Freesurfer surface (pial)",
"Freesurfer surface (white matter)",
"Freesurfer surface (inflated)",
"Freesurfer surface (sphere)"]
"""A description for each extension in :attr:`GEOMETRY_EXTENSIONS`. """
EXTRA_GEOMETRY_FILES = ['?h.orig.nofix',
'?h.smoothwm.nofix',
'?h.inflated.nofix',
'?h.qsphere.nofix',
'?h.sphere.nofix',
'?h.white.preaparc']
"""Other geometry files which may be present in Freesurfer output. """
VERTEX_DATA_FILES = ['?h.thickness',
'?h.curv',
'?h.area',
"""File patterns which are interpreted as Freesurfer vertex data files,
containing a scalar value for every vertex in the mesh.
"""
VERTEX_MGH_FILES = ['?h.*.mgh',
'?h.*.mgz']
"""File patterns which are interpreted as MGH files containing a
scalar value for every vertex in the mesh.
"""
VERTEX_LABEL_FILES = ['?h.*.label']
"""File patterns which are interpreted as Freesurfer vertex label files,
containing a scalar value for a sub-set of vertices in the mesh.
"""
VERTEX_ANNOT_FILES = ['?h.*.annot']
"""File patterns which are interpreted as Freesurfer vertex annotation files,
containing a scalar value and an RGBA colour for a sub-set of vertices in the
mesh.
"""
class FreesurferMesh(fslmesh.Mesh):
"""The :class:`FreesurferMesh` class represents a triangle mesh
loaded from a Freesurfer geometry file.

Paul McCarthy
committed
"""
def __init__(self, filename, fixWinding=False, loadAll=False):
"""Load the given Freesurfer surface file using ``nibabel``.
:arg infile: A Freesurfer geometry file (e.g. ``*.pial``).
:arg fixWinding: Passed through to the :meth:`addVertices` method
for the first vertex set.
:arg loadAll: If ``True``, the ``infile`` directory is scanned
for other freesurfer surface files which are then
loaded as additional vertex sets.

Paul McCarthy
committed
"""
vertices, indices, meta, comment = nibfs.read_geometry(

Paul McCarthy
committed
filename,
read_metadata=True,
read_stamp=True)
name = op.basename(filename)

Paul McCarthy
committed
fslmesh.Mesh.__init__(self,
indices,
name=name,
dataSource=filename)

Paul McCarthy
committed
self.addVertices(vertices, filename, fixWinding=fixWinding)

Paul McCarthy
committed
self.__luts = collections.OrderedDict()
self.setMeta('comment', comment)
for k, v in meta.items():
self.setMeta(k, v)

Paul McCarthy
committed

Paul McCarthy
committed
allFiles = relatedGeometryFiles(filename)

Paul McCarthy
committed
verts, idxs = nibfs.read_geometry(f)
self.addVertices(verts, f, select=False)

Paul McCarthy
committed
def loadVertices(self, infile, key=None, **kwargs):
"""Overrides :meth:`.Mesh.loadVertices`. If the given ``infile``
looks like a Freesurfer file, it is loaded via
``nibabel.freesurfer.load_geometry``. Otherwise, it is passed to
:meth:`.Mesh.loadVertices`.
"""
if not isGeometryFile(infile):
return fslmesh.Mesh.loadVertices(self, infile, key, **kwargs)
infile = op.abspath(infile)
if key is None:
key = infile
# TODO merge metadata
vertices, indices, meta, comment = nibfs.read_geometry(
infile,
read_metadata=True,
read_stamp=True)

Paul McCarthy
committed
return self.addVertices(vertices, key, **kwargs)
def loadVertexData(self, infile, key=None):
"""Overrides :meth:`.Mesh.loadVertexData`. If the given ``infile``
looks like a Freesurfer file, it is loaded via the
:func:`loadVertexDataFile` function. Otherwise, it is passed through
to the base-class function.
If the given ``infile`` is a vertex annotation file, it is assumed
that the file contains a value for every vertex in the mesh.
isvdata = isVertexDataFile( infile)
isvlabel = isVertexLabelFile(infile)
isvannot = isVertexAnnotFile(infile)
if not any((isvdata, isvmgh, isvlabel, isvannot)):

Paul McCarthy
committed
return fslmesh.Mesh.loadVertexData(self, infile)
infile = op.abspath(infile)

Paul McCarthy
committed
nvertices = self.nvertices
if key is None:
key = infile
vdata = loadVertexDataFile(infile)
if isvlabel:
# Currently ignoring scalar
# values stored in label files
idxs = np.asarray(vdata[0])
expanded = np.zeros(nvertices)
vdata = expanded
elif isvannot:
vdata, lut, names = vdata

Paul McCarthy
committed
vdata = self.addVertexData(key, vdata)
if isvannot:

Paul McCarthy
committed
def getVertexDataColourTable(self, key):
"""If the given ``key`` refers to a Freesurfer ``.annot`` file,
the corresponding RGBA lookup table and label names can be
retrieved via this method.
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
"""
return self.__luts[key]
def loadVertexDataFile(infile):
"""Loads the given Freesurfer vertex data, label, or annotation file.
This function return different things depending on what ``infile`` is:
- If ``infile`` is a vertex data file, a ``(nvertices,)`` array is
returned, containing one value for each vertex in the mesh.
- If ``infile`` is a ``mgh``/``mgz`` file, the image data is returned
as-is, with dimensions of length 1 squeezed out (under the assumption
that the image contains scalar vertex data).
- If ``infile`` is a vertex label file, a tuple containing the following
is returned:
- a ``(n,)`` array, containing the indices of all vertices that are
specified in the file.
- a ``(n,)`` array, containing scalar value for each vertex
- If ``infile`` is a vertex annotation file, a tuple containing the
following is returned:
- a ``(n,)`` array containing the indices of all ``n`` vertices that
are specified in the file.
- a ``(l, 5)`` array containing the RGBA colour, and the label value,
for every label that is specified in the file.
- A list of length ``l``, containing the names of every label that is
specified in the file.
if isVertexDataFile(infile):
return nibfs.read_morph_data(infile)
elif isVertexLabelFile(infile):
return nibfs.read_label(infile, read_scalars=True)
elif isVertexAnnotFile(infile):
# nibabel 2.2.1 is broken w.r.t. .annot files.
# raise ValueError('.annot files are not yet supported')
labels, lut, names = nibfs.read_annot(infile, orig_ids=False)
return labels, lut, names
elif isVertexMGHFile(infile):
return fslmgh.MGHImage(infile)[:].squeeze()
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
else:
raise ValueError('Unrecognised freesurfer '
'file type: {}'.format(infile))
@memoize.memoize
def isCoreGeometryFile(infile):
"""Returns ``True`` if ``infile`` looks like a core Freesurfer geometry
file, ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf) for gf in CORE_GEOMETRY_FILES])
@memoize.memoize
def isGeometryFile(infile):
"""Returns ``True`` if ``infile`` looks like a Freesurfer geometry
file (core or otherwise), ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf)
for gf in CORE_GEOMETRY_FILES + EXTRA_GEOMETRY_FILES])
@memoize.memoize
def isVertexDataFile(infile):
"""Returns ``True`` if ``infile`` looks like a Freesurfer vertex
data file, ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf) for gf in VERTEX_DATA_FILES])
@memoize.memoize
def isVertexMGHFile(infile):
"""Returns ``True`` if ``infile`` looks like a Freesurfer MGH file
containing vertex data, ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf) for gf in VERTEX_MGH_FILES])
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
@memoize.memoize
def isVertexLabelFile(infile):
"""Returns ``True`` if ``infile`` looks like a Freesurfer vertex
label file, ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf) for gf in VERTEX_LABEL_FILES])
@memoize.memoize
def isVertexAnnotFile(infile):
"""Returns ``True`` if ``infile`` looks like a Freesurfer vertex
annotation file, ``False`` otherwise.
"""
infile = op.basename(infile)
return any([fnmatch.fnmatch(infile, gf) for gf in VERTEX_ANNOT_FILES])
def relatedGeometryFiles(fname):
"""Returns a list of all files which (look like they) are freesurfer
geometry files which correspond to the given geometry file.
"""
if not isCoreGeometryFile(fname):
return []
dirname, fname = op.split(op.abspath(fname))
hemi = fname[0]
fpats = [hemi + p[1:] for p in CORE_GEOMETRY_FILES]
related = [glob.glob(op.join(dirname, p)) for p in fpats]
related = list(it.chain(*related))
return [r for r in related if op.basename(r) != fname]
def relatedVertexDataFiles(fname):
"""Returns a list of all files which (look like they) are vertex data,
label, or annotation files related to the given freesurfer geometry file.
"""
if not isCoreGeometryFile(fname):
return []
fname = op.abspath(fname)
dirname = op.dirname(fname)
hemi = op.basename(fname)[0]
fpats = (VERTEX_DATA_FILES +
VERTEX_LABEL_FILES +
VERTEX_ANNOT_FILES +
VERTEX_MGH_FILES)
fpats = [hemi + p[1:] if p.startswith('?h') else p for p in fpats]
basedir = op.dirname(dirname)
searchDirs = set([dirname,
op.join(basedir, 'surf'),
op.join(basedir, 'stats'),
op.join(basedir, 'label')])
searchPats = it.product(searchDirs, fpats)
related = []
for sdir, spat in searchPats:
related.extend(glob.glob(op.join(sdir, spat)))
return related
def findReferenceImage(fname):
"""Attempts to locate the volumetric reference image for (what is
assumed to be) the given Freesurfer geometry file.

Paul McCarthy
committed
"""
basedir = op.dirname(op.dirname(op.abspath(fname)))
t1 = op.join(basedir, 'mri', 'T1.mgz')
exts = fslimage.ALLOWED_EXTENSIONS + fslmgh.ALLOWED_EXTENSIONS
try:
return fslpath.addExt(t1, allowedExts=exts, mustExist=True)
except fslpath.PathError:
return None