Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9294190

Browse files
eric-wieserStephen-Chilcote
authored andcommitted
MAINT: Unify calculation of normal vectors from polygons (#12136)
This combines `get_normals` and `_generate_normals`, and eliminates all other calls to np.cross. `get_normals` and `_generate_normals` were profiled, and it was found that vectorizing `np.cross` like in `get_normals` was faster: ```python import numpy as np def get_normals(polygons): v1 = np.empty((len(polygons), 3)) v2 = np.empty((len(polygons), 3)) for poly_i, ps in enumerate(polygons): # pick three points around the polygon at which to find the # normal doesn't vectorize because polygons is jagged i1, i2, i3 = 0, len(ps)//3, 2*len(ps)//3 v1[poly_i, :] = ps[i1, :] - ps[i2, :] v2[poly_i, :] = ps[i2, :] - ps[i3, :] return np.cross(v1, v2) def _generate_normals(self, polygons): normals = [] for verts in polygons: v1 = np.array(verts[0]) - np.array(verts[1]) v2 = np.array(verts[2]) - np.array(verts[0]) normals.append(np.cross(v1, v2)) return np.array(normals) polygons = [ np.random.rand(np.random.randint(10, 1000), 3) for i in range(100) ] %timeit _generate_normals(polygons) # 3.14 ms ± 255 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) %timeit get_normals(polygons) # 452 µs ± 4.33 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each) ```
1 parent 46dc108 commit 9294190

File tree

1 file changed

+43
-32
lines changed

1 file changed

+43
-32
lines changed

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,28 +1684,14 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
16841684
if fcolors is not None:
16851685
colset.append(fcolors[rs][cs])
16861686

1687-
def get_normals(polygons):
1688-
"""
1689-
Takes a list of polygons and return an array of their normals
1690-
"""
1691-
v1 = np.empty((len(polygons), 3))
1692-
v2 = np.empty((len(polygons), 3))
1693-
for poly_i, ps in enumerate(polygons):
1694-
# pick three points around the polygon at which to find the
1695-
# normal doesn't vectorize because polygons is jagged
1696-
i1, i2, i3 = 0, len(ps)//3, 2*len(ps)//3
1697-
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1698-
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1699-
return np.cross(v1, v2)
1700-
17011687
# note that the striding causes some polygons to have more coordinates
17021688
# than others
17031689
polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
17041690

17051691
if fcolors is not None:
17061692
if shade:
17071693
colset = self._shade_colors(
1708-
colset, get_normals(polys), lightsource)
1694+
colset, self._generate_normals(polys), lightsource)
17091695
polyc.set_facecolors(colset)
17101696
polyc.set_edgecolors(colset)
17111697
elif cmap:
@@ -1719,7 +1705,7 @@ def get_normals(polygons):
17191705
else:
17201706
if shade:
17211707
colset = self._shade_colors(
1722-
color, get_normals(polys), lightsource)
1708+
color, self._generate_normals(polys), lightsource)
17231709
else:
17241710
colset = color
17251711
polyc.set_facecolors(colset)
@@ -1731,20 +1717,47 @@ def get_normals(polygons):
17311717

17321718
def _generate_normals(self, polygons):
17331719
"""
1734-
Generate normals for polygons by using the first three points.
1735-
This normal of course might not make sense for polygons with
1736-
more than three points not lying in a plane.
1720+
Takes a list of polygons and return an array of their normals.
17371721
17381722
Normals point towards the viewer for a face with its vertices in
17391723
counterclockwise order, following the right hand rule.
1740-
"""
17411724
1742-
normals = []
1743-
for verts in polygons:
1744-
v1 = np.array(verts[1]) - np.array(verts[0])
1745-
v2 = np.array(verts[2]) - np.array(verts[0])
1746-
normals.append(np.cross(v1, v2))
1747-
return normals
1725+
Uses three points equally spaced around the polygon.
1726+
This normal of course might not make sense for polygons with more than
1727+
three points not lying in a plane, but it's a plausible and fast
1728+
approximation.
1729+
1730+
Parameters
1731+
----------
1732+
polygons: list of (M_i, 3) array_like, or (..., M, 3) array_like
1733+
A sequence of polygons to compute normals for, which can have
1734+
varying numbers of vertices. If the polygons all have the same
1735+
number of vertices and array is passed, then the operation will
1736+
be vectorized.
1737+
1738+
Returns
1739+
-------
1740+
normals: (..., 3) array_like
1741+
A normal vector estimated for the polygon.
1742+
1743+
"""
1744+
if isinstance(polygons, np.ndarray):
1745+
# optimization: polygons all have the same number of points, so can
1746+
# vectorize
1747+
n = polygons.shape[-2]
1748+
i1, i2, i3 = 0, n//3, 2*n//3
1749+
v1 = polygons[..., i1, :] - polygons[..., i2, :]
1750+
v2 = polygons[..., i2, :] - polygons[..., i3, :]
1751+
else:
1752+
# The subtraction doesn't vectorize because polygons is jagged.
1753+
v1 = np.empty((len(polygons), 3))
1754+
v2 = np.empty((len(polygons), 3))
1755+
for poly_i, ps in enumerate(polygons):
1756+
n = len(ps)
1757+
i1, i2, i3 = 0, n//3, 2*n//3
1758+
v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1759+
v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1760+
return np.cross(v1, v2)
17481761

17491762
def _shade_colors(self, color, normals, lightsource=None):
17501763
"""
@@ -1991,9 +2004,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
19912004
polyc.set_norm(norm)
19922005
else:
19932006
if shade:
1994-
v1 = verts[:, 0, :] - verts[:, 1, :]
1995-
v2 = verts[:, 1, :] - verts[:, 2, :]
1996-
normals = np.cross(v1, v2)
2007+
normals = self._generate_normals(verts)
19972008
colset = self._shade_colors(color, normals, lightsource)
19982009
else:
19992010
colset = color
@@ -2040,9 +2051,9 @@ def _3d_extend_contour(self, cset, stride=5):
20402051
botverts[0][i2],
20412052
botverts[0][i1]])
20422053

2043-
v1 = np.array(topverts[0][i1]) - np.array(topverts[0][i2])
2044-
v2 = np.array(topverts[0][i1]) - np.array(botverts[0][i1])
2045-
normals.append(np.cross(v1, v2))
2054+
# all polygons have 4 vertices, so vectorize
2055+
polyverts = np.array(polyverts)
2056+
normals = self._generate_normals(polyverts)
20462057

20472058
colors = self._shade_colors(color, normals)
20482059
colors2 = self._shade_colors(color, normals)

0 commit comments

Comments
 (0)