Skip to content
This repository was archived by the owner on Feb 3, 2023. It is now read-only.

Commit a295a58

Browse files
authored
Speedup mesh_xsection more (#6)
Speedup of mesh xsection.
1 parent 122159c commit a295a58

File tree

3 files changed

+125
-89
lines changed

3 files changed

+125
-89
lines changed

blmath/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '1.2.0'
1+
__version__ = '1.2.1'

blmath/geometry/primitives/plane.py

+118-88
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ def __init__(self, point_on_plane, unit_normal):
1616

1717
unit_normal = vx.normalize(unit_normal)
1818

19-
self._r0 = point_on_plane
20-
self._n = unit_normal
19+
self._r0 = np.asarray(point_on_plane)
20+
self._n = np.asarray(unit_normal)
2121

2222
def __repr__(self):
2323
return "<Plane of {} through {}>".format(self.normal, self.reference_point)
@@ -127,7 +127,7 @@ def normal(self):
127127
Return the plane's normal vector.
128128
129129
'''
130-
return np.array(self._n)
130+
return self._n
131131

132132
def flipped(self):
133133
'''
@@ -210,25 +210,22 @@ def polyline_xsection(self, polyline):
210210
return intersection_points
211211

212212
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
213+
return self._line_xsection(np.asarray(pt).ravel(), np.asarray(ray).ravel())
214+
215+
def _line_xsection(self, pt, ray):
217216
denom = np.dot(ray, self.normal)
218217
if denom == 0:
219218
return None # parallel, either coplanar or non-intersecting
220219
p = np.dot(self.reference_point - pt, self.normal) / denom
221220
return p * ray + pt
222221

223222
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
223+
return self._line_segment_xsection(np.asarray(a).ravel(), np.asarray(b).ravel())
228224

229-
pt = self.line_xsection(a, b-a)
225+
def _line_segment_xsection(self, a, b):
226+
pt = self._line_xsection(a, b-a)
230227
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)):
228+
if any(np.logical_and(pt > a, pt > b)) or any(np.logical_and(pt < a, pt < b)):
232229
return None
233230
return pt
234231

@@ -245,6 +242,11 @@ def mesh_xsection(self, m, neighborhood=None):
245242
return Polyline(None)
246243
return Polyline(np.vstack([x.v for x in components]), closed=True)
247244

245+
def mesh_intersecting_faces(self, m):
246+
sgn_dists = self.signed_distance(m.v)
247+
which_fs = np.abs(np.sign(sgn_dists)[m.f].sum(axis=1)) != 3
248+
return m.f[which_fs]
249+
248250
def mesh_xsections(self, m, neighborhood=None):
249251
'''
250252
Takes a cross section of planar point cloud with a Mesh object.
@@ -262,103 +264,131 @@ def mesh_xsections(self, m, neighborhood=None):
262264
263265
Returns a list of Polylines.
264266
'''
265-
import operator
266267
from blmath.geometry import Polyline
267268

268269
# 1: Select those faces that intersect the plane, fs
269-
sgn_dists = self.signed_distance(m.v)
270-
which_fs = np.abs(np.sign(sgn_dists)[m.f].sum(axis=1)) != 3
271-
fs = m.f[which_fs]
272-
270+
fs = self.mesh_intersecting_faces(m)
273271
if len(fs) == 0:
274272
return [] # Nothing intersects
273+
# and edges of those faces
274+
es = np.vstack((fs[:, (0, 1)], fs[:, (1, 2)], fs[:, (2, 0)]))
275275

276276
# 2: Find the edges where each of those faces actually cross the plane
277-
def edge_from_face(f):
278-
face_verts = [
279-
[m.v[f[0]], m.v[f[1]]],
280-
[m.v[f[1]], m.v[f[2]]],
281-
[m.v[f[2]], m.v[f[0]]],
282-
]
283-
e = [self.line_segment_xsection(a, b) for a, b in face_verts]
284-
e = [x for x in e if x is not None]
285-
return e
286-
edges = np.vstack([np.hstack(edge_from_face(f)) for f in fs])
287-
288-
# 3: Find the set of unique vertices in `edges`
289-
v1s, v2s = np.hsplit(edges, 2)
290-
verts = edges.reshape((-1, 3))
291-
verts = np.vstack(sorted(verts, key=operator.itemgetter(0, 1, 2)))
292-
eps = 1e-15 # the floating point calculation of the intersection locations is not _quite_ exact
293-
verts = verts[list(np.sqrt(np.sum(np.diff(verts, axis=0) ** 2, axis=1)) > eps) + [True]]
294-
# the True at the end there is because np.diff returns pairwise differences; one less element than the original array
277+
class EdgeMap(object):
278+
# A quick two level dictionary where the two keys are interchangeable (i.e. a symmetric graph)
279+
def __init__(self):
280+
self.d = {} # store indicies into self.values here, to make it easier to get inds or values
281+
self.values = []
282+
def _order(self, u, v):
283+
if u < v:
284+
return u, v
285+
else:
286+
return v, u
287+
def add(self, u, v, val):
288+
low, high = self._order(u, v)
289+
if low not in self.d:
290+
self.d[low] = {}
291+
self.values.append(val)
292+
self.d[low][high] = len(self.values) - 1
293+
def contains(self, u, v):
294+
low, high = self._order(u, v)
295+
if low in self.d and high in self.d[low]:
296+
return True
297+
return False
298+
def index(self, u, v):
299+
low, high = self._order(u, v)
300+
try:
301+
return self.d[low][high]
302+
except KeyError:
303+
return None
304+
def get(self, u, v):
305+
ii = self.index(u, v)
306+
if ii is not None:
307+
return self.values[ii]
308+
else:
309+
return None
310+
311+
intersection_map = EdgeMap()
312+
for e in es:
313+
if not intersection_map.contains(e[0], e[1]):
314+
val = self._line_segment_xsection(m.v[e[0]], m.v[e[1]])
315+
if val is not None:
316+
intersection_map.add(e[0], e[1], val)
317+
verts = np.array(intersection_map.values)
295318

296319
class Graph(object):
297-
# A little utility class to build a symmetric graph
320+
# A little utility class to build a symmetric graph and calculate Euler Paths
298321
def __init__(self, size):
299322
self.size = size
300323
self.d = {}
301324
def __len__(self):
302325
return len(self.d)
303-
def add_edge(self, ii, jj):
304-
assert ii >= 0 and ii < self.size
305-
assert jj >= 0 and jj < self.size
306-
if ii not in self.d:
307-
self.d[ii] = set()
308-
if jj not in self.d:
309-
self.d[jj] = set()
310-
self.d[ii].add(jj)
311-
self.d[jj].add(ii)
326+
def add_edges(self, edges):
327+
for u, v in edges:
328+
self.add_edge(u, v)
329+
def add_edge(self, u, v):
330+
assert u >= 0 and u < self.size
331+
assert v >= 0 and v < self.size
332+
if u not in self.d:
333+
self.d[u] = set()
334+
if v not in self.d:
335+
self.d[v] = set()
336+
self.d[u].add(v)
337+
self.d[v].add(u)
338+
def remove_edge(self, u, v):
339+
if u in self.d and v in self.d[u]:
340+
self.d[u].remove(v)
341+
if v in self.d and u in self.d[v]:
342+
self.d[v].remove(u)
343+
if v in self.d and len(self.d[v]) == 0:
344+
del self.d[v]
345+
if u in self.d and len(self.d[u]) == 0:
346+
del self.d[u]
347+
def pop_euler_path(self, allow_multiple_connected_components=True):
348+
# Based on code from Przemek Drochomirecki, Krakow, 5 Nov 2006
349+
# http://code.activestate.com/recipes/498243-finding-eulerian-path-in-undirected-graph/
350+
# Under PSF License
351+
# NB: MUTATES d
352+
353+
# counting the number of vertices with odd degree
354+
odd = [x for x in self.d if len(self.d[x])&1]
355+
odd.append(self.d.keys()[0])
356+
if not allow_multiple_connected_components and len(odd) > 3:
357+
return None
358+
stack = [odd[0]]
359+
path = []
360+
# main algorithm
361+
while stack:
362+
v = stack[-1]
363+
if v in self.d:
364+
u = self.d[v].pop()
365+
stack.append(u)
366+
self.remove_edge(u, v)
367+
else:
368+
path.append(stack.pop())
369+
return path
312370

313371
# 4: Build the edge adjacency graph
314372
G = Graph(verts.shape[0])
315-
def indexof(v, in_this):
316-
return np.nonzero(np.all(np.abs(in_this - v) < eps, axis=1))[0]
317-
for ii, v in enumerate(verts):
318-
for other_v in list(v2s[indexof(v, v1s)]) + list(v1s[indexof(v, v2s)]):
319-
neighbors = indexof(other_v, verts)
320-
for jj in neighbors:
321-
G.add_edge(ii, jj)
322-
323-
def euler_path(graph):
324-
# Based on code from Przemek Drochomirecki, Krakow, 5 Nov 2006
325-
# http://code.activestate.com/recipes/498243-finding-eulerian-path-in-undirected-graph/
326-
# Under PSF License
327-
# NB: MUTATES graph
328-
329-
# counting the number of vertices with odd degree
330-
odd = [x for x in graph.keys() if len(graph[x])&1]
331-
odd.append(graph.keys()[0])
332-
# This check is appropriate if there is a single connected component.
333-
# Since we're willing to take away one connected component per call,
334-
# we skip this check.
335-
# if len(odd)>3:
336-
# return None
337-
stack = [odd[0]]
338-
path = []
339-
# main algorithm
340-
while stack:
341-
v = stack[-1]
342-
if v in graph:
343-
u = graph[v].pop()
344-
stack.append(u)
345-
# deleting edge u-v (v-u already removed by pop)
346-
graph[u].remove(v)
347-
# graph[v].remove(u)
348-
if len(graph[v]) == 0:
349-
del graph[v]
350-
if len(graph[u]) == 0:
351-
del graph[u]
352-
else:
353-
path.append(stack.pop())
354-
return path
373+
for f in fs:
374+
# Since we're dealing with a triangle that intersects the plane, exactly two of the edges
375+
# will intersect (note that the only other sorts of "intersections" are one edge in
376+
# plane or all three edges in plane, which won't be picked up by mesh_intersecting_faces).
377+
e0 = intersection_map.index(f[0], f[1])
378+
e1 = intersection_map.index(f[0], f[2])
379+
e2 = intersection_map.index(f[1], f[2])
380+
if e0 is None:
381+
G.add_edge(e1, e2)
382+
elif e1 is None:
383+
G.add_edge(e0, e2)
384+
else:
385+
G.add_edge(e0, e1)
355386

356387
# 5: Find the paths for each component
357388
components = []
358389
components_closed = []
359390
while len(G) > 0:
360-
# This works because euler_path mutates the graph as it goes
361-
path = euler_path(G.d)
391+
path = G.pop_euler_path()
362392
if path is None:
363393
raise ValueError("mesh slice has too many odd degree edges; can't find a path along the edge")
364394
component_verts = verts[path]

blmath/geometry/primitives/test_plane.py

+6
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,9 @@ def test_mesh_plane_intersection(self):
264264

265265
plane = Plane(sample, normal)
266266

267+
# Verify that we're finding the correct number of faces to start with
268+
self.assertEqual(len(plane.mesh_intersecting_faces(self.box_mesh)), 8)
269+
267270
xsections = plane.mesh_xsections(self.box_mesh)
268271
self.assertIsInstance(xsections, list)
269272
self.assertEqual(len(xsections), 1)
@@ -288,6 +291,9 @@ def test_mesh_plane_intersection_with_no_intersection(self):
288291

289292
plane = Plane(sample, normal)
290293

294+
# Verify that we're detecting faces that lay entirely in the plane as potential intersections
295+
self.assertEqual(len(plane.mesh_intersecting_faces(self.box_mesh)), 0)
296+
291297
xsections = plane.mesh_xsections(self.box_mesh)
292298
self.assertIsInstance(xsections, list)
293299
self.assertEqual(len(xsections), 0)

0 commit comments

Comments
 (0)