@@ -19,6 +19,9 @@ def __init__(self, point_on_plane, unit_normal):
19
19
self ._r0 = point_on_plane
20
20
self ._n = unit_normal
21
21
22
+ def __repr__ (self ):
23
+ return "<Plane of {} through {}>" .format (self .normal , self .reference_point )
24
+
22
25
@classmethod
23
26
def from_points (cls , p1 , p2 , p3 ):
24
27
'''
@@ -206,7 +209,43 @@ def polyline_xsection(self, polyline):
206
209
#assert(np.all(self.distance(intersection_points) < 1e-10))
207
210
return intersection_points
208
211
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
+
209
235
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 ):
210
249
'''
211
250
Takes a cross section of planar point cloud with a Mesh object.
212
251
Ignore those points which intersect at a vertex - the probability of
@@ -221,157 +260,111 @@ def mesh_xsection(self, m, neighborhood=None):
221
260
- neigbhorhood:
222
261
M x 3 np.array
223
262
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.
229
264
'''
265
+ import operator
266
+ import scipy .sparse as sp
230
267
from blmath .geometry import Polyline
231
268
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
236
270
sgn_dists = self .signed_distance (m .v )
237
271
which_fs = np .abs (np .sign (sgn_dists )[m .f ].sum (axis = 1 )) != 3
238
272
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 )
275
273
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'
304
361
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
- ])
340
362
341
363
kdtree = cKDTree (neighborhood )
342
364
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 )]
375
368
376
369
377
370
def main ():
0 commit comments