计算两个四元数之间角度的“正确方法”
实际上没有两个四元数之间的角度这样的东西,只有一个四元数通过乘法将一个四元数带到另一个四元数。但是,您可以通过计算两个四元数之间的差异(例如 qDiff = q1.mul(q2.inverse()),或者您的库可能能够使用类似 qDiff = q1.difference(q2) 的调用直接计算它)来测量该映射变换的总旋转角度,然后测量四元数轴的角度(你的四元数库可能有一个例程,例如ang = qDiff.angle())。
请注意,您可能需要固定该值,因为测量围绕轴的角度不一定会给出“短途”旋转,例如:
if (ang > Math.PI) {
ang -= 2.0 * Math.PI;
} else if (ang < -Math.PI) {
ang += 2.0 * Math.PI;
}
使用点积测量两个四元数的相似度
更新: See this answer instead.
我假设在最初的问题中,将四元数视为 4d 向量的目的是启用一种简单的方法来测量两个四元数的相似性,同时仍然牢记四元数表示旋转。 (从一个四元数到另一个四元数的实际旋转映射本身就是一个四元数,而不是一个标量。)
有几个答案建议使用点积的acos。 (首先要注意:四元数必须是单位四元数才能起作用。)但是,其他答案没有考虑“双重封面问题”:q 和 -q 都代表完全相同的旋转。
acos(q1 . q2) 和 acos(q1 . (-q2)) 都应该返回相同的值,因为 q2 和 -q2 代表相同的旋转。但是(x == 0 除外),acos(x) 和 acos(-x) 不会返回相同的值。因此,平均而言(给定随机四元数),acos(q1 . q2) 在一半的时间里不会给你你所期望的,这意味着它不会给你一个度量 q1 和 q2 之间的角度,假设你关心q1 和 q2 代表旋转。因此,即使您只打算使用点积或点积的acos 作为相似度度量,来测试q1 和q2 在旋转效果方面的相似度,答案你get 会有一半的时间是错误的。
更具体地说,如果您试图简单地将四元数视为 4d 向量,并计算 ang = acos(q1 . q2),您有时会得到您期望的 ang 的值,而其余时间则是您实际想要的值(考虑到双封面问题)将是PI - acos(-q1 . q2)。您获得的这两个值中的哪一个将在这些值之间随机波动,具体取决于 q1 和 q2 的计算方式!。
要解决这个问题,您必须对四元数进行归一化,使它们位于双覆盖空间的同一个“半球”中。有几种方法可以做到这一点,老实说,我什至不确定其中哪一种是“正确”或最佳方式。在某些情况下,它们确实会产生与其他方法不同的结果。任何关于上述三种归一化形式中哪一种是正确或最佳的反馈都将不胜感激。
import java.util.Random;
import org.joml.Quaterniond;
import org.joml.Vector3d;
public class TestQuatNorm {
private static Random random = new Random(1);
private static Quaterniond randomQuaternion() {
return new Quaterniond(
random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1,
random.nextDouble() * 2 - 1, random.nextDouble() * 2 - 1)
.normalize();
}
public static double normalizedDot0(Quaterniond q1, Quaterniond q2) {
return Math.abs(q1.dot(q2));
}
public static double normalizedDot1(Quaterniond q1, Quaterniond q2) {
return
(q1.w >= 0.0 ? q1 : new Quaterniond(-q1.x, -q1.y, -q1.z, -q1.w))
.dot(
q2.w >= 0.0 ? q2 : new Quaterniond(-q2.x, -q2.y, -q2.z, -q2.w));
}
public static double normalizedDot2(Quaterniond q1, Quaterniond q2) {
Vector3d v1 = new Vector3d(q1.x, q1.y, q1.z);
Vector3d v2 = new Vector3d(q2.x, q2.y, q2.z);
double dot = v1.dot(v2);
Quaterniond q2n = dot >= 0.0 ? q2
: new Quaterniond(-q2.x, -q2.y, -q2.z, -q2.w);
return q1.dot(q2n);
}
public static double acos(double val) {
return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));
}
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
var q1 = randomQuaternion();
var q2 = randomQuaternion();
double dot = q1.dot(q2);
double dot0 = normalizedDot0(q1, q2);
double dot1 = normalizedDot1(q1, q2);
double dot2 = normalizedDot2(q1, q2);
System.out.println(acos(dot) + "\t" + acos(dot0) + "\t" + acos(dot1)
+ "\t" + acos(dot2));
}
}
}
还要注意:
-
众所周知,
acos 在数值上不太准确(考虑到一些最坏情况的输入,最多有一半的最低有效数字可能是错误的);
-
acos 的实现在 JDK 标准库中异常缓慢;
-
acos 返回 NaN 如果其参数稍微超出 [-1,1],这对于偶数单位四元数的点积很常见——因此您需要在之前将点积的值绑定到该范围打电话给acos。请参阅上面代码中的这一行:
return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));
根据this cheatsheet 等式。 (42),有一种更稳健、更准确的方法来计算两个向量之间的角度,用atan2 替换acos(尽管请注意,这也不能解决双覆盖问题,因此您需要使用其中之一应用以下规范之前的上述规范化形式):
ang(q1, q2) = 2 * atan2(|q1 - q2|, |q1 + q2|)
我承认我不明白这个公式,因为四元数减法和加法没有几何意义。