Skip to content

Commit d77cea2

Browse files
committed
Merge pull request #78 from Eric89GXL/vert-norm
ENH: Plot with vertex normals
2 parents 3bb7827 + cb2918b commit d77cea2

File tree

3 files changed

+118
-2
lines changed

3 files changed

+118
-2
lines changed

surfer/tests/test_utils.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from os.path import join as pjoin
22

33
import numpy as np
4-
from numpy.testing import assert_array_almost_equal
4+
from numpy.testing import assert_array_almost_equal, assert_array_equal
55

66
from surfer import utils
77

@@ -10,12 +10,36 @@
1010
data_path = pjoin(subj_dir, subject_id)
1111

1212

13+
def _slow_compute_normals(rr, tris):
14+
"""Efficiently compute vertex normals for triangulated surface"""
15+
# first, compute triangle normals
16+
r1 = rr[tris[:, 0], :]
17+
r2 = rr[tris[:, 1], :]
18+
r3 = rr[tris[:, 2], :]
19+
tri_nn = np.cross((r2 - r1), (r3 - r1))
20+
21+
# Triangle normals and areas
22+
size = np.sqrt(np.sum(tri_nn * tri_nn, axis=1))
23+
zidx = np.where(size == 0)[0]
24+
size[zidx] = 1.0 # prevent ugly divide-by-zero
25+
tri_nn /= size[:, np.newaxis]
26+
27+
# accumulate the normals
28+
nn = np.zeros((len(rr), 3))
29+
for p, verts in enumerate(tris):
30+
nn[verts] += tri_nn[p, :]
31+
size = np.sqrt(np.sum(nn * nn, axis=1))
32+
size[size == 0] = 1.0 # prevent ugly divide-by-zero
33+
nn /= size[:, np.newaxis]
34+
return nn
35+
36+
1337
@utils.requires_fsaverage
1438
def test_surface():
1539
"""Test IO for Surface class"""
1640
for subjects_dir in [None, subj_dir]:
1741
surface = utils.Surface('fsaverage', 'lh', 'inflated',
18-
subjects_dir=subjects_dir)
42+
subjects_dir=subjects_dir)
1943
surface.load_geometry()
2044
surface.load_label('BA1')
2145
surface.load_curvature()
@@ -25,3 +49,18 @@ def test_surface():
2549
surface.apply_xfm(xfm)
2650
x_ = surface.x
2751
assert_array_almost_equal(x + 2, x_)
52+
53+
# normals
54+
nn = _slow_compute_normals(surface.coords, surface.faces[:10000])
55+
nn_fast = utils._compute_normals(surface.coords, surface.faces[:10000])
56+
assert_array_almost_equal(nn, nn_fast)
57+
58+
59+
def test_huge_cross():
60+
"""Test cross product with lots of elements
61+
"""
62+
x = np.random.rand(100000, 3)
63+
y = np.random.rand(1, 3)
64+
z = np.cross(x, y)
65+
zz = utils._fast_cross_3d(x, y)
66+
assert_array_equal(z, zz)

surfer/utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ class Surface(object):
3939
The vertices coordinates
4040
faces : 2d array
4141
The faces ie. the triangles
42+
nn : 2d array
43+
Normalized surface normals for vertices.
4244
subjects_dir : str | None
4345
If not None, this directory will be used as the subjects directory
4446
instead of the value set using the SUBJECTS_DIR environment variable.
@@ -80,6 +82,7 @@ def load_geometry(self):
8082
self.coords[:, 0] -= (np.max(self.coords[:, 0]) + self.offset)
8183
else:
8284
self.coords[:, 0] -= (np.min(self.coords[:, 0]) + self.offset)
85+
self.nn = _compute_normals(self.coords, self.faces)
8386

8487
def save_geometry(self):
8588
surf_path = op.join(self.data_path, "surf",
@@ -128,6 +131,77 @@ def apply_xfm(self, mtx):
128131
mtx.T)[:, :3]
129132

130133

134+
def _fast_cross_3d(x, y):
135+
"""Compute cross product between list of 3D vectors
136+
137+
Much faster than np.cross() when the number of cross products
138+
becomes large (>500). This is because np.cross() methods become
139+
less memory efficient at this stage.
140+
141+
Parameters
142+
----------
143+
x : array
144+
Input array 1.
145+
y : array
146+
Input array 2.
147+
148+
Returns
149+
-------
150+
z : array
151+
Cross product of x and y.
152+
153+
Notes
154+
-----
155+
x and y must both be 2D row vectors. One must have length 1, or both
156+
lengths must match.
157+
"""
158+
assert x.ndim == 2
159+
assert y.ndim == 2
160+
assert x.shape[1] == 3
161+
assert y.shape[1] == 3
162+
assert (x.shape[0] == 1 or y.shape[0] == 1) or x.shape[0] == y.shape[0]
163+
if max([x.shape[0], y.shape[0]]) >= 500:
164+
return np.c_[x[:, 1] * y[:, 2] - x[:, 2] * y[:, 1],
165+
x[:, 2] * y[:, 0] - x[:, 0] * y[:, 2],
166+
x[:, 0] * y[:, 1] - x[:, 1] * y[:, 0]]
167+
else:
168+
return np.cross(x, y)
169+
170+
171+
def _compute_normals(rr, tris):
172+
"""Efficiently compute vertex normals for triangulated surface"""
173+
# first, compute triangle normals
174+
r1 = rr[tris[:, 0], :]
175+
r2 = rr[tris[:, 1], :]
176+
r3 = rr[tris[:, 2], :]
177+
tri_nn = _fast_cross_3d((r2 - r1), (r3 - r1))
178+
179+
# Triangle normals and areas
180+
size = np.sqrt(np.sum(tri_nn * tri_nn, axis=1))
181+
zidx = np.where(size == 0)[0]
182+
size[zidx] = 1.0 # prevent ugly divide-by-zero
183+
tri_nn /= size[:, np.newaxis]
184+
185+
npts = len(rr)
186+
187+
# the following code replaces this, but is faster (vectorized):
188+
#
189+
# for p, verts in enumerate(tris):
190+
# nn[verts, :] += tri_nn[p, :]
191+
#
192+
nn = np.zeros((npts, 3))
193+
for verts in tris.T: # note this only loops 3x (number of verts per tri)
194+
counts = np.bincount(verts, minlength=npts)
195+
reord = np.argsort(verts)
196+
vals = np.r_[np.zeros((1, 3)), np.cumsum(tri_nn[reord, :], 0)]
197+
idx = np.cumsum(np.r_[0, counts])
198+
nn += vals[idx[1:], :] - vals[idx[:-1], :]
199+
size = np.sqrt(np.sum(nn * nn, axis=1))
200+
size[size == 0] = 1.0 # prevent ugly divide-by-zero
201+
nn /= size[:, np.newaxis]
202+
return nn
203+
204+
131205
###############################################################################
132206
# LOGGING (courtesy of mne-python)
133207

surfer/viz.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2022,6 +2022,9 @@ def __init__(self, subject_id, hemi, surf, figure, geo, curv, title,
20222022
x, y, z, f = self._geo.x, self._geo.y, self._geo.z, self._geo.faces
20232023
self._geo_mesh = mlab.pipeline.triangular_mesh_source(x, y, z, f,
20242024
**meshargs)
2025+
# add surface normals
2026+
self._geo_mesh.data.point_data.normals = self._geo.nn
2027+
self._geo_mesh.data.cell_data.normals = None
20252028
self._geo_surf = mlab.pipeline.surface(self._geo_mesh,
20262029
figure=self._f, reset_zoom=True,
20272030
**kwargs)

0 commit comments

Comments
 (0)