1313
1414
1515import 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
1820from .. import logging
1921
2022from ..interfaces .base import (BaseInterface , traits , TraitedSpec , File ,
2123 BaseInterfaceInputSpec )
24+ from warnings import warn
2225iflogger = 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 )
0 commit comments