pyquaternion
pyquaternion copied to clipboard
Quaternion.distance seems to produce incorrect result
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?
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 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))))
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