File size: 7,951 Bytes
7885a28 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 |
__all__ = ['geometric_slerp']
import warnings
from typing import TYPE_CHECKING
import numpy as np
from scipy.spatial.distance import euclidean
if TYPE_CHECKING:
import numpy.typing as npt
def _geometric_slerp(start, end, t):
# create an orthogonal basis using QR decomposition
basis = np.vstack([start, end])
Q, R = np.linalg.qr(basis.T)
signs = 2 * (np.diag(R) >= 0) - 1
Q = Q.T * signs.T[:, np.newaxis]
R = R.T * signs.T[:, np.newaxis]
# calculate the angle between `start` and `end`
c = np.dot(start, end)
s = np.linalg.det(R)
omega = np.arctan2(s, c)
# interpolate
start, end = Q
s = np.sin(t * omega)
c = np.cos(t * omega)
return start * c[:, np.newaxis] + end * s[:, np.newaxis]
def geometric_slerp(
start: "npt.ArrayLike",
end: "npt.ArrayLike",
t: "npt.ArrayLike",
tol: float = 1e-7,
) -> np.ndarray:
"""
Geometric spherical linear interpolation.
The interpolation occurs along a unit-radius
great circle arc in arbitrary dimensional space.
Parameters
----------
start : (n_dimensions, ) array-like
Single n-dimensional input coordinate in a 1-D array-like
object. `n` must be greater than 1.
end : (n_dimensions, ) array-like
Single n-dimensional input coordinate in a 1-D array-like
object. `n` must be greater than 1.
t : float or (n_points,) 1D array-like
A float or 1D array-like of doubles representing interpolation
parameters, with values required in the inclusive interval
between 0 and 1. A common approach is to generate the array
with ``np.linspace(0, 1, n_pts)`` for linearly spaced points.
Ascending, descending, and scrambled orders are permitted.
tol : float
The absolute tolerance for determining if the start and end
coordinates are antipodes.
Returns
-------
result : (t.size, D)
An array of doubles containing the interpolated
spherical path and including start and
end when 0 and 1 t are used. The
interpolated values should correspond to the
same sort order provided in the t array. The result
may be 1-dimensional if ``t`` is a float.
Raises
------
ValueError
If ``start`` and ``end`` are antipodes, not on the
unit n-sphere, or for a variety of degenerate conditions.
See Also
--------
scipy.spatial.transform.Slerp : 3-D Slerp that works with quaternions
Notes
-----
The implementation is based on the mathematical formula provided in [1]_,
and the first known presentation of this algorithm, derived from study of
4-D geometry, is credited to Glenn Davis in a footnote of the original
quaternion Slerp publication by Ken Shoemake [2]_.
.. versionadded:: 1.5.0
References
----------
.. [1] https://en.wikipedia.org/wiki/Slerp#Geometric_Slerp
.. [2] Ken Shoemake (1985) Animating rotation with quaternion curves.
ACM SIGGRAPH Computer Graphics, 19(3): 245-254.
Examples
--------
Interpolate four linearly-spaced values on the circumference of
a circle spanning 90 degrees:
>>> import numpy as np
>>> from scipy.spatial import geometric_slerp
>>> import matplotlib.pyplot as plt
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> start = np.array([1, 0])
>>> end = np.array([0, 1])
>>> t_vals = np.linspace(0, 1, 4)
>>> result = geometric_slerp(start,
... end,
... t_vals)
The interpolated results should be at 30 degree intervals
recognizable on the unit circle:
>>> ax.scatter(result[...,0], result[...,1], c='k')
>>> circle = plt.Circle((0, 0), 1, color='grey')
>>> ax.add_artist(circle)
>>> ax.set_aspect('equal')
>>> plt.show()
Attempting to interpolate between antipodes on a circle is
ambiguous because there are two possible paths, and on a
sphere there are infinite possible paths on the geodesic surface.
Nonetheless, one of the ambiguous paths is returned along
with a warning:
>>> opposite_pole = np.array([-1, 0])
>>> with np.testing.suppress_warnings() as sup:
... sup.filter(UserWarning)
... geometric_slerp(start,
... opposite_pole,
... t_vals)
array([[ 1.00000000e+00, 0.00000000e+00],
[ 5.00000000e-01, 8.66025404e-01],
[-5.00000000e-01, 8.66025404e-01],
[-1.00000000e+00, 1.22464680e-16]])
Extend the original example to a sphere and plot interpolation
points in 3D:
>>> from mpl_toolkits.mplot3d import proj3d
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111, projection='3d')
Plot the unit sphere for reference (optional):
>>> u = np.linspace(0, 2 * np.pi, 100)
>>> v = np.linspace(0, np.pi, 100)
>>> x = np.outer(np.cos(u), np.sin(v))
>>> y = np.outer(np.sin(u), np.sin(v))
>>> z = np.outer(np.ones(np.size(u)), np.cos(v))
>>> ax.plot_surface(x, y, z, color='y', alpha=0.1)
Interpolating over a larger number of points
may provide the appearance of a smooth curve on
the surface of the sphere, which is also useful
for discretized integration calculations on a
sphere surface:
>>> start = np.array([1, 0, 0])
>>> end = np.array([0, 0, 1])
>>> t_vals = np.linspace(0, 1, 200)
>>> result = geometric_slerp(start,
... end,
... t_vals)
>>> ax.plot(result[...,0],
... result[...,1],
... result[...,2],
... c='k')
>>> plt.show()
"""
start = np.asarray(start, dtype=np.float64)
end = np.asarray(end, dtype=np.float64)
t = np.asarray(t)
if t.ndim > 1:
raise ValueError("The interpolation parameter "
"value must be one dimensional.")
if start.ndim != 1 or end.ndim != 1:
raise ValueError("Start and end coordinates "
"must be one-dimensional")
if start.size != end.size:
raise ValueError("The dimensions of start and "
"end must match (have same size)")
if start.size < 2 or end.size < 2:
raise ValueError("The start and end coordinates must "
"both be in at least two-dimensional "
"space")
if np.array_equal(start, end):
return np.linspace(start, start, t.size)
# for points that violate equation for n-sphere
for coord in [start, end]:
if not np.allclose(np.linalg.norm(coord), 1.0,
rtol=1e-9,
atol=0):
raise ValueError("start and end are not"
" on a unit n-sphere")
if not isinstance(tol, float):
raise ValueError("tol must be a float")
else:
tol = np.fabs(tol)
coord_dist = euclidean(start, end)
# diameter of 2 within tolerance means antipodes, which is a problem
# for all unit n-spheres (even the 0-sphere would have an ambiguous path)
if np.allclose(coord_dist, 2.0, rtol=0, atol=tol):
warnings.warn("start and end are antipodes "
"using the specified tolerance; "
"this may cause ambiguous slerp paths",
stacklevel=2)
t = np.asarray(t, dtype=np.float64)
if t.size == 0:
return np.empty((0, start.size))
if t.min() < 0 or t.max() > 1:
raise ValueError("interpolation parameter must be in [0, 1]")
if t.ndim == 0:
return _geometric_slerp(start,
end,
np.atleast_1d(t)).ravel()
else:
return _geometric_slerp(start,
end,
t)
|