2025, Nov 10 09:00
Quaternion to Rotation Matrix in SciPy: Normalize Quaternions, Use Scalar-Last [x, y, z, w], Avoid Scaling
Learn why SciPy's quaternion-to-matrix conversion scales when quaternions aren't unit length, how to normalize, and why scalar-last [x, y, z, w] order matters.
Quaternion-to-matrix conversions in SciPy sometimes surprise people who compare the output to the classic formulas from textbooks or the Wikipedia rotation matrix page. The discrepancy typically shows up when the input quaternion is not unit length. Here is what is actually happening and how to keep your transforms predictable.
Minimal example of the mismatch
The following snippet feeds SciPy a quaternion that has only a scalar part and is not normalized. SciPy expects quaternions in scalar-last order, so the vector is [x, y, z, w].
import numpy as np
from scipy.spatial.transform import Rotation as Rot
# Non-unit quaternion, scalar last: w=2.0
q_raw = np.array([0.0, 0.0, 0.0, 2.0])
rot_bad = Rot.from_quat(q_raw)
mat_bad = rot_bad.as_matrix()
print("Matrix from non-unit quaternion:")
print(mat_bad)
print("Scaled-down matrix (by 1/4):")
print(0.25 * mat_bad)
With only a w term there is no rotation. The usual rotation matrix would be the identity. SciPy’s matrix here becomes 4 times the identity, because the quaternion length is 2.0.
What causes the difference
The well-known formulae for constructing a rotation matrix from a quaternion put 1’s on the diagonal. SciPy’s implementation replaces those diagonal 1’s with w² + x² + y² + z². If the quaternion is a unit quaternion, that sum is 1 and both forms coincide. If the quaternion is not unit length, the resulting matrix performs a rotation and a scaling.
There is one more practical detail to keep in mind. SciPy uses scalar-last ordering internally. You can see this in how quaternions are fed to the constructor, for example:
import numpy as np
from scipy.spatial.transform import Rotation as Rot
# Example with a 90-degree rotation around z: [x, y, z, w]
z90 = Rot.from_quat([0.0, 0.0, np.sin(np.pi / 4), np.cos(np.pi / 4)])
Z = z90.as_matrix()
print(Z)
This ordering is consistent with SciPy’s interface and is important when manually constructing quaternions.
How to get the expected pure rotation
If you want the classic rotation matrix (no scaling), ensure the quaternion is unit length before converting. This makes the diagonal term equal to 1 and aligns the result with the familiar formulas.
import numpy as np
from scipy.spatial.transform import Rotation as Rot
def unit_quat(q):
return q / np.linalg.norm(q)
# Same non-unit quaternion as above
q_raw = np.array([0.0, 0.0, 0.0, 2.0])
q_unit = unit_quat(q_raw)
rot_ok = Rot.from_quat(q_unit)
mat_ok = rot_ok.as_matrix()
print("Matrix from normalized quaternion:")
print(mat_ok)
Using a unit quaternion eliminates the scaling factor and yields the identity in this specific example.
Why this matters
Mixing normalized and non-normalized quaternions leads to silent scaling in your transforms. If you compare against textbook or Wikipedia formulas while passing a non-unit quaternion, you will see a mismatch and may misdiagnose the issue as a formula difference rather than a normalization issue. It is also relevant that SciPy’s quaternion representation is scalar-last. Feeding scalar-first data without reordering changes the intended rotation. There is also a closely related transform that is effectively the transpose of another common form; for rotation matrices the transpose is the inverse, which preserves correctness in that context.
Takeaways
If you want pure rotations, use unit quaternions. When building quaternions for SciPy, supply them in [x, y, z, w] order. If a result looks scaled compared to a reference matrix, check the quaternion’s norm first. This small habit keeps rotation matrices consistent with the usual formulas and avoids chasing ghosts in your math or code.
The article is based on a question from StackOverflow by Anon and an answer by Neil Butcher.