3
# -*- coding: utf-8 -*-
6
# Copyright (c) 2006, Christoph Gohlke
7
# Copyright (c) 2006-2009, The Regents of the University of California
10
# Redistribution and use in source and binary forms, with or without
11
# modification, are permitted provided that the following conditions are met:
13
# * Redistributions of source code must retain the above copyright
14
# notice, this list of conditions and the following disclaimer.
15
# * Redistributions in binary form must reproduce the above copyright
16
# notice, this list of conditions and the following disclaimer in the
17
# documentation and/or other materials provided with the distribution.
18
# * Neither the name of the copyright holders nor the names of any
19
# contributors may be used to endorse or promote products derived
20
# from this software without specific prior written permission.
22
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
23
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
24
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
25
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
26
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
27
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
28
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
29
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
30
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
31
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32
# POSSIBILITY OF SUCH DAMAGE.
34
"""Homogeneous Transformation Matrices and Quaternions.
36
A library for calculating 4x4 matrices for translating, rotating, reflecting,
37
scaling, shearing, projecting, orthogonalizing, and superimposing arrays of
38
3D homogeneous coordinates as well as for converting between rotation matrices,
39
Euler angles, and quaternions. Also includes an Arcball control object and
40
functions to decompose transformation matrices.
43
`Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`__,
44
Laboratory for Fluorescence Dynamics, University of California, Irvine
51
* `Python 2.6 <http://www.python.org>`__
52
* `Numpy 1.3 <http://numpy.scipy.org>`__
53
* `transformations.c 20090418 <http://www.lfd.uci.edu/~gohlke/>`__
54
(optional implementation of some functions in C)
59
Matrices (M) can be inverted using numpy.linalg.inv(M), concatenated using
60
numpy.dot(M0, M1), or used to transform homogeneous coordinates (v) using
61
numpy.dot(M, v) for shape (4, \*) "point of arrays", respectively
62
numpy.dot(v, M.T) for shape (\*, 4) "array of points".
64
Calculations are carried out with numpy.float64 precision.
66
This Python implementation is not optimized for speed.
68
Vector, point, quaternion, and matrix function arguments are expected to be
69
"array like", i.e. tuple, list, or numpy arrays.
71
Return types are numpy arrays unless specified otherwise.
73
Angles are in radians unless specified otherwise.
75
Quaternions ix+jy+kz+w are represented as [x, y, z, w].
77
Use the transpose of transformation matrices for OpenGL glMultMatrixd().
79
A triple of Euler angles can be applied/interpreted in 24 ways, which can
80
be specified using a 4 character string or encoded 4-tuple:
82
*Axes 4-string*: e.g. 'sxyz' or 'ryxy'
84
- first character : rotations are applied to 's'tatic or 'r'otating frame
85
- remaining characters : successive rotation axis 'x', 'y', or 'z'
87
*Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1)
89
- inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix.
90
- parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed
91
by 'z', or 'z' is followed by 'x'. Otherwise odd (1).
92
- repetition : first and last axis are same (1) or different (0).
93
- frame : rotations are applied to static (0) or rotating (1) frame.
98
(1) Matrices and transformations. Ronald Goldman.
99
In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990.
100
(2) More matrices and transformations: shear and pseudo-perspective.
101
Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
102
(3) Decomposing a matrix into simple transformations. Spencer Thomas.
103
In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991.
104
(4) Recovering the data from the transformation matrix. Ronald Goldman.
105
In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991.
106
(5) Euler angle conversion. Ken Shoemake.
107
In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994.
108
(6) Arcball rotation control. Ken Shoemake.
109
In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994.
110
(7) Representing attitude: Euler angles, unit quaternions, and rotation
111
vectors. James Diebel. 2006.
112
(8) A discussion of the solution for the best rotation to relate two sets
113
of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828.
114
(9) Closed-form solution of absolute orientation using unit quaternions.
115
BKP Horn. J Opt Soc Am A. 1987. 4(4), 629-642.
116
(10) Quaternions. Ken Shoemake.
117
http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf
118
(11) From quaternion to matrix and back. JMP van Waveren. 2005.
119
http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm
120
(12) Uniform random rotations. Ken Shoemake.
121
In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992.
127
>>> alpha, beta, gamma = 0.123, -1.234, 2.345
128
>>> origin, xaxis, yaxis, zaxis = (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1)
129
>>> I = identity_matrix()
130
>>> Rx = rotation_matrix(alpha, xaxis)
131
>>> Ry = rotation_matrix(beta, yaxis)
132
>>> Rz = rotation_matrix(gamma, zaxis)
133
>>> R = concatenate_matrices(Rx, Ry, Rz)
134
>>> euler = euler_from_matrix(R, 'rxyz')
135
>>> numpy.allclose([alpha, beta, gamma], euler)
137
>>> Re = euler_matrix(alpha, beta, gamma, 'rxyz')
138
>>> is_same_transform(R, Re)
140
>>> al, be, ga = euler_from_matrix(Re, 'rxyz')
141
>>> is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz'))
143
>>> qx = quaternion_about_axis(alpha, xaxis)
144
>>> qy = quaternion_about_axis(beta, yaxis)
145
>>> qz = quaternion_about_axis(gamma, zaxis)
146
>>> q = quaternion_multiply(qx, qy)
147
>>> q = quaternion_multiply(q, qz)
148
>>> Rq = quaternion_matrix(q)
149
>>> is_same_transform(R, Rq)
151
>>> S = scale_matrix(1.23, origin)
152
>>> T = translation_matrix((1, 2, 3))
153
>>> Z = shear_matrix(beta, xaxis, origin, zaxis)
154
>>> R = random_rotation_matrix(numpy.random.rand(3))
155
>>> M = concatenate_matrices(T, R, Z, S)
156
>>> scale, shear, angles, trans, persp = decompose_matrix(M)
157
>>> numpy.allclose(scale, 1.23)
159
>>> numpy.allclose(trans, (1, 2, 3))
161
>>> numpy.allclose(shear, (0, math.tan(beta), 0))
163
>>> is_same_transform(R, euler_matrix(axes='sxyz', *angles))
165
>>> M1 = compose_matrix(scale, shear, angles, trans, persp)
166
>>> is_same_transform(M, M1)
171
from __future__ import division
178
# Documentation in HTML format can be generated with Epydoc
179
__docformat__ = "restructuredtext en"
182
def identity_matrix():
183
"""Return 4x4 identity/unit matrix.
185
>>> I = identity_matrix()
186
>>> numpy.allclose(I, numpy.dot(I, I))
188
>>> numpy.sum(I), numpy.trace(I)
190
>>> numpy.allclose(I, numpy.identity(4, dtype=numpy.float64))
194
return numpy.identity(4, dtype=numpy.float64)
197
def translation_matrix(direction):
198
"""Return matrix to translate by direction vector.
200
>>> v = numpy.random.random(3) - 0.5
201
>>> numpy.allclose(v, translation_matrix(v)[:3, 3])
205
M = numpy.identity(4)
206
M[:3, 3] = direction[:3]
210
def translation_from_matrix(matrix):
211
"""Return translation vector from translation matrix.
213
>>> v0 = numpy.random.random(3) - 0.5
214
>>> v1 = translation_from_matrix(translation_matrix(v0))
215
>>> numpy.allclose(v0, v1)
219
return numpy.array(matrix, copy=False)[:3, 3].copy()
222
def reflection_matrix(point, normal):
223
"""Return matrix to mirror at plane defined by point and normal vector.
225
>>> v0 = numpy.random.random(4) - 0.5
227
>>> v1 = numpy.random.random(3) - 0.5
228
>>> R = reflection_matrix(v0, v1)
229
>>> numpy.allclose(2., numpy.trace(R))
231
>>> numpy.allclose(v0, numpy.dot(R, v0))
237
>>> numpy.allclose(v2, numpy.dot(R, v3))
241
normal = unit_vector(normal[:3])
242
M = numpy.identity(4)
243
M[:3, :3] -= 2.0 * numpy.outer(normal, normal)
244
M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal
248
def reflection_from_matrix(matrix):
249
"""Return mirror plane point and normal vector from reflection matrix.
251
>>> v0 = numpy.random.random(3) - 0.5
252
>>> v1 = numpy.random.random(3) - 0.5
253
>>> M0 = reflection_matrix(v0, v1)
254
>>> point, normal = reflection_from_matrix(M0)
255
>>> M1 = reflection_matrix(point, normal)
256
>>> is_same_transform(M0, M1)
260
M = numpy.array(matrix, dtype=numpy.float64, copy=False)
261
# normal: unit eigenvector corresponding to eigenvalue -1
262
l, V = numpy.linalg.eig(M[:3, :3])
263
i = numpy.where(abs(numpy.real(l) + 1.0) < 1e-8)[0]
265
raise ValueError("no unit eigenvector corresponding to eigenvalue -1")
266
normal = numpy.real(V[:, i[0]]).squeeze()
267
# point: any unit eigenvector corresponding to eigenvalue 1
268
l, V = numpy.linalg.eig(M)
269
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
271
raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
272
point = numpy.real(V[:, i[-1]]).squeeze()
277
def rotation_matrix(angle, direction, point=None):
278
"""Return matrix to rotate about axis defined by point and direction.
280
>>> angle = (random.random() - 0.5) * (2*math.pi)
281
>>> direc = numpy.random.random(3) - 0.5
282
>>> point = numpy.random.random(3) - 0.5
283
>>> R0 = rotation_matrix(angle, direc, point)
284
>>> R1 = rotation_matrix(angle-2*math.pi, direc, point)
285
>>> is_same_transform(R0, R1)
287
>>> R0 = rotation_matrix(angle, direc, point)
288
>>> R1 = rotation_matrix(-angle, -direc, point)
289
>>> is_same_transform(R0, R1)
291
>>> I = numpy.identity(4, numpy.float64)
292
>>> numpy.allclose(I, rotation_matrix(math.pi*2, direc))
294
>>> numpy.allclose(2., numpy.trace(rotation_matrix(math.pi/2,
299
sina = math.sin(angle)
300
cosa = math.cos(angle)
301
direction = unit_vector(direction[:3])
302
# rotation matrix around unit vector
303
R = numpy.array(((cosa, 0.0, 0.0),
305
(0.0, 0.0, cosa)), dtype=numpy.float64)
306
R += numpy.outer(direction, direction) * (1.0 - cosa)
308
R += numpy.array((( 0.0, -direction[2], direction[1]),
309
( direction[2], 0.0, -direction[0]),
310
(-direction[1], direction[0], 0.0)),
312
M = numpy.identity(4)
314
if point is not None:
315
# rotation not around origin
316
point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
317
M[:3, 3] = point - numpy.dot(R, point)
321
def rotation_from_matrix(matrix):
322
"""Return rotation angle and axis from rotation matrix.
324
>>> angle = (random.random() - 0.5) * (2*math.pi)
325
>>> direc = numpy.random.random(3) - 0.5
326
>>> point = numpy.random.random(3) - 0.5
327
>>> R0 = rotation_matrix(angle, direc, point)
328
>>> angle, direc, point = rotation_from_matrix(R0)
329
>>> R1 = rotation_matrix(angle, direc, point)
330
>>> is_same_transform(R0, R1)
334
R = numpy.array(matrix, dtype=numpy.float64, copy=False)
336
# direction: unit eigenvector of R33 corresponding to eigenvalue of 1
337
l, W = numpy.linalg.eig(R33.T)
338
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
340
raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
341
direction = numpy.real(W[:, i[-1]]).squeeze()
342
# point: unit eigenvector of R33 corresponding to eigenvalue of 1
343
l, Q = numpy.linalg.eig(R)
344
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
346
raise ValueError("no unit eigenvector corresponding to eigenvalue 1")
347
point = numpy.real(Q[:, i[-1]]).squeeze()
349
# rotation angle depending on direction
350
cosa = (numpy.trace(R33) - 1.0) / 2.0
351
if abs(direction[2]) > 1e-8:
352
sina = (R[1, 0] + (cosa-1.0)*direction[0]*direction[1]) / direction[2]
353
elif abs(direction[1]) > 1e-8:
354
sina = (R[0, 2] + (cosa-1.0)*direction[0]*direction[2]) / direction[1]
356
sina = (R[2, 1] + (cosa-1.0)*direction[1]*direction[2]) / direction[0]
357
angle = math.atan2(sina, cosa)
358
return angle, direction, point
361
def scale_matrix(factor, origin=None, direction=None):
362
"""Return matrix to scale by factor around origin in direction.
364
Use factor -1 for point symmetry.
366
>>> v = (numpy.random.rand(4, 5) - 0.5) * 20.0
368
>>> S = scale_matrix(-1.234)
369
>>> numpy.allclose(numpy.dot(S, v)[:3], -1.234*v[:3])
371
>>> factor = random.random() * 10 - 5
372
>>> origin = numpy.random.random(3) - 0.5
373
>>> direct = numpy.random.random(3) - 0.5
374
>>> S = scale_matrix(factor, origin)
375
>>> S = scale_matrix(factor, origin, direct)
378
if direction is None:
380
M = numpy.array(((factor, 0.0, 0.0, 0.0),
381
(0.0, factor, 0.0, 0.0),
382
(0.0, 0.0, factor, 0.0),
383
(0.0, 0.0, 0.0, 1.0)), dtype=numpy.float64)
384
if origin is not None:
385
M[:3, 3] = origin[:3]
386
M[:3, 3] *= 1.0 - factor
389
direction = unit_vector(direction[:3])
390
factor = 1.0 - factor
391
M = numpy.identity(4)
392
M[:3, :3] -= factor * numpy.outer(direction, direction)
393
if origin is not None:
394
M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction
398
def scale_from_matrix(matrix):
399
"""Return scaling factor, origin and direction from scaling matrix.
401
>>> factor = random.random() * 10 - 5
402
>>> origin = numpy.random.random(3) - 0.5
403
>>> direct = numpy.random.random(3) - 0.5
404
>>> S0 = scale_matrix(factor, origin)
405
>>> factor, origin, direction = scale_from_matrix(S0)
406
>>> S1 = scale_matrix(factor, origin, direction)
407
>>> is_same_transform(S0, S1)
409
>>> S0 = scale_matrix(factor, origin, direct)
410
>>> factor, origin, direction = scale_from_matrix(S0)
411
>>> S1 = scale_matrix(factor, origin, direction)
412
>>> is_same_transform(S0, S1)
416
M = numpy.array(matrix, dtype=numpy.float64, copy=False)
418
factor = numpy.trace(M33) - 2.0
420
# direction: unit eigenvector corresponding to eigenvalue factor
421
l, V = numpy.linalg.eig(M33)
422
i = numpy.where(abs(numpy.real(l) - factor) < 1e-8)[0][0]
423
direction = numpy.real(V[:, i]).squeeze()
424
direction /= vector_norm(direction)
427
factor = (factor + 2.0) / 3.0
429
# origin: any eigenvector corresponding to eigenvalue 1
430
l, V = numpy.linalg.eig(M)
431
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
433
raise ValueError("no eigenvector corresponding to eigenvalue 1")
434
origin = numpy.real(V[:, i[-1]]).squeeze()
436
return factor, origin, direction
439
def projection_matrix(point, normal, direction=None,
440
perspective=None, pseudo=False):
441
"""Return matrix to project onto plane defined by point and normal.
443
Using either perspective point, projection direction, or none of both.
445
If pseudo is True, perspective projections will preserve relative depth
446
such that Perspective = dot(Orthogonal, PseudoPerspective).
448
>>> P = projection_matrix((0, 0, 0), (1, 0, 0))
449
>>> numpy.allclose(P[1:, 1:], numpy.identity(4)[1:, 1:])
451
>>> point = numpy.random.random(3) - 0.5
452
>>> normal = numpy.random.random(3) - 0.5
453
>>> direct = numpy.random.random(3) - 0.5
454
>>> persp = numpy.random.random(3) - 0.5
455
>>> P0 = projection_matrix(point, normal)
456
>>> P1 = projection_matrix(point, normal, direction=direct)
457
>>> P2 = projection_matrix(point, normal, perspective=persp)
458
>>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True)
459
>>> is_same_transform(P2, numpy.dot(P0, P3))
461
>>> P = projection_matrix((3, 0, 0), (1, 1, 0), (1, 0, 0))
462
>>> v0 = (numpy.random.rand(4, 5) - 0.5) * 20.0
464
>>> v1 = numpy.dot(P, v0)
465
>>> numpy.allclose(v1[1], v0[1])
467
>>> numpy.allclose(v1[0], 3.0-v1[1])
471
M = numpy.identity(4)
472
point = numpy.array(point[:3], dtype=numpy.float64, copy=False)
473
normal = unit_vector(normal[:3])
474
if perspective is not None:
475
# perspective projection
476
perspective = numpy.array(perspective[:3], dtype=numpy.float64,
478
M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective-point, normal)
479
M[:3, :3] -= numpy.outer(perspective, normal)
481
# preserve relative depth
482
M[:3, :3] -= numpy.outer(normal, normal)
483
M[:3, 3] = numpy.dot(point, normal) * (perspective+normal)
485
M[:3, 3] = numpy.dot(point, normal) * perspective
487
M[3, 3] = numpy.dot(perspective, normal)
488
elif direction is not None:
489
# parallel projection
490
direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False)
491
scale = numpy.dot(direction, normal)
492
M[:3, :3] -= numpy.outer(direction, normal) / scale
493
M[:3, 3] = direction * (numpy.dot(point, normal) / scale)
495
# orthogonal projection
496
M[:3, :3] -= numpy.outer(normal, normal)
497
M[:3, 3] = numpy.dot(point, normal) * normal
501
def projection_from_matrix(matrix, pseudo=False):
502
"""Return projection plane and perspective point from projection matrix.
504
Return values are same as arguments for projection_matrix function:
505
point, normal, direction, perspective, and pseudo.
507
>>> point = numpy.random.random(3) - 0.5
508
>>> normal = numpy.random.random(3) - 0.5
509
>>> direct = numpy.random.random(3) - 0.5
510
>>> persp = numpy.random.random(3) - 0.5
511
>>> P0 = projection_matrix(point, normal)
512
>>> result = projection_from_matrix(P0)
513
>>> P1 = projection_matrix(*result)
514
>>> is_same_transform(P0, P1)
516
>>> P0 = projection_matrix(point, normal, direct)
517
>>> result = projection_from_matrix(P0)
518
>>> P1 = projection_matrix(*result)
519
>>> is_same_transform(P0, P1)
521
>>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False)
522
>>> result = projection_from_matrix(P0, pseudo=False)
523
>>> P1 = projection_matrix(*result)
524
>>> is_same_transform(P0, P1)
526
>>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True)
527
>>> result = projection_from_matrix(P0, pseudo=True)
528
>>> P1 = projection_matrix(*result)
529
>>> is_same_transform(P0, P1)
533
M = numpy.array(matrix, dtype=numpy.float64, copy=False)
535
l, V = numpy.linalg.eig(M)
536
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
537
if not pseudo and len(i):
538
# point: any eigenvector corresponding to eigenvalue 1
539
point = numpy.real(V[:, i[-1]]).squeeze()
541
# direction: unit eigenvector corresponding to eigenvalue 0
542
l, V = numpy.linalg.eig(M33)
543
i = numpy.where(abs(numpy.real(l)) < 1e-8)[0]
545
raise ValueError("no eigenvector corresponding to eigenvalue 0")
546
direction = numpy.real(V[:, i[0]]).squeeze()
547
direction /= vector_norm(direction)
548
# normal: unit eigenvector of M33.T corresponding to eigenvalue 0
549
l, V = numpy.linalg.eig(M33.T)
550
i = numpy.where(abs(numpy.real(l)) < 1e-8)[0]
552
# parallel projection
553
normal = numpy.real(V[:, i[0]]).squeeze()
554
normal /= vector_norm(normal)
555
return point, normal, direction, None, False
557
# orthogonal projection, where normal equals direction vector
558
return point, direction, None, None, False
560
# perspective projection
561
i = numpy.where(abs(numpy.real(l)) > 1e-8)[0]
564
"no eigenvector not corresponding to eigenvalue 0")
565
point = numpy.real(V[:, i[-1]]).squeeze()
568
perspective = M[:3, 3] / numpy.dot(point[:3], normal)
570
perspective -= normal
571
return point, normal, None, perspective, pseudo
574
def clip_matrix(left, right, bottom, top, near, far, perspective=False):
575
"""Return matrix to obtain normalized device coordinates from frustrum.
577
The frustrum bounds are axis-aligned along x (left, right),
578
y (bottom, top) and z (near, far).
580
Normalized device coordinates are in range [-1, 1] if coordinates are
583
If perspective is True the frustrum is a truncated pyramid with the
584
perspective point at origin and direction along z axis, otherwise an
585
orthographic canonical view volume (a box).
587
Homogeneous coordinates transformed by the perspective clip matrix
588
need to be dehomogenized (devided by w coordinate).
590
>>> frustrum = numpy.random.rand(6)
591
>>> frustrum[1] += frustrum[0]
592
>>> frustrum[3] += frustrum[2]
593
>>> frustrum[5] += frustrum[4]
594
>>> M = clip_matrix(*frustrum, perspective=False)
595
>>> numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0])
596
array([-1., -1., -1., 1.])
597
>>> numpy.dot(M, [frustrum[1], frustrum[3], frustrum[5], 1.0])
598
array([ 1., 1., 1., 1.])
599
>>> M = clip_matrix(*frustrum, perspective=True)
600
>>> v = numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0])
602
array([-1., -1., -1., 1.])
603
>>> v = numpy.dot(M, [frustrum[1], frustrum[3], frustrum[4], 1.0])
605
array([ 1., 1., -1., 1.])
608
if left >= right or bottom >= top or near >= far:
609
raise ValueError("invalid frustrum")
612
raise ValueError("invalid frustrum: near <= 0")
614
M = ((-t/(right-left), 0.0, (right+left)/(right-left), 0.0),
615
(0.0, -t/(top-bottom), (top+bottom)/(top-bottom), 0.0),
616
(0.0, 0.0, -(far+near)/(far-near), t*far/(far-near)),
617
(0.0, 0.0, -1.0, 0.0))
619
M = ((2.0/(right-left), 0.0, 0.0, (right+left)/(left-right)),
620
(0.0, 2.0/(top-bottom), 0.0, (top+bottom)/(bottom-top)),
621
(0.0, 0.0, 2.0/(far-near), (far+near)/(near-far)),
622
(0.0, 0.0, 0.0, 1.0))
623
return numpy.array(M, dtype=numpy.float64)
626
def shear_matrix(angle, direction, point, normal):
627
"""Return matrix to shear by angle along direction vector on shear plane.
629
The shear plane is defined by a point and normal vector. The direction
630
vector must be orthogonal to the plane's normal vector.
632
A point P is transformed by the shear matrix into P" such that
633
the vector P-P" is parallel to the direction vector and its extent is
634
given by the angle of P-P'-P", where P' is the orthogonal projection
635
of P onto the shear plane.
637
>>> angle = (random.random() - 0.5) * 4*math.pi
638
>>> direct = numpy.random.random(3) - 0.5
639
>>> point = numpy.random.random(3) - 0.5
640
>>> normal = numpy.cross(direct, numpy.random.random(3))
641
>>> S = shear_matrix(angle, direct, point, normal)
642
>>> numpy.allclose(1.0, numpy.linalg.det(S))
646
normal = unit_vector(normal[:3])
647
direction = unit_vector(direction[:3])
648
if abs(numpy.dot(normal, direction)) > 1e-6:
649
raise ValueError("direction and normal vectors are not orthogonal")
650
angle = math.tan(angle)
651
M = numpy.identity(4)
652
M[:3, :3] += angle * numpy.outer(direction, normal)
653
M[:3, 3] = -angle * numpy.dot(point[:3], normal) * direction
657
def shear_from_matrix(matrix):
658
"""Return shear angle, direction and plane from shear matrix.
660
>>> angle = (random.random() - 0.5) * 4*math.pi
661
>>> direct = numpy.random.random(3) - 0.5
662
>>> point = numpy.random.random(3) - 0.5
663
>>> normal = numpy.cross(direct, numpy.random.random(3))
664
>>> S0 = shear_matrix(angle, direct, point, normal)
665
>>> angle, direct, point, normal = shear_from_matrix(S0)
666
>>> S1 = shear_matrix(angle, direct, point, normal)
667
>>> is_same_transform(S0, S1)
671
M = numpy.array(matrix, dtype=numpy.float64, copy=False)
673
# normal: cross independent eigenvectors corresponding to the eigenvalue 1
674
l, V = numpy.linalg.eig(M33)
675
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-4)[0]
677
raise ValueError("No two linear independent eigenvectors found %s" % l)
678
V = numpy.real(V[:, i]).squeeze().T
680
for i0, i1 in ((0, 1), (0, 2), (1, 2)):
681
n = numpy.cross(V[i0], V[i1])
687
# direction and angle
688
direction = numpy.dot(M33 - numpy.identity(3), normal)
689
angle = vector_norm(direction)
691
angle = math.atan(angle)
692
# point: eigenvector corresponding to eigenvalue 1
693
l, V = numpy.linalg.eig(M)
694
i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0]
696
raise ValueError("no eigenvector corresponding to eigenvalue 1")
697
point = numpy.real(V[:, i[-1]]).squeeze()
699
return angle, direction, point, normal
702
def decompose_matrix(matrix):
703
"""Return sequence of transformations from transformation matrix.
706
Non-degenerative homogeneous transformation matrix
709
scale : vector of 3 scaling factors
710
shear : list of shear factors for x-y, x-z, y-z axes
711
angles : list of Euler angles about static x, y, z axes
712
translate : translation vector along x, y, z axes
713
perspective : perspective partition of matrix
715
Raise ValueError if matrix is of wrong type or degenerative.
717
>>> T0 = translation_matrix((1, 2, 3))
718
>>> scale, shear, angles, trans, persp = decompose_matrix(T0)
719
>>> T1 = translation_matrix(trans)
720
>>> numpy.allclose(T0, T1)
722
>>> S = scale_matrix(0.123)
723
>>> scale, shear, angles, trans, persp = decompose_matrix(S)
726
>>> R0 = euler_matrix(1, 2, 3)
727
>>> scale, shear, angles, trans, persp = decompose_matrix(R0)
728
>>> R1 = euler_matrix(*angles)
729
>>> numpy.allclose(R0, R1)
733
M = numpy.array(matrix, dtype=numpy.float64, copy=True).T
734
if abs(M[3, 3]) < _EPS:
735
raise ValueError("M[3, 3] is zero")
739
if not numpy.linalg.det(P):
740
raise ValueError("Matrix is singular")
742
scale = numpy.zeros((3, ), dtype=numpy.float64)
746
if any(abs(M[:3, 3]) > _EPS):
747
perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T))
750
perspective = numpy.array((0, 0, 0, 1), dtype=numpy.float64)
752
translate = M[3, :3].copy()
755
row = M[:3, :3].copy()
756
scale[0] = vector_norm(row[0])
758
shear[0] = numpy.dot(row[0], row[1])
759
row[1] -= row[0] * shear[0]
760
scale[1] = vector_norm(row[1])
763
shear[1] = numpy.dot(row[0], row[2])
764
row[2] -= row[0] * shear[1]
765
shear[2] = numpy.dot(row[1], row[2])
766
row[2] -= row[1] * shear[2]
767
scale[2] = vector_norm(row[2])
769
shear[1:] /= scale[2]
771
if numpy.dot(row[0], numpy.cross(row[1], row[2])) < 0:
775
angles[1] = math.asin(-row[0, 2])
776
if math.cos(angles[1]):
777
angles[0] = math.atan2(row[1, 2], row[2, 2])
778
angles[2] = math.atan2(row[0, 1], row[0, 0])
780
#angles[0] = math.atan2(row[1, 0], row[1, 1])
781
angles[0] = math.atan2(-row[2, 1], row[1, 1])
784
return scale, shear, angles, translate, perspective
787
def compose_matrix(scale=None, shear=None, angles=None, translate=None,
789
"""Return transformation matrix from sequence of transformations.
791
This is the inverse of the decompose_matrix function.
793
Sequence of transformations:
794
scale : vector of 3 scaling factors
795
shear : list of shear factors for x-y, x-z, y-z axes
796
angles : list of Euler angles about static x, y, z axes
797
translate : translation vector along x, y, z axes
798
perspective : perspective partition of matrix
800
>>> scale = numpy.random.random(3) - 0.5
801
>>> shear = numpy.random.random(3) - 0.5
802
>>> angles = (numpy.random.random(3) - 0.5) * (2*math.pi)
803
>>> trans = numpy.random.random(3) - 0.5
804
>>> persp = numpy.random.random(4) - 0.5
805
>>> M0 = compose_matrix(scale, shear, angles, trans, persp)
806
>>> result = decompose_matrix(M0)
807
>>> M1 = compose_matrix(*result)
808
>>> is_same_transform(M0, M1)
812
M = numpy.identity(4)
813
if perspective is not None:
814
P = numpy.identity(4)
815
P[3, :] = perspective[:4]
817
if translate is not None:
818
T = numpy.identity(4)
819
T[:3, 3] = translate[:3]
821
if angles is not None:
822
R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz')
824
if shear is not None:
825
Z = numpy.identity(4)
830
if scale is not None:
831
S = numpy.identity(4)
840
def orthogonalization_matrix(lengths, angles):
841
"""Return orthogonalization matrix for crystallographic cell coordinates.
843
Angles are expected in degrees.
845
The de-orthogonalization matrix is the inverse.
847
>>> O = orthogonalization_matrix((10., 10., 10.), (90., 90., 90.))
848
>>> numpy.allclose(O[:3, :3], numpy.identity(3, float) * 10)
850
>>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7])
851
>>> numpy.allclose(numpy.sum(O), 43.063229)
856
angles = numpy.radians(angles)
857
sina, sinb, _ = numpy.sin(angles)
858
cosa, cosb, cosg = numpy.cos(angles)
859
co = (cosa * cosb - cosg) / (sina * sinb)
861
( a*sinb*math.sqrt(1.0-co*co), 0.0, 0.0, 0.0),
862
(-a*sinb*co, b*sina, 0.0, 0.0),
863
( a*cosb, b*cosa, c, 0.0),
864
( 0.0, 0.0, 0.0, 1.0)),
868
def superimposition_matrix(v0, v1, scaling=False, usesvd=True):
869
"""Return matrix to transform given vector set into second vector set.
871
v0 and v1 are shape (3, \*) or (4, \*) arrays of at least 3 vectors.
873
If usesvd is True, the weighted sum of squared deviations (RMSD) is
874
minimized according to the algorithm by W. Kabsch [8]. Otherwise the
875
quaternion based algorithm by B. Horn [9] is used (slower when using
876
this Python implementation).
878
The returned matrix performs rotation, translation and uniform scaling
881
>>> v0 = numpy.random.rand(3, 10)
882
>>> M = superimposition_matrix(v0, v0)
883
>>> numpy.allclose(M, numpy.identity(4))
885
>>> R = random_rotation_matrix(numpy.random.random(3))
886
>>> v0 = ((1,0,0), (0,1,0), (0,0,1), (1,1,1))
887
>>> v1 = numpy.dot(R, v0)
888
>>> M = superimposition_matrix(v0, v1)
889
>>> numpy.allclose(v1, numpy.dot(M, v0))
891
>>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20.0
893
>>> v1 = numpy.dot(R, v0)
894
>>> M = superimposition_matrix(v0, v1)
895
>>> numpy.allclose(v1, numpy.dot(M, v0))
897
>>> S = scale_matrix(random.random())
898
>>> T = translation_matrix(numpy.random.random(3)-0.5)
899
>>> M = concatenate_matrices(T, R, S)
900
>>> v1 = numpy.dot(M, v0)
901
>>> v0[:3] += numpy.random.normal(0.0, 1e-9, 300).reshape(3, -1)
902
>>> M = superimposition_matrix(v0, v1, scaling=True)
903
>>> numpy.allclose(v1, numpy.dot(M, v0))
905
>>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False)
906
>>> numpy.allclose(v1, numpy.dot(M, v0))
908
>>> v = numpy.empty((4, 100, 3), dtype=numpy.float64)
910
>>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False)
911
>>> numpy.allclose(v1, numpy.dot(M, v[:, :, 0]))
915
v0 = numpy.array(v0, dtype=numpy.float64, copy=False)[:3]
916
v1 = numpy.array(v1, dtype=numpy.float64, copy=False)[:3]
918
if v0.shape != v1.shape or v0.shape[1] < 3:
919
raise ValueError("Vector sets are of wrong shape or type.")
921
# move centroids to origin
922
t0 = numpy.mean(v0, axis=1)
923
t1 = numpy.mean(v1, axis=1)
924
v0 = v0 - t0.reshape(3, 1)
925
v1 = v1 - t1.reshape(3, 1)
928
# Singular Value Decomposition of covariance matrix
929
u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T))
930
# rotation matrix from SVD orthonormal bases
932
if numpy.linalg.det(R) < 0.0:
933
# R does not constitute right handed system
934
R -= numpy.outer(u[:, 2], vh[2, :]*2.0)
936
# homogeneous transformation matrix
937
M = numpy.identity(4)
940
# compute symmetric matrix N
941
xx, yy, zz = numpy.sum(v0 * v1, axis=1)
942
xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1)
943
xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1)
944
N = ((xx+yy+zz, yz-zy, zx-xz, xy-yx),
945
(yz-zy, xx-yy-zz, xy+yx, zx+xz),
946
(zx-xz, xy+yx, -xx+yy-zz, yz+zy),
947
(xy-yx, zx+xz, yz+zy, -xx-yy+zz))
948
# quaternion: eigenvector corresponding to most positive eigenvalue
949
l, V = numpy.linalg.eig(N)
950
q = V[:, numpy.argmax(l)]
951
q /= vector_norm(q) # unit quaternion
952
q = numpy.roll(q, -1) # move w component to end
953
# homogeneous transformation matrix
954
M = quaternion_matrix(q)
956
# scale: ratio of rms deviations from centroid
960
M[:3, :3] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0))
964
T = numpy.identity(4)
970
def euler_matrix(ai, aj, ak, axes='sxyz'):
971
"""Return homogeneous rotation matrix from Euler angles and axis sequence.
973
ai, aj, ak : Euler's roll, pitch and yaw angles
974
axes : One of 24 axis sequences as string or encoded tuple
976
>>> R = euler_matrix(1, 2, 3, 'syxz')
977
>>> numpy.allclose(numpy.sum(R[0]), -1.34786452)
979
>>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1))
980
>>> numpy.allclose(numpy.sum(R[0]), -0.383436184)
982
>>> ai, aj, ak = (4.0*math.pi) * (numpy.random.random(3) - 0.5)
983
>>> for axes in _AXES2TUPLE.keys():
984
... R = euler_matrix(ai, aj, ak, axes)
985
>>> for axes in _TUPLE2AXES.keys():
986
... R = euler_matrix(ai, aj, ak, axes)
990
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes]
991
except (AttributeError, KeyError):
992
_ = _TUPLE2AXES[axes]
993
firstaxis, parity, repetition, frame = axes
996
j = _NEXT_AXIS[i+parity]
997
k = _NEXT_AXIS[i-parity+1]
1002
ai, aj, ak = -ai, -aj, -ak
1004
si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak)
1005
ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak)
1006
cc, cs = ci*ck, ci*sk
1007
sc, ss = si*ck, si*sk
1009
M = numpy.identity(4)
1033
def euler_from_matrix(matrix, axes='sxyz'):
1034
"""Return Euler angles from rotation matrix for specified axis sequence.
1036
axes : One of 24 axis sequences as string or encoded tuple
1038
Note that many Euler angle triplets can describe one matrix.
1040
>>> R0 = euler_matrix(1, 2, 3, 'syxz')
1041
>>> al, be, ga = euler_from_matrix(R0, 'syxz')
1042
>>> R1 = euler_matrix(al, be, ga, 'syxz')
1043
>>> numpy.allclose(R0, R1)
1045
>>> angles = (4.0*math.pi) * (numpy.random.random(3) - 0.5)
1046
>>> for axes in _AXES2TUPLE.keys():
1047
... R0 = euler_matrix(axes=axes, *angles)
1048
... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes))
1049
... if not numpy.allclose(R0, R1): print axes, "failed"
1053
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1054
except (AttributeError, KeyError):
1055
_ = _TUPLE2AXES[axes]
1056
firstaxis, parity, repetition, frame = axes
1059
j = _NEXT_AXIS[i+parity]
1060
k = _NEXT_AXIS[i-parity+1]
1062
M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3]
1064
sy = math.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k])
1066
ax = math.atan2( M[i, j], M[i, k])
1067
ay = math.atan2( sy, M[i, i])
1068
az = math.atan2( M[j, i], -M[k, i])
1070
ax = math.atan2(-M[j, k], M[j, j])
1071
ay = math.atan2( sy, M[i, i])
1074
cy = math.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i])
1076
ax = math.atan2( M[k, j], M[k, k])
1077
ay = math.atan2(-M[k, i], cy)
1078
az = math.atan2( M[j, i], M[i, i])
1080
ax = math.atan2(-M[j, k], M[j, j])
1081
ay = math.atan2(-M[k, i], cy)
1085
ax, ay, az = -ax, -ay, -az
1091
def euler_from_quaternion(quaternion, axes='sxyz'):
1092
"""Return Euler angles from quaternion for specified axis sequence.
1094
>>> angles = euler_from_quaternion([0.06146124, 0, 0, 0.99810947])
1095
>>> numpy.allclose(angles, [0.123, 0, 0])
1099
return euler_from_matrix(quaternion_matrix(quaternion), axes)
1102
def quaternion_from_euler(ai, aj, ak, axes='sxyz'):
1103
"""Return quaternion from Euler angles and axis sequence.
1105
ai, aj, ak : Euler's roll, pitch and yaw angles
1106
axes : One of 24 axis sequences as string or encoded tuple
1108
>>> q = quaternion_from_euler(1, 2, 3, 'ryxz')
1109
>>> numpy.allclose(q, [0.310622, -0.718287, 0.444435, 0.435953])
1114
firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()]
1115
except (AttributeError, KeyError):
1116
_ = _TUPLE2AXES[axes]
1117
firstaxis, parity, repetition, frame = axes
1120
j = _NEXT_AXIS[i+parity]
1121
k = _NEXT_AXIS[i-parity+1]
1142
quaternion = numpy.empty((4, ), dtype=numpy.float64)
1144
quaternion[i] = cj*(cs + sc)
1145
quaternion[j] = sj*(cc + ss)
1146
quaternion[k] = sj*(cs - sc)
1147
quaternion[3] = cj*(cc - ss)
1149
quaternion[i] = cj*sc - sj*cs
1150
quaternion[j] = cj*ss + sj*cc
1151
quaternion[k] = cj*cs - sj*sc
1152
quaternion[3] = cj*cc + sj*ss
1159
def quaternion_about_axis(angle, axis):
1160
"""Return quaternion for rotation about axis.
1162
>>> q = quaternion_about_axis(0.123, (1, 0, 0))
1163
>>> numpy.allclose(q, [0.06146124, 0, 0, 0.99810947])
1167
quaternion = numpy.zeros((4, ), dtype=numpy.float64)
1168
quaternion[:3] = axis[:3]
1169
qlen = vector_norm(quaternion)
1171
quaternion *= math.sin(angle/2.0) / qlen
1172
quaternion[3] = math.cos(angle/2.0)
1176
def quaternion_matrix(quaternion):
1177
"""Return homogeneous rotation matrix from quaternion.
1179
>>> R = quaternion_matrix([0.06146124, 0, 0, 0.99810947])
1180
>>> numpy.allclose(R, rotation_matrix(0.123, (1, 0, 0)))
1184
q = numpy.array(quaternion[:4], dtype=numpy.float64, copy=True)
1185
nq = numpy.dot(q, q)
1187
return numpy.identity(4)
1188
q *= math.sqrt(2.0 / nq)
1189
q = numpy.outer(q, q)
1190
return numpy.array((
1191
(1.0-q[1, 1]-q[2, 2], q[0, 1]-q[2, 3], q[0, 2]+q[1, 3], 0.0),
1192
( q[0, 1]+q[2, 3], 1.0-q[0, 0]-q[2, 2], q[1, 2]-q[0, 3], 0.0),
1193
( q[0, 2]-q[1, 3], q[1, 2]+q[0, 3], 1.0-q[0, 0]-q[1, 1], 0.0),
1194
( 0.0, 0.0, 0.0, 1.0)
1195
), dtype=numpy.float64)
1198
def quaternion_from_matrix(matrix):
1199
"""Return quaternion from rotation matrix.
1201
>>> R = rotation_matrix(0.123, (1, 2, 3))
1202
>>> q = quaternion_from_matrix(R)
1203
>>> numpy.allclose(q, [0.0164262, 0.0328524, 0.0492786, 0.9981095])
1207
q = numpy.empty((4, ), dtype=numpy.float64)
1208
M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:4, :4]
1212
q[2] = M[1, 0] - M[0, 1]
1213
q[1] = M[0, 2] - M[2, 0]
1214
q[0] = M[2, 1] - M[1, 2]
1217
if M[1, 1] > M[0, 0]:
1219
if M[2, 2] > M[i, i]:
1221
t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3]
1223
q[j] = M[i, j] + M[j, i]
1224
q[k] = M[k, i] + M[i, k]
1225
q[3] = M[k, j] - M[j, k]
1226
q *= 0.5 / math.sqrt(t * M[3, 3])
1230
def quaternion_multiply(quaternion1, quaternion0):
1231
"""Return multiplication of two quaternions.
1233
>>> q = quaternion_multiply([1, -2, 3, 4], [-5, 6, 7, 8])
1234
>>> numpy.allclose(q, [-44, -14, 48, 28])
1238
x0, y0, z0, w0 = quaternion0
1239
x1, y1, z1, w1 = quaternion1
1240
return numpy.array((
1241
x1*w0 + y1*z0 - z1*y0 + w1*x0,
1242
-x1*z0 + y1*w0 + z1*x0 + w1*y0,
1243
x1*y0 - y1*x0 + z1*w0 + w1*z0,
1244
-x1*x0 - y1*y0 - z1*z0 + w1*w0), dtype=numpy.float64)
1247
def quaternion_conjugate(quaternion):
1248
"""Return conjugate of quaternion.
1250
>>> q0 = random_quaternion()
1251
>>> q1 = quaternion_conjugate(q0)
1252
>>> q1[3] == q0[3] and all(q1[:3] == -q0[:3])
1256
return numpy.array((-quaternion[0], -quaternion[1],
1257
-quaternion[2], quaternion[3]), dtype=numpy.float64)
1260
def quaternion_inverse(quaternion):
1261
"""Return inverse of quaternion.
1263
>>> q0 = random_quaternion()
1264
>>> q1 = quaternion_inverse(q0)
1265
>>> numpy.allclose(quaternion_multiply(q0, q1), [0, 0, 0, 1])
1269
return quaternion_conjugate(quaternion) / numpy.dot(quaternion, quaternion)
1272
def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True):
1273
"""Return spherical linear interpolation between two quaternions.
1275
>>> q0 = random_quaternion()
1276
>>> q1 = random_quaternion()
1277
>>> q = quaternion_slerp(q0, q1, 0.0)
1278
>>> numpy.allclose(q, q0)
1280
>>> q = quaternion_slerp(q0, q1, 1.0, 1)
1281
>>> numpy.allclose(q, q1)
1283
>>> q = quaternion_slerp(q0, q1, 0.5)
1284
>>> angle = math.acos(numpy.dot(q0, q))
1285
>>> numpy.allclose(2.0, math.acos(numpy.dot(q0, q1)) / angle) or \
1286
numpy.allclose(2.0, math.acos(-numpy.dot(q0, q1)) / angle)
1290
q0 = unit_vector(quat0[:4])
1291
q1 = unit_vector(quat1[:4])
1294
elif fraction == 1.0:
1296
d = numpy.dot(q0, q1)
1297
if abs(abs(d) - 1.0) < _EPS:
1299
if shortestpath and d < 0.0:
1303
angle = math.acos(d) + spin * math.pi
1304
if abs(angle) < _EPS:
1306
isin = 1.0 / math.sin(angle)
1307
q0 *= math.sin((1.0 - fraction) * angle) * isin
1308
q1 *= math.sin(fraction * angle) * isin
1313
def random_quaternion(rand=None):
1314
"""Return uniform random unit quaternion.
1316
rand: array like or None
1317
Three independent random variables that are uniformly distributed
1320
>>> q = random_quaternion()
1321
>>> numpy.allclose(1.0, vector_norm(q))
1323
>>> q = random_quaternion(numpy.random.random(3))
1329
rand = numpy.random.rand(3)
1331
assert len(rand) == 3
1332
r1 = numpy.sqrt(1.0 - rand[0])
1333
r2 = numpy.sqrt(rand[0])
1337
return numpy.array((numpy.sin(t1)*r1,
1340
numpy.cos(t2)*r2), dtype=numpy.float64)
1343
def random_rotation_matrix(rand=None):
1344
"""Return uniform random rotation matrix.
1347
Three independent random variables that are uniformly distributed
1348
between 0 and 1 for each returned quaternion.
1350
>>> R = random_rotation_matrix()
1351
>>> numpy.allclose(numpy.dot(R.T, R), numpy.identity(4))
1355
return quaternion_matrix(random_quaternion(rand))
1358
class Arcball(object):
1359
"""Virtual Trackball Control.
1361
>>> ball = Arcball()
1362
>>> ball = Arcball(initial=numpy.identity(4))
1363
>>> ball.place([320, 320], 320)
1364
>>> ball.down([500, 250])
1365
>>> ball.drag([475, 275])
1366
>>> R = ball.matrix()
1367
>>> numpy.allclose(numpy.sum(R), 3.90583455)
1369
>>> ball = Arcball(initial=[0, 0, 0, 1])
1370
>>> ball.place([320, 320], 320)
1371
>>> ball.setaxes([1,1,0], [-1, 1, 0])
1372
>>> ball.setconstrain(True)
1373
>>> ball.down([400, 200])
1374
>>> ball.drag([200, 400])
1375
>>> R = ball.matrix()
1376
>>> numpy.allclose(numpy.sum(R), 0.2055924)
1382
def __init__(self, initial=None):
1383
"""Initialize virtual trackball control.
1385
initial : quaternion or rotation matrix
1391
self._center = [0.0, 0.0]
1392
self._vdown = numpy.array([0, 0, 1], dtype=numpy.float64)
1393
self._constrain = False
1396
self._qdown = numpy.array([0, 0, 0, 1], dtype=numpy.float64)
1398
initial = numpy.array(initial, dtype=numpy.float64)
1399
if initial.shape == (4, 4):
1400
self._qdown = quaternion_from_matrix(initial)
1401
elif initial.shape == (4, ):
1402
initial /= vector_norm(initial)
1403
self._qdown = initial
1405
raise ValueError("initial not a quaternion or matrix.")
1407
self._qnow = self._qpre = self._qdown
1409
def place(self, center, radius):
1410
"""Place Arcball, e.g. when window size changes.
1412
center : sequence[2]
1413
Window coordinates of trackball center.
1415
Radius of trackball in window coordinates.
1418
self._radius = float(radius)
1419
self._center[0] = center[0]
1420
self._center[1] = center[1]
1422
def setaxes(self, *axes):
1423
"""Set axes to constrain rotations."""
1427
self._axes = [unit_vector(axis) for axis in axes]
1429
def setconstrain(self, constrain):
1430
"""Set state of constrain to axis mode."""
1431
self._constrain = constrain == True
1433
def getconstrain(self):
1434
"""Return state of constrain to axis mode."""
1435
return self._constrain
1437
def down(self, point):
1438
"""Set initial cursor window coordinates and pick constrain-axis."""
1439
self._vdown = arcball_map_to_sphere(point, self._center, self._radius)
1440
self._qdown = self._qpre = self._qnow
1442
if self._constrain and self._axes is not None:
1443
self._axis = arcball_nearest_axis(self._vdown, self._axes)
1444
self._vdown = arcball_constrain_to_axis(self._vdown, self._axis)
1448
def drag(self, point):
1449
"""Update current cursor window coordinates."""
1450
vnow = arcball_map_to_sphere(point, self._center, self._radius)
1452
if self._axis is not None:
1453
vnow = arcball_constrain_to_axis(vnow, self._axis)
1455
self._qpre = self._qnow
1457
t = numpy.cross(self._vdown, vnow)
1458
if numpy.dot(t, t) < _EPS:
1459
self._qnow = self._qdown
1461
q = [t[0], t[1], t[2], numpy.dot(self._vdown, vnow)]
1462
self._qnow = quaternion_multiply(q, self._qdown)
1464
def next(self, acceleration=0.0):
1465
"""Continue rotation in direction of last drag."""
1466
q = quaternion_slerp(self._qpre, self._qnow, 2.0+acceleration, False)
1467
self._qpre, self._qnow = self._qnow, q
1470
"""Return homogeneous rotation matrix."""
1471
return quaternion_matrix(self._qnow)
1474
def arcball_map_to_sphere(point, center, radius):
1475
"""Return unit sphere coordinates from window coordinates."""
1476
v = numpy.array(((point[0] - center[0]) / radius,
1477
(center[1] - point[1]) / radius,
1478
0.0), dtype=numpy.float64)
1479
n = v[0]*v[0] + v[1]*v[1]
1481
v /= math.sqrt(n) # position outside of sphere
1483
v[2] = math.sqrt(1.0 - n)
1487
def arcball_constrain_to_axis(point, axis):
1488
"""Return sphere point perpendicular to axis."""
1489
v = numpy.array(point, dtype=numpy.float64, copy=True)
1490
a = numpy.array(axis, dtype=numpy.float64, copy=True)
1491
v -= a * numpy.dot(a, v) # on plane
1499
return numpy.array([1, 0, 0], dtype=numpy.float64)
1500
return unit_vector([-a[1], a[0], 0])
1503
def arcball_nearest_axis(point, axes):
1504
"""Return axis, which arc is nearest to point."""
1505
point = numpy.array(point, dtype=numpy.float64, copy=False)
1509
t = numpy.dot(arcball_constrain_to_axis(point, axis), point)
1516
# epsilon for testing whether a number is close to zero
1517
_EPS = numpy.finfo(float).eps * 4.0
1519
# axis sequences for Euler angles
1520
_NEXT_AXIS = [1, 2, 0, 1]
1522
# map axes strings to/from tuples of inner axis, parity, repetition, frame
1524
'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0),
1525
'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0),
1526
'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0),
1527
'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0),
1528
'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1),
1529
'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1),
1530
'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1),
1531
'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)}
1533
_TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items())
1537
def vector_norm(data, axis=None, out=None):
1538
"""Return length, i.e. eucledian norm, of ndarray along axis.
1540
>>> v = numpy.random.random(3)
1541
>>> n = vector_norm(v)
1542
>>> numpy.allclose(n, numpy.linalg.norm(v))
1544
>>> v = numpy.random.rand(6, 5, 3)
1545
>>> n = vector_norm(v, axis=-1)
1546
>>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2)))
1548
>>> n = vector_norm(v, axis=1)
1549
>>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1551
>>> v = numpy.random.rand(5, 4, 3)
1552
>>> n = numpy.empty((5, 3), dtype=numpy.float64)
1553
>>> vector_norm(v, axis=1, out=n)
1554
>>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1)))
1558
>>> vector_norm([1.0])
1562
data = numpy.array(data, dtype=numpy.float64, copy=True)
1565
return math.sqrt(numpy.dot(data, data))
1567
out = numpy.atleast_1d(numpy.sum(data, axis=axis))
1568
numpy.sqrt(out, out)
1572
numpy.sum(data, axis=axis, out=out)
1573
numpy.sqrt(out, out)
1576
def unit_vector(data, axis=None, out=None):
1577
"""Return ndarray normalized by length, i.e. eucledian norm, along axis.
1579
>>> v0 = numpy.random.random(3)
1580
>>> v1 = unit_vector(v0)
1581
>>> numpy.allclose(v1, v0 / numpy.linalg.norm(v0))
1583
>>> v0 = numpy.random.rand(5, 4, 3)
1584
>>> v1 = unit_vector(v0, axis=-1)
1585
>>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=2)), 2)
1586
>>> numpy.allclose(v1, v2)
1588
>>> v1 = unit_vector(v0, axis=1)
1589
>>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=1)), 1)
1590
>>> numpy.allclose(v1, v2)
1592
>>> v1 = numpy.empty((5, 4, 3), dtype=numpy.float64)
1593
>>> unit_vector(v0, axis=1, out=v1)
1594
>>> numpy.allclose(v1, v2)
1596
>>> list(unit_vector([]))
1598
>>> list(unit_vector([1.0]))
1603
data = numpy.array(data, dtype=numpy.float64, copy=True)
1605
data /= math.sqrt(numpy.dot(data, data))
1609
out[:] = numpy.array(data, copy=False)
1611
length = numpy.atleast_1d(numpy.sum(data*data, axis))
1612
numpy.sqrt(length, length)
1613
if axis is not None:
1614
length = numpy.expand_dims(length, axis)
1620
def random_vector(size):
1621
"""Return array of random doubles in the half-open interval [0.0, 1.0).
1623
>>> v = random_vector(10000)
1624
>>> numpy.all(v >= 0.0) and numpy.all(v < 1.0)
1626
>>> v0 = random_vector(10)
1627
>>> v1 = random_vector(10)
1628
>>> numpy.any(v0 == v1)
1632
return numpy.random.random(size)
1635
def inverse_matrix(matrix):
1636
"""Return inverse of square transformation matrix.
1638
>>> M0 = random_rotation_matrix()
1639
>>> M1 = inverse_matrix(M0.T)
1640
>>> numpy.allclose(M1, numpy.linalg.inv(M0.T))
1642
>>> for size in range(1, 7):
1643
... M0 = numpy.random.rand(size, size)
1644
... M1 = inverse_matrix(M0)
1645
... if not numpy.allclose(M1, numpy.linalg.inv(M0)): print size
1648
return numpy.linalg.inv(matrix)
1651
def concatenate_matrices(*matrices):
1652
"""Return concatenation of series of transformation matrices.
1654
>>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5
1655
>>> numpy.allclose(M, concatenate_matrices(M))
1657
>>> numpy.allclose(numpy.dot(M, M.T), concatenate_matrices(M, M.T))
1661
M = numpy.identity(4)
1667
def is_same_transform(matrix0, matrix1):
1668
"""Return True if two matrices perform same transformation.
1670
>>> is_same_transform(numpy.identity(4), numpy.identity(4))
1672
>>> is_same_transform(numpy.identity(4), random_rotation_matrix())
1676
matrix0 = numpy.array(matrix0, dtype=numpy.float64, copy=True)
1677
matrix0 /= matrix0[3, 3]
1678
matrix1 = numpy.array(matrix1, dtype=numpy.float64, copy=True)
1679
matrix1 /= matrix1[3, 3]
1680
return numpy.allclose(matrix0, matrix1)
1683
def _import_module(module_name, warn=True, prefix='_py_', ignore='_'):
1684
"""Try import all public attributes from module into global namespace.
1686
Existing attributes with name clashes are renamed with prefix.
1687
Attributes starting with underscore are ignored by default.
1689
Return True on successful import.
1693
module = __import__(module_name)
1696
warnings.warn("Failed to import module " + module_name)
1698
for attr in dir(module):
1699
if ignore and attr.startswith(ignore):
1702
if attr in globals():
1703
globals()[prefix + attr] = globals()[attr]
1705
warnings.warn("No Python implementation of " + attr)
1706
globals()[attr] = getattr(module, attr)