Spaces:
Running
Running
| # -*- coding: utf-8 -*- | |
| # transformations.py | |
| # Copyright (c) 2006, Christoph Gohlke | |
| # Copyright (c) 2006-2009, The Regents of the University of California | |
| # All rights reserved. | |
| # | |
| # Redistribution and use in source and binary forms, with or without | |
| # modification, are permitted provided that the following conditions are met: | |
| # | |
| # * Redistributions of source code must retain the above copyright | |
| # notice, this list of conditions and the following disclaimer. | |
| # * Redistributions in binary form must reproduce the above copyright | |
| # notice, this list of conditions and the following disclaimer in the | |
| # documentation and/or other materials provided with the distribution. | |
| # * Neither the name of the copyright holders nor the names of any | |
| # contributors may be used to endorse or promote products derived | |
| # from this software without specific prior written permission. | |
| # | |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE | |
| # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE | |
| # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | |
| # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | |
| # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | |
| # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | |
| # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | |
| # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE | |
| # POSSIBILITY OF SUCH DAMAGE. | |
| """Homogeneous Transformation Matrices and Quaternions. | |
| A library for calculating 4x4 matrices for translating, rotating, reflecting, | |
| scaling, shearing, projecting, orthogonalizing, and superimposing arrays of | |
| 3D homogeneous coordinates as well as for converting between rotation matrices, | |
| Euler angles, and quaternions. Also includes an Arcball control object and | |
| functions to decompose transformation matrices. | |
| :Authors: | |
| `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`__, | |
| Laboratory for Fluorescence Dynamics, University of California, Irvine | |
| :Version: 20090418 | |
| Requirements | |
| ------------ | |
| * `Python 2.6 <http://www.python.org>`__ | |
| * `Numpy 1.3 <http://numpy.scipy.org>`__ | |
| * `transformations.c 20090418 <http://www.lfd.uci.edu/~gohlke/>`__ | |
| (optional implementation of some functions in C) | |
| Notes | |
| ----- | |
| Matrices (M) can be inverted using numpy.linalg.inv(M), concatenated using | |
| numpy.dot(M0, M1), or used to transform homogeneous coordinates (v) using | |
| numpy.dot(M, v) for shape (4, \*) "point of arrays", respectively | |
| numpy.dot(v, M.T) for shape (\*, 4) "array of points". | |
| Calculations are carried out with numpy.float64 precision. | |
| This Python implementation is not optimized for speed. | |
| Vector, point, quaternion, and matrix function arguments are expected to be | |
| "array like", i.e. tuple, list, or numpy arrays. | |
| Return types are numpy arrays unless specified otherwise. | |
| Angles are in radians unless specified otherwise. | |
| Quaternions ix+jy+kz+w are represented as [x, y, z, w]. | |
| Use the transpose of transformation matrices for OpenGL glMultMatrixd(). | |
| A triple of Euler angles can be applied/interpreted in 24 ways, which can | |
| be specified using a 4 character string or encoded 4-tuple: | |
| *Axes 4-string*: e.g. 'sxyz' or 'ryxy' | |
| - first character : rotations are applied to 's'tatic or 'r'otating frame | |
| - remaining characters : successive rotation axis 'x', 'y', or 'z' | |
| *Axes 4-tuple*: e.g. (0, 0, 0, 0) or (1, 1, 1, 1) | |
| - inner axis: code of axis ('x':0, 'y':1, 'z':2) of rightmost matrix. | |
| - parity : even (0) if inner axis 'x' is followed by 'y', 'y' is followed | |
| by 'z', or 'z' is followed by 'x'. Otherwise odd (1). | |
| - repetition : first and last axis are same (1) or different (0). | |
| - frame : rotations are applied to static (0) or rotating (1) frame. | |
| References | |
| ---------- | |
| (1) Matrices and transformations. Ronald Goldman. | |
| In "Graphics Gems I", pp 472-475. Morgan Kaufmann, 1990. | |
| (2) More matrices and transformations: shear and pseudo-perspective. | |
| Ronald Goldman. In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991. | |
| (3) Decomposing a matrix into simple transformations. Spencer Thomas. | |
| In "Graphics Gems II", pp 320-323. Morgan Kaufmann, 1991. | |
| (4) Recovering the data from the transformation matrix. Ronald Goldman. | |
| In "Graphics Gems II", pp 324-331. Morgan Kaufmann, 1991. | |
| (5) Euler angle conversion. Ken Shoemake. | |
| In "Graphics Gems IV", pp 222-229. Morgan Kaufmann, 1994. | |
| (6) Arcball rotation control. Ken Shoemake. | |
| In "Graphics Gems IV", pp 175-192. Morgan Kaufmann, 1994. | |
| (7) Representing attitude: Euler angles, unit quaternions, and rotation | |
| vectors. James Diebel. 2006. | |
| (8) A discussion of the solution for the best rotation to relate two sets | |
| of vectors. W Kabsch. Acta Cryst. 1978. A34, 827-828. | |
| (9) Closed-form solution of absolute orientation using unit quaternions. | |
| BKP Horn. J Opt Soc Am A. 1987. 4(4), 629-642. | |
| (10) Quaternions. Ken Shoemake. | |
| http://www.sfu.ca/~jwa3/cmpt461/files/quatut.pdf | |
| (11) From quaternion to matrix and back. JMP van Waveren. 2005. | |
| http://www.intel.com/cd/ids/developer/asmo-na/eng/293748.htm | |
| (12) Uniform random rotations. Ken Shoemake. | |
| In "Graphics Gems III", pp 124-132. Morgan Kaufmann, 1992. | |
| Examples | |
| -------- | |
| alpha, beta, gamma = 0.123, -1.234, 2.345 | |
| origin, xaxis, yaxis, zaxis = (0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 1) | |
| I = identity_matrix() | |
| Rx = rotation_matrix(alpha, xaxis) | |
| Ry = rotation_matrix(beta, yaxis) | |
| Rz = rotation_matrix(gamma, zaxis) | |
| R = concatenate_matrices(Rx, Ry, Rz) | |
| euler = euler_from_matrix(R, 'rxyz') | |
| numpy.allclose([alpha, beta, gamma], euler) | |
| True | |
| Re = euler_matrix(alpha, beta, gamma, 'rxyz') | |
| is_same_transform(R, Re) | |
| True | |
| al, be, ga = euler_from_matrix(Re, 'rxyz') | |
| is_same_transform(Re, euler_matrix(al, be, ga, 'rxyz')) | |
| True | |
| qx = quaternion_about_axis(alpha, xaxis) | |
| qy = quaternion_about_axis(beta, yaxis) | |
| qz = quaternion_about_axis(gamma, zaxis) | |
| q = quaternion_multiply(qx, qy) | |
| q = quaternion_multiply(q, qz) | |
| Rq = quaternion_matrix(q) | |
| is_same_transform(R, Rq) | |
| True | |
| S = scale_matrix(1.23, origin) | |
| T = translation_matrix((1, 2, 3)) | |
| Z = shear_matrix(beta, xaxis, origin, zaxis) | |
| R = random_rotation_matrix(numpy.random.rand(3)) | |
| M = concatenate_matrices(T, R, Z, S) | |
| scale, shear, angles, trans, persp = decompose_matrix(M) | |
| numpy.allclose(scale, 1.23) | |
| True | |
| numpy.allclose(trans, (1, 2, 3)) | |
| True | |
| numpy.allclose(shear, (0, math.tan(beta), 0)) | |
| True | |
| is_same_transform(R, euler_matrix(axes='sxyz', *angles)) | |
| True | |
| M1 = compose_matrix(scale, shear, angles, trans, persp) | |
| is_same_transform(M, M1) | |
| True | |
| """ | |
| from __future__ import division | |
| import warnings | |
| import math | |
| import numpy | |
| # Documentation in HTML format can be generated with Epydoc | |
| __docformat__ = "restructuredtext en" | |
| def skew(v): | |
| """Returns the skew-symmetric matrix of a vector | |
| cfo, 2015/08/13 | |
| """ | |
| return numpy.array([[0, -v[2], v[1]], | |
| [v[2], 0, -v[0]], | |
| [-v[1], v[0], 0]], dtype=numpy.float64) | |
| def unskew(R): | |
| """Returns the coordinates of a skew-symmetric matrix | |
| cfo, 2015/08/13 | |
| """ | |
| return numpy.array([R[2, 1], R[0, 2], R[1, 0]], dtype=numpy.float64) | |
| def first_order_rotation(rotvec): | |
| """First order approximation of a rotation: I + skew(rotvec) | |
| cfo, 2015/08/13 | |
| """ | |
| R = numpy.zeros((3, 3), dtype=numpy.float64) | |
| R[0, 0] = 1.0 | |
| R[1, 0] = rotvec[2] | |
| R[2, 0] = -rotvec[1] | |
| R[0, 1] = -rotvec[2] | |
| R[1, 1] = 1.0 | |
| R[2, 1] = rotvec[0] | |
| R[0, 2] = rotvec[1] | |
| R[1, 2] = -rotvec[0] | |
| R[2, 2] = 1.0 | |
| return R | |
| def axis_angle(axis, theta): | |
| """Compute a rotation matrix from an axis and an angle. | |
| Returns 3x3 Matrix. | |
| Is the same as transformations.rotation_matrix(theta, axis). | |
| cfo, 2015/08/13 | |
| """ | |
| if theta*theta > _EPS: | |
| wx = axis[0] | |
| wy = axis[1] | |
| wz = axis[2] | |
| costheta = numpy.cos(theta) | |
| sintheta = numpy.sin(theta) | |
| c_1 = 1.0 - costheta | |
| wx_sintheta = wx * sintheta | |
| wy_sintheta = wy * sintheta | |
| wz_sintheta = wz * sintheta | |
| C00 = c_1 * wx * wx | |
| C01 = c_1 * wx * wy | |
| C02 = c_1 * wx * wz | |
| C11 = c_1 * wy * wy | |
| C12 = c_1 * wy * wz | |
| C22 = c_1 * wz * wz | |
| R = numpy.zeros((3, 3), dtype=numpy.float64) | |
| R[0, 0] = costheta + C00 | |
| R[1, 0] = wz_sintheta + C01 | |
| R[2, 0] = -wy_sintheta + C02 | |
| R[0, 1] = -wz_sintheta + C01 | |
| R[1, 1] = costheta + C11 | |
| R[2, 1] = wx_sintheta + C12 | |
| R[0, 2] = wy_sintheta + C02 | |
| R[1, 2] = -wx_sintheta + C12 | |
| R[2, 2] = costheta + C22 | |
| return R | |
| else: | |
| return first_order_rotation(axis*theta) | |
| def expmap_so3(rotvec): | |
| """Exponential map at identity. | |
| Create a rotation from canonical coordinates using Rodrigues' formula. | |
| cfo, 2015/08/13 | |
| """ | |
| theta = numpy.linalg.norm(rotvec) | |
| axis = rotvec/theta | |
| return axis_angle(axis, theta) | |
| def logmap_so3(R): | |
| """Logmap at the identity. | |
| Returns canonical coordinates of rotation. | |
| cfo, 2015/08/13 | |
| """ | |
| R11 = R[0, 0] | |
| R12 = R[0, 1] | |
| R13 = R[0, 2] | |
| R21 = R[1, 0] | |
| R22 = R[1, 1] | |
| R23 = R[1, 2] | |
| R31 = R[2, 0] | |
| R32 = R[2, 1] | |
| R33 = R[2, 2] | |
| tr = numpy.trace(R) | |
| omega = numpy.empty((3,), dtype=numpy.float64) | |
| # when trace == -1, i.e., when theta = +-pi, +-3pi, +-5pi, we do something | |
| # special | |
| if(numpy.abs(tr + 1.0) < 1e-10): | |
| if(numpy.abs(R33 + 1.0) > 1e-10): | |
| omega = (numpy.pi / numpy.sqrt(2.0 + 2.0 * R33)) * \ | |
| numpy.array([R13, R23, 1.0+R33]) | |
| elif(numpy.abs(R22 + 1.0) > 1e-10): | |
| omega = (numpy.pi / numpy.sqrt(2.0 + 2.0 * R22)) * \ | |
| numpy.array([R12, 1.0+R22, R32]) | |
| else: | |
| omega = (numpy.pi / numpy.sqrt(2.0 + 2.0 * R11)) * \ | |
| numpy.array([1.0+R11, R21, R31]) | |
| else: | |
| magnitude = 1.0 | |
| tr_3 = tr - 3.0 | |
| if tr_3 < -1e-7: | |
| theta = numpy.arccos((tr - 1.0) / 2.0) | |
| magnitude = theta / (2.0 * numpy.sin(theta)) | |
| else: | |
| # when theta near 0, +-2pi, +-4pi, etc. (trace near 3.0) | |
| # use Taylor expansion: theta \approx 1/2-(t-3)/12 + O((t-3)^2) | |
| magnitude = 0.5 - tr_3 * tr_3 / 12.0 | |
| omega = magnitude * numpy.array([R32 - R23, R13 - R31, R21 - R12]) | |
| return omega | |
| def right_jacobian_so3(rotvec): | |
| """Right Jacobian for Exponential map in SO(3) | |
| Equation (10.86) and following equations in G.S. Chirikjian, "Stochastic | |
| Models, Information Theory, and Lie Groups", Volume 2, 2008. | |
| > expmap_so3(thetahat + omega) \approx expmap_so3(thetahat) * expmap_so3(Jr * omega) | |
| where Jr = right_jacobian_so3(thetahat); | |
| This maps a perturbation in the tangent space (omega) to a perturbation | |
| on the manifold (expmap_so3(Jr * omega)) | |
| cfo, 2015/08/13 | |
| """ | |
| theta2 = numpy.dot(rotvec, rotvec) | |
| if theta2 <= _EPS: | |
| return numpy.identity(3, dtype=numpy.float64) | |
| else: | |
| theta = numpy.sqrt(theta2) | |
| Y = skew(rotvec) / theta | |
| I_3x3 = numpy.identity(3, dtype=numpy.float64) | |
| J_r = I_3x3 - ((1.0 - numpy.cos(theta)) / theta) * Y + \ | |
| (1.0 - numpy.sin(theta) / theta) * numpy.dot(Y, Y) | |
| return J_r | |
| def S_inv_eulerZYX_body(euler_coordinates): | |
| """ Relates angular rates w to changes in eulerZYX coordinates. | |
| dot(euler) = S^-1(euler_coordinates) * omega | |
| Also called: rotation-rate matrix. (E in Lupton paper) | |
| cfo, 2015/08/13 | |
| """ | |
| y = euler_coordinates[1] | |
| z = euler_coordinates[2] | |
| E = numpy.zeros((3, 3)) | |
| E[0, 1] = numpy.sin(z)/numpy.cos(y) | |
| E[0, 2] = numpy.cos(z)/numpy.cos(y) | |
| E[1, 1] = numpy.cos(z) | |
| E[1, 2] = -numpy.sin(z) | |
| E[2, 0] = 1.0 | |
| E[2, 1] = numpy.sin(z)*numpy.sin(y)/numpy.cos(y) | |
| E[2, 2] = numpy.cos(z)*numpy.sin(y)/numpy.cos(y) | |
| return E | |
| def S_inv_eulerZYX_body_deriv(euler_coordinates, omega): | |
| """ Compute dE(euler_coordinates)*omega/deuler_coordinates | |
| cfo, 2015/08/13 | |
| """ | |
| y = euler_coordinates[1] | |
| z = euler_coordinates[2] | |
| """ | |
| w1 = omega[0]; w2 = omega[1]; w3 = omega[2] | |
| J = numpy.zeros((3,3)) | |
| J[0,0] = 0 | |
| J[0,1] = math.tan(y) / math.cos(y) * (math.sin(z) * w2 + math.cos(z) * w3) | |
| J[0,2] = w2/math.cos(y)*math.cos(z) - w3/math.cos(y)*math.sin(z) | |
| J[1,0] = 0 | |
| J[1,1] = 0 | |
| J[1,2] = -w2*math.sin(z) - w3*math.cos(z) | |
| J[2,0] = w1 | |
| J[2,1] = 1.0/math.cos(y)**2 * (w2 * math.sin(z) + w3 * math.cos(z)) | |
| J[2,2] = w2*math.tan(y)*math.cos(z) - w3*math.tan(y)*math.sin(z) | |
| """ | |
| # second version, x = psi, y = theta, z = phi | |
| # J_x = numpy.zeros((3,3)) | |
| J_y = numpy.zeros((3, 3)) | |
| J_z = numpy.zeros((3, 3)) | |
| # dE^-1/dtheta | |
| J_y[0, 1] = math.tan(y)/math.cos(y)*math.sin(z) | |
| J_y[0, 2] = math.tan(y)/math.cos(y)*math.cos(z) | |
| J_y[2, 1] = math.sin(z)/(math.cos(y))**2 | |
| J_y[2, 2] = math.cos(z)/(math.cos(y))**2 | |
| # dE^-1/dphi | |
| J_z[0, 1] = math.cos(z)/math.cos(y) | |
| J_z[0, 2] = -math.sin(z)/math.cos(y) | |
| J_z[1, 1] = -math.sin(z) | |
| J_z[1, 2] = -math.cos(z) | |
| J_z[2, 1] = math.cos(z)*math.tan(y) | |
| J_z[2, 2] = -math.sin(z)*math.tan(y) | |
| J = numpy.zeros((3, 3)) | |
| J[:, 1] = numpy.dot(J_y, omega) | |
| J[:, 2] = numpy.dot(J_z, omega) | |
| return J | |
| def identity_matrix(): | |
| """Return 4x4 identity/unit matrix. | |
| >>> I = identity_matrix() | |
| >>> numpy.allclose(I, numpy.dot(I, I)) | |
| True | |
| >>> numpy.sum(I), numpy.trace(I) | |
| (4.0, 4.0) | |
| >>> numpy.allclose(I, numpy.identity(4, dtype=numpy.float64)) | |
| True | |
| """ | |
| return numpy.identity(4, dtype=numpy.float64) | |
| def translation_matrix(direction): | |
| """Return matrix to translate by direction vector. | |
| >>> v = numpy.random.random(3) - 0.5 | |
| >>> numpy.allclose(v, translation_matrix(v)[:3, 3]) | |
| True | |
| """ | |
| M = numpy.identity(4) | |
| M[:3, 3] = direction[:3] | |
| return M | |
| def translation_from_matrix(matrix): | |
| """Return translation vector from translation matrix. | |
| >>> v0 = numpy.random.random(3) - 0.5 | |
| >>> v1 = translation_from_matrix(translation_matrix(v0)) | |
| >>> numpy.allclose(v0, v1) | |
| True | |
| """ | |
| return numpy.array(matrix, copy=False)[:3, 3].copy() | |
| def convert_3x3_to_4x4(matrix_3x3): | |
| M = numpy.identity(4) | |
| M[:3, :3] = matrix_3x3 | |
| return M | |
| def reflection_matrix(point, normal): | |
| """Return matrix to mirror at plane defined by point and normal vector. | |
| >>> v0 = numpy.random.random(4) - 0.5 | |
| >>> v0[3] = 1.0 | |
| >>> v1 = numpy.random.random(3) - 0.5 | |
| >>> R = reflection_matrix(v0, v1) | |
| >>> numpy.allclose(2., numpy.trace(R)) | |
| True | |
| >>> numpy.allclose(v0, numpy.dot(R, v0)) | |
| True | |
| >>> v2 = v0.copy() | |
| >>> v2[:3] += v1 | |
| >>> v3 = v0.copy() | |
| >>> v2[:3] -= v1 | |
| >>> numpy.allclose(v2, numpy.dot(R, v3)) | |
| True | |
| """ | |
| normal = unit_vector(normal[:3]) | |
| M = numpy.identity(4) | |
| M[:3, :3] -= 2.0 * numpy.outer(normal, normal) | |
| M[:3, 3] = (2.0 * numpy.dot(point[:3], normal)) * normal | |
| return M | |
| def reflection_from_matrix(matrix): | |
| """Return mirror plane point and normal vector from reflection matrix. | |
| >>> v0 = numpy.random.random(3) - 0.5 | |
| >>> v1 = numpy.random.random(3) - 0.5 | |
| >>> M0 = reflection_matrix(v0, v1) | |
| >>> point, normal = reflection_from_matrix(M0) | |
| >>> M1 = reflection_matrix(point, normal) | |
| >>> is_same_transform(M0, M1) | |
| True | |
| """ | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False) | |
| # normal: unit eigenvector corresponding to eigenvalue -1 | |
| l, V = numpy.linalg.eig(M[:3, :3]) | |
| i = numpy.where(abs(numpy.real(l) + 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no unit eigenvector corresponding to eigenvalue -1") | |
| normal = numpy.real(V[:, i[0]]).squeeze() | |
| # point: any unit eigenvector corresponding to eigenvalue 1 | |
| l, V = numpy.linalg.eig(M) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no unit eigenvector corresponding to eigenvalue 1") | |
| point = numpy.real(V[:, i[-1]]).squeeze() | |
| point /= point[3] | |
| return point, normal | |
| def rotation_matrix(angle, direction, point=None): | |
| """Return matrix to rotate about axis defined by point and direction. | |
| >>> angle = (random.random() - 0.5) * (2*math.pi) | |
| >>> direc = numpy.random.random(3) - 0.5 | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> R0 = rotation_matrix(angle, direc, point) | |
| >>> R1 = rotation_matrix(angle-2*math.pi, direc, point) | |
| >>> is_same_transform(R0, R1) | |
| True | |
| >>> R0 = rotation_matrix(angle, direc, point) | |
| >>> R1 = rotation_matrix(-angle, -direc, point) | |
| >>> is_same_transform(R0, R1) | |
| True | |
| >>> I = numpy.identity(4, numpy.float64) | |
| >>> numpy.allclose(I, rotation_matrix(math.pi*2, direc)) | |
| True | |
| >>> numpy.allclose(2., numpy.trace(rotation_matrix(math.pi/2, | |
| ... direc, point))) | |
| True | |
| """ | |
| sina = math.sin(angle) | |
| cosa = math.cos(angle) | |
| direction = unit_vector(direction[:3]) | |
| # rotation matrix around unit vector | |
| R = numpy.array(((cosa, 0.0, 0.0), | |
| (0.0, cosa, 0.0), | |
| (0.0, 0.0, cosa)), dtype=numpy.float64) | |
| R += numpy.outer(direction, direction) * (1.0 - cosa) | |
| direction *= sina | |
| R += numpy.array(((0.0, -direction[2], direction[1]), | |
| (direction[2], 0.0, -direction[0]), | |
| (-direction[1], direction[0], 0.0)), | |
| dtype=numpy.float64) | |
| M = numpy.identity(4) | |
| M[:3, :3] = R | |
| if point is not None: | |
| # rotation not around origin | |
| point = numpy.array(point[:3], dtype=numpy.float64, copy=False) | |
| M[:3, 3] = point - numpy.dot(R, point) | |
| return M | |
| def rotation_from_matrix(matrix): | |
| """Return rotation angle and axis from rotation matrix. | |
| >>> angle = (random.random() - 0.5) * (2*math.pi) | |
| >>> direc = numpy.random.random(3) - 0.5 | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> R0 = rotation_matrix(angle, direc, point) | |
| >>> angle, direc, point = rotation_from_matrix(R0) | |
| >>> R1 = rotation_matrix(angle, direc, point) | |
| >>> is_same_transform(R0, R1) | |
| True | |
| """ | |
| R = numpy.array(matrix, dtype=numpy.float64, copy=False) | |
| R33 = R[:3, :3] | |
| # direction: unit eigenvector of R33 corresponding to eigenvalue of 1 | |
| l, W = numpy.linalg.eig(R33.T) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no unit eigenvector corresponding to eigenvalue 1") | |
| direction = numpy.real(W[:, i[-1]]).squeeze() | |
| # point: unit eigenvector of R33 corresponding to eigenvalue of 1 | |
| l, Q = numpy.linalg.eig(R) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no unit eigenvector corresponding to eigenvalue 1") | |
| point = numpy.real(Q[:, i[-1]]).squeeze() | |
| point /= point[3] | |
| # rotation angle depending on direction | |
| cosa = (numpy.trace(R33) - 1.0) / 2.0 | |
| if abs(direction[2]) > 1e-8: | |
| sina = (R[1, 0] + (cosa-1.0)*direction[0]*direction[1]) / direction[2] | |
| elif abs(direction[1]) > 1e-8: | |
| sina = (R[0, 2] + (cosa-1.0)*direction[0]*direction[2]) / direction[1] | |
| else: | |
| sina = (R[2, 1] + (cosa-1.0)*direction[1]*direction[2]) / direction[0] | |
| angle = math.atan2(sina, cosa) | |
| return angle, direction, point | |
| def scale_matrix(factor, origin=None, direction=None): | |
| """Return matrix to scale by factor around origin in direction. | |
| Use factor -1 for point symmetry. | |
| >>> v = (numpy.random.rand(4, 5) - 0.5) * 20.0 | |
| >>> v[3] = 1.0 | |
| >>> S = scale_matrix(-1.234) | |
| >>> numpy.allclose(numpy.dot(S, v)[:3], -1.234*v[:3]) | |
| True | |
| >>> factor = random.random() * 10 - 5 | |
| >>> origin = numpy.random.random(3) - 0.5 | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> S = scale_matrix(factor, origin) | |
| >>> S = scale_matrix(factor, origin, direct) | |
| """ | |
| if direction is None: | |
| # uniform scaling | |
| M = numpy.array(((factor, 0.0, 0.0, 0.0), | |
| (0.0, factor, 0.0, 0.0), | |
| (0.0, 0.0, factor, 0.0), | |
| (0.0, 0.0, 0.0, 1.0)), dtype=numpy.float64) | |
| if origin is not None: | |
| M[:3, 3] = origin[:3] | |
| M[:3, 3] *= 1.0 - factor | |
| else: | |
| # nonuniform scaling | |
| direction = unit_vector(direction[:3]) | |
| factor = 1.0 - factor | |
| M = numpy.identity(4) | |
| M[:3, :3] -= factor * numpy.outer(direction, direction) | |
| if origin is not None: | |
| M[:3, 3] = (factor * numpy.dot(origin[:3], direction)) * direction | |
| return M | |
| def scale_from_matrix(matrix): | |
| """Return scaling factor, origin and direction from scaling matrix. | |
| >>> factor = random.random() * 10 - 5 | |
| >>> origin = numpy.random.random(3) - 0.5 | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> S0 = scale_matrix(factor, origin) | |
| >>> factor, origin, direction = scale_from_matrix(S0) | |
| >>> S1 = scale_matrix(factor, origin, direction) | |
| >>> is_same_transform(S0, S1) | |
| True | |
| >>> S0 = scale_matrix(factor, origin, direct) | |
| >>> factor, origin, direction = scale_from_matrix(S0) | |
| >>> S1 = scale_matrix(factor, origin, direction) | |
| >>> is_same_transform(S0, S1) | |
| True | |
| """ | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False) | |
| M33 = M[:3, :3] | |
| factor = numpy.trace(M33) - 2.0 | |
| try: | |
| # direction: unit eigenvector corresponding to eigenvalue factor | |
| l, V = numpy.linalg.eig(M33) | |
| i = numpy.where(abs(numpy.real(l) - factor) < 1e-8)[0][0] | |
| direction = numpy.real(V[:, i]).squeeze() | |
| direction /= vector_norm(direction) | |
| except IndexError: | |
| # uniform scaling | |
| factor = (factor + 2.0) / 3.0 | |
| direction = None | |
| # origin: any eigenvector corresponding to eigenvalue 1 | |
| l, V = numpy.linalg.eig(M) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no eigenvector corresponding to eigenvalue 1") | |
| origin = numpy.real(V[:, i[-1]]).squeeze() | |
| origin /= origin[3] | |
| return factor, origin, direction | |
| def projection_matrix(point, normal, direction=None, | |
| perspective=None, pseudo=False): | |
| """Return matrix to project onto plane defined by point and normal. | |
| Using either perspective point, projection direction, or none of both. | |
| If pseudo is True, perspective projections will preserve relative depth | |
| such that Perspective = dot(Orthogonal, PseudoPerspective). | |
| >>> P = projection_matrix((0, 0, 0), (1, 0, 0)) | |
| >>> numpy.allclose(P[1:, 1:], numpy.identity(4)[1:, 1:]) | |
| True | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> normal = numpy.random.random(3) - 0.5 | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> persp = numpy.random.random(3) - 0.5 | |
| >>> P0 = projection_matrix(point, normal) | |
| >>> P1 = projection_matrix(point, normal, direction=direct) | |
| >>> P2 = projection_matrix(point, normal, perspective=persp) | |
| >>> P3 = projection_matrix(point, normal, perspective=persp, pseudo=True) | |
| >>> is_same_transform(P2, numpy.dot(P0, P3)) | |
| True | |
| >>> P = projection_matrix((3, 0, 0), (1, 1, 0), (1, 0, 0)) | |
| >>> v0 = (numpy.random.rand(4, 5) - 0.5) * 20.0 | |
| >>> v0[3] = 1.0 | |
| >>> v1 = numpy.dot(P, v0) | |
| >>> numpy.allclose(v1[1], v0[1]) | |
| True | |
| >>> numpy.allclose(v1[0], 3.0-v1[1]) | |
| True | |
| """ | |
| M = numpy.identity(4) | |
| point = numpy.array(point[:3], dtype=numpy.float64, copy=False) | |
| normal = unit_vector(normal[:3]) | |
| if perspective is not None: | |
| # perspective projection | |
| perspective = numpy.array(perspective[:3], dtype=numpy.float64, | |
| copy=False) | |
| M[0, 0] = M[1, 1] = M[2, 2] = numpy.dot(perspective-point, normal) | |
| M[:3, :3] -= numpy.outer(perspective, normal) | |
| if pseudo: | |
| # preserve relative depth | |
| M[:3, :3] -= numpy.outer(normal, normal) | |
| M[:3, 3] = numpy.dot(point, normal) * (perspective+normal) | |
| else: | |
| M[:3, 3] = numpy.dot(point, normal) * perspective | |
| M[3, :3] = -normal | |
| M[3, 3] = numpy.dot(perspective, normal) | |
| elif direction is not None: | |
| # parallel projection | |
| direction = numpy.array(direction[:3], dtype=numpy.float64, copy=False) | |
| scale = numpy.dot(direction, normal) | |
| M[:3, :3] -= numpy.outer(direction, normal) / scale | |
| M[:3, 3] = direction * (numpy.dot(point, normal) / scale) | |
| else: | |
| # orthogonal projection | |
| M[:3, :3] -= numpy.outer(normal, normal) | |
| M[:3, 3] = numpy.dot(point, normal) * normal | |
| return M | |
| def projection_from_matrix(matrix, pseudo=False): | |
| """Return projection plane and perspective point from projection matrix. | |
| Return values are same as arguments for projection_matrix function: | |
| point, normal, direction, perspective, and pseudo. | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> normal = numpy.random.random(3) - 0.5 | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> persp = numpy.random.random(3) - 0.5 | |
| >>> P0 = projection_matrix(point, normal) | |
| >>> result = projection_from_matrix(P0) | |
| >>> P1 = projection_matrix(*result) | |
| >>> is_same_transform(P0, P1) | |
| True | |
| >>> P0 = projection_matrix(point, normal, direct) | |
| >>> result = projection_from_matrix(P0) | |
| >>> P1 = projection_matrix(*result) | |
| >>> is_same_transform(P0, P1) | |
| True | |
| >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=False) | |
| >>> result = projection_from_matrix(P0, pseudo=False) | |
| >>> P1 = projection_matrix(*result) | |
| >>> is_same_transform(P0, P1) | |
| True | |
| >>> P0 = projection_matrix(point, normal, perspective=persp, pseudo=True) | |
| >>> result = projection_from_matrix(P0, pseudo=True) | |
| >>> P1 = projection_matrix(*result) | |
| >>> is_same_transform(P0, P1) | |
| True | |
| """ | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False) | |
| M33 = M[:3, :3] | |
| l, V = numpy.linalg.eig(M) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not pseudo and len(i): | |
| # point: any eigenvector corresponding to eigenvalue 1 | |
| point = numpy.real(V[:, i[-1]]).squeeze() | |
| point /= point[3] | |
| # direction: unit eigenvector corresponding to eigenvalue 0 | |
| l, V = numpy.linalg.eig(M33) | |
| i = numpy.where(abs(numpy.real(l)) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no eigenvector corresponding to eigenvalue 0") | |
| direction = numpy.real(V[:, i[0]]).squeeze() | |
| direction /= vector_norm(direction) | |
| # normal: unit eigenvector of M33.T corresponding to eigenvalue 0 | |
| l, V = numpy.linalg.eig(M33.T) | |
| i = numpy.where(abs(numpy.real(l)) < 1e-8)[0] | |
| if len(i): | |
| # parallel projection | |
| normal = numpy.real(V[:, i[0]]).squeeze() | |
| normal /= vector_norm(normal) | |
| return point, normal, direction, None, False | |
| else: | |
| # orthogonal projection, where normal equals direction vector | |
| return point, direction, None, None, False | |
| else: | |
| # perspective projection | |
| i = numpy.where(abs(numpy.real(l)) > 1e-8)[0] | |
| if not len(i): | |
| raise ValueError( | |
| "no eigenvector not corresponding to eigenvalue 0") | |
| point = numpy.real(V[:, i[-1]]).squeeze() | |
| point /= point[3] | |
| normal = - M[3, :3] | |
| perspective = M[:3, 3] / numpy.dot(point[:3], normal) | |
| if pseudo: | |
| perspective -= normal | |
| return point, normal, None, perspective, pseudo | |
| def clip_matrix(left, right, bottom, top, near, far, perspective=False): | |
| """Return matrix to obtain normalized device coordinates from frustrum. | |
| The frustrum bounds are axis-aligned along x (left, right), | |
| y (bottom, top) and z (near, far). | |
| Normalized device coordinates are in range [-1, 1] if coordinates are | |
| inside the frustrum. | |
| If perspective is True the frustrum is a truncated pyramid with the | |
| perspective point at origin and direction along z axis, otherwise an | |
| orthographic canonical view volume (a box). | |
| Homogeneous coordinates transformed by the perspective clip matrix | |
| need to be dehomogenized (devided by w coordinate). | |
| >>> frustrum = numpy.random.rand(6) | |
| >>> frustrum[1] += frustrum[0] | |
| >>> frustrum[3] += frustrum[2] | |
| >>> frustrum[5] += frustrum[4] | |
| >>> M = clip_matrix(*frustrum, perspective=False) | |
| >>> numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]) | |
| array([-1., -1., -1., 1.]) | |
| >>> numpy.dot(M, [frustrum[1], frustrum[3], frustrum[5], 1.0]) | |
| array([ 1., 1., 1., 1.]) | |
| >>> M = clip_matrix(*frustrum, perspective=True) | |
| >>> v = numpy.dot(M, [frustrum[0], frustrum[2], frustrum[4], 1.0]) | |
| >>> v / v[3] | |
| array([-1., -1., -1., 1.]) | |
| >>> v = numpy.dot(M, [frustrum[1], frustrum[3], frustrum[4], 1.0]) | |
| >>> v / v[3] | |
| array([ 1., 1., -1., 1.]) | |
| """ | |
| if left >= right or bottom >= top or near >= far: | |
| raise ValueError("invalid frustrum") | |
| if perspective: | |
| if near <= _EPS: | |
| raise ValueError("invalid frustrum: near <= 0") | |
| t = 2.0 * near | |
| M = ((-t/(right-left), 0.0, (right+left)/(right-left), 0.0), | |
| (0.0, -t/(top-bottom), (top+bottom)/(top-bottom), 0.0), | |
| (0.0, 0.0, -(far+near)/(far-near), t*far/(far-near)), | |
| (0.0, 0.0, -1.0, 0.0)) | |
| else: | |
| M = ((2.0/(right-left), 0.0, 0.0, (right+left)/(left-right)), | |
| (0.0, 2.0/(top-bottom), 0.0, (top+bottom)/(bottom-top)), | |
| (0.0, 0.0, 2.0/(far-near), (far+near)/(near-far)), | |
| (0.0, 0.0, 0.0, 1.0)) | |
| return numpy.array(M, dtype=numpy.float64) | |
| def shear_matrix(angle, direction, point, normal): | |
| """Return matrix to shear by angle along direction vector on shear plane. | |
| The shear plane is defined by a point and normal vector. The direction | |
| vector must be orthogonal to the plane's normal vector. | |
| A point P is transformed by the shear matrix into P" such that | |
| the vector P-P" is parallel to the direction vector and its extent is | |
| given by the angle of P-P'-P", where P' is the orthogonal projection | |
| of P onto the shear plane. | |
| >>> angle = (random.random() - 0.5) * 4*math.pi | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> normal = numpy.cross(direct, numpy.random.random(3)) | |
| >>> S = shear_matrix(angle, direct, point, normal) | |
| >>> numpy.allclose(1.0, numpy.linalg.det(S)) | |
| True | |
| """ | |
| normal = unit_vector(normal[:3]) | |
| direction = unit_vector(direction[:3]) | |
| if abs(numpy.dot(normal, direction)) > 1e-6: | |
| raise ValueError("direction and normal vectors are not orthogonal") | |
| angle = math.tan(angle) | |
| M = numpy.identity(4) | |
| M[:3, :3] += angle * numpy.outer(direction, normal) | |
| M[:3, 3] = -angle * numpy.dot(point[:3], normal) * direction | |
| return M | |
| def shear_from_matrix(matrix): | |
| """Return shear angle, direction and plane from shear matrix. | |
| >>> angle = (random.random() - 0.5) * 4*math.pi | |
| >>> direct = numpy.random.random(3) - 0.5 | |
| >>> point = numpy.random.random(3) - 0.5 | |
| >>> normal = numpy.cross(direct, numpy.random.random(3)) | |
| >>> S0 = shear_matrix(angle, direct, point, normal) | |
| >>> angle, direct, point, normal = shear_from_matrix(S0) | |
| >>> S1 = shear_matrix(angle, direct, point, normal) | |
| >>> is_same_transform(S0, S1) | |
| True | |
| """ | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False) | |
| M33 = M[:3, :3] | |
| # normal: cross independent eigenvectors corresponding to the eigenvalue 1 | |
| l, V = numpy.linalg.eig(M33) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-4)[0] | |
| if len(i) < 2: | |
| raise ValueError("No two linear independent eigenvectors found %s" % l) | |
| V = numpy.real(V[:, i]).squeeze().T | |
| lenorm = -1.0 | |
| for i0, i1 in ((0, 1), (0, 2), (1, 2)): | |
| n = numpy.cross(V[i0], V[i1]) | |
| l = vector_norm(n) | |
| if l > lenorm: | |
| lenorm = l | |
| normal = n | |
| normal /= lenorm | |
| # direction and angle | |
| direction = numpy.dot(M33 - numpy.identity(3), normal) | |
| angle = vector_norm(direction) | |
| direction /= angle | |
| angle = math.atan(angle) | |
| # point: eigenvector corresponding to eigenvalue 1 | |
| l, V = numpy.linalg.eig(M) | |
| i = numpy.where(abs(numpy.real(l) - 1.0) < 1e-8)[0] | |
| if not len(i): | |
| raise ValueError("no eigenvector corresponding to eigenvalue 1") | |
| point = numpy.real(V[:, i[-1]]).squeeze() | |
| point /= point[3] | |
| return angle, direction, point, normal | |
| def decompose_matrix(matrix): | |
| """Return sequence of transformations from transformation matrix. | |
| matrix : array_like | |
| Non-degenerative homogeneous transformation matrix | |
| Return tuple of: | |
| scale : vector of 3 scaling factors | |
| shear : list of shear factors for x-y, x-z, y-z axes | |
| angles : list of Euler angles about static x, y, z axes | |
| translate : translation vector along x, y, z axes | |
| perspective : perspective partition of matrix | |
| Raise ValueError if matrix is of wrong type or degenerative. | |
| >>> T0 = translation_matrix((1, 2, 3)) | |
| >>> scale, shear, angles, trans, persp = decompose_matrix(T0) | |
| >>> T1 = translation_matrix(trans) | |
| >>> numpy.allclose(T0, T1) | |
| True | |
| >>> S = scale_matrix(0.123) | |
| >>> scale, shear, angles, trans, persp = decompose_matrix(S) | |
| >>> scale[0] | |
| 0.123 | |
| >>> R0 = euler_matrix(1, 2, 3) | |
| >>> scale, shear, angles, trans, persp = decompose_matrix(R0) | |
| >>> R1 = euler_matrix(*angles) | |
| >>> numpy.allclose(R0, R1) | |
| True | |
| """ | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=True).T | |
| if abs(M[3, 3]) < _EPS: | |
| raise ValueError("M[3, 3] is zero") | |
| M /= M[3, 3] | |
| P = M.copy() | |
| P[:, 3] = 0, 0, 0, 1 | |
| if not numpy.linalg.det(P): | |
| raise ValueError("Matrix is singular") | |
| scale = numpy.zeros((3, ), dtype=numpy.float64) | |
| shear = [0, 0, 0] | |
| angles = [0, 0, 0] | |
| if any(abs(M[:3, 3]) > _EPS): | |
| perspective = numpy.dot(M[:, 3], numpy.linalg.inv(P.T)) | |
| M[:, 3] = 0, 0, 0, 1 | |
| else: | |
| perspective = numpy.array((0, 0, 0, 1), dtype=numpy.float64) | |
| translate = M[3, :3].copy() | |
| M[3, :3] = 0 | |
| row = M[:3, :3].copy() | |
| scale[0] = vector_norm(row[0]) | |
| row[0] /= scale[0] | |
| shear[0] = numpy.dot(row[0], row[1]) | |
| row[1] -= row[0] * shear[0] | |
| scale[1] = vector_norm(row[1]) | |
| row[1] /= scale[1] | |
| shear[0] /= scale[1] | |
| shear[1] = numpy.dot(row[0], row[2]) | |
| row[2] -= row[0] * shear[1] | |
| shear[2] = numpy.dot(row[1], row[2]) | |
| row[2] -= row[1] * shear[2] | |
| scale[2] = vector_norm(row[2]) | |
| row[2] /= scale[2] | |
| shear[1:] /= scale[2] | |
| if numpy.dot(row[0], numpy.cross(row[1], row[2])) < 0: | |
| scale *= -1 | |
| row *= -1 | |
| angles[1] = math.asin(-row[0, 2]) | |
| if math.cos(angles[1]): | |
| angles[0] = math.atan2(row[1, 2], row[2, 2]) | |
| angles[2] = math.atan2(row[0, 1], row[0, 0]) | |
| else: | |
| #angles[0] = math.atan2(row[1, 0], row[1, 1]) | |
| angles[0] = math.atan2(-row[2, 1], row[1, 1]) | |
| angles[2] = 0.0 | |
| return scale, shear, angles, translate, perspective | |
| def compose_matrix(scale=None, shear=None, angles=None, translate=None, | |
| perspective=None): | |
| """Return transformation matrix from sequence of transformations. | |
| This is the inverse of the decompose_matrix function. | |
| Sequence of transformations: | |
| scale : vector of 3 scaling factors | |
| shear : list of shear factors for x-y, x-z, y-z axes | |
| angles : list of Euler angles about static x, y, z axes | |
| translate : translation vector along x, y, z axes | |
| perspective : perspective partition of matrix | |
| >>> scale = numpy.random.random(3) - 0.5 | |
| >>> shear = numpy.random.random(3) - 0.5 | |
| >>> angles = (numpy.random.random(3) - 0.5) * (2*math.pi) | |
| >>> trans = numpy.random.random(3) - 0.5 | |
| >>> persp = numpy.random.random(4) - 0.5 | |
| >>> M0 = compose_matrix(scale, shear, angles, trans, persp) | |
| >>> result = decompose_matrix(M0) | |
| >>> M1 = compose_matrix(*result) | |
| >>> is_same_transform(M0, M1) | |
| True | |
| """ | |
| M = numpy.identity(4) | |
| if perspective is not None: | |
| P = numpy.identity(4) | |
| P[3, :] = perspective[:4] | |
| M = numpy.dot(M, P) | |
| if translate is not None: | |
| T = numpy.identity(4) | |
| T[:3, 3] = translate[:3] | |
| M = numpy.dot(M, T) | |
| if angles is not None: | |
| R = euler_matrix(angles[0], angles[1], angles[2], 'sxyz') | |
| M = numpy.dot(M, R) | |
| if shear is not None: | |
| Z = numpy.identity(4) | |
| Z[1, 2] = shear[2] | |
| Z[0, 2] = shear[1] | |
| Z[0, 1] = shear[0] | |
| M = numpy.dot(M, Z) | |
| if scale is not None: | |
| S = numpy.identity(4) | |
| S[0, 0] = scale[0] | |
| S[1, 1] = scale[1] | |
| S[2, 2] = scale[2] | |
| M = numpy.dot(M, S) | |
| M /= M[3, 3] | |
| return M | |
| def orthogonalization_matrix(lengths, angles): | |
| """Return orthogonalization matrix for crystallographic cell coordinates. | |
| Angles are expected in degrees. | |
| The de-orthogonalization matrix is the inverse. | |
| >>> O = orthogonalization_matrix((10., 10., 10.), (90., 90., 90.)) | |
| >>> numpy.allclose(O[:3, :3], numpy.identity(3, float) * 10) | |
| True | |
| >>> O = orthogonalization_matrix([9.8, 12.0, 15.5], [87.2, 80.7, 69.7]) | |
| >>> numpy.allclose(numpy.sum(O), 43.063229) | |
| True | |
| """ | |
| a, b, c = lengths | |
| angles = numpy.radians(angles) | |
| sina, sinb, _ = numpy.sin(angles) | |
| cosa, cosb, cosg = numpy.cos(angles) | |
| co = (cosa * cosb - cosg) / (sina * sinb) | |
| return numpy.array(( | |
| (a*sinb*math.sqrt(1.0-co*co), 0.0, 0.0, 0.0), | |
| (-a*sinb*co, b*sina, 0.0, 0.0), | |
| (a*cosb, b*cosa, c, 0.0), | |
| (0.0, 0.0, 0.0, 1.0)), | |
| dtype=numpy.float64) | |
| def superimposition_matrix(v0, v1, scaling=False, usesvd=True): | |
| """Return matrix to transform given vector set into second vector set. | |
| v0 and v1 are shape (3, \*) or (4, \*) arrays of at least 3 vectors. | |
| If usesvd is True, the weighted sum of squared deviations (RMSD) is | |
| minimized according to the algorithm by W. Kabsch [8]. Otherwise the | |
| quaternion based algorithm by B. Horn [9] is used (slower when using | |
| this Python implementation). | |
| The returned matrix performs rotation, translation and uniform scaling | |
| (if specified). | |
| >>> v0 = numpy.random.rand(3, 10) | |
| >>> M = superimposition_matrix(v0, v0) | |
| >>> numpy.allclose(M, numpy.identity(4)) | |
| True | |
| >>> R = random_rotation_matrix(numpy.random.random(3)) | |
| >>> v0 = ((1,0,0), (0,1,0), (0,0,1), (1,1,1)) | |
| >>> v1 = numpy.dot(R, v0) | |
| >>> M = superimposition_matrix(v0, v1) | |
| >>> numpy.allclose(v1, numpy.dot(M, v0)) | |
| True | |
| >>> v0 = (numpy.random.rand(4, 100) - 0.5) * 20.0 | |
| >>> v0[3] = 1.0 | |
| >>> v1 = numpy.dot(R, v0) | |
| >>> M = superimposition_matrix(v0, v1) | |
| >>> numpy.allclose(v1, numpy.dot(M, v0)) | |
| True | |
| >>> S = scale_matrix(random.random()) | |
| >>> T = translation_matrix(numpy.random.random(3)-0.5) | |
| >>> M = concatenate_matrices(T, R, S) | |
| >>> v1 = numpy.dot(M, v0) | |
| >>> v0[:3] += numpy.random.normal(0.0, 1e-9, 300).reshape(3, -1) | |
| >>> M = superimposition_matrix(v0, v1, scaling=True) | |
| >>> numpy.allclose(v1, numpy.dot(M, v0)) | |
| True | |
| >>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False) | |
| >>> numpy.allclose(v1, numpy.dot(M, v0)) | |
| True | |
| >>> v = numpy.empty((4, 100, 3), dtype=numpy.float64) | |
| >>> v[:, :, 0] = v0 | |
| >>> M = superimposition_matrix(v0, v1, scaling=True, usesvd=False) | |
| >>> numpy.allclose(v1, numpy.dot(M, v[:, :, 0])) | |
| True | |
| """ | |
| v0 = numpy.array(v0, dtype=numpy.float64, copy=False)[:3] | |
| v1 = numpy.array(v1, dtype=numpy.float64, copy=False)[:3] | |
| if v0.shape != v1.shape or v0.shape[1] < 3: | |
| raise ValueError("Vector sets are of wrong shape or type.") | |
| # move centroids to origin | |
| t0 = numpy.mean(v0, axis=1) | |
| t1 = numpy.mean(v1, axis=1) | |
| v0 = v0 - t0.reshape(3, 1) | |
| v1 = v1 - t1.reshape(3, 1) | |
| if usesvd: | |
| # Singular Value Decomposition of covariance matrix | |
| u, s, vh = numpy.linalg.svd(numpy.dot(v1, v0.T)) | |
| # rotation matrix from SVD orthonormal bases | |
| R = numpy.dot(u, vh) | |
| if numpy.linalg.det(R) < 0.0: | |
| # R does not constitute right handed system | |
| R -= numpy.outer(u[:, 2], vh[2, :]*2.0) | |
| s[-1] *= -1.0 | |
| # homogeneous transformation matrix | |
| M = numpy.identity(4) | |
| M[:3, :3] = R | |
| else: | |
| # compute symmetric matrix N | |
| xx, yy, zz = numpy.sum(v0 * v1, axis=1) | |
| xy, yz, zx = numpy.sum(v0 * numpy.roll(v1, -1, axis=0), axis=1) | |
| xz, yx, zy = numpy.sum(v0 * numpy.roll(v1, -2, axis=0), axis=1) | |
| N = ((xx+yy+zz, yz-zy, zx-xz, xy-yx), | |
| (yz-zy, xx-yy-zz, xy+yx, zx+xz), | |
| (zx-xz, xy+yx, -xx+yy-zz, yz+zy), | |
| (xy-yx, zx+xz, yz+zy, -xx-yy+zz)) | |
| # quaternion: eigenvector corresponding to most positive eigenvalue | |
| l, V = numpy.linalg.eig(N) | |
| q = V[:, numpy.argmax(l)] | |
| q /= vector_norm(q) # unit quaternion | |
| q = numpy.roll(q, -1) # move w component to end | |
| # homogeneous transformation matrix | |
| M = quaternion_matrix(q) | |
| # scale: ratio of rms deviations from centroid | |
| if scaling: | |
| v0 *= v0 | |
| v1 *= v1 | |
| M[:3, :3] *= math.sqrt(numpy.sum(v1) / numpy.sum(v0)) | |
| # translation | |
| M[:3, 3] = t1 | |
| T = numpy.identity(4) | |
| T[:3, 3] = -t0 | |
| M = numpy.dot(M, T) | |
| return M | |
| def euler_matrix(ai, aj, ak, axes='sxyz'): | |
| """Return homogeneous rotation matrix from Euler angles and axis sequence. | |
| ai, aj, ak : Euler's roll, pitch and yaw angles | |
| axes : One of 24 axis sequences as string or encoded tuple | |
| >>> R = euler_matrix(1, 2, 3, 'syxz') | |
| >>> numpy.allclose(numpy.sum(R[0]), -1.34786452) | |
| True | |
| >>> R = euler_matrix(1, 2, 3, (0, 1, 0, 1)) | |
| >>> numpy.allclose(numpy.sum(R[0]), -0.383436184) | |
| True | |
| >>> ai, aj, ak = (4.0*math.pi) * (numpy.random.random(3) - 0.5) | |
| >>> for axes in _AXES2TUPLE.keys(): | |
| ... R = euler_matrix(ai, aj, ak, axes) | |
| >>> for axes in _TUPLE2AXES.keys(): | |
| ... R = euler_matrix(ai, aj, ak, axes) | |
| """ | |
| try: | |
| firstaxis, parity, repetition, frame = _AXES2TUPLE[axes] | |
| except (AttributeError, KeyError): | |
| _ = _TUPLE2AXES[axes] | |
| firstaxis, parity, repetition, frame = axes | |
| i = firstaxis | |
| j = _NEXT_AXIS[i+parity] | |
| k = _NEXT_AXIS[i-parity+1] | |
| if frame: | |
| ai, ak = ak, ai | |
| if parity: | |
| ai, aj, ak = -ai, -aj, -ak | |
| si, sj, sk = math.sin(ai), math.sin(aj), math.sin(ak) | |
| ci, cj, ck = math.cos(ai), math.cos(aj), math.cos(ak) | |
| cc, cs = ci*ck, ci*sk | |
| sc, ss = si*ck, si*sk | |
| M = numpy.identity(4) | |
| if repetition: | |
| M[i, i] = cj | |
| M[i, j] = sj*si | |
| M[i, k] = sj*ci | |
| M[j, i] = sj*sk | |
| M[j, j] = -cj*ss+cc | |
| M[j, k] = -cj*cs-sc | |
| M[k, i] = -sj*ck | |
| M[k, j] = cj*sc+cs | |
| M[k, k] = cj*cc-ss | |
| else: | |
| M[i, i] = cj*ck | |
| M[i, j] = sj*sc-cs | |
| M[i, k] = sj*cc+ss | |
| M[j, i] = cj*sk | |
| M[j, j] = sj*ss+cc | |
| M[j, k] = sj*cs-sc | |
| M[k, i] = -sj | |
| M[k, j] = cj*si | |
| M[k, k] = cj*ci | |
| return M | |
| def euler_from_matrix(matrix, axes='sxyz'): | |
| """Return Euler angles from rotation matrix for specified axis sequence. | |
| axes : One of 24 axis sequences as string or encoded tuple | |
| Note that many Euler angle triplets can describe one matrix. | |
| >>> R0 = euler_matrix(1, 2, 3, 'syxz') | |
| >>> al, be, ga = euler_from_matrix(R0, 'syxz') | |
| >>> R1 = euler_matrix(al, be, ga, 'syxz') | |
| >>> numpy.allclose(R0, R1) | |
| True | |
| >>> angles = (4.0*math.pi) * (numpy.random.random(3) - 0.5) | |
| >>> for axes in _AXES2TUPLE.keys(): | |
| ... R0 = euler_matrix(axes=axes, *angles) | |
| ... R1 = euler_matrix(axes=axes, *euler_from_matrix(R0, axes)) | |
| ... if not numpy.allclose(R0, R1): print axes, "failed" | |
| """ | |
| try: | |
| firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()] | |
| except (AttributeError, KeyError): | |
| _ = _TUPLE2AXES[axes] | |
| firstaxis, parity, repetition, frame = axes | |
| i = firstaxis | |
| j = _NEXT_AXIS[i+parity] | |
| k = _NEXT_AXIS[i-parity+1] | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:3, :3] | |
| if repetition: | |
| sy = math.sqrt(M[i, j]*M[i, j] + M[i, k]*M[i, k]) | |
| if sy > _EPS: | |
| ax = math.atan2(M[i, j], M[i, k]) | |
| ay = math.atan2(sy, M[i, i]) | |
| az = math.atan2(M[j, i], -M[k, i]) | |
| else: | |
| ax = math.atan2(-M[j, k], M[j, j]) | |
| ay = math.atan2(sy, M[i, i]) | |
| az = 0.0 | |
| else: | |
| cy = math.sqrt(M[i, i]*M[i, i] + M[j, i]*M[j, i]) | |
| if cy > _EPS: | |
| ax = math.atan2(M[k, j], M[k, k]) | |
| ay = math.atan2(-M[k, i], cy) | |
| az = math.atan2(M[j, i], M[i, i]) | |
| else: | |
| ax = math.atan2(-M[j, k], M[j, j]) | |
| ay = math.atan2(-M[k, i], cy) | |
| az = 0.0 | |
| if parity: | |
| ax, ay, az = -ax, -ay, -az | |
| if frame: | |
| ax, az = az, ax | |
| return ax, ay, az | |
| def euler_from_quaternion(quaternion, axes='sxyz'): | |
| """Return Euler angles from quaternion for specified axis sequence. | |
| >>> angles = euler_from_quaternion([0.06146124, 0, 0, 0.99810947]) | |
| >>> numpy.allclose(angles, [0.123, 0, 0]) | |
| True | |
| """ | |
| return euler_from_matrix(quaternion_matrix(quaternion), axes) | |
| def quaternion_from_euler(ai, aj, ak, axes='sxyz'): | |
| """Return quaternion from Euler angles and axis sequence. | |
| ai, aj, ak : Euler's roll, pitch and yaw angles | |
| axes : One of 24 axis sequences as string or encoded tuple | |
| >>> q = quaternion_from_euler(1, 2, 3, 'ryxz') | |
| >>> numpy.allclose(q, [0.310622, -0.718287, 0.444435, 0.435953]) | |
| True | |
| """ | |
| try: | |
| firstaxis, parity, repetition, frame = _AXES2TUPLE[axes.lower()] | |
| except (AttributeError, KeyError): | |
| _ = _TUPLE2AXES[axes] | |
| firstaxis, parity, repetition, frame = axes | |
| i = firstaxis | |
| j = _NEXT_AXIS[i+parity] | |
| k = _NEXT_AXIS[i-parity+1] | |
| if frame: | |
| ai, ak = ak, ai | |
| if parity: | |
| aj = -aj | |
| ai /= 2.0 | |
| aj /= 2.0 | |
| ak /= 2.0 | |
| ci = math.cos(ai) | |
| si = math.sin(ai) | |
| cj = math.cos(aj) | |
| sj = math.sin(aj) | |
| ck = math.cos(ak) | |
| sk = math.sin(ak) | |
| cc = ci*ck | |
| cs = ci*sk | |
| sc = si*ck | |
| ss = si*sk | |
| quaternion = numpy.empty((4, ), dtype=numpy.float64) | |
| if repetition: | |
| quaternion[i] = cj*(cs + sc) | |
| quaternion[j] = sj*(cc + ss) | |
| quaternion[k] = sj*(cs - sc) | |
| quaternion[3] = cj*(cc - ss) | |
| else: | |
| quaternion[i] = cj*sc - sj*cs | |
| quaternion[j] = cj*ss + sj*cc | |
| quaternion[k] = cj*cs - sj*sc | |
| quaternion[3] = cj*cc + sj*ss | |
| if parity: | |
| quaternion[j] *= -1 | |
| return quaternion | |
| def quaternion_about_axis(angle, axis): | |
| """Return quaternion for rotation about axis. | |
| >>> q = quaternion_about_axis(0.123, (1, 0, 0)) | |
| >>> numpy.allclose(q, [0.06146124, 0, 0, 0.99810947]) | |
| True | |
| """ | |
| quaternion = numpy.zeros((4, ), dtype=numpy.float64) | |
| quaternion[:3] = axis[:3] | |
| qlen = vector_norm(quaternion) | |
| if qlen > _EPS: | |
| quaternion *= math.sin(angle/2.0) / qlen | |
| quaternion[3] = math.cos(angle/2.0) | |
| return quaternion | |
| def matrix_from_quaternion(quaternion): | |
| return quaternion_matrix(quaternion) | |
| def quaternion_matrix(quaternion): | |
| """Return homogeneous rotation matrix from quaternion. | |
| >>> R = quaternion_matrix([0.06146124, 0, 0, 0.99810947]) | |
| >>> numpy.allclose(R, rotation_matrix(0.123, (1, 0, 0))) | |
| True | |
| """ | |
| q = numpy.array(quaternion[:4], dtype=numpy.float64, copy=True) | |
| nq = numpy.dot(q, q) | |
| if nq < _EPS: | |
| return numpy.identity(4) | |
| q *= math.sqrt(2.0 / nq) | |
| q = numpy.outer(q, q) | |
| return numpy.array(( | |
| (1.0-q[1, 1]-q[2, 2], q[0, 1]-q[2, 3], q[0, 2]+q[1, 3], 0.0), | |
| (q[0, 1]+q[2, 3], 1.0-q[0, 0]-q[2, 2], q[1, 2]-q[0, 3], 0.0), | |
| (q[0, 2]-q[1, 3], q[1, 2]+q[0, 3], 1.0-q[0, 0]-q[1, 1], 0.0), | |
| (0.0, 0.0, 0.0, 1.0) | |
| ), dtype=numpy.float64) | |
| def quaternionJPL_matrix(quaternion): | |
| """Return homogeneous rotation matrix from quaternion in JPL notation. | |
| quaternion = [x y z w] | |
| """ | |
| q0 = quaternion[0] | |
| q1 = quaternion[1] | |
| q2 = quaternion[2] | |
| q3 = quaternion[3] | |
| return numpy.array([ | |
| [q0**2 - q1**2 - q2**2 + q3**2, 2.0*q0*q1 + | |
| 2.0*q2*q3, 2.0*q0*q2 - 2.0*q1*q3, 0], | |
| [2.0*q0*q1 - 2.0*q2*q3, - q0**2 + q1**2 - | |
| q2**2 + q3**2, 2.0*q0*q3 + 2.0*q1*q2, 0], | |
| [2.0*q0*q2 + 2.0*q1*q3, 2.0*q1*q2 - 2.0*q0 * | |
| q3, - q0**2 - q1**2 + q2**2 + q3**2, 0], | |
| [0, 0, 0, 1.0]], dtype=numpy.float64) | |
| def quaternion_from_matrix(matrix): | |
| """Return quaternion from rotation matrix. | |
| >>> R = rotation_matrix(0.123, (1, 2, 3)) | |
| >>> q = quaternion_from_matrix(R) | |
| >>> numpy.allclose(q, [0.0164262, 0.0328524, 0.0492786, 0.9981095]) | |
| True | |
| """ | |
| q = numpy.empty((4, ), dtype=numpy.float64) | |
| M = numpy.array(matrix, dtype=numpy.float64, copy=False)[:4, :4] | |
| t = numpy.trace(M) | |
| if t > M[3, 3]: | |
| q[3] = t | |
| q[2] = M[1, 0] - M[0, 1] | |
| q[1] = M[0, 2] - M[2, 0] | |
| q[0] = M[2, 1] - M[1, 2] | |
| else: | |
| i, j, k = 0, 1, 2 | |
| if M[1, 1] > M[0, 0]: | |
| i, j, k = 1, 2, 0 | |
| if M[2, 2] > M[i, i]: | |
| i, j, k = 2, 0, 1 | |
| t = M[i, i] - (M[j, j] + M[k, k]) + M[3, 3] | |
| q[i] = t | |
| q[j] = M[i, j] + M[j, i] | |
| q[k] = M[k, i] + M[i, k] | |
| q[3] = M[k, j] - M[j, k] | |
| q *= 0.5 / math.sqrt(t * M[3, 3]) | |
| return q | |
| def quaternion_multiply(quaternion1, quaternion0): | |
| """Return multiplication of two quaternions. | |
| >>> q = quaternion_multiply([1, -2, 3, 4], [-5, 6, 7, 8]) | |
| >>> numpy.allclose(q, [-44, -14, 48, 28]) | |
| True | |
| """ | |
| x0, y0, z0, w0 = quaternion0 | |
| x1, y1, z1, w1 = quaternion1 | |
| return numpy.array(( | |
| x1*w0 + y1*z0 - z1*y0 + w1*x0, | |
| -x1*z0 + y1*w0 + z1*x0 + w1*y0, | |
| x1*y0 - y1*x0 + z1*w0 + w1*z0, | |
| -x1*x0 - y1*y0 - z1*z0 + w1*w0), dtype=numpy.float64) | |
| def quaternion_conjugate(quaternion): | |
| """Return conjugate of quaternion. | |
| >>> q0 = random_quaternion() | |
| >>> q1 = quaternion_conjugate(q0) | |
| >>> q1[3] == q0[3] and all(q1[:3] == -q0[:3]) | |
| True | |
| """ | |
| return numpy.array((-quaternion[0], -quaternion[1], | |
| -quaternion[2], quaternion[3]), dtype=numpy.float64) | |
| def quaternion_inverse(quaternion): | |
| """Return inverse of quaternion. | |
| >>> q0 = random_quaternion() | |
| >>> q1 = quaternion_inverse(q0) | |
| >>> numpy.allclose(quaternion_multiply(q0, q1), [0, 0, 0, 1]) | |
| True | |
| """ | |
| return quaternion_conjugate(quaternion) / numpy.dot(quaternion, quaternion) | |
| def quaternion_slerp(quat0, quat1, fraction, spin=0, shortestpath=True): | |
| """Return spherical linear interpolation between two quaternions. | |
| >>> q0 = random_quaternion() | |
| >>> q1 = random_quaternion() | |
| >>> q = quaternion_slerp(q0, q1, 0.0) | |
| >>> numpy.allclose(q, q0) | |
| True | |
| >>> q = quaternion_slerp(q0, q1, 1.0, 1) | |
| >>> numpy.allclose(q, q1) | |
| True | |
| >>> q = quaternion_slerp(q0, q1, 0.5) | |
| >>> angle = math.acos(numpy.dot(q0, q)) | |
| >>> numpy.allclose(2.0, math.acos(numpy.dot(q0, q1)) / angle) or \ | |
| numpy.allclose(2.0, math.acos(-numpy.dot(q0, q1)) / angle) | |
| True | |
| """ | |
| q0 = unit_vector(quat0[:4]) | |
| q1 = unit_vector(quat1[:4]) | |
| if fraction == 0.0: | |
| return q0 | |
| elif fraction == 1.0: | |
| return q1 | |
| d = numpy.dot(q0, q1) | |
| if abs(abs(d) - 1.0) < _EPS: | |
| return q0 | |
| if shortestpath and d < 0.0: | |
| # invert rotation | |
| d = -d | |
| q1 *= -1.0 | |
| angle = math.acos(d) + spin * math.pi | |
| if abs(angle) < _EPS: | |
| return q0 | |
| isin = 1.0 / math.sin(angle) | |
| q0 *= math.sin((1.0 - fraction) * angle) * isin | |
| q1 *= math.sin(fraction * angle) * isin | |
| q0 += q1 | |
| return q0 | |
| def random_quaternion(rand=None): | |
| """Return uniform random unit quaternion. | |
| rand: array like or None | |
| Three independent random variables that are uniformly distributed | |
| between 0 and 1. | |
| >>> q = random_quaternion() | |
| >>> numpy.allclose(1.0, vector_norm(q)) | |
| True | |
| >>> q = random_quaternion(numpy.random.random(3)) | |
| >>> q.shape | |
| (4,) | |
| """ | |
| if rand is None: | |
| rand = numpy.random.rand(3) | |
| else: | |
| assert len(rand) == 3 | |
| r1 = numpy.sqrt(1.0 - rand[0]) | |
| r2 = numpy.sqrt(rand[0]) | |
| pi2 = math.pi * 2.0 | |
| t1 = pi2 * rand[1] | |
| t2 = pi2 * rand[2] | |
| return numpy.array((numpy.sin(t1)*r1, | |
| numpy.cos(t1)*r1, | |
| numpy.sin(t2)*r2, | |
| numpy.cos(t2)*r2), dtype=numpy.float64) | |
| def random_rotation_matrix(rand=None): | |
| """Return uniform random rotation matrix. | |
| rnd: array like | |
| Three independent random variables that are uniformly distributed | |
| between 0 and 1 for each returned quaternion. | |
| >>> R = random_rotation_matrix() | |
| >>> numpy.allclose(numpy.dot(R.T, R), numpy.identity(4)) | |
| True | |
| """ | |
| return quaternion_matrix(random_quaternion(rand)) | |
| def random_direction_3d(): | |
| """ equal-area projection according to: | |
| https://math.stackexchange.com/questions/44689/how-to-find-a-random-axis-or-unit-vector-in-3d | |
| cfo, 2015/10/16 | |
| """ | |
| z = numpy.random.rand() * 2.0 - 1.0 | |
| t = numpy.random.rand() * 2.0 * numpy.pi | |
| r = numpy.sqrt(1.0 - z*z) | |
| x = r * numpy.cos(t) | |
| y = r * numpy.sin(t) | |
| return numpy.array([x, y, z], dtype=numpy.float64) | |
| class Arcball(object): | |
| """Virtual Trackball Control. | |
| >>> ball = Arcball() | |
| >>> ball = Arcball(initial=numpy.identity(4)) | |
| >>> ball.place([320, 320], 320) | |
| >>> ball.down([500, 250]) | |
| >>> ball.drag([475, 275]) | |
| >>> R = ball.matrix() | |
| >>> numpy.allclose(numpy.sum(R), 3.90583455) | |
| True | |
| >>> ball = Arcball(initial=[0, 0, 0, 1]) | |
| >>> ball.place([320, 320], 320) | |
| >>> ball.setaxes([1,1,0], [-1, 1, 0]) | |
| >>> ball.setconstrain(True) | |
| >>> ball.down([400, 200]) | |
| >>> ball.drag([200, 400]) | |
| >>> R = ball.matrix() | |
| >>> numpy.allclose(numpy.sum(R), 0.2055924) | |
| True | |
| >>> ball.next() | |
| """ | |
| def __init__(self, initial=None): | |
| """Initialize virtual trackball control. | |
| initial : quaternion or rotation matrix | |
| """ | |
| self._axis = None | |
| self._axes = None | |
| self._radius = 1.0 | |
| self._center = [0.0, 0.0] | |
| self._vdown = numpy.array([0, 0, 1], dtype=numpy.float64) | |
| self._constrain = False | |
| if initial is None: | |
| self._qdown = numpy.array([0, 0, 0, 1], dtype=numpy.float64) | |
| else: | |
| initial = numpy.array(initial, dtype=numpy.float64) | |
| if initial.shape == (4, 4): | |
| self._qdown = quaternion_from_matrix(initial) | |
| elif initial.shape == (4, ): | |
| initial /= vector_norm(initial) | |
| self._qdown = initial | |
| else: | |
| raise ValueError("initial not a quaternion or matrix.") | |
| self._qnow = self._qpre = self._qdown | |
| def place(self, center, radius): | |
| """Place Arcball, e.g. when window size changes. | |
| center : sequence[2] | |
| Window coordinates of trackball center. | |
| radius : float | |
| Radius of trackball in window coordinates. | |
| """ | |
| self._radius = float(radius) | |
| self._center[0] = center[0] | |
| self._center[1] = center[1] | |
| def setaxes(self, *axes): | |
| """Set axes to constrain rotations.""" | |
| if axes is None: | |
| self._axes = None | |
| else: | |
| self._axes = [unit_vector(axis) for axis in axes] | |
| def setconstrain(self, constrain): | |
| """Set state of constrain to axis mode.""" | |
| self._constrain = constrain == True | |
| def getconstrain(self): | |
| """Return state of constrain to axis mode.""" | |
| return self._constrain | |
| def down(self, point): | |
| """Set initial cursor window coordinates and pick constrain-axis.""" | |
| self._vdown = arcball_map_to_sphere(point, self._center, self._radius) | |
| self._qdown = self._qpre = self._qnow | |
| if self._constrain and self._axes is not None: | |
| self._axis = arcball_nearest_axis(self._vdown, self._axes) | |
| self._vdown = arcball_constrain_to_axis(self._vdown, self._axis) | |
| else: | |
| self._axis = None | |
| def drag(self, point): | |
| """Update current cursor window coordinates.""" | |
| vnow = arcball_map_to_sphere(point, self._center, self._radius) | |
| if self._axis is not None: | |
| vnow = arcball_constrain_to_axis(vnow, self._axis) | |
| self._qpre = self._qnow | |
| t = numpy.cross(self._vdown, vnow) | |
| if numpy.dot(t, t) < _EPS: | |
| self._qnow = self._qdown | |
| else: | |
| q = [t[0], t[1], t[2], numpy.dot(self._vdown, vnow)] | |
| self._qnow = quaternion_multiply(q, self._qdown) | |
| def next(self, acceleration=0.0): | |
| """Continue rotation in direction of last drag.""" | |
| q = quaternion_slerp(self._qpre, self._qnow, 2.0+acceleration, False) | |
| self._qpre, self._qnow = self._qnow, q | |
| def matrix(self): | |
| """Return homogeneous rotation matrix.""" | |
| return quaternion_matrix(self._qnow) | |
| def arcball_map_to_sphere(point, center, radius): | |
| """Return unit sphere coordinates from window coordinates.""" | |
| v = numpy.array(((point[0] - center[0]) / radius, | |
| (center[1] - point[1]) / radius, | |
| 0.0), dtype=numpy.float64) | |
| n = v[0]*v[0] + v[1]*v[1] | |
| if n > 1.0: | |
| v /= math.sqrt(n) # position outside of sphere | |
| else: | |
| v[2] = math.sqrt(1.0 - n) | |
| return v | |
| def arcball_constrain_to_axis(point, axis): | |
| """Return sphere point perpendicular to axis.""" | |
| v = numpy.array(point, dtype=numpy.float64, copy=True) | |
| a = numpy.array(axis, dtype=numpy.float64, copy=True) | |
| v -= a * numpy.dot(a, v) # on plane | |
| n = vector_norm(v) | |
| if n > _EPS: | |
| if v[2] < 0.0: | |
| v *= -1.0 | |
| v /= n | |
| return v | |
| if a[2] == 1.0: | |
| return numpy.array([1, 0, 0], dtype=numpy.float64) | |
| return unit_vector([-a[1], a[0], 0]) | |
| def arcball_nearest_axis(point, axes): | |
| """Return axis, which arc is nearest to point.""" | |
| point = numpy.array(point, dtype=numpy.float64, copy=False) | |
| nearest = None | |
| mx = -1.0 | |
| for axis in axes: | |
| t = numpy.dot(arcball_constrain_to_axis(point, axis), point) | |
| if t > mx: | |
| nearest = axis | |
| mx = t | |
| return nearest | |
| # epsilon for testing whether a number is close to zero | |
| _EPS = numpy.finfo(float).eps * 4.0 | |
| # axis sequences for Euler angles | |
| _NEXT_AXIS = [1, 2, 0, 1] | |
| # map axes strings to/from tuples of inner axis, parity, repetition, frame | |
| _AXES2TUPLE = { | |
| 'sxyz': (0, 0, 0, 0), 'sxyx': (0, 0, 1, 0), 'sxzy': (0, 1, 0, 0), | |
| 'sxzx': (0, 1, 1, 0), 'syzx': (1, 0, 0, 0), 'syzy': (1, 0, 1, 0), | |
| 'syxz': (1, 1, 0, 0), 'syxy': (1, 1, 1, 0), 'szxy': (2, 0, 0, 0), | |
| 'szxz': (2, 0, 1, 0), 'szyx': (2, 1, 0, 0), 'szyz': (2, 1, 1, 0), | |
| 'rzyx': (0, 0, 0, 1), 'rxyx': (0, 0, 1, 1), 'ryzx': (0, 1, 0, 1), | |
| 'rxzx': (0, 1, 1, 1), 'rxzy': (1, 0, 0, 1), 'ryzy': (1, 0, 1, 1), | |
| 'rzxy': (1, 1, 0, 1), 'ryxy': (1, 1, 1, 1), 'ryxz': (2, 0, 0, 1), | |
| 'rzxz': (2, 0, 1, 1), 'rxyz': (2, 1, 0, 1), 'rzyz': (2, 1, 1, 1)} | |
| _TUPLE2AXES = dict((v, k) for k, v in _AXES2TUPLE.items()) | |
| # helper functions | |
| def vector_norm(data, axis=None, out=None): | |
| """Return length, i.e. eucledian norm, of ndarray along axis. | |
| >>> v = numpy.random.random(3) | |
| >>> n = vector_norm(v) | |
| >>> numpy.allclose(n, numpy.linalg.norm(v)) | |
| True | |
| >>> v = numpy.random.rand(6, 5, 3) | |
| >>> n = vector_norm(v, axis=-1) | |
| >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=2))) | |
| True | |
| >>> n = vector_norm(v, axis=1) | |
| >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1))) | |
| True | |
| >>> v = numpy.random.rand(5, 4, 3) | |
| >>> n = numpy.empty((5, 3), dtype=numpy.float64) | |
| >>> vector_norm(v, axis=1, out=n) | |
| >>> numpy.allclose(n, numpy.sqrt(numpy.sum(v*v, axis=1))) | |
| True | |
| >>> vector_norm([]) | |
| 0.0 | |
| >>> vector_norm([1.0]) | |
| 1.0 | |
| """ | |
| data = numpy.array(data, dtype=numpy.float64, copy=True) | |
| if out is None: | |
| if data.ndim == 1: | |
| return math.sqrt(numpy.dot(data, data)) | |
| data *= data | |
| out = numpy.atleast_1d(numpy.sum(data, axis=axis)) | |
| numpy.sqrt(out, out) | |
| return out | |
| else: | |
| data *= data | |
| numpy.sum(data, axis=axis, out=out) | |
| numpy.sqrt(out, out) | |
| def unit_vector(data, axis=None, out=None): | |
| """Return ndarray normalized by length, i.e. eucledian norm, along axis. | |
| >>> v0 = numpy.random.random(3) | |
| >>> v1 = unit_vector(v0) | |
| >>> numpy.allclose(v1, v0 / numpy.linalg.norm(v0)) | |
| True | |
| >>> v0 = numpy.random.rand(5, 4, 3) | |
| >>> v1 = unit_vector(v0, axis=-1) | |
| >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=2)), 2) | |
| >>> numpy.allclose(v1, v2) | |
| True | |
| >>> v1 = unit_vector(v0, axis=1) | |
| >>> v2 = v0 / numpy.expand_dims(numpy.sqrt(numpy.sum(v0*v0, axis=1)), 1) | |
| >>> numpy.allclose(v1, v2) | |
| True | |
| >>> v1 = numpy.empty((5, 4, 3), dtype=numpy.float64) | |
| >>> unit_vector(v0, axis=1, out=v1) | |
| >>> numpy.allclose(v1, v2) | |
| True | |
| >>> list(unit_vector([])) | |
| [] | |
| >>> list(unit_vector([1.0])) | |
| [1.0] | |
| """ | |
| if out is None: | |
| data = numpy.array(data, dtype=numpy.float64, copy=True) | |
| if data.ndim == 1: | |
| data /= math.sqrt(numpy.dot(data, data)) | |
| return data | |
| else: | |
| if out is not data: | |
| out[:] = numpy.array(data, copy=False) | |
| data = out | |
| length = numpy.atleast_1d(numpy.sum(data*data, axis)) | |
| numpy.sqrt(length, length) | |
| if axis is not None: | |
| length = numpy.expand_dims(length, axis) | |
| data /= length | |
| if out is None: | |
| return data | |
| def random_vector(size): | |
| """Return array of random doubles in the half-open interval [0.0, 1.0). | |
| >>> v = random_vector(10000) | |
| >>> numpy.all(v >= 0.0) and numpy.all(v < 1.0) | |
| True | |
| >>> v0 = random_vector(10) | |
| >>> v1 = random_vector(10) | |
| >>> numpy.any(v0 == v1) | |
| False | |
| """ | |
| return numpy.random.random(size) | |
| def inverse_matrix(matrix): | |
| """Return inverse of square transformation matrix. | |
| >>> M0 = random_rotation_matrix() | |
| >>> M1 = inverse_matrix(M0.T) | |
| >>> numpy.allclose(M1, numpy.linalg.inv(M0.T)) | |
| True | |
| >>> for size in range(1, 7): | |
| ... M0 = numpy.random.rand(size, size) | |
| ... M1 = inverse_matrix(M0) | |
| ... if not numpy.allclose(M1, numpy.linalg.inv(M0)): print size | |
| """ | |
| return numpy.linalg.inv(matrix) | |
| def concatenate_matrices(*matrices): | |
| """Return concatenation of series of transformation matrices. | |
| >>> M = numpy.random.rand(16).reshape((4, 4)) - 0.5 | |
| >>> numpy.allclose(M, concatenate_matrices(M)) | |
| True | |
| >>> numpy.allclose(numpy.dot(M, M.T), concatenate_matrices(M, M.T)) | |
| True | |
| """ | |
| M = numpy.identity(4) | |
| for i in matrices: | |
| M = numpy.dot(M, i) | |
| return M | |
| def is_same_transform(matrix0, matrix1): | |
| """Return True if two matrices perform same transformation. | |
| >>> is_same_transform(numpy.identity(4), numpy.identity(4)) | |
| True | |
| >>> is_same_transform(numpy.identity(4), random_rotation_matrix()) | |
| False | |
| """ | |
| matrix0 = numpy.array(matrix0, dtype=numpy.float64, copy=True) | |
| matrix0 /= matrix0[3, 3] | |
| matrix1 = numpy.array(matrix1, dtype=numpy.float64, copy=True) | |
| matrix1 /= matrix1[3, 3] | |
| return numpy.allclose(matrix0, matrix1) | |
| def _import_module(module_name, warn=True, prefix='_py_', ignore='_'): | |
| """Try import all public attributes from module into global namespace. | |
| Existing attributes with name clashes are renamed with prefix. | |
| Attributes starting with underscore are ignored by default. | |
| Return True on successful import. | |
| """ | |
| try: | |
| module = __import__(module_name) | |
| except ImportError: | |
| if warn: | |
| warnings.warn("Failed to import module " + module_name) | |
| else: | |
| for attr in dir(module): | |
| if ignore and attr.startswith(ignore): | |
| continue | |
| if prefix: | |
| if attr in globals(): | |
| globals()[prefix + attr] = globals()[attr] | |
| elif warn: | |
| warnings.warn("No Python implementation of " + attr) | |
| globals()[attr] = getattr(module, attr) | |
| return True | |