Commit 8f8ce1bd authored by Paul McCarthy's avatar Paul McCarthy 🚵
Browse files

Routine to fix winding order/normal vectors on triangle meshes. I am not

convinced that this is foolproof, but it seems to work.
parent 61e0ccd3
...@@ -49,7 +49,7 @@ class GiftiSurface(mesh.TriangleMesh): ...@@ -49,7 +49,7 @@ class GiftiSurface(mesh.TriangleMesh):
""" """
def __init__(self, infile): def __init__(self, infile, fixWinding=False):
"""Load the given GIFTI file using ``nibabel``, and extracts surface """Load the given GIFTI file using ``nibabel``, and extracts surface
data using the :func:`loadGiftiSurface` function. data using the :func:`loadGiftiSurface` function.
...@@ -61,7 +61,7 @@ class GiftiSurface(mesh.TriangleMesh): ...@@ -61,7 +61,7 @@ class GiftiSurface(mesh.TriangleMesh):
surfimg, vertices, indices = loadGiftiSurface(infile) surfimg, vertices, indices = loadGiftiSurface(infile)
mesh.TriangleMesh.__init__(self, vertices, indices) mesh.TriangleMesh.__init__(self, vertices, indices, fixWinding)
name = fslpath.removeExt(op.basename(infile), ALLOWED_EXTENSIONS) name = fslpath.removeExt(op.basename(infile), ALLOWED_EXTENSIONS)
infile = op.abspath(infile) infile = op.abspath(infile)
......
...@@ -27,6 +27,8 @@ import numpy as np ...@@ -27,6 +27,8 @@ import numpy as np
import six import six
import fsl.utils.transform as transform
from . import image as fslimage from . import image as fslimage
...@@ -34,9 +36,9 @@ log = logging.getLogger(__name__) ...@@ -34,9 +36,9 @@ log = logging.getLogger(__name__)
class TriangleMesh(object): class TriangleMesh(object):
"""The ``TriangleMesh`` class represents a 3D model. A mesh is defined by """The ``TriangleMesh`` class represents a 3D model. A mesh is defined by a
a collection of vertices and indices. The indices index into the list of collection of ``N`` vertices, and ``M`` triangles. The triangles are
vertices, and define a set of triangles which make the model. defined by ``(M, 3)) indices into the list of vertices.
A ``TriangleMesh`` instance has the following attributes: A ``TriangleMesh`` instance has the following attributes:
...@@ -74,16 +76,20 @@ class TriangleMesh(object): ...@@ -74,16 +76,20 @@ class TriangleMesh(object):
""" """
def __init__(self, data, indices=None): def __init__(self, data, indices=None, fixWinding=False):
"""Create a ``TriangleMesh`` instance. """Create a ``TriangleMesh`` instance.
:arg data: Can either be a file name, or a :math:`N\\times 3` :arg data: Can either be a file name, or a :math:`N\\times 3`
``numpy`` array containing vertex data. If ``data`` is ``numpy`` array containing vertex data. If ``data``
a file name, it is passed to the is a file name, it is passed to the
:func:`loadVTKPolydataFile` function. :func:`loadVTKPolydataFile` function.
:arg indices: A list of indices into the vertex data, defining
the triangles.
:arg indices: A list of indices into the vertex data, defining :arg fixWinding: Defaults to ``False``. If ``True``, the vertex
the triangles. winding order of every triangle is is fixed so they
all have outward-facing normal vectors.
""" """
if isinstance(data, six.string_types): if isinstance(data, six.string_types):
...@@ -103,8 +109,8 @@ class TriangleMesh(object): ...@@ -103,8 +109,8 @@ class TriangleMesh(object):
if indices is None: if indices is None:
indices = np.arange(data.shape[0]) indices = np.arange(data.shape[0])
self.vertices = np.array(data) self.__vertices = np.array(data)
self.indices = np.array(indices).reshape((-1, 3)) self.__indices = np.array(indices).reshape((-1, 3))
self.__vertexData = {} self.__vertexData = {}
self.__faceNormals = None self.__faceNormals = None
...@@ -112,6 +118,9 @@ class TriangleMesh(object): ...@@ -112,6 +118,9 @@ class TriangleMesh(object):
self.__loBounds = self.vertices.min(axis=0) self.__loBounds = self.vertices.min(axis=0)
self.__hiBounds = self.vertices.max(axis=0) self.__hiBounds = self.vertices.max(axis=0)
if fixWinding:
self.__fixWindingOrder()
def __repr__(self): def __repr__(self):
"""Returns a string representation of this ``TriangleMesh`` instance. """Returns a string representation of this ``TriangleMesh`` instance.
...@@ -127,35 +136,73 @@ class TriangleMesh(object): ...@@ -127,35 +136,73 @@ class TriangleMesh(object):
@property @property
def normals(self): def vertices(self):
"""Returns a ``(M, 3)`` array containing normals for every triangle """The ``(N, 3)`` vertices of this mesh. """
in the mesh. return self.__vertices
@property
def indices(self):
"""The ``(M, 3)`` triangles of this mesh. """
return self.__indices
def __fixWindingOrder(self):
"""Called by :meth:`__init__` if ``fixWinding is True``. Fixes the
mesh triangle winding order so that all face normals are facing
outwards from the centre of the mesh.
""" """
if self.__faceNormals is not None:
return self.__faceNormals
v1 = self.vertices[self.indices[:, 0]] # Define a viewpoint which is
v2 = self.vertices[self.indices[:, 1]] # far away from the mesh.
fnormals = self.normals
camera = self.__loBounds - (self.__hiBounds - self.__loBounds)
# Find the nearest vertex
# to the viewpoint
dists = np.sqrt(np.sum((self.vertices - camera) ** 2, axis=1))
ivert = np.argmin(dists)
vert = self.vertices[ivert]
# Pick a triangle that
# this vertex in and
# ges its face normal
itri = np.where(self.indices == ivert)[0][0]
n = fnormals[itri, :]
# Make sure the angle between the
# normal, and a vector from the
# vertex to the camera is positive
# If it isn't, flip the triangle
# winding order.
if np.dot(n, transform.normalise(camera - vert)) < 0:
self.indices[:, [1, 2]] = self.indices[:, [2, 1]]
self.__faceNormals *= -1
fnormals = np.cross(v1, v2)
# TODO make fast, and make sure that this actually works. @property
for i in range(self.indices.shape[0]): def normals(self):
"""A ``(M, 3)`` array containing surface normals for every
triangle in the mesh, normalised to unit length.
"""
p0, p1, p2 = self.vertices[self.indices[i], :] if self.__faceNormals is not None:
n = fnormals[i, :] return self.__faceNormals
v0 = self.vertices[self.indices[:, 0]]
v1 = self.vertices[self.indices[:, 1]]
v2 = self.vertices[self.indices[:, 2]]
if np.dot(n, np.cross(p1 - p0, p2 - p0)) > 0: n = np.cross((v1 - v0), (v2 - v0))
fnormals[i, :] *= -1
self.__faceNormals = fnormals self.__faceNormals = transform.normalise(n)
return self.__faceNormals return self.__faceNormals
@property @property
def vnormals(self): def vnormals(self):
"""Returns a ``(N, 3)`` array containing normals for every vertex """A ``(N, 3)`` array containing normals for every vertex
in the mesh. in the mesh.
""" """
if self.__vertNormals is not None: if self.__vertNormals is not None:
...@@ -163,10 +210,12 @@ class TriangleMesh(object): ...@@ -163,10 +210,12 @@ class TriangleMesh(object):
# per-face normals # per-face normals
fnormals = self.normals fnormals = self.normals
vnormals = np.zeros((self.vertices.shape[0], 3), dtype=np.float) vnormals = np.zeros((self.vertices.shape[0], 3), dtype=np.float)
# TODO make fast # TODO make fast. I can't figure
# out how to use np.add.at to
# accumulate the face normals for
# each vertex.
for i in range(self.indices.shape[0]): for i in range(self.indices.shape[0]):
v0, v1, v2 = self.indices[i] v0, v1, v2 = self.indices[i]
...@@ -176,10 +225,7 @@ class TriangleMesh(object): ...@@ -176,10 +225,7 @@ class TriangleMesh(object):
vnormals[v2, :] += fnormals[i] vnormals[v2, :] += fnormals[i]
# normalise to unit length # normalise to unit length
lens = np.sqrt(np.sum(vnormals * vnormals, axis=1)) self.__vertNormals = transform.normalise(vnormals)
vnormals = (vnormals.T / lens).T
self.__vertNormals = vnormals
return self.__vertNormals return self.__vertNormals
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment