两个四元数旋转的点积

14

我知道两个四元数的点积(或内积)是旋转之间的角度(包括轴-旋转)。这使得点积等于四元数超球面上两点之间的夹角。
然而,我找不到如何实际计算点积。

任何帮助都将不胜感激!

当前代码:

public static float dot(Quaternion left, Quaternion right){
    float angle;

    //compute

    return angle;
}

定义了四元数的 w、x、y 和 z。

注:可以假设四元数已被归一化。

4个回答

15

四元数的点积就是在四维空间中的标准欧几里得点积:

dot = left.x * right.x + left.y * right.y + left.z * right.z + left.w * right.w

那么您要找的角度是点积的反余弦函数(请注意,点积不是角度):acos(dot)

但是,如果您正在寻找两个四元数之间的相对旋转,比如从 q1q2,您应该计算相对四元数 q = q1^-1 * q2,然后找到与 q 相关联的旋转。


@Luke:我认为你误解了这个答案,它包含两个不同的部分。将四元数视为4D向量的夹角是这个答案的第一部分(acos(点积)的内容)。从一个四元数到另一个四元数的最小旋转角度是这个答案的第二部分(与第一部分不同)。你的答案仅是这个答案的第二部分的细节(即获取上述q的实际最小旋转角度)。 - James Tursa
@JamesTursa,OP要求以单个标量值的形式给出“旋转之间的角度(包括轴旋转)”,而不是将一个四元数映射到另一个四元数的四元数,因此答案的后半部分实际上没有回答问题。答案的前半部分仍然是不正确的,因为acos需要乘以2,并且由于四元数“双覆盖”问题,在调用acos之前必须取绝对值点积。 - Luke Hutchison
@Luke:我认为这篇文章的前半部分试图回答OP有关将四元数视为超球面上简单的4D点(OP自己的话)的问题。因此,在我看来,acos(dot)的答案对于这个特定的请求是正确的。本文中提到的q东西与此不同(将四元数视为3D旋转),并且作者也指出了这一点。 q东西与你的答案完全相同,只是没有详细说明如何找到与q相关联的旋转...而你在你的答案中提供了这些细节。所以我没有看到任何矛盾之处。 - James Tursa
但是你不能直接将四元数视为4D点,因为由于“双覆盖问题”,q-q代表完全相同的旋转。您必须首先进行归一化,以使两个四元数位于双覆盖空间的同一半球中,然后您可以将它们视为4D向量,此时点积实际上是有意义的。我在下面扩展了我的答案,以描述针对双覆盖问题的归一化。 - Luke Hutchison

6

计算两个四元数之间角度的“正确”方法

实际上不存在两个四元数之间的角度,只有通过乘法将一个四元数转换为另一个四元数的四元数。但是,可以通过计算两个四元数之间的差异(例如 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都代表完全相同的旋转。

因为q2-q2表示相同的旋转,所以acos(q1 . q2)acos(q1 . (-q2))应该返回相同的值。然而(除了x==0的情况),acos(x)acos(-x)并不返回相同的值。因此,平均而言(给定随机四元数),acos(q1 . q2)将不会一半的时间给出您期望的答案,这意味着它将无法作为测量q1q2之间夹角的度量标准,除非您关心q1q2表示旋转的情况。因此,即使您只计划使用点积或acos点积作为相似性度量标准,以测试q1q2在旋转效果方面有多相似,您得到的答案也会有一半的时间是错误的。

更具体地说,如果您试图将四元数简单地视为4D向量,并计算ang = acos(q1 . q2),则有时您将获得您期望的ang值,其余的时间您实际想要的值(考虑双重覆盖问题)将是PI - acos(-q1 . q2)。 这两个值中的哪一个您得到的将根据计算q1q2的方式在这些值之间随机波动!

为了解决这个问题,您需要将四元数归一化,使它们在双覆盖空间的同一“半球”中。有几种方法可以实现这一点,说实话我甚至不确定哪种方法是“正确”或最优的。它们在某些情况下确实会产生与其他方法不同的结果。如您对上述三种归一化形式中的哪一种是正确或最优的有任何反馈,将不胜感激。
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));
        }
    }
}

还要注意以下几点:

  1. acos 在数值计算上不够精确(在某些最坏情况下输入数据时,最低有效位可以错误一半);
  2. JDK标准库中 acos 的实现非常缓慢;
  3. acos 如果其参数稍微超出 [-1,1] 范围的话就会返回 NaN,对于偶数单位四元数的点积而言,这是一个常见的情况 -- 所以在调用 acos 之前需要将点积的值限制在该范围内。请参阅上面代码中的这一行:
        return Math.toDegrees(Math.acos(Math.max(-1.0, Math.min(1.0, val))));

根据这份备忘单中的方程式(42),有一种更加稳健和准确的方法来计算两个向量之间的角度,使用 atan2代替acos(请注意,这仍然无法解决双重覆盖问题,所以在应用以下公式之前,您需要使用上述任一归一化形式)。
ang(q1, q2) = 2 * atan2(|q1 - q2|, |q1 + q2|)

我承认,我不理解这个表述,因为四元数的减法和加法没有几何意义。


5

请注意:从数值角度来看,acos(dot)非常不稳定。

正如之前所说的,先令q = q1^-1 * q2,然后角度等于2乘以atan2(q.vec.length(), q.w)。


请与我在回答中链接的“cheatsheet”中的atan2公式进行比较 - 我不理解它们之间的区别。 - Luke Hutchison
我不明白你的问题。 - minorlogic

3

如果要计算四元数之间的角度,应该使用 2 x acos(dot) 的方法。


你确定吗?我认为这是不正确的。请再仔细检查一下。(参考:https://www.3dgep.com/understanding-quaternions/) - code_dredd
1
四元数点积的范围将从1.0到0.0,而3D方向向量点积的范围将从1.0到-1.0。因此,将arccos(dot)加倍似乎是合理的。 - Isometriq
@Isometriq 对于单位/旋转四元数,它们的点积范围在[-1...1]之间。我也认为这是错误的。被接受的答案应该是正确的。 - KeyC0de
两个归一化向量的点积给出向量之间夹角的余弦,因此acos(dot)就足够了(几乎),不需要乘以2。然而,我认为你所说的是双覆盖问题:q-q表示相同的旋转。请参阅我的答案以获取详细信息(以及我为什么说“(几乎)”)。 - Luke Hutchison

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接