Quaternion to Euler Rotations

Does anyone have some Python code for handling this? I’ve looked around online and tried to understand how to convert from quaternion to euler, and having a tough time. I’ve ported some existing solutions I’ve found but they’re either messing up some rotations or I’m getting a Math Domain error message instead.

I’ve ported the following Stack Overflow answer here: language agnostic - Is there an algorithm for converting quaternion rotations to Euler angle rotations? - Stack Overflow to Python. And found that the rotation order zyx is what produces accurate results from the Quaternion data I’ve got into my Maya scene (Z-axis up).

These are the problem angles:

#Quaternion is a named tuple, wxyz params
q = Quaternion(-0.5, -0.5, 0.5, 0.5) #StackOverflow port produces correct values for this previously problematic quaternion
q = Quaternion(-0.707107, 0, -0.707107, 0) #This was causing a math error, follow the SO answers link to source, ported normalize method, now produces correct result
q = Quaternion(-3.09086e-08, 0.707107, 3.09086e-08, 0.707107) #Even with normalized quaternion value this is erroring, should be roughly -180, -90, 0 I think?

Previously I ported the java code shown here: Maths - Conversion Quaternion to Euler - Martin Baker , This didn’t cause any errors but some rotations were off by 180deg or in the case of the first listed quaternion, I think I was getting 0 , 270, -90, when I wanted 90, 0, -90 which the Stack Overflow answer provides. I can share the Python code if it helps, else if anyone has a lib or code of their own they can advise with that’d be appreciated, spent a good 2 days looking into this.

1 Like

Alas, the quaternion to euler conversion is not deterministic: each quaternion produces an infinite number of valid euler rotations and vice-versa. A given eulerization is based on convention and on rotation order; it sounds like you’ve got the rotation order but Maya controls the conventions. The sad truth is that there are many equally valid ways of reporting the same orientation in both systems, so you can’t get a reliable mapping (“Euler Filter”, anyone?)

You might find it easier to just use API quaternions and convert them to Eulers that way. The new api makes that much less painful:


import maya.api.OpenMaya as api
import math

q = api.MQuaternion(.707, 0, 0 , .707)
e = q.asEulerRotation()
print map(math.degree, e) 
[-180, -89.999999, 0.0]

The only wrinkle is that the MEulerRotation value will be in radians, so you need to convert it to degrees as above.

[QUOTE=Theodox;27078]Alas, the quaternion to euler conversion is not deterministic: each quaternion produces an infinite number of valid euler rotations and vice-versa. A given eulerization is based on convention and on rotation order; it sounds like you’ve got the rotation order but Maya controls the conventions. The sad truth is that there are many equally valid ways of reporting the same orientation in both systems, so you can’t get a reliable mapping (“Euler Filter”, anyone?)

You might find it easier to just use API quaternions and convert them to Eulers that way. The new api makes that much less painful:


import maya.api.OpenMaya as api
import math

q = api.MQuaternion(.707, 0, 0 , .707)
e = q.asEulerRotation()
print map(math.degree, e) 
[-180, -89.999999, 0.0]

The only wrinkle is that the MEulerRotation value will be in radians, so you need to convert it to degrees as above.[/QUOTE]
Yes I’m aware of the issues with conversions. I was considering maybe using the python in max/blender in future so less reliance on the api for each app might be a good thing? I tried your example code, it errored, I changed math.degree to math.degrees, and that worked but my result was [90.0, -0.0, 0.0], could be due to me having Z up axis instead of Y?

After 2 days looking into the conversion math, I sourced/ported several implementations and mix’n’matched what I could make sense of. The following code works for my use case perfectly:


import math
from collections import namedtuple

Quaternion = namedtuple('Quaternion', 'w x y z')
Euler = namedtuple('Euler', 'x y z')

def crop_rotation(angle):
    if angle > 180:
        return angle-360
    elif angle < -180:
        return angle+360
    else:
        return angle

def convert_to_radians(degrees):
    return (degrees/180) * math.pi

def convert_to_degrees(radians):
    return (radians*180) / math.pi

def quaternion_to_euler(q):
    sqw = q.w * q.w
    sqx = q.x * q.x
    sqy = q.y * q.y
    sqz = q.z * q.z

    normal = math.sqrt(sqw + sqx + sqy + sqz)
    pole_result = (q.x * q.z) + (q.y * q.w)

    if (pole_result > (0.5 * normal)): # singularity at north pole
        ry = math.pi/2 #heading/yaw?
        rz = 0 #attitude/roll?
        rx = 2 * math.atan2(q.x, q.w) #bank/pitch?
        return Euler(rx, ry, rz)
    if (pole_result < (-0.5 * normal)): # singularity at south pole
        ry = -math.pi/2
        rz = 0
        rx = -2 * math.atan2(q.x, q.w)
        return Euler(rx, ry, rz)

    r11 = 2*(q.x*q.y + q.w*q.z)
    r12 = sqw + sqx - sqy - sqz
    r21 = -2*(q.x*q.z - q.w*q.y)
    r31 = 2*(q.y*q.z + q.w*q.x)
    r32 = sqw - sqx - sqy + sqz

    rx = math.atan2( r31, r32 )
    ry = math.asin ( r21 )
    rz = math.atan2( r11, r12 )

    return Euler(rx, ry, rz)
    
def euler_to_quaternion(angle_x, angle_y, angle_z):
    heading  = convert_to_radians(angle_y)
    attitude = convert_to_radians(angle_z)
    bank     = convert_to_radians(angle_x)

    c1 = math.cos(heading/2)
    c2 = math.cos(attitude/2)
    c3 = math.cos(bank/2)

    s1 = math.sin(heading/2)
    s2 = math.sin(attitude/2)
    s3 = math.sin(bank/2)

    w = (c1 * c2 * c3) - (s1 * s2 * s3)
    x = (s1 * s2 * c3) + (c1 * c2 * s3)
    y = (s1 * c2 * c3) + (c1 * s2 * s3)
    z = (c1 * s2 * c3) - (s1 * c2 * s3)
    
    return Quaternion(w, x, y, z)

def test_q2e(q):
    rotations = quaternion_to_euler(q)
    rx = crop_rotation( convert_to_degrees( rotations.x ) *-1 )
    ry = crop_rotation( convert_to_degrees( rotations.y ) )
    rz = crop_rotation( convert_to_degrees( rotations.z ) )
    e = Euler(rx, ry, rz)
    print e
    return e
    
def test_e2q(e):
    angleX = crop_rotation(e.x*-1)
    angleY = crop_rotation(e.y*-1)
    angleZ = crop_rotation(e.z)

    q = euler_to_quaternion(angleX, angleY, angleZ)
    print q
    return q

test1 = Quaternion(-0.5, -0.5, 0.5, 0.5)
test2 = Quaternion(-0.707107, 0, -0.707107, 0)
test3 = Quaternion(-3.09086e-08, 0.707107, 3.09086e-08, 0.707107)

print str(test1)+" #Original Quaternion
"+str(test1 == test_e2q( test_q2e( test1 ) ))+"
"
print str(test2)+" #Original Quaternion
"+str(test2 == test_e2q( test_q2e( test2 ) ))+"
"
print str(test3)+" #Original Quaternion
"+str(test3 == test_e2q( test_q2e( test3 ) ))+"
"

The conversion from euler to quaternion is slightly imprecise, at least in my case I don’t think that will cause any major issues. I don’t think you can really round it based on the differences from the 3 tests.

1 Like