计算两个四元数之间角度的“正确方法”
实际上没有两个四元数之间的角度之类的东西,只有一个四元数通过乘法将一个四元数带到另一个四元数。但是,您可以通过计算两个四元数之间的差异来测量该映射变换的总旋转角度(例如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;
}
使用点积测量两个四元数的相似度
更新: 请参阅此答案。
我假设在最初的问题中,将四元数视为 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
它们作为旋转的效果,您得到的答案也有一半是错误的。
更具体地说,如果您试图简单地将四元数视为 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
已知在数字上不是很准确(给定一些最坏情况的输入,最多一半的最低有效数字可能是错误的);
- JDK 标准库中的实现
acos
异常缓慢;
acos
如果其参数稍微超出 [-1,1] 则返回NaN
,这对于偶数单位四元数的点积很常见——因此您需要在调用之前将点积的值绑定到该范围acos
。请参阅上面代码中的这一行:
return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));
根据这个备忘单等式。(42),有一种更稳健和准确的方法来计算两个向量之间的角度,acos
替换atan2
为以下):
ang(q1, q2) = 2 * atan2(|q1 - q2|, |q1 + q2|)
我承认我不理解这个公式,因为四元数减法和加法没有几何意义。