Skip to content

Commit f889e19

Browse files
committed
Add spline interpolator to tesseract_common
This function uses Eigen to interpolate each column of a TrajArray using a cubic spline.
1 parent 5cf6b91 commit f889e19

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* @file utils.h
3+
* @brief Common Tesseract Utilities for Interpolating
4+
*
5+
* @date January 7, 2020
6+
* @version TODO
7+
* @bug No known bugs
8+
*
9+
* @copyright Copyright (c) 2019, Southwest Research Institute
10+
*
11+
* @par License
12+
* Software License Agreement (Apache License)
13+
* @par
14+
* Licensed under the Apache License, Version 2.0 (the "License");
15+
* you may not use this file except in compliance with the License.
16+
* You may obtain a copy of the License at
17+
* http://www.apache.org/licenses/LICENSE-2.0
18+
* @par
19+
* Unless required by applicable law or agreed to in writing, software
20+
* distributed under the License is distributed on an "AS IS" BASIS,
21+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
22+
* See the License for the specific language governing permissions and
23+
* limitations under the License.
24+
*/
25+
#ifndef TESSERACT_COMMON_INTERPOLATORS_H
26+
#define TESSERACT_COMMON_INTERPOLATORS_H
27+
28+
#include <tesseract_common/macros.h>
29+
#include <tesseract_common/types.h>
30+
TESSERACT_COMMON_IGNORE_WARNINGS_PUSH
31+
#include <Eigen/Core>
32+
#include <unsupported/Eigen/Splines>
33+
TESSERACT_COMMON_IGNORE_WARNINGS_POP
34+
35+
namespace tesseract_common
36+
{
37+
/** @brief Uses Eigen/Splines to fit a 3rd order B-Spline to the input data and provides an operator for retrieving
38+
* intermediate points
39+
*
40+
* Note: B-Spline may be lower order than requested for short input vectors */
41+
class SplineFunction
42+
{
43+
public:
44+
/**
45+
* @brief SplineFunction Constructor for interpolating while applying restrictions on the derivates of some points
46+
*
47+
* Example: Start/end with 0 velocity,
48+
* Derivatives is a VectorXd of length 2 filled with zeros;
49+
* Indices is then a VectorXi of length 2 filled with 0 and x_vec.size()-1
50+
*
51+
* **Note: There is a known bug in Eigen as of 1/6/2020. This will give incorrect results unless you patch
52+
* SplineFitting.h.** See stackoverflow.com/questions/48382939 and https://gitlab.com/libeigen/eigen/merge_requests/41
53+
* @param x_vec Independent variable. Probably time for most motion planning problems
54+
* @param y_vec Dependent variable. Probably joint values for most motion planning problems.
55+
* @param derivatives This is a vector of derivative values.
56+
* @param indices This is a vector of indices of where those derivatives are applied
57+
*/
58+
SplineFunction(const Eigen::Ref<Eigen::VectorXd>& x_vec,
59+
const Eigen::Ref<Eigen::VectorXd>& y_vec,
60+
const Eigen::Ref<Eigen::VectorXd>& derivatives,
61+
const Eigen::Ref<Eigen::VectorXi>& indices)
62+
: x_min_(x_vec.minCoeff())
63+
, x_max_(x_vec.maxCoeff())
64+
,
65+
// Scale x values to [0:1] and curve fit with a 3rd order B-Spline.
66+
spline_(Eigen::SplineFitting<Eigen::Spline<double, 1>>::InterpolateWithDerivatives(
67+
y_vec.transpose(),
68+
derivatives.transpose(),
69+
indices,
70+
std::min<unsigned>(static_cast<unsigned>(x_vec.rows()) - 1, 3),
71+
scaledValues(x_vec)))
72+
{
73+
assert(derivatives.size() == indices.size());
74+
assert(!(x_vec.array().isNaN().any()));
75+
assert(!(x_vec.array().isInf().any()));
76+
assert(!(y_vec.array().isNaN().any()));
77+
assert(!(y_vec.array().isInf().any()));
78+
}
79+
80+
/**
81+
* @brief Constructor for interpolating with no restrictions on derivatives
82+
* @param x_vec Independent variable. Probably time for most motion planning problems
83+
* @param y_vec Dependent variable. Probably joint values for most motion planning problems.
84+
*/
85+
SplineFunction(const Eigen::Ref<Eigen::VectorXd>& x_vec, const Eigen::Ref<Eigen::VectorXd>& y_vec)
86+
: x_min_(x_vec.minCoeff())
87+
, x_max_(x_vec.maxCoeff())
88+
,
89+
// Scale x values to [0:1] and curve fit with a 3rd order B-Spline.
90+
spline_(Eigen::SplineFitting<Eigen::Spline<double, 1>>::Interpolate(y_vec.transpose(),
91+
std::min<long>(x_vec.rows() - 1, 3),
92+
scaledValues(x_vec)))
93+
{
94+
assert(!(x_vec.array().isNaN().any()));
95+
assert(!(x_vec.array().isInf().any()));
96+
assert(!(y_vec.array().isNaN().any()));
97+
assert(!(y_vec.array().isInf().any()));
98+
}
99+
100+
/** @brief Returns the y value at point x in the spline */
101+
double operator()(double x) const
102+
{
103+
// x values need to be scaled to [0:1] in extraction as well.
104+
return spline_(scaledValue(x))(0);
105+
}
106+
107+
/** @brief Returns a vector of interpolated y values for each x in the input vector */
108+
Eigen::VectorXd operator()(const Eigen::Ref<Eigen::VectorXd>& x_vec) const
109+
{
110+
return x_vec.unaryExpr([this](double x) { return spline_(scaledValue(x))(0); });
111+
}
112+
113+
private:
114+
/** @brief Scales value to [0:1] based on x_min and x_max */
115+
double scaledValue(double x) const { return (x - x_min_) / (x_max_ - x_min_); }
116+
117+
/** @brief Scales vector such that each value is [0:1] based on x_min and x_max
118+
*
119+
* It is a requirement for Interpolate that x be on the interval [0:1] */
120+
Eigen::RowVectorXd scaledValues(const Eigen::Ref<Eigen::VectorXd>& x_vec) const
121+
{
122+
return x_vec.unaryExpr([this](double x) { return scaledValue(x); }).transpose();
123+
}
124+
125+
/** @brief Minimum value in x_vec. Used to scale inputs to [0:1] */
126+
double x_min_;
127+
/** @brief Maximum value in x_vec. Used to scale inputs to [0:1] */
128+
double x_max_;
129+
130+
/** @brief One dimensional Spline that is used to interpolate the data
131+
*
132+
* Note: operator(double) returns an array of points. In this case the 0th element is the y value*/
133+
Eigen::Spline<double, 1> spline_;
134+
};
135+
136+
/**
137+
* @brief Interpolates each column in a TrajArray using a cubic spline. The resuls are an evenly spaced trajectory with
138+
* result_length size.
139+
*
140+
* Note: While the spline will hit these points, the resulting TrajArray is not guaranteed to include the original
141+
* points unless result_length is of size n * (input_traj.rows() - 1) + 1
142+
* @param input_traj Input TrajArray. Each column will be interpolated with a cubic spline.
143+
* @param result_length Number of rows in the resulting TrajArray. For best results this should be size n *
144+
* (input_traj.rows() - 1) + 1
145+
* @return Resulting TrajArray of size results_length x input_traj.cols()
146+
*/
147+
inline TrajArray interpolateCubicSpline(const Eigen::Ref<TrajArray>& input_traj, const int& result_length)
148+
{
149+
TrajArray results(result_length, input_traj.cols());
150+
for (long ind = 0; ind < input_traj.cols(); ind++)
151+
{
152+
// Fit the spline to the input data
153+
Eigen::VectorXd t_in =
154+
Eigen::VectorXd::LinSpaced(input_traj.rows(), 0.0, static_cast<double>(input_traj.rows() - 1));
155+
Eigen::VectorXd y_in = input_traj.col(ind);
156+
SplineFunction spline(t_in, y_in);
157+
158+
// Extract evenly spaced points from spline
159+
Eigen::VectorXd t_out = Eigen::VectorXd::LinSpaced(result_length, 0.0, static_cast<double>(input_traj.rows() - 1));
160+
Eigen::VectorXd y_out = spline(t_out);
161+
results.col(ind) = y_out;
162+
}
163+
164+
return results;
165+
}
166+
167+
} // namespace tesseract_common
168+
169+
#endif

tesseract_common/test/tesseract_common_unit.cpp

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,97 @@ TEST(TesseractCommonUnit, serializationIsometry3d)
653653
}
654654
}
655655

656+
657+
TEST(TesseractCommonUnit, splineFunctionNoDerivatives) // NOLINT
658+
{
659+
Eigen::VectorXd xvals(10);
660+
Eigen::VectorXd yvals(xvals.rows());
661+
662+
xvals << 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;
663+
yvals = xvals.array().square();
664+
665+
// Fit the spline
666+
tesseract_common::SplineFunction spline(xvals, yvals);
667+
668+
// Check that input knots are hit exactly
669+
for (long ind = 0; ind < xvals.size(); ind++)
670+
EXPECT_NEAR(spline(xvals[ind]), yvals[ind], 1e-8);
671+
672+
Eigen::VectorXd xvals_interp(8);
673+
xvals_interp << 1.5, 2.5, 3.5, 4.5, 5.5, 6.5, 7.5, 8.5;
674+
// Check that the intermediate points are within 1% for a quadratic (they should be really close)
675+
for (long ind = 0; ind < xvals_interp.size(); ind++)
676+
{
677+
double gt = xvals_interp[ind] * xvals_interp[ind];
678+
EXPECT_NEAR(spline(xvals_interp[ind]), gt, gt * 0.01 + 1e-8);
679+
}
680+
}
681+
682+
TEST(TesseractCommonUnit, splineFunctionWithDerivatives) // NOLINT
683+
{
684+
Eigen::VectorXd xvals(10);
685+
Eigen::VectorXd yvals(xvals.rows());
686+
687+
xvals << 0, 1, 2, 3, 4, 5, 6, 7, 8, 9;
688+
yvals = xvals.array().cube();
689+
690+
Eigen::VectorXd derivatives(2);
691+
derivatives << 0., 0.;
692+
Eigen::VectorXi indices(2);
693+
indices << 0, static_cast<int>(xvals.size() - 1);
694+
695+
// Fit the spline
696+
tesseract_common::SplineFunction spline(xvals, yvals, derivatives, indices);
697+
698+
// Check that input knots are hit exactly
699+
for (long ind = 0; ind < xvals.size(); ind++)
700+
EXPECT_NEAR(spline(xvals[ind]), yvals[ind], 1e-8);
701+
702+
// Note: Interpolating using derivatives is not tested here because it requires patches to Eigen. See note in class
703+
// documentation
704+
}
705+
706+
TEST(TesseractCommonUnit, interpolateCubicSpline) // NOLINT
707+
{
708+
tesseract_common::TrajArray input_traj(10, 5);
709+
Eigen::VectorXd t_in = Eigen::VectorXd::LinSpaced(input_traj.rows(), 0.0, static_cast<double>(input_traj.rows() - 1));
710+
input_traj.col(0) = t_in.array().square();
711+
input_traj.col(1) = t_in.array().cube();
712+
input_traj.col(2) = t_in.array().sin();
713+
input_traj.col(3) = t_in.array().cos();
714+
input_traj.col(4) = t_in.array().exp();
715+
std::cout << "Input Trajectory: \n" << input_traj << std::endl;
716+
717+
const int result_length = 46;
718+
tesseract_common::TrajArray results = tesseract_common::interpolateCubicSpline(input_traj, result_length);
719+
std::cout << "Spline Results: \n" << results << std::endl;
720+
721+
EXPECT_EQ(results.rows(), result_length);
722+
EXPECT_EQ(results.cols(), input_traj.cols());
723+
724+
// Check that all of the knots are hit exactly
725+
for (long ind = 0; ind < input_traj.rows(); ind++)
726+
{
727+
EXPECT_NEAR(input_traj.col(0)[ind], results.col(0)[ind * 5], 1e-8);
728+
EXPECT_NEAR(input_traj.col(1)[ind], results.col(1)[ind * 5], 1e-8);
729+
EXPECT_NEAR(input_traj.col(2)[ind], results.col(2)[ind * 5], 1e-8);
730+
EXPECT_NEAR(input_traj.col(3)[ind], results.col(3)[ind * 5], 1e-8);
731+
EXPECT_NEAR(input_traj.col(4)[ind], results.col(4)[ind * 5], 1e-8);
732+
}
733+
734+
// Check that the intermediate points are within a small percentage. The polynomials should be really close
735+
Eigen::VectorXd t_interp =
736+
Eigen::VectorXd::LinSpaced(results.rows(), 0.0, static_cast<double>(input_traj.rows() - 1));
737+
for (long ind = 0; ind < results.rows(); ind++)
738+
{
739+
EXPECT_NEAR(t_interp.array().square()[ind], results.col(0)[ind], t_interp.array().square()[ind] * 0.01 + 1e-8);
740+
EXPECT_NEAR(t_interp.array().cube()[ind], results.col(1)[ind], t_interp.array().cube()[ind] * 0.01 + 1e-8);
741+
EXPECT_NEAR(t_interp.array().sin()[ind], results.col(2)[ind], 1.0 * 0.05 + 1e-8);
742+
EXPECT_NEAR(t_interp.array().cos()[ind], results.col(3)[ind], 1.0 * 0.05 + 1e-8);
743+
EXPECT_NEAR(t_interp.array().exp()[ind], results.col(4)[ind], t_interp.array().exp()[ind] * 0.10 + 1e-8);
744+
}
745+
}
746+
656747
int main(int argc, char** argv)
657748
{
658749
testing::InitGoogleTest(&argc, argv);

0 commit comments

Comments
 (0)