quaternion.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. #!/usr/bin/env python
  2. """
  3. Quaternion implementation for use in pymavlink
  4. """
  5. from __future__ import absolute_import, division, print_function
  6. from builtins import object
  7. import numpy as np
  8. from .rotmat import Vector3, Matrix3
  9. __author__ = "Thomas Gubler"
  10. __copyright__ = "Copyright (C) 2014 Thomas Gubler"
  11. __license__ = "GNU Lesser General Public License v3"
  12. __email__ = "thomasgubler@gmail.com"
  13. class QuaternionBase(object):
  14. """
  15. Quaternion class, this is the version which supports numpy arrays
  16. If you need support for Matrix3 look at the Quaternion class
  17. Usage:
  18. >>> from quaternion import QuaternionBase
  19. >>> import numpy as np
  20. >>> q = QuaternionBase([np.radians(20), np.radians(20), np.radians(20)])
  21. >>> print(q)
  22. [ 0.9603483 0.13871646 0.19810763 0.13871646]
  23. >>> print(q.dcm)
  24. [[ 0.88302222 -0.21147065 0.41898917]
  25. [ 0.3213938 0.92303098 -0.21147065]
  26. [-0.34202014 0.3213938 0.88302222]]
  27. >>> q = QuaternionBase([1, 0, 0, 0])
  28. >>> print(q.euler)
  29. [ 0. -0. 0.]
  30. >>> m = [[1, 0, 0], [0, 0, -1], [0, 1, 0]]
  31. >>> q = QuaternionBase(m)
  32. >>> vector = [0, 1, 0]
  33. >>> vector2 = q.transform(vector)
  34. """
  35. def __init__(self, attitude=[1, 0, 0, 0]):
  36. """
  37. Construct a quaternion from an attitude
  38. :param attitude: another QuaternionBase,
  39. 3 element list [roll, pitch, yaw],
  40. 4 element list [w, x, y ,z], DCM (3x3 array)
  41. """
  42. if isinstance(attitude, QuaternionBase):
  43. self.q = attitude.q
  44. elif np.array(attitude).shape == (3, 3):
  45. self.dcm = attitude
  46. elif len(attitude) == 4:
  47. self.q = attitude
  48. elif len(attitude) == 3:
  49. self.euler = attitude
  50. else:
  51. raise TypeError("attitude is not valid")
  52. @property
  53. def q(self):
  54. """
  55. Get the quaternion
  56. :returns: array containing the quaternion elements
  57. """
  58. if self._q is None:
  59. if self._euler is not None:
  60. # get q from euler
  61. self._q = self._euler_to_q(self.euler)
  62. elif self._dcm is not None:
  63. # get q from DCM
  64. self._q = self._dcm_to_q(self.dcm)
  65. return self._q
  66. def __getitem__(self, index):
  67. """Returns the quaternion entry at index"""
  68. return self.q[index]
  69. @q.setter
  70. def q(self, q):
  71. """
  72. Set the quaternion
  73. :param q: list or array of quaternion values [w, x, y, z]
  74. """
  75. self._q = np.array(q)
  76. # mark other representations as outdated, will get generated on next
  77. # read
  78. self._euler = None
  79. self._dcm = None
  80. @property
  81. def euler(self):
  82. """
  83. Get the euler angles.
  84. The convention is Tait-Bryan (ZY'X'')
  85. :returns: array containing the euler angles [roll, pitch, yaw]
  86. """
  87. if self._euler is None:
  88. if self._q is not None:
  89. # try to get euler angles from q via DCM
  90. self._dcm = self._q_to_dcm(self.q)
  91. self._euler = self._dcm_to_euler(self.dcm)
  92. elif self._dcm is not None:
  93. # get euler angles from DCM
  94. self._euler = self._dcm_to_euler(self.dcm)
  95. return self._euler
  96. @euler.setter
  97. def euler(self, euler):
  98. """
  99. Set the euler angles
  100. :param euler: list or array of the euler angles [roll, pitch, yaw]
  101. """
  102. assert(len(euler) == 3)
  103. self._euler = np.array(euler)
  104. # mark other representations as outdated, will get generated on next
  105. # read
  106. self._q = None
  107. self._dcm = None
  108. @property
  109. def dcm(self):
  110. """
  111. Get the DCM
  112. :returns: 3x3 array
  113. """
  114. if self._dcm is None:
  115. if self._q is not None:
  116. # try to get dcm from q
  117. self._dcm = self._q_to_dcm(self.q)
  118. elif self._euler is not None:
  119. # try to get get dcm from euler
  120. self._dcm = self._euler_to_dcm(self._euler)
  121. return self._dcm
  122. @dcm.setter
  123. def dcm(self, dcm):
  124. """
  125. Set the DCM
  126. :param dcm: 3x3 array
  127. """
  128. assert(len(dcm) == 3)
  129. for sub in dcm:
  130. assert(len(sub) == 3)
  131. self._dcm = np.array(dcm)
  132. # mark other representations as outdated, will get generated on next
  133. # read
  134. self._q = None
  135. self._euler = None
  136. def transform(self, v):
  137. """
  138. Calculates the vector transformed by this quaternion
  139. :param v: array with len 3 to be transformed
  140. :returns: transformed vector
  141. """
  142. assert(len(v) == 3)
  143. assert(np.allclose(self.norm, 1))
  144. # perform transformation t = q * [0, v] * q^-1 but avoid multiplication
  145. # because terms cancel out
  146. q0 = self.q[0]
  147. qi = self.q[1:4]
  148. ui = np.array(v)
  149. a = q0 * ui + np.cross(qi, ui)
  150. t = np.dot(qi, ui) * qi + q0 * a - np.cross(a, qi)
  151. return t
  152. @property
  153. def norm(self):
  154. """
  155. Returns norm of quaternion
  156. :returns: norm (scalar)
  157. """
  158. return QuaternionBase.norm_array(self.q)
  159. def normalize(self):
  160. """Normalizes the quaternion"""
  161. self.q = QuaternionBase.normalize_array(self.q)
  162. @property
  163. def inversed(self):
  164. """
  165. Get inversed quaternion
  166. :returns: inversed quaternion
  167. """
  168. q_inv = self._q_inversed(self.q)
  169. return QuaternionBase(q_inv)
  170. def __eq__(self, other):
  171. """
  172. Equality test (same orientation, not necessarily same rotation)
  173. :param other: a QuaternionBase
  174. :returns: true if the quaternions are equal
  175. """
  176. if isinstance(other, QuaternionBase):
  177. return abs(self.q.dot(other.q)) > 1 - np.finfo(float).eps
  178. return NotImplemented
  179. def close(self, other):
  180. """
  181. Equality test with tolerance
  182. (same orientation, not necessarily same rotation)
  183. :param other: a QuaternionBase
  184. :returns: true if the quaternions are almost equal
  185. """
  186. if isinstance(other, QuaternionBase):
  187. return np.allclose(self.q, other.q) or np.allclose(self.q, -other.q)
  188. return NotImplemented
  189. def __mul__(self, other):
  190. """
  191. :param other: QuaternionBase
  192. :returns: multiplaction of this Quaternion with other
  193. """
  194. if isinstance(other, QuaternionBase):
  195. o = other.q
  196. elif len(other) == 4:
  197. o = other
  198. else:
  199. return NotImplemented
  200. return QuaternionBase(self._mul_array(self.q, o))
  201. def __truediv__(self, other):
  202. """
  203. :param other: QuaternionBase
  204. :returns: division of this Quaternion with other
  205. """
  206. if isinstance(other, QuaternionBase):
  207. o = other
  208. elif len(other) == 4:
  209. o = QuaternionBase(other)
  210. else:
  211. return NotImplemented
  212. return self * o.inversed
  213. @staticmethod
  214. def normalize_array(q):
  215. """
  216. Normalizes the list with len 4 so that it can be used as quaternion
  217. :param q: array of len 4
  218. :returns: normalized array
  219. """
  220. assert(len(q) == 4)
  221. q = np.array(q)
  222. n = QuaternionBase.norm_array(q)
  223. return q / n
  224. @staticmethod
  225. def norm_array(q):
  226. """
  227. Calculate quaternion norm on array q
  228. :param quaternion: array of len 4
  229. :returns: norm (scalar)
  230. """
  231. assert(len(q) == 4)
  232. return np.sqrt(np.dot(q, q))
  233. def _mul_array(self, p, q):
  234. """
  235. Performs multiplication of the 2 quaterniona arrays p and q
  236. :param p: array of len 4
  237. :param q: array of len 4
  238. :returns: array of len, result of p * q (with p, q quaternions)
  239. """
  240. assert(len(q) == len(p) == 4)
  241. p0 = p[0]
  242. pi = p[1:4]
  243. q0 = q[0]
  244. qi = q[1:4]
  245. res = np.zeros(4)
  246. res[0] = p0 * q0 - np.dot(pi, qi)
  247. res[1:4] = p0 * qi + q0 * pi + np.cross(pi, qi)
  248. return res
  249. def _euler_to_q(self, euler):
  250. """
  251. Create q array from euler angles
  252. :param euler: array [roll, pitch, yaw] in rad
  253. :returns: array q which represents a quaternion [w, x, y, z]
  254. """
  255. assert(len(euler) == 3)
  256. phi = euler[0]
  257. theta = euler[1]
  258. psi = euler[2]
  259. c_phi_2 = np.cos(phi / 2)
  260. s_phi_2 = np.sin(phi / 2)
  261. c_theta_2 = np.cos(theta / 2)
  262. s_theta_2 = np.sin(theta / 2)
  263. c_psi_2 = np.cos(psi / 2)
  264. s_psi_2 = np.sin(psi / 2)
  265. q = np.zeros(4)
  266. q[0] = (c_phi_2 * c_theta_2 * c_psi_2 +
  267. s_phi_2 * s_theta_2 * s_psi_2)
  268. q[1] = (s_phi_2 * c_theta_2 * c_psi_2 -
  269. c_phi_2 * s_theta_2 * s_psi_2)
  270. q[2] = (c_phi_2 * s_theta_2 * c_psi_2 +
  271. s_phi_2 * c_theta_2 * s_psi_2)
  272. q[3] = (c_phi_2 * c_theta_2 * s_psi_2 -
  273. s_phi_2 * s_theta_2 * c_psi_2)
  274. return q
  275. def _q_to_dcm(self, q):
  276. """
  277. Create DCM from q
  278. :param q: array q which represents a quaternion [w, x, y, z]
  279. :returns: 3x3 dcm array
  280. """
  281. assert(len(q) == 4)
  282. assert(np.allclose(QuaternionBase.norm_array(q), 1))
  283. dcm = np.zeros([3, 3])
  284. a = q[0]
  285. b = q[1]
  286. c = q[2]
  287. d = q[3]
  288. a_sq = a * a
  289. b_sq = b * b
  290. c_sq = c * c
  291. d_sq = d * d
  292. dcm[0][0] = a_sq + b_sq - c_sq - d_sq
  293. dcm[0][1] = 2 * (b * c - a * d)
  294. dcm[0][2] = 2 * (a * c + b * d)
  295. dcm[1][0] = 2 * (b * c + a * d)
  296. dcm[1][1] = a_sq - b_sq + c_sq - d_sq
  297. dcm[1][2] = 2 * (c * d - a * b)
  298. dcm[2][0] = 2 * (b * d - a * c)
  299. dcm[2][1] = 2 * (a * b + c * d)
  300. dcm[2][2] = a_sq - b_sq - c_sq + d_sq
  301. return dcm
  302. def _dcm_to_q(self, dcm):
  303. """
  304. Create q from dcm
  305. Reference:
  306. - Shoemake, Quaternions,
  307. http://www.cs.ucr.edu/~vbz/resources/quatut.pdf
  308. :param dcm: 3x3 dcm array
  309. returns: quaternion array
  310. """
  311. assert(dcm.shape == (3, 3))
  312. q = np.zeros(4)
  313. tr = np.trace(dcm)
  314. if tr > 0:
  315. s = np.sqrt(tr + 1.0)
  316. q[0] = s * 0.5
  317. s = 0.5 / s
  318. q[1] = (dcm[2][1] - dcm[1][2]) * s
  319. q[2] = (dcm[0][2] - dcm[2][0]) * s
  320. q[3] = (dcm[1][0] - dcm[0][1]) * s
  321. else:
  322. dcm_i = np.argmax(np.diag(dcm))
  323. dcm_j = (dcm_i + 1) % 3
  324. dcm_k = (dcm_i + 2) % 3
  325. s = np.sqrt((dcm[dcm_i][dcm_i] - dcm[dcm_j][dcm_j] -
  326. dcm[dcm_k][dcm_k]) + 1.0)
  327. q[dcm_i + 1] = s * 0.5
  328. s = 0.5 / s
  329. q[dcm_j + 1] = (dcm[dcm_i][dcm_j] + dcm[dcm_j][dcm_i]) * s
  330. q[dcm_k + 1] = (dcm[dcm_k][dcm_i] + dcm[dcm_i][dcm_k]) * s
  331. q[0] = (dcm[dcm_k][dcm_j] - dcm[dcm_j][dcm_k]) * s
  332. return q
  333. def _euler_to_dcm(self, euler):
  334. """
  335. Create DCM from euler angles
  336. :param euler: array [roll, pitch, yaw] in rad
  337. :returns: 3x3 dcm array
  338. """
  339. assert(len(euler) == 3)
  340. phi = euler[0]
  341. theta = euler[1]
  342. psi = euler[2]
  343. dcm = np.zeros([3, 3])
  344. c_phi = np.cos(phi)
  345. s_phi = np.sin(phi)
  346. c_theta = np.cos(theta)
  347. s_theta = np.sin(theta)
  348. c_psi = np.cos(psi)
  349. s_psi = np.sin(psi)
  350. dcm[0][0] = c_theta * c_psi
  351. dcm[0][1] = -c_phi * s_psi + s_phi * s_theta * c_psi
  352. dcm[0][2] = s_phi * s_psi + c_phi * s_theta * c_psi
  353. dcm[1][0] = c_theta * s_psi
  354. dcm[1][1] = c_phi * c_psi + s_phi * s_theta * s_psi
  355. dcm[1][2] = -s_phi * c_psi + c_phi * s_theta * s_psi
  356. dcm[2][0] = -s_theta
  357. dcm[2][1] = s_phi * c_theta
  358. dcm[2][2] = c_phi * c_theta
  359. return dcm
  360. def _dcm_to_euler(self, dcm):
  361. """
  362. Create DCM from euler angles
  363. :param dcm: 3x3 dcm array
  364. :returns: array [roll, pitch, yaw] in rad
  365. """
  366. assert(dcm.shape == (3, 3))
  367. theta = np.arcsin(min(1, max(-1, -dcm[2][0])))
  368. if abs(theta - np.pi/2) < 1.0e-3:
  369. phi = 0.0
  370. psi = (np.arctan2(dcm[1][2] - dcm[0][1],
  371. dcm[0][2] + dcm[1][1]) + phi)
  372. elif abs(theta + np.pi/2) < 1.0e-3:
  373. phi = 0.0
  374. psi = np.arctan2(dcm[1][2] - dcm[0][1],
  375. dcm[0][2] + dcm[1][1] - phi)
  376. else:
  377. phi = np.arctan2(dcm[2][1], dcm[2][2])
  378. psi = np.arctan2(dcm[1][0], dcm[0][0])
  379. return np.array([phi, theta, psi])
  380. def _q_inversed(self, q):
  381. """
  382. Returns inversed quaternion q
  383. :param q: array q which represents a quaternion [w, x, y, z]
  384. :returns: inversed array q which is a quaternion [w, x, y ,z]
  385. """
  386. assert(len(q) == 4)
  387. return np.hstack([q[0], -q[1:4]])
  388. def __str__(self):
  389. """String of quaternion values"""
  390. return str(self.q)
  391. class Quaternion(QuaternionBase):
  392. """
  393. Quaternion class that supports pymavlink's Vector3 and Matrix3
  394. Usage:
  395. >>> from quaternion import Quaternion
  396. >>> from rotmat import Vector3, Matrix3
  397. >>> m = Matrix3()
  398. >>> m.from_euler(45, 0, 0)
  399. >>> print(m)
  400. Matrix3((1.00, 0.00, 0.00), (0.00, 0.53, -0.85), (-0.00, 0.85, 0.53))
  401. >>> q = Quaternion(m)
  402. >>> print(q)
  403. [ 0.87330464 0.48717451 0. 0. ]
  404. >>> print(q.dcm)
  405. Matrix3((1.00, 0.00, 0.00), (0.00, 0.53, -0.85), (-0.00, 0.85, 0.53))
  406. >>> v = Vector3(0, 1, 0)
  407. >>> v2 = q.transform(v)
  408. >>> print(v2)
  409. Vector3(0.00, 0.53, 0.85)
  410. """
  411. def __init__(self, attitude):
  412. """
  413. Construct a quaternion from an attitude
  414. :param attitude: another Quaternion, QuaternionBase,
  415. 3 element list [roll, pitch, yaw],
  416. 4 element list [w, x, y ,z], DCM (3x3 array or Matrix3)
  417. """
  418. if isinstance(attitude, Quaternion):
  419. self.q = attitude.q
  420. if isinstance(attitude, Matrix3):
  421. self.dcm = attitude
  422. elif np.array(attitude).shape == (3, 3):
  423. # convert dcm array to Matrix3
  424. self.dcm = self._dcm_array_to_matrix3(attitude)
  425. elif isinstance(attitude, Vector3):
  426. # provided euler angles
  427. euler = [attitude.x, attitude.y, attitude.z]
  428. super(Quaternion, self).__init__(euler)
  429. else:
  430. super(Quaternion, self).__init__(attitude)
  431. @property
  432. def dcm(self):
  433. """
  434. Get the DCM
  435. :returns: Matrix3
  436. """
  437. if self._dcm is None:
  438. if self._q is not None:
  439. # try to get dcm from q
  440. self._dcm = self._q_to_dcm(self.q)
  441. elif self._euler is not None:
  442. # try to get get dcm from euler
  443. self._dcm = self._euler_to_dcm(self._euler)
  444. return self._dcm
  445. @dcm.setter
  446. def dcm(self, dcm):
  447. """
  448. Set the DCM
  449. :param dcm: Matrix3
  450. """
  451. assert(isinstance(dcm, Matrix3))
  452. self._dcm = dcm.copy()
  453. # mark other representations as outdated, will get generated on next
  454. # read
  455. self._q = None
  456. self._euler = None
  457. @property
  458. def inversed(self):
  459. """
  460. Get inversed quaternion
  461. :returns: inversed quaternion
  462. """
  463. return Quaternion(super(Quaternion, self).inversed)
  464. def transform(self, v3):
  465. """
  466. Calculates the vector transformed by this quaternion
  467. :param v3: Vector3 to be transformed
  468. :returns: transformed vector
  469. """
  470. if isinstance(v3, Vector3):
  471. t = super(Quaternion, self).transform([v3.x, v3.y, v3.z])
  472. return Vector3(t[0], t[1], t[2])
  473. elif len(v3) == 3:
  474. return super(Quaternion, self).transform(v3)
  475. else:
  476. raise TypeError("param v3 is not a vector type")
  477. def _dcm_array_to_matrix3(self, dcm):
  478. """
  479. Converts dcm array into Matrix3
  480. :param dcm: 3x3 dcm array
  481. :returns: Matrix3
  482. """
  483. assert(dcm.shape == (3, 3))
  484. a = Vector3(dcm[0][0], dcm[0][1], dcm[0][2])
  485. b = Vector3(dcm[1][0], dcm[1][1], dcm[1][2])
  486. c = Vector3(dcm[2][0], dcm[2][1], dcm[2][2])
  487. return Matrix3(a, b, c)
  488. def _matrix3_to_dcm_array(self, m):
  489. """
  490. Converts Matrix3 in an array
  491. :param m: Matrix3
  492. :returns: 3x3 array
  493. """
  494. assert(isinstance(m, Matrix3))
  495. return np.array([[m.a.x, m.a.y, m.a.z],
  496. [m.b.x, m.b.y, m.b.z],
  497. [m.c.x, m.c.y, m.c.z]])
  498. def _q_to_dcm(self, q):
  499. """
  500. Create DCM (Matrix3) from q
  501. :param q: array q which represents a quaternion [w, x, y, z]
  502. :returns: Matrix3
  503. """
  504. assert(len(q) == 4)
  505. arr = super(Quaternion, self)._q_to_dcm(q)
  506. return self._dcm_array_to_matrix3(arr)
  507. def _dcm_to_q(self, dcm):
  508. """
  509. Create q from dcm (Matrix3)
  510. :param dcm: Matrix3
  511. :returns: array q which represents a quaternion [w, x, y, z]
  512. """
  513. assert(isinstance(dcm, Matrix3))
  514. arr = self._matrix3_to_dcm_array(dcm)
  515. return super(Quaternion, self)._dcm_to_q(arr)
  516. def _euler_to_dcm(self, euler):
  517. """
  518. Create DCM (Matrix3) from euler angles
  519. :param euler: array [roll, pitch, yaw] in rad
  520. :returns: Matrix3
  521. """
  522. assert(len(euler) == 3)
  523. m = Matrix3()
  524. m.from_euler(*euler)
  525. return m
  526. def _dcm_to_euler(self, dcm):
  527. """
  528. Create DCM from euler angles
  529. :param dcm: Matrix3
  530. :returns: array [roll, pitch, yaw] in rad
  531. """
  532. assert(isinstance(dcm, Matrix3))
  533. return np.array(dcm.to_euler())
  534. def __mul__(self, other):
  535. """
  536. :param other: Quaternion
  537. :returns: multiplaction of this Quaternion with other
  538. """
  539. return Quaternion(super(Quaternion, self).__mul__(other))
  540. def __truediv__(self, other):
  541. """
  542. :param other: Quaternion
  543. :returns: division of this Quaternion with other
  544. """
  545. return Quaternion(super(Quaternion, self).__truediv__(other))
  546. if __name__ == "__main__":
  547. import doctest
  548. doctest.testmod()
  549. import unittest
  550. unittest.main('quaterniontest')