Add the Quaternion manifold and tests.
Change-Id: I0a396a5834168c1526d6f2b8c99a1ad3348ad657
diff --git a/include/ceres/manifold.h b/include/ceres/manifold.h
index 30ecae7..0867997 100644
--- a/include/ceres/manifold.h
+++ b/include/ceres/manifold.h
@@ -339,6 +339,48 @@
int buffer_size_ = 0;
};
+// Implements the manifold for a Hamilton quaternion as defined in
+// https://en.wikipedia.org/wiki/Quaternion. Quaternions are represented as unit
+// norm 4-vectors, i.e.
+//
+// q = [q0; q1; q2; q3], |q| = 1
+//
+// is the ambient space representation.
+//
+// q0 scalar part.
+// q1 coefficient of i.
+// q2 coefficient of j.
+// q3 coefficient of k.
+//
+// where: i*i = j*j = k*k = -1 and i*j = k, j*k = i, k*i = j.
+//
+// The tangent space is R^3, which relates to the ambient space through
+// the Plus and Minus operations defined as:
+//
+// Plus(x, delta) = [cos(|delta|); sin(|delta|) * delta / |delta|] * x
+// Minus(y, x) = to_delta(y * x^{-1})
+//
+// where "*" is the quaternion product and because q is a unit quaternion
+// (|q|=1), q^-1 = [q0; -q1; -q2; -q3]
+//
+// and to_delta( [q0; u_{3x1}] ) = u / |u| * atan2(|u|, q0)
+class CERES_EXPORT Quaternion : public Manifold {
+ public:
+ Quaternion() = default;
+ virtual ~Quaternion() = default;
+ int AmbientSize() const override { return 4; }
+ int TangentSize() const override { return 3; }
+
+ bool Plus(const double* x,
+ const double* delta,
+ double* x_plus_delta) const override;
+ bool PlusJacobian(const double* x, double* jacobian) const override;
+ bool Minus(const double* y,
+ const double* x,
+ double* y_minus_x) const override;
+ bool MinusJacobian(const double* x, double* jacobian) const override;
+};
+
} // namespace ceres
// clang-format off
diff --git a/internal/ceres/manifold.cc b/internal/ceres/manifold.cc
index b0fead2..70b904c 100644
--- a/internal/ceres/manifold.cc
+++ b/internal/ceres/manifold.cc
@@ -1,5 +1,7 @@
#include "ceres/manifold.h"
+#include <cmath>
+
#include "ceres/internal/eigen.h"
#include "ceres/internal/fixed_array.h"
#include "glog/logging.h"
@@ -272,4 +274,82 @@
return true;
}
+bool Quaternion::Plus(const double* x,
+ const double* delta,
+ double* x_plus_delta) const {
+ // x_plus_delta = QuaternionProduct(q_delta, x), where q_delta is the
+ // quaternion constructed from delta.
+ const double norm_delta = std::sqrt(
+ delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]);
+ if (norm_delta > 0.0) {
+ const double sin_delta_by_delta = (std::sin(norm_delta) / norm_delta);
+ double q_delta[4];
+ q_delta[0] = std::cos(norm_delta);
+ q_delta[1] = sin_delta_by_delta * delta[0];
+ q_delta[2] = sin_delta_by_delta * delta[1];
+ q_delta[3] = sin_delta_by_delta * delta[2];
+
+ // clang-format off
+ x_plus_delta[0] = q_delta[0] * x[0] - q_delta[1] * x[1] - q_delta[2] * x[2] - q_delta[3] * x[3];
+ x_plus_delta[1] = q_delta[0] * x[1] + q_delta[1] * x[0] + q_delta[2] * x[3] - q_delta[3] * x[2];
+ x_plus_delta[2] = q_delta[0] * x[2] - q_delta[1] * x[3] + q_delta[2] * x[0] + q_delta[3] * x[1];
+ x_plus_delta[3] = q_delta[0] * x[3] + q_delta[1] * x[2] - q_delta[2] * x[1] + q_delta[3] * x[0];
+ // clang-format on
+ } else {
+ for (int i = 0; i < 4; ++i) {
+ x_plus_delta[i] = x[i];
+ }
+ }
+ return true;
+}
+
+bool Quaternion::PlusJacobian(const double* x, double* jacobian) const {
+ // clang-format off
+ jacobian[0] = -x[1]; jacobian[1] = -x[2]; jacobian[2] = -x[3];
+ jacobian[3] = x[0]; jacobian[4] = x[3]; jacobian[5] = -x[2];
+ jacobian[6] = -x[3]; jacobian[7] = x[0]; jacobian[8] = x[1];
+ jacobian[9] = x[2]; jacobian[10] = -x[1]; jacobian[11] = x[0];
+ // clang-format on
+ return true;
+}
+
+bool Quaternion::Minus(const double* y,
+ const double* x,
+ double* y_minus_x) const {
+ // ambient_y_minus_x = QuaternionProduct(y, -x) where -x is the conjugate of
+ // x.
+ double ambient_y_minus_x[4];
+
+ // clang-format off
+ ambient_y_minus_x[0] = y[0] * x[0] + y[1] * x[1] + y[2] * x[2] + y[3] * x[3];
+ ambient_y_minus_x[1] = -y[0] * x[1] + y[1] * x[0] - y[2] * x[3] + y[3] * x[2];
+ ambient_y_minus_x[2] = -y[0] * x[2] + y[1] * x[3] + y[2] * x[0] - y[3] * x[1];
+ ambient_y_minus_x[3] = -y[0] * x[3] - y[1] * x[2] + y[2] * x[1] + y[3] * x[0];
+ // clang-format on
+
+ const double u_norm = std::sqrt(ambient_y_minus_x[1] * ambient_y_minus_x[1] +
+ ambient_y_minus_x[2] * ambient_y_minus_x[2] +
+ ambient_y_minus_x[3] * ambient_y_minus_x[3]);
+ if (u_norm > 0.0) {
+ const double theta = std::atan2(u_norm, ambient_y_minus_x[0]);
+ y_minus_x[0] = theta * ambient_y_minus_x[1] / u_norm;
+ y_minus_x[1] = theta * ambient_y_minus_x[2] / u_norm;
+ y_minus_x[2] = theta * ambient_y_minus_x[3] / u_norm;
+ } else {
+ y_minus_x[0] = 0.0;
+ y_minus_x[1] = 0.0;
+ y_minus_x[2] = 0.0;
+ }
+ return true;
+}
+
+bool Quaternion::MinusJacobian(const double* x, double* jacobian) const {
+ // clang-format off
+ jacobian[0] = - x[1]; jacobian[1]= x[0]; jacobian[2] = -x[3]; jacobian[3] = x[2];
+ jacobian[4] = - x[2]; jacobian[5]= x[3]; jacobian[6] = x[0]; jacobian[7] = -x[1];
+ jacobian[8] = - x[3]; jacobian[9]= -x[2]; jacobian[10] = x[1]; jacobian[11] = x[0];
+ // clang-format on
+ return true;
+}
+
} // namespace ceres
diff --git a/internal/ceres/manifold_test.cc b/internal/ceres/manifold_test.cc
index a3777ae..6c95fa6 100644
--- a/internal/ceres/manifold_test.cc
+++ b/internal/ceres/manifold_test.cc
@@ -548,5 +548,111 @@
}
}
+TEST(Quaternion, PlusPiBy2) {
+ Quaternion manifold;
+ Vector x = Vector::Zero(4);
+ x[0] = 1.0;
+
+ for (int i = 0; i < 3; ++i) {
+ Vector delta = Vector::Zero(3);
+ delta[i] = M_PI / 2;
+ Vector x_plus_delta = Vector::Zero(4);
+ EXPECT_TRUE(manifold.Plus(x.data(), delta.data(), x_plus_delta.data()));
+
+ // Expect that the element corresponding to pi/2 is +/- 1. All other
+ // elements should be zero.
+ for (int j = 0; j < 4; ++j) {
+ if (i == (j - 1)) {
+ EXPECT_LT(std::abs(x_plus_delta[j]) - 1,
+ std::numeric_limits<double>::epsilon())
+ << "\ndelta = " << delta.transpose()
+ << "\nx_plus_delta = " << x_plus_delta.transpose()
+ << "\n expected the " << j
+ << "th element of x_plus_delta to be +/- 1.";
+ } else {
+ EXPECT_LT(std::abs(x_plus_delta[j]),
+ std::numeric_limits<double>::epsilon())
+ << "\ndelta = " << delta.transpose()
+ << "\nx_plus_delta = " << x_plus_delta.transpose()
+ << "\n expected the " << j << "th element of x_plus_delta to be 0.";
+ }
+ }
+ EXPECT_THAT_MANIFOLD_INVARIANTS_HOLD(manifold, x, delta, x_plus_delta);
+ }
+}
+
+// Compute the expected value of Quaternion::Plus via functions in rotation.h
+// and compares it to the one computed by Quaternion::Plus.
+MATCHER_P2(QuaternionPlusIsCorrectAt, x, delta, "") {
+ // This multiplication by 2 is needed because AngleAxisToQuaternion uses
+ // |delta|/2 as the angle of rotation where as in the implementation of
+ // Quaternion for historical reasons we use |delta|.
+ const Vector two_delta = delta * 2;
+ Vector delta_q(4);
+ AngleAxisToQuaternion(two_delta.data(), delta_q.data());
+
+ Vector expected(4);
+ QuaternionProduct(delta_q.data(), x.data(), expected.data());
+ Vector actual(4);
+ EXPECT_TRUE(arg.Plus(x.data(), delta.data(), actual.data()));
+
+ const double n = (actual - expected).norm();
+ const double d = expected.norm();
+ const double diffnorm = n / d;
+ if (diffnorm > kEpsilon) {
+ *result_listener << "\nx: " << x.transpose()
+ << "\ndelta: " << delta.transpose()
+ << "\nexpected: " << expected.transpose()
+ << "\nactual: " << actual.transpose()
+ << "\ndiff: " << (expected - actual).transpose()
+ << "\ndiffnorm : " << diffnorm;
+ return false;
+ }
+ return true;
+}
+
+Vector RandomQuaternion() {
+ Vector x = Vector::Random(4);
+ x.normalize();
+ return x;
+}
+
+TEST(Quaternion, GenericDelta) {
+ Quaternion manifold;
+ for (int trial = 0; trial < kNumTrials; ++trial) {
+ const Vector x = RandomQuaternion();
+ const Vector y = RandomQuaternion();
+ Vector delta = Vector::Random(3);
+ EXPECT_THAT(manifold, QuaternionPlusIsCorrectAt(x, delta));
+ EXPECT_THAT_MANIFOLD_INVARIANTS_HOLD(manifold, x, delta, y);
+ }
+}
+
+TEST(Quaternion, SmallDelta) {
+ Quaternion manifold;
+ for (int trial = 0; trial < kNumTrials; ++trial) {
+ const Vector x = RandomQuaternion();
+ const Vector y = RandomQuaternion();
+ Vector delta = Vector::Random(3);
+ delta.normalize();
+ delta *= 1e-6;
+ EXPECT_THAT(manifold, QuaternionPlusIsCorrectAt(x, delta));
+ EXPECT_THAT_MANIFOLD_INVARIANTS_HOLD(manifold, x, delta, y);
+ }
+}
+
+TEST(Quaternion, DeltaJustBelowPi) {
+ Quaternion manifold;
+ for (int trial = 0; trial < kNumTrials; ++trial) {
+ const Vector x = RandomQuaternion();
+ const Vector y = RandomQuaternion();
+ Vector delta = Vector::Random(3);
+ delta.normalize();
+ delta *= (M_PI - 1e-6);
+ EXPECT_THAT(manifold, QuaternionPlusIsCorrectAt(x, delta));
+ EXPECT_THAT_MANIFOLD_INVARIANTS_HOLD(manifold, x, delta, y);
+ }
+}
+
} // namespace internal
} // namespace ceres