Skip to content

Commit 17bdfb0

Browse files
committed
fix regression tests, add new MeshWarpMaths interface
1 parent 1a5d5c7 commit 17bdfb0

File tree

4 files changed

+194
-13
lines changed

4 files changed

+194
-13
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
* FIX: Amend create_tbss_non_fa() workflow to match FSL's tbss_non_fa command. (https://github.com/nipy/nipype/pull/1033)
510
* FIX: remove unused mandatory flag from spm normalize (https://github.com/nipy/nipype/pull/1048)
611
* ENH: Update ANTSCorticalThickness interface (https://github.com/nipy/nipype/pull/1013)

nipype/algorithms/mesh.py

Lines changed: 146 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313

1414

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

1920
from .. import logging
2021

@@ -79,14 +80,16 @@ class ComputeMeshWarp(BaseInterface):
7980
output_spec = ComputeMeshWarpOutputSpec
8081

8182
def _triangle_area(self, A, B, C):
82-
ABxAC = euclidean(A, B) * euclidean(A, C)
83-
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 = norm(A - B) * norm(A - C)
87+
prod = np.dot(B - A, C - A)
8488
angle = np.arccos(prod / ABxAC)
8589
area = 0.5 * ABxAC * np.sin(angle)
8690
return area
8791

8892
def _run_interface(self, runtime):
89-
from numpy import linalg as nla
9093
try:
9194
from tvtk.api import tvtk
9295
except ImportError:
@@ -116,7 +119,7 @@ def _run_interface(self, runtime):
116119
diff = points2 - points1
117120
weights = np.ones(len(diff))
118121

119-
errvector = nla.norm(diff, axis=1)
122+
errvector = norm(diff, axis=1)
120123
if self.inputs.metric == 'sqeuclidean':
121124
errvector = errvector ** 2
122125

@@ -147,7 +150,6 @@ def _run_interface(self, runtime):
147150
file_name=op.abspath(self.inputs.out_warp))
148151
writer.set_input_data(out_mesh)
149152
writer.write()
150-
#write_data(out_mesh, op.abspath(self.inputs.out_warp))
151153

152154
self._distance = np.average(errvector, weights=weights)
153155
return runtime
@@ -160,6 +162,144 @@ def _list_outputs(self):
160162
return outputs
161163

162164

165+
class MeshWarpMathsInputSpec(BaseInterfaceInputSpec):
166+
in_surf = File(exists=True, mandatory=True,
167+
desc=('Input surface in vtk format, with associated warp '
168+
'field as point data (ie. from ComputeMeshWarp'))
169+
float_trait = traits.Either(traits.Float(1.0), traits.Tuple(
170+
traits.Float(1.0), traits.Float(1.0), traits.Float(1.0)))
171+
172+
operator = traits.Either(
173+
float_trait, File(exists=True), default=1.0, mandatory=True,
174+
desc=('image, float or tuple of floats to act as operator'))
175+
176+
operation = traits.Enum('sum', 'sub', 'mul', 'div', usedefault=True,
177+
desc=('operation to be performed'))
178+
179+
out_warp = File('warp_maths.vtk', usedefault=True,
180+
desc='vtk file based on in_surf and warpings mapping it '
181+
'to out_file')
182+
out_file = File('warped_surf.vtk', usedefault=True,
183+
desc='vtk with surface warped')
184+
185+
186+
class MeshWarpMathsOutputSpec(TraitedSpec):
187+
out_warp = File(exists=True, desc=('vtk file with the vertex-wise '
188+
'mapping of surface1 to surface2'))
189+
out_file = File(exists=True,
190+
desc='vtk with surface warped')
191+
192+
193+
class MeshWarpMaths(BaseInterface):
194+
195+
"""
196+
Performs the most basic mathematical operations on the warping field
197+
defined at each vertex of the input surface. A surface with scalar
198+
or vector data can be used as operator for non-uniform operations.
199+
200+
.. warning:
201+
202+
A point-to-point correspondence between surfaces is required
203+
204+
205+
Example
206+
-------
207+
208+
>>> import nipype.algorithms.mesh as m
209+
>>> mmath = m.MeshWarpMaths()
210+
>>> mmath.inputs.in_surf = 'surf1.vtk'
211+
>>> mmath.inputs.operator = 'surf2.vtk'
212+
>>> mmath.inputs.operation = 'mul'
213+
>>> res = mmath.run() # doctest: +SKIP
214+
215+
"""
216+
217+
input_spec = MeshWarpMathsInputSpec
218+
output_spec = MeshWarpMathsOutputSpec
219+
220+
def _run_interface(self, runtime):
221+
try:
222+
from tvtk.api import tvtk
223+
except ImportError:
224+
raise ImportError('Interface ComputeMeshWarp requires tvtk')
225+
226+
try:
227+
from enthought.etsconfig.api import ETSConfig
228+
ETSConfig.toolkit = 'null'
229+
except ImportError:
230+
iflogger.warn(('ETS toolkit could not be imported'))
231+
pass
232+
except ValueError:
233+
iflogger.warn(('ETS toolkit is already set'))
234+
pass
235+
236+
r1 = tvtk.PolyDataReader(file_name=self.inputs.in_surf)
237+
vtk1 = r1.output
238+
r1.update()
239+
points1 = np.array(vtk1.points)
240+
241+
if vtk1.point_data.vectors is None:
242+
raise RuntimeError(('No warping field was found in in_surf'))
243+
244+
operator = self.inputs.operator
245+
opfield = np.ones_like(points1)
246+
247+
if isinstance(operator, six.string_types):
248+
r2 = tvtk.PolyDataReader(file_name=self.inputs.surface2)
249+
vtk2 = r2.output
250+
r2.update()
251+
assert(len(points1) == len(vtk2.points))
252+
253+
opfield = vtk2.point_data.vectors
254+
255+
if opfield is None:
256+
opfield = vtk2.point_data.scalars
257+
258+
if opfield is None:
259+
raise RuntimeError(
260+
('No operator values found in operator file'))
261+
262+
opfield = np.array(opfield)
263+
264+
if opfield.shape[1] < points1.shape[1]:
265+
opfield = np.array([opfield.tolist()] * points1.shape[1]).T
266+
else:
267+
operator = np.atleast_1d(operator)
268+
opfield *= operator
269+
270+
warping = np.array(vtk1.point_data.vectors)
271+
272+
if self.inputs.operation == 'sum':
273+
warping += opfield
274+
elif self.inputs.operation == 'sub':
275+
warping -= opfield
276+
elif self.inputs.operation == 'mul':
277+
warping *= opfield
278+
elif self.inputs.operation == 'div':
279+
warping /= opfield
280+
281+
vtk1.point_data.vectors = warping
282+
writer = tvtk.PolyDataWriter(
283+
file_name=op.abspath(self.inputs.out_warp))
284+
writer.set_input_data(vtk1)
285+
writer.write()
286+
287+
vtk1.point_data.vectors = None
288+
vtk1.points = points1 + warping
289+
writer = tvtk.PolyDataWriter(
290+
file_name=op.abspath(self.inputs.out_file))
291+
writer.set_input_data(vtk1)
292+
writer.write()
293+
294+
return runtime
295+
296+
def _list_outputs(self):
297+
outputs = self._outputs().get()
298+
outputs['out_file'] = op.abspath(self.inputs.out_file)
299+
outputs['out_warp'] = op.abspath(self.inputs.out_warp)
300+
return outputs
301+
302+
163303
class P2PDistance(ComputeMeshWarp):
164304

165305
"""
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# AUTO-GENERATED by tools/checkspecs.py - DO NOT EDIT
2+
from nipype.testing import assert_equal
3+
from nipype.algorithms.mesh import MeshWarpMaths
4+
5+
def test_MeshWarpMaths_inputs():
6+
input_map = dict(float_trait=dict(),
7+
ignore_exception=dict(nohash=True,
8+
usedefault=True,
9+
),
10+
in_surf=dict(mandatory=True,
11+
),
12+
operation=dict(usedefault=True,
13+
),
14+
operator=dict(mandatory=True,
15+
),
16+
out_file=dict(usedefault=True,
17+
),
18+
out_warp=dict(usedefault=True,
19+
),
20+
)
21+
inputs = MeshWarpMaths.input_spec()
22+
23+
for key, metadata in input_map.items():
24+
for metakey, value in metadata.items():
25+
yield assert_equal, getattr(inputs.traits()[key], metakey), value
26+
27+
def test_MeshWarpMaths_outputs():
28+
output_map = dict(out_file=dict(),
29+
out_warp=dict(),
30+
)
31+
outputs = MeshWarpMaths.output_spec()
32+
33+
for key, metadata in output_map.items():
34+
for metakey, value in metadata.items():
35+
yield assert_equal, getattr(outputs.traits()[key], metakey), value
36+

nipype/algorithms/tests/test_mesh_ops.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from shutil import rmtree
77
from tempfile import mkdtemp
88

9-
from nipype.testing import (assert_equal, assert_raises,
9+
from nipype.testing import (assert_equal, assert_raises, skipif,
1010
assert_almost_equal, example_data)
1111

1212
import numpy as np
@@ -34,11 +34,11 @@ def test_ident_distances():
3434
dist_ident.inputs.surface2 = in_surf
3535
dist_ident.inputs.out_file = os.path.join(tempdir, 'distance.npy')
3636
res = dist_ident.run()
37-
yield assert_equal, res.outputs.distance == 0.0
37+
yield assert_equal, res.outputs.distance, 0.0
3838

3939
dist_ident.inputs.weighting = 'area'
4040
res = dist_ident.run()
41-
yield assert_equal, res.outputs.distance == 0.0
41+
yield assert_equal, res.outputs.distance, 0.0
4242

4343
os.chdir(curdir)
4444
rmtree(tempdir)
@@ -68,10 +68,10 @@ def test_trans_distances():
6868
dist.inputs.surface2 = warped_surf
6969
dist.inputs.out_file = os.path.join(tempdir, 'distance.npy')
7070
res = dist.run()
71-
yield assert_equal, np.isclose(res.outputs.distance, np.norm(inc))
72-
dist_ident.inputs.weighting = 'area'
73-
res = dist_ident.run()
74-
yield assert_equal, np.isclose(res.outputs.distance, np.norm(inc))
71+
yield assert_almost_equal, res.outputs.distance, np.linalg.norm(inc), 4
72+
dist.inputs.weighting = 'area'
73+
res = dist.run()
74+
yield assert_almost_equal, res.outputs.distance, np.linalg.norm(inc), 4
7575

7676
os.chdir(curdir)
7777
rmtree(tempdir)

0 commit comments

Comments
 (0)