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

Skip to content

Commit 5e6e837

Browse files
committed
MAINT: Unify calculation of normal vectors from polygons
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 3e90025 commit 5e6e837

File tree

1 file changed

+44
-33
lines changed

1 file changed

+44
-33
lines changed

lib/mpl_toolkits/mplot3d/axes3d.py

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

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

17071693
if fcolors is not None:
17081694
if shade:
17091695
colset = self._shade_colors(
1710-
colset, get_normals(polys), lightsource)
1696+
colset, self._generate_normals(polys), lightsource)
17111697
polyc.set_facecolors(colset)
17121698
polyc.set_edgecolors(colset)
17131699
elif cmap:
@@ -1721,7 +1707,7 @@ def get_normals(polygons):
17211707
else:
17221708
if shade:
17231709
colset = self._shade_colors(
1724-
color, get_normals(polys), lightsource)
1710+
color, self._generate_normals(polys), lightsource)
17251711
else:
17261712
colset = color
17271713
polyc.set_facecolors(colset)
@@ -1732,21 +1718,48 @@ def get_normals(polygons):
17321718
return polyc
17331719

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

17511764
def _shade_colors(self, color, normals, lightsource=None):
17521765
'''
@@ -1993,9 +2006,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
19932006
polyc.set_norm(norm)
19942007
else:
19952008
if shade:
1996-
v1 = verts[:, 0, :] - verts[:, 1, :]
1997-
v2 = verts[:, 1, :] - verts[:, 2, :]
1998-
normals = np.cross(v1, v2)
2009+
normals = self._generate_normals(verts)
19992010
colset = self._shade_colors(color, normals, lightsource)
20002011
else:
20012012
colset = color
@@ -2042,9 +2053,9 @@ def _3d_extend_contour(self, cset, stride=5):
20422053
botverts[0][i2],
20432054
botverts[0][i1]])
20442055

2045-
v1 = np.array(topverts[0][i1]) - np.array(topverts[0][i2])
2046-
v2 = np.array(topverts[0][i1]) - np.array(botverts[0][i1])
2047-
normals.append(np.cross(v1, v2))
2056+
# all polygons have 4 vertices, so vectorize
2057+
polyverts = np.array(polyverts)
2058+
normals = self._generate_normals(polyverts)
20482059

20492060
colors = self._shade_colors(color, normals)
20502061
colors2 = self._shade_colors(color, normals)

0 commit comments

Comments
 (0)