SO(3) Logarithm
AI Chat Series
Y-2025
SO3
AI_Assistant_Programming
]
本文是AI Chat系列文章的第2篇,主要介绍实现SO(3)的对数运算。
Introduction
有一段SO(3)的对数操作的代码,如下所示:
/* Logrithm of a Rotation Matrix */
template <typename T>
Matrix<T, 3, 1> Log(const Matrix<T, 3, 3>& R) {
const T tr = R.trace();
T theta = (tr > 3.0 - 1e-6 || tr < -1.0 + 1e-6) ? 0.0 : std::acos(0.5 * (tr - 1));
Matrix<T, 3, 1> K(R(2, 1) - R(1, 2), R(0, 2) - R(2, 0), R(1, 0) - R(0, 1));
return (std::abs(theta) < 0.001) ? (0.5 * K) : (0.5 * theta / std::sin(theta) * K);
}
我想让AI帮我检查一下,是否有潜在的Issues
Ask the AI
Me: this function is used to calculate the tangent of the Rotation manifold, so please check its validity
AI: This function calculates the logarithm map of a rotation matrix (SO(3) -> so(3)), converting a rotation matrix to its corresponding rotation vector (axis-angle representation). Let me analyze its validity:
- Mathematical Background:
- For a rotation matrix R ∈ SO(3), its logarithm gives a skew-symmetric matrix in so(3)
- The formula used is based on Rodrigues’ formula in reverse
- The result is a 3×1 vector ω where [ω]ₓ is the skew-symmetric matrix
- Code Analysis:
// Trace is used to find rotation angle const T tr = R.trace(); // Calculate rotation angle theta T theta = (tr > 3.0 - 1e-6 || tr < -1.0 + 1e-6) ? 0.0 : std::acos(0.5 * (tr - 1)); // Extract the skew-symmetric part wlt::Matrix<T, 3, 1> K(R(2, 1) - R(1, 2), // [R - R^T]_23 R(0, 2) - R(2, 0), // [R - R^T]_31 R(1, 0) - R(0, 1)); // [R - R^T]_12 // Scale based on angle return (std::abs(theta) < 0.001) ? (0.5 * K) : (0.5 * theta / std::sin(theta) * K);
- Validity Check:
- ✅ Trace formula is correct: tr(R) = 1 + 2cos(θ)
- ✅ Special case handling when θ ≈ 0 (identity rotation)
- ✅ Extraction of skew-symmetric part is correct
- ✅ Scaling factor θ/(2sin(θ)) is correct for non-zero angles
- Potential Issues:
- ⚠️ The threshold 1e-6 for trace comparison might be too tight for some applications
- ⚠️ No handling for the case when θ = π (180° rotation)
- ⚠️ Could have numerical stability issues near θ = π
Here’s an improved version:
template <typename T>
wlt::Matrix<T, 3, 1> Log(const wlt::Matrix<T, 3, 3>& R) {
const T tr = R.trace();
const T cos_theta = (tr - 1) * 0.5;
// Clamp cos_theta to [-1,1] to handle numerical errors
T theta = std::acos(std::max(T(-1), std::min(T(1), cos_theta)));
// Extract skew-symmetric part
wlt::Matrix<T, 3, 1> K(R(2, 1) - R(1, 2),
R(0, 2) - R(2, 0),
R(1, 0) - R(0, 1));
if (std::abs(theta) < T(1e-10)) {
// Near identity rotation
return T(0.5) * K;
} else if (std::abs(theta - M_PI) < T(1e-10)) {
// 180-degree rotation case
// Need to extract rotation axis from R+I
wlt::Matrix<T, 3, 1> axis;
T max_diag = R(0,0) + 1;
int max_idx = 0;
for (int i = 1; i < 3; ++i) {
if (R(i,i) + 1 > max_diag) {
max_diag = R(i,i) + 1;
max_idx = i;
}
}
axis = wlt::Matrix<T, 3, 1>(R(max_idx,0), R(max_idx,1), R(max_idx,2));
axis += wlt::Matrix<T, 3, 1>::Unit(max_idx);
axis.normalize();
return theta * axis;
} else {
// General case
return (T(0.5) * theta / std::sin(theta)) * K;
}
}
The improvements include:
- Better handling of numerical stability near identity
- Added handling for 180-degree rotations
- Clamping of cos_theta to prevent acos domain errors
- More robust threshold checks
This implementation should handle all cases in SO(3) more robustly while maintaining the mathematical correctness of the logarithm map.
Me: please give me more details about the case when 180-degree rotation
AI: Let me explain the 180-degree rotation case in detail:
When θ = π (180°), we have special issues because:
- sin(θ) = 0, making the formula θ/(2sin(θ)) undefined
- The trace = -1, making it hard to determine the rotation axis
Here’s a detailed analysis:
template <typename T>
wlt::Matrix<T, 3, 1> Log(const wlt::Matrix<T, 3, 3>& R) {
const T tr = R.trace();
const T cos_theta = (tr - 1) * 0.5; // For 180°, cos_theta = -1
if (std::abs(cos_theta + 1) < T(1e-10)) { // Check if it's close to 180°
// For 180° rotation:
// R = I + 2[k]_x^2, where k is the rotation axis
// R + I = 2kk^T
// Therefore, the column with largest diagonal entry in (R + I)
// gives us the rotation axis
T max_diag = R(0,0) + 1; // Diagonal elements of R + I
int max_idx = 0;
// Find the largest diagonal element
for (int i = 1; i < 3; ++i) {
if (R(i,i) + 1 > max_diag) {
max_diag = R(i,i) + 1;
max_idx = i;
}
}
// Extract rotation axis from the corresponding column
wlt::Matrix<T, 3, 1> axis;
switch (max_idx) {
case 0:
axis = wlt::Matrix<T, 3, 1>(
std::sqrt((R(0,0) + 1) * 0.5),
(R(0,1) + R(1,0)) * 0.25 / std::sqrt((R(0,0) + 1) * 0.5),
(R(0,2) + R(2,0)) * 0.25 / std::sqrt((R(0,0) + 1) * 0.5)
);
break;
case 1:
axis = wlt::Matrix<T, 3, 1>(
(R(1,0) + R(0,1)) * 0.25 / std::sqrt((R(1,1) + 1) * 0.5),
std::sqrt((R(1,1) + 1) * 0.5),
(R(1,2) + R(2,1)) * 0.25 / std::sqrt((R(1,1) + 1) * 0.5)
);
break;
case 2:
axis = wlt::Matrix<T, 3, 1>(
(R(2,0) + R(0,2)) * 0.25 / std::sqrt((R(2,2) + 1) * 0.5),
(R(2,1) + R(1,2)) * 0.25 / std::sqrt((R(2,2) + 1) * 0.5),
std::sqrt((R(2,2) + 1) * 0.5)
);
break;
}
// Return π times the rotation axis
return M_PI * axis;
}
// ... rest of the code for non-180° cases ...
}
Mathematical Background:
-
For a 180° rotation around axis k (unit vector): \(R = I + 2[k]_×^2 = I - 2(k k^T - I) = 2k k^T - I\)
-
Therefore: \(R + I = 2k k^T\)
-
Properties of this case:
- Trace(R) = -1
- R is symmetric
- Eigenvalues are {-1, -1, 1}
- Eigenvector with eigenvalue 1 is the rotation axis
Example:
// Test case for 180° rotation around x-axis
wlt::Matrix<double, 3, 3> R_180_x;
R_180_x << 1, 0, 0,
0, -1, 0,
0, 0, -1;
wlt::Matrix<double, 3, 1> log_R = Log(R_180_x);
// Expected result: [π, 0, 0]^T
Key Points:
- We can’t use the standard formula θ/(2sin(θ)) * K because sin(π) = 0
- The rotation axis is the eigenvector corresponding to eigenvalue 1
- We can find this axis by looking at R + I
- The column of R + I with the largest diagonal entry gives us the most numerically stable way to compute the axis
This improved handling of 180° rotations makes the logarithm map complete and numerically stable for all rotation matrices in SO(3).
Me: 这里补充一下,180°旋转的时候,通过上面的方法,\(R-R^T\),是没法恢复出k的,因为\(R-R^T\)是0矩阵,所以需要通过\(R+I\)来恢复出k
下面继续…
Me: I’m confused with the switch case, it’s a bit different with the above logic “axis += wlt::Matrix<T, 3, 1>::Unit(max_idx);”
Let me explain the difference between these two approaches for finding the rotation axis in a 180° rotation:
- The simpler approach you mentioned:
axis = wlt::Matrix<T, 3, 1>(R(max_idx,0), R(max_idx,1), R(max_idx,2)); axis += wlt::Matrix<T, 3, 1>::Unit(max_idx); axis.normalize();
- The switch case approach in the code:
case 0: // When x-axis has largest diagonal axis = wlt::Matrix<T, 3, 1>( std::sqrt((R(0,0) + 1) * 0.5), // = sqrt(2k_x^2) = |k_x| (R(0,1) + R(1,0)) * 0.25 / sqrt(...), // = k_x*k_y / |k_x| (R(0,2) + R(2,0)) * 0.25 / sqrt(...) // = k_x*k_z / |k_x| );
The switch case approach is mathematically more precise because:
- For a 180° rotation, \(R + I = 2kk^T\) where \(k\) is the unit rotation axis
- The diagonal elements of \(2kk^T\) are \(2k_x^2, 2k_y^2, 2k_z^2\)
- The off-diagonal elements are \(2k_xk_y, 2k_xk_z, 2k_yk_z\)
This gives us a way to directly compute the components of k without needing normalization.
However, both approaches should give the same result, with the simpler approach being more intuitive but potentially less numerically stable.
Me: This line
(R(0,1) + R(1,0)) * 0.25 / sqrt(...), // = k_x*k_y / |k_x|
, why not use the same method as obtaining the k_x to obtain the k_y
AI: Ah, you raise a good point! Let me explain why we can’t use the same method for all components:
For a 180° rotation around axis k = [k_x, k_y, k_z]:
- When we choose the x-component (case 0), we know:
R(0,0) + 1 = 2k_x^2 // Diagonal term R(0,1) + R(1,0) = 2k_x*k_y // Off-diagonal term
-
We could try to get k_y from \(R(1,1) + 1 = 2k_y^2\), but in this case (when x-component is largest), this value would be close to zero and numerically unstable.
- Instead, we use:
k_x = ±sqrt((R(0,0) + 1)/2) // Get magnitude from diagonal k_y = (R(0,1) + R(1,0))/(4*k_x) // Use off-diagonal for sign and magnitude
You’re right that theoretically we could use sqrt((R(1,1) + 1)/2) to get k_y directly, but numerically it’s more stable to use the off-diagonal elements when we’re working with the non-dominant components.
This is why we use different formulas for the dominant component (using sqrt) versus the other components (using ratios).