libstp
Robotics library for Botball
Loading...
Searching...
No Matches
catmull_rom_spline.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <stdexcept>
6#include <vector>
7
8#include <Eigen/Core>
9
10namespace libstp::motion
11{
23 {
24 public:
32 explicit CatmullRomSpline(std::vector<Eigen::Vector2d> waypoints,
33 int samples_per_segment = 200)
34 {
35 if (waypoints.size() < 2)
36 throw std::invalid_argument("CatmullRomSpline requires at least 2 waypoints");
37
38 // Virtual control points via reflection for natural endpoint behavior.
39 // Duplicating the same point would cause zero knot intervals in
40 // centripetal parameterization (division by zero).
41 // Instead: P_virtual[-1] = 2*P[0] - P[1], P_virtual[N] = 2*P[N-1] - P[N-2]
42 const auto& first = waypoints.front();
43 const auto& second = waypoints[1];
44 const auto& last = waypoints.back();
45 const auto& second_last = waypoints[waypoints.size() - 2];
46
47 points_.reserve(waypoints.size() + 2);
48 points_.push_back(2.0 * first - second); // virtual start
49 for (auto& p : waypoints)
50 points_.push_back(std::move(p));
51 points_.push_back(2.0 * last - second_last); // virtual end
52
53 num_segments_ = static_cast<int>(points_.size()) - 3;
54 buildArcLengthLUT(samples_per_segment);
55 }
56
58 [[nodiscard]] double totalLength() const { return arc_length_lut_.back(); }
59
61 [[nodiscard]] int numWaypoints() const { return num_segments_ + 1; }
62
66 [[nodiscard]] Eigen::Vector2d positionAt(double s) const
67 {
68 auto [seg, t] = arcLengthToParam(s);
69 return evaluateSegment(seg, t);
70 }
71
75 [[nodiscard]] Eigen::Vector2d tangentAt(double s) const
76 {
77 auto [seg, t] = arcLengthToParam(s);
78 Eigen::Vector2d d = evaluateSegmentDerivative(seg, t);
79 double norm = d.norm();
80 if (norm < 1e-12)
81 {
82 // Degenerate: return direction toward next distinct point
83 return Eigen::Vector2d(1.0, 0.0);
84 }
85 return d / norm;
86 }
87
97 [[nodiscard]] double findNearestArcLength(const Eigen::Vector2d& point,
98 double s_hint) const
99 {
100 const double total = totalLength();
101 if (total < 1e-12) return 0.0;
102
103 // Scan a window around s_hint in the LUT
104 const double window = total * 0.25; // 25% of path
105 const double s_lo = std::max(0.0, s_hint - window);
106 const double s_hi = std::min(total, s_hint + window);
107
108 // Find closest LUT entry in window
109 double best_s = s_hint;
110 double best_dist_sq = (positionAt(s_hint) - point).squaredNorm();
111
112 const double ds = total / static_cast<double>(arc_length_lut_.size() - 1);
113 int i_lo = std::max(0, static_cast<int>(s_lo / ds));
114 int i_hi = std::min(static_cast<int>(arc_length_lut_.size()) - 1,
115 static_cast<int>(s_hi / ds) + 1);
116
117 for (int i = i_lo; i <= i_hi; ++i)
118 {
119 double si = arc_length_lut_[i];
120 double d2 = (positionAt(si) - point).squaredNorm();
121 if (d2 < best_dist_sq)
122 {
123 best_dist_sq = d2;
124 best_s = si;
125 }
126 }
127
128 // Newton refinement: minimize f(s) = ||P(s) - point||^2
129 // f'(s) = 2 * (P(s) - point) . P'(s)
130 // Clamp to a neighborhood around best_s to avoid jumping to distant
131 // local minima on curved paths.
132 const double newton_radius = window;
133 const double newton_lo = std::max(0.0, best_s - newton_radius);
134 const double newton_hi = std::min(total, best_s + newton_radius);
135
136 double s = best_s;
137 for (int iter = 0; iter < 5; ++iter)
138 {
139 auto [seg, t] = arcLengthToParam(s);
140 Eigen::Vector2d p = evaluateSegment(seg, t);
141 Eigen::Vector2d dp = evaluateSegmentDerivative(seg, t);
142 double dp_norm = dp.norm();
143 if (dp_norm < 1e-12) break;
144
145 Eigen::Vector2d diff = p - point;
146 double f_prime = 2.0 * diff.dot(dp);
147 // Approximate f'' ≈ 2 * dp . dp (ignore curvature term)
148 double f_double_prime = 2.0 * dp.squaredNorm();
149 if (std::abs(f_double_prime) < 1e-12) break;
150
151 double step = f_prime / f_double_prime;
152 s = std::clamp(s - step, newton_lo, newton_hi);
153 }
154
155 // Accept Newton result only if it's actually closer
156 double newton_dist_sq = (positionAt(s) - point).squaredNorm();
157 return (newton_dist_sq <= best_dist_sq) ? s : best_s;
158 }
159
160 private:
161 // Catmull-Rom centripetal parameterization helpers
162 static double knotValue(const Eigen::Vector2d& p0, const Eigen::Vector2d& p1,
163 double t_prev)
164 {
165 // Centripetal: alpha = 0.5 → t_i = t_{i-1} + |p_i - p_{i-1}|^0.5
166 double d = (p1 - p0).norm();
167 return t_prev + std::sqrt(std::max(d, 1e-12));
168 }
169
176 [[nodiscard]] Eigen::Vector2d evaluateSegment(int seg, double t) const
177 {
178 const Eigen::Vector2d& P0 = points_[seg];
179 const Eigen::Vector2d& P1 = points_[seg + 1];
180 const Eigen::Vector2d& P2 = points_[seg + 2];
181 const Eigen::Vector2d& P3 = points_[seg + 3];
182
183 // Centripetal knot values
184 double t0 = 0.0;
185 double t1 = knotValue(P0, P1, t0);
186 double t2 = knotValue(P1, P2, t1);
187 double t3 = knotValue(P2, P3, t2);
188
189 // Map local t ∈ [0,1] to knot space [t1, t2]
190 double u = t1 + t * (t2 - t1);
191
192 // Barry-Goldman recursive evaluation
193 auto lerp = [](const Eigen::Vector2d& a, const Eigen::Vector2d& b,
194 double ta, double tb, double u_) -> Eigen::Vector2d
195 {
196 double denom = tb - ta;
197 if (std::abs(denom) < 1e-12) return a;
198 double alpha = (u_ - ta) / denom;
199 return (1.0 - alpha) * a + alpha * b;
200 };
201
202 Eigen::Vector2d A1 = lerp(P0, P1, t0, t1, u);
203 Eigen::Vector2d A2 = lerp(P1, P2, t1, t2, u);
204 Eigen::Vector2d A3 = lerp(P2, P3, t2, t3, u);
205
206 Eigen::Vector2d B1 = lerp(A1, A2, t0, t2, u);
207 Eigen::Vector2d B2 = lerp(A2, A3, t1, t3, u);
208
209 return lerp(B1, B2, t1, t2, u);
210 }
211
217 [[nodiscard]] Eigen::Vector2d evaluateSegmentDerivative(int seg, double t) const
218 {
219 constexpr double h = 1e-5;
220 double t_lo = std::max(0.0, t - h);
221 double t_hi = std::min(1.0, t + h);
222 double dt = t_hi - t_lo;
223 if (dt < 1e-12) return Eigen::Vector2d(0.0, 0.0);
224 return (evaluateSegment(seg, t_hi) - evaluateSegment(seg, t_lo)) / dt;
225 }
226
230 void buildArcLengthLUT(int samples_per_segment)
231 {
232 int total_samples = num_segments_ * samples_per_segment + 1;
233 arc_length_lut_.resize(total_samples);
234 param_lut_.resize(total_samples);
235
236 arc_length_lut_[0] = 0.0;
237 param_lut_[0] = {0, 0.0};
238
239 Eigen::Vector2d prev = evaluateSegment(0, 0.0);
240 int idx = 1;
241
242 for (int seg = 0; seg < num_segments_; ++seg)
243 {
244 for (int i = 1; i <= samples_per_segment; ++i)
245 {
246 double t = static_cast<double>(i) / samples_per_segment;
247 Eigen::Vector2d p = evaluateSegment(seg, t);
248 double chord = (p - prev).norm();
249 arc_length_lut_[idx] = arc_length_lut_[idx - 1] + chord;
250 param_lut_[idx] = {seg, t};
251 prev = p;
252 ++idx;
253 }
254 }
255 }
256
260 [[nodiscard]] std::pair<int, double> arcLengthToParam(double s) const
261 {
262 s = std::clamp(s, 0.0, totalLength());
263
264 // Binary search for the LUT interval containing s
265 auto it = std::lower_bound(arc_length_lut_.begin(), arc_length_lut_.end(), s);
266 int idx = static_cast<int>(it - arc_length_lut_.begin());
267
268 if (idx == 0) return param_lut_[0];
269 if (idx >= static_cast<int>(arc_length_lut_.size()))
270 return param_lut_.back();
271
272 // Linearly interpolate between adjacent LUT entries
273 double s0 = arc_length_lut_[idx - 1];
274 double s1 = arc_length_lut_[idx];
275 double frac = (s1 > s0) ? (s - s0) / (s1 - s0) : 0.0;
276
277 auto [seg0, t0] = param_lut_[idx - 1];
278 auto [seg1, t1] = param_lut_[idx];
279
280 if (seg0 == seg1)
281 {
282 return {seg0, t0 + frac * (t1 - t0)};
283 }
284 else
285 {
286 // Cross-segment boundary: snap to closer entry
287 return (frac < 0.5) ? param_lut_[idx - 1] : param_lut_[idx];
288 }
289 }
290
291 std::vector<Eigen::Vector2d> points_; // [duplicate_first, wp0, wp1, ..., wpN, duplicate_last]
292 int num_segments_{0}; // number of spline segments = waypoints - 1
293
294 // Arc-length LUT: arc_length_lut_[i] = cumulative arc length at param_lut_[i]
295 std::vector<double> arc_length_lut_;
296 std::vector<std::pair<int, double>> param_lut_; // (segment_index, local_t)
297 };
298}
CatmullRomSpline(std::vector< Eigen::Vector2d > waypoints, int samples_per_segment=200)
Eigen::Vector2d tangentAt(double s) const
double findNearestArcLength(const Eigen::Vector2d &point, double s_hint) const
Eigen::Vector2d positionAt(double s) const