@@ -19,6 +19,9 @@ def __init__(self, point_on_plane, unit_normal):
1919 self ._r0 = point_on_plane
2020 self ._n = unit_normal
2121
22+ def __repr__ (self ):
23+ return "<Plane of {} through {}>" .format (self .normal , self .reference_point )
24+
2225 @classmethod
2326 def from_points (cls , p1 , p2 , p3 ):
2427 '''
@@ -206,7 +209,43 @@ def polyline_xsection(self, polyline):
206209 #assert(np.all(self.distance(intersection_points) < 1e-10))
207210 return intersection_points
208211
212+ def line_xsection (self , pt , ray ):
213+ pt = np .asarray (pt ).ravel ()
214+ ray = np .asarray (ray ).ravel ()
215+ assert len (pt ) == 3
216+ assert len (ray ) == 3
217+ denom = np .dot (ray , self .normal )
218+ if denom == 0 :
219+ return None # parallel, either coplanar or non-intersecting
220+ p = np .dot (self .reference_point - pt , self .normal ) / denom
221+ return p * ray + pt
222+
223+ def line_segment_xsection (self , a , b ):
224+ a = np .asarray (a ).ravel ()
225+ b = np .asarray (b ).ravel ()
226+ assert len (a ) == 3
227+ assert len (b ) == 3
228+
229+ pt = self .line_xsection (a , b - a )
230+ if pt is not None :
231+ if np .any (pt < np .min ((a , b ), axis = 0 )) or np .any (pt > np .max ((a , b ), axis = 0 )):
232+ return None
233+ return pt
234+
209235 def mesh_xsection (self , m , neighborhood = None ):
236+ '''
237+ Backwards compatible.
238+ Returns one polyline that may connect supposedly disconnected components.
239+ Returns an empty Polyline if there's no intersection.
240+ '''
241+ from blmath .geometry import Polyline
242+
243+ components = self .mesh_xsections (m , neighborhood )
244+ if len (components ) == 0 :
245+ return Polyline (None )
246+ return Polyline (np .vstack ([x .v for x in components ]), closed = True )
247+
248+ def mesh_xsections (self , m , neighborhood = None ):
210249 '''
211250 Takes a cross section of planar point cloud with a Mesh object.
212251 Ignore those points which intersect at a vertex - the probability of
@@ -221,157 +260,111 @@ def mesh_xsection(self, m, neighborhood=None):
221260 - neigbhorhood:
222261 M x 3 np.array
223262
224- Returns a Polyline.
225-
226- TODO Return `None` instead of an empty polyline to signal no
227- intersection.
228-
263+ Returns a list of Polylines.
229264 '''
265+ import operator
266+ import scipy .sparse as sp
230267 from blmath .geometry import Polyline
231268
232- # Step 1:
233- # Select those faces that intersect the plane, fs. Also construct
234- # the signed distances (fs_dists) and normalized signed distances
235- # (fs_norm_dists) for each such face.
269+ # 1: Select those faces that intersect the plane, fs
236270 sgn_dists = self .signed_distance (m .v )
237271 which_fs = np .abs (np .sign (sgn_dists )[m .f ].sum (axis = 1 )) != 3
238272 fs = m .f [which_fs ]
239- fs_dists = sgn_dists [fs ]
240- fs_norm_dists = np .sign (fs_dists )
241-
242- # Step 2:
243- # Build a length 3 array of edges es. Each es[i] is an np.array
244- # edge_pts of shape (fs.shape[0], 3). Each vector edge_pts[i, :]
245- # in edge_pts is an interesection of the plane with the
246- # fs[i], or [np.nan, np.nan, np.nan].
247- es = []
248-
249- import itertools
250- for i , j in itertools .combinations ([0 , 1 , 2 ], 2 ):
251- vi = m .v [fs [:, i ]]
252- vj = m .v [fs [:, j ]]
253-
254- vi_dist = np .absolute (fs_dists [:, i ])
255- vj_dist = np .absolute (fs_dists [:, j ])
256-
257- vi_norm_dist = fs_norm_dists [:, i ]
258- vj_norm_dist = fs_norm_dists [:, j ]
259-
260- # use broadcasting to bulk traverse the edges
261- t = vi_dist / (vi_dist + vj_dist )
262- t = t [:, np .newaxis ]
263-
264- edge_pts = t * vj + (1 - t ) * vi
265-
266- # flag interior edge points that have the same sign with [nan, nan, nan].
267- # note also that sum(trash.shape[0] for all i, j) == fs.shape[0], which holds.
268- trash = np .nonzero (vi_norm_dist * vj_norm_dist == + 1 )[0 ]
269- edge_pts [trash , :] = np .nan
270-
271- es .append (edge_pts )
272-
273- if any ([edge .shape [0 ] == 0 for edge in es ]):
274- return Polyline (None )
275273
276- # Step 3:
277- # Build and return the verts v and edges e. Dump trash.
278- hstacked = np .hstack (es )
279- trash = np .isnan (hstacked )
280-
281- cleaned = hstacked [np .logical_not (trash )].reshape (fs .shape [0 ], 6 )
282- v1s , v2s = np .hsplit (cleaned , 2 )
283-
284- v = np .empty ((2 * v1s .shape [0 ], 3 ), dtype = v1s .dtype )
285- v [0 ::2 ] = v1s
286- v [1 ::2 ] = v2s
287-
288- if neighborhood is None :
289- return Polyline (v , closed = True )
290-
291- # FIXME This e is incorrect.
292- # Contains e.g.
293- # [0, 1], [2, 3], [4, 5], ...
294- # But should contain
295- # [0, 1], [1, 2], [2, 3], ...
296- # Leaving in place since the code below may depend on it.
297- e = np .array ([[i , i + 1 ] for i in xrange (0 , v .shape [0 ], 2 )])
298-
299- # Step 4 (optional - only if 'neighborhood' is provided):
300- # Build and return the ordered vertices cmp_v, and the
301- # edges cmp_e. Get connected components, use a KDTree
302- # to select the one with minimal distance to 'component'.
303- # Return the cmp_v and (re-indexed) edge mapping cmp_e.
274+ if len (fs ) == 0 :
275+ return [] # Nothing intersects
276+
277+ # 2: Find the edges where each of those faces actually cross the plane
278+ def edge_from_face (f ):
279+ face_verts = [
280+ [m .v [f [0 ]], m .v [f [1 ]]],
281+ [m .v [f [1 ]], m .v [f [2 ]]],
282+ [m .v [f [2 ]], m .v [f [0 ]]],
283+ ]
284+ e = [self .line_segment_xsection (a , b ) for a , b in face_verts ]
285+ e = [x for x in e if x is not None ]
286+ return e
287+ edges = np .vstack ([np .hstack (edge_from_face (f )) for f in fs ])
288+
289+ # 3: Find the set of unique vertices in `edges`
290+ v1s , v2s = np .hsplit (edges , 2 )
291+ verts = edges .reshape ((- 1 , 3 ))
292+ verts = np .vstack (sorted (verts , key = operator .itemgetter (0 , 1 , 2 )))
293+ eps = 1e-15 # the floating point calculation of the intersection locations is not _quite_ exact
294+ verts = verts [list (np .sqrt (np .sum (np .diff (verts , axis = 0 ) ** 2 , axis = 1 )) > eps ) + [True ]]
295+ # the True at the end there is because np.diff returns pairwise differences; one less element than the original array
296+
297+ # 4: Build the edge adjacency matrix
298+ E = sp .dok_matrix ((verts .shape [0 ], verts .shape [0 ]), dtype = np .bool )
299+ def indexof (v , in_this ):
300+ return np .nonzero (np .all (np .abs (in_this - v ) < eps , axis = 1 ))[0 ]
301+ for ii , v in enumerate (verts ):
302+ for other_v in list (v2s [indexof (v , v1s )]) + list (v1s [indexof (v , v2s )]):
303+ neighbors = indexof (other_v , verts )
304+ E [ii , neighbors ] = True
305+ E [neighbors , ii ] = True
306+
307+ def eulerPath (E ):
308+ # Based on code from Przemek Drochomirecki, Krakow, 5 Nov 2006
309+ # http://code.activestate.com/recipes/498243-finding-eulerian-path-in-undirected-graph/
310+ # Under PSF License
311+ # NB: MUTATES graph
312+ if len (E .nonzero ()[0 ]) == 0 :
313+ return None
314+ # counting the number of vertices with odd degree
315+ odd = list (np .nonzero (np .bitwise_and (np .sum (E , axis = 0 ), 1 ))[1 ])
316+ odd .append (np .nonzero (E )[0 ][0 ])
317+ # This check is appropriate if there is a single connected component.
318+ # Since we're willing to take away one connected component per call,
319+ # we skip this check.
320+ # if len(odd) > 3:
321+ # return None
322+ stack = [odd [0 ]]
323+ path = []
324+ # main algorithm
325+ while stack :
326+ v = stack [- 1 ]
327+ nonzero = np .nonzero (E )
328+ nbrs = nonzero [1 ][nonzero [0 ] == v ]
329+ if len (nbrs ) > 0 :
330+ u = nbrs [0 ]
331+ stack .append (u )
332+ # deleting edge u-v
333+ E [u , v ] = False
334+ E [v , u ] = False
335+ else :
336+ path .append (stack .pop ())
337+ return path
338+
339+ # 5: Find the paths for each component
340+ components = []
341+ components_closed = []
342+ while len (E .nonzero ()[0 ]) > 0 :
343+ # This works because eulerPath mutates the graph as it goes
344+ path = eulerPath (E )
345+ if path is None :
346+ raise ValueError ("mesh slice has too many odd degree edges; can't find a path along the edge" )
347+ component_verts = verts [path ]
348+
349+ if np .all (component_verts [0 ] == component_verts [- 1 ]):
350+ # Because the closed polyline will make that last link:
351+ component_verts = np .delete (component_verts , 0 , axis = 0 )
352+ components_closed .append (True )
353+ else :
354+ components_closed .append (False )
355+ components .append (component_verts )
356+
357+ if neighborhood is None or len (components ) == 1 :
358+ return [Polyline (v , closed = closed ) for v , closed in zip (components , components_closed )]
359+
360+ # 6 (optional - only if 'neighborhood' is provided): Use a KDTree to select the component with minimal distance to 'neighborhood'
304361 from scipy .spatial import cKDTree # First thought this warning was caused by a pythonpath problem, but it seems more likely that the warning is caused by scipy import hackery. pylint: disable=no-name-in-module
305- from scipy .sparse import csc_matrix
306- from scipy .sparse .csgraph import connected_components
307-
308- from bodylabs .mesh .topology .connectivity import remove_redundant_verts
309-
310- # get rid of redundancies, or we
311- # overcount connected components
312- v , e = remove_redundant_verts (v , e )
313-
314- # connxns:
315- # sparse matrix of connected components.
316- # ij:
317- # edges transposed
318- # (connected_components needs these.)
319- ij = np .vstack ((
320- e [:, 0 ].reshape (1 , e .shape [0 ]),
321- e [:, 1 ].reshape (1 , e .shape [0 ]),
322- ))
323- connxns = csc_matrix ((np .ones (len (e )), ij ), shape = (len (v ), len (v )))
324-
325- cmp_N , cmp_labels = connected_components (connxns )
326-
327- if cmp_N == 1 :
328- # no work to do, bail
329- polyline = Polyline (v , closed = True )
330- # This function used to return (v, e), so we include this
331- # sanity check to make sure the edges match what Polyline uses.
332- # np.testing.assert_all_equal(polyline.e, e)
333- # Hmm, this fails.
334- return polyline
335-
336- cmps = np .array ([
337- v [np .where (cmp_labels == cmp_i )]
338- for cmp_i in range (cmp_N )
339- ])
340362
341363 kdtree = cKDTree (neighborhood )
342364
343- # cmp_N will not be large in
344- # practice, so this loop won't hurt
345- means = np .array ([
346- np .mean (kdtree .query (cmps [cmp_i ])[0 ])
347- for cmp_i in range (cmp_N )
348- ])
349-
350- which_cmp = np .where (means == np .min (means ))[0 ][0 ]
351-
352- # re-index edge mapping based on which_cmp. necessary
353- # particularly when which_cmp is not contiguous in cmp_labels.
354- which_vs = np .where (cmp_labels == which_cmp )[0 ]
355- # which_es = np.logical_or(
356- # np.in1d(e[:, 0], which_vs),
357- # np.in1d(e[:, 1], which_vs),
358- # )
359-
360- vmap = cmp_labels .astype (float )
361- vmap [cmp_labels != which_cmp ] = np .nan
362- vmap [cmp_labels == which_cmp ] = np .arange (which_vs .size )
363-
364- cmp_v = v [which_vs ] # equivalently, cmp_v = cmp[which_cmp]
365- # cmp_e = vmap[e[which_es]].astype(int)
366-
367- polyline = Polyline (cmp_v , closed = True )
368- # This function used to return (cmp_v, cmp_e), so we include this
369- # sanity check to make sure the edges match what Polyline uses.
370- # Remove # this, and probably the code which creates 'vmap', when
371- # we're more confident.
372- # Hmm, this fails.
373- # np.testing.assert_all_equal(polyline.e, cmp_e)
374- return polyline
365+ # number of components will not be large in practice, so this loop won't hurt
366+ means = [np .mean (kdtree .query (component )[0 ]) for component in components ]
367+ return [Polyline (components [np .argmin (means )], closed = True )]
375368
376369
377370def main ():
0 commit comments