Coverage for encodermap/misc/transformations.py: 9%
709 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-07 11:05 +0000
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-07 11:05 +0000
1# -*- coding: utf-8 -*-
2# encodermap/misc/transformations.py
4# Copyright (c) 2006-2013, Christoph Gohlke
5# Copyright (c) 2006-2013, The Regents of the University of California
6# Produced at the Laboratory for Fluorescence Dynamics
7# All rights reserved.
8#
9# Redistribution and use in source and binary forms, with or without
10# modification, are permitted provided that the following conditions are met:
11#
12# * Redistributions of source code must retain the above copyright
13# notice, this list of conditions and the following disclaimer.
14# * Redistributions in binary form must reproduce the above copyright
15# notice, this list of conditions and the following disclaimer in the
16# documentation and/or other materials provided with the distribution.
17# * Neither the name of the copyright holders nor the names of any
18# contributors may be used to endorse or promote products derived
19# from this software without specific prior written permission.
20#
21# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
22# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
25# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
26# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
27# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
28# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
29# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
30# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31# POSSIBILITY OF SUCH DAMAGE.
33"""Homogeneous Transformation Matrices and Quaternions.
35A library for calculating 4x4 matrices for translating, rotating, reflecting,
36scaling, shearing, projecting, orthogonalizing, and superimposing arrays of
373D homogeneous coordinates as well as for converting between rotation matrices,
38Euler angles, and quaternions. Also includes an Arcball control object and
39functions to decompose transformation matrices.
41:Author:
42 `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_
44:Organization:
45 Laboratory for Fluorescence Dynamics, University of California, Irvine
47:Version: 2013.06.29
49Requirements
50------------
51* `CPython 2.7 or 3.3 <http://www.python.org>`_
52* `Numpy 1.7 <http://www.numpy.org>`_
53* `Transformations.c 2013.01.18 <http://www.lfd.uci.edu/~gohlke/>`_
54 (recommended for speedup of some functions)
56Notes
57-----
58The API is not stable yet and is expected to change between revisions.
60This Python code is not optimized for speed. Refer to the transformations.c
61module for a faster implementation of some functions.
63Documentation in HTML format can be generated with epydoc.
65Matrices (M) can be inverted using numpy.linalg.inv(M), be concatenated using
66numpy.dot(M0, M1), or transform homogeneous coordinate arrays (v) using
67numpy.dot(M, v) for shape (4, \*) column vectors, respectively
68numpy.dot(v, M.T) for shape (\*, 4) row vectors ("array of points").
70This module follows the "column vectors on the right" and "row major storage"
71(C contiguous) conventions. The translation components are in the right column
72of the transformation matrix, i.e. M[:3, 3].
73The transpose of the transformation matrices may have to be used to interface
74with other graphics systems, e.g. with OpenGL's glMultMatrixd(). See also [16].
76Calculations are carried out with numpy.float64 precision.
78Vector, point, quaternion, and matrix function arguments are expected to be
79"array like", i.e. tuple, list, or numpy arrays.
81Return types are numpy arrays unless specified otherwise.
83Angles are in radians unless specified otherwise.
85Quaternions w+ix+jy+kz are represented as [w, x, y, z].
87A triple of Euler angles can be applied/interpreted in 24 ways, which can
88be specified using a 4 character string or encoded 4-tuple:
90 *Axes 4-string*: e.g. 'sxyz' or 'ryxy'
92 - first character : rotations are applied to 's'tatic or 'r'otating frame
93 - remaining characters : successive rotation axis 'x', 'y', or 'z'
95 *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1)
97 - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix.
98 - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed
99 by 'z', or 'z' is followed by 'x'. Otherwise odd (1).
100 - repetition : first and last axis are same (1) or different (0).
101 - frame : rotations are applied to static (0) or rotating (1) frame.
103References
104----------
105(1) Matrices and transformations. Ronald Goldman.
106 In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990.
107(2) More matrices and transformations: shear and pseudo-perspective.
108 Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
109(3) Decomposing a matrix into simple transformations. Spencer Thomas.
110 In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
111(4) Recovering the data from the transformation matrix. Ronald Goldman.
112 In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991.
113(5) Euler angle conversion. Ken Shoemake.
114 In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994.
115(6) Arcball rotation control. Ken Shoemake.
116 In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994.
117(7) Representing attitude: Euler angles, unit quaternions, and rotation
118 vectors. James Diebel. 2006.
119(8) A discussion of the solution for the best rotation to relate two sets
120 of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828.
121(9) Closed-form solution of absolute orientation using unit quaternions.
122 BKP Horn. J Opt Soc Am A. 1987. 4(4):629-642.
123(10) Quaternions. Ken Shoemake.
124 http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf
125(11) From quaternion to matrix and back. JMP van Waveren. 2005.
126 http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm
127(12) Uniform random rotations. Ken Shoemake.
128 In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992.
129(13) Quaternion in molecular modeling. CFF Karney.
130 J Mol Graph Mod, 25(5):595-604
131(14) New method for extracting the quaternion from a rotation matrix.
132 Itzhack Y Bar-Itzhack, J Guid Contr Dynam. 2000. 23(6): 1085-1087.
133(15) Multiple View Geometry in Computer Vision. Hartley and Zissermann.
134 Cambridge University Press; 2nd Ed. 2004. Chapter 4, Algorithm 4.7, p 130.
135(16) Column Vectors vs. Row Vectors.
136 http://steve.hollasch.net/cgindex/math/matrix/column-vec.html
138Examples
139--------
140>>> alpha, beta, gamma = 0.123, -1.234, 2.345
141>>> origin, xaxis, yaxis, zaxis = [0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1]
142>>> I = identity_matrix()
143>>> Rx = rotation_matrix(alpha, xaxis)
144>>> Ry = rotation_matrix(beta, yaxis)
145>>> Rz = rotation_matrix(gamma, zaxis)
146>>> R = concatenate_matrices(Rx, Ry, Rz)
147>>> euler = euler_from_matrix(R, 'rxyz')
148>>> numpy.allclose([alpha, beta, gamma], euler)
149True
150>>> Re = euler_matrix(alpha, beta, gamma, 'rxyz')
151>>> is_same_transform(R, Re)
152True
153>>> al, be, ga = euler_from_matrix(Re, 'rxyz')
154>>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz'))
155True
156>>> qx = quaternion_about_axis(alpha, xaxis)
157>>> qy = quaternion_about_axis(beta, yaxis)
158>>> qz = quaternion_about_axis(gamma, zaxis)
159>>> q = quaternion_multiply(qx, qy)
160>>> q = quaternion_multiply(q, qz)
161>>> Rq = quaternion_matrix(q)
162>>> is_same_transform(R, Rq)
163True
164>>> S = scale_matrix(1.23, origin)
165>>> T = translation_matrix([1, 2, 3])
166>>> Z = shear_matrix(beta, xaxis, origin, zaxis)
167>>> R = random_rotation_matrix(numpy.random.rand(3))
168>>> M = concatenate_matrices(T, R, Z, S)
169>>> scale, shear, angles, trans, persp = decompose_matrix(M)
170>>> numpy.allclose(scale, 1.23)
171True
172>>> numpy.allclose(trans, [1, 2, 3])
173True
174>>> numpy.allclose(shear, [0, math.tan(beta), 0])
175True
176>>> is_same_transform(R, euler_matrix(axes='sxyz', *angles))
177True
178>>> M1 = compose_matrix(scale, shear, angles, trans, persp)
179>>> is_same_transform(M, M1)
180True
181>>> v0, v1 = random_vector(3), random_vector(3)
182>>> M = rotation_matrix(angle_between_vectors(v0, v1), vector_product(v0, v1))
183>>> v2 = numpy.dot(v0, M[:3,:3].T)
184>>> numpy.allclose(unit_vector(v1), unit_vector(v2))
185True
187"""
189from __future__ import division, print_function
191import math
193import numpy
195__version__ = "2013.06.29"
196__docformat__ = "restructuredtext en"
197__all__ = []
200def identity_matrix():
201 """Return 4x4 identity/unit matrix.
203 >>> I = identity_matrix()
204 >>> numpy.allclose(I, numpy.dot(I, I))
205 True
206 >>> numpy.sum(I), numpy.trace(I)
207 (4.0, 4.0)
208 >>> numpy.allclose(I, numpy.identity(4))
209 True
211 """
212 return numpy.identity(4)
215def translation_matrix(direction):
216 """Return matrix to translate by direction vector.
218 >>> v = numpy.random.random(3) - 0.5
219 >>> numpy.allclose(v, translation_matrix(v)[:3, 3])
220 True
222 """
223 M = numpy.identity(4)
224 M[:3, 3] = direction[:3]
225 return M
228def translation_from_matrix(matrix):
229 """Return translation vector from translation matrix.
231 >>> v0 = numpy.random.random(3) - 0.5
232 >>> v1 = translation_from_matrix(translation_matrix(v0))
233 >>> numpy.allclose(v0, v1)
234 True
236 """
237 return numpy.array(matrix, copy=False)[:3, 3].copy()
240def reflection_matrix(point, normal):
241 """Return matrix to mirror at plane defined by point and normal vector.
243 >>> v0 = numpy.random.random(4) - 0.5
244 >>> v0[3] = 1.
245 >>> v1 = numpy.random.random(3) - 0.5
246 >>> R = reflection_matrix(v0, v1)
247 >>> numpy.allclose(2, numpy.trace(R))
248 True
249 >>> numpy.allclose(v0, numpy.dot(R, v0))
250 True
251 >>> v2 = v0.copy()
252 >>> v2[:3] += v1
253 >>> v3 = v0.copy()
254 >>> v2[:3] -= v1
255 >>> numpy.allclose(v2, numpy.dot(R, v3))
256 True
258 """
259 normal = unit_vector(normal[:3])
260 M = numpy.identity(4)
261 M[:3, :3] -= 2.0 * numpy.outer(normal, normal)
262 M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal
263 return M
266def reflection_from_matrix(matrix):
267 """Return mirror plane point and normal vector from reflection matrix.
269 >>> v0 = numpy.random.random(3) - 0.5
270 >>> v1 = numpy.random.random(3) - 0.5
271 >>> M0 = reflection_matrix(v0, v1)
272 >>> point, normal = reflection_from_matrix(M0)
273 >>> M1 = reflection_matrix(point, normal)
274 >>> is_same_transform(M0, M1)
275 True
277 """
278 M = numpy.array(matrix, dtype=numpy.float64, copy=False)
279 # normal: unit eigenvector corresponding to eigenvalue -1
280 w, V = numpy.linalg.eig(M[:3, :3])
281 i = numpy.where(abs(numpy.real(w) + 1.0) < 1e-8)[0]
282 if not len(i):
283 raise ValueError("no unit eigenvector corresponding to eigenvalue -1")
284 normal = numpy.real(V[:, i[0]]).squeeze()
285 # point: any unit eigenvector corresponding to eigenvalue 1
286 w, V = numpy.linalg.eig(M)
287 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
288 if not len(i):
289 raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
290 point = numpy.real(V[:, i[-1]]).squeeze()
291 point /= point[3]
292 return point, normal
295def rotation_matrix(angle, direction, point=None):
296 """Return matrix to rotate about axis defined by point and direction.
298 >>> R = rotation_matrix(math.pi/2, [0, 0, 1], [1, 0, 0])
299 >>> numpy.allclose(numpy.dot(R, [0, 0, 0, 1]), [1, -1, 0, 1])
300 True
301 >>> angle = (random.random() - 0.5) * (2*math.pi)
302 >>> direc = numpy.random.random(3) - 0.5
303 >>> point = numpy.random.random(3) - 0.5
304 >>> R0 = rotation_matrix(angle, direc, point)
305 >>> R1 = rotation_matrix(angle-2*math.pi, direc, point)
306 >>> is_same_transform(R0, R1)
307 True
308 >>> R0 = rotation_matrix(angle, direc, point)
309 >>> R1 = rotation_matrix(-angle, -direc, point)
310 >>> is_same_transform(R0, R1)
311 True
312 >>> I = numpy.identity(4, numpy.float64)
313 >>> numpy.allclose(I, rotation_matrix(math.pi*2, direc))
314 True
315 >>> numpy.allclose(2, numpy.trace(rotation_matrix(math.pi/2,
316 ... direc, point)))
317 True
319 """
320 sina = math.sin(angle)
321 cosa = math.cos(angle)
322 direction = unit_vector(direction[:3])
323 # rotation matrix around unit vector
324 R = numpy.diag([cosa, cosa, cosa])
325 R += numpy.outer(direction, direction) * (1.0 - cosa)
326 direction *= sina
327 R += numpy.array(
328 [
329 [0.0, -direction[2], direction[1]],
330 [direction[2], 0.0, -direction[0]],
331 [-direction[1], direction[0], 0.0],
332 ]
333 )
334 M = numpy.identity(4)
335 M[:3, :3] = R
336 if point is not None:
337 # rotation not around origin
338 point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
339 M[:3, 3] = point - numpy.dot(R, point)
340 return M
343def rotation_from_matrix(matrix):
344 """Return rotation angle and axis from rotation matrix.
346 >>> angle = (random.random() - 0.5) * (2*math.pi)
347 >>> direc = numpy.random.random(3) - 0.5
348 >>> point = numpy.random.random(3) - 0.5
349 >>> R0 = rotation_matrix(angle, direc, point)
350 >>> angle, direc, point = rotation_from_matrix(R0)
351 >>> R1 = rotation_matrix(angle, direc, point)
352 >>> is_same_transform(R0, R1)
353 True
355 """
356 R = numpy.array(matrix, dtype=numpy.float64, copy=False)
357 R33 = R[:3, :3]
358 # direction: unit eigenvector of R33 corresponding to eigenvalue of 1
359 w, W = numpy.linalg.eig(R33.T)
360 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
361 if not len(i):
362 raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
363 direction = numpy.real(W[:, i[-1]]).squeeze()
364 # point: unit eigenvector of R33 corresponding to eigenvalue of 1
365 w, Q = numpy.linalg.eig(R)
366 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
367 if not len(i):
368 raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
369 point = numpy.real(Q[:, i[-1]]).squeeze()
370 point /= point[3]
371 # rotation angle depending on direction
372 cosa = (numpy.trace(R33) - 1.0) / 2.0
373 if abs(direction[2]) > 1e-8:
374 sina = (R[1, 0] + (cosa - 1.0) * direction[0] * direction[1]) / direction[2]
375 elif abs(direction[1]) > 1e-8:
376 sina = (R[0, 2] + (cosa - 1.0) * direction[0] * direction[2]) / direction[1]
377 else:
378 sina = (R[2, 1] + (cosa - 1.0) * direction[1] * direction[2]) / direction[0]
379 angle = math.atan2(sina, cosa)
380 return angle, direction, point
383def scale_matrix(factor, origin=None, direction=None):
384 """Return matrix to scale by factor around origin in direction.
386 Use factor -1 for point symmetry.
388 >>> v = (numpy.random.rand(4, 5) - 0.5) * 20
389 >>> v[3] = 1
390 >>> S = scale_matrix(-1.234)
391 >>> numpy.allclose(numpy.dot(S, v)[:3], -1.234*v[:3])
392 True
393 >>> factor = random.random() * 10 - 5
394 >>> origin = numpy.random.random(3) - 0.5
395 >>> direct = numpy.random.random(3) - 0.5
396 >>> S = scale_matrix(factor, origin)
397 >>> S = scale_matrix(factor, origin, direct)
399 """
400 if direction is None:
401 # uniform scaling
402 M = numpy.diag([factor, factor, factor, 1.0])
403 if origin is not None:
404 M[:3, 3] = origin[:3]
405 M[:3, 3] *= 1.0 - factor
406 else:
407 # nonuniform scaling
408 direction = unit_vector(direction[:3])
409 factor = 1.0 - factor
410 M = numpy.identity(4)
411 M[:3, :3] -= factor * numpy.outer(direction, direction)
412 if origin is not None:
413 M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction
414 return M
417def scale_from_matrix(matrix):
418 """Return scaling factor, origin and direction from scaling matrix.
420 >>> factor = random.random() * 10 - 5
421 >>> origin = numpy.random.random(3) - 0.5
422 >>> direct = numpy.random.random(3) - 0.5
423 >>> S0 = scale_matrix(factor, origin)
424 >>> factor, origin, direction = scale_from_matrix(S0)
425 >>> S1 = scale_matrix(factor, origin, direction)
426 >>> is_same_transform(S0, S1)
427 True
428 >>> S0 = scale_matrix(factor, origin, direct)
429 >>> factor, origin, direction = scale_from_matrix(S0)
430 >>> S1 = scale_matrix(factor, origin, direction)
431 >>> is_same_transform(S0, S1)
432 True
434 """
435 M = numpy.array(matrix, dtype=numpy.float64, copy=False)
436 M33 = M[:3, :3]
437 factor = numpy.trace(M33) - 2.0
438 try:
439 # direction: unit eigenvector corresponding to eigenvalue factor
440 w, V = numpy.linalg.eig(M33)
441 i = numpy.where(abs(numpy.real(w) - factor) < 1e-8)[0][0]
442 direction = numpy.real(V[:, i]).squeeze()
443 direction /= vector_norm(direction)
444 except IndexError:
445 # uniform scaling
446 factor = (factor + 2.0) / 3.0
447 direction = None
448 # origin: any eigenvector corresponding to eigenvalue 1
449 w, V = numpy.linalg.eig(M)
450 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
451 if not len(i):
452 raise ValueError("no eigenvector corresponding to eigenvalue 1")
453 origin = numpy.real(V[:, i[-1]]).squeeze()
454 origin /= origin[3]
455 return factor, origin, direction
458def projection_matrix(point, normal, direction=None, perspective=None, pseudo=False):
459 """Return matrix to project onto plane defined by point and normal.
461 Using either perspective point, projection direction, or none of both.
463 If pseudo is True, perspective projections will preserve relative depth
464 such that Perspective = dot(Orthogonal, PseudoPerspective).
466 >>> P = projection_matrix([0, 0, 0], [1, 0, 0])
467 >>> numpy.allclose(P[1:, 1:], numpy.identity(4)[1:, 1:])
468 True
469 >>> point = numpy.random.random(3) - 0.5
470 >>> normal = numpy.random.random(3) - 0.5
471 >>> direct = numpy.random.random(3) - 0.5
472 >>> persp = numpy.random.random(3) - 0.5
473 >>> P0 = projection_matrix(point, normal)
474 >>> P1 = projection_matrix(point, normal, direction=direct)
475 >>> P2 = projection_matrix(point, normal, perspective=persp)
476 >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True)
477 >>> is_same_transform(P2, numpy.dot(P0, P3))
478 True
479 >>> P = projection_matrix([3, 0, 0], [1, 1, 0], [1, 0, 0])
480 >>> v0 = (numpy.random.rand(4, 5) - 0.5) * 20
481 >>> v0[3] = 1
482 >>> v1 = numpy.dot(P, v0)
483 >>> numpy.allclose(v1[1], v0[1])
484 True
485 >>> numpy.allclose(v1[0], 3-v1[1])
486 True
488 """
489 M = numpy.identity(4)
490 point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
491 normal = unit_vector(normal[:3])
492 if perspective is not None:
493 # perspective projection
494 perspective = numpy.array(perspective[:3], dtype=numpy.float64, copy=False)
495 M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective - point, normal)
496 M[:3, :3] -= numpy.outer(perspective, normal)
497 if pseudo:
498 # preserve relative depth
499 M[:3, :3] -= numpy.outer(normal, normal)
500 M[:3, 3] = numpy.dot(point, normal) * (perspective + normal)
501 else:
502 M[:3, 3] = numpy.dot(point, normal) * perspective
503 M[3, :3] = -normal
504 M[3, 3] = numpy.dot(perspective, normal)
505 elif direction is not None:
506 # parallel projection
507 direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False)
508 scale = numpy.dot(direction, normal)
509 M[:3, :3] -= numpy.outer(direction, normal) / scale
510 M[:3, 3] = direction * (numpy.dot(point, normal) / scale)
511 else:
512 # orthogonal projection
513 M[:3, :3] -= numpy.outer(normal, normal)
514 M[:3, 3] = numpy.dot(point, normal) * normal
515 return M
518def projection_from_matrix(matrix, pseudo=False):
519 """Return projection plane and perspective point from projection matrix.
521 Return values are same as arguments for projection_matrix function:
522 point, normal, direction, perspective, and pseudo.
524 >>> point = numpy.random.random(3) - 0.5
525 >>> normal = numpy.random.random(3) - 0.5
526 >>> direct = numpy.random.random(3) - 0.5
527 >>> persp = numpy.random.random(3) - 0.5
528 >>> P0 = projection_matrix(point, normal)
529 >>> result = projection_from_matrix(P0)
530 >>> P1 = projection_matrix(*result)
531 >>> is_same_transform(P0, P1)
532 True
533 >>> P0 = projection_matrix(point, normal, direct)
534 >>> result = projection_from_matrix(P0)
535 >>> P1 = projection_matrix(*result)
536 >>> is_same_transform(P0, P1)
537 True
538 >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False)
539 >>> result = projection_from_matrix(P0, pseudo=False)
540 >>> P1 = projection_matrix(*result)
541 >>> is_same_transform(P0, P1)
542 True
543 >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True)
544 >>> result = projection_from_matrix(P0, pseudo=True)
545 >>> P1 = projection_matrix(*result)
546 >>> is_same_transform(P0, P1)
547 True
549 """
550 M = numpy.array(matrix, dtype=numpy.float64, copy=False)
551 M33 = M[:3, :3]
552 w, V = numpy.linalg.eig(M)
553 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
554 if not pseudo and len(i):
555 # point: any eigenvector corresponding to eigenvalue 1
556 point = numpy.real(V[:, i[-1]]).squeeze()
557 point /= point[3]
558 # direction: unit eigenvector corresponding to eigenvalue 0
559 w, V = numpy.linalg.eig(M33)
560 i = numpy.where(abs(numpy.real(w)) < 1e-8)[0]
561 if not len(i):
562 raise ValueError("no eigenvector corresponding to eigenvalue 0")
563 direction = numpy.real(V[:, i[0]]).squeeze()
564 direction /= vector_norm(direction)
565 # normal: unit eigenvector of M33.T corresponding to eigenvalue 0
566 w, V = numpy.linalg.eig(M33.T)
567 i = numpy.where(abs(numpy.real(w)) < 1e-8)[0]
568 if len(i):
569 # parallel projection
570 normal = numpy.real(V[:, i[0]]).squeeze()
571 normal /= vector_norm(normal)
572 return point, normal, direction, None, False
573 else:
574 # orthogonal projection, where normal equals direction vector
575 return point, direction, None, None, False
576 else:
577 # perspective projection
578 i = numpy.where(abs(numpy.real(w)) > 1e-8)[0]
579 if not len(i):
580 raise ValueError("no eigenvector not corresponding to eigenvalue 0")
581 point = numpy.real(V[:, i[-1]]).squeeze()
582 point /= point[3]
583 normal = -M[3, :3]
584 perspective = M[:3, 3] / numpy.dot(point[:3], normal)
585 if pseudo:
586 perspective -= normal
587 return point, normal, None, perspective, pseudo
590def clip_matrix(left, right, bottom, top, near, far, perspective=False):
591 """Return matrix to obtain normalized device coordinates from frustum.
593 The frustum bounds are axis-aligned along x (left, right),
594 y (bottom, top) and z (near, far).
596 Normalized device coordinates are in range [-1, 1] if coordinates are
597 inside the frustum.
599 If perspective is True the frustum is a truncated pyramid with the
600 perspective point at origin and direction along z axis, otherwise an
601 orthographic canonical view volume (a box).
603 Homogeneous coordinates transformed by the perspective clip matrix
604 need to be dehomogenized (divided by w coordinate).
606 >>> frustum = numpy.random.rand(6)
607 >>> frustum[1] += frustum[0]
608 >>> frustum[3] += frustum[2]
609 >>> frustum[5] += frustum[4]
610 >>> M = clip_matrix(perspective=False, *frustum)
611 >>> numpy.dot(M, [frustum[0], frustum[2], frustum[4], 1])
612 array([-1., -1., -1., 1.])
613 >>> numpy.dot(M, [frustum[1], frustum[3], frustum[5], 1])
614 array([ 1., 1., 1., 1.])
615 >>> M = clip_matrix(perspective=True, *frustum)
616 >>> v = numpy.dot(M, [frustum[0], frustum[2], frustum[4], 1])
617 >>> v / v[3]
618 array([-1., -1., -1., 1.])
619 >>> v = numpy.dot(M, [frustum[1], frustum[3], frustum[4], 1])
620 >>> v / v[3]
621 array([ 1., 1., -1., 1.])
623 """
624 if left >= right or bottom >= top or near >= far:
625 raise ValueError("invalid frustum")
626 if perspective:
627 if near <= _EPS:
628 raise ValueError("invalid frustum: near <= 0")
629 t = 2.0 * near
630 M = [
631 [t / (left - right), 0.0, (right + left) / (right - left), 0.0],
632 [0.0, t / (bottom - top), (top + bottom) / (top - bottom), 0.0],
633 [0.0, 0.0, (far + near) / (near - far), t * far / (far - near)],
634 [0.0, 0.0, -1.0, 0.0],
635 ]
636 else:
637 M = [
638 [2.0 / (right - left), 0.0, 0.0, (right + left) / (left - right)],
639 [0.0, 2.0 / (top - bottom), 0.0, (top + bottom) / (bottom - top)],
640 [0.0, 0.0, 2.0 / (far - near), (far + near) / (near - far)],
641 [0.0, 0.0, 0.0, 1.0],
642 ]
643 return numpy.array(M)
646def shear_matrix(angle, direction, point, normal):
647 """Return matrix to shear by angle along direction vector on shear plane.
649 The shear plane is defined by a point and normal vector. The direction
650 vector must be orthogonal to the plane's normal vector.
652 A point P is transformed by the shear matrix into P" such that
653 the vector P-P" is parallel to the direction vector and its extent is
654 given by the angle of P-P'-P", where P' is the orthogonal projection
655 of P onto the shear plane.
657 >>> angle = (random.random() - 0.5) * 4*math.pi
658 >>> direct = numpy.random.random(3) - 0.5
659 >>> point = numpy.random.random(3) - 0.5
660 >>> normal = numpy.cross(direct, numpy.random.random(3))
661 >>> S = shear_matrix(angle, direct, point, normal)
662 >>> numpy.allclose(1, numpy.linalg.det(S))
663 True
665 """
666 normal = unit_vector(normal[:3])
667 direction = unit_vector(direction[:3])
668 if abs(numpy.dot(normal, direction)) > 1e-6:
669 raise ValueError(
670 "direction and normal vectors are not orthogonal, diff is %f"
671 % abs(numpy.dot(normal, direction))
672 )
673 angle = math.tan(angle)
674 M = numpy.identity(4)
675 M[:3, :3] += angle * numpy.outer(direction, normal)
676 M[:3, 3] = -angle * numpy.dot(point[:3], normal) * direction
677 return M
680def shear_from_matrix(matrix):
681 """Return shear angle, direction and plane from shear matrix.
683 >>> angle = (random.random() - 0.5) * 4*math.pi
684 >>> direct = numpy.random.random(3) - 0.5
685 >>> point = numpy.random.random(3) - 0.5
686 >>> normal = numpy.cross(direct, numpy.random.random(3))
687 >>> S0 = shear_matrix(angle, direct, point, normal)
688 >>> angle, direct, point, normal = shear_from_matrix(S0)
689 >>> S1 = shear_matrix(angle, direct, point, normal)
690 >>> is_same_transform(S0, S1)
691 True
693 """
694 M = numpy.array(matrix, dtype=numpy.float64, copy=False)
695 M33 = M[:3, :3]
696 # normal: cross independent eigenvectors corresponding to the eigenvalue 1
697 w, V = numpy.linalg.eig(M33)
698 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-4)[0]
699 if len(i) < 2:
700 raise ValueError("no two linear independent eigenvectors found %s" % w)
701 V = numpy.real(V[:, i]).squeeze().T
702 lenorm = -1.0
703 for i0, i1 in ((0, 1), (0, 2), (1, 2)):
704 n = numpy.cross(V[i0], V[i1])
705 w = vector_norm(n)
706 if w > lenorm:
707 lenorm = w
708 normal = n
709 normal /= lenorm
710 # direction and angle
711 direction = numpy.dot(M33 - numpy.identity(3), normal)
712 angle = vector_norm(direction)
713 direction /= angle
714 angle = math.atan(angle)
715 # point: eigenvector corresponding to eigenvalue 1
716 w, V = numpy.linalg.eig(M)
717 i = numpy.where(abs(numpy.real(w) - 1.0) < 1e-8)[0]
718 if not len(i):
719 raise ValueError("no eigenvector corresponding to eigenvalue 1")
720 point = numpy.real(V[:, i[-1]]).squeeze()
721 point /= point[3]
722 return angle, direction, point, normal
725def decompose_matrix(matrix):
726 """Return sequence of transformations from transformation matrix.
728 matrix : array_like
729 Non-degenerative homogeneous transformation matrix
731 Return tuple of:
732 scale : vector of 3 scaling factors
733 shear : list of shear factors for x-y, x-z, y-z axes
734 angles : list of Euler angles about static x, y, z axes
735 translate : translation vector along x, y, z axes
736 perspective : perspective partition of matrix
738 Raise ValueError if matrix is of wrong type or degenerative.
740 >>> T0 = translation_matrix([1, 2, 3])
741 >>> scale, shear, angles, trans, persp = decompose_matrix(T0)
742 >>> T1 = translation_matrix(trans)
743 >>> numpy.allclose(T0, T1)
744 True
745 >>> S = scale_matrix(0.123)
746 >>> scale, shear, angles, trans, persp = decompose_matrix(S)
747 >>> scale[0]
748 0.123
749 >>> R0 = euler_matrix(1, 2, 3)
750 >>> scale, shear, angles, trans, persp = decompose_matrix(R0)
751 >>> R1 = euler_matrix(*angles)
752 >>> numpy.allclose(R0, R1)
753 True
755 """
756 M = numpy.array(matrix, dtype=numpy.float64, copy=True).T
757 if abs(M[3, 3]) < _EPS:
758 raise ValueError("M[3, 3] is zero")
759 M /= M[3, 3]
760 P = M.copy()
761 P[:, 3] = 0.0, 0.0, 0.0, 1.0
762 if not numpy.linalg.det(P):
763 raise ValueError("matrix is singular")
765 scale = numpy.zeros((3,))
766 shear = [0.0, 0.0, 0.0]
767 angles = [0.0, 0.0, 0.0]
769 if any(abs(M[:3, 3]) > _EPS):
770 perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T))
771 M[:, 3] = 0.0, 0.0, 0.0, 1.0
772 else:
773 perspective = numpy.array([0.0, 0.0, 0.0, 1.0])
775 translate = M[3, :3].copy()
776 M[3, :3] = 0.0
778 row = M[:3, :3].copy()
779 scale[0] = vector_norm(row[0])
780 row[0] /= scale[0]
781 shear[0] = numpy.dot(row[0], row[1])
782 row[1] -= row[0] * shear[0]
783 scale[1] = vector_norm(row[1])
784 row[1] /= scale[1]
785 shear[0] /= scale[1]
786 shear[1] = numpy.dot(row[0], row[2])
787 row[2] -= row[0] * shear[1]
788 shear[2] = numpy.dot(row[1], row[2])
789 row[2] -= row[1] * shear[2]
790 scale[2] = vector_norm(row[2])
791 row[2] /= scale[2]
792 shear[1:] /= scale[2]
794 if numpy.dot(row[0], numpy.cross(row[1], row[2])) < 0:
795 numpy.negative(scale, scale)
796 numpy.negative(row, row)
798 angles[1] = math.asin(-row[0, 2])
799 if math.cos(angles[1]):
800 angles[0] = math.atan2(row[1, 2], row[2, 2])
801 angles[2] = math.atan2(row[0, 1], row[0, 0])
802 else:
803 # angles[0] = math.atan2(row[1, 0], row[1, 1])
804 angles[0] = math.atan2(-row[2, 1], row[1, 1])
805 angles[2] = 0.0
807 return scale, shear, angles, translate, perspective
810def compose_matrix(
811 scale=None, shear=None, angles=None, translate=None, perspective=None
812):
813 """Return transformation matrix from sequence of transformations.
815 This is the inverse of the decompose_matrix function.
817 Sequence of transformations:
818 scale : vector of 3 scaling factors
819 shear : list of shear factors for x-y, x-z, y-z axes
820 angles : list of Euler angles about static x, y, z axes
821 translate : translation vector along x, y, z axes
822 perspective : perspective partition of matrix
824 >>> scale = numpy.random.random(3) - 0.5
825 >>> shear = numpy.random.random(3) - 0.5
826 >>> angles = (numpy.random.random(3) - 0.5) * (2*math.pi)
827 >>> trans = numpy.random.random(3) - 0.5
828 >>> persp = numpy.random.random(4) - 0.5
829 >>> M0 = compose_matrix(scale, shear, angles, trans, persp)
830 >>> result = decompose_matrix(M0)
831 >>> M1 = compose_matrix(*result)
832 >>> is_same_transform(M0, M1)
833 True
835 """
836 M = numpy.identity(4)
837 if perspective is not None:
838 P = numpy.identity(4)
839 P[3, :] = perspective[:4]
840 M = numpy.dot(M, P)
841 if translate is not None:
842 T = numpy.identity(4)
843 T[:3, 3] = translate[:3]
844 M = numpy.dot(M, T)
845 if angles is not None:
846 R = euler_matrix(angles[0], angles[1], angles[2], "sxyz")
847 M = numpy.dot(M, R)
848 if shear is not None:
849 Z = numpy.identity(4)
850 Z[1, 2] = shear[2]
851 Z[0, 2] = shear[1]
852 Z[0, 1] = shear[0]
853 M = numpy.dot(M, Z)
854 if scale is not None:
855 S = numpy.identity(4)
856 S[0, 0] = scale[0]
857 S[1, 1] = scale[1]
858 S[2, 2] = scale[2]
859 M = numpy.dot(M, S)
860 M /= M[3, 3]
861 return M
864def orthogonalization_matrix(lengths, angles):
865 """Return orthogonalization matrix for crystallographic cell coordinates.
867 Angles are expected in degrees.
869 The de-orthogonalization matrix is the inverse.
871 >>> O = orthogonalization_matrix([10, 10, 10], [90, 90, 90])
872 >>> numpy.allclose(O[:3, :3], numpy.identity(3, float) * 10)
873 True
874 >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7])
875 >>> numpy.allclose(numpy.sum(O), 43.063229)
876 True
878 """
879 a, b, c = lengths
880 angles = numpy.radians(angles)
881 sina, sinb, _ = numpy.sin(angles)
882 cosa, cosb, cosg = numpy.cos(angles)
883 co = (cosa * cosb - cosg) / (sina * sinb)
884 return numpy.array(
885 [
886 [a * sinb * math.sqrt(1.0 - co * co), 0.0, 0.0, 0.0],
887 [-a * sinb * co, b * sina, 0.0, 0.0],
888 [a * cosb, b * cosa, c, 0.0],
889 [0.0, 0.0, 0.0, 1.0],
890 ]
891 )
894def affine_matrix_from_points(v0, v1, shear=True, scale=True, usesvd=True):
895 """Return affine transform matrix to register two point sets.
897 v0 and v1 are shape (ndims, \*) arrays of at least ndims non-homogeneous
898 coordinates, where ndims is the dimensionality of the coordinate space.
900 If shear is False, a similarity transformation matrix is returned.
901 If also scale is False, a rigid/Euclidean transformation matrix
902 is returned.
904 By default the algorithm by Hartley and Zissermann [15] is used.
905 If usesvd is True, similarity and Euclidean transformation matrices
906 are calculated by minimizing the weighted sum of squared deviations
907 (RMSD) according to the algorithm by Kabsch [8].
908 Otherwise, and if ndims is 3, the quaternion based algorithm by Horn [9]
909 is used, which is slower when using this Python implementation.
911 The returned matrix performs rotation, translation and uniform scaling
912 (if specified).
914 >>> v0 = [[0, 1031, 1031, 0], [0, 0, 1600, 1600]]
915 >>> v1 = [[675, 826, 826, 677], [55, 52, 281, 277]]
916 >>> affine_matrix_from_points(v0, v1)
917 array([[ 0.14549, 0.00062, 675.50008],
918 [ 0.00048, 0.14094, 53.24971],
919 [ 0. , 0. , 1. ]])
920 >>> T = translation_matrix(numpy.random.random(3)-0.5)
921 >>> R = random_rotation_matrix(numpy.random.random(3))
922 >>> S = scale_matrix(random.random())
923 >>> M = concatenate_matrices(T, R, S)
924 >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20
925 >>> v0[3] = 1
926 >>> v1 = numpy.dot(M, v0)
927 >>> v0[:3] += numpy.random.normal(0, 1e-8, 300).reshape(3, -1)
928 >>> M = affine_matrix_from_points(v0[:3], v1[:3])
929 >>> numpy.allclose(v1, numpy.dot(M, v0))
930 True
932 More examples in superimposition_matrix()
934 """
935 v0 = numpy.array(v0, dtype=numpy.float64, copy=True)
936 v1 = numpy.array(v1, dtype=numpy.float64, copy=True)
938 ndims = v0.shape[0]
939 if ndims < 2 or v0.shape[1] < ndims or v0.shape != v1.shape:
940 raise ValueError("input arrays are of wrong shape or type")
942 # move centroids to origin
943 t0 = -numpy.mean(v0, axis=1)
944 M0 = numpy.identity(ndims + 1)
945 M0[:ndims, ndims] = t0
946 v0 += t0.reshape(ndims, 1)
947 t1 = -numpy.mean(v1, axis=1)
948 M1 = numpy.identity(ndims + 1)
949 M1[:ndims, ndims] = t1
950 v1 += t1.reshape(ndims, 1)
952 if shear:
953 # Affine transformation
954 A = numpy.concatenate((v0, v1), axis=0)
955 u, s, vh = numpy.linalg.svd(A.T)
956 vh = vh[:ndims].T
957 B = vh[:ndims]
958 C = vh[ndims : 2 * ndims]
959 t = numpy.dot(C, numpy.linalg.pinv(B))
960 t = numpy.concatenate((t, numpy.zeros((ndims, 1))), axis=1)
961 M = numpy.vstack((t, ((0.0,) * ndims) + (1.0,)))
962 elif usesvd or ndims != 3:
963 # Rigid transformation via SVD of covariance matrix
964 u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T))
965 # rotation matrix from SVD orthonormal bases
966 R = numpy.dot(u, vh)
967 if numpy.linalg.det(R) < 0.0:
968 # R does not constitute right handed system
969 R -= numpy.outer(u[:, ndims - 1], vh[ndims - 1, :] * 2.0)
970 s[-1] *= -1.0
971 # homogeneous transformation matrix
972 M = numpy.identity(ndims + 1)
973 M[:ndims, :ndims] = R
974 else:
975 # Rigid transformation matrix via quaternion
976 # compute symmetric matrix N
977 xx, yy, zz = numpy.sum(v0 * v1, axis=1)
978 xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1)
979 xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1)
980 N = [
981 [xx + yy + zz, 0.0, 0.0, 0.0],
982 [yz - zy, xx - yy - zz, 0.0, 0.0],
983 [zx - xz, xy + yx, yy - xx - zz, 0.0],
984 [xy - yx, zx + xz, yz + zy, zz - xx - yy],
985 ]
986 # quaternion: eigenvector corresponding to most positive eigenvalue
987 w, V = numpy.linalg.eigh(N)
988 q = V[:, numpy.argmax(w)]
989 q /= vector_norm(q) # unit quaternion
990 # homogeneous transformation matrix
991 M = quaternion_matrix(q)
993 if scale and not shear:
994 # Affine transformation; scale is ratio of RMS deviations from centroid
995 v0 *= v0
996 v1 *= v1
997 M[:ndims, :ndims] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0))
999 # move centroids back
1000 M = numpy.dot(numpy.linalg.inv(M1), numpy.dot(M, M0))
1001 M /= M[ndims, ndims]
1002 return M
1005def superimposition_matrix(v0, v1, scale=False, usesvd=True):
1006 """Return matrix to transform given 3D point set into second point set.
1008 v0 and v1 are shape (3, \*) or (4, \*) arrays of at least 3 points.
1010 The parameters scale and usesvd are explained in the more general
1011 affine_matrix_from_points function.
1013 The returned matrix is a similarity or Euclidean transformation matrix.
1014 This function has a fast C implementation in transformations.c.
1016 >>> v0 = numpy.random.rand(3, 10)
1017 >>> M = superimposition_matrix(v0, v0)
1018 >>> numpy.allclose(M, numpy.identity(4))
1019 True
1020 >>> R = random_rotation_matrix(numpy.random.random(3))
1021 >>> v0 = [[1,0,0], [0,1,0], [0,0,1], [1,1,1]]
1022 >>> v1 = numpy.dot(R, v0)
1023 >>> M = superimposition_matrix(v0, v1)
1024 >>> numpy.allclose(v1, numpy.dot(M, v0))
1025 True
1026 >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20
1027 >>> v0[3] = 1
1028 >>> v1 = numpy.dot(R, v0)
1029 >>> M = superimposition_matrix(v0, v1)
1030 >>> numpy.allclose(v1, numpy.dot(M, v0))
1031 True
1032 >>> S = scale_matrix(random.random())
1033 >>> T = translation_matrix(numpy.random.random(3)-0.5)
1034 >>> M = concatenate_matrices(T, R, S)
1035 >>> v1 = numpy.dot(M, v0)
1036 >>> v0[:3] += numpy.random.normal(0, 1e-9, 300).reshape(3, -1)
1037 >>> M = superimposition_matrix(v0, v1, scale=True)
1038 >>> numpy.allclose(v1, numpy.dot(M, v0))
1039 True
1040 >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False)
1041 >>> numpy.allclose(v1, numpy.dot(M, v0))
1042 True
1043 >>> v = numpy.empty((4, 100, 3))
1044 >>> v[:, :, 0] = v0
1045 >>> M = superimposition_matrix(v0, v1, scale=True, usesvd=False)
1046 >>> numpy.allclose(v1, numpy.dot(M, v[:, :, 0]))
1047 True
1049 """
1050 v0 = numpy.array(v0, dtype=numpy.float64, copy=False)[:3]
1051 v1 = numpy.array(v1, dtype=numpy.float64, copy=False)[:3]
1052 return affine_matrix_from_points(v0, v1, shear=False, scale=scale, usesvd=usesvd)
1055def euler_matrix(ai, aj, ak, axes="sxyz"):
1056 """Return homogeneous rotation matrix from Euler angles and axis sequence.
1058 ai, aj, ak : Euler's roll, pitch and yaw angles
1059 axes : One of 24 axis sequences as string or encoded tuple
1061 >>> R = euler_matrix(1, 2, 3, 'syxz')
1062 >>> numpy.allclose(numpy.sum(R[0]), -1.34786452)
1063 True
1064 >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1))
1065 >>> numpy.allclose(numpy.sum(R[0]), -0.383436184)
1066 True
1067 >>> ai, aj, ak = (4*math.pi) * (numpy.random.random(3) - 0.5)
1068 >>> for axes in _AXES2TUPLE.keys():
1069 ... R = euler_matrix(ai, aj, ak, axes)
1070 >>> for axes in _TUPLE2AXES.keys():
1071 ... R = euler_matrix(ai, aj, ak, axes)
1073 """
1074 try:
1075 firstaxis, parity, repetition, frame = _AXES2TUPLE[axes]
1076 except (AttributeError, KeyError):
1077 _TUPLE2AXES[axes] # validation
1078 firstaxis, parity, repetition, frame = axes
1080 i = firstaxis
1081 j = _NEXT_AXIS[i + parity]
1082 k = _NEXT_AXIS[i - parity + 1]
1084 if frame:
1085 ai, ak = ak, ai
1086 if parity:
1087 ai, aj, ak = -ai, -aj, -ak
1089 si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak)
1090 ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak)
1091 cc, cs = ci * ck, ci * sk
1092 sc, ss = si * ck, si * sk
1094 M = numpy.identity(4)
1095 if repetition:
1096 M[i, i] = cj
1097 M[i, j] = sj * si
1098 M[i, k] = sj * ci
1099 M[j, i] = sj * sk
1100 M[j, j] = -cj * ss + cc
1101 M[j, k] = -cj * cs - sc
1102 M[k, i] = -sj * ck
1103 M[k, j] = cj * sc + cs
1104 M[k, k] = cj * cc - ss
1105 else:
1106 M[i, i] = cj * ck
1107 M[i, j] = sj * sc - cs
1108 M[i, k] = sj * cc + ss
1109 M[j, i] = cj * sk
1110 M[j, j] = sj * ss + cc
1111 M[j, k] = sj * cs - sc
1112 M[k, i] = -sj
1113 M[k, j] = cj * si
1114 M[k, k] = cj * ci
1115 return M
1118def euler_from_matrix(matrix, axes="sxyz"):
1119 """Return Euler angles from rotation matrix for specified axis sequence.
1121 axes : One of 24 axis sequences as string or encoded tuple
1123 Note that many Euler angle triplets can describe one matrix.
1125 >>> R0 = euler_matrix(1, 2, 3, 'syxz')
1126 >>> al, be, ga = euler_from_matrix(R0, 'syxz')
1127 >>> R1 = euler_matrix(al, be, ga, 'syxz')
1128 >>> numpy.allclose(R0, R1)
1129 True
1130 >>> angles = (4*math.pi) * (numpy.random.random(3) - 0.5)
1131 >>> for axes in _AXES2TUPLE.keys():
1132 ... R0 = euler_matrix(axes=axes, *angles)
1133 ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes))
1134 ... if not numpy.allclose(R0, R1): print(axes, "failed")
1136 """
1137 try:
1138 firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1139 except (AttributeError, KeyError):
1140 _TUPLE2AXES[axes] # validation
1141 firstaxis, parity, repetition, frame = axes
1143 i = firstaxis
1144 j = _NEXT_AXIS[i + parity]
1145 k = _NEXT_AXIS[i - parity + 1]
1147 M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3]
1148 if repetition:
1149 sy = math.sqrt(M[i, j] * M[i, j] + M[i, k] * M[i, k])
1150 if sy > _EPS:
1151 ax = math.atan2(M[i, j], M[i, k])
1152 ay = math.atan2(sy, M[i, i])
1153 az = math.atan2(M[j, i], -M[k, i])
1154 else:
1155 ax = math.atan2(-M[j, k], M[j, j])
1156 ay = math.atan2(sy, M[i, i])
1157 az = 0.0
1158 else:
1159 cy = math.sqrt(M[i, i] * M[i, i] + M[j, i] * M[j, i])
1160 if cy > _EPS:
1161 ax = math.atan2(M[k, j], M[k, k])
1162 ay = math.atan2(-M[k, i], cy)
1163 az = math.atan2(M[j, i], M[i, i])
1164 else:
1165 ax = math.atan2(-M[j, k], M[j, j])
1166 ay = math.atan2(-M[k, i], cy)
1167 az = 0.0
1169 if parity:
1170 ax, ay, az = -ax, -ay, -az
1171 if frame:
1172 ax, az = az, ax
1173 return ax, ay, az
1176def euler_from_quaternion(quaternion, axes="sxyz"):
1177 """Return Euler angles from quaternion for specified axis sequence.
1179 >>> angles = euler_from_quaternion([0.99810947, 0.06146124, 0, 0])
1180 >>> numpy.allclose(angles, [0.123, 0, 0])
1181 True
1183 """
1184 return euler_from_matrix(quaternion_matrix(quaternion), axes)
1187def quaternion_from_euler(ai, aj, ak, axes="sxyz"):
1188 """Return quaternion from Euler angles and axis sequence.
1190 ai, aj, ak : Euler's roll, pitch and yaw angles
1191 axes : One of 24 axis sequences as string or encoded tuple
1193 >>> q = quaternion_from_euler(1, 2, 3, 'ryxz')
1194 >>> numpy.allclose(q, [0.435953, 0.310622, -0.718287, 0.444435])
1195 True
1197 """
1198 try:
1199 firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1200 except (AttributeError, KeyError):
1201 _TUPLE2AXES[axes] # validation
1202 firstaxis, parity, repetition, frame = axes
1204 i = firstaxis + 1
1205 j = _NEXT_AXIS[i + parity - 1] + 1
1206 k = _NEXT_AXIS[i - parity] + 1
1208 if frame:
1209 ai, ak = ak, ai
1210 if parity:
1211 aj = -aj
1213 ai /= 2.0
1214 aj /= 2.0
1215 ak /= 2.0
1216 ci = math.cos(ai)
1217 si = math.sin(ai)
1218 cj = math.cos(aj)
1219 sj = math.sin(aj)
1220 ck = math.cos(ak)
1221 sk = math.sin(ak)
1222 cc = ci * ck
1223 cs = ci * sk
1224 sc = si * ck
1225 ss = si * sk
1227 q = numpy.empty((4,))
1228 if repetition:
1229 q[0] = cj * (cc - ss)
1230 q[i] = cj * (cs + sc)
1231 q[j] = sj * (cc + ss)
1232 q[k] = sj * (cs - sc)
1233 else:
1234 q[0] = cj * cc + sj * ss
1235 q[i] = cj * sc - sj * cs
1236 q[j] = cj * ss + sj * cc
1237 q[k] = cj * cs - sj * sc
1238 if parity:
1239 q[j] *= -1.0
1241 return q
1244def quaternion_about_axis(angle, axis):
1245 """Return quaternion for rotation about axis.
1247 >>> q = quaternion_about_axis(0.123, [1, 0, 0])
1248 >>> numpy.allclose(q, [0.99810947, 0.06146124, 0, 0])
1249 True
1251 """
1252 q = numpy.array([0.0, axis[0], axis[1], axis[2]])
1253 qlen = vector_norm(q)
1254 if qlen > _EPS:
1255 q *= math.sin(angle / 2.0) / qlen
1256 q[0] = math.cos(angle / 2.0)
1257 return q
1260def quaternion_matrix(quaternion):
1261 """Return homogeneous rotation matrix from quaternion.
1263 >>> M = quaternion_matrix([0.99810947, 0.06146124, 0, 0])
1264 >>> numpy.allclose(M, rotation_matrix(0.123, [1, 0, 0]))
1265 True
1266 >>> M = quaternion_matrix([1, 0, 0, 0])
1267 >>> numpy.allclose(M, numpy.identity(4))
1268 True
1269 >>> M = quaternion_matrix([0, 1, 0, 0])
1270 >>> numpy.allclose(M, numpy.diag([1, -1, -1, 1]))
1271 True
1273 """
1274 q = numpy.array(quaternion, dtype=numpy.float64, copy=True)
1275 n = numpy.dot(q, q)
1276 if n < _EPS:
1277 return numpy.identity(4)
1278 q *= math.sqrt(2.0 / n)
1279 q = numpy.outer(q, q)
1280 return numpy.array(
1281 [
1282 [1.0 - q[2, 2] - q[3, 3], q[1, 2] - q[3, 0], q[1, 3] + q[2, 0], 0.0],
1283 [q[1, 2] + q[3, 0], 1.0 - q[1, 1] - q[3, 3], q[2, 3] - q[1, 0], 0.0],
1284 [q[1, 3] - q[2, 0], q[2, 3] + q[1, 0], 1.0 - q[1, 1] - q[2, 2], 0.0],
1285 [0.0, 0.0, 0.0, 1.0],
1286 ]
1287 )
1290def quaternion_from_matrix(matrix, isprecise=False):
1291 """Return quaternion from rotation matrix.
1293 If isprecise is True, the input matrix is assumed to be a precise rotation
1294 matrix and a faster algorithm is used.
1296 >>> q = quaternion_from_matrix(numpy.identity(4), True)
1297 >>> numpy.allclose(q, [1, 0, 0, 0])
1298 True
1299 >>> q = quaternion_from_matrix(numpy.diag([1, -1, -1, 1]))
1300 >>> numpy.allclose(q, [0, 1, 0, 0]) or numpy.allclose(q, [0, -1, 0, 0])
1301 True
1302 >>> R = rotation_matrix(0.123, (1, 2, 3))
1303 >>> q = quaternion_from_matrix(R, True)
1304 >>> numpy.allclose(q, [0.9981095, 0.0164262, 0.0328524, 0.0492786])
1305 True
1306 >>> R = [[-0.545, 0.797, 0.260, 0], [0.733, 0.603, -0.313, 0],
1307 ... [-0.407, 0.021, -0.913, 0], [0, 0, 0, 1]]
1308 >>> q = quaternion_from_matrix(R)
1309 >>> numpy.allclose(q, [0.19069, 0.43736, 0.87485, -0.083611])
1310 True
1311 >>> R = [[0.395, 0.362, 0.843, 0], [-0.626, 0.796, -0.056, 0],
1312 ... [-0.677, -0.498, 0.529, 0], [0, 0, 0, 1]]
1313 >>> q = quaternion_from_matrix(R)
1314 >>> numpy.allclose(q, [0.82336615, -0.13610694, 0.46344705, -0.29792603])
1315 True
1316 >>> R = random_rotation_matrix()
1317 >>> q = quaternion_from_matrix(R)
1318 >>> is_same_transform(R, quaternion_matrix(q))
1319 True
1321 """
1322 M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:4, :4]
1323 if isprecise:
1324 q = numpy.empty((4,))
1325 t = numpy.trace(M)
1326 if t > M[3, 3]:
1327 q[0] = t
1328 q[3] = M[1, 0] - M[0, 1]
1329 q[2] = M[0, 2] - M[2, 0]
1330 q[1] = M[2, 1] - M[1, 2]
1331 else:
1332 i, j, k = 1, 2, 3
1333 if M[1, 1] > M[0, 0]:
1334 i, j, k = 2, 3, 1
1335 if M[2, 2] > M[i, i]:
1336 i, j, k = 3, 1, 2
1337 t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3]
1338 q[i] = t
1339 q[j] = M[i, j] + M[j, i]
1340 q[k] = M[k, i] + M[i, k]
1341 q[3] = M[k, j] - M[j, k]
1342 q *= 0.5 / math.sqrt(t * M[3, 3])
1343 else:
1344 m00 = M[0, 0]
1345 m01 = M[0, 1]
1346 m02 = M[0, 2]
1347 m10 = M[1, 0]
1348 m11 = M[1, 1]
1349 m12 = M[1, 2]
1350 m20 = M[2, 0]
1351 m21 = M[2, 1]
1352 m22 = M[2, 2]
1353 # symmetric matrix K
1354 K = numpy.array(
1355 [
1356 [m00 - m11 - m22, 0.0, 0.0, 0.0],
1357 [m01 + m10, m11 - m00 - m22, 0.0, 0.0],
1358 [m02 + m20, m12 + m21, m22 - m00 - m11, 0.0],
1359 [m21 - m12, m02 - m20, m10 - m01, m00 + m11 + m22],
1360 ]
1361 )
1362 K /= 3.0
1363 # quaternion is eigenvector of K that corresponds to largest eigenvalue
1364 w, V = numpy.linalg.eigh(K)
1365 q = V[[3, 0, 1, 2], numpy.argmax(w)]
1366 if q[0] < 0.0:
1367 numpy.negative(q, q)
1368 return q
1371def quaternion_multiply(quaternion1, quaternion0):
1372 """Return multiplication of two quaternions.
1374 >>> q = quaternion_multiply([4, 1, -2, 3], [8, -5, 6, 7])
1375 >>> numpy.allclose(q, [28, -44, -14, 48])
1376 True
1378 """
1379 w0, x0, y0, z0 = quaternion0
1380 w1, x1, y1, z1 = quaternion1
1381 return numpy.array(
1382 [
1383 -x1 * x0 - y1 * y0 - z1 * z0 + w1 * w0,
1384 x1 * w0 + y1 * z0 - z1 * y0 + w1 * x0,
1385 -x1 * z0 + y1 * w0 + z1 * x0 + w1 * y0,
1386 x1 * y0 - y1 * x0 + z1 * w0 + w1 * z0,
1387 ],
1388 dtype=numpy.float64,
1389 )
1392def quaternion_conjugate(quaternion):
1393 """Return conjugate of quaternion.
1395 >>> q0 = random_quaternion()
1396 >>> q1 = quaternion_conjugate(q0)
1397 >>> q1[0] == q0[0] and all(q1[1:] == -q0[1:])
1398 True
1400 """
1401 q = numpy.array(quaternion, dtype=numpy.float64, copy=True)
1402 numpy.negative(q[1:], q[1:])
1403 return q
1406def quaternion_inverse(quaternion):
1407 """Return inverse of quaternion.
1409 >>> q0 = random_quaternion()
1410 >>> q1 = quaternion_inverse(q0)
1411 >>> numpy.allclose(quaternion_multiply(q0, q1), [1, 0, 0, 0])
1412 True
1414 """
1415 q = numpy.array(quaternion, dtype=numpy.float64, copy=True)
1416 numpy.negative(q[1:], q[1:])
1417 return q / numpy.dot(q, q)
1420def quaternion_real(quaternion):
1421 """Return real part of quaternion.
1423 >>> quaternion_real([3, 0, 1, 2])
1424 3.0
1426 """
1427 return float(quaternion[0])
1430def quaternion_imag(quaternion):
1431 """Return imaginary part of quaternion.
1433 >>> quaternion_imag([3, 0, 1, 2])
1434 array([ 0., 1., 2.])
1436 """
1437 return numpy.array(quaternion[1:4], dtype=numpy.float64, copy=True)
1440def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True):
1441 """Return spherical linear interpolation between two quaternions.
1443 >>> q0 = random_quaternion()
1444 >>> q1 = random_quaternion()
1445 >>> q = quaternion_slerp(q0, q1, 0)
1446 >>> numpy.allclose(q, q0)
1447 True
1448 >>> q = quaternion_slerp(q0, q1, 1, 1)
1449 >>> numpy.allclose(q, q1)
1450 True
1451 >>> q = quaternion_slerp(q0, q1, 0.5)
1452 >>> angle = math.acos(numpy.dot(q0, q))
1453 >>> numpy.allclose(2, math.acos(numpy.dot(q0, q1)) / angle) or \
1454 numpy.allclose(2, math.acos(-numpy.dot(q0, q1)) / angle)
1455 True
1457 """
1458 q0 = unit_vector(quat0[:4])
1459 q1 = unit_vector(quat1[:4])
1460 if fraction == 0.0:
1461 return q0
1462 elif fraction == 1.0:
1463 return q1
1464 d = numpy.dot(q0, q1)
1465 if abs(abs(d) - 1.0) < _EPS:
1466 return q0
1467 if shortestpath and d < 0.0:
1468 # invert rotation
1469 d = -d
1470 numpy.negative(q1, q1)
1471 angle = math.acos(d) + spin * math.pi
1472 if abs(angle) < _EPS:
1473 return q0
1474 isin = 1.0 / math.sin(angle)
1475 q0 *= math.sin((1.0 - fraction) * angle) * isin
1476 q1 *= math.sin(fraction * angle) * isin
1477 q0 += q1
1478 return q0
1481def random_quaternion(rand=None):
1482 """Return uniform random unit quaternion.
1484 rand: array like or None
1485 Three independent random variables that are uniformly distributed
1486 between 0 and 1.
1488 >>> q = random_quaternion()
1489 >>> numpy.allclose(1, vector_norm(q))
1490 True
1491 >>> q = random_quaternion(numpy.random.random(3))
1492 >>> len(q.shape), q.shape[0]==4
1493 (1, True)
1495 """
1496 if rand is None:
1497 rand = numpy.random.rand(3)
1498 else:
1499 assert len(rand) == 3
1500 r1 = numpy.sqrt(1.0 - rand[0])
1501 r2 = numpy.sqrt(rand[0])
1502 pi2 = math.pi * 2.0
1503 t1 = pi2 * rand[1]
1504 t2 = pi2 * rand[2]
1505 return numpy.array(
1506 [numpy.cos(t2) * r2, numpy.sin(t1) * r1, numpy.cos(t1) * r1, numpy.sin(t2) * r2]
1507 )
1510def random_rotation_matrix(rand=None):
1511 """Return uniform random rotation matrix.
1513 rand: array like
1514 Three independent random variables that are uniformly distributed
1515 between 0 and 1 for each returned quaternion.
1517 >>> R = random_rotation_matrix()
1518 >>> numpy.allclose(numpy.dot(R.T, R), numpy.identity(4))
1519 True
1521 """
1522 return quaternion_matrix(random_quaternion(rand))
1525class Arcball(object):
1526 """Virtual Trackball Control.
1528 >>> ball = Arcball()
1529 >>> ball = Arcball(initial=numpy.identity(4))
1530 >>> ball.place([320, 320], 320)
1531 >>> ball.down([500, 250])
1532 >>> ball.drag([475, 275])
1533 >>> R = ball.matrix()
1534 >>> numpy.allclose(numpy.sum(R), 3.90583455)
1535 True
1536 >>> ball = Arcball(initial=[1, 0, 0, 0])
1537 >>> ball.place([320, 320], 320)
1538 >>> ball.setaxes([1, 1, 0], [-1, 1, 0])
1539 >>> ball.constrain = True
1540 >>> ball.down([400, 200])
1541 >>> ball.drag([200, 400])
1542 >>> R = ball.matrix()
1543 >>> numpy.allclose(numpy.sum(R), 0.2055924)
1544 True
1545 >>> ball.next()
1547 """
1549 def __init__(self, initial=None):
1550 """Initialize virtual trackball control.
1552 initial : quaternion or rotation matrix
1554 """
1555 self._axis = None
1556 self._axes = None
1557 self._radius = 1.0
1558 self._center = [0.0, 0.0]
1559 self._vdown = numpy.array([0.0, 0.0, 1.0])
1560 self._constrain = False
1561 if initial is None:
1562 self._qdown = numpy.array([1.0, 0.0, 0.0, 0.0])
1563 else:
1564 initial = numpy.array(initial, dtype=numpy.float64)
1565 if initial.shape == (4, 4):
1566 self._qdown = quaternion_from_matrix(initial)
1567 elif initial.shape == (4,):
1568 initial /= vector_norm(initial)
1569 self._qdown = initial
1570 else:
1571 raise ValueError("initial not a quaternion or matrix")
1572 self._qnow = self._qpre = self._qdown
1574 def place(self, center, radius):
1575 """Place Arcball, e.g. when window size changes.
1577 center : sequence[2]
1578 Window coordinates of trackball center.
1579 radius : float
1580 Radius of trackball in window coordinates.
1582 """
1583 self._radius = float(radius)
1584 self._center[0] = center[0]
1585 self._center[1] = center[1]
1587 def setaxes(self, *axes):
1588 """Set axes to constrain rotations."""
1589 if axes is None:
1590 self._axes = None
1591 else:
1592 self._axes = [unit_vector(axis) for axis in axes]
1594 @property
1595 def constrain(self):
1596 """Return state of constrain to axis mode."""
1597 return self._constrain
1599 @constrain.setter
1600 def constrain(self, value):
1601 """Set state of constrain to axis mode."""
1602 self._constrain = bool(value)
1604 def down(self, point):
1605 """Set initial cursor window coordinates and pick constrain-axis."""
1606 self._vdown = arcball_map_to_sphere(point, self._center, self._radius)
1607 self._qdown = self._qpre = self._qnow
1608 if self._constrain and self._axes is not None:
1609 self._axis = arcball_nearest_axis(self._vdown, self._axes)
1610 self._vdown = arcball_constrain_to_axis(self._vdown, self._axis)
1611 else:
1612 self._axis = None
1614 def drag(self, point):
1615 """Update current cursor window coordinates."""
1616 vnow = arcball_map_to_sphere(point, self._center, self._radius)
1617 if self._axis is not None:
1618 vnow = arcball_constrain_to_axis(vnow, self._axis)
1619 self._qpre = self._qnow
1620 t = numpy.cross(self._vdown, vnow)
1621 if numpy.dot(t, t) < _EPS:
1622 self._qnow = self._qdown
1623 else:
1624 q = [numpy.dot(self._vdown, vnow), t[0], t[1], t[2]]
1625 self._qnow = quaternion_multiply(q, self._qdown)
1627 def next(self, acceleration=0.0):
1628 """Continue rotation in direction of last drag."""
1629 q = quaternion_slerp(self._qpre, self._qnow, 2.0 + acceleration, False)
1630 self._qpre, self._qnow = self._qnow, q
1632 def matrix(self):
1633 """Return homogeneous rotation matrix."""
1634 return quaternion_matrix(self._qnow)
1637def arcball_map_to_sphere(point, center, radius):
1638 """Return unit sphere coordinates from window coordinates."""
1639 v0 = (point[0] - center[0]) / radius
1640 v1 = (center[1] - point[1]) / radius
1641 n = v0 * v0 + v1 * v1
1642 if n > 1.0:
1643 # position outside of sphere
1644 n = math.sqrt(n)
1645 return numpy.array([v0 / n, v1 / n, 0.0])
1646 else:
1647 return numpy.array([v0, v1, math.sqrt(1.0 - n)])
1650def arcball_constrain_to_axis(point, axis):
1651 """Return sphere point perpendicular to axis."""
1652 v = numpy.array(point, dtype=numpy.float64, copy=True)
1653 a = numpy.array(axis, dtype=numpy.float64, copy=True)
1654 v -= a * numpy.dot(a, v) # on plane
1655 n = vector_norm(v)
1656 if n > _EPS:
1657 if v[2] < 0.0:
1658 numpy.negative(v, v)
1659 v /= n
1660 return v
1661 if a[2] == 1.0:
1662 return numpy.array([1.0, 0.0, 0.0])
1663 return unit_vector([-a[1], a[0], 0.0])
1666def arcball_nearest_axis(point, axes):
1667 """Return axis, which arc is nearest to point."""
1668 point = numpy.array(point, dtype=numpy.float64, copy=False)
1669 nearest = None
1670 mx = -1.0
1671 for axis in axes:
1672 t = numpy.dot(arcball_constrain_to_axis(point, axis), point)
1673 if t > mx:
1674 nearest = axis
1675 mx = t
1676 return nearest
1679# epsilon for testing whether a number is close to zero
1680_EPS = numpy.finfo(float).eps * 4.0
1682# axis sequences for Euler angles
1683_NEXT_AXIS = [1, 2, 0, 1]
1685# map axes strings to/from tuples of inner axis, parity, repetition, frame
1686_AXES2TUPLE = {
1687 "sxyz": (0, 0, 0, 0),
1688 "sxyx": (0, 0, 1, 0),
1689 "sxzy": (0, 1, 0, 0),
1690 "sxzx": (0, 1, 1, 0),
1691 "syzx": (1, 0, 0, 0),
1692 "syzy": (1, 0, 1, 0),
1693 "syxz": (1, 1, 0, 0),
1694 "syxy": (1, 1, 1, 0),
1695 "szxy": (2, 0, 0, 0),
1696 "szxz": (2, 0, 1, 0),
1697 "szyx": (2, 1, 0, 0),
1698 "szyz": (2, 1, 1, 0),
1699 "rzyx": (0, 0, 0, 1),
1700 "rxyx": (0, 0, 1, 1),
1701 "ryzx": (0, 1, 0, 1),
1702 "rxzx": (0, 1, 1, 1),
1703 "rxzy": (1, 0, 0, 1),
1704 "ryzy": (1, 0, 1, 1),
1705 "rzxy": (1, 1, 0, 1),
1706 "ryxy": (1, 1, 1, 1),
1707 "ryxz": (2, 0, 0, 1),
1708 "rzxz": (2, 0, 1, 1),
1709 "rxyz": (2, 1, 0, 1),
1710 "rzyz": (2, 1, 1, 1),
1711}
1713_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items())
1716def vector_norm(data, axis=None, out=None):
1717 """Return length, i.e. Euclidean norm, of ndarray along axis.
1719 >>> v = numpy.random.random(3)
1720 >>> n = vector_norm(v)
1721 >>> numpy.allclose(n, numpy.linalg.norm(v))
1722 True
1723 >>> v = numpy.random.rand(6, 5, 3)
1724 >>> n = vector_norm(v, axis=-1)
1725 >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2)))
1726 True
1727 >>> n = vector_norm(v, axis=1)
1728 >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1729 True
1730 >>> v = numpy.random.rand(5, 4, 3)
1731 >>> n = numpy.empty((5, 3))
1732 >>> vector_norm(v, axis=1, out=n)
1733 >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1734 True
1735 >>> vector_norm([])
1736 0.0
1737 >>> vector_norm([1])
1738 1.0
1740 """
1741 data = numpy.array(data, dtype=numpy.float64, copy=True)
1742 if out is None:
1743 if data.ndim == 1:
1744 return math.sqrt(numpy.dot(data, data))
1745 data *= data
1746 out = numpy.atleast_1d(numpy.sum(data, axis=axis))
1747 numpy.sqrt(out, out)
1748 return out
1749 else:
1750 data *= data
1751 numpy.sum(data, axis=axis, out=out)
1752 numpy.sqrt(out, out)
1755def unit_vector(data, axis=None, out=None):
1756 """Return ndarray normalized by length, i.e. Euclidean norm, along axis.
1758 >>> v0 = numpy.random.random(3)
1759 >>> v1 = unit_vector(v0)
1760 >>> numpy.allclose(v1, v0 / numpy.linalg.norm(v0))
1761 True
1762 >>> v0 = numpy.random.rand(5, 4, 3)
1763 >>> v1 = unit_vector(v0, axis=-1)
1764 >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=2)), 2)
1765 >>> numpy.allclose(v1, v2)
1766 True
1767 >>> v1 = unit_vector(v0, axis=1)
1768 >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=1)), 1)
1769 >>> numpy.allclose(v1, v2)
1770 True
1771 >>> v1 = numpy.empty((5, 4, 3))
1772 >>> unit_vector(v0, axis=1, out=v1)
1773 >>> numpy.allclose(v1, v2)
1774 True
1775 >>> list(unit_vector([]))
1776 []
1777 >>> list(unit_vector([1]))
1778 [1.0]
1780 """
1781 if out is None:
1782 data = numpy.array(data, dtype=numpy.float64, copy=True)
1783 if data.ndim == 1:
1784 data /= math.sqrt(numpy.dot(data, data))
1785 return data
1786 else:
1787 if out is not data:
1788 out[:] = numpy.array(data, copy=False)
1789 data = out
1790 length = numpy.atleast_1d(numpy.sum(data * data, axis))
1791 numpy.sqrt(length, length)
1792 if axis is not None:
1793 length = numpy.expand_dims(length, axis)
1794 data /= length
1795 if out is None:
1796 return data
1799def random_vector(size):
1800 """Return array of random doubles in the half-open interval [0.0, 1.0).
1802 >>> v = random_vector(10000)
1803 >>> numpy.all(v >= 0) and numpy.all(v < 1)
1804 True
1805 >>> v0 = random_vector(10)
1806 >>> v1 = random_vector(10)
1807 >>> numpy.any(v0 == v1)
1808 False
1810 """
1811 return numpy.random.random(size)
1814def vector_product(v0, v1, axis=0):
1815 """Return vector perpendicular to vectors.
1817 >>> v = vector_product([2, 0, 0], [0, 3, 0])
1818 >>> numpy.allclose(v, [0, 0, 6])
1819 True
1820 >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]]
1821 >>> v1 = [[3], [0], [0]]
1822 >>> v = vector_product(v0, v1)
1823 >>> numpy.allclose(v, [[0, 0, 0, 0], [0, 0, 6, 6], [0, -6, 0, -6]])
1824 True
1825 >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]]
1826 >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]]
1827 >>> v = vector_product(v0, v1, axis=1)
1828 >>> numpy.allclose(v, [[0, 0, 6], [0, -6, 0], [6, 0, 0], [0, -6, 6]])
1829 True
1831 """
1832 return numpy.cross(v0, v1, axis=axis)
1835def angle_between_vectors(v0, v1, directed=True, axis=0):
1836 """Return angle between vectors.
1838 If directed is False, the input vectors are interpreted as undirected axes,
1839 i.e. the maximum angle is pi/2.
1841 >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3])
1842 >>> numpy.allclose(a, math.pi)
1843 True
1844 >>> a = angle_between_vectors([1, -2, 3], [-1, 2, -3], directed=False)
1845 >>> numpy.allclose(a, 0)
1846 True
1847 >>> v0 = [[2, 0, 0, 2], [0, 2, 0, 2], [0, 0, 2, 2]]
1848 >>> v1 = [[3], [0], [0]]
1849 >>> a = angle_between_vectors(v0, v1)
1850 >>> numpy.allclose(a, [0, 1.5708, 1.5708, 0.95532])
1851 True
1852 >>> v0 = [[2, 0, 0], [2, 0, 0], [0, 2, 0], [2, 0, 0]]
1853 >>> v1 = [[0, 3, 0], [0, 0, 3], [0, 0, 3], [3, 3, 3]]
1854 >>> a = angle_between_vectors(v0, v1, axis=1)
1855 >>> numpy.allclose(a, [1.5708, 1.5708, 1.5708, 0.95532])
1856 True
1858 """
1859 v0 = numpy.array(v0, dtype=numpy.float64, copy=False)
1860 v1 = numpy.array(v1, dtype=numpy.float64, copy=False)
1861 dot = numpy.sum(v0 * v1, axis=axis)
1862 dot /= vector_norm(v0, axis=axis) * vector_norm(v1, axis=axis)
1863 return numpy.arccos(dot if directed else numpy.fabs(dot))
1866def inverse_matrix(matrix):
1867 """Return inverse of square transformation matrix.
1869 >>> M0 = random_rotation_matrix()
1870 >>> M1 = inverse_matrix(M0.T)
1871 >>> numpy.allclose(M1, numpy.linalg.inv(M0.T))
1872 True
1873 >>> for size in range(1, 7):
1874 ... M0 = numpy.random.rand(size, size)
1875 ... M1 = inverse_matrix(M0)
1876 ... if not numpy.allclose(M1, numpy.linalg.inv(M0)): print(size)
1878 """
1879 return numpy.linalg.inv(matrix)
1882def concatenate_matrices(*matrices):
1883 """Return concatenation of series of transformation matrices.
1885 >>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5
1886 >>> numpy.allclose(M, concatenate_matrices(M))
1887 True
1888 >>> numpy.allclose(numpy.dot(M, M.T), concatenate_matrices(M, M.T))
1889 True
1891 """
1892 M = numpy.identity(4)
1893 for i in matrices:
1894 M = numpy.dot(M, i)
1895 return M
1898def apply_transform(matrix, points):
1899 """Return 3D cartesian points after applying transform.
1901 The points are converted to 3D Homogeneous, the transformation
1902 is applied, and then they are converted back to Cartesian"""
1903 if points.shape[0] != 3:
1904 raise ValueError("points must be 3xN")
1905 points = numpy.vstack([points, numpy.ones((1, points.shape[1]))])
1906 tformed_points = numpy.dot(matrix, points)
1907 tformed_points = tformed_points[:3, :] / tformed_points[3, :]
1908 return tformed_points
1911def is_same_transform(matrix0, matrix1):
1912 """Return True if two matrices perform same transformation.
1914 >>> is_same_transform(numpy.identity(4), numpy.identity(4))
1915 True
1916 >>> is_same_transform(numpy.identity(4), random_rotation_matrix())
1917 False
1919 """
1920 matrix0 = numpy.array(matrix0, dtype=numpy.float64, copy=True)
1921 matrix0 /= matrix0[3, 3]
1922 matrix1 = numpy.array(matrix1, dtype=numpy.float64, copy=True)
1923 matrix1 /= matrix1[3, 3]
1924 return numpy.allclose(matrix0, matrix1)
1927def _import_module(name, package=None, warn=False, prefix="_py_", ignore="_"):
1928 """Try import all public attributes from module into global namespace.
1930 Existing attributes with name clashes are renamed with prefix.
1931 Attributes starting with underscore are ignored by default.
1933 Return True on successful import.
1935 """
1936 import warnings
1937 from importlib import import_module
1939 try:
1940 if not package: 1940 ↛ 1943line 1940 didn't jump to line 1943, because the condition on line 1940 was never false
1941 module = import_module(name)
1942 else:
1943 module = import_module("." + name, package=package)
1944 except ImportError:
1945 if warn: 1945 ↛ 1946line 1945 didn't jump to line 1946, because the condition on line 1945 was never true
1946 warnings.warn("failed to import module %s" % name)
1947 else:
1948 for attr in dir(module):
1949 if ignore and attr.startswith(ignore):
1950 continue
1951 if prefix:
1952 if attr in globals():
1953 globals()[prefix + attr] = globals()[attr]
1954 elif warn:
1955 warnings.warn("no Python implementation of " + attr)
1956 globals()[attr] = getattr(module, attr)
1957 return True
1960# print("about to do import_module, id(inverse_matrix) = %x" % id(inverse_matrix))
1961_import_module("transformations._transformations")
1962# print("after done import_module, id(inverse_matrix) = %x" % id(inverse_matrix))
1964if __name__ == "__main__":
1965 import doctest
1966 import random # used in doctests
1968 numpy.set_printoptions(suppress=True, precision=5)
1969 doctest.testmod()