système de coordonnées rotatif via un quaternion
Nous avons un gazillion de coordonnées spatiales (x, Y et z) représentant des atomes dans l'espace 3d, et je construis une fonction qui va traduire ces points en un nouveau système de coordonnées. Déplacer les coordonnées vers une origine arbitraire est simple, mais je ne peux pas me concentrer sur l'étape suivante: les calculs de rotation de points 3d. En d'autres termes, je suis en train de traduire les points de (x, y, z) à (x', y', z'), où x', y' et z' sont en termes de i', j' et k', le nouvel axe des vecteurs que je fais avec l'aide du module Python Euclide .
Je pense que Tout ce dont j'ai besoin est un quaternion Euclide pour le faire, c'est-à-dire
>>> q * Vector3(x, y, z)
Vector3(x', y', z')
Mais pour cela, je crois que j'ai besoin d'un vecteur d'axe de rotation et d'un angle de rotation. Mais je n'ai aucune idée de comment les calculer à partir de i', j 'et k'. Cela semble être une procédure simple pour coder à partir de zéro, mais je soupçonne que quelque chose comme ça nécessite une algèbre linéaire pour comprendre par moi-même. Merci beaucoup pour un coup de pouce dans la bonne direction.
3 réponses
Utiliser des quaternions pour représenter la rotation n'est pas difficile d'un point de vue algébrique. Personnellement, je trouve difficile de raisonner visuellement sur les quaternions, mais les formules impliquées dans leur utilisation pour les rotations sont assez simples. Je vais fournir un ensemble de base de fonctions de référence ici.1 (Voir Aussi cette belle réponse de hosolmaz , dans laquelle il les regroupe pour créer une classe quaternion pratique.)
Vous pouvez penser aux quaternions (pour notre en tant que scalaire plus un vecteur 3D-abstractly, w + xi + yj + zk
, ici représenté par un tuple simple (w, x, y, z)
. L'espace des rotations 3D est représenté en entier par un sous-espace des quaternions, l'espace deunité quaternions, donc vous voulez vous assurer que vos quaternions sont normalisés. Vous pouvez le faire de la même manière que vous normaliseriez n'importe quel vecteur 4 (c'est-à-dire que la magnitude devrait être proche de 1; si ce n'est pas le cas, réduisez les valeurs par la magnitude):
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
Veuillez noter que pour pour simplifier, les fonctions suivantes supposent que les valeurs de quaternion sont déjà normalisées . En pratique, vous devrez les renormaliser de temps en temps, mais la meilleure façon de traiter cela dépendra du domaine du problème. Ces fonctions fournissent juste les bases mêmes, à des fins de référence seulement.
Chaque rotation est représentée par un quaternion unitaire, et les concaténations de rotations correspondent à multiplications de quaternions unitaires. La formule2 pour cela est comme suit:
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
Pour faire pivoter un vecteur par un quaternion, vous avez également besoin du conjugué du quaternion. C'est facile:
def q_conjugate(q):
w, x, y, z = q
return (w, -x, -y, -z)
Maintenant, la multiplication par quaternion-vecteur est aussi simple que de convertir un vecteur en quaternion (en définissant w = 0
et en laissant x
, y
, et z
le même) et ensuite multiplier 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:]
Enfin, vous devez savoir comment convertir les rotations axe-angle en quaternions. Aussi facile! Il est logique de " désinfecter" entrée et sortie ici en appelant 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
Et retour:
def q_to_axisangle(q):
w, v = q[0], q[1:]
theta = acos(w) * 2.0
return normalize(v), theta
Voici un exemple d'utilisation. Une séquence de rotations de 90 degrés autour des axes x, y et z ramènera un vecteur sur l'axe y à sa position d'origine. Ce code effectue ces rotations:
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)
Gardez à l'esprit que cette séquence de rotations ne ramènera pas tous les vecteurs à la même position; par exemple, pour un vecteur sur l'axe x, il correspondra à une rotation de 90 degrés autour du y axe. (Gardez la règle de droite à l'esprit ici; une rotation positive autour de l'axe y pousse un vecteur sur l'axe x dans la région négative 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)
Comme toujours, faites - le moi savoir si vous trouvez des problèmes ici.
1. Ceux - ci sont adaptés d'un tutoriel OpenGL archivé ici.
2. La formule de multiplication des quaternions ressemble à un nid de rat fou, mais la dérivation est simple (si fastidieuse). Il suffit de noter d'abord que ii = jj = kk = -1
; alors que ij = k
, jk = i
, ki = j
; et enfin que ji = -k
, kj = -i
, ik = -j
. Ensuite, multipliez les deux quaternions, distribuez les Termes et réorganisez-les en fonction des résultats de chacune des 16 multiplications. Cela aide également à illustrer pourquoi Vous pouvez utiliser des quaternions pour représenter la rotation; les six dernières identités suivent la règle de droite, créant des bijections entre les rotations depuis i
à j
et rotations autour k
, et ainsi sur.
Notez que l'inversion de la matrice n'est pas si triviale du tout! Premièrement, tous les points n (où n est la dimension de votre espace) doivent être en position générale (c'est-à-dire qu'aucun point individuel ne peut être exprimé comme une combinaison linéaire du reste des points [mise en garde: cela peut sembler être une exigence simple, mais dans le domaine de l'algèbre linéaire numérique, c'est non trivial; la décision finale connaissance]).
Aussi la 'correspondance' des nouveaux et anciens points peut ne pas être exacte (et alors vous devriez utiliser le meilleur approximateur possible de la 'vraie correspondance', c'est-à-dire:). Pseudo inverse (au lieu d'essayer d'utiliser l'inverse simple) est recommandé toujours quand votre lib le fournit.
Le pseudo inverse a l'avantage de pouvoir utiliser plus de points pour votre transformation, augmentant ainsi la probabilité qu'au moins n points soient en général position.
Voici un exemple, la rotation de l'unité carré 90 deg. ccw en 2D (mais évidemment cette détermination fonctionne dans n'importe quel dim), avec 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.]])
P.S. numpy
est aussi rapide. Transformation de 1 million de points dans mon ordinateur modeste:
In []: P= matrix(rand(2, 1e6))
In []: %timeit T* P
10 loops, best of 3: 37.7 ms per loop
Cette question et la réponse donnée par @senderle m'ont vraiment aidé dans l'un de mes projets. La réponse est minime et couvre le cœur de la plupart des calculs quaternion que l'on pourrait avoir besoin d'effectuer.
Pour mon propre projet, j'ai trouvé fastidieux d'avoir des fonctions séparées pour toutes les opérations et de les importer une par une chaque fois que j'en ai besoin, donc j'ai implémenté une version orientée objet.
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)
Dans cette version, on peut simplement utiliser le surchargé opérateurs pour la multiplication quaternion-quaternion et quaternion-vecteur
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)
Je n'avais pas l'intention de mettre en œuvre un module quaternion à part entière, donc c'est à nouveau à des fins pédagogiques, comme dans la grande réponse de @senderle. J'espère que cela aide à ceux qui veulent comprendre et essayer de nouvelles choses avec les quaternions.