2025, Nov 14 12:02

Кватернионы в матрицу вращения в SciPy: почему появляется масштабирование и как его убрать

Разбираем, почему SciPy дает масштабирование при преобразовании неединичных кватернионов в матрицу вращения, как нормализовать и использовать порядок [x,y,z,w].

Преобразование кватернионов в матрицы в SciPy порой удивляет тех, кто сверяет результат с классическими формулами из учебников или со страницей Wikipedia про матрицы вращения. Несовпадение обычно проявляется, когда входной кватернион не единичной длины. Ниже — что на самом деле происходит и как сохранить предсказуемость преобразований.

Минимальный пример несоответствия

В примере ниже в SciPy подается кватернион, у которого есть только скалярная часть, и он не нормирован. SciPy ожидает порядок со скаляром в конце, то есть вектор имеет вид [x, y, z, w].

import numpy as np
from scipy.spatial.transform import Rotation as Rot

# Неединичный кватернион, скаляр в конце: 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)

При наличии только компоненты w вращения нет. Обычная матрица вращения была бы единичной. В SciPy здесь получается матрица, равная четырехкратной единичной, потому что длина кватерниона равна 2.0.

Откуда берется разница

В известных формулах построения матрицы вращения по кватерниону на диагонали стоят единицы. В реализации SciPy эти единицы заменены на w² + x² + y² + z². Если кватернион единичный, сумма равна 1, и обе формы совпадают. Если кватернион не единичной длины, результирующая матрица задает одновременно вращение и масштабирование.

Есть еще одна практическая деталь. Внутри SciPy используется порядок со скалярной частью в конце. Это видно по тому, как кватернионы передаются в конструктор, например:

import numpy as np
from scipy.spatial.transform import Rotation as Rot

# Пример: поворот на 90 градусов вокруг 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)

Такой порядок согласован с интерфейсом SciPy и важен, когда вы формируете кватернионы вручную.

Как получить ожидаемое чистое вращение

Если вам нужна классическая матрица вращения (без масштабирования), перед преобразованием убедитесь, что кватернион единичной длины. Тогда диагональный член станет равен 1, и результат совпадет с привычными формулами.

import numpy as np
from scipy.spatial.transform import Rotation as Rot

def unit_quat(q):
    return q / np.linalg.norm(q)

# Тот же неединичный кватернион, что выше
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)

Использование единичного кватерниона устраняет масштабирование и в этом случае дает единичную матрицу.

Почему это важно

Смешивание нормированных и ненормированных кватернионов приводит к незаметному масштабированию в ваших преобразованиях. Если вы сверяете результат с формулами из учебника или Wikipedia, передавая при этом неединичный кватернион, вы увидите расхождение и можете ошибочно списать его на различие формул, а не на отсутствие нормализации. Важно и то, что в SciPy кватернионы представлены со скаляром в конце: если подать данные со скаляром в начале без перестановки, фактически изменится задуманное вращение. Существует и близкое преобразование, которое по сути является транспонированием другой распространенной формы; для матриц вращения транспонирование равно обращению, поэтому корректность в этой ситуации сохраняется.

Итоги

Нужны чистые вращения — используйте единичные кватернионы. Формируя кватернионы для SciPy, задавайте их в порядке [x, y, z, w]. Если результат выглядит масштабированным по сравнению с опорной матрицей, сперва проверьте норму кватерниона. Эта простая привычка удерживает матрицы вращения в соответствии с привычными формулами и избавляет от «призраков» в математике и коде.