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