From 79a554ffcf33156b81f3098e4e67821da867e2d6 Mon Sep 17 00:00:00 2001 From: Sameer Agarwal Date: Fri, 13 Jan 2023 11:37:27 -0800 Subject: [PATCH] Fix a bug in QuaternionRotatePoint. In https://ceres-solver-review.git.corp.google.com/c/ceres-solver/+/23802 the computation of the norm of a quaternion scale = 1/sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); was replaced by scale = 1/hypot(q[0], q[1], hypot(q[2], q[3])); while this appear to be a more accurate computation because of the use of hypot which can handle over and underflow it introduces a bug for the case where q[2] = q[3] = 0. While the hypot(q[2], q[3]) == 0 as scalars, if q[2] and q[3] are jets, then the derivative will be NaN. Which means that even though q[0] or q[1] is non-zero and the norm of the quaternion is non-zero, and the resulting derivative is finite, this way of computing the scale will produce nans in the derivative of scale. The following quaternion will replicate the problem described above. using Jet = ceres::Jet; std::array quaternion = {Jet(1.0, 0), Jet(0.0, 1), Jet(0.0, 2), Jet(0.0, 3)}; This CL reverts the change to QuaternionRotatePoint and adds a test for it. Thanks to Jonathan Taylor for reproducing this bug. Change-Id: I0fbbcc77d6945a38563d82efba4429f4b5278cd5 --- include/ceres/rotation.h | 4 ++-- internal/ceres/rotation_test.cc | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/ceres/rotation.h b/include/ceres/rotation.h index 1220173dd..ab1e69e6a 100644 --- a/include/ceres/rotation.h +++ b/include/ceres/rotation.h @@ -743,10 +743,10 @@ inline void UnitQuaternionRotatePoint(const T q[4], template inline void QuaternionRotatePoint(const T q[4], const T pt[3], T result[3]) { DCHECK_NE(pt, result) << "Inplace rotation is not supported."; - using std::hypot; // 'scale' is 1 / norm(q). - const T scale = T(1) / hypot(q[0], q[1], hypot(q[2], q[3])); + const T scale = + T(1) / sqrt(q[0] * q[0] + q[1] * q[1] + q[2] * q[2] + q[3] * q[3]); // Make unit-norm version of q. const T unit[4] = { diff --git a/internal/ceres/rotation_test.cc b/internal/ceres/rotation_test.cc index b59c48737..28a4271c4 100644 --- a/internal/ceres/rotation_test.cc +++ b/internal/ceres/rotation_test.cc @@ -30,6 +30,7 @@ #include "ceres/rotation.h" +#include #include #include #include @@ -969,8 +970,8 @@ void ExpectJetArraysClose(const Jet* x, const Jet* y) { } // Log-10 of a value well below machine precision. -static const int kSmallTinyCutoff = - static_cast(2 * log(std::numeric_limits::epsilon()) / log(10.0)); +static const int kSmallTinyCutoff = static_cast( + 2 * log(std::numeric_limits::epsilon()) / log(10.0)); // Log-10 of a value just below values representable by double. static const int kTinyZeroLimit = @@ -1221,6 +1222,23 @@ TEST(AngleAxis, RotatePointGivesSameAnswerAsRotationMatrix) { } } +TEST(Quaternion, UnitQuaternion) { + using Jet = ceres::Jet; + std::array quaternion = { + Jet(1.0, 0), Jet(0.0, 1), Jet(0.0, 2), Jet(0.0, 3)}; + std::array point = {Jet(0.0), Jet(0.0), Jet(0.0)}; + Eigen::Vector3 rotated_point; + QuaternionRotatePoint(quaternion.data(), point.data(), rotated_point.data()); + LOG(INFO) << rotated_point[0]; + LOG(INFO) << rotated_point[1]; + LOG(INFO) << rotated_point[2]; + + for (int i = 0; i < 3; ++i) { + EXPECT_EQ(rotated_point[i], point[i]); + EXPECT_FALSE(rotated_point[i].v.array().isNaN().any()); + } +} + TEST(AngleAxis, NearZeroRotatePointGivesSameAnswerAsRotationMatrix) { std::mt19937 prng; std::uniform_real_distribution uniform_distribution{-1.0, 1.0};