Files
ceres-solver/internal/ceres/rotation_test.cc
T
David Gossow ac3b8e8217 Gradient checking cleanup and local parameterization bugfix
Change the Ceres gradient checking API to make is useful for
unit testing, clean up code duplication and fix interaction between
gradient checking and local parameterizations.

There were two gradient checking implementations, one being used
when using the check_gradients flag in the Solver, the other
being a standalone class. The standalone version was restricted
to cost functions with fixed parameter sizes at compile time, which
is being lifted here. This enables it to be used inside the
GradientCheckingCostFunction as well.

In addition, this installs new hooks in the Solver to ensure
that Solve will fail if any incorrect gradients are detected. This
way, you can set the check_gradient flags to true and detect
errors in an automated way, instead of just printing error information
to the log. The error log is now also returned in the Solver summary
instead of being printed directly. The user can then decide what to
do with it. The existing hooks for user callbacks are used for
this purpose to keep the internal API changes minimal and non-invasive.

The last and biggest change is the way the the interaction between
local parameterizations and the gradient checker works. Before,
local parameterizations would be ignored by the checker. However,
if a cost function does not compute its Jacobian along the null
space of the local parameterization, this wil not have any effect
on the solver, but would result in a gradient checker error.
With this change, the Jacobians are multiplied by the Jacobians
of the respective local parameterization and thus being compared
in the tangent space only.

The typical use case for this are quaternion parameters, where
a cost function will typically assume that the quaternion is
always normalized, skipping the correct computation of the Jacobian
along the normal to save computation cost.

Change-Id: I5e1bb97b8a899436cea25101efe5011b0bb13282
2016-08-18 06:18:43 +00:00

1133 lines
35 KiB
C++

// Ceres Solver - A fast non-linear least squares minimizer
// Copyright 2015 Google Inc. All rights reserved.
// http://ceres-solver.org/
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of Google Inc. nor the names of its contributors may be
// used to endorse or promote products derived from this software without
// specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.
//
// Author: sameeragarwal@google.com (Sameer Agarwal)
#include <cmath>
#include <limits>
#include <string>
#include "ceres/internal/eigen.h"
#include "ceres/is_close.h"
#include "ceres/internal/port.h"
#include "ceres/jet.h"
#include "ceres/rotation.h"
#include "ceres/stringprintf.h"
#include "ceres/test_util.h"
#include "glog/logging.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
namespace ceres {
namespace internal {
using std::min;
using std::max;
using std::numeric_limits;
using std::string;
using std::swap;
const double kPi = 3.14159265358979323846;
const double kHalfSqrt2 = 0.707106781186547524401;
double RandDouble() {
double r = rand();
return r / RAND_MAX;
}
// A tolerance value for floating-point comparisons.
static double const kTolerance = numeric_limits<double>::epsilon() * 10;
// Looser tolerance used for numerically unstable conversions.
static double const kLooseTolerance = 1e-9;
// Use as:
// double quaternion[4];
// EXPECT_THAT(quaternion, IsNormalizedQuaternion());
MATCHER(IsNormalizedQuaternion, "") {
if (arg == NULL) {
*result_listener << "Null quaternion";
return false;
}
double norm2 = arg[0] * arg[0] + arg[1] * arg[1] +
arg[2] * arg[2] + arg[3] * arg[3];
if (fabs(norm2 - 1.0) > kTolerance) {
*result_listener << "squared norm is " << norm2;
return false;
}
return true;
}
// Use as:
// double expected_quaternion[4];
// double actual_quaternion[4];
// EXPECT_THAT(actual_quaternion, IsNearQuaternion(expected_quaternion));
MATCHER_P(IsNearQuaternion, expected, "") {
if (arg == NULL) {
*result_listener << "Null quaternion";
return false;
}
// Quaternions are equivalent upto a sign change. So we will compare
// both signs before declaring failure.
bool near = true;
for (int i = 0; i < 4; i++) {
if (fabs(arg[i] - expected[i]) > kTolerance) {
near = false;
break;
}
}
if (near) {
return true;
}
near = true;
for (int i = 0; i < 4; i++) {
if (fabs(arg[i] + expected[i]) > kTolerance) {
near = false;
break;
}
}
if (near) {
return true;
}
*result_listener << "expected : "
<< expected[0] << " "
<< expected[1] << " "
<< expected[2] << " "
<< expected[3] << " "
<< "actual : "
<< arg[0] << " "
<< arg[1] << " "
<< arg[2] << " "
<< arg[3];
return false;
}
// Use as:
// double expected_axis_angle[3];
// double actual_axis_angle[3];
// EXPECT_THAT(actual_axis_angle, IsNearAngleAxis(expected_axis_angle));
MATCHER_P(IsNearAngleAxis, expected, "") {
if (arg == NULL) {
*result_listener << "Null axis/angle";
return false;
}
Eigen::Vector3d a(arg[0], arg[1], arg[2]);
Eigen::Vector3d e(expected[0], expected[1], expected[2]);
const double e_norm = e.norm();
double delta_norm = numeric_limits<double>::max();
if (e_norm > 0) {
// Deal with the sign ambiguity near PI. Since the sign can flip,
// we take the smaller of the two differences.
if (fabs(e_norm - kPi) < kLooseTolerance) {
delta_norm = min((a - e).norm(), (a + e).norm()) / e_norm;
} else {
delta_norm = (a - e).norm() / e_norm;
}
} else {
delta_norm = a.norm();
}
if (delta_norm <= kLooseTolerance) {
return true;
}
*result_listener << " arg:"
<< " " << arg[0]
<< " " << arg[1]
<< " " << arg[2]
<< " was expected to be:"
<< " " << expected[0]
<< " " << expected[1]
<< " " << expected[2];
return false;
}
// Use as:
// double matrix[9];
// EXPECT_THAT(matrix, IsOrthonormal());
MATCHER(IsOrthonormal, "") {
if (arg == NULL) {
*result_listener << "Null matrix";
return false;
}
for (int c1 = 0; c1 < 3; c1++) {
for (int c2 = 0; c2 < 3; c2++) {
double v = 0;
for (int i = 0; i < 3; i++) {
v += arg[i + 3 * c1] * arg[i + 3 * c2];
}
double expected = (c1 == c2) ? 1 : 0;
if (fabs(expected - v) > kTolerance) {
*result_listener << "Columns " << c1 << " and " << c2
<< " should have dot product " << expected
<< " but have " << v;
return false;
}
}
}
return true;
}
// Use as:
// double matrix1[9];
// double matrix2[9];
// EXPECT_THAT(matrix1, IsNear3x3Matrix(matrix2));
MATCHER_P(IsNear3x3Matrix, expected, "") {
if (arg == NULL) {
*result_listener << "Null matrix";
return false;
}
for (int i = 0; i < 9; i++) {
if (fabs(arg[i] - expected[i]) > kTolerance) {
*result_listener << "component " << i << " should be " << expected[i];
return false;
}
}
return true;
}
// Transforms a zero axis/angle to a quaternion.
TEST(Rotation, ZeroAngleAxisToQuaternion) {
double axis_angle[3] = { 0, 0, 0 };
double quaternion[4];
double expected[4] = { 1, 0, 0, 0 };
AngleAxisToQuaternion(axis_angle, quaternion);
EXPECT_THAT(quaternion, IsNormalizedQuaternion());
EXPECT_THAT(quaternion, IsNearQuaternion(expected));
}
// Test that exact conversion works for small angles.
TEST(Rotation, SmallAngleAxisToQuaternion) {
// Small, finite value to test.
double theta = 1.0e-2;
double axis_angle[3] = { theta, 0, 0 };
double quaternion[4];
double expected[4] = { cos(theta/2), sin(theta/2.0), 0, 0 };
AngleAxisToQuaternion(axis_angle, quaternion);
EXPECT_THAT(quaternion, IsNormalizedQuaternion());
EXPECT_THAT(quaternion, IsNearQuaternion(expected));
}
// Test that approximate conversion works for very small angles.
TEST(Rotation, TinyAngleAxisToQuaternion) {
// Very small value that could potentially cause underflow.
double theta = pow(numeric_limits<double>::min(), 0.75);
double axis_angle[3] = { theta, 0, 0 };
double quaternion[4];
double expected[4] = { cos(theta/2), sin(theta/2.0), 0, 0 };
AngleAxisToQuaternion(axis_angle, quaternion);
EXPECT_THAT(quaternion, IsNormalizedQuaternion());
EXPECT_THAT(quaternion, IsNearQuaternion(expected));
}
// Transforms a rotation by pi/2 around X to a quaternion.
TEST(Rotation, XRotationToQuaternion) {
double axis_angle[3] = { kPi / 2, 0, 0 };
double quaternion[4];
double expected[4] = { kHalfSqrt2, kHalfSqrt2, 0, 0 };
AngleAxisToQuaternion(axis_angle, quaternion);
EXPECT_THAT(quaternion, IsNormalizedQuaternion());
EXPECT_THAT(quaternion, IsNearQuaternion(expected));
}
// Transforms a unit quaternion to an axis angle.
TEST(Rotation, UnitQuaternionToAngleAxis) {
double quaternion[4] = { 1, 0, 0, 0 };
double axis_angle[3];
double expected[3] = { 0, 0, 0 };
QuaternionToAngleAxis(quaternion, axis_angle);
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
}
// Transforms a quaternion that rotates by pi about the Y axis to an axis angle.
TEST(Rotation, YRotationQuaternionToAngleAxis) {
double quaternion[4] = { 0, 0, 1, 0 };
double axis_angle[3];
double expected[3] = { 0, kPi, 0 };
QuaternionToAngleAxis(quaternion, axis_angle);
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
}
// Transforms a quaternion that rotates by pi/3 about the Z axis to an axis
// angle.
TEST(Rotation, ZRotationQuaternionToAngleAxis) {
double quaternion[4] = { sqrt(3) / 2, 0, 0, 0.5 };
double axis_angle[3];
double expected[3] = { 0, 0, kPi / 3 };
QuaternionToAngleAxis(quaternion, axis_angle);
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
}
// Test that exact conversion works for small angles.
TEST(Rotation, SmallQuaternionToAngleAxis) {
// Small, finite value to test.
double theta = 1.0e-2;
double quaternion[4] = { cos(theta/2), sin(theta/2.0), 0, 0 };
double axis_angle[3];
double expected[3] = { theta, 0, 0 };
QuaternionToAngleAxis(quaternion, axis_angle);
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
}
// Test that approximate conversion works for very small angles.
TEST(Rotation, TinyQuaternionToAngleAxis) {
// Very small value that could potentially cause underflow.
double theta = pow(numeric_limits<double>::min(), 0.75);
double quaternion[4] = { cos(theta/2), sin(theta/2.0), 0, 0 };
double axis_angle[3];
double expected[3] = { theta, 0, 0 };
QuaternionToAngleAxis(quaternion, axis_angle);
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected));
}
TEST(Rotation, QuaternionToAngleAxisAngleIsLessThanPi) {
double quaternion[4];
double angle_axis[3];
const double half_theta = 0.75 * kPi;
quaternion[0] = cos(half_theta);
quaternion[1] = 1.0 * sin(half_theta);
quaternion[2] = 0.0;
quaternion[3] = 0.0;
QuaternionToAngleAxis(quaternion, angle_axis);
const double angle = sqrt(angle_axis[0] * angle_axis[0] +
angle_axis[1] * angle_axis[1] +
angle_axis[2] * angle_axis[2]);
EXPECT_LE(angle, kPi);
}
static const int kNumTrials = 10000;
// Takes a bunch of random axis/angle values, converts them to quaternions,
// and back again.
TEST(Rotation, AngleAxisToQuaterionAndBack) {
srand(5);
for (int i = 0; i < kNumTrials; i++) {
double axis_angle[3];
// Make an axis by choosing three random numbers in [-1, 1) and
// normalizing.
double norm = 0;
for (int i = 0; i < 3; i++) {
axis_angle[i] = RandDouble() * 2 - 1;
norm += axis_angle[i] * axis_angle[i];
}
norm = sqrt(norm);
// Angle in [-pi, pi).
double theta = kPi * 2 * RandDouble() - kPi;
for (int i = 0; i < 3; i++) {
axis_angle[i] = axis_angle[i] * theta / norm;
}
double quaternion[4];
double round_trip[3];
// We use ASSERTs here because if there's one failure, there are
// probably many and spewing a million failures doesn't make anyone's
// day.
AngleAxisToQuaternion(axis_angle, quaternion);
ASSERT_THAT(quaternion, IsNormalizedQuaternion());
QuaternionToAngleAxis(quaternion, round_trip);
ASSERT_THAT(round_trip, IsNearAngleAxis(axis_angle));
}
}
// Takes a bunch of random quaternions, converts them to axis/angle,
// and back again.
TEST(Rotation, QuaterionToAngleAxisAndBack) {
srand(5);
for (int i = 0; i < kNumTrials; i++) {
double quaternion[4];
// Choose four random numbers in [-1, 1) and normalize.
double norm = 0;
for (int i = 0; i < 4; i++) {
quaternion[i] = RandDouble() * 2 - 1;
norm += quaternion[i] * quaternion[i];
}
norm = sqrt(norm);
for (int i = 0; i < 4; i++) {
quaternion[i] = quaternion[i] / norm;
}
double axis_angle[3];
double round_trip[4];
QuaternionToAngleAxis(quaternion, axis_angle);
AngleAxisToQuaternion(axis_angle, round_trip);
ASSERT_THAT(round_trip, IsNormalizedQuaternion());
ASSERT_THAT(round_trip, IsNearQuaternion(quaternion));
}
}
// Transforms a zero axis/angle to a rotation matrix.
TEST(Rotation, ZeroAngleAxisToRotationMatrix) {
double axis_angle[3] = { 0, 0, 0 };
double matrix[9];
double expected[9] = { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
AngleAxisToRotationMatrix(axis_angle, matrix);
EXPECT_THAT(matrix, IsOrthonormal());
EXPECT_THAT(matrix, IsNear3x3Matrix(expected));
}
TEST(Rotation, NearZeroAngleAxisToRotationMatrix) {
double axis_angle[3] = { 1e-24, 2e-24, 3e-24 };
double matrix[9];
double expected[9] = { 1, 0, 0, 0, 1, 0, 0, 0, 1 };
AngleAxisToRotationMatrix(axis_angle, matrix);
EXPECT_THAT(matrix, IsOrthonormal());
EXPECT_THAT(matrix, IsNear3x3Matrix(expected));
}
// Transforms a rotation by pi/2 around X to a rotation matrix and back.
TEST(Rotation, XRotationToRotationMatrix) {
double axis_angle[3] = { kPi / 2, 0, 0 };
double matrix[9];
// The rotation matrices are stored column-major.
double expected[9] = { 1, 0, 0, 0, 0, 1, 0, -1, 0 };
AngleAxisToRotationMatrix(axis_angle, matrix);
EXPECT_THAT(matrix, IsOrthonormal());
EXPECT_THAT(matrix, IsNear3x3Matrix(expected));
double round_trip[3];
RotationMatrixToAngleAxis(matrix, round_trip);
EXPECT_THAT(round_trip, IsNearAngleAxis(axis_angle));
}
// Transforms an axis angle that rotates by pi about the Y axis to a
// rotation matrix and back.
TEST(Rotation, YRotationToRotationMatrix) {
double axis_angle[3] = { 0, kPi, 0 };
double matrix[9];
double expected[9] = { -1, 0, 0, 0, 1, 0, 0, 0, -1 };
AngleAxisToRotationMatrix(axis_angle, matrix);
EXPECT_THAT(matrix, IsOrthonormal());
EXPECT_THAT(matrix, IsNear3x3Matrix(expected));
double round_trip[3];
RotationMatrixToAngleAxis(matrix, round_trip);
EXPECT_THAT(round_trip, IsNearAngleAxis(axis_angle));
}
TEST(Rotation, NearPiAngleAxisRoundTrip) {
double in_axis_angle[3];
double matrix[9];
double out_axis_angle[3];
srand(5);
for (int i = 0; i < kNumTrials; i++) {
// Make an axis by choosing three random numbers in [-1, 1) and
// normalizing.
double norm = 0;
for (int i = 0; i < 3; i++) {
in_axis_angle[i] = RandDouble() * 2 - 1;
norm += in_axis_angle[i] * in_axis_angle[i];
}
norm = sqrt(norm);
// Angle in [pi - kMaxSmallAngle, pi).
const double kMaxSmallAngle = 1e-8;
double theta = kPi - kMaxSmallAngle * RandDouble();
for (int i = 0; i < 3; i++) {
in_axis_angle[i] *= (theta / norm);
}
AngleAxisToRotationMatrix(in_axis_angle, matrix);
RotationMatrixToAngleAxis(matrix, out_axis_angle);
EXPECT_THAT(in_axis_angle, IsNearAngleAxis(out_axis_angle));
}
}
TEST(Rotation, AtPiAngleAxisRoundTrip) {
// A rotation of kPi about the X axis;
static const double kMatrix[3][3] = {
{1.0, 0.0, 0.0},
{0.0, -1.0, 0.0},
{0.0, 0.0, -1.0}
};
double in_matrix[9];
// Fill it from kMatrix in col-major order.
for (int j = 0, k = 0; j < 3; ++j) {
for (int i = 0; i < 3; ++i, ++k) {
in_matrix[k] = kMatrix[i][j];
}
}
const double expected_axis_angle[3] = { kPi, 0, 0 };
double out_matrix[9];
double axis_angle[3];
RotationMatrixToAngleAxis(in_matrix, axis_angle);
AngleAxisToRotationMatrix(axis_angle, out_matrix);
LOG(INFO) << "AngleAxis = " << axis_angle[0] << " " << axis_angle[1]
<< " " << axis_angle[2];
LOG(INFO) << "Expected AngleAxis = " << kPi << " 0 0";
double out_rowmajor[3][3];
for (int j = 0, k = 0; j < 3; ++j) {
for (int i = 0; i < 3; ++i, ++k) {
out_rowmajor[i][j] = out_matrix[k];
}
}
LOG(INFO) << "Rotation:";
LOG(INFO) << "EXPECTED | ACTUAL";
for (int i = 0; i < 3; ++i) {
string line;
for (int j = 0; j < 3; ++j) {
StringAppendF(&line, "%g ", kMatrix[i][j]);
}
line += " | ";
for (int j = 0; j < 3; ++j) {
StringAppendF(&line, "%g ", out_rowmajor[i][j]);
}
LOG(INFO) << line;
}
EXPECT_THAT(axis_angle, IsNearAngleAxis(expected_axis_angle));
EXPECT_THAT(out_matrix, IsNear3x3Matrix(in_matrix));
}
// Transforms an axis angle that rotates by pi/3 about the Z axis to a
// rotation matrix.
TEST(Rotation, ZRotationToRotationMatrix) {
double axis_angle[3] = { 0, 0, kPi / 3 };
double matrix[9];
// This is laid-out row-major on the screen but is actually stored
// column-major.
double expected[9] = { 0.5, sqrt(3) / 2, 0, // Column 1
-sqrt(3) / 2, 0.5, 0, // Column 2
0, 0, 1 }; // Column 3
AngleAxisToRotationMatrix(axis_angle, matrix);
EXPECT_THAT(matrix, IsOrthonormal());
EXPECT_THAT(matrix, IsNear3x3Matrix(expected));
double round_trip[3];
RotationMatrixToAngleAxis(matrix, round_trip);
EXPECT_THAT(round_trip, IsNearAngleAxis(axis_angle));
}
// Takes a bunch of random axis/angle values, converts them to rotation
// matrices, and back again.
TEST(Rotation, AngleAxisToRotationMatrixAndBack) {
srand(5);
for (int i = 0; i < kNumTrials; i++) {
double axis_angle[3];
// Make an axis by choosing three random numbers in [-1, 1) and
// normalizing.
double norm = 0;
for (int i = 0; i < 3; i++) {
axis_angle[i] = RandDouble() * 2 - 1;
norm += axis_angle[i] * axis_angle[i];
}
norm = sqrt(norm);
// Angle in [-pi, pi).
double theta = kPi * 2 * RandDouble() - kPi;
for (int i = 0; i < 3; i++) {
axis_angle[i] = axis_angle[i] * theta / norm;
}
double matrix[9];
double round_trip[3];
AngleAxisToRotationMatrix(axis_angle, matrix);
ASSERT_THAT(matrix, IsOrthonormal());
RotationMatrixToAngleAxis(matrix, round_trip);
for (int i = 0; i < 3; ++i) {
EXPECT_NEAR(round_trip[i], axis_angle[i], kLooseTolerance);
}
}
}
// Takes a bunch of random axis/angle values near zero, converts them
// to rotation matrices, and back again.
TEST(Rotation, AngleAxisToRotationMatrixAndBackNearZero) {
srand(5);
for (int i = 0; i < kNumTrials; i++) {
double axis_angle[3];
// Make an axis by choosing three random numbers in [-1, 1) and
// normalizing.
double norm = 0;
for (int i = 0; i < 3; i++) {
axis_angle[i] = RandDouble() * 2 - 1;
norm += axis_angle[i] * axis_angle[i];
}
norm = sqrt(norm);
// Tiny theta.
double theta = 1e-16 * (kPi * 2 * RandDouble() - kPi);
for (int i = 0; i < 3; i++) {
axis_angle[i] = axis_angle[i] * theta / norm;
}
double matrix[9];
double round_trip[3];
AngleAxisToRotationMatrix(axis_angle, matrix);
ASSERT_THAT(matrix, IsOrthonormal());
RotationMatrixToAngleAxis(matrix, round_trip);
for (int i = 0; i < 3; ++i) {
EXPECT_NEAR(round_trip[i], axis_angle[i],
numeric_limits<double>::epsilon());
}
}
}
// Transposes a 3x3 matrix.
static void Transpose3x3(double m[9]) {
swap(m[1], m[3]);
swap(m[2], m[6]);
swap(m[5], m[7]);
}
// Convert Euler angles from radians to degrees.
static void ToDegrees(double euler_angles[3]) {
for (int i = 0; i < 3; ++i) {
euler_angles[i] *= 180.0 / kPi;
}
}
// Compare the 3x3 rotation matrices produced by the axis-angle
// rotation 'aa' and the Euler angle rotation 'ea' (in radians).
static void CompareEulerToAngleAxis(double aa[3], double ea[3]) {
double aa_matrix[9];
AngleAxisToRotationMatrix(aa, aa_matrix);
Transpose3x3(aa_matrix); // Column to row major order.
double ea_matrix[9];
ToDegrees(ea); // Radians to degrees.
const int kRowStride = 3;
EulerAnglesToRotationMatrix(ea, kRowStride, ea_matrix);
EXPECT_THAT(aa_matrix, IsOrthonormal());
EXPECT_THAT(ea_matrix, IsOrthonormal());
EXPECT_THAT(ea_matrix, IsNear3x3Matrix(aa_matrix));
}
// Test with rotation axis along the x/y/z axes.
// Also test zero rotation.
TEST(EulerAnglesToRotationMatrix, OnAxis) {
int n_tests = 0;
for (double x = -1.0; x <= 1.0; x += 1.0) {
for (double y = -1.0; y <= 1.0; y += 1.0) {
for (double z = -1.0; z <= 1.0; z += 1.0) {
if ((x != 0) + (y != 0) + (z != 0) > 1)
continue;
double axis_angle[3] = {x, y, z};
double euler_angles[3] = {x, y, z};
CompareEulerToAngleAxis(axis_angle, euler_angles);
++n_tests;
}
}
}
CHECK_EQ(7, n_tests);
}
// Test that a random rotation produces an orthonormal rotation
// matrix.
TEST(EulerAnglesToRotationMatrix, IsOrthonormal) {
srand(5);
for (int trial = 0; trial < kNumTrials; ++trial) {
double euler_angles_degrees[3];
for (int i = 0; i < 3; ++i) {
euler_angles_degrees[i] = RandDouble() * 360.0 - 180.0;
}
double rotation_matrix[9];
EulerAnglesToRotationMatrix(euler_angles_degrees, 3, rotation_matrix);
EXPECT_THAT(rotation_matrix, IsOrthonormal());
}
}
// Tests using Jets for specific behavior involving auto differentiation
// near singularity points.
typedef Jet<double, 3> J3;
typedef Jet<double, 4> J4;
J3 MakeJ3(double a, double v0, double v1, double v2) {
J3 j;
j.a = a;
j.v[0] = v0;
j.v[1] = v1;
j.v[2] = v2;
return j;
}
J4 MakeJ4(double a, double v0, double v1, double v2, double v3) {
J4 j;
j.a = a;
j.v[0] = v0;
j.v[1] = v1;
j.v[2] = v2;
j.v[3] = v3;
return j;
}
bool IsClose(double x, double y) {
EXPECT_FALSE(IsNaN(x));
EXPECT_FALSE(IsNaN(y));
return internal::IsClose(x, y, kTolerance, NULL, NULL);
}
template <int N>
bool IsClose(const Jet<double, N> &x, const Jet<double, N> &y) {
if (!IsClose(x.a, y.a)) {
return false;
}
for (int i = 0; i < N; i++) {
if (!IsClose(x.v[i], y.v[i])) {
return false;
}
}
return true;
}
template <int M, int N>
void ExpectJetArraysClose(const Jet<double, N> *x, const Jet<double, N> *y) {
for (int i = 0; i < M; i++) {
if (!IsClose(x[i], y[i])) {
LOG(ERROR) << "Jet " << i << "/" << M << " not equal";
LOG(ERROR) << "x[" << i << "]: " << x[i];
LOG(ERROR) << "y[" << i << "]: " << y[i];
Jet<double, N> d, zero;
d.a = y[i].a - x[i].a;
for (int j = 0; j < N; j++) {
d.v[j] = y[i].v[j] - x[i].v[j];
}
LOG(ERROR) << "diff: " << d;
EXPECT_TRUE(IsClose(x[i], y[i]));
}
}
}
// Log-10 of a value well below machine precision.
static const int kSmallTinyCutoff =
static_cast<int>(2 * log(numeric_limits<double>::epsilon())/log(10.0));
// Log-10 of a value just below values representable by double.
static const int kTinyZeroLimit =
static_cast<int>(1 + log(numeric_limits<double>::min())/log(10.0));
// Test that exact conversion works for small angles when jets are used.
TEST(Rotation, SmallAngleAxisToQuaternionForJets) {
// Examine small x rotations that are still large enough
// to be well within the range represented by doubles.
for (int i = -2; i >= kSmallTinyCutoff; i--) {
double theta = pow(10.0, i);
J3 axis_angle[3] = { J3(theta, 0), J3(0, 1), J3(0, 2) };
J3 quaternion[4];
J3 expected[4] = {
MakeJ3(cos(theta/2), -sin(theta/2)/2, 0, 0),
MakeJ3(sin(theta/2), cos(theta/2)/2, 0, 0),
MakeJ3(0, 0, sin(theta/2)/theta, 0),
MakeJ3(0, 0, 0, sin(theta/2)/theta),
};
AngleAxisToQuaternion(axis_angle, quaternion);
ExpectJetArraysClose<4, 3>(quaternion, expected);
}
}
// Test that conversion works for very small angles when jets are used.
TEST(Rotation, TinyAngleAxisToQuaternionForJets) {
// Examine tiny x rotations that extend all the way to where
// underflow occurs.
for (int i = kSmallTinyCutoff; i >= kTinyZeroLimit; i--) {
double theta = pow(10.0, i);
J3 axis_angle[3] = { J3(theta, 0), J3(0, 1), J3(0, 2) };
J3 quaternion[4];
// To avoid loss of precision in the test itself,
// a finite expansion is used here, which will
// be exact up to machine precision for the test values used.
J3 expected[4] = {
MakeJ3(1.0, 0, 0, 0),
MakeJ3(0, 0.5, 0, 0),
MakeJ3(0, 0, 0.5, 0),
MakeJ3(0, 0, 0, 0.5),
};
AngleAxisToQuaternion(axis_angle, quaternion);
ExpectJetArraysClose<4, 3>(quaternion, expected);
}
}
// Test that derivatives are correct for zero rotation.
TEST(Rotation, ZeroAngleAxisToQuaternionForJets) {
J3 axis_angle[3] = { J3(0, 0), J3(0, 1), J3(0, 2) };
J3 quaternion[4];
J3 expected[4] = {
MakeJ3(1.0, 0, 0, 0),
MakeJ3(0, 0.5, 0, 0),
MakeJ3(0, 0, 0.5, 0),
MakeJ3(0, 0, 0, 0.5),
};
AngleAxisToQuaternion(axis_angle, quaternion);
ExpectJetArraysClose<4, 3>(quaternion, expected);
}
// Test that exact conversion works for small angles.
TEST(Rotation, SmallQuaternionToAngleAxisForJets) {
// Examine small x rotations that are still large enough
// to be well within the range represented by doubles.
for (int i = -2; i >= kSmallTinyCutoff; i--) {
double theta = pow(10.0, i);
double s = sin(theta);
double c = cos(theta);
J4 quaternion[4] = { J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3) };
J4 axis_angle[3];
J4 expected[3] = {
MakeJ4(2*theta, -2*s, 2*c, 0, 0),
MakeJ4(0, 0, 0, 2*theta/s, 0),
MakeJ4(0, 0, 0, 0, 2*theta/s),
};
QuaternionToAngleAxis(quaternion, axis_angle);
ExpectJetArraysClose<3, 4>(axis_angle, expected);
}
}
// Test that conversion works for very small angles.
TEST(Rotation, TinyQuaternionToAngleAxisForJets) {
// Examine tiny x rotations that extend all the way to where
// underflow occurs.
for (int i = kSmallTinyCutoff; i >= kTinyZeroLimit; i--) {
double theta = pow(10.0, i);
double s = sin(theta);
double c = cos(theta);
J4 quaternion[4] = { J4(c, 0), J4(s, 1), J4(0, 2), J4(0, 3) };
J4 axis_angle[3];
// To avoid loss of precision in the test itself,
// a finite expansion is used here, which will
// be exact up to machine precision for the test values used.
J4 expected[3] = {
MakeJ4(2*theta, -2*s, 2.0, 0, 0),
MakeJ4(0, 0, 0, 2.0, 0),
MakeJ4(0, 0, 0, 0, 2.0),
};
QuaternionToAngleAxis(quaternion, axis_angle);
ExpectJetArraysClose<3, 4>(axis_angle, expected);
}
}
// Test that conversion works for no rotation.
TEST(Rotation, ZeroQuaternionToAngleAxisForJets) {
J4 quaternion[4] = { J4(1, 0), J4(0, 1), J4(0, 2), J4(0, 3) };
J4 axis_angle[3];
J4 expected[3] = {
MakeJ4(0, 0, 2.0, 0, 0),
MakeJ4(0, 0, 0, 2.0, 0),
MakeJ4(0, 0, 0, 0, 2.0),
};
QuaternionToAngleAxis(quaternion, axis_angle);
ExpectJetArraysClose<3, 4>(axis_angle, expected);
}
TEST(Quaternion, RotatePointGivesSameAnswerAsRotationByMatrixCanned) {
// Canned data generated in octave.
double const q[4] = {
+0.1956830471754074,
-0.0150618562474847,
+0.7634572982788086,
-0.3019454777240753,
};
double const Q[3][3] = { // Scaled rotation matrix.
{ -0.6355194033477252, 0.0951730541682254, 0.3078870197911186 },
{ -0.1411693904792992, 0.5297609702153905, -0.4551502574482019 },
{ -0.2896955822708862, -0.4669396571547050, -0.4536309793389248 },
};
double const R[3][3] = { // With unit rows and columns.
{ -0.8918859164053080, 0.1335655625725649, 0.4320876677394745 },
{ -0.1981166751680096, 0.7434648665444399, -0.6387564287225856 },
{ -0.4065578619806013, -0.6553016349046693, -0.6366242786393164 },
};
// Compute R from q and compare to known answer.
double Rq[3][3];
QuaternionToScaledRotation<double>(q, Rq[0]);
ExpectArraysClose(9, Q[0], Rq[0], kTolerance);
// Now do the same but compute R with normalization.
QuaternionToRotation<double>(q, Rq[0]);
ExpectArraysClose(9, R[0], Rq[0], kTolerance);
}
TEST(Quaternion, RotatePointGivesSameAnswerAsRotationByMatrix) {
// Rotation defined by a unit quaternion.
double const q[4] = {
0.2318160216097109,
-0.0178430356832060,
0.9044300776717159,
-0.3576998641394597,
};
double const p[3] = {
+0.11,
-13.15,
1.17,
};
double R[3 * 3];
QuaternionToRotation(q, R);
double result1[3];
UnitQuaternionRotatePoint(q, p, result1);
double result2[3];
VectorRef(result2, 3) = ConstMatrixRef(R, 3, 3)* ConstVectorRef(p, 3);
ExpectArraysClose(3, result1, result2, kTolerance);
}
// Verify that (a * b) * c == a * (b * c).
TEST(Quaternion, MultiplicationIsAssociative) {
double a[4];
double b[4];
double c[4];
for (int i = 0; i < 4; ++i) {
a[i] = 2 * RandDouble() - 1;
b[i] = 2 * RandDouble() - 1;
c[i] = 2 * RandDouble() - 1;
}
double ab[4];
double ab_c[4];
QuaternionProduct(a, b, ab);
QuaternionProduct(ab, c, ab_c);
double bc[4];
double a_bc[4];
QuaternionProduct(b, c, bc);
QuaternionProduct(a, bc, a_bc);
ASSERT_NEAR(ab_c[0], a_bc[0], kTolerance);
ASSERT_NEAR(ab_c[1], a_bc[1], kTolerance);
ASSERT_NEAR(ab_c[2], a_bc[2], kTolerance);
ASSERT_NEAR(ab_c[3], a_bc[3], kTolerance);
}
TEST(AngleAxis, RotatePointGivesSameAnswerAsRotationMatrix) {
double angle_axis[3];
double R[9];
double p[3];
double angle_axis_rotated_p[3];
double rotation_matrix_rotated_p[3];
for (int i = 0; i < 10000; ++i) {
double theta = (2.0 * i * 0.0011 - 1.0) * kPi;
for (int j = 0; j < 50; ++j) {
double norm2 = 0.0;
for (int k = 0; k < 3; ++k) {
angle_axis[k] = 2.0 * RandDouble() - 1.0;
p[k] = 2.0 * RandDouble() - 1.0;
norm2 = angle_axis[k] * angle_axis[k];
}
const double inv_norm = theta / sqrt(norm2);
for (int k = 0; k < 3; ++k) {
angle_axis[k] *= inv_norm;
}
AngleAxisToRotationMatrix(angle_axis, R);
rotation_matrix_rotated_p[0] = R[0] * p[0] + R[3] * p[1] + R[6] * p[2];
rotation_matrix_rotated_p[1] = R[1] * p[0] + R[4] * p[1] + R[7] * p[2];
rotation_matrix_rotated_p[2] = R[2] * p[0] + R[5] * p[1] + R[8] * p[2];
AngleAxisRotatePoint(angle_axis, p, angle_axis_rotated_p);
for (int k = 0; k < 3; ++k) {
EXPECT_NEAR(rotation_matrix_rotated_p[k],
angle_axis_rotated_p[k],
kTolerance) << "p: " << p[0]
<< " " << p[1]
<< " " << p[2]
<< " angle_axis: " << angle_axis[0]
<< " " << angle_axis[1]
<< " " << angle_axis[2];
}
}
}
}
TEST(AngleAxis, NearZeroRotatePointGivesSameAnswerAsRotationMatrix) {
double angle_axis[3];
double R[9];
double p[3];
double angle_axis_rotated_p[3];
double rotation_matrix_rotated_p[3];
for (int i = 0; i < 10000; ++i) {
double norm2 = 0.0;
for (int k = 0; k < 3; ++k) {
angle_axis[k] = 2.0 * RandDouble() - 1.0;
p[k] = 2.0 * RandDouble() - 1.0;
norm2 = angle_axis[k] * angle_axis[k];
}
double theta = (2.0 * i * 0.0001 - 1.0) * 1e-16;
const double inv_norm = theta / sqrt(norm2);
for (int k = 0; k < 3; ++k) {
angle_axis[k] *= inv_norm;
}
AngleAxisToRotationMatrix(angle_axis, R);
rotation_matrix_rotated_p[0] = R[0] * p[0] + R[3] * p[1] + R[6] * p[2];
rotation_matrix_rotated_p[1] = R[1] * p[0] + R[4] * p[1] + R[7] * p[2];
rotation_matrix_rotated_p[2] = R[2] * p[0] + R[5] * p[1] + R[8] * p[2];
AngleAxisRotatePoint(angle_axis, p, angle_axis_rotated_p);
for (int k = 0; k < 3; ++k) {
EXPECT_NEAR(rotation_matrix_rotated_p[k],
angle_axis_rotated_p[k],
kTolerance) << "p: " << p[0]
<< " " << p[1]
<< " " << p[2]
<< " angle_axis: " << angle_axis[0]
<< " " << angle_axis[1]
<< " " << angle_axis[2];
}
}
}
TEST(MatrixAdapter, RowMajor3x3ReturnTypeAndAccessIsCorrect) {
double array[9] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 };
const float const_array[9] =
{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f };
MatrixAdapter<double, 3, 1> A = RowMajorAdapter3x3(array);
MatrixAdapter<const float, 3, 1> B = RowMajorAdapter3x3(const_array);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
// The values are integers from 1 to 9, so equality tests are appropriate
// even for float and double values.
EXPECT_EQ(A(i, j), array[3*i+j]);
EXPECT_EQ(B(i, j), const_array[3*i+j]);
}
}
}
TEST(MatrixAdapter, ColumnMajor3x3ReturnTypeAndAccessIsCorrect) {
double array[9] = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 };
const float const_array[9] =
{ 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f };
MatrixAdapter<double, 1, 3> A = ColumnMajorAdapter3x3(array);
MatrixAdapter<const float, 1, 3> B = ColumnMajorAdapter3x3(const_array);
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
// The values are integers from 1 to 9, so equality tests are
// appropriate even for float and double values.
EXPECT_EQ(A(i, j), array[3*j+i]);
EXPECT_EQ(B(i, j), const_array[3*j+i]);
}
}
}
TEST(MatrixAdapter, RowMajor2x4IsCorrect) {
const int expected[8] = { 1, 2, 3, 4, 5, 6, 7, 8 };
int array[8];
MatrixAdapter<int, 4, 1> M(array);
M(0, 0) = 1; M(0, 1) = 2; M(0, 2) = 3; M(0, 3) = 4;
M(1, 0) = 5; M(1, 1) = 6; M(1, 2) = 7; M(1, 3) = 8;
for (int k = 0; k < 8; ++k) {
EXPECT_EQ(array[k], expected[k]);
}
}
TEST(MatrixAdapter, ColumnMajor2x4IsCorrect) {
const int expected[8] = { 1, 5, 2, 6, 3, 7, 4, 8 };
int array[8];
MatrixAdapter<int, 1, 2> M(array);
M(0, 0) = 1; M(0, 1) = 2; M(0, 2) = 3; M(0, 3) = 4;
M(1, 0) = 5; M(1, 1) = 6; M(1, 2) = 7; M(1, 3) = 8;
for (int k = 0; k < 8; ++k) {
EXPECT_EQ(array[k], expected[k]);
}
}
TEST(RotationMatrixToAngleAxis, NearPiExampleOneFromTobiasStrauss) {
// Example from Tobias Strauss
const double rotation_matrix[] = {
-0.999807135425239, -0.0128154391194470, -0.0148814136745799,
-0.0128154391194470, -0.148441438622958, 0.988838158557669,
-0.0148814136745799, 0.988838158557669, 0.148248574048196
};
double angle_axis[3];
RotationMatrixToAngleAxis(RowMajorAdapter3x3(rotation_matrix), angle_axis);
double round_trip[9];
AngleAxisToRotationMatrix(angle_axis, RowMajorAdapter3x3(round_trip));
EXPECT_THAT(rotation_matrix, IsNear3x3Matrix(round_trip));
}
void CheckRotationMatrixToAngleAxisRoundTrip(const double theta,
const double phi,
const double angle) {
double angle_axis[3];
angle_axis[0] = angle * sin(phi) * cos(theta);
angle_axis[1] = angle * sin(phi) * sin(theta);
angle_axis[2] = angle * cos(phi);
double rotation_matrix[9];
AngleAxisToRotationMatrix(angle_axis, rotation_matrix);
double angle_axis_round_trip[3];
RotationMatrixToAngleAxis(rotation_matrix, angle_axis_round_trip);
EXPECT_THAT(angle_axis_round_trip, IsNearAngleAxis(angle_axis));
}
TEST(RotationMatrixToAngleAxis, ExhaustiveRoundTrip) {
const double kMaxSmallAngle = 1e-8;
const int kNumSteps = 1000;
for (int i = 0; i < kNumSteps; ++i) {
const double theta = static_cast<double>(i) / kNumSteps * 2.0 * kPi;
for (int j = 0; j < kNumSteps; ++j) {
const double phi = static_cast<double>(j) / kNumSteps * kPi;
// Rotations of angle Pi.
CheckRotationMatrixToAngleAxisRoundTrip(theta, phi, kPi);
// Rotation of angle approximately Pi.
CheckRotationMatrixToAngleAxisRoundTrip(
theta, phi, kPi - kMaxSmallAngle * RandDouble());
// Rotations of angle approximately zero.
CheckRotationMatrixToAngleAxisRoundTrip(
theta, phi, kMaxSmallAngle * 2.0 * RandDouble() - 1.0);
}
}
}
} // namespace internal
} // namespace ceres