This repository has been archived by the owner on Apr 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 10
/
Copy pathcatmullRom.py
257 lines (200 loc) · 8.01 KB
/
catmullRom.py
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
#
# catmullRom.py
# shotmanager
#
# A Catmull-Rom spline class.
#
# Created by Will Silva on 11/22/2015.
# Copyright (c) 2016 3D Robotics.
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from vector3 import Vector3
TOL = 1.0e-3
class CatmullRom:
# Derivation: https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
# Class inspiration:
# https://code.google.com/p/gamekernel/source/browse/kgraphics/math/CatmullRom.cpp
def __init__(self, Pts):
# internal point storage
self.points = Pts
# make sure we have enough points
if len(Pts) < 4:
raise ValueError(
'Not enough points provided to generate a spline.')
# calculate total length of the spline
self.splineCoefficients = []
self.arcLengths = []
self.totalArcLength = 0.0
for seg in range(0, len(self.points) - 3):
self.splineCoefficients.append(self.updateSplineCoefficients(seg))
self.arcLengths.append(self.arcLength(seg, 0, 1))
self.totalArcLength = sum(self.arcLengths)
def updateSplineCoefficients(self, seg):
if seg < 0 or seg > len(self.points) - 4:
raise ValueError(
'Invalid segment number received (%d). Check the number of spline control points.' % seg)
P0 = self.points[seg]
P1 = self.points[seg + 1]
P2 = self.points[seg + 2]
P3 = self.points[seg + 3]
A = 3.0 * P1 \
- P0 \
- 3.0 * P2 \
+ P3
B = 2.0 * P0 \
- 5.0 * P1 \
+ 4.0 * P2 \
- P3
C = P2 - P0
return P0,P1,P2,P3,A,B,C
def position(self, seg, u):
'''Returns x,y,z position of spline at parameter u'''
P0,P1,P2,P3,A,B,C = self.splineCoefficients[seg]
return P1 + (0.5 * u) * (C + u * (B + u * A))
def velocity(self, seg, u):
'''Returns x,y,z velocity of spline at parameter u'''
P0,P1,P2,P3,A,B,C = self.splineCoefficients[seg]
return (0.5 * C + u * (B + 1.5 * u * A))
def acceleration(self, seg, u):
'''Returns x,y,z acceleration of spline at parameter u'''
P0,P1,P2,P3,A,B,C = self.splineCoefficients[seg]
return B + (3.0 * u) * A
def curvature(self, seg, u):
'''Returns Frenet curvature of spline at parameter u'''
# https://en.wikipedia.org/wiki/Frenet-Serret_formulas
vel = self.velocity(seg,u)
return Vector3.cross(vel, self.acceleration(seg, u)).length() / (vel.length())**3
def arcLength(self, seg, u1, u2):
'''Calculates arc length between u1 and u2 using Gaussian Quadrature'''
# sanitize u1,u2
if u2 <= u1 or u1 > 1.0 or u2 < 0.0:
return 0.0
if u1 < 0.0:
u1 = 0.0
if u2 > 1.0:
u2 = 1.0
# Gaussian Quadrature weights and abscissae for n = 5
abscissae = [
0.0000000000, 0.5384693101, -0.5384693101, 0.9061798459, -0.9061798459]
weights = [0.5688888889, 0.4786286705,
0.4786286705, 0.2369268850, 0.2369268850]
# Gaussian Quadrature
length = 0.0
for j in range(0, 5):
u = 0.5 * ((u2 - u1) * abscissae[j] + u2 + u1)
length += weights[j] * self.velocity(seg, u).length()
length *= 0.5 * (u2 - u1)
return length
def findParameterByDistance(self, seg, u1, s, newtonIterations = 32):
'''Returns a parameter u that is s meters ahead of u1'''
'''From CatmullRom.cpp: This extends the approach in the text and uses a mixture of bisection and
Newton-Raphson to find the root. The result is more stable than Newton-
Raphson alone because a) we won't end up with a situation where we divide by
zero in the Newton-Raphson step and b) the end result converges faster.
See Numerical Recipes or http://www.essentialmath.com/blog for more details.'''
# if desired arclength is beyond the end of the current segment then
# just return end of segment
if s >= self.arcLength(seg, u1, 1.0):
return 1.0
# can't do negative distances
if s <= 0.0:
return u1
a = u1
b = 1.0
# make first guess
p = u1 + s * (1 / self.arcLengths[seg])
# iterate and look for zeros
for i in range(0, newtonIterations):
# compute function value and test against zero
func = self.arcLength(seg, u1, p) - s
# if within tolerance, return root
if abs(func) < TOL:
return p
# if result less than zero then adjust lower limit
if func < 0.0:
a = p
# if result greater than zero then adjust upper limit
else:
b = p
# Use speed to do a bisection method (will accelerate convergence)
# get speed along the curve
speed = self.velocity(seg, p).length()
# if result lies outside [a,b]
if ((p - a) * speed - func) * ((p - b) * speed - func) > -TOL:
# do bisection
p = 0.5 * (a + b)
else:
p -= func / speed
return float("inf")
def arclengthToNonDimensional(self, p, dp=None):
'''
inputs:
p - spline position reparameterized to total spline arcLength
dp - spline velocity reparameterized to total spline arclength
returns:
seg - current spline segment
u - current non-dimensional spline parameter on segment "seg"
v - velocity along spline in meters per second
'''
# sanitize p
if p > 1:
p = 1
elif p < 0:
p = 0
# calculate target arc length
targetArcLength = p * self.totalArcLength
# find out what segment that's in
for seg in range(1, len(self.arcLengths) + 1):
if sum(self.arcLengths[0:seg]) > targetArcLength:
break
# increment down one
seg -= 1
# calculate distance in that segment
dist = targetArcLength - sum(self.arcLengths[0:seg])
# calculate dist ahead of u = 0 on seg
u = self.findParameterByDistance(seg, 0, dist)
if dp is not None:
v = dp * self.totalArcLength
return (seg, u, v)
else:
return (seg, u)
def nonDimensionalToArclength(self, seg, u, v=None):
'''
inputs:
seg - current spline segment
u - current non-dimensional spline parameter on segment "seg"
v - velocity along spline in meters per second
returns:
p - spline position reparameterized to total spline arcLength
dp - spline velocity reparameterized to total spline arcLength
'''
# sanitize seg
if seg < 0:
seg = 0
elif seg > len(self.points) - 4:
seg = len(self.points) - 4
# sanitize u
if u < 0:
u = 0
elif u > 1:
u = 1
# calculate distance along in segment
dist = self.arcLength(seg, 0, u)
# add all previous segment distances
dist += sum(self.arcLengths[0:seg])
# convert to arclength parameter
p = dist / self.totalArcLength
if v is not None:
dp = v / self.totalArcLength
return (p, dp)
else:
return (p,)