From fd5fdfe42a54832b5fcd5aa3fc4e324d45cba216 Mon Sep 17 00:00:00 2001 From: Paul McCarthy <pauld.mccarthy@gmail.com> Date: Wed, 25 Jan 2017 16:25:55 +0000 Subject: [PATCH] model.Model named to mesh.TriangleMesh. Indices are now shape (M, 3), instead of (M*3,). New gifti module for basic gifti surface support. --- fsl/data/gifti.py | 131 +++++++++++++++++++++++++++++++++ fsl/data/{model.py => mesh.py} | 66 ++++++++++++----- 2 files changed, 177 insertions(+), 20 deletions(-) create mode 100644 fsl/data/gifti.py rename fsl/data/{model.py => mesh.py} (70%) diff --git a/fsl/data/gifti.py b/fsl/data/gifti.py new file mode 100644 index 000000000..ea14b4e2c --- /dev/null +++ b/fsl/data/gifti.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# +# gifti.py - GIFTI file support. +# +# Author: Paul McCarthy <pauldmccarthy@gmail.com> +# Michiel Cottar <michiel.cottaar@ndcn.ox.ac.uk> +# +"""This class provides classes and functions for working with GIFTI files. + +The GIFTI file format specification can be found at +http://www.nitrc.org/projects/gifti/ + +Support is currently very basic - only the following classes/functions +are available: + + .. autosummary:: + :nosignatures: + + GiftiSurface + extractGiftiSurface +""" + + +import os.path as op + +import fsl.utils.path as fslpath +from . import mesh + + +class GiftiSurface(mesh.TriangleMesh): + """Class which represents a GIFTI surface image. This is essentially + just a 3D model made of triangles. + + In addition to the ``vertices`` and ``indices`` provided by the + :class:`.TriangleMesh` class (from which the ``GiftiSurface`` class + derives), a ``GiftiSurface`` instance has the following attributes: + + ============== ==================================================== + ``name`` A name, typically the file name sans-suffix. + ``dataSource`` Full path to the GIFTI file. + ``surfImg`` Reference to the loaded ``nibabel.gifti.GiftiImage`` + ============== ==================================================== + """ + + + def __init__(self, infile): + """Load the given GIFTI file using ``nibabel``, and extracts surface + data using the :func:`extractGiftiSurface` function. + + :arg infile: A GIFTI surface file + """ + + import nibabel as nib + + surfimg = nib.load(infile) + vertices, indices = extractGiftiSurface(surfimg) + + mesh.TriangleMesh.__init__(self, vertices, indices) + + name = fslpath.removeExt(op.basename(infile), ALLOWED_EXTENSIONS) + infile = op.abspath(infile) + + self.name = name + self.dataSource = infile + self.surfImg = surfimg + + +ALLOWED_EXTENSIONS = ['.surf.gii', '.gii'] +"""List of file extensions that a file containing Gifti surface data +is expected to have. +""" + + +EXTENSION_DESCRIPTIONS = ['GIFTI surface file', 'GIFTI surface file'] +"""A description for each of the :data:`ALLOWED_EXTENSIONS`. +""" + + +def extractGiftiSurface(surfimg): + """Extracts surface data from the given ``nibabel.gifti.GiftiImage``. + + The image is expected to contain the following``<DataArray>`` elements: + + - one comprising ``NIFTI_INTENT_POINTSET`` data (the surface vertices) + - one comprising ``NIFTI_INTENT_TRIANGLE`` data (vertex indices + defining the triangles). + + A ``ValueError`` will be raised if this is not the case. + + :arg surfimg: A ``GiftiImage`` containing surface data. + + :returns: A tuple containing these values: + + - A :math:`N\\times 3` ``numpy`` array containing :math:`N` + vertices. + + - A :math:`M\\times 3` ``numpy`` array containing the + vertex indices for :math:`M` triangles. + """ + + from nibabel import gifti + + codes = gifti.gifti.intent_codes.code + + indices = None + vertices = None + + for darray in surfimg.darrays: + + if darray.intent == codes['pointset']: + + if vertices is not None: + raise ValueError('multiple arrays with intent "{}"'.format( + darray.intent)) + + vertices = darray.data + + elif darray.intent == codes['triangle']: + if indices is not None: + raise ValueError('multiple arrays with intent "{}"'.format( + darray.intent)) + + indices = darray.data + + if vertices is None: + raise ValueError('no array with intent "pointset" found') + + if indices is None: + raise ValueError('no array witbh intent "triangle"found') + + return vertices, indices diff --git a/fsl/data/model.py b/fsl/data/mesh.py similarity index 70% rename from fsl/data/model.py rename to fsl/data/mesh.py index 13b3510e6..978fbbdd9 100644 --- a/fsl/data/model.py +++ b/fsl/data/mesh.py @@ -1,18 +1,22 @@ #!/usr/bin/env python # -# model.py - The Model class, for VTK polygon data. +# mesh.py - The TriangleMesh class. # # Author: Paul McCarthy <pauldmccarthy@gmail.com> # -"""This module provides the :class:`Model` class, which represents a 3D model. +"""This module provides the :class:`TriangleMesh` class, which represents a +3D model made of triangles. -.. note:: I/O support is very limited - currently, the only supported file +.. note:: I/O support is very limited - currently, the only supported file type is the VTK legacy file format, containing the ``POLYDATA`` - dataset. the :class:`Model` class assumes that every polygon defined - in an input file is a triangle (i.e. refers to three vertices). + dataset. the :class:`TriangleMesh` class assumes that every polygon + defined in an input file is a triangle (i.e. refers to three + vertices). See http://www.vtk.org/wp-content/uploads/2015/04/file-formats.pdf for an overview of the VTK legacy file format. + + In the future, I may or may not add support for more complex meshes. """ @@ -29,22 +33,41 @@ from . import image as fslimage log = logging.getLogger(__name__) -class Model(object): - """The ``Model`` class represents a 3D model. A model is defined by a - collection of vertices and indices. The indices index into the list of +class TriangleMesh(object): + """The ``TriangleMesh`` class represents a 3D model. A mesh is defined by + a collection of vertices and indices. The indices index into the list of vertices, and define a set of triangles which make the model. + + + A ``TriangleMesh`` instance has the following attributes: + + + ============== ==================================================== + ``name`` A name, typically the file name sans-suffix. + + ``dataSource`` Full path to the mesh file (or ``None`` if there is + no file associated with this mesh). + + ``vertices`` A :math:`N\times 3` ``numpy `` array containing + the vertices. + + ``indices`` A :meth:`M\times 3` ``numpy`` array containing + the vertex indices for :math:`M` triangles + ============== ==================================================== + """ def __init__(self, data, indices=None): - """Create a ``Model`` instance. + """Create a ``TriangleMesh`` instance. :arg data: Can either be a file name, or a :math:`N\\times 3` ``numpy`` array containing vertex data. If ``data`` is a file name, it is passed to the :func:`loadVTKPolydataFile` function. - :arg indices: A list of indices into the vertex data. + :arg indices: A list of indices into the vertex data, defining + the triangles. """ if isinstance(data, six.string_types): @@ -58,34 +81,36 @@ class Model(object): self.name = op.basename(infile) self.dataSource = infile else: - self.name = 'Model' - self.dataSource = 'Model' + self.name = 'TriangleMesh' + self.dataSource = None if indices is None: - indices = np.arange(data.shape[0], dtype=np.uint32) + indices = np.arange(data.shape[0]) - self.vertices = np.array(data, dtype=np.float32) - self.indices = indices + self.vertices = np.array(data) + self.indices = np.array(indices).reshape((-1, 3)) self.__loBounds = self.vertices.min(axis=0) self.__hiBounds = self.vertices.max(axis=0) def __repr__(self): - """Rewturns a string representation of this ``Model`` instance. """ + """Returns a string representation of this ``TriangleMesh`` instance. + """ return '{}({}, {})'.format(type(self).__name__, self.name, self.dataSource) def __str__(self): - """Rewturns a string representation of this ``Model`` instance. """ + """Returns a string representation of this ``TriangleMesh`` instance. + """ return self.__repr__() def getBounds(self): """Returns a tuple of values which define a minimal bounding box that - will contain all vertices in this ``Model`` instance. The bounding - box is arranged like so: + will contain all vertices in this ``TriangleMesh`` instance. The + bounding box is arranged like so: ``((xlow, ylow, zlow), (xhigh, yhigh, zhigh))`` """ @@ -93,7 +118,8 @@ class Model(object): ALLOWED_EXTENSIONS = ['.vtk'] -"""A list of file extensions which could contain :class:`Model` data. """ +"""A list of file extensions which could contain :class:`TriangleMesh` data. +""" EXTENSION_DESCRIPTIONS = ['VTK polygon model file'] -- GitLab