pyquaternion icon indicating copy to clipboard operation
pyquaternion copied to clipboard

Quaternion.distance seems to produce incorrect result

Open rgolovanov opened this issue 4 years ago • 3 comments

I'm evaluating the angular distance between two quaternions and it seems that values produced by Quaternion.distance are twice less in comparison with this formula:

The UTs checking angular distance for original and modified distance functions (test_angular_distance.py):

import pyquaternion as quat
import unittest
import math
import numpy as np


def angle_distance_orig(q0, q1):
    return quat.Quaternion.distance(q0, q1) / math.pi * 180


def angle_distance(q0, q1):
    q0 = q0.elements
    q1 = q1.elements
    dotProd = q0[0]*q1[0] + q0[1]*q1[1] + q0[2]*q1[2] + q0[3]*q1[3]
    return 2.0 * math.acos(max(0.0, min(abs(dotProd), 1.0))) / math.pi * 180.0


if __name__ == '__main__':

    orig_quat = quat.Quaternion(axis=[1, 0, 0], degrees=0)
    for angle in range(0, 361, 15):
        for ax in range(0, 3):
            new_quat = quat.Quaternion(axis=np.roll(
                [1, 0, 0], ax), degrees=angle)

            a0 = angle_distance_orig(orig_quat, new_quat) # 0 - 360
            a1 = angle_distance(orig_quat, new_quat) # 0 - 180
            print(f"Angle: {min(angle, 360 - angle):5}, axis: {ax}, original={a0:5.1f}, modified={a1:5.1f}")

The output:

$ python test_angular_distance.py

Angle:     0, axis: 0, original=  0.0, modified=  0.0
Angle:     0, axis: 1, original=  0.0, modified=  0.0
Angle:     0, axis: 2, original=  0.0, modified=  0.0
Angle:    15, axis: 0, original=  7.5, modified= 15.0
Angle:    15, axis: 1, original=  7.5, modified= 15.0
Angle:    15, axis: 2, original=  7.5, modified= 15.0
Angle:    30, axis: 0, original= 15.0, modified= 30.0
Angle:    30, axis: 1, original= 15.0, modified= 30.0
Angle:    30, axis: 2, original= 15.0, modified= 30.0
Angle:    45, axis: 0, original= 22.5, modified= 45.0
Angle:    45, axis: 1, original= 22.5, modified= 45.0
Angle:    45, axis: 2, original= 22.5, modified= 45.0
Angle:    60, axis: 0, original= 30.0, modified= 60.0
Angle:    60, axis: 1, original= 30.0, modified= 60.0
Angle:    60, axis: 2, original= 30.0, modified= 60.0
Angle:    75, axis: 0, original= 37.5, modified= 75.0
Angle:    75, axis: 1, original= 37.5, modified= 75.0
Angle:    75, axis: 2, original= 37.5, modified= 75.0
Angle:    90, axis: 0, original= 45.0, modified= 90.0
Angle:    90, axis: 1, original= 45.0, modified= 90.0
Angle:    90, axis: 2, original= 45.0, modified= 90.0
Angle:   105, axis: 0, original= 52.5, modified=105.0
Angle:   105, axis: 1, original= 52.5, modified=105.0
Angle:   105, axis: 2, original= 52.5, modified=105.0
Angle:   120, axis: 0, original= 60.0, modified=120.0
Angle:   120, axis: 1, original= 60.0, modified=120.0
Angle:   120, axis: 2, original= 60.0, modified=120.0
Angle:   135, axis: 0, original= 67.5, modified=135.0
Angle:   135, axis: 1, original= 67.5, modified=135.0
Angle:   135, axis: 2, original= 67.5, modified=135.0
Angle:   150, axis: 0, original= 75.0, modified=150.0
Angle:   150, axis: 1, original= 75.0, modified=150.0
Angle:   150, axis: 2, original= 75.0, modified=150.0
Angle:   165, axis: 0, original= 82.5, modified=165.0
Angle:   165, axis: 1, original= 82.5, modified=165.0
Angle:   165, axis: 2, original= 82.5, modified=165.0
Angle:   180, axis: 0, original= 90.0, modified=180.0
Angle:   180, axis: 1, original= 90.0, modified=180.0
Angle:   180, axis: 2, original= 90.0, modified=180.0
Angle:   165, axis: 0, original= 97.5, modified=165.0
Angle:   165, axis: 1, original= 97.5, modified=165.0
Angle:   165, axis: 2, original= 97.5, modified=165.0
Angle:   150, axis: 0, original=105.0, modified=150.0
Angle:   150, axis: 1, original=105.0, modified=150.0
Angle:   150, axis: 2, original=105.0, modified=150.0
Angle:   135, axis: 0, original=112.5, modified=135.0
Angle:   135, axis: 1, original=112.5, modified=135.0
Angle:   135, axis: 2, original=112.5, modified=135.0
Angle:   120, axis: 0, original=120.0, modified=120.0
Angle:   120, axis: 1, original=120.0, modified=120.0
Angle:   120, axis: 2, original=120.0, modified=120.0
Angle:   105, axis: 0, original=127.5, modified=105.0
Angle:   105, axis: 1, original=127.5, modified=105.0
Angle:   105, axis: 2, original=127.5, modified=105.0
Angle:    90, axis: 0, original=135.0, modified= 90.0
Angle:    90, axis: 1, original=135.0, modified= 90.0
Angle:    90, axis: 2, original=135.0, modified= 90.0
Angle:    75, axis: 0, original=142.5, modified= 75.0
Angle:    75, axis: 1, original=142.5, modified= 75.0
Angle:    75, axis: 2, original=142.5, modified= 75.0
Angle:    60, axis: 0, original=150.0, modified= 60.0
Angle:    60, axis: 1, original=150.0, modified= 60.0
Angle:    60, axis: 2, original=150.0, modified= 60.0
Angle:    45, axis: 0, original=157.5, modified= 45.0
Angle:    45, axis: 1, original=157.5, modified= 45.0
Angle:    45, axis: 2, original=157.5, modified= 45.0
Angle:    30, axis: 0, original=165.0, modified= 30.0
Angle:    30, axis: 1, original=165.0, modified= 30.0
Angle:    30, axis: 2, original=165.0, modified= 30.0
Angle:    15, axis: 0, original=172.5, modified= 15.0
Angle:    15, axis: 1, original=172.5, modified= 15.0
Angle:    15, axis: 2, original=172.5, modified= 15.0
Angle:     0, axis: 0, original=180.0, modified=  0.0
Angle:     0, axis: 1, original=180.0, modified=  0.0
Angle:     0, axis: 2, original=180.0, modified=  0.0

Is it a bug in function or I'm confusing the usage of the function?

rgolovanov avatar Jul 03 '20 07:07 rgolovanov

Quaternion.distance() returns the geodesic distance not angular distance. If you provide a source for the equation you are using, I can add an angular_distace() method based on this.

KieranWynn avatar Oct 05 '20 01:10 KieranWynn

@KieranWynn thanks for your comment, please have a look at this (see Algorithms section)

Possible implementation:

def angular_distance(q0, q1):
    #
    # Calculates the angular distance between two quaternions in degrees
    #    q0, q1 shall be normalized
    #
    #   According to formula:
    #     theta = 2*acos(|<q0,q1>|)
    #   from https://www.mathworks.com/help/fusion/ref/quaternion.dist.html
    #
    # Returns angle in degrees within the range [0, 180] degrees
    #

    dotProd = q0.x*q1.x + q0.y*q1.y + q0.z*q1.z + q0.w*q1.w
    return np.degrees(2.0 * math.acos(max(0.0, min(abs(dotProd), 1.0))))

rgolovanov avatar Oct 05 '20 11:10 rgolovanov

I ran into this same issue: the values I'm getting from absolute_distance() are twice as large as the expected value. An 'angular_distance()' function would be great!

There seems to be some confusion about this online as well, see https://stackoverflow.com/a/55553058/5191069

jstmn avatar Jan 19 '23 03:01 jstmn