@@ -480,13 +480,17 @@ def vec_spherical_safe(vector, /, *, out=None, dtype=None):
480
480
return out
481
481
482
482
483
- def quat_to_euler (quaternion , / , * , out = None , dtype = None ):
484
- """Convert quaternions to XYZ Euler angles.
483
+ def quat_to_euler (quaternion , / , * , order = "xyz" , out = None , dtype = None ):
484
+ """Convert quaternions to Euler angles with specified rotation order .
485
485
486
486
Parameters
487
487
----------
488
488
quaternion : ndarray, [4]
489
489
The quaternion to convert (in xyzw format).
490
+ order : str, optional
491
+ The rotation order as a string. Can include 'X', 'Y', 'Z' for intrinsic
492
+ rotation (uppercase) or 'x', 'y', 'z' for extrinsic rotation (lowercase).
493
+ Default is "xyz".
490
494
out : ndarray, optional
491
495
A location into which the result is stored. If provided, it
492
496
must have a shape that the inputs broadcast to. If not provided or
@@ -498,35 +502,130 @@ def quat_to_euler(quaternion, /, *, out=None, dtype=None):
498
502
Returns
499
503
-------
500
504
out : ndarray, [3]
501
- The XYZ Euler angles.
505
+ The Euler angles in the specified order.
506
+
507
+ References
508
+ ----------
509
+ This implementation is based on the method described in the following paper:
510
+ Bernardes E, Viollet S (2022) Quaternion to Euler angles conversion: A direct, general and computationally efficient method.
511
+ PLoS ONE 17(11): e0276302. https://doi.org/10.1371/journal.pone.0276302
502
512
"""
503
513
quaternion = np .asarray (quaternion , dtype = float )
514
+ quat = np .atleast_2d (quaternion )
515
+ num_rotations = quat .shape [0 ]
504
516
505
517
if out is None :
506
518
out = np .empty ((* quaternion .shape [:- 1 ], 3 ), dtype = dtype )
507
519
508
- ysqr = quaternion [..., 1 ] ** 2
509
-
510
- t0 = 2 * (
511
- quaternion [..., 3 ] * quaternion [..., 0 ]
512
- + quaternion [..., 1 ] * quaternion [..., 2 ]
513
- )
514
- t1 = 1 - 2 * (quaternion [..., 0 ] ** 2 + ysqr )
515
- out [..., 0 ] = np .arctan2 (t0 , t1 )
516
-
517
- t2 = 2 * (
518
- quaternion [..., 3 ] * quaternion [..., 1 ]
519
- - quaternion [..., 2 ] * quaternion [..., 0 ]
520
- )
521
- t2 = np .clip (t2 , a_min = - 1 , a_max = 1 )
522
- out [..., 1 ] = np .arcsin (t2 )
523
-
524
- t3 = 2 * (
525
- quaternion [..., 3 ] * quaternion [..., 2 ]
526
- + quaternion [..., 0 ] * quaternion [..., 1 ]
527
- )
528
- t4 = 1 - 2 * (ysqr + quaternion [..., 2 ] ** 2 )
529
- out [..., 2 ] = np .arctan2 (t3 , t4 )
520
+ extrinsic = order .islower ()
521
+ order = order .lower ()
522
+ if not extrinsic :
523
+ order = order [::- 1 ]
524
+
525
+ basis_index = {"x" : 0 , "y" : 1 , "z" : 2 }
526
+ i = basis_index [order [0 ]]
527
+ j = basis_index [order [1 ]]
528
+ k = basis_index [order [2 ]]
529
+
530
+ is_proper = i == k
531
+
532
+ if is_proper :
533
+ k = 3 - i - j # get third axis
534
+
535
+ # Step 0
536
+ # Check if permutation is even (+1) or odd (-1)
537
+ sign = int ((i - j ) * (j - k ) * (k - i ) / 2 )
538
+
539
+ eps = 1e-7
540
+ for ind in range (num_rotations ):
541
+ if num_rotations == 1 and out .ndim == 1 :
542
+ _angles = out
543
+ else :
544
+ _angles = out [ind , :]
545
+
546
+ if is_proper :
547
+ a = quat [ind , 3 ]
548
+ b = quat [ind , i ]
549
+ c = quat [ind , j ]
550
+ d = quat [ind , k ] * sign
551
+ else :
552
+ a = quat [ind , 3 ] - quat [ind , j ]
553
+ b = quat [ind , i ] + quat [ind , k ] * sign
554
+ c = quat [ind , j ] + quat [ind , 3 ]
555
+ d = quat [ind , k ] * sign - quat [ind , i ]
556
+
557
+ n2 = a ** 2 + b ** 2 + c ** 2 + d ** 2
558
+
559
+ # Step 3
560
+ # Compute second angle...
561
+ # _angles[1] = 2*np.arccos(np.sqrt((a**2 + b**2) / n2))
562
+ _angles [1 ] = np .arccos (2 * (a ** 2 + b ** 2 ) / n2 - 1 )
563
+
564
+ # ... and check if it is equal to 0 or pi, causing a singularity
565
+ safe1 = np .abs (_angles [1 ]) >= eps
566
+ safe2 = np .abs (_angles [1 ] - np .pi ) >= eps
567
+ safe = safe1 and safe2
568
+
569
+ # Step 4
570
+ # compute first and third angles, according to case
571
+ if safe :
572
+ half_sum = np .arctan2 (b , a ) # == (alpha+gamma)/2
573
+ half_diff = np .arctan2 (- d , c ) # == (alpha-gamma)/2
574
+
575
+ _angles [0 ] = half_sum + half_diff
576
+ _angles [2 ] = half_sum - half_diff
577
+
578
+ else :
579
+ # _angles[0] = 0
580
+
581
+ if not extrinsic :
582
+ # For intrinsic, set first angle to zero so that after reversal we
583
+ # ensure that third angle is zero
584
+ # 6a
585
+ if not safe :
586
+ _angles [0 ] = 0
587
+ if not safe1 :
588
+ half_sum = np .arctan2 (b , a )
589
+ _angles [2 ] = 2 * half_sum
590
+ # 6c
591
+ if not safe2 :
592
+ half_diff = np .arctan2 (- d , c )
593
+ _angles [2 ] = - 2 * half_diff
594
+ else :
595
+ # For extrinsic, set third angle to zero
596
+ # 6b
597
+ if not safe :
598
+ _angles [2 ] = 0
599
+ if not safe1 :
600
+ half_sum = np .arctan2 (b , a )
601
+ _angles [0 ] = 2 * half_sum
602
+ # 6c
603
+ if not safe2 :
604
+ half_diff = np .arctan2 (- d , c )
605
+ _angles [0 ] = 2 * half_diff
606
+
607
+ for i_ in range (3 ):
608
+ if _angles [i_ ] < - np .pi :
609
+ _angles [i_ ] += 2 * np .pi
610
+ elif _angles [i_ ] > np .pi :
611
+ _angles [i_ ] -= 2 * np .pi
612
+
613
+ # for Tait-Bryan angles
614
+ if not is_proper :
615
+ _angles [2 ] *= sign
616
+ _angles [1 ] -= np .pi / 2
617
+
618
+ if not extrinsic :
619
+ # reversal
620
+ _angles [0 ], _angles [2 ] = _angles [2 ], _angles [0 ]
621
+
622
+ # Step 8
623
+ if not safe :
624
+ logger .warning (
625
+ "Gimbal lock detected. Setting third angle to zero "
626
+ "since it is not possible to uniquely determine "
627
+ "all angles."
628
+ )
530
629
531
630
return out
532
631
0 commit comments