Skip to content

Commit ccfcdcf

Browse files
committed
Merge remote-tracking branch 'oesteban/fix/AreaWeighedP2PDistance'
Conflicts: CHANGES
2 parents 35cf777 + f15e0c0 commit ccfcdcf

File tree

7 files changed

+93514
-38
lines changed

7 files changed

+93514
-38
lines changed

CHANGES

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
Next release
22
============
33

4+
* ENH: New mesh.MeshWarpMaths to operate on surface-defined warpings
5+
(https://github.com/nipy/nipype/pull/1016)
6+
* FIX: Refactor P2PDistance, change name to ComputeMeshWarp, add regression tests,
7+
fix bug in area weighted distance, and added optimizations
8+
(https://github.com/nipy/nipype/pull/1016)
49
* ENH: Add an option not to resubmit Nodes that finished running when using SGEGraph (https://github.com/nipy/nipype/pull/1002)
510
* FIX: FUGUE is now properly listing outputs. (https://github.com/nipy/nipype/pull/978)
611
* ENH: Improved FieldMap-Based (FMB) workflow for correction of susceptibility distortions in EPI seqs.

nipype/algorithms/mesh.py

Lines changed: 245 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -13,55 +13,78 @@
1313

1414

1515
import numpy as np
16-
from scipy.spatial.distance import euclidean
16+
from numpy import linalg as nla
17+
import os.path as op
18+
from nipype.external import six
1719

1820
from .. import logging
1921

2022
from ..interfaces.base import (BaseInterface, traits, TraitedSpec, File,
2123
BaseInterfaceInputSpec)
24+
from warnings import warn
2225
iflogger = logging.getLogger('interface')
2326

2427

25-
class P2PDistanceInputSpec(BaseInterfaceInputSpec):
28+
class ComputeMeshWarpInputSpec(BaseInterfaceInputSpec):
2629
surface1 = File(exists=True, mandatory=True,
27-
desc=("Reference surface (vtk format) to which compute "
28-
"distance."))
30+
desc=('Reference surface (vtk format) to which compute '
31+
'distance.'))
2932
surface2 = File(exists=True, mandatory=True,
30-
desc=("Test surface (vtk format) from which compute "
31-
"distance."))
32-
weighting = traits.Enum("none", "surface", usedefault=True,
33-
desc=('"none": no weighting is performed, '
34-
'"surface": edge distance is weighted by the '
35-
'corresponding surface area'))
33+
desc=('Test surface (vtk format) from which compute '
34+
'distance.'))
35+
metric = traits.Enum('euclidean', 'sqeuclidean', usedefault=True,
36+
desc=('norm used to report distance'))
37+
weighting = traits.Enum(
38+
'none', 'area', usedefault=True,
39+
desc=('"none": no weighting is performed, surface": edge distance is '
40+
'weighted by the corresponding surface area'))
41+
out_warp = File('surfwarp.vtk', usedefault=True,
42+
desc='vtk file based on surface1 and warpings mapping it '
43+
'to surface2')
44+
out_file = File('distance.npy', usedefault=True,
45+
desc='numpy file keeping computed distances and weights')
3646

3747

38-
class P2PDistanceOutputSpec(TraitedSpec):
48+
class ComputeMeshWarpOutputSpec(TraitedSpec):
3949
distance = traits.Float(desc="computed distance")
50+
out_warp = File(exists=True, desc=('vtk file with the vertex-wise '
51+
'mapping of surface1 to surface2'))
52+
out_file = File(exists=True,
53+
desc='numpy file keeping computed distances and weights')
4054

4155

42-
class P2PDistance(BaseInterface):
56+
class ComputeMeshWarp(BaseInterface):
4357

44-
"""Calculates a point-to-point (p2p) distance between two corresponding
45-
VTK-readable meshes or contours.
58+
"""
59+
Calculates a the vertex-wise warping to get surface2 from surface1.
60+
It also reports the average distance of vertices, using the norm specified
61+
as input.
62+
63+
.. warning:
64+
65+
A point-to-point correspondence between surfaces is required
4666
47-
A point-to-point correspondence between nodes is required
4867
4968
Example
5069
-------
5170
52-
>>> import nipype.algorithms.mesh as mesh
53-
>>> dist = mesh.P2PDistance()
71+
>>> import nipype.algorithms.mesh as m
72+
>>> dist = m.ComputeMeshWarp()
5473
>>> dist.inputs.surface1 = 'surf1.vtk'
5574
>>> dist.inputs.surface2 = 'surf2.vtk'
5675
>>> res = dist.run() # doctest: +SKIP
76+
5777
"""
5878

59-
input_spec = P2PDistanceInputSpec
60-
output_spec = P2PDistanceOutputSpec
79+
input_spec = ComputeMeshWarpInputSpec
80+
output_spec = ComputeMeshWarpOutputSpec
6181

6282
def _triangle_area(self, A, B, C):
63-
ABxAC = euclidean(A, B) * euclidean(A, C)
64-
prod = np.dot(np.array(B) - np.array(A), np.array(C) - np.array(A))
83+
A = np.array(A)
84+
B = np.array(B)
85+
C = np.array(C)
86+
ABxAC = nla.norm(A - B) * nla.norm(A - C)
87+
prod = np.dot(B - A, C - A)
6588
angle = np.arccos(prod / ABxAC)
6689
area = 0.5 * ABxAC * np.sin(angle)
6790
return area
@@ -70,14 +93,17 @@ def _run_interface(self, runtime):
7093
try:
7194
from tvtk.api import tvtk
7295
except ImportError:
73-
raise ImportError('Interface P2PDistance requires tvtk')
96+
raise ImportError('Interface ComputeMeshWarp requires tvtk')
7497

7598
try:
7699
from enthought.etsconfig.api import ETSConfig
77100
ETSConfig.toolkit = 'null'
78101
except ImportError:
79102
iflogger.warn(('ETS toolkit could not be imported'))
80103
pass
104+
except ValueError:
105+
iflogger.warn(('ETS toolkit is already set'))
106+
pass
81107

82108
r1 = tvtk.PolyDataReader(file_name=self.inputs.surface1)
83109
r2 = tvtk.PolyDataReader(file_name=self.inputs.surface2)
@@ -86,32 +112,213 @@ def _run_interface(self, runtime):
86112
r1.update()
87113
r2.update()
88114
assert(len(vtk1.points) == len(vtk2.points))
89-
d = 0.0
90-
totalWeight = 0.0
91115

92-
points = vtk1.points
93-
faces = vtk1.polys.to_array().reshape(-1, 4).astype(int)[:, 1:]
116+
points1 = np.array(vtk1.points)
117+
points2 = np.array(vtk2.points)
118+
119+
diff = points2 - points1
120+
weights = np.ones(len(diff))
121+
122+
try:
123+
errvector = nla.norm(diff, axis=1)
124+
except TypeError: # numpy < 1.9
125+
errvector = np.apply_along_axis(nla.norm, 1, diff)
126+
pass
127+
128+
if self.inputs.metric == 'sqeuclidean':
129+
errvector = errvector ** 2
130+
131+
if (self.inputs.weighting == 'area'):
132+
faces = vtk1.polys.to_array().reshape(-1, 4).astype(int)[:, 1:]
94133

95-
for p1, p2 in zip(points, vtk2.points):
96-
weight = 1.0
97-
if (self.inputs.weighting == 'surface'):
134+
for i, p1 in enumerate(points2):
98135
# compute surfaces, set in weight
99-
weight = 0.0
100-
point_faces = faces[(faces[:, :] == 0).any(axis=1)]
136+
w = 0.0
137+
point_faces = faces[(faces[:, :] == i).any(axis=1)]
101138

102139
for idset in point_faces:
103-
p1 = points[int(idset[0])]
104-
p2 = points[int(idset[1])]
105-
p3 = points[int(idset[2])]
106-
weight = weight + self._triangle_area(p1, p2, p3)
140+
fp1 = points1[int(idset[0])]
141+
fp2 = points1[int(idset[1])]
142+
fp3 = points1[int(idset[2])]
143+
w += self._triangle_area(fp1, fp2, fp3)
144+
weights[i] = w
107145

108-
d += weight * euclidean(p1, p2)
109-
totalWeight = totalWeight + weight
146+
result = np.vstack([errvector, weights])
147+
np.save(op.abspath(self.inputs.out_file), result.transpose())
110148

111-
self._distance = d / totalWeight
149+
out_mesh = tvtk.PolyData()
150+
out_mesh.points = vtk1.points
151+
out_mesh.polys = vtk1.polys
152+
out_mesh.point_data.vectors = diff
153+
out_mesh.point_data.vectors.name = 'warpings'
154+
writer = tvtk.PolyDataWriter(
155+
file_name=op.abspath(self.inputs.out_warp))
156+
writer.set_input_data(out_mesh)
157+
writer.write()
158+
159+
self._distance = np.average(errvector, weights=weights)
112160
return runtime
113161

114162
def _list_outputs(self):
115163
outputs = self._outputs().get()
164+
outputs['out_file'] = op.abspath(self.inputs.out_file)
165+
outputs['out_warp'] = op.abspath(self.inputs.out_warp)
116166
outputs['distance'] = self._distance
117167
return outputs
168+
169+
170+
class MeshWarpMathsInputSpec(BaseInterfaceInputSpec):
171+
in_surf = File(exists=True, mandatory=True,
172+
desc=('Input surface in vtk format, with associated warp '
173+
'field as point data (ie. from ComputeMeshWarp'))
174+
float_trait = traits.Either(traits.Float(1.0), traits.Tuple(
175+
traits.Float(1.0), traits.Float(1.0), traits.Float(1.0)))
176+
177+
operator = traits.Either(
178+
float_trait, File(exists=True), default=1.0, mandatory=True,
179+
desc=('image, float or tuple of floats to act as operator'))
180+
181+
operation = traits.Enum('sum', 'sub', 'mul', 'div', usedefault=True,
182+
desc=('operation to be performed'))
183+
184+
out_warp = File('warp_maths.vtk', usedefault=True,
185+
desc='vtk file based on in_surf and warpings mapping it '
186+
'to out_file')
187+
out_file = File('warped_surf.vtk', usedefault=True,
188+
desc='vtk with surface warped')
189+
190+
191+
class MeshWarpMathsOutputSpec(TraitedSpec):
192+
out_warp = File(exists=True, desc=('vtk file with the vertex-wise '
193+
'mapping of surface1 to surface2'))
194+
out_file = File(exists=True,
195+
desc='vtk with surface warped')
196+
197+
198+
class MeshWarpMaths(BaseInterface):
199+
200+
"""
201+
Performs the most basic mathematical operations on the warping field
202+
defined at each vertex of the input surface. A surface with scalar
203+
or vector data can be used as operator for non-uniform operations.
204+
205+
.. warning:
206+
207+
A point-to-point correspondence between surfaces is required
208+
209+
210+
Example
211+
-------
212+
213+
>>> import nipype.algorithms.mesh as m
214+
>>> mmath = m.MeshWarpMaths()
215+
>>> mmath.inputs.in_surf = 'surf1.vtk'
216+
>>> mmath.inputs.operator = 'surf2.vtk'
217+
>>> mmath.inputs.operation = 'mul'
218+
>>> res = mmath.run() # doctest: +SKIP
219+
220+
"""
221+
222+
input_spec = MeshWarpMathsInputSpec
223+
output_spec = MeshWarpMathsOutputSpec
224+
225+
def _run_interface(self, runtime):
226+
try:
227+
from tvtk.api import tvtk
228+
except ImportError:
229+
raise ImportError('Interface ComputeMeshWarp requires tvtk')
230+
231+
try:
232+
from enthought.etsconfig.api import ETSConfig
233+
ETSConfig.toolkit = 'null'
234+
except ImportError:
235+
iflogger.warn(('ETS toolkit could not be imported'))
236+
pass
237+
except ValueError:
238+
iflogger.warn(('ETS toolkit is already set'))
239+
pass
240+
241+
r1 = tvtk.PolyDataReader(file_name=self.inputs.in_surf)
242+
vtk1 = r1.output
243+
r1.update()
244+
points1 = np.array(vtk1.points)
245+
246+
if vtk1.point_data.vectors is None:
247+
raise RuntimeError(('No warping field was found in in_surf'))
248+
249+
operator = self.inputs.operator
250+
opfield = np.ones_like(points1)
251+
252+
if isinstance(operator, six.string_types):
253+
r2 = tvtk.PolyDataReader(file_name=self.inputs.surface2)
254+
vtk2 = r2.output
255+
r2.update()
256+
assert(len(points1) == len(vtk2.points))
257+
258+
opfield = vtk2.point_data.vectors
259+
260+
if opfield is None:
261+
opfield = vtk2.point_data.scalars
262+
263+
if opfield is None:
264+
raise RuntimeError(
265+
('No operator values found in operator file'))
266+
267+
opfield = np.array(opfield)
268+
269+
if opfield.shape[1] < points1.shape[1]:
270+
opfield = np.array([opfield.tolist()] * points1.shape[1]).T
271+
else:
272+
operator = np.atleast_1d(operator)
273+
opfield *= operator
274+
275+
warping = np.array(vtk1.point_data.vectors)
276+
277+
if self.inputs.operation == 'sum':
278+
warping += opfield
279+
elif self.inputs.operation == 'sub':
280+
warping -= opfield
281+
elif self.inputs.operation == 'mul':
282+
warping *= opfield
283+
elif self.inputs.operation == 'div':
284+
warping /= opfield
285+
286+
vtk1.point_data.vectors = warping
287+
writer = tvtk.PolyDataWriter(
288+
file_name=op.abspath(self.inputs.out_warp))
289+
writer.set_input_data(vtk1)
290+
writer.write()
291+
292+
vtk1.point_data.vectors = None
293+
vtk1.points = points1 + warping
294+
writer = tvtk.PolyDataWriter(
295+
file_name=op.abspath(self.inputs.out_file))
296+
writer.set_input_data(vtk1)
297+
writer.write()
298+
299+
return runtime
300+
301+
def _list_outputs(self):
302+
outputs = self._outputs().get()
303+
outputs['out_file'] = op.abspath(self.inputs.out_file)
304+
outputs['out_warp'] = op.abspath(self.inputs.out_warp)
305+
return outputs
306+
307+
308+
class P2PDistance(ComputeMeshWarp):
309+
310+
"""
311+
Calculates a point-to-point (p2p) distance between two corresponding
312+
VTK-readable meshes or contours.
313+
314+
A point-to-point correspondence between nodes is required
315+
316+
.. deprecated:: 1.0-dev
317+
Use :py:class:`ComputeMeshWarp` instead.
318+
"""
319+
320+
def __init__(self, **inputs):
321+
super(P2PDistance, self).__init__(**inputs)
322+
warn(('This interface has been deprecated since 1.0, please use '
323+
'ComputeMeshWarp'),
324+
DeprecationWarning)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from nipype.testing import assert_equal
3+
from nipype.algorithms.mesh import ComputeMeshWarp
4+
5+
def test_ComputeMeshWarp_inputs():
6+
input_map = dict(ignore_exception=dict(nohash=True,
7+
usedefault=True,
8+
),
9+
metric=dict(usedefault=True,
10+
),
11+
out_file=dict(usedefault=True,
12+
),
13+
out_warp=dict(usedefault=True,
14+
),
15+
surface1=dict(mandatory=True,
16+
),
17+
surface2=dict(mandatory=True,
18+
),
19+
weighting=dict(usedefault=True,
20+
),
21+
)
22+
inputs = ComputeMeshWarp.input_spec()
23+
24+
for key, metadata in input_map.items():
25+
for metakey, value in metadata.items():
26+
yield assert_equal, getattr(inputs.traits()[key], metakey), value
27+
28+
def test_ComputeMeshWarp_outputs():
29+
output_map = dict(distance=dict(),
30+
out_file=dict(),
31+
out_warp=dict(),
32+
)
33+
outputs = ComputeMeshWarp.output_spec()
34+
35+
for key, metadata in output_map.items():
36+
for metakey, value in metadata.items():
37+
yield assert_equal, getattr(outputs.traits()[key], metakey), value
38+

0 commit comments

Comments
 (0)