вращающаяся система координат через кватернион - PullRequest
28 голосов
/ 02 февраля 2011

У нас есть миллиард пространственных координат (x, y и z), представляющих атомы в трехмерном пространстве, и я создаю функцию, которая переведет эти точки в новую систему координат.Сдвинуть координаты к произвольному началу очень просто, но я не могу обернуть голову вокруг следующего шага: вычисления вращения трехмерной точки.Другими словами, я пытаюсь перевести точки из (x, y, z) в (x ', y', z '), где x', y 'и z' в терминах i ', j'и k ', новые векторы осей, которые я создаю с помощью модуля euclid python .

I думаю все, что мне нужно, - это кватернион евклидаэто, то есть

>>> q * Vector3(x, y, z)
Vector3(x', y', z')

, но чтобы сделать ЭТО, я верю, мне нужен вектор оси вращения и угол поворота.Но я понятия не имею, как рассчитать их по i, j и k.Это кажется простой процедурой для кодирования с нуля, но я подозреваю, что что-то вроде этого требует линейной алгебры, чтобы понять это самостоятельно.Большое спасибо за толчок в правильном направлении.

Ответы [ 3 ]

66 голосов
/ 02 февраля 2011

Использование кватернионов для представления вращения не является сложным с алгебраической точки зрения.Лично мне трудно рассуждать визуально о кватернионах, но формулы, используемые при их использовании для вращений, довольно просты.Я приведу здесь базовый набор справочных функций. 1 (см. Также этот прекрасный ответ hosolmaz , в котором он упаковывает их вместе, чтобы создать удобный класс Quaternion.)

Вы можете думать о кватернионах (для наших целей) как о скаляре плюс трехмерный вектор - абстрактно, w + xi + yj + zk, здесь представленный простым кортежем (w, x, y, z).Пространство трехмерных вращений полностью представлено подпространством кватернионов, пространством единица кватернионов, поэтому вы хотите убедиться, что ваши кватернионы нормализованы.Вы можете сделать это так же, как вы бы нормализовали любой 4-вектор (т.е. величина должна быть близка к 1; если это не так, уменьшите значения на величину):

def normalize(v, tolerance=0.00001):
    mag2 = sum(n * n for n in v)
    if abs(mag2 - 1.0) > tolerance:
        mag = sqrt(mag2)
        v = tuple(n / mag for n in v)
    return v

Обратите вниманиечто для простоты следующие функции предполагают, что значения кватернионов уже нормализованы .На практике вам нужно время от времени перенормировать их, но лучший способ справиться с этим будет зависеть от проблемной области.Эти функции обеспечивают только самые основы, только для справочных целей.

Каждый поворот представлен единичным кватернионом, а конкатенации вращений соответствуют умножениям единичных кватернионов.Формула 2 для этого выглядит следующим образом:

def q_mult(q1, q2):
    w1, x1, y1, z1 = q1
    w2, x2, y2, z2 = q2
    w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
    x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
    y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
    z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2
    return w, x, y, z

Чтобы повернуть вектор на кватернион, вам также потребуется сопряжение кватерниона.Это просто:

def q_conjugate(q):
    w, x, y, z = q
    return (w, -x, -y, -z)

Теперь умножение кватерниона на вектор так же просто, как преобразование вектора в кватернион (установив w = 0 и оставив x, y и z одинаковыми)а затем умножить q * v * q_conjugate(q):

def qv_mult(q1, v1):
    q2 = (0.0,) + v1
    return q_mult(q_mult(q1, q2), q_conjugate(q1))[1:]

Наконец, вам нужно знать, как преобразовать вращение вокруг оси в кватернионы.Также легко!Здесь имеет смысл «санировать» ввод и вывод, вызывая normalize.

def axisangle_to_q(v, theta):
    v = normalize(v)
    x, y, z = v
    theta /= 2
    w = cos(theta)
    x = x * sin(theta)
    y = y * sin(theta)
    z = z * sin(theta)
    return w, x, y, z

И обратно:

def q_to_axisangle(q):
    w, v = q[0], q[1:]
    theta = acos(w) * 2.0
    return normalize(v), theta

Вот краткий пример использования.Последовательность поворотов на 90 градусов вокруг осей x, y и z вернет вектор на оси y в исходное положение.Этот код выполняет эти вращения:

x_axis_unit = (1, 0, 0)
y_axis_unit = (0, 1, 0)
z_axis_unit = (0, 0, 1)
r1 = axisangle_to_q(x_axis_unit, numpy.pi / 2)
r2 = axisangle_to_q(y_axis_unit, numpy.pi / 2)
r3 = axisangle_to_q(z_axis_unit, numpy.pi / 2)

v = qv_mult(r1, y_axis_unit)
v = qv_mult(r2, v)
v = qv_mult(r3, v)

print v
# output: (0.0, 1.0, 2.220446049250313e-16)

Имейте в виду, что эта последовательность вращений не вернет все векторов в одну и ту же позицию;например, для вектора на оси x он будет соответствовать повороту на 90 градусов вокруг оси y.(Имейте в виду правило правой руки; положительное вращение вокруг оси y толкает вектор на оси x в отрицательную z область.)

v = qv_mult(r1, x_axis_unit)
v = qv_mult(r2, v)
v = qv_mult(r3, v)

print v
# output: (4.930380657631324e-32, 2.220446049250313e-16, -1.0)

Как всегдаПожалуйста, дайте мне знать, если вы обнаружите какие-либо проблемы здесь.


1.Они адаптированы из учебника OpenGL , заархивированного здесь .

2.Формула умножения кватернионов выглядит как гнездо сумасшедшей крысы, но вывод прост (если утомителен).Просто отметьте сначала, что ii = jj = kk = -1;тогда это ij = k, jk = i, ki = j;и, наконец, ji = -k, kj = -i, ik = -j.Затем умножьте два кватерниона, распределяя слагаемые и переставляя их на основе результатов каждого из 16 умножений.Это также помогает проиллюстрировать , почему вы можете использовать кватернионы для представления вращения;последние шесть тождеств следуют правилу правой руки, создавая биекции между поворотами от i до j и поворотами вокруг k и т. д.

4 голосов
/ 11 февраля 2017

Этот вопрос и ответ, данный @senderle, действительно помогли мне с одним из моих проектов. Ответ минимален и охватывает ядро ​​большинства кватернионных вычислений, которое может потребоваться выполнить.

Для моего собственного проекта было утомительно иметь отдельные функции для всех операций и импортировать их одну за другой каждый раз, когда мне это нужно, поэтому я реализовал объектно-ориентированную версию.

quaternion.py:

import numpy as np
from math import sin, cos, acos, sqrt

def normalize(v, tolerance=0.00001):
    mag2 = sum(n * n for n in v)
    if abs(mag2 - 1.0) > tolerance:
        mag = sqrt(mag2)
        v = tuple(n / mag for n in v)
    return np.array(v)

class Quaternion:

    def from_axisangle(theta, v):
        theta = theta
        v = normalize(v)

        new_quaternion = Quaternion()
        new_quaternion._axisangle_to_q(theta, v)
        return new_quaternion

    def from_value(value):
        new_quaternion = Quaternion()
        new_quaternion._val = value
        return new_quaternion

    def _axisangle_to_q(self, theta, v):
        x = v[0]
        y = v[1]
        z = v[2]

        w = cos(theta/2.)
        x = x * sin(theta/2.)
        y = y * sin(theta/2.)
        z = z * sin(theta/2.)

        self._val = np.array([w, x, y, z])

    def __mul__(self, b):

        if isinstance(b, Quaternion):
            return self._multiply_with_quaternion(b)
        elif isinstance(b, (list, tuple, np.ndarray)):
            if len(b) != 3:
                raise Exception(f"Input vector has invalid length {len(b)}")
            return self._multiply_with_vector(b)
        else:
            raise Exception(f"Multiplication with unknown type {type(b)}")

    def _multiply_with_quaternion(self, q2):
        w1, x1, y1, z1 = self._val
        w2, x2, y2, z2 = q2._val
        w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2
        x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2
        y = w1 * y2 + y1 * w2 + z1 * x2 - x1 * z2
        z = w1 * z2 + z1 * w2 + x1 * y2 - y1 * x2

        result = Quaternion.from_value(np.array((w, x, y, z)))
        return result

    def _multiply_with_vector(self, v):
        q2 = Quaternion.from_value(np.append((0.0), v))
        return (self * q2 * self.get_conjugate())._val[1:]

    def get_conjugate(self):
        w, x, y, z = self._val
        result = Quaternion.from_value(np.array((w, -x, -y, -z)))
        return result

    def __repr__(self):
        theta, v = self.get_axisangle()
        return f"((%.6f; %.6f, %.6f, %.6f))"%(theta, v[0], v[1], v[2])

    def get_axisangle(self):
        w, v = self._val[0], self._val[1:]
        theta = acos(w) * 2.0

        return theta, normalize(v)

    def tolist(self):
        return self._val.tolist()

    def vector_norm(self):
        w, v = self.get_axisangle()
        return np.linalg.norm(v)

В этой версии можно просто использовать перегруженные операторы для умножения кватернион-кватернион и вектор-кватернион

from quaternion import Quaternion
import numpy as np

x_axis_unit = (1, 0, 0)
y_axis_unit = (0, 1, 0)
z_axis_unit = (0, 0, 1)

r1 = Quaternion.from_axisangle(np.pi / 2, x_axis_unit)
r2 = Quaternion.from_axisangle(np.pi / 2, y_axis_unit)
r3 = Quaternion.from_axisangle(np.pi / 2, z_axis_unit)

# Quaternion - vector multiplication
v = r1 * y_axis_unit
v = r2 * v
v = r3 * v

print(v)

# Quaternion - quaternion multiplication
r_total = r3 * r2 * r1
v = r_total * y_axis_unit

print(v)

Я не собирался реализовывать полноценный модуль кватерниона, так что это опять-таки для учебных целей, как в отличном ответе @ senderle. Я надеюсь, что это поможет тем, кто хочет понять и испытать новые вещи с кватернионами.

2 голосов
/ 02 февраля 2011

Обратите внимание, что инверсия матрицы вовсе не так уж тривиальна!Во-первых, все n (где n - размерность вашего пространства) точек должны находиться в общем положении (т.е. ни одна отдельная точка не может быть выражена как линейная комбинация остальных точек [предостережение: это может показаться простым требованием,но в области числовой линейной алгебры она нетривиальна: окончательное решение, существует ли такая конфигурация на самом деле или нет, в конечном итоге будет основано на знаниях, относящихся к «фактической области»]).

Также «соответствие» новых и старых точек может быть не точным (и тогда вам следует использовать наилучший из возможных приближений «истинного соответствия», т. Е. :).Псевдообратное (вместо того, чтобы пытаться использовать обычное обратное) рекомендуется всегда, когда ваша библиотека предоставляет это.

Преимущество псевдообращения заключается в том, что вы сможете использовать больше точек для своего преобразования, что увеличивает вероятность того, что по крайней мере n точек будет в общем положении.

Вот пример, поворот единицы площади на 90 град.ccw в 2D (но, очевидно, это определение работает в любом затемнении), с numpy:

In []: P=  matrix([[0, 0, 1, 1],
                   [0, 1, 1, 0]])
In []: Pn= matrix([[0, -1, -1, 0],
                   [0,  0,  1, 1]])
In []: T= Pn* pinv(P)
In []: (T* P).round()
Out[]:
matrix([[ 0., -1., -1.,  0.],
        [ 0.,  0.,  1.,  1.]])

PS numpy также быстро.Преобразование 1 миллиона очков в моем скромном компьютере:

In []: P= matrix(rand(2, 1e6))
In []: %timeit T* P
10 loops, best of 3: 37.7 ms per loop
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...